1"""
2Support for haproxy
3
4.. versionadded:: 2014.7.0
5"""
6
7
8import logging
9import os
10import stat
11import time
12
13try:
14    import haproxy.cmds  # pylint: disable=no-name-in-module
15    import haproxy.conn  # pylint: disable=no-name-in-module
16
17    HAS_HAPROXY = True
18except ImportError:
19    HAS_HAPROXY = False
20
21log = logging.getLogger(__name__)
22
23__virtualname__ = "haproxy"
24
25# Default socket location
26DEFAULT_SOCKET_URL = "/var/run/haproxy.sock"
27
28# Numeric fields returned by stats
29FIELD_NUMERIC = ["weight", "bin", "bout"]
30# Field specifying the actual server name
31FIELD_NODE_NAME = "name"
32
33
34def __virtual__():
35    """
36    Only load the module if haproxyctl is installed
37    """
38    if HAS_HAPROXY:
39        return __virtualname__
40    return (
41        False,
42        "The haproxyconn execution module cannot be loaded: haproxyctl module not"
43        " available",
44    )
45
46
47def _get_conn(socket=DEFAULT_SOCKET_URL):
48    """
49    Get connection to haproxy socket.
50    """
51    assert os.path.exists(socket), "{} does not exist.".format(socket)
52    issock = os.stat(socket).st_mode
53    assert stat.S_ISSOCK(issock), "{} is not a socket.".format(socket)
54    ha_conn = haproxy.conn.HaPConn(socket)
55    return ha_conn
56
57
58def list_servers(backend, socket=DEFAULT_SOCKET_URL, objectify=False):
59    """
60    List servers in haproxy backend.
61
62    backend
63        haproxy backend
64
65    socket
66        haproxy stats socket, default ``/var/run/haproxy.sock``
67
68    CLI Example:
69
70    .. code-block:: bash
71
72        salt '*' haproxy.list_servers mysql
73    """
74    ha_conn = _get_conn(socket)
75    ha_cmd = haproxy.cmds.listServers(backend=backend)
76    return ha_conn.sendCmd(ha_cmd, objectify=objectify)
77
78
79def wait_state(backend, server, value="up", timeout=60 * 5, socket=DEFAULT_SOCKET_URL):
80    """
81
82    Wait for a specific server state
83
84    backend
85        haproxy backend
86
87    server
88        targeted server
89
90    value
91        state value
92
93    timeout
94        timeout before giving up state value, default 5 min
95
96    socket
97        haproxy stats socket, default ``/var/run/haproxy.sock``
98
99    CLI Example:
100
101    .. code-block:: bash
102
103        salt '*' haproxy.wait_state mysql server01 up 60
104    """
105    t = time.time() + timeout
106    while time.time() < t:
107        if (
108            get_backend(backend=backend, socket=socket)[server]["status"].lower()
109            == value.lower()
110        ):
111            return True
112    return False
113
114
115def get_backend(backend, socket=DEFAULT_SOCKET_URL):
116    """
117
118    Receive information about a specific backend.
119
120    backend
121        haproxy backend
122
123    socket
124        haproxy stats socket, default ``/var/run/haproxy.sock``
125
126    CLI Example:
127
128    .. code-block:: bash
129
130        salt '*' haproxy.get_backend mysql
131    """
132
133    backend_data = (
134        list_servers(backend=backend, socket=socket).replace("\n", " ").split(" ")
135    )
136    result = {}
137
138    # Convert given string to Integer
139    def num(s):
140        try:
141            return int(s)
142        except ValueError:
143            return s
144
145    for data in backend_data:
146        # Check if field or server name
147        if ":" in data:
148            active_field = data.replace(":", "").lower()
149            continue
150        elif active_field.lower() == FIELD_NODE_NAME:
151            active_server = data
152            result[active_server] = {}
153            continue
154        # Format and set returned field data to active server
155        if active_field in FIELD_NUMERIC:
156            if data == "":
157                result[active_server][active_field] = 0
158            else:
159                result[active_server][active_field] = num(data)
160        else:
161            result[active_server][active_field] = data
162
163    return result
164
165
166def enable_server(name, backend, socket=DEFAULT_SOCKET_URL):
167    """
168    Enable Server in haproxy
169
170    name
171        Server to enable
172
173    backend
174        haproxy backend, or all backends if "*" is supplied
175
176    socket
177        haproxy stats socket, default ``/var/run/haproxy.sock``
178
179    CLI Example:
180
181    .. code-block:: bash
182
183        salt '*' haproxy.enable_server web1.example.com www
184    """
185
186    if backend == "*":
187        backends = show_backends(socket=socket).split("\n")
188    else:
189        backends = [backend]
190
191    results = {}
192    for backend in backends:
193        ha_conn = _get_conn(socket)
194        ha_cmd = haproxy.cmds.enableServer(server=name, backend=backend)
195        ha_conn.sendCmd(ha_cmd)
196        results[backend] = list_servers(backend, socket=socket)
197
198    return results
199
200
201def disable_server(name, backend, socket=DEFAULT_SOCKET_URL):
202    """
203    Disable server in haproxy.
204
205    name
206        Server to disable
207
208    backend
209        haproxy backend, or all backends if "*" is supplied
210
211    socket
212        haproxy stats socket, default ``/var/run/haproxy.sock``
213
214    CLI Example:
215
216    .. code-block:: bash
217
218        salt '*' haproxy.disable_server db1.example.com mysql
219    """
220
221    if backend == "*":
222        backends = show_backends(socket=socket).split("\n")
223    else:
224        backends = [backend]
225
226    results = {}
227    for backend in backends:
228        ha_conn = _get_conn(socket)
229        ha_cmd = haproxy.cmds.disableServer(server=name, backend=backend)
230        ha_conn.sendCmd(ha_cmd)
231        results[backend] = list_servers(backend, socket=socket)
232
233    return results
234
235
236def get_weight(name, backend, socket=DEFAULT_SOCKET_URL):
237    """
238    Get server weight
239
240    name
241        Server name
242
243    backend
244        haproxy backend
245
246    socket
247        haproxy stats socket, default ``/var/run/haproxy.sock``
248
249    CLI Example:
250
251    .. code-block:: bash
252
253        salt '*' haproxy.get_weight web1.example.com www
254    """
255    ha_conn = _get_conn(socket)
256    ha_cmd = haproxy.cmds.getWeight(server=name, backend=backend)
257    return ha_conn.sendCmd(ha_cmd)
258
259
260def set_weight(name, backend, weight=0, socket=DEFAULT_SOCKET_URL):
261    """
262    Set server weight
263
264    name
265        Server name
266
267    backend
268        haproxy backend
269
270    weight
271        Server Weight
272
273    socket
274        haproxy stats socket, default ``/var/run/haproxy.sock``
275
276    CLI Example:
277
278    .. code-block:: bash
279
280        salt '*' haproxy.set_weight web1.example.com www 13
281    """
282    ha_conn = _get_conn(socket)
283    ha_cmd = haproxy.cmds.getWeight(server=name, backend=backend, weight=weight)
284    ha_conn.sendCmd(ha_cmd)
285    return get_weight(name, backend, socket=socket)
286
287
288def set_state(name, backend, state, socket=DEFAULT_SOCKET_URL):
289    """
290    Force a server's administrative state to a new state. This can be useful to
291    disable load balancing and/or any traffic to a server. Setting the state to
292    "ready" puts the server in normal mode, and the command is the equivalent of
293    the "enable server" command. Setting the state to "maint" disables any traffic
294    to the server as well as any health checks. This is the equivalent of the
295    "disable server" command. Setting the mode to "drain" only removes the server
296    from load balancing but still allows it to be checked and to accept new
297    persistent connections. Changes are propagated to tracking servers if any.
298
299    name
300        Server name
301
302    backend
303        haproxy backend
304
305    state
306        A string of the state to set. Must be 'ready', 'drain', or 'maint'
307
308    socket
309        haproxy stats socket, default ``/var/run/haproxy.sock``
310
311    CLI Example:
312
313    .. code-block:: bash
314
315        salt '*' haproxy.set_state my_proxy_server my_backend ready
316
317    """
318    # Pulling this in from the latest 0.5 release which is not yet in PyPi.
319    # https://github.com/neurogeek/haproxyctl
320    class setServerState(haproxy.cmds.Cmd):
321        """Set server state command."""
322
323        cmdTxt = "set server %(backend)s/%(server)s state %(value)s\r\n"
324        p_args = ["backend", "server", "value"]
325        helpTxt = "Force a server's administrative state to a new state."
326
327    ha_conn = _get_conn(socket)
328    ha_cmd = setServerState(server=name, backend=backend, value=state)
329    return ha_conn.sendCmd(ha_cmd)
330
331
332def show_frontends(socket=DEFAULT_SOCKET_URL):
333    """
334    Show HaProxy frontends
335
336    socket
337        haproxy stats socket, default ``/var/run/haproxy.sock``
338
339    CLI Example:
340
341    .. code-block:: bash
342
343        salt '*' haproxy.show_frontends
344    """
345    ha_conn = _get_conn(socket)
346    ha_cmd = haproxy.cmds.showFrontends()
347    return ha_conn.sendCmd(ha_cmd)
348
349
350def list_frontends(socket=DEFAULT_SOCKET_URL):
351    """
352
353    List HaProxy frontends
354
355    socket
356        haproxy stats socket, default ``/var/run/haproxy.sock``
357
358    CLI Example:
359
360    .. code-block:: bash
361
362        salt '*' haproxy.list_frontends
363    """
364    return show_frontends(socket=socket).split("\n")
365
366
367def show_backends(socket=DEFAULT_SOCKET_URL):
368    """
369    Show HaProxy Backends
370
371    socket
372        haproxy stats socket, default ``/var/run/haproxy.sock``
373
374    CLI Example:
375
376    .. code-block:: bash
377
378        salt '*' haproxy.show_backends
379    """
380    ha_conn = _get_conn(socket)
381    ha_cmd = haproxy.cmds.showBackends()
382    return ha_conn.sendCmd(ha_cmd)
383
384
385def list_backends(servers=True, socket=DEFAULT_SOCKET_URL):
386    """
387
388    List HaProxy Backends
389
390    socket
391        haproxy stats socket, default ``/var/run/haproxy.sock``
392
393    servers
394        list backends with servers
395
396    CLI Example:
397
398    .. code-block:: bash
399
400        salt '*' haproxy.list_backends
401    """
402    if not servers:
403        return show_backends(socket=socket).split("\n")
404    else:
405        result = {}
406        for backend in list_backends(servers=False, socket=socket):
407            result[backend] = get_backend(backend=backend, socket=socket)
408        return result
409
410
411def get_sessions(name, backend, socket=DEFAULT_SOCKET_URL):
412    """
413    .. versionadded:: 2016.11.0
414
415    Get number of current sessions on server in backend (scur)
416
417    name
418        Server name
419
420    backend
421        haproxy backend
422
423    socket
424        haproxy stats socket, default ``/var/run/haproxy.sock``
425
426    CLI Example:
427
428    .. code-block:: bash
429
430        salt '*' haproxy.get_sessions web1.example.com www
431    """
432
433    class getStats(haproxy.cmds.Cmd):
434        p_args = ["backend", "server"]
435        cmdTxt = "show stat\r\n"
436        helpText = "Fetch all statistics"
437
438    ha_conn = _get_conn(socket)
439    ha_cmd = getStats(server=name, backend=backend)
440    result = ha_conn.sendCmd(ha_cmd)
441    for line in result.split("\n"):
442        if line.startswith(backend):
443            outCols = line.split(",")
444            if outCols[1] == name:
445                return outCols[4]
446