├── AUTHORS ├── ChangeLog ├── LICENSE.md ├── README.rst ├── docs ├── .ipynb_checkpoints │ └── haproxyadmin-checkpoint.ipynb ├── Makefile ├── haproxyadmin.ipynb ├── make.bat └── source │ ├── TODO.rst │ ├── changelog.rst │ ├── conf.py │ ├── dev │ └── api.rst │ ├── index.rst │ ├── introduction.rst │ └── user │ ├── backend.rst │ ├── frontend.rst │ ├── guide.rst │ ├── haproxy.rst │ └── server.rst ├── haproxyadmin ├── __init__.py ├── backend.py ├── command_status.py ├── exceptions.py ├── frontend.py ├── haproxy.py ├── internal │ ├── __init__.py │ ├── backend.py │ ├── frontend.py │ ├── haproxy.py │ └── server.py ├── server.py └── utils.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tools ├── generate_constants_for_metrics.py ├── get_errors_messages.sh └── haproxy.cfg /AUTHORS: -------------------------------------------------------------------------------- 1 | Christian Rovner 2 | Michael Balser 3 | Ori Shoshan 4 | Pavlos Parissis 5 | Pavlos Parissis 6 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 0.2.4 5 | ----- 6 | 7 | * RELEASE 0.2.4 version 8 | * Allow connected\_socket() to use custom timeout 9 | 10 | 0.2.3 11 | ----- 12 | 13 | * RELEASE 0.2.3 version 14 | * connected\_socket(): fix validation check for HAPEE >= 2.1 15 | * Remove debugging code 16 | * Add support for setting the timeout 17 | * DOC: Update TODO 18 | * Split internal classes to individual modules 19 | * Try to smarter when we return address/port 20 | * Remove useless object inheritance 21 | * setup.cfg: We don't need psutil anymore 22 | * DOC: Update docstrings 23 | 24 | 0.2.2 25 | ----- 26 | 27 | * RELEASE 0.2.2 version 28 | * Revert "Add 'slim' metric for servers" 29 | * DOC: One more try to get this right 30 | * DOC: Update docstring for address method 31 | * Add support for changing address and port of a server 32 | * DOC: Fix various documentation issues 33 | * Add 'slim' metric for servers 34 | * Don't check if ACL/MAP is a file 35 | * Return empty list if a acl doesn't have any entries 36 | * DOC: Change python version we use for development 37 | * Ignore Python 3 class hierarchy of OSError errors 38 | * Return empty list if a map doesn't have any entries 39 | * Fix example code for show\_map function 40 | * Fix incorrect module path for constants in docstring 41 | * Add support for slim metric to Server object 42 | * List server metric names in alphabetic order 43 | * Added setaddress and address to Servers 44 | * Fix docstrings 45 | * Remove unused variables 46 | * Ignore Python 3 class hierarchy of OSError errors 47 | * Update installation instructions 48 | 49 | 0.2.1 50 | ----- 51 | 52 | * RELEASE 0.2.1 version 53 | * Reorder inclusion of modules 54 | * Add docstring for isint() 55 | * Simplify conditional statement 56 | * Fix typos in a docstring 57 | * Reorder inclusion of modules and remove unused exceptions 58 | * Return False when a file isn't a valid stats socket 59 | * Update copyright 60 | * Pass keyword parameters in format method, fix #1 61 | 62 | 0.2.0 63 | ----- 64 | 65 | * RELEASE 0.2.0 version 66 | * Refactor constants for metrics 67 | * Include a module docstring 68 | 69 | 0.1.12 70 | ------ 71 | 72 | * RELEASE 0.1.12 version 73 | * Return zero rather None for metrics without value 74 | 75 | 0.1.11 76 | ------ 77 | 78 | * RELEASE 0.1.11 version 79 | * Make sure we clear out possible previous errors 80 | * Remove unnecessary keyword argument 81 | 82 | 0.1.10 83 | ------ 84 | 85 | * RELEASE 0.1.10 version 86 | * Implement a proper retry logic for socket failures 87 | 88 | 0.1.9 89 | ----- 90 | 91 | * RELEASE 0.1.9 version 92 | * Improve the way we internally use values for metrics 93 | 94 | 0.1.8 95 | ----- 96 | 97 | * RELEASE 0.1.8 version 98 | * Remove unnecessary filtering of empty values 99 | * Fix broken design in converter function 100 | * fix type in README 101 | * cosmetic fix in doc string 102 | * extend the support of error strings returned by haproxy 103 | * add items in the TODO list 104 | * mention from which socket file we don't get any data 105 | 106 | 0.1.7 107 | ----- 108 | 109 | * RELEASE 0.1.7 version 110 | * 9fbb459 didn't fix regression from dcc5173e31deac 111 | * better handling of error when we connect to socket 112 | * fix a regression introduced with dcc5173e31deac 113 | 114 | 0.1.6 115 | ----- 116 | 117 | * RELEASE 0.1.6 version 118 | * update TODO 119 | * fix a regression introduced with dcc5173e31deac 120 | * add support for sending commands to haproxy 121 | * simplify the way we send commands to socket 122 | * add support for keyword arguments in cmd\_across\_all\_procs() 123 | * fix (once again) format issues in TODO.rst 124 | * fix format issues in TODO.rst 125 | * add some ordering in our TODO items 126 | 127 | 0.1.5 128 | ----- 129 | 130 | * RELEASE 0.1.5 version 131 | * dummy commit to force new release as previous one got issues with git tags 132 | 133 | 0.1.4 134 | ----- 135 | 136 | * RELEASE 0.1.4 version 137 | * improve the way we detect proxy id changes 138 | * fixes on comments 139 | * update docstrings 140 | * utils.py: calculate use the length of the correct list(filtered) 141 | * exceptions.py: update docstrings 142 | * README: more reStructured friendly format 143 | * README: update release instructions 144 | * more reStructuredText for exceptions.py 145 | 146 | 0.1.3 147 | ----- 148 | 149 | * RELEASE 0.1.3 version 150 | * catch ConnectionRefusedError when we send a command to the socket 151 | * include socket file in the message when HAProxySocketError is raised 152 | * restructure exceptions 153 | * Update TODO 154 | * safe one call for retrieving process creation time 155 | * updates on TODO 156 | * add a note in documentation about request property when frontend is in TCP mode 157 | 158 | 0.1.2 159 | ----- 160 | 161 | * RELEASE 0.1.2 version 162 | * internal.py: OSError exception doesn't have message attribute 163 | * remove unnecessary declaration 164 | * don't use relative imports as our module layout is quit flat and very short 165 | * \_\_init\_\_.py:add version and remove ascii art 166 | * import all exceptions in the doc rather import each one individually 167 | * exceptions.py: use correct exception names 168 | * add SocketTimeout exception and raise it when we got timeout after X retries 169 | * README:fix typo 170 | * internal.py: catch timeout exception when reading data from the socket 171 | 172 | 0.1.1 173 | ----- 174 | 175 | * RELEASE 0.1.1 version 176 | * remove debugging statements 177 | * close the socket when we test if we can connect to it 178 | * fix 2 major bugs in the way we handle the socket 179 | * include SocketTransportError in the documentation 180 | * internal.py: catch transport error on socket 181 | * add exception to catch transport errors on the socket 182 | 183 | 0.1.0 184 | ----- 185 | 186 | * RELEASE 0.1.0 version 187 | * raise CommandFailed rather ValueError in show\_acl 188 | * show\_acl: rename acl argument to aclid to be consistent with show\_map 189 | * update TODO 190 | * update docstring for acl commands 191 | 192 | 0.0.7 193 | ----- 194 | 195 | * RELEASE 0.0.7 version 196 | * update docstring for map commands 197 | * haproxy: raise CommandFailed when output indicates something bad happened 198 | * remove empty string when more than 1 line is returned by HAProxy 199 | 200 | 0.0.6 201 | ----- 202 | 203 | * RELEASE 0.0.6 version 204 | * internal.py: remove empty string from data returned from socket 205 | * update TODO 206 | * fix typo 207 | * tiny reformatting on exceptions 208 | * haproxy.py: explicitly check for the existence of socket directory 209 | * Update TODO 210 | * extend ERROR\_OUTPUT\_STRINGS to support address field 211 | * include Socket family exceptions in the documentation 212 | * updates on ChangeLog 213 | 214 | 0.0.5 215 | ----- 216 | 217 | * RELEASE 0.0.5 version 218 | * haproxy.py: reformating 219 | * utils.py: raise an appropriate exception when we check for valid socket files 220 | * add a bunch of exceptions for catching errors when we test socket file 221 | * connected\_socket() perform a sanity on the date returned 222 | 223 | 0.0.4 224 | ----- 225 | 226 | * RELEASE 0.0.4 version 227 | * update TODO 228 | * haproxy.py: fix a bug in add map where we forgot to set value 229 | * haproxy.py: ignore socket files not bound to a process 230 | * utils.py: add connected\_socket to check if a socket is bound to a process 231 | * include six and not docopt in requirements.txt 232 | * add requirements file for pip installations 233 | * bump version on docs as well 234 | * use stot metric name for fetching requests for backends/servers 235 | * Update TODO.rst 236 | * remove tune.rst as we don't need it anymore 237 | 238 | 0.0.3 239 | ----- 240 | 241 | * RELEASE 0.0.3 version 242 | * DOC: another set of updates 243 | * rename get\_frontends to frontends 244 | * Performance improvements due to the way we interact with stats socket 245 | * update haproxy.cfg, give a unique name for each listen directive 246 | * Update TODO.rst 247 | * TODO: add and remove items 248 | * update docstrings in few classes and functions 249 | * DOC: add examples for server in User Guide 250 | * DOC: add a reference to Frontend class in User Guide 251 | * DOC: add examples for backends in User Guide 252 | * haproxy.py: use long variable names in order to be consistent with rest of code 253 | * DOC: add remaining examples for frontends in User Guide 254 | * README: add missing variable 255 | * DOC: add examples for backends in User Guide 256 | * backend.py: remove status from BACKEND\_METRICS 257 | * DOC: add a bunch of examples for frontends in User Guide 258 | * DOC: add missing example code 259 | * DOC: add more examples for HAProxy operations in the User Guide 260 | * DOC: add examples in HAProxy section of User Guide for backends/servers 261 | * DOC: create a reference to HAProxy class 262 | * DOC: add a bunch of examples in HAProxy section of User Guide for Frontends 263 | * DOC: name the 1st section properly 264 | * DOC: Another restructure for User Guide 265 | * DOC: restructure the section leves for User Guide 266 | * DOC: add User Guide sections and few examples for HAProxy 267 | * TODO: remove items which are completed 268 | * move TODO subsection out of README and make it a section in the documentation 269 | * bump release in the docs 270 | * README: remove changelog section as we have it in the documentation 271 | * docs: Add Changes section 272 | 273 | 0.0.2 274 | ----- 275 | 276 | * RELEASE 0.0.2 version 277 | * README: merged TODO into README 278 | * README: documention reference doesn't need to be a section 279 | * internal.py: wrong refactoring for \_Backend class 280 | * refactor Pool to backend 281 | * refactor PoolMember to Server 282 | * major updates on docstrings to allow sphinx integration 283 | * add sphinx doc build 284 | * utils.py: update docstrings 285 | * utils.py: converter didn't actually truncate towards zero for floating numbers 286 | * utils.py update docstrings 287 | * TODO: work in progress for updating docstrings 288 | * internal.py: update docstrings 289 | * internal.py: change parameter name to name for get\_frontends 290 | * merged NOTES into TODO 291 | * NOTES: tiny fix 292 | * add some notes 293 | * NOTES: use reStructuredText Markup and update it accordingly 294 | * utils.py round the results of calculations as we don't use floating numbers 295 | * utils.py: convert number/string only to integer 296 | * haproxy.py: fix typo 297 | * We don't need it anymore and it was a bad idea 298 | * add haproxy.cfg which we use 299 | * utils.py: we don't perform any calculation for Uptime\_sec field 300 | * haproxy.py: docstring fix 301 | * haproxy.py: add a bunch of properties for HAProxy process 302 | * utils.py don't remove trailing whitespace when parse 'show info' output 303 | * haproxy.py: perform calculation in metric() if the caller wants it 304 | * internal.py remove unused function run\_commandold 305 | * change license to Apache 2.0 306 | * README.rst: add acknowledgement section 307 | * switch to README.rst by removing README.md 308 | * add more text in README.rst 309 | 310 | 0.0.1 311 | ----- 312 | 313 | * Initial commit of the library in functional state 314 | * Initial commit 315 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. haproxyadmin 2 | .. README.rst 3 | 4 | ============ 5 | haproxyadmin 6 | ============ 7 | 8 | *A Python library to manage HAProxy via stats socket.* 9 | 10 | .. contents:: 11 | 12 | 13 | Introduction 14 | ------------ 15 | 16 | **haproxyadmin** is a Python library for interacting with `HAProxy`_ 17 | load balancer to perform operations such as enabling/disabling servers. 18 | It does that by issuing the appropriate commands over the `stats socket`_ 19 | provided by HAProxy. It also uses that stats socket for retrieving 20 | statistics and changing settings. 21 | 22 | HAProxy is a multi-process daemon and each process can only be accessed by a 23 | distinct stats socket. There isn't any shared memory for all these processes. 24 | That means that if a frontend or backend is managed by more than one processes, 25 | you have to find which stats socket you need to send the query/command. 26 | This makes the life of a sysadmin a bit difficult as he has to keep track of 27 | which stats socket to use for a given object(frontend/backend/server). 28 | 29 | **haproxyadmin** resolves this problem by presenting objects as single entities 30 | even when they are managed by multiple processes. It also supports aggregation 31 | for various statistics provided by HAProxy. For instance, to report the 32 | requests processed by a frontend it queries all processes which manage that 33 | frontend and return the sum. 34 | 35 | The library works with Python 2.7 and Python 3.6, but for development and 36 | testing Python 3.6 is used. The `Six Python 2 and 3 Compatibility Library`_ 37 | is being used to provide the necessary wrapping over the differences between 38 | these 2 major versions of Python. 39 | 40 | 41 | .. code-block:: python 42 | 43 | 44 | >>> from haproxyadmin import haproxy 45 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 46 | >>> frontends = hap.frontends() 47 | >>> for frontend in frontends: 48 | ... print(frontend.name, frontend.requests, frontend.process_nb) 49 | ... 50 | frontend_proc2 0 [2] 51 | haproxy 0 [4, 3, 2, 1] 52 | frontend_proc1 0 [1] 53 | frontend1_proc34 0 [4, 3] 54 | frontend2_proc34 0 [4, 3] 55 | >>> 56 | >>> 57 | >>> backends = hap.backends() 58 | >>> for backend in backends: 59 | ... print(backend.name, backend.requests, backend.process_nb) 60 | ... servers = backend.servers() 61 | ... for server in servers: 62 | ... print(" ", server.name, server.requests) 63 | ... 64 | backend_proc2 100 [2] 65 | bck_proc2_srv4_proc2 25 66 | bck_proc2_srv3_proc2 25 67 | bck_proc2_srv1_proc2 25 68 | bck_proc2_srv2_proc2 25 69 | haproxy 0 [4, 3, 2, 1] 70 | backend1_proc34 16 [4, 3] 71 | bck1_proc34_srv1 6 72 | bck_all_srv1 5 73 | bck1_proc34_srv2 5 74 | backend_proc1 29 [1] 75 | member2_proc1 14 76 | member1_proc1 15 77 | bck_all_srv1 0 78 | backend2_proc34 100 [4, 3] 79 | bck2_proc34_srv2 97 80 | bck2_proc34_srv1 2 81 | bck_all_srv1 1 82 | >>> 83 | 84 | 85 | The documentation of the library is available at http://haproxyadmin.readthedocs.org 86 | 87 | 88 | Features 89 | -------- 90 | 91 | - HAProxy in multi-process mode (nbproc >1) 92 | - UNIX stats socket, no support for querying HTTP statistics page 93 | - Frontend operations 94 | - Backend operations 95 | - Server operations 96 | - ACL operations 97 | - MAP operations 98 | - Aggregation on various statistics 99 | - Change global options for HAProxy 100 | 101 | 102 | Installation 103 | ------------ 104 | 105 | Use pip:: 106 | 107 | pip install haproxyadmin 108 | 109 | From Source:: 110 | 111 | sudo python setup.py install 112 | 113 | Build (source) RPMs:: 114 | 115 | python setup.py clean --all; python setup.py bdist_rpm 116 | 117 | Build a source archive for manual installation:: 118 | 119 | python setup.py sdist 120 | 121 | Release 122 | ------- 123 | 124 | #. Bump versions in docs/source/conf.py and haproxyadmin/__init__.py 125 | 126 | #. Commit above change with:: 127 | 128 | git commit -av -m'RELEASE 0.1.3 version' 129 | 130 | #. Create a signed tag, pbr will use this for the version number:: 131 | 132 | git tag -s 0.1.3 -m 'bump release' 133 | 134 | #. Create the source distribution archive (the archive will be placed in the **dist** directory):: 135 | 136 | python setup.py sdist 137 | 138 | #. pbr will update ChangeLog file and we want to squeeze them to the previous commit thus we run:: 139 | 140 | git commit -av --amend 141 | 142 | #. Move current tag to the last commit:: 143 | 144 | git tag -fs 0.1.3 -m 'bump release' 145 | 146 | #. Push changes:: 147 | 148 | git push;git push --tags 149 | 150 | 151 | Development 152 | ----------- 153 | I would love to hear what other people think about **haproxyadmin** and provide 154 | feedback. Please post your comments, bug reports, wishes on my `issues page 155 | `_. 156 | 157 | Licensing 158 | --------- 159 | 160 | Apache 2.0 161 | 162 | 163 | Acknowledgement 164 | --------------- 165 | This program was originally developed for Booking.com. With approval 166 | from Booking.com, the code was generalised and published as Open Source 167 | on github, for which the author would like to express his gratitude. 168 | 169 | Contacts 170 | -------- 171 | 172 | **Project website**: https://github.com/unixsurfer/haproxyadmin 173 | 174 | **Author**: Pavlos Parissis 175 | 176 | .. _HAProxy: http://www.haproxy.org/ 177 | .. _stats socket: http://cbonte.github.io/haproxy-dconv/configuration-1.5.html#9.2 178 | .. _Six Python 2 and 3 Compatibility Library: https://pythonhosted.org/six/ 179 | 180 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/haproxyadmin.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/haproxyadmin.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/haproxyadmin" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/haproxyadmin" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\haproxyadmin.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\haproxyadmin.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/TODO.rst: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | #. Add support for enabling/disabling health/agent checks 5 | 6 | #. TLS ticket operations 7 | 8 | #. Add support for TLS ticket operations 9 | 10 | #. Add support for OCSP stapling 11 | 12 | #. Add support for DNS resolvers 13 | 14 | #. Add support for dumping sessions 15 | 16 | #. make internal._HAProxyProcess.send_command() to return file type object as it will avoid to run through the list 2 times. 17 | 18 | #. Investigate the use of __slots__ in utils.CSVLine as it could speed up the library when we create 100K objects 19 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../ChangeLog 2 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # haproxyadmin documentation build configuration file, created by 5 | # sphinx-quickstart on Tue May 26 23:41:46 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('.')) 23 | import haproxyadmin 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.doctest', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.coverage', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = 'haproxyadmin' 53 | copyright = '2015, Pavlos Parissis' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '0.2.1' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '0.2.1' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = [] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all 79 | # documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | # If true, keep warnings as "system message" paragraphs in the built documents. 100 | #keep_warnings = False 101 | 102 | 103 | # -- Options for HTML output ---------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | html_theme = 'default' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | #html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # Add any extra paths that contain custom files (such as robots.txt or 139 | # .htaccess) here, relative to this directory. These files are copied 140 | # directly to the root of the documentation. 141 | #html_extra_path = [] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'haproxyadmindoc' 186 | 187 | 188 | # -- Options for LaTeX output --------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, 203 | # author, documentclass [howto, manual, or own class]). 204 | latex_documents = [ 205 | ('index', 'haproxyadmin.tex', 'haproxyadmin Documentation', 206 | 'Pavlos Parissis', 'manual'), 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | #latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | #latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | #latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | #latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'haproxyadmin', 'haproxyadmin Documentation', 236 | ['Pavlos Parissis'], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | #man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ('index', 'haproxyadmin', 'haproxyadmin Documentation', 250 | 'Pavlos Parissis', 'haproxyadmin', 'One line description of project.', 251 | 'Miscellaneous'), 252 | ] 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #texinfo_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #texinfo_domain_indices = True 259 | 260 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 261 | #texinfo_show_urls = 'footnote' 262 | 263 | # If true, do not generate a @detailmenu in the "Top" node's menu. 264 | #texinfo_no_detailmenu = False 265 | 266 | autodoc_member_order = 'groupwise' 267 | -------------------------------------------------------------------------------- /docs/source/dev/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Developer Interface 4 | =================== 5 | 6 | This part of the documentation covers all the available interfaces of 7 | `haproxyadmin package`_. Public and internal interfaces are described. 8 | 9 | :class:`HAProxy <.HAProxy>`, :class:`Frontend <.Frontend>`, :class:`Backend <.Backend>` 10 | and :class:`server <.Server>` classes are the main 4 public interfaces. 11 | These classes provide methods to run various operations. `HAProxy`_ provides a 12 | several statistics which can be retrieved by calling ``metric()``, see 13 | `HAProxy statistics`_ for the full list of statistics. 14 | 15 | :py:mod:`haproxyadmin.internal` module provides a set of classes that are not 16 | meant for external use. 17 | 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | .. automodule:: haproxyadmin.haproxy 23 | 24 | .. autoclass:: HAProxy 25 | :members: 26 | 27 | .. automodule:: haproxyadmin.frontend 28 | 29 | 30 | .. autoclass:: haproxyadmin.frontend.Frontend 31 | :members: 32 | 33 | .. automodule:: haproxyadmin.backend 34 | 35 | .. autoclass:: Backend 36 | :members: 37 | 38 | .. automodule:: haproxyadmin.server 39 | 40 | .. autoclass:: Server 41 | :members: 42 | 43 | .. automodule:: haproxyadmin.internal.haproxy 44 | :members: 45 | :private-members: 46 | 47 | .. automodule:: haproxyadmin.internal.frontend 48 | :members: 49 | :private-members: 50 | 51 | .. automodule:: haproxyadmin.internal.backend 52 | :members: 53 | :private-members: 54 | 55 | .. automodule:: haproxyadmin.internal.server 56 | :members: 57 | :private-members: 58 | 59 | .. automodule:: haproxyadmin.utils 60 | :members: 61 | 62 | .. automodule:: haproxyadmin.exceptions 63 | :members: 64 | 65 | 66 | Constants 67 | --------- 68 | 69 | Metric names 70 | ^^^^^^^^^^^^ 71 | 72 | Various stats field names for which a value can be retrieved by using 73 | ``metric`` method available in all public and internal interfaces. 74 | 75 | .. data:: haproxyadmin.FRONTEND_METRICS 76 | :annotation: = a list of metric names for retrieving varius statistics for 77 | frontends 78 | 79 | .. data:: haproxyadmin.BACKEND_METRICS 80 | :annotation: = a list of metric names for retrieving varius statistics for 81 | backends 82 | 83 | .. data:: haproxyadmin.SERVER_METRICS 84 | :annotation: = a list of metric names for retrieving varius statistics for 85 | servers 86 | 87 | Aggregation rules 88 | ^^^^^^^^^^^^^^^^^ 89 | 90 | The following 2 constants define the type of aggregation, either sum or 91 | average, which is performed for values returned by all HAProxy processes. 92 | 93 | .. autodata:: haproxyadmin.utils.METRICS_SUM 94 | 95 | .. autodata:: haproxyadmin.utils.METRICS_AVG 96 | 97 | 98 | Valid server states 99 | ^^^^^^^^^^^^^^^^^^^ 100 | 101 | A list of constants to use in ``setstate`` of :class:`.Server` to change 102 | the state of a server. 103 | 104 | .. autodata:: haproxyadmin.haproxy.STATE_ENABLE 105 | 106 | .. autodata:: haproxyadmin.haproxy.STATE_DISABLE 107 | 108 | .. autodata:: haproxyadmin.haproxy.STATE_READY 109 | 110 | .. autodata:: haproxyadmin.haproxy.STATE_DRAIN 111 | 112 | .. autodata:: haproxyadmin.haproxy.STATE_MAINT 113 | 114 | 115 | .. _HAProxy statistics: http://cbonte.github.io/haproxy-dconv/configuration-1.5.html#9 116 | .. _HAProxy: http://www.haproxy.org/ 117 | .. _haproxyadmin package: https://github.com/unixsurfer/haproxyadmin 118 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | Welcome to haproxyadmin's documentation 3 | ======================================= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | introduction 9 | user/guide 10 | dev/api 11 | changelog 12 | TODO.rst 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | 22 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | -------------------------------------------------------------------------------- /docs/source/user/backend.rst: -------------------------------------------------------------------------------- 1 | .. _backend: 2 | 3 | Backend Operations 4 | ------------------ 5 | 6 | A quick way to check if a certain backend exists 7 | 8 | .. code:: python 9 | 10 | >>> backends = hap.backends() 11 | >>> if 'backend1_proc34' in backends: 12 | ... print('have it') 13 | ... 14 | have it 15 | >>> if 'backend1_proc34foo' in backends: 16 | ... print('have it') 17 | ... 18 | >>> 19 | 20 | Retrieve various statistics 21 | 22 | .. code:: python 23 | 24 | >>> backend = hap.backend('backend1_proc34') 25 | >>> for m in BACKEND_METRICS: 26 | ... print("name {} value {}".format(m, backend.metric(m))) 27 | ... 28 | name act value 3 29 | name bck value 0 30 | name bin value 0 31 | name bout value 0 32 | name chkdown value 2 33 | name cli_abrt value 0 34 | name comp_byp value 0 35 | name comp_in value 0 36 | name comp_out value 0 37 | name comp_rsp value 0 38 | name ctime value 0 39 | name downtime value 8237 40 | name dreq value 0 41 | name dresp value 0 42 | name econ value 0 43 | name eresp value 0 44 | name hrsp_1xx value 0 45 | name hrsp_2xx value 0 46 | name hrsp_3xx value 0 47 | name hrsp_4xx value 0 48 | name hrsp_5xx value 0 49 | name hrsp_other value 0 50 | name lastchg value 11373 51 | name lastsess value -1 52 | name lbtot value 0 53 | name qcur value 0 54 | name qmax value 0 55 | name qtime value 0 56 | name rate value 0 57 | name rate_max value 0 58 | name rtime value 0 59 | name scur value 0 60 | name slim value 200000 61 | name smax value 0 62 | name srv_abrt value 0 63 | name stot value 0 64 | name ttime value 0 65 | name weight value 3 66 | name wredis value 0 67 | name wretr value 0 68 | >>> 69 | >>> 70 | >>> backend.process_nb 71 | [4, 3] 72 | >>> backend.requests_per_process() 73 | [(4, 2), (3, 3)] 74 | >>> backend.requests 75 | 5 76 | >>> 77 | 78 | Get all servers in across all backends 79 | 80 | .. code:: python 81 | 82 | >>> for backend in backends: 83 | >>> backends = hap.backends() 84 | ... print(backend.name, backend.requests, backend.process_nb) 85 | ... servers = backend.servers() 86 | ... for server in servers: 87 | ... print(" ", server.name, server.requests) 88 | ... 89 | backend_proc2 100 [2] 90 | bck_proc2_srv4_proc2 25 91 | bck_proc2_srv3_proc2 25 92 | bck_proc2_srv1_proc2 25 93 | bck_proc2_srv2_proc2 25 94 | haproxy 0 [4, 3, 2, 1] 95 | backend1_proc34 16 [4, 3] 96 | bck1_proc34_srv1 6 97 | bck_all_srv1 5 98 | bck1_proc34_srv2 5 99 | backend_proc1 29 [1] 100 | member2_proc1 14 101 | member1_proc1 15 102 | bck_all_srv1 0 103 | backend2_proc34 100 [4, 3] 104 | bck2_proc34_srv2 97 105 | bck2_proc34_srv1 2 106 | bck_all_srv1 1 107 | >>> 108 | 109 | Get servers of a specific backend 110 | 111 | .. code:: python 112 | 113 | >>> backend = hap.backend('backend1_proc34') 114 | >>> for s in backend.servers(): 115 | ... print(s.name, s.status, s.weight) 116 | ... 117 | bck1_proc34_srv2 UP 1 118 | bck_all_srv1 UP 1 119 | bck1_proc34_srv1 UP 1 120 | >>> 121 | 122 | Get a specific server from a backend 123 | 124 | .. code:: python 125 | 126 | >>> s1 = backend.server('bck1_proc34_srv2') 127 | >>> s1.name, s1.backendname, s1.status, s1.requests, s1.weight 128 | ('bck1_proc34_srv2', 'backend1_proc34', 'UP', 9, 1) 129 | 130 | Read :class:`Backend <.Backend>` class for more information. 131 | -------------------------------------------------------------------------------- /docs/source/user/frontend.rst: -------------------------------------------------------------------------------- 1 | .. _frontend: 2 | 3 | Frontend Operations 4 | ------------------- 5 | 6 | A quick way to check if a certain frontend exists 7 | 8 | .. code:: python 9 | 10 | >>> frontends = hap.frontends() 11 | >>> if 'frontend2_proc34' in frontends: 12 | ... print('have it') 13 | ... 14 | have it 15 | >>> if 'frontend2_proc34foo' in frontends: 16 | ... print('have it') 17 | ... 18 | >>> 19 | 20 | Change maximum connections to all frontends 21 | 22 | .. code:: python 23 | 24 | >>> frontends = hap.frontends() 25 | >>> for f in frontends: 26 | ... print(f.maxconn, f.name) 27 | ... f.setmaxconn(10000) 28 | ... print(f.maxconn, f.name) 29 | ... print('---------------') 30 | ... 31 | 3000 haproxy-stats2 32 | True 33 | 10000 haproxy-stats2 34 | --------------- 35 | 6000 frontend1_proc34 36 | True 37 | 20000 frontend1_proc34 38 | --------------- 39 | 6000 frontend2_proc34 40 | True 41 | 20000 frontend2_proc34 42 | --------------- 43 | 3000 frontend_proc2 44 | True 45 | 10000 frontend_proc2 46 | --------------- 47 | 3000 haproxy-stats 48 | True 49 | 10000 haproxy-stats 50 | --------------- 51 | 3000 haproxy-stats3 52 | True 53 | 10000 haproxy-stats3 54 | --------------- 55 | 3000 haproxy-stats4 56 | True 57 | 10000 haproxy-stats4 58 | --------------- 59 | 3000 frontend_proc1 60 | True 61 | 10000 frontend_proc1 62 | --------------- 63 | 64 | Do the same only on specific frontend 65 | 66 | .. code:: python 67 | 68 | >>> frontend = hap.frontend('frontend1_proc34') 69 | >>> frontend.maxconn 70 | 20000 71 | >>> frontend.setmaxconn(50000) 72 | True 73 | >>> frontend.maxconn 74 | 100000 75 | 76 | 77 | Disable and enable a frontend 78 | 79 | .. code:: python 80 | 81 | >>> frontend = hap.frontend('frontend1_proc34') 82 | >>> frontend.status 83 | 'OPEN' 84 | >>> frontend.disable() 85 | True 86 | >>> frontend.status 87 | 'STOP' 88 | >>> frontend.enable() 89 | True 90 | >>> frontend.status 91 | 'OPEN' 92 | 93 | Shutdown a frontend 94 | 95 | .. code:: python 96 | 97 | >>> frontend.shutdown() 98 | True 99 | 100 | .. warning:: 101 | HAProxy removes from the running configuration the frontend, so 102 | further operations on the frontend will return an error. 103 | 104 | .. code:: python 105 | 106 | >>> frontend.status 107 | Traceback (most recent call last): 108 | File "", line 1, in 109 | File "/..ages/haproxyadmin/frontend.py", line 243, in status 110 | 'status') 111 | File "/...ages/haproxyadmin/utils.py", line 168, in cmd_across_all_procs 112 | (getattr(obj, 'process_nb'), getattr(obj, method)(*arg)) 113 | File "/...ages/haproxyadmin/internal.py", line 210, in metric 114 | getattr(self.hap_process.frontends_stats()[self.name], name)) 115 | KeyError: 'frontend1_proc34' 116 | 117 | 118 | Retrieve various statistics 119 | 120 | .. code:: python 121 | 122 | >>> frontend = hap.frontend('frontend2_proc34') 123 | >>> for m in FRONTEND_METRICS: 124 | ... print("name {} value {}".format(m, frontend.metric(m))) 125 | ... 126 | name bin value 380 127 | name bout value 1065 128 | name comp_byp value 0 129 | name comp_in value 0 130 | name comp_out value 0 131 | name comp_rsp value 0 132 | name dreq value 0 133 | name dresp value 0 134 | name ereq value 0 135 | name hrsp_1xx value 0 136 | name hrsp_2xx value 0 137 | name hrsp_3xx value 0 138 | name hrsp_4xx value 0 139 | name hrsp_5xx value 5 140 | name hrsp_other value 0 141 | name rate value 0 142 | name rate_lim value 200000 143 | name rate_max value 2 144 | name req_rate value 0 145 | name req_rate_max value 2 146 | name req_tot value 5 147 | name scur value 0 148 | name slim value 20000 149 | name smax value 3 150 | name stot value 5 151 | >>> 152 | >>> frontend.process_nb 153 | [4, 3] 154 | >>> frontend.requests_per_process() 155 | [(4, 2), (3, 3)] 156 | >>> frontend.requests 157 | 5 158 | >>> 159 | 160 | 161 | .. note:: 162 | ``requests`` returns HTTP requests that are processed by the frontend. 163 | If the frontend is in TCP mode the number will be always 0 and *stot* 164 | metric should be used to retrieve the number of TCP requests processsed. 165 | 166 | 167 | Read :class:`Frontend <.Frontend>` class for more information. 168 | -------------------------------------------------------------------------------- /docs/source/user/guide.rst: -------------------------------------------------------------------------------- 1 | .. _guide: 2 | 3 | User Guide 4 | ========== 5 | 6 | This part of the documentation covers step-by-step instructions for getting 7 | the most out of **haproxyadmin**. It begins by introducing operations related 8 | to HAProxy process and then focus on providing the most frequent operations 9 | for frontends, backends and servers. In all examples HAProxy is configured 10 | with 4 processes, see example `HAProxy configuration`_. 11 | 12 | A :class:`HAProxy <.HAProxy>` object with the name ``hap`` needs to be created 13 | prior running the code mentioned in the following sections: 14 | 15 | .. code:: python 16 | 17 | >>> from haproxyadmin import haproxy 18 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 19 | 20 | .. warning:: Make sure you have appropriate privillage to write in the socket files. 21 | 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | haproxy 27 | frontend 28 | backend 29 | server 30 | 31 | 32 | .. _HAProxy configuration: https://raw.githubusercontent.com/unixsurfer/haproxyadmin/master/tools/haproxy.cfg 33 | -------------------------------------------------------------------------------- /docs/source/user/haproxy.rst: -------------------------------------------------------------------------------- 1 | .. _haproxy: 2 | 3 | HAProxy Operations 4 | ------------------ 5 | 6 | Get some information about the running processes 7 | 8 | .. code:: python 9 | 10 | >>> hap.processids 11 | [871, 870, 869, 868] 12 | >>> 13 | >>> hap.description 14 | 'Test server' 15 | >>> 16 | >>> hap.releasedate 17 | '2014/10/31' 18 | >>> 19 | >>> hap.version 20 | '1.5.8' 21 | >>> 22 | >>> hap.uptime 23 | '2d 0h55m09s' 24 | >>> 25 | >>> hap.uptimesec 26 | 176112 27 | >>> 28 | >>> hap.nodename 29 | 'test.foo.com' 30 | >>> 31 | >>> hap.totalrequests 32 | 796 33 | 34 | .. note:: 35 | ``totalrequests`` returns the total number of requests that are processed 36 | by HAProxy. It counts requests for frontends and backends. Don't forget that 37 | a single client request passes HAProxy twice. 38 | 39 | Dynamically change the specified global maxconn setting. 40 | 41 | .. code:: python 42 | 43 | >>> print(hap.maxconn) 44 | 40000 45 | >>> hap.setmaxconn(5000) 46 | True 47 | >>> print(hap.maxconn) 48 | 20000 49 | >>> 50 | 51 | .. note:: New setting is applied per process and the sum is returned. 52 | 53 | 54 | Get a list of :class:`Frontend <.Frontend>` objects for all frontends 55 | 56 | .. code:: python 57 | 58 | >>> frontends = hap.frontends() 59 | >>> for f in frontends: 60 | ... print(f.name) 61 | ... 62 | frontend_proc1 63 | haproxy 64 | frontend1_proc34 65 | frontend2_proc34 66 | frontend_proc2 67 | 68 | 69 | Get a :class:`Frontend <.Frontend>` object for a single frontend 70 | 71 | .. code:: python 72 | 73 | >>> frontend1 = hap.frontend('frontend1_proc34') 74 | >>> frontend1.name, frontend1.process_nb 75 | ('frontend1_proc34', [4, 3]) 76 | 77 | Get a list of :class:`Backend <.Backend>` objects for all backends 78 | 79 | .. code:: python 80 | 81 | >>> backends = hap.backends() 82 | >>> for b in backends: 83 | ... print(b.name) 84 | ... 85 | haproxy 86 | backend1_proc34 87 | backend_proc2 88 | backend_proc1 89 | backend2_proc34 90 | 91 | Get a :class:`Backend <.Backend>` object for a single backend 92 | 93 | .. code:: python 94 | 95 | >>> backend1 = hap.backend('backend1_proc34') 96 | >>> backend1.name, backend1.process_nb 97 | ('backend1_proc34', [4, 3]) 98 | 99 | Get a list of :class:`Server <.Server>` objects for each server 100 | 101 | .. code:: python 102 | 103 | >>> servers = hap.servers() 104 | >>> for s in servers: 105 | ... print(s.name, s.backendname) 106 | ... 107 | bck1_proc34_srv1 backend1_proc34 108 | bck1_proc34_srv2 backend1_proc34 109 | bck_all_srv1 backend1_proc34 110 | bck_proc2_srv3_proc2 backend_proc2 111 | bck_proc2_srv1_proc2 backend_proc2 112 | bck_proc2_srv4_proc2 backend_proc2 113 | bck_proc2_srv2_proc2 backend_proc2 114 | member1_proc1 backend_proc1 115 | bck_all_srv1 backend_proc1 116 | member2_proc1 backend_proc1 117 | bck2_proc34_srv1 backend2_proc34 118 | bck_all_srv1 backend2_proc34 119 | bck2_proc34_srv2 backend2_proc34 120 | 121 | .. note:: 122 | if a server is member of more than 1 backends then muliple 123 | :class:`Server <.Server>` objects for the server is returned 124 | 125 | Limit the list of server for a specific pool 126 | 127 | .. code:: python 128 | 129 | >>> servers = hap.servers(backend='backend1_proc34') 130 | >>> for s in servers: 131 | ... print(s.name, s.backendname) 132 | ... 133 | bck1_proc34_srv1 backend1_proc34 134 | bck1_proc34_srv2 backend1_proc34 135 | bck_all_srv1 backend1_proc34 136 | 137 | Work on specific server across all backends 138 | 139 | .. code:: python 140 | 141 | >>> s1 = hap.server(hostname='bck_all_srv1') 142 | >>> for x in s1: 143 | ... print(x.name, x.backendname, x.status) 144 | ... x.setstate(haproxy.STATE_DISABLE) 145 | ... print(x.status) 146 | ... 147 | bck_all_srv1 backend1_proc34 DOWN 148 | True 149 | MAINT 150 | bck_all_srv1 backend_proc1 DOWN 151 | True 152 | MAINT 153 | bck_all_srv1 backend2_proc34 no check 154 | True 155 | MAINT 156 | 157 | 158 | Examples for ACLs 159 | 160 | .. code:: python 161 | 162 | >>> from pprint import pprint 163 | >>> pprint(hap.show_acl()) 164 | ['# id (file) description', 165 | "0 (/etc/haproxy/wl_stats) pattern loaded from file '/etc/haproxy/wl_stats' " 166 | "used by acl at file '/etc/haproxy/haproxy.cfg' line 53", 167 | "1 () acl 'src' file '/etc/haproxy/haproxy.cfg' line 53", 168 | "3 () acl 'ssl_fc' file '/etc/haproxy/haproxy.cfg' line 85", 169 | '4 (/etc/haproxy/bl_frontend) pattern loaded from file ' 170 | "'/etc/haproxy/bl_frontend' used by acl at file '/etc/haproxy/haproxy.cfg' " 171 | 'line 97', 172 | "5 () acl 'src' file '/etc/haproxy/haproxy.cfg' line 97", 173 | "6 () acl 'path_beg' file '/etc/haproxy/haproxy.cfg' line 99", 174 | "7 () acl 'req.cook' file '/etc/haproxy/haproxy.cfg' line 114", 175 | "8 () acl 'req.cook' file '/etc/haproxy/haproxy.cfg' line 115", 176 | "9 () acl 'req.cook' file '/etc/haproxy/haproxy.cfg' line 116", 177 | ''] 178 | >>> hap.show_acl(6) 179 | ['0x12ea940 /static/css/', ''] 180 | >>> hap.add_acl(6, '/foobar') 181 | True 182 | >>> hap.show_acl(6) 183 | ['0x12ea940 /static/css/', '0x13a38b0 /foobar', ''] 184 | >>> hap.add_acl(6, '/foobar') 185 | True 186 | >>> hap.show_acl(6) 187 | ['0x12ea940 /static/css/', '0x13a38b0 /foobar', '0x13a3930 /foobar', ''] 188 | >>> hap.del_acl(6, '/foobar') 189 | True 190 | >>> hap.show_acl(6) 191 | ['0x12ea8a0 /static/js/', '0x12ea940 /static/css/', ''] 192 | 193 | 194 | Examples for MAPs 195 | 196 | .. code:: python 197 | 198 | >>> from haproxyadmin import haproxy 199 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 200 | >>> hap.show_map(map=6) 201 | ['# id (file) description', 202 | "0 (/etc/haproxy/v-m1-bk) pattern loaded ...... line 82", 203 | ''] 204 | >>> hap.show_map(0) 205 | ['0x1a78ab0 0 www.foo.com-0', '0x1a78b20 1 www.foo.com-1', ''] 206 | 207 | 208 | Manage MAPs 209 | 210 | .. code:: python 211 | 212 | >>> hap.show_map(0) 213 | ['0x1a78b20 1 www.foo.com-1', ''] 214 | >>> hap.add_map(0, '9', 'foo') 215 | True 216 | >>> hap.show_map(0) 217 | ['0x1a78b20 1 www.foo.com-1', '0x1b15c80 9 foo', ''] 218 | 219 | .. code:: python 220 | 221 | >>> hap.show_map(0) 222 | ['0x1b15cd0 9 foo', '0x1a78980 11 bar', ''] 223 | >>> hap.del_map(0, '0x1b15cd0') 224 | True 225 | >>> hap.show_map(0) 226 | ['0x1a78980 11 bar', ''] 227 | >>> hap.add_map(0, '22', 'bar22') 228 | True 229 | >>> hap.show_map(0) 230 | ['0x1a78980 11 bar', '0x1b15c00 22 bar22', ''] 231 | >>> hap.del_map(0, '22') 232 | True 233 | >>> hap.show_map(0) 234 | ['0x1a78980 11 bar', ''] 235 | 236 | -------------------------------------------------------------------------------- /docs/source/user/server.rst: -------------------------------------------------------------------------------- 1 | .. _server: 2 | 3 | Server Operations 4 | ----------------- 5 | 6 | A quick way to check if a certain server exists 7 | 8 | .. code:: python 9 | 10 | >>> servers = hap.servers() 11 | >>> if 'bck_all_srv1' in servers: 12 | ... print("have it") 13 | ... 14 | have it 15 | >>> if 'bck_all_srv1foo' in servers: 16 | ... print("have it") 17 | ... 18 | >>> 19 | 20 | Retrieve various statistics 21 | 22 | .. code:: python 23 | 24 | >>> backend = hap.backend('backend1_proc34') 25 | >>> for server in backend.servers(): 26 | ... print(server.name) 27 | ... for m in SERVER_METRICS: 28 | ... print("name {} value {}".format(m, server.metric(m))) 29 | ... print("-----------") 30 | ... 31 | bck1_proc34_srv2 32 | name qcur value 0 33 | name qmax value 0 34 | name scur value 0 35 | name smax value 0 36 | name stot value 0 37 | name bin value 0 38 | name bout value 0 39 | name dresp value 0 40 | name econ value 0 41 | name eresp value 0 42 | name wretr value 0 43 | name wredis value 0 44 | name weight value 1 45 | name act value 1 46 | name bck value 0 47 | name chkfail value 6 48 | name chkdown value 4 49 | name lastchg value 39464 50 | name downtime value 47702 51 | name qlimit value 0 52 | name throttle value 0 53 | name lbtot value 0 54 | name rate value 0 55 | name rate_max value 0 56 | name check_duration value 5001 57 | name hrsp_1xx value 0 58 | name hrsp_2xx value 0 59 | name hrsp_3xx value 0 60 | name hrsp_4xx value 0 61 | name hrsp_5xx value 0 62 | name hrsp_other value 0 63 | name cli_abrt value 0 64 | name srv_abrt value 0 65 | name lastsess value -1 66 | name qtime value 0 67 | name ctime value 0 68 | name rtime value 0 69 | name ttime value 0 70 | ----------- 71 | bck1_proc34_srv1 72 | name qcur value 0 73 | name qmax value 0 74 | name scur value 0 75 | name smax value 0 76 | name stot value 0 77 | name bin value 0 78 | name bout value 0 79 | name dresp value 0 80 | name econ value 0 81 | name eresp value 0 82 | name wretr value 0 83 | name wredis value 0 84 | name weight value 1 85 | name act value 1 86 | name bck value 0 87 | name chkfail value 6 88 | name chkdown value 4 89 | name lastchg value 39464 90 | name downtime value 47702 91 | name qlimit value 0 92 | name throttle value 0 93 | name lbtot value 0 94 | name rate value 0 95 | name rate_max value 0 96 | name check_duration value 5001 97 | name hrsp_1xx value 0 98 | name hrsp_2xx value 0 99 | name hrsp_3xx value 0 100 | name hrsp_4xx value 0 101 | name hrsp_5xx value 0 102 | name hrsp_other value 0 103 | name cli_abrt value 0 104 | name srv_abrt value 0 105 | name lastsess value -1 106 | name qtime value 0 107 | name ctime value 0 108 | name rtime value 0 109 | name ttime value 0 110 | ----------- 111 | bck_all_srv1 112 | name qcur value 0 113 | name qmax value 0 114 | name scur value 0 115 | name smax value 0 116 | name stot value 0 117 | name bin value 0 118 | name bout value 0 119 | name dresp value 0 120 | name econ value 0 121 | name eresp value 0 122 | name wretr value 0 123 | name wredis value 0 124 | name weight value 1 125 | name act value 1 126 | name bck value 0 127 | name chkfail value 6 128 | name chkdown value 4 129 | name lastchg value 39462 130 | name downtime value 47700 131 | name qlimit value 0 132 | name throttle value 0 133 | name lbtot value 0 134 | name rate value 0 135 | name rate_max value 0 136 | name check_duration value 5001 137 | name hrsp_1xx value 0 138 | name hrsp_2xx value 0 139 | name hrsp_3xx value 0 140 | name hrsp_4xx value 0 141 | name hrsp_5xx value 0 142 | name hrsp_other value 0 143 | name cli_abrt value 0 144 | name srv_abrt value 0 145 | name lastsess value -1 146 | name qtime value 0 147 | name ctime value 0 148 | name rtime value 0 149 | name ttime value 0 150 | ----------- 151 | >>> 152 | 153 | Change weight of server in a backend 154 | 155 | .. code:: python 156 | 157 | >>> backend = hap.backend('backend1_proc34') 158 | >>> server = backend.server('bck_all_srv1') 159 | >>> server.weight 160 | 100 161 | >>> server.setweight('20%') 162 | True 163 | >>> server.weight 164 | 20 165 | >>> server.setweight(58) 166 | True 167 | >>> server.weight 168 | 58 169 | 170 | .. note:: 171 | If the value ends with the '%' sign, then the new weight will be relative 172 | to the initially configured weight. Absolute weights are permitted between 173 | 0 and 256. 174 | 175 | or across all backends 176 | 177 | .. code:: python 178 | 179 | >>> server_per_backend = hap.server('bck_all_srv1') 180 | >>> for server in server_per_backend: 181 | ... print(server.backendname, server.weight) 182 | ... server.setweight(8) 183 | ... print(server.backendname, server.weight) 184 | ... 185 | backend2_proc34 1 186 | True 187 | backend2_proc34 8 188 | backend1_proc34 0 189 | True 190 | backend1_proc34 8 191 | backend_proc1 100 192 | True 193 | backend_proc1 8 194 | >>> 195 | 196 | Terminate all the sessions attached to the specified server. 197 | 198 | .. code:: python 199 | 200 | >>> backend = hap.backend('backend1_proc34') 201 | >>> server = backend.server('bck_all_srv1') 202 | >>> server.metric('scur') 203 | 8 204 | >>> server.shutdown() 205 | True 206 | >>> server.metric('scur') 207 | 0 208 | 209 | Disable a server in a backend 210 | 211 | .. code:: python 212 | 213 | >>> server = hap.server('member_bkall', backend='backend_proc1')[0] 214 | >>> server.setstate(haproxy.STATE_DISABLE) 215 | True 216 | >>> server.status 217 | 'MAINT' 218 | >>> server.setstate(haproxy.STATE_ENABLE) 219 | True 220 | >>> server.status 221 | 'no check' 222 | 223 | Get status of server 224 | 225 | .. code:: python 226 | 227 | >>> backend = hap.backend('backend1_proc34') 228 | >>> server = backend.server('bck_all_srv1') 229 | >>> server.last_agent_check 230 | '' 231 | >>> server.check_status 232 | 'L4TOUT' 233 | >>> server.check_ 234 | server.check_code server.check_status 235 | >>> server.check_code 236 | '' 237 | >>> server.status 238 | 'DOWN' 239 | >>> 240 | 241 | Read :class:`Server <.Server>` class for more information. 242 | -------------------------------------------------------------------------------- /haproxyadmin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:fenc=utf-8 3 | # 4 | """ 5 | haproxyadmin 6 | ~~~~~~~~~~~~ 7 | 8 | A python library to interact with HAProxy over UNIX socket 9 | """ 10 | __title__ = 'haproxyadmin' 11 | __author__ = 'Pavlos Parissis' 12 | __license__ = 'Apache 2.0' 13 | __version__ = '0.2.4' 14 | __copyright__ = 'Copyright 2015-2019 Pavlos Parissis' 15 | 16 | from haproxyadmin.haproxy import HAPROXY_METRICS 17 | from haproxyadmin.frontend import FRONTEND_METRICS 18 | from haproxyadmin.backend import BACKEND_METRICS 19 | from haproxyadmin.server import (SERVER_METRICS, VALID_STATES, 20 | STATE_ENABLE, STATE_DISABLE, STATE_READY, 21 | STATE_DRAIN, STATE_MAINT) 22 | -------------------------------------------------------------------------------- /haproxyadmin/backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.backend 7 | ~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides the :class:`Backend <.Backend>` class which allows to 10 | run operation for a backend. 11 | 12 | """ 13 | from haproxyadmin.utils import (calculate, cmd_across_all_procs, 14 | compare_values, converter) 15 | from haproxyadmin.server import Server 16 | 17 | 18 | BACKEND_METRICS = [ 19 | 'act', 20 | 'bck', 21 | 'bin', 22 | 'bout', 23 | 'chkdown', 24 | 'cli_abrt', 25 | 'comp_byp', 26 | 'comp_in', 27 | 'comp_out', 28 | 'comp_rsp', 29 | 'ctime', 30 | 'downtime', 31 | 'dreq', 32 | 'dresp', 33 | 'econ', 34 | 'eresp', 35 | 'hrsp_1xx', 36 | 'hrsp_2xx', 37 | 'hrsp_3xx', 38 | 'hrsp_4xx', 39 | 'hrsp_5xx', 40 | 'hrsp_other', 41 | 'lastchg', 42 | 'lastsess', 43 | 'lbtot', 44 | 'qcur', 45 | 'qmax', 46 | 'qtime', 47 | 'rate', 48 | 'rate_max', 49 | 'rtime', 50 | 'scur', 51 | 'slim', 52 | 'smax', 53 | 'srv_abrt', 54 | 'stot', 55 | 'ttime', 56 | 'weight', 57 | 'wredis', 58 | 'wretr', 59 | ] 60 | 61 | 62 | class Backend(object): 63 | """Build a user-created :class:`Backend` for a single backend. 64 | 65 | :param backend_per_proc: list of :class:`._Backend` objects. 66 | :type backend_per_proc: ``list`` 67 | :rtype: a :class:`Backend`. 68 | """ 69 | 70 | def __init__(self, backend_per_proc): 71 | self._backend_per_proc = backend_per_proc 72 | self._name = self._backend_per_proc[0].name 73 | 74 | # built-in comparison operator is adjusted 75 | def __eq__(self, other): 76 | if isinstance(other, Backend): 77 | return (self.name == other.name) 78 | elif isinstance(other, str): 79 | return (self.name == other) 80 | else: 81 | return False 82 | 83 | def __ne__(self, other): 84 | return (not self.__eq__(other)) 85 | 86 | @property 87 | def iid(self): 88 | """Return the unique proxy ID of the backend. 89 | 90 | .. note:: 91 | Because proxy ID is the same across all processes, 92 | we return the proxy ID from the 1st process. 93 | 94 | :rtype: ``int`` 95 | """ 96 | return int(self._backend_per_proc[0].iid) 97 | 98 | def servers(self, name=None): 99 | """Return Server object for each server. 100 | 101 | :param name: (optional) servername to look for. Defaults to None. 102 | :type name: string 103 | :return: A list of :class:`Server ` objects 104 | :rtype: list 105 | """ 106 | return_list = [] 107 | 108 | # store _Server objects for each server as it is reported by each 109 | # process. 110 | # key: name of the server 111 | # value: a list of _Server object 112 | servers_across_hap_processes = {} 113 | 114 | # Get a list of servers (_Server objects) per process 115 | for backend in self._backend_per_proc: 116 | for server in backend.servers(name): 117 | if server.name not in servers_across_hap_processes: 118 | servers_across_hap_processes[server.name] = [] 119 | servers_across_hap_processes[server.name].append(server) 120 | 121 | # For each server build a Server object 122 | for server_per_proc in servers_across_hap_processes.values(): 123 | return_list.append(Server(server_per_proc, self.name)) 124 | 125 | return return_list 126 | 127 | def server(self, name): 128 | """Return a Server object 129 | 130 | :param name: Name of the server 131 | :type name: string 132 | :return: :class:`Server ` object 133 | :rtype: haproxyadmin.Server 134 | """ 135 | server = self.servers(name) 136 | if len(server) == 1: 137 | return server[0] 138 | elif len(server) == 0: 139 | raise ValueError("Could not find server") 140 | else: 141 | raise ValueError("Found more than one server, this is a bug!") 142 | 143 | def metric(self, name): 144 | """Return the value of a metric. 145 | 146 | Performs a calculation on the metric across all HAProxy processes. 147 | The type of calculation is either sum or avg and defined in 148 | utils.METRICS_SUM and utils.METRICS_AVG. 149 | 150 | :param name: Name of the metric, any of BACKEND_METRICS 151 | :type name: ``string`` 152 | :return: Value of the metric after the appropriate calculation 153 | has been performed. 154 | :rtype: number, either ``integer`` or ``float``. 155 | :raise: ValueError when a given metric is not found. 156 | """ 157 | metrics = [] 158 | if name not in BACKEND_METRICS: 159 | raise ValueError("{} is not valid metric".format(name)) 160 | 161 | metrics = [x.metric(name) for x in self._backend_per_proc] 162 | metrics[:] = (converter(x) for x in metrics) 163 | metrics[:] = (x for x in metrics if x is not None) 164 | 165 | return calculate(name, metrics) 166 | 167 | @property 168 | def name(self): 169 | """Return the name of the backend. 170 | 171 | :rtype: string 172 | """ 173 | return self._name 174 | 175 | @property 176 | def process_nb(self): 177 | """Return a list of process number in which backend is configured. 178 | 179 | :rtype: list 180 | """ 181 | process_numbers = [] 182 | for backend in self._backend_per_proc: 183 | process_numbers.append(backend.process_nb) 184 | 185 | return process_numbers 186 | 187 | @property 188 | def requests(self): 189 | """Return the number of requests. 190 | 191 | :rtype: integer 192 | """ 193 | return self.metric('stot') 194 | 195 | def requests_per_process(self): 196 | """Return the number of requests for the backend per process. 197 | 198 | :return: a list of tuples with 2 elements 199 | 200 | #. process number of HAProxy 201 | #. requests 202 | 203 | :rtype: ``list`` 204 | 205 | """ 206 | results = cmd_across_all_procs(self._backend_per_proc, 'metric', 'stot') 207 | 208 | return results 209 | 210 | def stats_per_process(self): 211 | """Return all stats of the backend per process. 212 | 213 | :return: a list of tuples with 2 elements 214 | 215 | #. process number 216 | #. a dict with all stats 217 | 218 | :rtype: ``list`` 219 | 220 | """ 221 | values = cmd_across_all_procs(self._backend_per_proc, 'stats') 222 | 223 | return values 224 | 225 | @property 226 | def status(self): 227 | """Return the status of the backend. 228 | 229 | :rtype: ``string`` 230 | :raise: :class:`IncosistentData` exception if status is different 231 | per process. 232 | 233 | """ 234 | results = cmd_across_all_procs(self._backend_per_proc, 'metric', 'status') 235 | 236 | return compare_values(results) 237 | -------------------------------------------------------------------------------- /haproxyadmin/command_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:fenc=utf-8 3 | 4 | """ 5 | haproxyadmin.command_status.py 6 | ~~~~~~~~~~~~~~~~~~~ 7 | 8 | This module categorizes the output returned by various commands 9 | 10 | """ 11 | # CLI doesn't return a message if operation is successfully executed. 12 | # But, versions prior 1.5.10 was returning 'Done.' message only for ACL/MAPs 13 | # operations. 73f1d8f447087 commit in haproxy-1.5 makes the output consistent 14 | # by removing 'Done.' message. 15 | SUCCESS_OUTPUT_STRINGS = [ 16 | 'Done.', 17 | '' 18 | ] 19 | 20 | ERROR_OUTPUT_STRINGS = [ 21 | "'add acl' expects two parameters: ACL identifier and pattern.", 22 | "'add map' expects three parameters: map identifier, key and value.", 23 | "'add' only supports 'map'.", 24 | "A frontend name is expected.", 25 | "agent checks are not enabled on this server.", 26 | "Agent was not configured on this server, cannot enable.", 27 | "cannot change health on a tracking server.", 28 | "content-based lookup is only supported with the \"show\" and \"clear\" actions", 29 | "\"data.\" followed by a value expected", 30 | "Data type not stored in this table", 31 | "'del' only supports 'map' or 'acl'.", 32 | "'disable' only supports 'agent', 'frontend', 'health', and 'server'.", 33 | "'enable' only supports 'agent', 'frontend', 'health', and 'server'.", 34 | "Entry currently in use, cannot remove", 35 | "Expects a maximum input byte rate in kB/s.", 36 | "Expects an integer value.", 37 | "Failed to pause frontend, check logs for precise cause.", 38 | "Failed to resume frontend, check logs for precise cause (port conflict?).", 39 | "Frontend is already disabled.", 40 | "Frontend is already enabled.", 41 | "Frontend was already shut down.", 42 | "Frontend was previously shut down, cannot disable.", 43 | "Frontend was previously shut down, cannot enable.", 44 | "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.", 45 | "Health checks are not configured on this server, cannot enable.", 46 | "Integer value expected.", 47 | "Invalid key", 48 | "Invalid timeout value.", 49 | "Key not found.", 50 | "Key value expected", 51 | "Malformed identifier. Please use # or .", 52 | "Missing ACL identifier.", 53 | "Missing ACL identifier and/or key.", 54 | "Missing map identifier.", 55 | "Missing map identifier and/or key.", 56 | "No such backend.", 57 | "No such frontend.", 58 | "No such server.", 59 | "No such session (use 'show sess').", 60 | "No such table", 61 | "OCSP Response updated!", 62 | "Optional argument only supports \"data.\" and key ", 63 | "Out of memory error.", 64 | "Proxy is disabled.", 65 | "Removing keys from ip tables of type other than ip, ipv6, string and integer is not supported", 66 | "Require and operator among \"eq\", \"ne\", \"le\", \"ge\", \"lt\", \"gt\"", 67 | "Require a valid integer value to compare against", 68 | "Require a valid integer value to store", 69 | "Require 'backend/server'.", 70 | "Required arguments: \"data.\" or
key ", 71 | "Session pointer expected (use 'show sess').", 72 | "'set map' expects three parameters: map identifier, key and value.", 73 | "'set maxconn' only supports 'frontend' and 'global'.", 74 | "'set rate-limit connections' only supports 'global'.", 75 | "'set rate-limit http-compression' only supports 'global'.", 76 | "'set rate-limit sessions' only supports 'global'.", 77 | "'set rate-limit ssl-sessions' only supports 'global'.", 78 | "'set rate-limit' supports 'connections', 'sessions', 'ssl-sessions', and 'http-compression'.", 79 | "'set server agent' expects 'up' or 'down'.", 80 | "'set server health' expects 'up', 'stopping', or 'down'.", 81 | "'set server ' only supports 'agent', 'health', 'state', 'weight' add 'addr'.", 82 | "'set server state' expects 'ready', 'drain' and 'maint'.", 83 | "'set ssl ocsp-response' expects response in base64 encoding.", 84 | "'set ssl ocsp-response' received invalid base64 encoded response.", 85 | "'set ssl' only supports 'ocsp-response'.", 86 | "'set timeout' only supports 'cli'.", 87 | "Showing keys from tables of type other than ip, ipv6, string and integer is not supported", 88 | "'shutdown' only supports 'frontend', 'session' and 'sessions'.", 89 | "'shutdown sessions' only supports 'server'.", 90 | "This ACL is shared with a map containing samples. ", 91 | "This command expects two parameters: ACL identifier and key.", 92 | "This command expects two parameters: map identifier and key.", 93 | "Unable to allocate a new entry", 94 | "Unknown ACL identifier. Please use # or .", 95 | "Unknown action", 96 | "Unknown data type", 97 | "Unknown map identifier. Please use # or .", 98 | "Unknown command. Please enter one of the following commands only :", 99 | "Value out of range.", 100 | "Missing resolver section identifier.", 101 | "Can't find resolvers section.", 102 | "Can't find backend.", 103 | ] 104 | 105 | SUCCESS_STRING_ADDRESS = "IP changed from|no need to change the addr" 106 | SUCCESS_STRING_PORT = ("no need to change the addr, port changed from|no need " 107 | "to change the addr, no need to change the port" 108 | ) 109 | -------------------------------------------------------------------------------- /haproxyadmin/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:fenc=utf-8 3 | 4 | """ 5 | haproxyadmin.exceptions 6 | ~~~~~~~~~~~~~~~~~~~~~~~ 7 | 8 | This module contains the set of haproxyadmin' exceptions with the following 9 | hierarchy:: 10 | 11 | HAProxyBaseError 12 | ├── CommandFailed 13 | ├── HAProxyDataError 14 | │   ├── IncosistentData 15 | │   └── MultipleCommandResults 16 | └── HAProxySocketError 17 | ├── SocketApplicationError 18 | ├── SocketConnectionError 19 | ├── SocketPermissionError 20 | ├── SocketTimeout 21 | └── SocketTransportError 22 | """ 23 | 24 | 25 | class HAProxyBaseError(Exception): 26 | """haproxyadmin base exception. 27 | 28 | :param message: error message. 29 | :type message: ``string`` 30 | """ 31 | message = '' 32 | 33 | def __init__(self, message=''): 34 | if message: 35 | self.message = message 36 | super(HAProxyBaseError, self).__init__(self.message) 37 | 38 | 39 | class CommandFailed(HAProxyBaseError): 40 | """Raised when a command to HAProxy returned an error.""" 41 | 42 | 43 | class HAProxyDataError(HAProxyBaseError): 44 | """Base DataError class. 45 | 46 | :param results: A structure which contains data returned be each socket. 47 | :type results: ``list`` of ``list`` 48 | """ 49 | def __init__(self, results): 50 | self.results = results 51 | super(HAProxyDataError, self).__init__() 52 | 53 | 54 | class MultipleCommandResults(HAProxyDataError): 55 | """Command returned different results per HAProxy process.""" 56 | message = 'Received different result per HAProxy process' 57 | 58 | 59 | class IncosistentData(HAProxyDataError): 60 | """Data across all processes is not the same.""" 61 | message = 'Received different data per HAProxy process' 62 | 63 | 64 | class HAProxySocketError(HAProxyBaseError): 65 | """Base SocketError class. 66 | 67 | :param socket_file: socket file. 68 | :type socket_file: ``string`` 69 | """ 70 | def __init__(self, socket_file): 71 | self.socket_file = socket_file 72 | self.message = self.message + ' ' + self.socket_file 73 | super(HAProxySocketError, self).__init__(self.message) 74 | 75 | 76 | class SocketTimeout(HAProxySocketError): 77 | """Raised when we timeout on the socket.""" 78 | message = 'Socket timed out' 79 | 80 | 81 | class SocketPermissionError(HAProxySocketError): 82 | """Raised when permissions are not granted to access socket file.""" 83 | message = 'No permissions are granted to access socket file' 84 | 85 | 86 | class SocketConnectionError(HAProxySocketError): 87 | """Raised when socket file is not bound to a process.""" 88 | message = 'No process is bound to socket file' 89 | 90 | 91 | class SocketApplicationError(HAProxySocketError): 92 | """Raised when we connect to a socket and HAProxy is not bound to it.""" 93 | message = 'HAProxy is not bound to socket file' 94 | 95 | 96 | class SocketTransportError(HAProxySocketError): 97 | """Raised when endpoint of socket hasn't closed an old connection. 98 | 99 | .. note:: 100 | It only occurs in cases where HAProxy is ~90% CPU utilization for 101 | processing traffic and we reconnect to the socket too 102 | fast and as a result HAProxy doesn't have enough time to close the 103 | previous connection. 104 | 105 | """ 106 | message = 'Transport endpoint is already connected' 107 | -------------------------------------------------------------------------------- /haproxyadmin/frontend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.frontend 7 | ~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides the :class:`Frontend <.Frontend>` class. This class can 10 | be used to run operations on a frontend and retrieve statistics. 11 | 12 | """ 13 | from haproxyadmin.utils import (calculate, cmd_across_all_procs, converter, 14 | check_command, should_die, compare_values) 15 | 16 | 17 | FRONTEND_METRICS = [ 18 | 'bin', 19 | 'bout', 20 | 'comp_byp', 21 | 'comp_in', 22 | 'comp_out', 23 | 'comp_rsp', 24 | 'dreq', 25 | 'dresp', 26 | 'ereq', 27 | 'hrsp_1xx', 28 | 'hrsp_2xx', 29 | 'hrsp_3xx', 30 | 'hrsp_4xx', 31 | 'hrsp_5xx', 32 | 'hrsp_other', 33 | 'rate', 34 | 'rate_lim', 35 | 'rate_max', 36 | 'req_rate', 37 | 'req_rate_max', 38 | 'req_tot', 39 | 'scur', 40 | 'slim', 41 | 'smax', 42 | 'stot', 43 | ] 44 | 45 | 46 | class Frontend(object): 47 | """Build a user-created :class:`Frontend` for a single frontend. 48 | 49 | :param frontend_per_proc: list of :class:`._Frontend` objects. 50 | :type frontend_per_proc: ``list`` 51 | :rtype: a :class:`Frontend`. 52 | """ 53 | 54 | def __init__(self, frontend_per_proc): 55 | self._frontend_per_proc = frontend_per_proc 56 | self._name = self._frontend_per_proc[0].name 57 | 58 | # built-in comparison operator is adjusted to support 59 | # if 'x' in list_of_frontend_obj 60 | # x == frontend_obj 61 | def __eq__(self, other): 62 | if isinstance(other, Frontend): 63 | return (self.name == other.name) 64 | elif isinstance(other, str): 65 | return (self.name == other) 66 | else: 67 | return False 68 | 69 | def __ne__(self, other): 70 | return (not self.__eq__(other)) 71 | 72 | @property 73 | def iid(self): 74 | """Return the unique proxy ID of the frontend. 75 | 76 | .. note:: 77 | Because proxy ID is the same across all processes, 78 | we return the proxy ID from the 1st process. 79 | 80 | :rtype: ``int`` 81 | """ 82 | return int(self._frontend_per_proc[0].iid) 83 | 84 | @should_die 85 | def disable(self): 86 | """Disable frontend. 87 | 88 | :param die: control the handling of errors. 89 | :type die: ``bool`` 90 | :return: ``True`` if frontend is disabled otherwise ``False``. 91 | :rtype: bool 92 | :raise: If ``die`` is ``True`` 93 | :class:`haproxyadmin.exceptions.CommandFailed` or 94 | :class:`haproxyadmin.exceptions.MultipleCommandResults` is raised 95 | when something bad happens otherwise returns ``False``. 96 | 97 | """ 98 | cmd = "disable frontend {}".format(self.name) 99 | results = cmd_across_all_procs(self._frontend_per_proc, 'command', cmd) 100 | 101 | return check_command(results) 102 | 103 | @should_die 104 | def enable(self): 105 | """Enable frontend. 106 | 107 | :param die: control the handling of errors. 108 | :type die: ``bool`` 109 | :return: ``True`` if frontend is enabled otherwise ``False``. 110 | :rtype: bool 111 | :raise: If ``die`` is ``True`` 112 | :class:`haproxyadmin.exceptions.CommandFailed` or 113 | :class:`haproxyadmin.exceptions.MultipleCommandResults` is raised 114 | when something bad happens otherwise returns ``False``. 115 | """ 116 | cmd = "enable frontend {}".format(self.name) 117 | results = cmd_across_all_procs(self._frontend_per_proc, 'command', cmd) 118 | 119 | return check_command(results) 120 | 121 | def metric(self, name): 122 | """Return the value of a metric. 123 | 124 | Performs a calculation on the metric across all HAProxy processes. 125 | The type of calculation is either sum or avg and defined in 126 | :data:`haproxyadmin.utils.METRICS_SUM` and 127 | :data:`haproxyadmin.utils.METRICS_AVG`. 128 | 129 | :param name: metric name to retrieve 130 | :type name: any of :data:`haproxyadmin.haproxy.FRONTEND_METRICS` 131 | :return: value of the metric 132 | :rtype: ``integer`` 133 | :raise: ``ValueError`` when a given metric is not found 134 | """ 135 | if name not in FRONTEND_METRICS: 136 | raise ValueError("{} is not valid metric".format(name)) 137 | 138 | metrics = [x.metric(name) for x in self._frontend_per_proc] 139 | metrics[:] = (converter(x) for x in metrics) 140 | metrics[:] = (x for x in metrics if x is not None) 141 | 142 | return calculate(name, metrics) 143 | 144 | @property 145 | def maxconn(self): 146 | """Return the configured maximum connection allowed for frontend. 147 | 148 | :rtype: ``integer`` 149 | """ 150 | return self.metric('slim') 151 | 152 | @should_die 153 | def setmaxconn(self, value): 154 | """Set maximum connection to the frontend. 155 | 156 | :param die: control the handling of errors. 157 | :type die: ``bool`` 158 | :param value: max connection value. 159 | :type value: ``integer`` 160 | :return: ``True`` if value was set. 161 | :rtype: ``bool`` 162 | :raise: If ``die`` is ``True`` 163 | :class:`haproxyadmin.exceptions.CommandFailed` or 164 | :class:`haproxyadmin.exceptions.MultipleCommandResults` is raised 165 | when something bad happens otherwise returns ``False``. 166 | 167 | Usage:: 168 | 169 | >>> from haproxyadmin import haproxy 170 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 171 | >>> frontend = hap.frontend('frontend1_proc34') 172 | >>> frontend.maxconn 173 | >>> frontend.setmaxconn(50000) 174 | True 175 | >>> frontend.maxconn 176 | 100000 177 | """ 178 | if not isinstance(value, int): 179 | raise ValueError("Expected integer and got {}".format(type(value))) 180 | 181 | cmd = "set maxconn frontend {} {}".format(self.name, value) 182 | results = cmd_across_all_procs(self._frontend_per_proc, 'command', cmd) 183 | 184 | return check_command(results) 185 | 186 | @property 187 | def name(self): 188 | """Return the name of the frontend. 189 | 190 | :rtype: ``string`` 191 | """ 192 | return self._name 193 | 194 | @property 195 | def process_nb(self): 196 | """Return a list of process number in which frontend is configured. 197 | 198 | :rtype: ``list`` 199 | 200 | Usage:: 201 | 202 | >>> from haproxyadmin import haproxy 203 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 204 | >>> frontend = hap.frontend('frontend2_proc34') 205 | >>> frontend.process_nb 206 | [4, 3] 207 | """ 208 | process_numbers = [] 209 | for frontend in self._frontend_per_proc: 210 | process_numbers.append(frontend.process_nb) 211 | 212 | return process_numbers 213 | 214 | @property 215 | def requests(self): 216 | """Return the number of requests. 217 | 218 | :rtype: ``integer`` 219 | 220 | Usage:: 221 | 222 | >>> from haproxyadmin import haproxy 223 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 224 | >>> frontend = hap.frontend('frontend2_proc34') 225 | >>> frontend.requests 226 | 5 227 | """ 228 | return self.metric('req_tot') 229 | 230 | def requests_per_process(self): 231 | """Return the number of requests for the frontend per process. 232 | 233 | :return: a list of tuples with 2 elements 234 | 235 | #. process number of HAProxy 236 | #. requests 237 | 238 | :rtype: ``list`` 239 | 240 | Usage:: 241 | 242 | >>> from haproxyadmin import haproxy 243 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 244 | >>> frontend = hap.frontend('frontend2_proc34') 245 | >>> frontend.requests_per_process() 246 | [(4, 2), (3, 3)] 247 | """ 248 | results = cmd_across_all_procs(self._frontend_per_proc, 'metric', 249 | 'req_tot') 250 | 251 | return results 252 | 253 | @should_die 254 | def shutdown(self): 255 | """Disable the frontend. 256 | 257 | .. warning:: 258 | HAProxy removes from the running configuration a frontend, so 259 | further operations on the frontend will return an error. 260 | 261 | :rtype: ``bool`` 262 | """ 263 | cmd = "shutdown frontend {}".format(self.name) 264 | results = cmd_across_all_procs(self._frontend_per_proc, 'command', cmd) 265 | 266 | return check_command(results) 267 | 268 | def stats_per_process(self): 269 | """Return all stats of the frontend per process. 270 | 271 | :return: a list of tuples with 2 elements 272 | 273 | #. process number 274 | #. a dict with all stats 275 | 276 | :rtype: ``list`` 277 | 278 | """ 279 | results = cmd_across_all_procs(self._frontend_per_proc, 'stats') 280 | 281 | return results 282 | 283 | @property 284 | def status(self): 285 | """Return the status of the frontend. 286 | 287 | :rtype: ``string`` 288 | :raise: :class:`IncosistentData` exception if status is different 289 | per process 290 | 291 | Usage:: 292 | 293 | >>> from haproxyadmin import haproxy 294 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 295 | >>> frontend = hap.frontend('frontend2_proc34') 296 | >>> frontend.status 297 | 'OPEN' 298 | """ 299 | results = cmd_across_all_procs(self._frontend_per_proc, 'metric', 300 | 'status') 301 | 302 | return compare_values(results) 303 | -------------------------------------------------------------------------------- /haproxyadmin/haproxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # 2 | # pylint: disable=superfluous-parens 3 | # 4 | 5 | """ 6 | haproxyadmin.haproxy 7 | ~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module implements the main haproxyadmin API. 10 | 11 | """ 12 | import os 13 | import glob 14 | 15 | from haproxyadmin.frontend import Frontend 16 | from haproxyadmin.backend import Backend 17 | from haproxyadmin.utils import (is_unix_socket, cmd_across_all_procs, converter, 18 | calculate, isint, should_die, check_command, 19 | check_output, compare_values, connected_socket) 20 | from haproxyadmin.internal.haproxy import _HAProxyProcess 21 | from haproxyadmin.exceptions import CommandFailed 22 | 23 | 24 | HAPROXY_METRICS = [ 25 | 'SslFrontendMaxKeyRate', 26 | 'Hard_maxconn', 27 | 'SessRateLimit', 28 | 'Process_num', 29 | 'Memmax_MB', 30 | 'CompressBpsRateLim', 31 | 'MaxSslConns', 32 | 'ConnRateLimit', 33 | 'SslRateLimit', 34 | 'MaxConnRate', 35 | 'CumConns', 36 | 'SslBackendKeyRate', 37 | 'SslCacheLookups', 38 | 'CurrSslConns', 39 | 'Run_queue', 40 | 'Maxpipes', 41 | 'Idle_pct', 42 | 'SslFrontendKeyRate', 43 | 'Tasks', 44 | 'MaxZlibMemUsage', 45 | 'SslFrontendSessionReuse_pct', 46 | 'CurrConns', 47 | 'SslCacheMisses', 48 | 'SslRate', 49 | 'CumSslConns', 50 | 'PipesUsed', 51 | 'Maxconn', 52 | 'CompressBpsIn', 53 | 'ConnRate', 54 | 'Ulimit-n', 55 | 'SessRate', 56 | 'SslBackendMaxKeyRate', 57 | 'CumReq', 58 | 'PipesFree', 59 | 'ZlibMemUsage', 60 | 'Uptime_sec', 61 | 'CompressBpsOut', 62 | 'Maxsock', 63 | 'MaxSslRate', 64 | 'MaxSessRate', 65 | ] 66 | 67 | 68 | class HAProxy(object): 69 | """Build a user-created :class:`HAProxy` object for HAProxy. 70 | 71 | This is the main class to interact with HAProxy and provides methods 72 | to create objects for managing frontends, backends and servers. It also 73 | provides an interface to interact with HAProxy as a way to 74 | retrieve settings/statistics but also change settings. 75 | 76 | ACLs and MAPs are also managed by :class:`HAProxy` class. 77 | 78 | :param socket_dir: a directory with HAProxy stats files. 79 | :type socket_dir: ``string`` 80 | :param socket_file: absolute path of HAProxy stats file. 81 | :type socket_file: ``string`` 82 | :param retry: number of times to retry to open a UNIX socket 83 | connection after a failure occurred, possible values 84 | 85 | - None => don't retry 86 | - 0 => retry indefinitely 87 | - 1..N => times to retry 88 | 89 | :type retry: ``integer`` or ``None`` 90 | :param retry_interval: sleep time between the retries. 91 | :type retry_interval: ``integer`` 92 | :param timeout: timeout for the connection 93 | :type timeout: ``float`` 94 | :return: a user-created :class:`HAProxy` object. 95 | :rtype: :class:`HAProxy` 96 | """ 97 | 98 | def __init__(self, 99 | socket_dir=None, 100 | socket_file=None, 101 | retry=2, 102 | retry_interval=2, 103 | timeout=1, 104 | ): 105 | 106 | self._hap_processes = [] 107 | socket_files = [] 108 | 109 | if socket_dir: 110 | if not os.path.exists(socket_dir): 111 | raise ValueError("socket directory does not exist " 112 | "{}".format(socket_dir)) 113 | 114 | for _file in glob.glob(os.path.join(socket_dir, '*')): 115 | if is_unix_socket(_file) and connected_socket(_file, timeout): 116 | socket_files.append(_file) 117 | elif (socket_file and not os.path.exists(socket_file)): 118 | raise ValueError("{} UNIX socket file was not found".format(socket_file)) 119 | elif (socket_file and os.path.exists(socket_file) and is_unix_socket(socket_file) and 120 | connected_socket(socket_file, timeout)): 121 | socket_files.append(os.path.realpath(socket_file)) 122 | else: 123 | raise ValueError("UNIX socket file was not set") 124 | 125 | if not socket_files: 126 | raise ValueError("No valid UNIX socket file was found, directory: " 127 | "{} file: {}".format(socket_dir, socket_file)) 128 | 129 | for so_file in socket_files: 130 | self._hap_processes.append( 131 | _HAProxyProcess( 132 | socket_file=so_file, 133 | retry=retry, 134 | retry_interval=retry_interval, 135 | timeout=timeout 136 | ) 137 | ) 138 | 139 | @should_die 140 | def add_acl(self, acl, pattern): 141 | """Add an entry into the acl. 142 | 143 | :param acl: acl id or a file. 144 | :type acl: ``integer`` or a file path passed as ``string`` 145 | :param pattern: entry to add. 146 | :type pattern: ``string`` 147 | :return: ``True`` if command succeeds otherwise ``False`` 148 | :rtype: ``bool`` 149 | 150 | Usage:: 151 | 152 | >>> from haproxyadmin import haproxy 153 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 154 | >>> hap.show_acl(acl=4) 155 | ['0x23181c0 /static/css/'] 156 | >>> hap.add_acl(acl=4, pattern='/foo/' ) 157 | True 158 | >>> hap.show_acl(acl=4) 159 | ['0x23181c0 /static/css/', '0x238f790 /foo/'] 160 | """ 161 | if isint(acl): 162 | cmd = "add acl #{} {}".format(acl, pattern) 163 | else: 164 | cmd = "add acl {} {}".format(acl, pattern) 165 | 166 | results = cmd_across_all_procs(self._hap_processes, 'command', 167 | cmd) 168 | 169 | return check_command(results) 170 | 171 | @should_die 172 | def add_map(self, mapid, key, value): 173 | """Add an entry into the map. 174 | 175 | :param mapid: map id or a file. 176 | :type mapid: ``integer`` or a file path passed as ``string`` 177 | :param key: key to add. 178 | :type key: ``string`` 179 | :param value: Value assciated to the key. 180 | :type value: ``string`` 181 | :return: ``True`` if command succeeds otherwise ``False``. 182 | :rtype: ``bool`` 183 | 184 | Usage:: 185 | 186 | >>> from haproxyadmin import haproxy 187 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 188 | >>> hap.show_map(0) 189 | ['0x1a78b20 1 www.foo.com-1'] 190 | >>> hap.add_map(0, '9', 'foo') 191 | True 192 | >>> hap.show_map(0) 193 | ['0x1a78b20 1 www.foo.com-1', '0x1b15c80 9 foo'] 194 | """ 195 | if isint(mapid): 196 | cmd = "add map #{} {} {}".format(mapid, key, value) 197 | else: 198 | cmd = "add map {} {} {}".format(mapid, key, value) 199 | 200 | results = cmd_across_all_procs(self._hap_processes, 'command', 201 | cmd) 202 | 203 | return check_command(results) 204 | 205 | @should_die 206 | def clear_acl(self, acl): 207 | """Remove all entries from a acl. 208 | 209 | :param acl: acl id or a file. 210 | :type acl: ``integer`` or a file path passed as ``string`` 211 | :return: True if command succeeds otherwise False 212 | :rtype: bool 213 | 214 | Usage:: 215 | 216 | >>> from haproxyadmin import haproxy 217 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 218 | >>> hap.clear_acl(acl=4) 219 | True 220 | >>> hap.clear_acl(acl='/etc/haproxy/bl_frontend') 221 | True 222 | """ 223 | if isint(acl): 224 | cmd = "clear acl #{}".format(acl) 225 | else: 226 | cmd = "clear acl {}".format(acl) 227 | 228 | results = cmd_across_all_procs(self._hap_processes, 'command', 229 | cmd) 230 | 231 | return check_command(results) 232 | 233 | @should_die 234 | def clear_map(self, mapid): 235 | """Remove all entries from a mapid. 236 | 237 | :param mapid: map id or a file 238 | :type mapid: ``integer`` or a file path passed as ``string`` 239 | :return: ``True`` if command succeeds otherwise ``False`` 240 | :rtype: ``bool`` 241 | 242 | Usage:: 243 | 244 | >>> from haproxyadmin import haproxy 245 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 246 | >>> hap.clear_map(0) 247 | True 248 | >>> hap.clear_map(mapid='/etc/haproxy/bl_frontend') 249 | True 250 | """ 251 | if isint(mapid): 252 | cmd = "clear map #{}".format(mapid) 253 | else: 254 | cmd = "clear map {}".format(mapid) 255 | 256 | results = cmd_across_all_procs(self._hap_processes, 'command', 257 | cmd) 258 | 259 | return check_command(results) 260 | 261 | @should_die 262 | def clearcounters(self, all=False): 263 | """Clear the max values of the statistics counters. 264 | 265 | When ``all`` is set to ``True`` clears all statistics counters in 266 | each proxy (frontend & backend) and in each server. This has the same 267 | effect as restarting. 268 | 269 | :param all: (optional) clear all statistics counters. 270 | :type all: ``bool`` 271 | :return: ``True`` if command succeeds otherwise ``False``. 272 | :rtype: ``bool`` 273 | """ 274 | if all: 275 | cmd = "clear counters all" 276 | else: 277 | cmd = "clear counters" 278 | 279 | results = cmd_across_all_procs(self._hap_processes, 'command', 280 | cmd) 281 | 282 | return check_command(results) 283 | 284 | @property 285 | def totalrequests(self): 286 | """Return total cumulative number of requests processed by all processes. 287 | 288 | :rtype: ``integer`` 289 | 290 | .. note:: 291 | This is the total number of requests that are processed by HAProxy. 292 | It counts requests for frontends and backends. Don't forget that 293 | a single client request passes HAProxy twice. 294 | 295 | Usage:: 296 | 297 | >>> from haproxyadmin import haproxy 298 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 299 | >>> hap.totalrequests 300 | 457 301 | """ 302 | return self.metric('CumReq') 303 | 304 | @property 305 | def processids(self): 306 | """Return the process IDs of all HAProxy processes. 307 | 308 | :rtype: ``list`` 309 | 310 | Usage:: 311 | 312 | >>> from haproxyadmin import haproxy 313 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 314 | >>> hap.processids 315 | [22029, 22028, 22027, 22026] 316 | """ 317 | return [x.metric('Pid') for x in self._hap_processes] 318 | 319 | @should_die 320 | def del_acl(self, acl, key): 321 | """Delete all the acl entries from the acl corresponding to the key. 322 | 323 | :param acl: acl id or a file 324 | :type acl: ``integer`` or a file path passed as ``string`` 325 | :param key: key to delete. 326 | :type key: ``string`` 327 | :return: ``True`` if command succeeds otherwise ``False``. 328 | :rtype: ``bool`` 329 | 330 | Usage:: 331 | 332 | >>> from haproxyadmin import haproxy 333 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 334 | >>> hap.show_acl(acl=4) 335 | ['0x23181c0 /static/css/', '0x238f790 /foo/', '0x238f810 /bar/'] 336 | >>> hap.del_acl(acl=4, key='/static/css/') 337 | True 338 | >>> hap.show_acl(acl=4) 339 | ['0x238f790 /foo/', '0x238f810 /bar/'] 340 | >>> hap.del_acl(acl=4, key='0x238f790') 341 | True 342 | >>> hap.show_acl(acl=4) 343 | ['0x238f810 /bar/'] 344 | """ 345 | if key.startswith('0x'): 346 | key = "#{}".format(key) 347 | 348 | if isint(acl): 349 | cmd = "del acl #{} {}".format(acl, key) 350 | else: 351 | cmd = "del acl {} {}".format(acl, key) 352 | 353 | results = cmd_across_all_procs(self._hap_processes, 'command', 354 | cmd) 355 | 356 | return check_command(results) 357 | 358 | @should_die 359 | def del_map(self, mapid, key): 360 | """Delete all the map entries from the map corresponding to the key. 361 | 362 | :param mapid: map id or a file. 363 | :type mapid: ``integer`` or a file path passed as ``string``. 364 | :param key: key to delete 365 | :type key: ``string`` 366 | :return: ``True`` if command succeeds otherwise ``False``. 367 | :rtype: ``bool`` 368 | 369 | Usage:: 370 | 371 | >>> from haproxyadmin import haproxy 372 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 373 | >>> hap.show_map(0) 374 | ['0x1b15cd0 9 foo', '0x1a78980 11 bar'] 375 | >>> hap.del_map(0, '0x1b15cd0') 376 | True 377 | >>> hap.show_map(0) 378 | ['0x1a78980 11 bar'] 379 | >>> hap.add_map(0, '22', 'bar22') 380 | True 381 | >>> hap.show_map(0) 382 | ['0x1a78980 11 bar', '0x1b15c00 22 bar22'] 383 | >>> hap.del_map(0, '22') 384 | True 385 | >>> hap.show_map(0) 386 | ['0x1a78980 11 bar'] 387 | """ 388 | if key.startswith('0x'): 389 | key = "#{}".format(key) 390 | 391 | if isint(mapid): 392 | cmd = "del map #{} {}".format(mapid, key) 393 | else: 394 | cmd = "del map {} {}".format(mapid, key) 395 | 396 | results = cmd_across_all_procs(self._hap_processes, 'command', 397 | cmd) 398 | 399 | return check_command(results) 400 | 401 | @should_die 402 | def errors(self, iid=None): 403 | """Dump last known request and response errors. 404 | 405 | If is specified, the limit the dump to errors concerning 406 | either frontend or backend whose ID is . 407 | 408 | :param iid: (optional) ID of frontend or backend. 409 | :type iid: integer 410 | :return: A list of tuples of errors per process. 411 | 412 | #. process number 413 | #. ``list`` of errors 414 | 415 | :rtype: ``list`` 416 | """ 417 | if iid: 418 | cmd = "show errors {}".format(iid) 419 | else: 420 | cmd = "show errors" 421 | 422 | return cmd_across_all_procs(self._hap_processes, 'command', 423 | cmd, full_output=True) 424 | 425 | def frontends(self, name=None): 426 | """Build a list of :class:`Frontend ` 427 | 428 | :param name: (optional) frontend name to look up. 429 | :type name: ``string`` 430 | :return: list of :class:`Frontend `. 431 | :rtype: ``list`` 432 | """ 433 | return_list = [] 434 | 435 | # store _Frontend objects for each frontend per haproxy process. 436 | # key: name of the frontend 437 | # value: a list of _Frontend objects 438 | frontends_across_hap_processes = {} 439 | 440 | # loop over all haproxy processes and get a list of frontend objects 441 | for haproxy in self._hap_processes: 442 | for frontend in haproxy.frontends(name): 443 | if frontend.name not in frontends_across_hap_processes: 444 | frontends_across_hap_processes[frontend.name] = [] 445 | frontends_across_hap_processes[frontend.name].append(frontend) 446 | 447 | # build the returned list 448 | for value in frontends_across_hap_processes.values(): 449 | return_list.append(Frontend(value)) 450 | 451 | return return_list 452 | 453 | def frontend(self, name): 454 | """Build a :class:`Frontend ` object. 455 | 456 | :param name: frontend name to look up. 457 | :type name: ``string`` 458 | :return: a :class:`Frontend ` object 459 | for the frontend. 460 | :rtype: :class:`Frontend ` 461 | :raises: :class::`ValueError` when frontend isn't found or more than 1 462 | frontend is found. 463 | """ 464 | _frontend = self.frontends(name) 465 | if len(_frontend) == 1: 466 | return _frontend[0] 467 | elif len(_frontend) == 0: 468 | raise ValueError("Could not find frontend") 469 | else: 470 | raise ValueError("Found more than one frontend!") 471 | 472 | @should_die 473 | def get_acl(self, acl, value): 474 | """Lookup the value in the ACL. 475 | 476 | :param acl: acl id or a file. 477 | :type acl: ``integer`` or a file path passed as ``string`` 478 | :param value: value to lookup 479 | :type value: ``string`` 480 | :return: matching patterns associated with ACL. 481 | :rtype: ``string`` 482 | 483 | Usage:: 484 | 485 | >>> from haproxyadmin import haproxy 486 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 487 | >>> hap.show_acl(acl=4) 488 | ['0x2318120 /static/js/', '0x23181c0 /static/css/'] 489 | >>> hap.get_acl(acl=4, value='/foo') 490 | 'type=beg, case=sensitive, match=no' 491 | >>> hap.get_acl(acl=4, value='/static/js/') 492 | 'type=beg, case=sensitive, match=yes, idx=tree, pattern="/static/js/"' 493 | """ 494 | if isint(acl): 495 | cmd = "get acl #{} {}".format(acl, value) 496 | else: 497 | cmd = "get acl {} {}".format(acl, value) 498 | 499 | get_results = cmd_across_all_procs(self._hap_processes, 'command', cmd) 500 | get_info_proc1 = get_results[0][1] 501 | if not check_output(get_info_proc1): 502 | raise ValueError(get_info_proc1) 503 | 504 | return get_info_proc1 505 | 506 | @should_die 507 | def get_map(self, mapid, value): 508 | """Lookup the value in the map. 509 | 510 | :param mapid: map id or a file. 511 | :type mapid: ``integer`` or a file path passed as ``string`` 512 | :param value: value to lookup. 513 | :type value: ``string`` 514 | :return: matching patterns associated with map. 515 | :rtype: ``string`` 516 | 517 | Usage:: 518 | 519 | >>> from haproxyadmin import haproxy 520 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 521 | >>> hap.show_map(0) 522 | ['0x1a78980 11 new2', '0x1b15c00 22 0'] 523 | >>> hap.get_map(0, '11') 524 | 'type=str, case=sensitive, found=yes, idx=tree, key="11", value="new2", type="str"' 525 | >>> hap.get_map(0, '10') 526 | 'type=str, case=sensitive, found=no' 527 | """ 528 | if isint(mapid): 529 | cmd = "get map #{} {}".format(mapid, value) 530 | else: 531 | cmd = "get map {} {}".format(mapid, value) 532 | 533 | get_results = cmd_across_all_procs(self._hap_processes, 'command', 534 | cmd) 535 | get_info_proc1 = get_results[0][1] 536 | if not check_output(get_info_proc1): 537 | raise CommandFailed(get_info_proc1[0]) 538 | 539 | return get_info_proc1 540 | 541 | def info(self): 542 | """Dump info about haproxy stats on current process. 543 | 544 | :return: A list of ``dict`` for each process. 545 | :rtype: ``list`` 546 | """ 547 | return_list = [] 548 | 549 | for haproxy in self._hap_processes: 550 | return_list.append(haproxy.proc_info()) 551 | 552 | return return_list 553 | 554 | @property 555 | def maxconn(self): 556 | """Return the sum of configured maximum connection allowed for HAProxy. 557 | 558 | :rtype: ``integer`` 559 | """ 560 | return self.metric('Maxconn') 561 | 562 | def server(self, hostname, backend=None): 563 | """Build :class:`Server for a server.` 564 | objects for the given server. 565 | 566 | If ``backend`` specified then lookup is limited to that backend. 567 | 568 | .. note:: 569 | If a server is member of more than 1 backend then muliple objects 570 | for the same server is returned. 571 | 572 | :param hostname: servername to look for. 573 | :type hostname: ``string`` 574 | :param backend: (optional) backend name to look in. 575 | :type backend: ``string`` 576 | :return: a list of :class:`Server ` 577 | objects. 578 | :rtype: ``list`` 579 | """ 580 | ret = [] 581 | for backend in self.backends(backend): 582 | try: 583 | ret.append(backend.server(hostname)) 584 | except ValueError: 585 | # lookup for an nonexistent server in backend raise VauleError 586 | # catch and pass as we query all backends 587 | pass 588 | 589 | if not ret: 590 | raise ValueError("Could not find server") 591 | 592 | return ret 593 | 594 | def servers(self, backend=None): 595 | """Build :class:`Server ` for each server. 596 | 597 | If ``backend`` specified then lookup is limited to that backend. 598 | 599 | :param backend: (optional) backend name. 600 | :type backend: ``string`` 601 | :return: A list of :class:`Server ` objects 602 | :rtype: ``list``. 603 | """ 604 | return_list = [] 605 | for backend in self.backends(backend): 606 | servers = backend.servers() 607 | return_list += servers 608 | 609 | return return_list 610 | 611 | def metric(self, name): 612 | """Return the value of a metric. 613 | 614 | Performs a calculation on the metric across all HAProxy processes. 615 | The type of calculation is either sum or avg and defined in 616 | :data:`haproxyadmin.utils.METRICS_SUM` and 617 | :data:`haproxyadmin.utils.METRICS_AVG`. 618 | 619 | :param name: metric name to retrieve 620 | :type name: any of :data:`haproxyadmin.haproxy.HAPROXY_METRICS` 621 | :return: value of the metric 622 | :rtype: ``integer`` 623 | :raise: ``ValueError`` when a given metric is not found 624 | """ 625 | if name not in HAPROXY_METRICS: 626 | raise ValueError("{} is not valid metric".format(name)) 627 | 628 | metrics = [x.metric(name) for x in self._hap_processes] 629 | metrics[:] = (converter(x) for x in metrics) 630 | metrics[:] = (x for x in metrics if x is not None) 631 | 632 | return calculate(name, metrics) 633 | 634 | def backends(self, name=None): 635 | """Build a list of :class:`Backend ` 636 | 637 | :param name: (optional) backend name to look up. 638 | :type name: ``string`` 639 | :return: list of :class:`Backend `. 640 | :rtype: ``list`` 641 | """ 642 | return_list = [] 643 | 644 | # store _Backend objects for each backend per haproxy process. 645 | # key: name of the backend 646 | # value: a list of _Backend objects 647 | backends_across_hap_processes = {} 648 | 649 | # loop over all HAProxy processes and get a set of backends 650 | for hap_process in self._hap_processes: 651 | # Returns object _Backend 652 | for backend in hap_process.backends(name): 653 | if backend.name not in backends_across_hap_processes: 654 | backends_across_hap_processes[backend.name] = [] 655 | backends_across_hap_processes[backend.name].append(backend) 656 | 657 | # build the returned list 658 | for backend_obj in backends_across_hap_processes.values(): 659 | return_list.append(Backend(backend_obj)) 660 | 661 | return return_list 662 | 663 | def backend(self, name): 664 | """Build a :class:`Backend ` object. 665 | 666 | :param name: backend name to look up. 667 | :type name: ``string`` 668 | :raises: :class::`ValueError` when backend isn't found or more than 1 669 | backend is found. 670 | """ 671 | _backend = self.backends(name) 672 | if len(_backend) == 1: 673 | return _backend[0] 674 | elif len(_backend) == 0: 675 | raise ValueError("Could not find backend") 676 | else: 677 | raise ValueError("Found more than one backend!") 678 | 679 | @property 680 | def ratelimitconn(self): 681 | """Return the process-wide connection rate limit.""" 682 | return self.metric('ConnRateLimit') 683 | 684 | @property 685 | def ratelimitsess(self): 686 | """Return the process-wide session rate limit.""" 687 | return self.metric('SessRateLimit') 688 | 689 | @property 690 | def ratelimitsslsess(self): 691 | """Return the process-wide ssl session rate limit.""" 692 | return self.metric('SslRateLimit') 693 | 694 | @property 695 | def requests(self): 696 | """Return total requests processed by all frontends. 697 | 698 | :rtype: ``integer`` 699 | 700 | Usage:: 701 | 702 | >>> from haproxyadmin import haproxy 703 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 704 | >>> hap.requests 705 | 457 706 | """ 707 | return sum([x.requests for x in self.frontends()]) 708 | 709 | @should_die 710 | def set_map(self, mapid, key, value): 711 | """Modify the value corresponding to each key in a map. 712 | 713 | mapid is the # or returned by 714 | :func:`show_map `. 715 | 716 | :param mapid: map id or a file. 717 | :type mapid: ``integer`` or a file path passed as ``string`` 718 | :param key: key id 719 | :type key: ``string`` 720 | :param value: value to set for the key. 721 | :type value: ``string`` 722 | :return: ``True`` if command succeeds otherwise ``False``. 723 | :rtype: ``bool`` 724 | 725 | Usage:: 726 | 727 | >>> from haproxyadmin import haproxy 728 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 729 | >>> hap.show_map(0) 730 | ['0x1a78980 11 9', '0x1b15c00 22 0'] 731 | >>> hap.set_map(0, '11', 'new') 732 | True 733 | >>> hap.show_map(0) 734 | ['0x1a78980 11 new', '0x1b15c00 22 0'] 735 | >>> hap.set_map(0, '0x1a78980', 'new2') 736 | True 737 | >>> hap.show_map(0) 738 | ['0x1a78980 11 new2', '0x1b15c00 22 0'] 739 | """ 740 | if key.startswith('0x'): 741 | key = "#{}".format(key) 742 | 743 | if isint(mapid): 744 | cmd = "set map #{} {} {}".format(mapid, key, value) 745 | else: 746 | cmd = "set map {} {} {}".format(mapid, key, value) 747 | 748 | results = cmd_across_all_procs(self._hap_processes, 'command', cmd) 749 | 750 | return check_command(results) 751 | 752 | @should_die 753 | def command(self, cmd): 754 | """Send a command to haproxy process. 755 | 756 | This allows a user to send any kind of command to 757 | haproxy. We **do not* perfom any sanitization on input 758 | and on output. 759 | 760 | :param cmd: a command to send to haproxy process. 761 | :type cmd: ``string`` 762 | :return: list of 2-item tuple 763 | 764 | #. HAProxy process number 765 | #. what the method returned 766 | 767 | :rtype: ``list`` 768 | 769 | Usage:: 770 | 771 | >>> from haproxyadmin import haproxy 772 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 773 | >>> hap.command('show stats') 774 | ['0x23181c0 /static/css/'] 775 | >>> hap.add_acl(acl=4, pattern='/foo/' ) 776 | True 777 | >>> hap.show_acl(acl=4) 778 | ['0x23181c0 /static/css/', '0x238f790 /foo/'] 779 | """ 780 | return cmd_across_all_procs(self._hap_processes, 781 | 'command', cmd, full_output=True) 782 | 783 | @should_die 784 | def setmaxconn(self, value): 785 | """Set maximum connection to the frontend. 786 | 787 | :param value: value to set. 788 | :type value: ``integer`` 789 | :return: ``True`` if command succeeds otherwise ``False``. 790 | :rtype: ``bool`` 791 | 792 | Usage: 793 | 794 | >>> from haproxyadmin import haproxy 795 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 796 | >>> hap.setmaxconn(5555) 797 | True 798 | """ 799 | if not isinstance(value, int): 800 | raise ValueError("Expected integer and got {}".format(type(value))) 801 | cmd = "set maxconn global {}".format(value) 802 | 803 | results = cmd_across_all_procs(self._hap_processes, 'command', cmd) 804 | 805 | return check_command(results) 806 | 807 | @should_die 808 | def setratelimitconn(self, value): 809 | """Set process-wide connection rate limit. 810 | 811 | :param value: rate connection limit. 812 | :type value: ``integer`` 813 | :return: ``True`` if command succeeds otherwise ``False``. 814 | :rtype: ``bool`` 815 | :raises: ``ValueError`` if value is not an ``integer``. 816 | """ 817 | if not isinstance(value, int): 818 | raise ValueError("Expected integer and got {}".format(type(value))) 819 | cmd = "set rate-limit connections global {}".format(value) 820 | 821 | results = cmd_across_all_procs(self._hap_processes, 'command', cmd) 822 | 823 | return check_command(results) 824 | 825 | @should_die 826 | def setratelimitsess(self, value): 827 | """Set process-wide session rate limit. 828 | 829 | :param value: rate session limit. 830 | :type value: ``integer`` 831 | :return: ``True`` if command succeeds otherwise ``False``. 832 | :rtype: ``bool`` 833 | :raises: ``ValueError`` if value is not an ``integer``. 834 | """ 835 | if not isinstance(value, int): 836 | raise ValueError("Expected integer and got {}".format(type(value))) 837 | cmd = "set rate-limit sessions global {}".format(value) 838 | 839 | results = cmd_across_all_procs(self._hap_processes, 'command', cmd) 840 | 841 | return check_command(results) 842 | 843 | @should_die 844 | def setratelimitsslsess(self, value): 845 | """Set process-wide ssl session rate limit. 846 | 847 | :param value: rate ssl session limit. 848 | :type value: ``integer`` 849 | :return: ``True`` if command succeeds otherwise ``False``. 850 | :rtype: ``bool`` 851 | :raises: ``ValueError`` if value is not an ``integer``. 852 | """ 853 | if not isinstance(value, int): 854 | raise ValueError("Expected integer and got {}".format(type(value))) 855 | cmd = "set rate-limit ssl-sessions global {}".format(value) 856 | 857 | results = cmd_across_all_procs(self._hap_processes, 'command', cmd) 858 | 859 | return check_command(results) 860 | 861 | @should_die 862 | def show_acl(self, aclid=None): 863 | """Dump info about acls. 864 | 865 | Without argument, the list of all available acls is returned. 866 | If a aclid is specified, its contents are dumped. 867 | 868 | :param aclid: (optional) acl id or a file 869 | :type aclid: ``integer`` or a file path passed as ``string`` 870 | :return: a list with the acls 871 | :rtype: ``list`` 872 | 873 | Usage:: 874 | 875 | >>> from haproxyadmin import haproxy 876 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 877 | >>> hap.show_acl(aclid=6) 878 | ['0x1d09730 ver%3A27%3Bvar%3A0'] 879 | >>> hap.show_acl() 880 | ['# id (file) description', 881 | "1 () acl 'ssl_fc' file '/etc/haproxy/haproxy.cfg' line 83", 882 | "2 () acl 'src' file '/etc/haproxy/haproxy.cfg' line 95", 883 | "3 () acl 'path_beg' file '/etc/haproxy/haproxy.cfg' line 97", 884 | ] 885 | """ 886 | if aclid is not None: 887 | if isint(aclid): 888 | cmd = "show acl #{}".format(aclid) 889 | else: 890 | cmd = "show acl {}".format(aclid) 891 | else: 892 | cmd = "show acl" 893 | 894 | acl_info = cmd_across_all_procs(self._hap_processes, 'command', 895 | cmd, 896 | full_output=True) 897 | # ACL can't be different per process thus we only return the acl 898 | # content found in 1st process. 899 | acl_info_proc1 = acl_info[0][1] 900 | 901 | if not check_output(acl_info_proc1): 902 | raise CommandFailed(acl_info_proc1[0]) 903 | 904 | if len(acl_info_proc1) == 1 and not acl_info_proc1[0]: 905 | return [] 906 | else: 907 | return acl_info_proc1 908 | 909 | @should_die 910 | def show_map(self, mapid=None): 911 | """Dump info about maps. 912 | 913 | Without argument, the list of all available maps is returned. 914 | If a mapid is specified, its contents are dumped. 915 | 916 | :param mapid: (optional) map id or a file. 917 | :type mapid: ``integer`` or a file path passed as ``string`` 918 | :return: a list with the maps. 919 | :rtype: ``list`` 920 | 921 | Usage:: 922 | 923 | >>> from haproxyadmin import haproxy 924 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 925 | >>> hap.show_map() 926 | ['# id (file) description', 927 | "0 (/etc/haproxy/v-m1-bk) pattern loaded ...... line 82", 928 | ] 929 | >>> hap.show_map(mapid=0) 930 | ['0x1a78ab0 0 www.foo.com-0', '0x1a78b20 1 www.foo.com-1'] 931 | """ 932 | if mapid is not None: 933 | if isint(mapid): 934 | cmd = "show map #{}".format(mapid) 935 | else: 936 | cmd = "show map {}".format(mapid) 937 | else: 938 | cmd = "show map" 939 | map_info = cmd_across_all_procs(self._hap_processes, 'command', 940 | cmd, 941 | full_output=True) 942 | # map can't be different per process thus we only return the map 943 | # content found in 1st process. 944 | map_info_proc1 = map_info[0][1] 945 | 946 | if not check_output(map_info_proc1): 947 | raise CommandFailed(map_info_proc1[0]) 948 | 949 | if len(map_info_proc1) == 1 and not map_info_proc1[0]: 950 | return [] 951 | else: 952 | return map_info_proc1 953 | 954 | @property 955 | def uptime(self): 956 | """Return uptime of HAProxy process 957 | 958 | :rtype: string 959 | 960 | Usage:: 961 | 962 | >>> from haproxyadmin import haproxy 963 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 964 | >>> hap.uptime 965 | '4d 0h16m26s' 966 | """ 967 | values = cmd_across_all_procs(self._hap_processes, 'metric', 968 | 'Uptime') 969 | 970 | # Just return the uptime of the 1st process 971 | return values[0][1] 972 | 973 | @property 974 | def description(self): 975 | """Return description of HAProxy 976 | 977 | :rtype: ``string`` 978 | 979 | Usage:: 980 | 981 | >>> from haproxyadmin import haproxy 982 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 983 | >>> hap.description 984 | 'test' 985 | """ 986 | values = cmd_across_all_procs(self._hap_processes, 'metric', 987 | 'description') 988 | 989 | return compare_values(values) 990 | 991 | @property 992 | def nodename(self): 993 | """Return nodename of HAProxy 994 | 995 | :rtype: ``string`` 996 | 997 | Usage:: 998 | 999 | >>> from haproxyadmin import haproxy 1000 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 1001 | >>> hap.nodename 1002 | 'test.foo.com' 1003 | """ 1004 | values = cmd_across_all_procs(self._hap_processes, 'metric', 1005 | 'node') 1006 | 1007 | return compare_values(values) 1008 | 1009 | @property 1010 | def uptimesec(self): 1011 | """Return uptime of HAProxy process in seconds 1012 | 1013 | :rtype: ``integer`` 1014 | 1015 | Usage:: 1016 | 1017 | >>> from haproxyadmin import haproxy 1018 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 1019 | >>> hap.uptimesec 1020 | 346588 1021 | """ 1022 | values = cmd_across_all_procs(self._hap_processes, 'metric', 1023 | 'Uptime_sec') 1024 | 1025 | # Just return the uptime of the 1st process 1026 | return values[0][1] 1027 | 1028 | @property 1029 | def releasedate(self): 1030 | """Return release date of HAProxy 1031 | 1032 | :rtype: ``string`` 1033 | 1034 | Usage:: 1035 | 1036 | >>> from haproxyadmin import haproxy 1037 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 1038 | >>> hap.releasedate 1039 | '2014/10/31' 1040 | """ 1041 | values = cmd_across_all_procs(self._hap_processes, 'metric', 1042 | 'Release_date') 1043 | 1044 | return compare_values(values) 1045 | 1046 | @property 1047 | def version(self): 1048 | """Return version of HAProxy 1049 | 1050 | :rtype: ``string`` 1051 | 1052 | Usage:: 1053 | >>> from haproxyadmin import haproxy 1054 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 1055 | >>> hap.version 1056 | '1.5.8' 1057 | """ 1058 | # If multiple version of HAProxy share the same socket directory 1059 | # then this wil always raise IncosistentData exception. 1060 | # TODO: Document this on README 1061 | values = cmd_across_all_procs(self._hap_processes, 'metric', 'Version') 1062 | 1063 | return compare_values(values) 1064 | -------------------------------------------------------------------------------- /haproxyadmin/internal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixsurfer/haproxyadmin/3016b9a239ff2d69efcb96cf0dae9020cf3f6cad/haproxyadmin/internal/__init__.py -------------------------------------------------------------------------------- /haproxyadmin/internal/backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.internal.backend 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides a class, which is used within haproxyadmin for creating a 10 | object to work with a backend. This object is associated only with a single 11 | HAProxy process. 12 | 13 | """ 14 | from haproxyadmin.internal.server import _Server 15 | 16 | class _Backend: 17 | """Class for interacting with a backend in one HAProxy process. 18 | 19 | :param hap_process: a :class::`_HAProxyProcess` object. 20 | :param name: backend name. 21 | :type name: ``string`` 22 | :param iid: unique proxy id of the backend. 23 | :type iid: ``integer`` 24 | """ 25 | def __init__(self, hap_process, name, iid): 26 | self.hap_process = hap_process 27 | self._name = name 28 | self.hap_process_nb = self.hap_process.process_nb 29 | self._iid = iid 30 | 31 | @property 32 | def name(self): 33 | """Return a string which is the name of the backend""" 34 | return self._name 35 | 36 | @property 37 | def iid(self): 38 | """Return Proxy ID""" 39 | data = self.stats_data() 40 | self._iid = data.iid 41 | 42 | return self._iid 43 | 44 | @property 45 | def process_nb(self): 46 | """Return the process number of the haproxy process 47 | 48 | :rtype: ``int`` 49 | """ 50 | return int(self.hap_process_nb) 51 | 52 | def stats_data(self): 53 | """Return stats data 54 | 55 | Check documentation of ``stats_data`` method in :class:`_Frontend`. 56 | 57 | :rtype: ``utils.CSVLine`` object 58 | """ 59 | # Fetch data using the last known iid 60 | try: 61 | data = self.hap_process.backends_stats(self._iid)[self.name] 62 | except KeyError: 63 | # A lookup on HAProxy with the current id doesn't return 64 | # an object with our name. 65 | # Most likely object got different id due to a reshuffle in conf. 66 | # Thus retrieve all objects to get latest data for the object. 67 | try: 68 | data = self.hap_process.backends_stats()[self.name] 69 | except KeyError: 70 | # The object has gone from running configuration! 71 | # We cant recover from this situation. 72 | raise 73 | 74 | return data['stats'] 75 | 76 | def stats(self): 77 | """Build dictionary for all statistics reported by HAProxy. 78 | 79 | :return: A dictionary with statistics 80 | :rtype: ``dict`` 81 | """ 82 | data = self.stats_data() 83 | keys = data.heads 84 | values = data.parts 85 | 86 | return dict(zip(keys, values)) 87 | 88 | def metric(self, name): 89 | data = self.stats_data() 90 | 91 | return getattr(data, name) 92 | 93 | def command(self, cmd): 94 | """Send command to HAProxy 95 | 96 | :param cmd: command to send 97 | :type cmd: ``string`` 98 | :return: the output of the command 99 | :rtype: ``string`` 100 | """ 101 | return self.hap_process.command(cmd) 102 | 103 | def servers(self, name=None): 104 | """Return a list of _Server objects for each server of the backend. 105 | 106 | :param name: (optional): server name to lookup, defaults to None. 107 | :type name: ``string`` 108 | """ 109 | servers = [] 110 | return_list = [] 111 | 112 | servers = self.hap_process.servers_stats(self.name, self.iid) 113 | if name is not None: 114 | if name in servers: 115 | return_list.append(_Server(self, 116 | name, 117 | servers[name].sid)) 118 | else: 119 | return [] 120 | else: 121 | for _name in servers: 122 | return_list.append(_Server(self, 123 | _name, 124 | servers[_name].sid)) 125 | 126 | return return_list 127 | -------------------------------------------------------------------------------- /haproxyadmin/internal/frontend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.internal.frontend 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides a class, which is used within haproxyadmin for creating a 10 | object to work with a frontend. This object is associated only with a single 11 | HAProxy process. 12 | 13 | 14 | """ 15 | 16 | 17 | class _Frontend: 18 | """Class for interacting with a frontend in one HAProxy process. 19 | 20 | :param hap_process: a :class:`_HAProxyProcess` object. 21 | :param name: frontend name. 22 | :type name: ``string`` 23 | :param iid: unique proxy id of the frontend. 24 | :type iid: ``integer`` 25 | """ 26 | def __init__(self, hap_process, name, iid): 27 | self.hap_process = hap_process 28 | self._name = name 29 | self.hap_process_nb = self.hap_process.process_nb 30 | self._iid = iid 31 | 32 | @property 33 | def name(self): 34 | """Return a string which is the name of the frontend""" 35 | return self._name 36 | 37 | @property 38 | def iid(self): 39 | """Return Proxy ID""" 40 | data = self.stats_data() 41 | self._iid = data.iid 42 | 43 | return self._iid 44 | 45 | @property 46 | def process_nb(self): 47 | return int(self.hap_process_nb) 48 | 49 | def stats_data(self): 50 | """Return stats data 51 | 52 | :rtype: ``utils.CSVLine`` object 53 | 54 | HAProxy assigns unique ids to each object during the startup. 55 | The id can change when configuration changes, objects order 56 | is reshuffled or additions/removals take place. 57 | In those cases the id we store at the instantiation of the object may 58 | reference to another object or even to non-existent object when 59 | configuration takes places afterwards. 60 | 61 | The technique we use is quite simple. When an object is created 62 | we store the name and the id. In order to detect if iid is changed, 63 | we simply send a request to fetch data only for the given iid and check 64 | if the current id points to an object of the same type 65 | (frontend, backend, server) which has the same name. 66 | """ 67 | # Fetch data using the last known iid 68 | try: 69 | data = self.hap_process.frontends_stats(self._iid)[self.name] 70 | except KeyError: 71 | # A lookup on HAProxy with the current id doesn't return 72 | # an object with our name. 73 | # Most likely object got different id due to a reshuffle in conf. 74 | # Thus retrieve all objects to get latest data for the object. 75 | try: 76 | # This will basically request all object of the type 77 | data = self.hap_process.frontends_stats()[self.name] 78 | except KeyError: 79 | # The object has gone from running configuration! 80 | # This occurs when object was removed from configuration 81 | # and haproxy was reloaded or frontend was shutdowned. 82 | # We cant recover from this situation 83 | raise 84 | 85 | return data 86 | 87 | def stats(self): 88 | """Build dictionary for all statistics reported by HAProxy. 89 | 90 | :return: A dictionary with statistics 91 | :rtype: ``dict`` 92 | 8. split internal to multiple files 93 | """ 94 | data = self.stats_data() 95 | keys = data.heads 96 | values = data.parts 97 | 98 | return dict(zip(keys, values)) 99 | 100 | def metric(self, name): 101 | """Return the value of a metric""" 102 | data = self.stats_data() 103 | 104 | return getattr(data, name) 105 | 106 | def command(self, cmd): 107 | """Run command to HAProxy 108 | 109 | :param cmd: a valid command to execute. 110 | :type cmd: ``string`` 111 | :return: 1st line of the output. 112 | :rtype: ``string`` 113 | """ 114 | return self.hap_process.command(cmd) 115 | -------------------------------------------------------------------------------- /haproxyadmin/internal/haproxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.internal.haproxy 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides the main class that is used within haproxyadmin for creating 10 | object to work with a single HAProxy process. All other internal classes use 11 | this class to send commands to HAProxy process. 12 | 13 | """ 14 | 15 | import socket 16 | import errno 17 | import time 18 | import six 19 | 20 | from haproxyadmin.utils import (info2dict, stat2dict) 21 | from haproxyadmin.exceptions import (SocketTransportError, SocketTimeout, 22 | SocketConnectionError) 23 | from haproxyadmin.internal.frontend import _Frontend 24 | from haproxyadmin.internal.backend import _Backend 25 | 26 | 27 | class _HAProxyProcess: 28 | """An object to a single HAProxy process. 29 | 30 | It acts as a communication pipe between the caller and individual 31 | HAProxy process using UNIX stats socket. 32 | 33 | :param socket_file: Full path of socket file. 34 | :type socket_file: ``string`` 35 | :param retry: (optional) Number of connect retries (defaults to 3) 36 | :type retry: ``integer`` 37 | :param retry_interval: (optional) Interval time in seconds between retries 38 | (defaults to 2) 39 | :param timeout: timeout for the connection 40 | :type timeout: ``float`` 41 | :type retry_interval: ``integer`` 42 | """ 43 | def __init__(self, socket_file, retry=3, retry_interval=2, timeout=1): 44 | self.socket_file = socket_file 45 | self.hap_stats = {} 46 | self.hap_info = {} 47 | self.retry = retry 48 | self.retry_interval = retry_interval 49 | self.timeout = timeout 50 | # process number associated with this object 51 | self.process_nb = self.metric('Process_num') 52 | 53 | def command(self, command, full_output=False): 54 | """Send a command to HAProxy over UNIX stats socket. 55 | 56 | Newline character returned from haproxy is stripped off. 57 | 58 | :param command: A valid command to execute 59 | :type command: string 60 | :param full_output: (optional) Return all output, by default 61 | returns only the 1st line of the output 62 | :type full_output: ``bool`` 63 | :return: 1st line of the output or the whole output as a list 64 | :rtype: ``string`` or ``list`` if full_output is True 65 | """ 66 | data = [] # hold data returned from socket 67 | raised = None # hold possible exception raised during connect phase 68 | attempt = 0 # times to attempt to connect after a connection failure 69 | if self.retry == 0: 70 | # 0 means retry indefinitely 71 | attempt = -1 72 | elif self.retry is None: 73 | # None means don't retry 74 | attempt = 1 75 | else: 76 | # any other value means retry N times 77 | attempt = self.retry + 1 78 | while attempt != 0: 79 | try: 80 | unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 81 | unix_socket.settimeout(self.timeout) 82 | unix_socket.connect(self.socket_file) 83 | unix_socket.send(six.b(command + '\n')) 84 | file_handle = unix_socket.makefile() 85 | data = file_handle.read().splitlines() 86 | except socket.timeout: 87 | raised = SocketTimeout(socket_file=self.socket_file) 88 | except OSError as exc: 89 | # while stress testing HAProxy and querying for all frontend 90 | # metrics I sometimes get: 91 | # OSError: [Errno 106] Transport endpoint is already connected 92 | # catch this one only and reraise it withour exception 93 | if exc.errno == errno.EISCONN: 94 | raised = SocketTransportError(socket_file=self.socket_file) 95 | elif exc.errno == errno.ECONNREFUSED: 96 | raised = SocketConnectionError(self.socket_file) 97 | else: 98 | # for the rest of OSError exceptions just reraise them 99 | raised = exc 100 | else: 101 | # HAProxy always send an empty string at the end 102 | # we remove it as it adds noise for things like ACL/MAP and etc 103 | # We only do that when we get more than 1 line, which only 104 | # happens when we ask for ACL/MAP/etc and not for giving cmds 105 | # such as disable/enable server 106 | if len(data) > 1 and data[-1] == '': 107 | data.pop() 108 | # make sure possible previous errors are cleared 109 | raised = None 110 | # get out from the retry loop 111 | break 112 | finally: 113 | unix_socket.close() 114 | if raised: 115 | time.sleep(self.retry_interval) 116 | 117 | attempt -= 1 118 | 119 | if raised: 120 | raise raised 121 | elif data: 122 | if full_output: 123 | return data 124 | else: 125 | return data[0] 126 | else: 127 | raise ValueError("no data returned from socket {}".format( 128 | self.socket_file)) 129 | 130 | def proc_info(self): 131 | """Return a dictionary containing information about HAProxy daemon. 132 | 133 | :rtype: dictionary, see utils.info2dict() for details 134 | """ 135 | raw_info = self.command('show info', full_output=True) 136 | 137 | return info2dict(raw_info) 138 | 139 | def stats(self, iid=-1, obj_type=-1, sid=-1): 140 | """Return a nested dictionary containing backend information. 141 | 142 | :param iid: unique proxy id, applicable for frontends and backends. 143 | :type iid: ``string`` 144 | :param obj_type: selects the type of dumpable objects 145 | 146 | - 1 for frontends 147 | - 2 for backends 148 | - 4 for servers 149 | - -1 for everything. 150 | 151 | These values can be ORed, for example: 152 | 153 | 1 + 2 = 3 -> frontend + backend. 154 | 1 + 2 + 4 = 7 -> frontend + backend + server. 155 | :type obj_type: ``integer`` 156 | :param sid: a server ID, -1 to dump everything. 157 | :type sid: ``integer`` 158 | :rtype: dict, see ``utils.stat2dict`` for details on the structure 159 | """ 160 | csv_data = self.command('show stat {i} {o} {s}'.format(i=iid, 161 | o=obj_type, 162 | s=sid), 163 | full_output=True) 164 | self.hap_stats = stat2dict(csv_data) 165 | return self.hap_stats 166 | 167 | def metric(self, name): 168 | return self.proc_info()[name] 169 | 170 | def backends_stats(self, iid=-1): 171 | """Build the data structure for backends 172 | 173 | If ``iid`` is set then builds a structure only for the particul 174 | backend. 175 | 176 | :param iid: (optinal) unique proxy id of a backend. 177 | :type iid: ``string`` 178 | :retur: a dictinary with backend information. 179 | :rtype: ``dict`` 180 | """ 181 | return self.stats(iid, obj_type=2)['backends'] 182 | 183 | def frontends_stats(self, iid=-1): 184 | """Build the data structure for frontends 185 | 186 | If ``iid`` is set then builds a structure only for the particular 187 | frontend. 188 | 189 | :param iid: (optinal) unique proxy id of a frontend. 190 | :type iid: ``string`` 191 | :retur: a dictinary with frontend information. 192 | :rtype: ``dict`` 193 | """ 194 | return self.stats(iid, obj_type=1)['frontends'] 195 | 196 | def servers_stats(self, backend, iid=-1, sid=-1): 197 | return self.stats(iid=iid, 198 | obj_type=6, 199 | sid=sid)['backends'][backend]['servers'] 200 | 201 | def backends(self, name=None): 202 | """Build _backend objects for each backend. 203 | 204 | :param name: (optional) backend name, defaults to None 205 | :type name: string 206 | :return: a list of _backend objects for each backend 207 | :rtype: list 208 | """ 209 | backends = [] 210 | return_list = [] 211 | backends = self.backends_stats() 212 | if name is not None: 213 | if name in backends: 214 | return_list.append(_Backend(self, 215 | name, 216 | backends[name]['stats'].iid)) 217 | else: 218 | return return_list 219 | else: 220 | for name in backends: 221 | return_list.append(_Backend(self, 222 | name, 223 | backends[name]['stats'].iid)) 224 | 225 | return return_list 226 | 227 | def frontends(self, name=None): 228 | """Build :class:`_Frontend` objects for each frontend. 229 | 230 | :param name: (optional) backend name, defaults to ``None`` 231 | :type name: ``string`` 232 | :return: a list of :class:`_Frontend` objects for each backend 233 | :rtype: ``list`` 234 | """ 235 | frontends = [] 236 | return_list = [] 237 | frontends = self.frontends_stats() 238 | if name is not None: 239 | if name in frontends: 240 | return_list.append(_Frontend(self, 241 | name, 242 | frontends[name].iid)) 243 | else: 244 | return return_list 245 | else: 246 | for frontend in frontends: 247 | return_list.append(_Frontend(self, 248 | frontend, 249 | frontends[frontend].iid)) 250 | 251 | return return_list 252 | -------------------------------------------------------------------------------- /haproxyadmin/internal/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.internal.server 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides a class, which is used within haproxyadmin for creating a 10 | object to work with a server. This object is associated only with a single 11 | HAProxy process. 12 | 13 | """ 14 | 15 | class _Server: 16 | """Class for interacting with a server of a backend in one HAProxy. 17 | 18 | :param backend: a _Backend object in which server is part of. 19 | :param name: server name. 20 | :type name: ``string`` 21 | :param sid: server id (unique inside a proxy). 22 | :type sid: ``string`` 23 | """ 24 | def __init__(self, backend, name, sid): 25 | self.backend = backend 26 | self._name = name 27 | self.process_nb = self.backend.process_nb 28 | self._sid = sid 29 | 30 | @property 31 | def name(self): 32 | """Return the name of the backend server.""" 33 | return self._name 34 | 35 | @property 36 | def sid(self): 37 | """Return server id""" 38 | data = self.stats_data() 39 | self._sid = data.sid 40 | 41 | return self._sid 42 | 43 | def stats_data(self): 44 | """Return stats data 45 | 46 | Check documentation of ``stats_data`` method in :class:`_Frontend`. 47 | 48 | :rtype: ``utils.CSVLine`` object 49 | """ 50 | # Fetch data using the last known sid 51 | try: 52 | data = self.backend.hap_process.servers_stats( 53 | self.backend.name, self.backend.iid, self._sid)[self.name] 54 | except KeyError: 55 | # A lookup on HAProxy with the current id doesn't return 56 | # an object with our name. 57 | # Most likely object got different id due to a reshuffle in conf. 58 | # Thus retrieve all objects to get latest data for the object. 59 | try: 60 | data = self.backend.hap_process.servers_stats( 61 | self.backend.name)[self.name] 62 | except KeyError: 63 | # The object has gone from running configuration! 64 | # This occurs when object was removed from configuration 65 | # and haproxy was reloaded.We cant recover from this situation. 66 | raise 67 | 68 | return data 69 | 70 | def metric(self, name): 71 | data = self.stats_data() 72 | 73 | return getattr(data, name) 74 | 75 | def stats(self): 76 | """Build dictionary for all statistics reported by HAProxy. 77 | 78 | :return: A dictionary with statistics 79 | :rtype: ``dict`` 80 | """ 81 | data = self.stats_data() 82 | keys = data.heads 83 | values = data.parts 84 | 85 | return dict(zip(keys, values)) 86 | 87 | def command(self, cmd): 88 | return self.backend.hap_process.command(cmd) 89 | -------------------------------------------------------------------------------- /haproxyadmin/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylint: disable=superfluous-parens 4 | # 5 | """ 6 | haproxyadmin.server 7 | ~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | This module provides the :class:`Server <.Server>` class which allows to 10 | run operation for a server. 11 | 12 | """ 13 | from haproxyadmin.utils import (calculate, cmd_across_all_procs, compare_values, 14 | should_die, check_command, converter, 15 | check_command_addr_port, elements_of_list_same) 16 | from haproxyadmin.exceptions import IncosistentData 17 | 18 | 19 | STATE_ENABLE = 'enable' 20 | STATE_DISABLE = 'disable' 21 | STATE_READY = 'ready' 22 | STATE_DRAIN = 'drain' 23 | STATE_MAINT = 'maint' 24 | VALID_STATES = [ 25 | STATE_ENABLE, 26 | STATE_DISABLE, 27 | STATE_MAINT, 28 | STATE_DRAIN, 29 | STATE_READY, 30 | ] 31 | SERVER_METRICS = [ 32 | 'act', 33 | 'bck', 34 | 'bin', 35 | 'bout', 36 | 'check_duration', 37 | 'chkdown', 38 | 'chkfail', 39 | 'cli_abrt', 40 | 'ctime', 41 | 'downtime', 42 | 'dresp', 43 | 'econ', 44 | 'eresp', 45 | 'hrsp_1xx', 46 | 'hrsp_2xx', 47 | 'hrsp_3xx', 48 | 'hrsp_4xx', 49 | 'hrsp_5xx', 50 | 'hrsp_other', 51 | 'lastchg', 52 | 'lastsess', 53 | 'lbtot', 54 | 'qcur', 55 | 'qlimit', 56 | 'qmax', 57 | 'qtime', 58 | 'rate', 59 | 'rate_max', 60 | 'rtime', 61 | 'scur', 62 | 'slim', 63 | 'smax', 64 | 'srv_abrt', 65 | 'stot', 66 | 'throttle', 67 | 'ttime', 68 | 'weight', 69 | 'wredis', 70 | 'wretr', 71 | ] 72 | 73 | 74 | class Server: 75 | """Build a user-created :class:`Server` for a single server. 76 | 77 | :param _server_per_proc: list of :class:`._Server` objects. 78 | :type _server_per_proc: ``list`` 79 | :rtype: a :class:`Server`. 80 | """ 81 | 82 | def __init__(self, server_per_proc, backendname): 83 | self._server_per_proc = server_per_proc 84 | self.backendname = backendname 85 | self._name = self._server_per_proc[0].name 86 | 87 | # built-in comparison operator is adjusted 88 | def __eq__(self, other): 89 | if isinstance(other, Server): 90 | return (self.name == other.name) 91 | elif isinstance(other, str): 92 | return (self.name == other) 93 | else: 94 | return False 95 | 96 | def __ne__(self, other): 97 | return (not self.__eq__(other)) 98 | 99 | @property 100 | def sid(self): 101 | """Return the unique proxy server ID of the server. 102 | 103 | .. note:: 104 | Because server ID is the same across all processes, 105 | we return the proxy ID from the 1st process. 106 | 107 | :rtype: ``int`` 108 | """ 109 | return int(self._server_per_proc[0].sid) 110 | 111 | @property 112 | def check_code(self): 113 | """Return the check code. 114 | 115 | :rtype: ``integer`` 116 | """ 117 | values = cmd_across_all_procs( 118 | self._server_per_proc, 'metric', 'check_code' 119 | ) 120 | 121 | return compare_values(values) 122 | 123 | @property 124 | def check_status(self): 125 | """Return the check status. 126 | 127 | :rtype: ``string`` 128 | """ 129 | values = cmd_across_all_procs( 130 | self._server_per_proc, 'metric', 'check_status' 131 | ) 132 | 133 | return compare_values(values) 134 | 135 | @property 136 | def port(self): 137 | """The assigned port of server. 138 | 139 | :getter: :rtype: ``string`` 140 | :setter: 141 | :param port: port to set. 142 | :type port: ``string`` 143 | :rtype: ``bool`` 144 | """ 145 | values = cmd_across_all_procs( 146 | self._server_per_proc, 'metric', 'addr' 147 | ) 148 | 149 | try: 150 | value = compare_values(values) 151 | except IncosistentData as exc: 152 | # haproxy returns address:port and compare_values() may raise 153 | # IncosistentData exception because assigned address is different 154 | # per process and not the assigned port. 155 | # Since we want to report the port, we simply catch that case and 156 | # report the assigned port. 157 | ports_across_proc = [value[1].split(':')[1] for value in values] 158 | if not elements_of_list_same(ports_across_proc): 159 | raise exc 160 | else: 161 | return ports_across_proc[0] 162 | else: 163 | return value.split(':')[1] 164 | 165 | @port.setter 166 | def port(self, port): 167 | """Set server's port.""" 168 | cmd = "set server {}/{} addr {} port {}".format( 169 | self.backendname, self.name, self.address, port 170 | ) 171 | results = cmd_across_all_procs(self._server_per_proc, 'command', cmd) 172 | 173 | return check_command_addr_port('port', results) 174 | 175 | @property 176 | def address(self): 177 | """The assigned address of server. 178 | 179 | :getter: :rtype: ``string`` 180 | :setter: 181 | :param address: address to set. 182 | :type address: ``string`` 183 | :rtype: ``bool`` 184 | """ 185 | values = cmd_across_all_procs( 186 | self._server_per_proc, 'metric', 'addr' 187 | ) 188 | 189 | try: 190 | value = compare_values(values) 191 | except IncosistentData as exc: 192 | # haproxy returns address:port and compare_values() may raise 193 | # IncosistentData exception because assigned port is different 194 | # per process and not the assigned address. 195 | # Since we want to report the address, we simply catch that case 196 | # and report the assigned address. 197 | addr_across_proc = [value[1].split(':')[0] for value in values] 198 | if not elements_of_list_same(addr_across_proc): 199 | raise exc 200 | else: 201 | return addr_across_proc[0] 202 | else: 203 | return value.split(':')[0] 204 | 205 | @address.setter 206 | def address(self, address): 207 | """Set server's address.""" 208 | cmd = "set server {}/{} addr {}".format( 209 | self.backendname, self.name, address 210 | ) 211 | results = cmd_across_all_procs(self._server_per_proc, 'command', cmd) 212 | 213 | return check_command_addr_port('addr', results) 214 | 215 | @property 216 | def last_status(self): 217 | """Return the last health check contents or textual error. 218 | 219 | :rtype: ``string`` 220 | """ 221 | values = cmd_across_all_procs( 222 | self._server_per_proc, 'metric', 'last_chk' 223 | ) 224 | 225 | return compare_values(values) 226 | 227 | @property 228 | def last_agent_check(self): 229 | """Return the last agent check contents or textual error. 230 | 231 | :rtype: ``string`` 232 | """ 233 | values = cmd_across_all_procs( 234 | self._server_per_proc, 'metric', 'last_agt' 235 | ) 236 | 237 | return compare_values(values) 238 | 239 | def metric(self, name): 240 | """Return the value of a metric. 241 | 242 | Performs a calculation on the metric across all HAProxy processes. 243 | The type of calculation is either sum or avg and defined in 244 | :data:`haproxyadmin.utils.METRICS_SUM` and 245 | :data:`haproxyadmin.utils.METRICS_AVG`. 246 | 247 | :param name: The name of the metric 248 | :type name: any of :data:`haproxyadmin.haproxy.SERVER_METRICS` 249 | :rtype: number, integer 250 | :raise: ``ValueError`` when a given metric is not found 251 | """ 252 | if name not in SERVER_METRICS: 253 | raise ValueError("{} is not valid metric".format(name)) 254 | 255 | metrics = [x.metric(name) for x in self._server_per_proc] 256 | # num_metrics = filter(None, map(converter, metrics)) 257 | metrics[:] = (converter(x) for x in metrics) 258 | metrics[:] = (x for x in metrics if x is not None) 259 | 260 | return calculate(name, metrics) 261 | 262 | @property 263 | def name(self): 264 | """Return the name of the server. 265 | 266 | :rtype: ``string`` 267 | 268 | """ 269 | return self._name 270 | 271 | @property 272 | def requests(self): 273 | """Return the number of requests. 274 | 275 | :rtype: ``integer`` 276 | 277 | """ 278 | return self.metric('stot') 279 | 280 | def requests_per_process(self): 281 | """Return the number of requests for the server per process. 282 | 283 | :rtype: A list of tuple, where 1st element is process number and 2nd 284 | element is requests. 285 | 286 | """ 287 | results = cmd_across_all_procs(self._server_per_proc, 'metric', 'stot') 288 | 289 | return results 290 | 291 | @property 292 | def process_nb(self): 293 | """Return a list of process number in which backend server is configured. 294 | 295 | :return: a list of process numbers. 296 | :rtype: ``list`` 297 | 298 | """ 299 | process_numbers = [] 300 | for server in self._server_per_proc: 301 | process_numbers.append(server.process_nb) 302 | 303 | return process_numbers 304 | 305 | @should_die 306 | def setstate(self, state): 307 | """Set the state of a server in the backend. 308 | 309 | State can be any of the following 310 | 311 | * :const:`haproxyadmin.STATE_ENABLE`: Mark the server UP and 312 | checks are re-enabled 313 | * :const:`haproxyadmin.STATE_DISABLE`: Mark the server DOWN 314 | for maintenance and checks disabled. 315 | * :const:`haproxyadmin.STATE_READY`: Put server in normal 316 | mode. 317 | * :const:`haproxyadmin.STATE_DRAIN`: Remove the server from 318 | load balancing. 319 | * :const:`haproxyadmin.STATE_MAINT`: Remove the server from 320 | load balancing and health checks are disabled. 321 | 322 | :param state: state to set. 323 | :type state: ``string`` 324 | :return: ``True`` if command succeeds otherwise ``False``. 325 | :rtype: ``bool`` 326 | 327 | Usage: 328 | 329 | >>> from haproxyadmin import haproxy, STATE_DISABLE, STATE_ENABLE 330 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 331 | >>> server = hap.server('member_bkall', backend='backend_proc1')[0] 332 | >>> server.setstate(haproxy.STATE_DISABLE) 333 | True 334 | >>> server.status 335 | 'MAINT' 336 | >>> server.setstate(haproxy.STATE_ENABLE) 337 | True 338 | >>> server.status 339 | 'no check' 340 | 341 | """ 342 | if state not in VALID_STATES: 343 | states = ', '.join(VALID_STATES) 344 | raise ValueError("Wrong state, allowed states {}".format(states)) 345 | if state in ('enable', 'disable'): 346 | cmd = "{} server {}/{}".format(state, self.backendname, self.name) 347 | else: 348 | cmd = "set server {}/{} state {}".format( 349 | self.backendname, self.name, state 350 | ) 351 | 352 | results = cmd_across_all_procs(self._server_per_proc, 'command', cmd) 353 | 354 | return check_command(results) 355 | 356 | def stats_per_process(self): 357 | """Return all stats of the server per process. 358 | 359 | :return: A list of tuple 2 elements 360 | 361 | #. process number 362 | #. a dict with all stats 363 | 364 | :rtype: ``list`` 365 | 366 | """ 367 | values = cmd_across_all_procs(self._server_per_proc, 'stats') 368 | 369 | return values 370 | 371 | @property 372 | def status(self): 373 | """Return the status of the server. 374 | 375 | :rtype: ``string`` 376 | :raise: :class:`IncosistentData` exception if status is different 377 | per process 378 | 379 | """ 380 | values = cmd_across_all_procs(self._server_per_proc, 'metric', 'status') 381 | 382 | return compare_values(values) 383 | 384 | @property 385 | def weight(self): 386 | """Return the weight. 387 | 388 | :rtype: ``integer`` 389 | :raise: :class:`IncosistentData` exception if weight is different 390 | per process 391 | """ 392 | values = cmd_across_all_procs(self._server_per_proc, 'metric', 'weight') 393 | 394 | return compare_values(values) 395 | 396 | @should_die 397 | def setweight(self, value): 398 | """Set a weight. 399 | 400 | If the value ends with the '%' sign, then the new weight will be 401 | relative to the initially configured weight. Absolute weights 402 | are permitted between 0 and 256. 403 | 404 | :param value: Weight to set 405 | :type value: integer or string with '%' sign 406 | :return: ``True`` if command succeeds otherwise ``False``. 407 | :rtype: ``bool`` 408 | 409 | Usage: 410 | 411 | >>> from haproxyadmin import haproxy 412 | >>> hap = haproxy.HAProxy(socket_dir='/run/haproxy') 413 | >>> server = hap.server('member_bkall', backend='backend_proc1')[0] 414 | >>> server.weight 415 | 100 416 | >>> server.setweight('20%') 417 | True 418 | >>> server.weight 419 | 20 420 | >>> server.setweight(58) 421 | True 422 | >>> server.weight 423 | 58 424 | """ 425 | msg = ( 426 | "Invalid weight, absolute weights are permitted between 0 and " 427 | "256 and need to be passed as integers or relative weights " 428 | "are allowed when the value ends with the '%' sign pass as " 429 | "string" 430 | ) 431 | if isinstance(value, int) and 0 <= value < 256 or ( 432 | isinstance(value, str) and value.endswith('%')): 433 | cmd = "set weight {}/{} {}".format(self.backendname, 434 | self.name, 435 | value) 436 | else: 437 | raise ValueError(msg) 438 | 439 | results = cmd_across_all_procs(self._server_per_proc, 'command', cmd) 440 | 441 | return check_command(results) 442 | 443 | @should_die 444 | def shutdown(self): 445 | """Terminate all the sessions attached to the specified server. 446 | 447 | :return: ``True`` if command succeeds otherwise ``False``. 448 | :rtype: ``bool`` 449 | """ 450 | 451 | cmd = "shutdown sessions server {b}/{s}".format(b=self.backendname, 452 | s=self.name) 453 | results = cmd_across_all_procs(self._server_per_proc, 'command', cmd) 454 | 455 | return check_command(results) 456 | -------------------------------------------------------------------------------- /haproxyadmin/utils.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=superfluous-parens 2 | # 3 | """ 4 | haproxyadmin.utils 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | This module provides utility functions and classes that are used within 8 | haproxyadmin. 9 | 10 | """ 11 | 12 | import socket 13 | import os 14 | import stat 15 | from functools import wraps 16 | import six 17 | import re 18 | 19 | from haproxyadmin.exceptions import (CommandFailed, MultipleCommandResults, 20 | IncosistentData) 21 | from haproxyadmin.command_status import (ERROR_OUTPUT_STRINGS, 22 | SUCCESS_OUTPUT_STRINGS, SUCCESS_STRING_PORT, SUCCESS_STRING_ADDRESS) 23 | 24 | METRICS_SUM = [ 25 | 'CompressBpsIn', 26 | 'CompressBpsOut', 27 | 'CompressBpsRateLim', 28 | 'ConnRate', 29 | 'ConnRateLimit', 30 | 'CumConns', 31 | 'CumReq', 32 | 'CumSslConns', 33 | 'CurrConns', 34 | 'CurrSslConns', 35 | 'Hard_maxconn', 36 | 'Idle_pct', 37 | 'MaxConnRate', 38 | 'MaxSessRate', 39 | 'MaxSslConns', 40 | 'MaxSslRate', 41 | 'MaxZlibMemUsage', 42 | 'Maxconn', 43 | 'Maxpipes', 44 | 'Maxsock', 45 | 'Memmax_MB', 46 | 'PipesFree', 47 | 'PipesUsed', 48 | 'Process_num', 49 | 'Run_queue', 50 | 'SessRate', 51 | 'SessRateLimit', 52 | 'SslBackendKeyRate', 53 | 'SslBackendMaxKeyRate', 54 | 'SslCacheLookups', 55 | 'SslCacheMisses', 56 | 'SslFrontendKeyRate', 57 | 'SslFrontendMaxKeyRate', 58 | 'SslFrontendSessionReuse_pct', 59 | 'SslRate', 60 | 'SslRateLimit', 61 | 'Tasks', 62 | 'Ulimit-n', 63 | 'ZlibMemUsage', 64 | 'bin', 65 | 'bout', 66 | 'chkdown', 67 | 'chkfail', 68 | 'comp_byp', 69 | 'comp_in', 70 | 'comp_out', 71 | 'comp_rsp', 72 | 'cli_abrt', 73 | 'dreq', 74 | 'dresp', 75 | 'ereq', 76 | 'eresp', 77 | 'econ', 78 | 'hrsp_1xx', 79 | 'hrsp_2xx', 80 | 'hrsp_3xx', 81 | 'hrsp_4xx', 82 | 'hrsp_5xx', 83 | 'hrsp_other', 84 | 'lbtot', 85 | 'qcur', 86 | 'qmax', 87 | 'rate', 88 | 'rate_lim', 89 | 'rate_max', 90 | 'req_rate', 91 | 'req_rate_max', 92 | 'req_tot', 93 | 'scur', 94 | 'slim', 95 | 'srv_abrt', 96 | 'smax', 97 | 'stot', 98 | 'wretr', 99 | 'wredis', 100 | ] 101 | 102 | METRICS_AVG = [ 103 | 'act', 104 | 'bck', 105 | 'check_duration', 106 | 'ctime', 107 | 'downtime', 108 | 'lastchg', 109 | 'lastsess', 110 | 'qlimit', 111 | 'qtime', 112 | 'rtime', 113 | 'throttle', 114 | 'ttime', 115 | 'weight', 116 | ] 117 | 118 | 119 | def should_die(old_implementation): 120 | """Build a decorator to control exceptions. 121 | 122 | When a function raises an exception in some cases we don't care for the 123 | reason but only if the function run successfully or not. We add an extra 124 | argument to the decorated function with the name ``die`` to control this 125 | behavior. When it is set to ``True``, which is the default value, it 126 | raises any exception raised by the decorated function. When it is set to 127 | ``False`` it returns ``True`` if decorated function run successfully or 128 | ``False`` if an exception was raised. 129 | """ 130 | @wraps(old_implementation) 131 | def new_implementation(*args, **kwargs): 132 | try: 133 | die = kwargs['die'] 134 | del(kwargs['die']) 135 | except KeyError: 136 | die = True 137 | 138 | try: 139 | rv = old_implementation(*args, **kwargs) 140 | return rv 141 | except Exception as error: 142 | if die: 143 | raise error 144 | else: 145 | return False 146 | 147 | return new_implementation 148 | 149 | 150 | def is_unix_socket(path): 151 | """Return ``True`` if path is a valid UNIX socket otherwise False. 152 | 153 | :param path: file name path 154 | :type path: ``string`` 155 | :rtype: ``bool`` 156 | """ 157 | try: 158 | mode = os.stat(path).st_mode 159 | except OSError: 160 | return False 161 | 162 | return stat.S_ISSOCK(mode) 163 | 164 | def connected_socket(path, timeout): 165 | """Check if socket file is a valid HAProxy socket file. 166 | 167 | We send a 'show info' command to the socket, build a dictionary structure 168 | and check if 'Name' key is present in the dictionary to confirm that 169 | there is a HAProxy process connected to it. 170 | 171 | :param path: file name path 172 | :type path: ``string`` 173 | :param timeout: timeout for the connection, in seconds 174 | :type timeout: ``float`` 175 | :return: ``True`` is socket file is a valid HAProxy stats socket file False 176 | otherwise 177 | :rtype: ``bool`` 178 | """ 179 | try: 180 | unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 181 | unix_socket.settimeout(timeout) 182 | unix_socket.connect(path) 183 | unix_socket.send(six.b('show info' + '\n')) 184 | file_handle = unix_socket.makefile() 185 | except (socket.timeout, OSError): 186 | return False 187 | else: 188 | try: 189 | data = file_handle.read().splitlines() 190 | except (socket.timeout, OSError): 191 | return False 192 | else: 193 | hap_info = info2dict(data) 194 | finally: 195 | unix_socket.close() 196 | 197 | try: 198 | return hap_info['Name'] in ['HAProxy', 'hapee-lb'] 199 | except KeyError: 200 | return False 201 | 202 | 203 | def cmd_across_all_procs(hap_objects, method, *arg, **kargs): 204 | """Return the result of a command executed in all HAProxy process. 205 | 206 | .. note:: 207 | Objects must have a property with the name 'process_nb' which 208 | returns the HAProxy process number. 209 | 210 | :param hap_objects: a list of objects. 211 | :type hap_objects: ``list`` 212 | :param method: a valid method for the objects. 213 | :return: list of 2-item tuple 214 | 215 | #. HAProxy process number 216 | #. what the method returned 217 | 218 | :rtype: ``list`` 219 | """ 220 | results = [] 221 | for obj in hap_objects: 222 | results.append( 223 | (getattr(obj, 'process_nb'), getattr(obj, method)(*arg, **kargs)) 224 | ) 225 | 226 | return results 227 | 228 | 229 | def elements_of_list_same(iterator): 230 | """Check is all elements of an iterator are equal. 231 | 232 | :param iterator: a iterator 233 | :type iterator: ``list`` 234 | :rtype: ``bool`` 235 | 236 | Usage:: 237 | 238 | >>> from haproxyadmin import utils 239 | >>> iterator = ['OK', 'ok'] 240 | >>> utils.elements_of_list_same(iterator) 241 | False 242 | >>> iterator = ['OK', 'OK'] 243 | >>> utils.elements_of_list_same(iterator) 244 | True 245 | >>> iterator = [22, 22, 22] 246 | >>> utils.elements_of_list_same(iterator) 247 | True 248 | >>> iterator = [22, 22, 222] 249 | >>> utils.elements_of_list_same(iterator) 250 | False 251 | """ 252 | return len(set(iterator)) == 1 253 | 254 | 255 | def compare_values(values): 256 | """Run an intersection test across values returned by processes. 257 | 258 | It is possible that not all processes return the same value for certain 259 | keys(status, weight etc) due to various reasons. We must detect these cases 260 | and either return the value which is the same across all processes or 261 | raise :class:``. 262 | 263 | :param values: a list of tuples with 2 elements. 264 | 265 | #. process number of HAProxy process returned the data 266 | #. value returned by HAProxy process. 267 | 268 | :type values: ``list`` 269 | :return: value 270 | :rtype: ``string`` 271 | :raise: :class:`.IncosistentData`. 272 | """ 273 | if elements_of_list_same([msg[1] for msg in values]): 274 | return values[0][1] 275 | else: 276 | raise IncosistentData(values) 277 | 278 | 279 | def check_output(output): 280 | """Check if output contains any error. 281 | 282 | Several commands return output which we need to return back to the caller. 283 | But, before we return anything back we want to perform a sanity check on 284 | on the output in order to catch wrong input as it is impossible to 285 | perform any sanitization on values/patterns which are passed as input to 286 | the command. 287 | 288 | :param output: output of the command. 289 | :type output: ``list`` 290 | :return: ``True`` if no errors found in output otherwise ``False``. 291 | :rtype: ``bool`` 292 | """ 293 | # We only care about the 1st line as that one contains possible error 294 | # message 295 | first_line = output[0] 296 | if first_line in ERROR_OUTPUT_STRINGS: 297 | return False 298 | else: 299 | return True 300 | 301 | 302 | def check_command(results): 303 | """Check if command was successfully executed. 304 | 305 | After a command is executed. We care about the following cases: 306 | 307 | * The same output is returned by all processes 308 | * If output matches to a list of outputs which indicate that 309 | command was valid 310 | 311 | :param results: a list of tuples with 2 elements. 312 | 313 | #. process number of HAProxy 314 | #. message returned by HAProxy 315 | 316 | :type results: ``list`` 317 | :return: ``True`` if command was successfully executed otherwise ``False``. 318 | :rtype: ``bool`` 319 | :raise: :class:`.MultipleCommandResults` when output differers. 320 | """ 321 | if elements_of_list_same([msg[1] for msg in results]): 322 | msg = results[0][1] 323 | if msg in SUCCESS_OUTPUT_STRINGS: 324 | return True 325 | else: 326 | raise CommandFailed(msg) 327 | else: 328 | raise MultipleCommandResults(results) 329 | 330 | def check_command_addr_port(change_type, results): 331 | """Check if command to set port or address was successfully executed. 332 | 333 | Unfortunately, haproxy returns many different combinations of output when 334 | we change the address or the port of the server and trying to determine 335 | if address or port was successfully changed isn't that trivial. 336 | 337 | So, after we change address or port, we check if the same output is 338 | returned by all processes and we also check if a collection of specific 339 | strings are part of the output. This is a suboptimal solution, but I 340 | couldn't come up with something more elegant. 341 | 342 | :param change_type: either ``addr`` or ``port`` 343 | :type change_type: ``string`` 344 | :param results: a list of tuples with 2 elements. 345 | 346 | #. process number of HAProxy 347 | #. message returned by HAProxy 348 | :type results: ``list`` 349 | :return: ``True`` if command was successfully executed otherwise ``False``. 350 | :rtype: ``bool`` 351 | :raise: :class:`.MultipleCommandResults`, :class:`.CommandFailed` and 352 | :class:`ValueError`. 353 | """ 354 | if change_type == 'addr': 355 | _match = SUCCESS_STRING_ADDRESS 356 | elif change_type == 'port': 357 | _match = SUCCESS_STRING_PORT 358 | else: 359 | raise ValueError('invalid value for change_type') 360 | 361 | if elements_of_list_same([msg[1] for msg in results]): 362 | msg = results[0][1] 363 | if re.match(_match, msg): 364 | return True 365 | else: 366 | raise CommandFailed(msg) 367 | else: 368 | raise MultipleCommandResults(results) 369 | 370 | 371 | def calculate(name, metrics): 372 | """Perform the appropriate calculation across a list of metrics. 373 | 374 | :param name: name of the metric. 375 | :type name: ``string`` 376 | :param metrics: a list of metrics. Elements need to be either ``int`` 377 | or ``float`` type number. 378 | :type metrics: ``list`` 379 | :return: either the sum or the average of metrics. 380 | :rtype: ``integer`` 381 | :raise: :class:`ValueError` when matric name has unknown type of 382 | calculation. 383 | """ 384 | if not metrics: 385 | return 0 386 | 387 | if name in METRICS_SUM: 388 | return sum(metrics) 389 | elif name in METRICS_AVG: 390 | return int(sum(metrics)/len(metrics)) 391 | else: 392 | # This is to catch the case where the caller forgets to check if 393 | # metric name is a valide metric for HAProxy. 394 | raise ValueError("Unknown type of calculation for {}".format(name)) 395 | 396 | def isint(value): 397 | """Check if input can be converted to an integer 398 | 399 | :param value: value to check 400 | :type value: a ``string`` or ``int`` 401 | :return: ``True`` if value can be converted to an integer 402 | :rtype: ``bool`` 403 | :raise: :class:`ValueError` when value can't be converted to an integer 404 | """ 405 | try: 406 | int(value) 407 | return True 408 | except ValueError: 409 | return False 410 | 411 | def converter(value): 412 | """Tries to convert input value to an integer. 413 | 414 | If input can be safely converted to number it returns an ``int`` type. 415 | If input is a valid string but not an empty one it returns that. 416 | In all other cases we return None, including the ones which an 417 | ``TypeError`` exception is raised by ``int()``. 418 | For floating point numbers, it truncates towards zero. 419 | 420 | Why are we doing this? 421 | HAProxy may return for a metric either a number or zero or string or an 422 | empty string. 423 | 424 | It is up to the caller to correctly use the returned value. If the returned 425 | value is passed to a function which does math operations the caller has to 426 | filtered out possible ``None`` values. 427 | 428 | :param value: a value to convert to int. 429 | :type value: ``string`` 430 | :rtype: ``integer or ``string`` or ``None`` if value can't be converted 431 | to ``int`` or to ``string``. 432 | 433 | Usage:: 434 | 435 | >>> from haproxyadmin import utils 436 | >>> utils.converter('0') 437 | 0 438 | >>> utils.converter('13.5') 439 | 13 440 | >>> utils.converter('13.5f') 441 | '13.5f' 442 | >>> utils.converter('') 443 | >>> utils.converter(' ') 444 | >>> utils.converter('UP') 445 | 'UP' 446 | >>> utils.converter('UP 1/2') 447 | 'UP 1/2' 448 | >>> 449 | """ 450 | try: 451 | return int(float(value)) 452 | except ValueError: 453 | # if it isn't an empty string return it otherwise return None 454 | return value.strip() or None 455 | except TypeError: 456 | # This is to catch the case where input value is a data structure or 457 | # object. It is very unlikely someone to pass those, but you never know. 458 | return None 459 | 460 | 461 | class CSVLine(object): 462 | """An object that holds field/value of a CSV line. 463 | 464 | The field name becomes the attribute of the class. 465 | Needs the header line of CSV during instantiation. 466 | 467 | :param parts: A list with field values 468 | :type parts: list 469 | 470 | Usage:: 471 | 472 | >>> from haproxyadmin import utils 473 | >>> heads = ['pxname', 'type', 'lbtol'] 474 | >>> parts = ['foor', 'backend', '444'] 475 | >>> utils.CSVLine.heads = heads 476 | >>> csvobj = utils.CSVLine(parts) 477 | >>> csvobj.pxname 478 | 'foor' 479 | >>> csvobj.type 480 | 'backend' 481 | >>> csvobj.lbtol 482 | '444' 483 | >>> csvobj.bar 484 | Traceback (most recent call last): 485 | File "", line 1, in 486 | File "/.../haproxyadmin/haproxyadmin/utils.py", line 341, in __getattr__ 487 | _index = self.heads.index(attr) 488 | ValueError: 'bar' is not in list 489 | """ 490 | # This holds the field names of the CSV 491 | heads = [] 492 | 493 | def __init__(self, parts): 494 | self.parts = parts 495 | 496 | def __getattr__(self, attr): 497 | _index = self.heads.index(attr) 498 | setattr(self, attr, self.parts[_index]) 499 | 500 | return self.parts[_index] 501 | 502 | 503 | def info2dict(raw_info): 504 | """Build a dictionary structure from the output of 'show info' command. 505 | 506 | :param raw_info: data returned by 'show info' UNIX socket command 507 | :type raw_info: ``list`` 508 | :return: A dictionary with the following keys/values(examples) 509 | 510 | .. code-block:: python 511 | 512 | { 513 | Name: HAProxy 514 | Version: 1.4.24 515 | Release_date: 2013/06/17 516 | Nbproc: 1 517 | Process_num: 1 518 | Pid: 1155 519 | Uptime: 5d 4h42m16s 520 | Uptime_sec: 448936 521 | Memmax_MB: 0 522 | Ulimit-n: 131902 523 | Maxsock: 131902 524 | Maxconn: 65536 525 | Maxpipes: 0 526 | CurrConns: 1 527 | PipesUsed: 0 528 | PipesFree: 0 529 | Tasks: 819 530 | Run_queue: 1 531 | node: node1 532 | description: 533 | } 534 | 535 | :rtype: ``dict`` 536 | """ 537 | info = {} 538 | for line in raw_info: 539 | line = line.lstrip() 540 | if ': ' in line: 541 | key, value = line.split(': ', 1) 542 | info[key] = value 543 | 544 | return info 545 | 546 | 547 | def stat2dict(csv_data): 548 | """Build a nested dictionary structure. 549 | 550 | :param csv_data: data returned by 'show stat' command in a CSV format. 551 | :type csv_data: ``list`` 552 | :return: a nested dictionary with all counters/settings found in the input. 553 | Following is a sample of the structure:: 554 | 555 | { 556 | 'backends': { 557 | 'acq-misc': { 558 | 'stats': { _CSVLine object }, 559 | 'servers': { 560 | 'acqrdb-01': { _CSVLine object }, 561 | 'acqrdb-02': { _CSVLine object }, 562 | ... 563 | } 564 | }, 565 | ... 566 | }, 567 | 'frontends': { 568 | 'acq-misc': { _CSVLine object }, 569 | ... 570 | }, 571 | ... 572 | } 573 | 574 | :rtype: ``dict`` 575 | """ 576 | heads = [] 577 | dicts = { 578 | 'backends': {}, 579 | 'frontends': {} 580 | } 581 | 582 | # get the header line 583 | headers = csv_data.pop(0) 584 | # make a shiny list of heads 585 | heads = headers[2:].strip().split(',') 586 | # set for all _CSVLine object the header fields 587 | CSVLine.heads = heads 588 | 589 | # We need to parse the following 590 | # haproxy,FRONTEND,,,... 591 | # haproxy,BACKEND,0,0,0... 592 | # test,FRONTEND,,,0,0,10... 593 | # dummy,BACKEND,0,0,0,0,1.. 594 | # app_com,FRONTEND,,,0... 595 | # app_com,appfe-103.foo.com,0,... 596 | # app_com,BACKEND,0,0,... 597 | # monapp_com,FRONTEND,,,.... 598 | # monapp_com,monappfe-102.foo.com,0... 599 | # monapp_com,BACKEND,0,0... 600 | # app_api_com,FRONTEND,,,... 601 | # app_api_com,appfe-105.foo.com,0... 602 | # app_api_com,appfe-106.foo.com,0... 603 | # app_api_com,BACKEND,0,0,0,0,100000,0,0,0,0,0,,0,0,... 604 | 605 | # A line which holds frontend definition: 606 | # ,FRONTEND,.... 607 | # A line holds server definition: 608 | # ,,.... 609 | # A line which holds backend definition: 610 | # ,BACKEND,.... 611 | # NOTE: we can have a single line for a backend definition without any 612 | # lines for servers associated with for that backend 613 | for line in csv_data: 614 | line = line.strip() 615 | if line: 616 | # make list of parts 617 | parts = line.split(',') 618 | # each line is a distinct object 619 | csvline = CSVLine(parts) 620 | # parts[0] => pxname field, backend or frontend name 621 | # parts[1] => svname field, servername or BACKEND or FRONTEND 622 | if parts[1] == 'FRONTEND': 623 | # This is a frontend line. 624 | # Frontend definitions aren't spread across multiple lines. 625 | dicts['frontends'][parts[0]] = csvline 626 | elif (parts[1] == 'BACKEND' and parts[0] not in dicts['backends']): 627 | # I see this backend information for 1st time. 628 | dicts['backends'][parts[0]] = {} 629 | dicts['backends'][parts[0]]['servers'] = {} 630 | dicts['backends'][parts[0]]['stats'] = csvline 631 | else: 632 | if parts[0] not in dicts['backends']: 633 | # This line holds server information for a backend I haven't 634 | # seen before, thus create the backend structure and store 635 | # server details. 636 | dicts['backends'][parts[0]] = {} 637 | dicts['backends'][parts[0]]['servers'] = {} 638 | if parts[1] == 'BACKEND': 639 | dicts['backends'][parts[0]]['stats'] = csvline 640 | else: 641 | dicts['backends'][parts[0]]['servers'][parts[1]] = csvline 642 | 643 | return dicts 644 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = haproxyadmin 3 | author = Pavlos Parissis 4 | author-email = pavlos.parissis@gmail.com 5 | maintainer = Pavlos Parissis 6 | maintainer-email = pavlos.parissis@gmail.com 7 | summary = A library to work with HAProxy via the stats socket 8 | license = Apache 2.0 9 | description-file = README.rst 10 | classifier = 11 | Development Status :: 5 - Production/Stable 12 | Environment :: Console 13 | Intended Audience :: Information Technology 14 | Intended Audience :: System Administrators 15 | Natural Language :: English 16 | Operating System :: POSIX 17 | Programming Language :: Python :: 2.7 18 | Programming Language :: Python :: 3.4 19 | Topic :: Utilities 20 | install_requires = 21 | six 22 | keywords = 23 | haproxy 24 | 25 | [files] 26 | packages = 27 | haproxyadmin 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | setuptools.setup( 6 | setup_requires=['pbr'], 7 | pbr=True) 8 | -------------------------------------------------------------------------------- /tools/generate_constants_for_metrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # File name: a.py 6 | # 7 | # Creation date: 09-04-2015 8 | # 9 | # Created by: Pavlos Parissis 10 | # 11 | import re 12 | 13 | 14 | def main(): 15 | # [[:digit:]]{1,}\. [0-9A-Za-z_]{1,} \[.*B.*\]:' 16 | frontend = [] 17 | backend = [] 18 | server = [] 19 | with open('/home/pparissis/configuration.txt') as file: 20 | for line in file: 21 | line = line.strip() 22 | match = re.search(r'\d+\. (\w+) (\[.*\]:) .*', line) 23 | if match: 24 | if 'F' in match.group(2): 25 | frontend.append(match.group(1)) 26 | if 'B' in match.group(2): 27 | backend.append(match.group(1)) 28 | if 'S' in match.group(2): 29 | server.append(match.group(1)) 30 | print("FRONTEND_METRICS = [") 31 | for m in frontend: 32 | print("{:<4}'{}',".format('', m)) 33 | print("]") 34 | print("POOL_METRICS = [") 35 | for m in backend: 36 | print("{:<4}'{}',".format('', m)) 37 | print("]") 38 | print("SERVER_METRICS = [") 39 | for m in server: 40 | print("{:<4}'{}',".format('', m)) 41 | print("]") 42 | 43 | 44 | # This is the standard boilerplate that calls the main() function. 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /tools/get_errors_messages.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # get_errors_messages.sh 4 | # Copyright (C) 2015 pparissis 5 | # 6 | # Distributed under terms of the MIT license. 7 | # 8 | 9 | cat src/dumpstats.c |awk -F'=' '{if (match($0,/appctx->ctx.cli.msg/) && match($2,/^ "/")) {sub(/\\n"/,"\"",$2");sub(/;$/,",",$2);print $2}}' |sort|uniq' 10 | -------------------------------------------------------------------------------- /tools/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | log /dev/log local0 debug 3 | log /dev/log local1 debug 4 | chroot /var/lib/haproxy 5 | stats socket /run/haproxy/admin1.sock mode 666 level admin process 1 6 | stats socket /run/haproxy/admin2.sock mode 666 level admin process 2 7 | stats socket /run/haproxy/admin3.sock mode 666 level admin process 3 8 | stats socket /run/haproxy/admin4.sock mode 666 level admin process 4 9 | stats timeout 30s 10 | user haproxy 11 | group haproxy 12 | daemon 13 | nbproc 4 14 | cpu-map 1 0 15 | cpu-map 2 1 16 | cpu-map 3 1 17 | cpu-map 4 0 18 | # Default SSL material locations 19 | ca-base /etc/ssl/certs 20 | crt-base /etc/ssl/private 21 | 22 | # Default ciphers to use on SSL-enabled listening sockets. 23 | # For more information, see ciphers(1SSL). 24 | ssl-default-bind-ciphers kEECDH+aRSA+AES:kRSA+AES:+AES256:RC4-SHA:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL 25 | ssl-default-bind-options no-sslv3 26 | maxconn 10000 27 | defaults 28 | log global 29 | rate-limit sessions 100000 30 | maxconn 1000000 31 | mode http 32 | option httplog 33 | option dontlognull 34 | timeout connect 5000 35 | timeout client 50000 36 | timeout server 50000 37 | errorfile 400 /etc/haproxy/errors/400.http 38 | errorfile 403 /etc/haproxy/errors/403.http 39 | errorfile 408 /etc/haproxy/errors/408.http 40 | errorfile 500 /etc/haproxy/errors/500.http 41 | errorfile 502 /etc/haproxy/errors/502.http 42 | errorfile 503 /etc/haproxy/errors/503.http 43 | errorfile 504 /etc/haproxy/errors/504.http 44 | 45 | listen haproxy-stats 46 | bind-process 1 47 | bind :8080 48 | stats uri / 49 | stats show-node 50 | stats refresh 10s 51 | stats show-legends 52 | no log 53 | acl wl_stats src -f /etc/haproxy/wl_stats 54 | tcp-request connection reject unless wl_stats 55 | 56 | listen haproxy-stats2 57 | bind-process 2 58 | bind :8081 59 | stats uri / 60 | stats show-node 61 | stats refresh 10s 62 | stats show-legends 63 | no log 64 | listen haproxy-stats3 65 | bind-process 3 66 | bind :8082 67 | stats uri / 68 | stats show-node 69 | stats refresh 10s 70 | stats show-legends 71 | no log 72 | 73 | listen haproxy-stats4 74 | bind-process 4 75 | bind :8083 76 | stats uri / 77 | stats show-node 78 | stats refresh 10s 79 | stats show-legends 80 | no log 81 | frontend frontend_proc1 82 | bind-process 1 83 | bind 0.0.0.0:81 84 | http-request add-header X-rand %[rand(2),map(/etc/haproxy/v-m1-bk,www.foo.com-0)] 85 | acl https_traffic ssl_fc 86 | default_backend backend_proc1 87 | backend backend_proc1 88 | bind-process 1 89 | default-server inter 1000s 90 | option httpchk GET / HTTP/1.1\r\nHost:\ app.foo.com\r\nUser-Agent:\ HAProxy 91 | server member1_proc1 10.196.70.109:80 92 | server member2_proc1 10.196.70.109:80 93 | server bck_all_srv1 10.196.70.109:88 weight 100 check fall 2 inter 5s rise 3 94 | 95 | frontend frontend_proc2 96 | bind :82 97 | acl bl_frontend src -f /etc/haproxy/bl_frontend 98 | tcp-request connection reject if bl_frontend 99 | acl catch_static path_beg /static/js/ /static/css/ 100 | bind-process 2 101 | default_backend backend_proc2 102 | backend backend_proc2 103 | bind-process 2 104 | default-server inter 1000s 105 | option httpchk GET / HTTP/1.1\r\nHost:\ app.foo.com\r\nUser-Agent:\ HAProxy 106 | server bck_proc2_srv1_proc2 127.0.0.1:8001 check fall 2 inter 5s rise 3 107 | server bck_proc2_srv2_proc2 127.0.0.1:8002 check fall 2 inter 5s rise 3 108 | server bck_proc2_srv3_proc2 127.0.0.1:8003 check fall 2 inter 5s rise 3 109 | server bck_proc2_srv4_proc2 127.0.0.1:8004 check fall 2 inter 5s rise 3 110 | 111 | frontend frontend1_proc34 112 | bind :83 process 3 113 | bind :83 process 4 114 | acl vuvuzela_found req.cook(vuvuzela) -m found 115 | acl vuvuzela_0 req.cook(vuvuzela) ver%3A27%3Bvar%3A0 116 | acl vuvuzela_1 req.cook(vuvuzela) ver%3A27%3Bvar%3A1 117 | 118 | stick-table type ip size 1m expire 5m store gpc0 119 | default_backend backend1_proc34 120 | backend backend1_proc34 121 | bind-process 3,4 122 | default-server inter 1000s 123 | option httpchk GET / HTTP/1.1\r\nHost:\ app.foo.com\r\nUser-Agent:\ HAProxy 124 | server bck1_proc34_srv1 10.196.70.109:80 check fall 2 inter 5s rise 3 125 | server bck1_proc34_srv2 10.196.70.109:80 check fall 2 inter 5s rise 3 126 | server bck_all_srv1 10.196.70.109:80 check fall 2 inter 5s rise 3 127 | frontend frontend2_proc34 128 | bind :84 process 3 129 | bind :84 process 4 130 | default_backend backend2_proc34 131 | backend backend2_proc34 132 | bind-process 3,4 133 | default-server inter 1000s 134 | option httpchk GET / HTTP/1.1\r\nHost:\ app.foo.com\r\nUser-Agent:\ HAProxy 135 | server bck2_proc34_srv1 10.196.70.109:80 136 | server bck2_proc34_srv2 10.196.70.109:80 check weight 100 fall 2 inter 5s rise 3 137 | server bck_all_srv1 10.196.70.109:80 138 | --------------------------------------------------------------------------------