net/haproxy: replace bundled haproxyctl library

The bundled library is a modified version of haproxyctl:
https://github.com/neurogeek/haproxyctl

Now a proper fork was created:
https://github.com/markt-de/haproxy-cli

The FreeBSD port is pending approval:
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=275562
This commit is contained in:
Frank Wall
2023-12-05 23:29:33 +01:00
parent 6b4680f58f
commit 13ab16cd96
11 changed files with 4 additions and 781 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ PLUGIN_NAME= haproxy
PLUGIN_VERSION= 4.1
PLUGIN_REVISION= 1
PLUGIN_COMMENT= Reliable, high performance TCP/HTTP load balancer
PLUGIN_DEPENDS= haproxy26
PLUGIN_DEPENDS= haproxy26 py${PLUGIN_PYTHON}-haproxy-cli
PLUGIN_MAINTAINER= opnsense@moov.de
.include "../../Mk/plugins.mk"
+3
View File
@@ -9,6 +9,9 @@ Plugin Changelog
Fixed:
* fix typo in cert sync script
Changed:
* replace bundled haproxyctl library with haproxy-cli
4.1
Fixed:
@@ -1,3 +0,0 @@
"""haproxy lib for socket commands.
Based on: https://github.com/neurogeek/haproxyctl"""
__version__ = "1.0"
@@ -1,335 +0,0 @@
"""cmds.py - Implementations of the different HAProxy commands"""
import re
import csv
import json
from collections import OrderedDict
from io import StringIO
class Cmd():
"""Cmd - Command base class"""
req_args = []
args = {}
cmdTxt = ""
helpTxt = ""
# pylint: disable=unused-argument
def __init__(self, *args, **kwargs):
"""Argument to the command are given in kwargs only. We ignore *args."""
self.args = kwargs
valid_kwargs = [k for (k, v) in kwargs.items() if v is not None]
if not all([a in valid_kwargs for a in self.req_args]):
raise Exception(f"Wrong number of arguments. Required arguments are: {self.WhatArgs()}")
def WhatArgs(self):
"""Returns a formatted string of arguments to this command."""
return ",".join(self.req_args)
@classmethod
def getHelp(cls):
"""Get formatted help string for this command."""
txtArgs = ",".join(cls.req_args)
if not txtArgs:
txtArgs = "None"
return " ".join((cls.helpTxt, "Arguments: %s" % txtArgs))
def getCmd(self):
"""Gets the command line for this command.
The default behavior is to apply the args dict to cmdTxt
"""
return self.cmdTxt % self.args
def getBootstrapOutput(self, resObj):
""" Returns results gathered from HAProxy as jquery bootstrap output """
args = {
"rows": resObj,
"page": int(self.args['page']) if self.args['page'] != None else 1,
"page_rows": int(self.args['page_rows']) if self.args['page_rows'] != None else len(rows),
"search": self.args['search'],
"sort_col": self.args['sort_col'] if self.args['sort_col'] else 'id',
"sort_dir": self.args['sort_dir'],
}
rows = args['rows']
# search
if args['search']:
filtered_rows = []
for row in rows:
def inner(row):
for k, v in row.items():
if args['search'] in v:
return row
return None
match = inner(row)
if match:
filtered_rows.append(match)
rows = filtered_rows
# sort
rows.sort(key=lambda k: k[args['sort_col']], reverse=True if args['sort_dir'] == 'desc' else False)
# pager
total = len(rows)
pages = [rows[i:i + args['page_rows']] for i in range(0, total, args['page_rows'])]
if pages and (args['page'] > len(pages) or args['page'] < 1):
raise KeyError(f"Current page {args['page']} does not exist. Available pages: {len(pages)}")
page = pages[args['page'] - 1] if pages else []
return json.dumps({
"rows": page,
"total": total,
"rowCount": args['page_rows'],
"current": args['page']
})
def getJsonOutput(self, resObj):
"""Returns results gathered from HAProxy as json"""
return json.dumps(resObj)
def getResult(self, res):
"""Returns raw results gathered from HAProxy"""
if res == '\n':
res = None
if self.args['output'] == 'json':
return self.getJsonOutput(self.getResultObj(res))
if self.args['output'] == 'bootstrap':
return self.getBootstrapOutput(self.getResultObj(res))
return res
def getResultObj(self, res):
"""Returns refined output from HAProxy, packed inside a Python obj i.e. a dict()"""
return res
class setServerAgent(Cmd):
cmdTxt = "set server %(backend)s/%(server)s agent %(value)s\r\n"
req_args = ['backend', 'server', 'value']
helpTxt = "Force a server's agent to a new state."
class setServerHealth(Cmd):
cmdTxt = "set server %(backend)s/%(server)s health %(value)s\r\n"
req_args = ['backend', 'server', 'value']
helpTxt = "Force a server's health to a new state."
class setServerState(Cmd):
cmdTxt = "set server %(backend)s/%(server)s state %(value)s\r\n"
req_args = ['backend', 'server', 'value']
helpTxt = "Force a server's administrative state to a new state."
class setServerWeight(Cmd):
cmdTxt = "set server %(backend)s/%(server)s weight %(value)s\r\n"
req_args = ['backend', 'server', 'value']
helpTxt = "Force a server's weight to a new state."
class showSslCrtLists(Cmd):
cmdTxt = "show ssl crt-list\r\n"
helpTxt = "Show the list of crt-lists."
def getResultObj(self, res):
result = { "crt_lists": []}
for line in res.split("\n"):
if line.startswith('/'):
result["crt_lists"].append(line)
return result
class showSslCrtList(Cmd):
cmdTxt = "show ssl crt-list -n %(crt_list)s\r\n"
req_args = ['crt_list']
helpTxt = "Show the the content of a crt-list."
def getResultObj(self, res):
result = {}
list_id = None
for line in res.split("\n"):
if line.startswith('# '):
list_id = line.split("# ")[1]
result["certs"] = []
if list_id and line.startswith('/'):
result["certs"].append(line)
if result:
return result
return {"error": res.strip()}
class showSslCerts(Cmd):
cmdTxt = "show ssl cert\r\n"
helpTxt = "Display the SSL certificates used in memory."
def getResultObj(self, res):
result = {
"transaction": [],
"filename": []
}
for line in res.split("\n"):
if line.startswith('*'):
result['transaction'].append(line)
elif line.startswith('/'):
result['filename'].append(line)
return result
class showSslCert(Cmd):
cmdTxt = "show ssl cert %(certfile)s\r\n"
req_args = ['certfile']
helpTxt = "Display the details of a SSL certificate used in memory."
def getResultObj(self, res):
result = {}
cert_id = None
for line in res.split("\n"):
if line:
key = line.split(":")[0]
val = line.split(":")[1].strip()
if key == 'Filename':
cert_id = val
if cert_id:
result[key] = val
if result:
return result
return {"error": res.strip()}
class addToSslCrtList(Cmd):
cmdTxt = "add ssl crt-list %(crt_list)s %(certfile)s\r\n"
req_args = ['crt_list', 'certfile']
helpTxt = "Add a ssl cert to a crt-list."
class delFromSslCrtList(Cmd):
cmdTxt = "del ssl crt-list %(crt_list)s %(certfile)s\r\n"
req_args = ['crt_list', 'certfile']
helpTxt = "Delete a ssl cert from a crt-list."
class newSslCrt(Cmd):
"""" Create an empty slot for the certificate in HAProxys memory """
cmdTxt = "new ssl cert %(certfile)s\r\n"
req_args = ['certfile']
helpTxt = "Create a new certificate file to be used in a crt-list or a directory."
class updateSslCrt(Cmd):
"""" Begin a transaction to upload the certificate into a slot in HAProxys memory """
cmdTxt = "set ssl cert %(certfile)s <<\n%(payload)s\r\n"
req_args = ['certfile', 'payload']
helpTxt = "Replace a certificate file."
class delSslCrt(Cmd):
"""" Begin a transaction to remove the certificate from a slot in HAProxys memory """
cmdTxt = "del ssl cert %(certfile)s\r\n"
req_args = ['certfile']
helpTxt = "Delete delete an unused certificate file."
class commitSslCrt(Cmd):
""" Commit the transaction so HAProxy detects the change. """
cmdTxt = "commit ssl cert %(certfile)s\r\n"
req_args = ['certfile']
helpTxt = "Commit a certificate file."
class abortSslCrt(Cmd):
cmdTxt = "abort ssl cert %(certfile)s\r\n"
req_args = ['certfile']
helpTxt = "Abort a transaction for a certificate file."
class showFBEnds(Cmd):
"""Base class for getting a listing Frontends and Backends"""
switch = ""
cmdTxt = "show stat\r\n"
def getResult(self, res):
return "\n".join(self._getResult(res))
def getResultObj(self, res):
return self._getResult(res)
def _getResult(self, res):
"""Show Frontend/Backends. To do this, we extract info from
the stat command and filter out by a specific
switch (FRONTEND/BACKEND)"""
if not self.switch:
raise Exception("No action specified")
result = []
lines = res.split('\n')
cl = re.compile("^[^,].+," + self.switch.upper() + ",.*$")
for e in lines:
me = re.match(cl, e)
if me:
print(e)
result.append(e.split(",")[0])
return result
class showFrontends(showFBEnds):
"""Show frontends command."""
switch = "frontend"
helpTxt = "List all Frontends."
class showBackends(showFBEnds):
"""Show backends command."""
switch = "backend"
helpTxt = "List all Backends."
class showInfo(Cmd):
"""Show info HAProxy command"""
cmdTxt = "show info\r\n"
helpTxt = "Show info on HAProxy instance."
def getResultObj(self, res):
resDict = {}
for line in res.split('\n'):
k, v = line.split(':')
resDict[k] = v
return resDict
class showSessions(Cmd):
"""Show sess HAProxy command"""
cmdTxt = "show sess\r\n"
helpTxt = "Show HAProxy sessions."
def getResultObj(self, res):
return res.split('\n')
class baseStat(Cmd):
"""Base class for stats commands."""
def getDict(self, res):
# clean response
res = re.sub(r'^# ', '', res, re.MULTILINE)
res = re.sub(r',\n', '\n', res, re.MULTILINE)
res = re.sub(r',\n\n', '\n', res, re.MULTILINE)
csv_string = StringIO(res)
return csv.DictReader(csv_string, delimiter=',')
class showServers(baseStat):
"""Show all servers. If backend is given, show only servers for this backend. """
cmdTxt = "show stat\r\n"
helpTxt = "Lists all servers. Filter for servers in backend, if set."
def getResultObj(self, res):
servers = []
reader = self.getDict(res)
for row in reader:
row = OrderedDict(row)
# show only server
if row['svname'] in ['BACKEND', 'FRONTEND']:
continue
# filter server for given backend
if self.args['backend'] and row['pxname'] != self.args['backend']:
continue
# add id
row['id'] = f"{row['pxname']}/{row['svname']}"
row.move_to_end('id', last=False)
servers.append(dict(row))
return servers
@@ -1,83 +0,0 @@
# pylint: disable=locally-disabled, too-few-public-methods, no-self-use, invalid-name
"""conn.py - Connection module."""
import re
from socket import socket, AF_INET, AF_UNIX, SOCK_STREAM
from haproxy import const
class HapError(Exception):
"""Generic exception for haproxyctl."""
pass
class HaPConn(object):
"""HAProxy Socket object.
This class abstract the socket interface so
commands can be sent to HAProxy and results received and
parse by the command objects"""
def __init__(self, sfile, socket_module=socket):
"""Initializes an HAProxy and opens a connection to it
(sfile, type) -> Path for the UNIX socket"""
self.sock = None
sfile = sfile.strip()
stype = AF_UNIX
self.socket_module = socket_module
mobj = re.match(
'(?P<proto>unix://|tcp://)(?P<addr>[^:]+):*(?P<port>[0-9]*)$', sfile)
if mobj:
proto = mobj.groupdict().get('proto', None)
addr = mobj.groupdict().get('addr', None)
port = mobj.groupdict().get('port', '')
if not addr or not proto:
raise HapError('Could not determine type of socket.')
if proto == const.HAP_TCP_PATH:
if not port:
raise HapError('When using a tcp socket, a port is needed.')
stype = AF_INET
sfile = (addr, int(port))
if proto == const.HAP_UNIX_PATH:
stype = AF_UNIX
sfile = addr
# Fallback should be sfile/AF_UNIX by default
self.sfile = (sfile, stype)
self.open()
def open(self):
"""Opens a connection for the socket.
This function should only be called if
self.closed() method was called"""
sfile, stype = self.sfile
self.sock = self.socket_module(stype, SOCK_STREAM)
self.sock.connect(sfile)
def sendCmd(self, cmd, objectify=False):
"""Receives a command obj and sends it to the socket. Receives the output and passes it
through the command to parse it.
objectify -> Return an object instead of plain text"""
res = ""
try:
self.sock.send(cmd.getCmd())
except TypeError:
self.sock.send(bytearray(cmd.getCmd(), 'ASCII'))
output = self.sock.recv(const.HAP_BUFSIZE)
while output:
res += output.decode('UTF-8')
output = self.sock.recv(const.HAP_BUFSIZE)
if objectify:
return cmd.getResultObj(res)
return cmd.getResult(res)
def close(self):
"""Closes the socket"""
self.sock.close()
@@ -1,7 +0,0 @@
"""const.py - Constants for haproxyctl."""
HAP_OK = 1
HAP_ERR = 2
HAP_SOCK_ERR = 3
HAP_BUFSIZE = 8192
HAP_UNIX_PATH = 'unix://'
HAP_TCP_PATH = 'tcp://'
@@ -1,291 +0,0 @@
# pylint: disable=star-args, locally-disabled, too-few-public-methods, no-self-use, invalid-name
"""test_cmds.py - Unittests related to command implementations."""
import sys, os, unittest
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from haproxy import cmds
class TestCommands(unittest.TestCase):
"""Tests all of the commands."""
def setUp(self):
self.maxDiff = None
self.pem_cert_content = """
-----BEGIN CERTIFICATE-----
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
+VCAIWMkC36Dvg==
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIJKgIBAAKCAgEAvsNKU59F2gpl1fzbk7hD64Q3zk1hH9Im1IsN9ppF5tqdsPPG
n3Q8QgcKpnJ/SrtnPl4Xqp3bJyV20lJWTxuVU7ImM8g4tStQB4XojTtv8A4E7/wN
T0beUBFI5CQ+YLzRqEYBbcXV5pxx/TyqdxYIbEopeZ4v2ZsCwDiTr7qkirxhEGow
ITZuRFD+xwBrykvuazmFx441Z+qMcLXbMinJq++d81VnrPtCXAcSHdFqOTUhocvI
Tv736hDfNeoW0zqwG9eW9WHCyLOCQfdmh03zXJy8eg/UxaocYdfSSVERD2pGse5K
6oeyMkdfyC1uojZn0NU5RQS6dsiQiZPCpiN7m2nuDhFAlSAB2xq7I3TY1N15R5yL
4kO8DcUcmkC0Du0Nch41zI+NGGjXuLhVQhxhqy7kkMXSJI8NP9KvG3oSwOe0MUNy
jUdcywWpSExtjb6BRYLc/Sf3xVZI74EK0B0Vtzj43dAQzxGdAojzpYdH++dUOiKA
6EAu6ldrpzOMsYyNpIsMjSfInkzzWvxm4Fla7ZDodOKQ2ahaXfDrH71a9jpNB306
kKNBS7XdQxUByjwIYQ6PRmqGEtNxxfx3E8r/TVtcy0VGK8l97q8wv47Q3nglsjlx
peZoR1wS5rMBUla3Vk8YVMyOLD87RLodvRVgkpeqMmz233k7kykv1bhi8bECAwEA
AQKCAgEAswbSPXJPetahRdcdNyAKVgBq4ykJinSOTpAF1bZo/cOTlFrjwAe0+X5k
R1tTDQ6dURG7AjtNTgrB3Za6O1m2paqeYaB5X8U7QSQx4EG0xsRRa+vPjeQDhX8D
OmCtTdpGpLa2Zo/xM5EFBVUm4cYCt6ZOED4dyAnK5hzytUvjWfR6343Yh4LurxyY
TqidgGgMZALDA0n54wFjNe/lu8kt5Ddns9MmDlhrqbRVEzjSiMfNPWvjHAf7IGcf
JBkBvNDqL+b/XGCYDgUxrLkDNt44E2VhGOi8lZkVM9n5FyeGbEIgAKKTGlGpMbh8
MoA4wPFwMrO5IIXUfN+zjfnnBkZsnAomGQYDh/hrsQPwU7MoyfO0Wzw+RzLWK8JH
EnjR7O/Lgh+A2AdLhCLiRC5td2uuJ2yLRIRUlcQPsCsYnCCL6Ip9IwK1idmQySGw
bG83decXNSJUv5h3qF6f3fl+JPrHnAbviBzEJ67xAf1MdHbFxwYvRFVfEHj9RZ3W
z+cw7ofD8XVHTfXn0XipvYqI/bVsitMXI35pOt+/ZV8rjJlXopw+IV6U9/60cBkk
BXC7ONDyH2pNwxPbRgcLm2sEK0L9qhxRzCj0iD1WyOAiFJX4ytVbJhR7pt0goiun
i2XDh2l8hoK1lKZNS/yJ+VhnbX595mdqScmIXD8utlgK8f0bLfECggEBAORXimSK
gzegnsBjieTtzC6MmRRxxN46vnMZ2LCeLMxhs3vM7LBcBfsQYqbt/FVFtYBRpr+d
TGTmfPXqKuSqbtAbghxAMo/lECXzALa0nQSsz1fFhX8B7slFarsDmmCb1GmXF/kG
ku/Uoa7jmY3htBj5rjVHjDKPZFVetU+2wbuwlU17Bj4nlSzqud4NMlu56pm3FZ/1
BAhMxm3z6dLnOgqJzpN1QmKZHNkjLmi8fza/HQM5pP3DpQcPiyuLzywGIqHaO1qT
OIdpZfLEvNpMV7bJ2bagv5nX3TVRWWsBkh0HCAuH30qqaVPpQvkPem1zsM3x+D5q
+PhMIPGpbQiUyCUCggEBANXefd0ZcJymG15WJyO44eFwzgMz9ezfdB8INa+vCOiZ
Y7FtYDgEKu4uzBxtMjO4mQO6DCkfi7JwTJFN4ag3dJEJNGmrf7Xe84IAImJQk0Of
BojAXCFAuNf1Xl3prkvnvtzNirwQMHCUbv5wYzOqglgj2i/hjIj3/Wbt91riq5j+
4qQT4kkw/XgCtbQ27HohKIcC/mXbHchEi7NtXrGoM1xqmu1mGH1uul3LQ6p5VwHc
ZFiIAC0awsx9Qe9khZ5EGpZuS0tqJsREcv8ygYMvWcPJEv8aMQM7Nj4biA5rKEgo
L+66ibpntldvbz2qntEvJ2rKzGci0RDUQHy4sW8/d50CggEBAKCZaX7ZZPzk/YL2
/2+CSQ+cV7ZnZj2fN4Ag96UROxTsyp4SPY60yogQuDIMRGN9SfDcfNlcOvTkn5Me
hdiafqHkFxjjlixawYbPaPsYAS/ek156UDBKHbZ2GmE6YYP9VeKGIJhHpWUFOkqV
TdTaoB7IzVwv3E1bSQg6Om+8bHoj8n6yPmvMz0DuPpgM1BRrqLNAb/c3DwT/ari+
ywBJHSt4TVCtMmnCouWdtvB3U0ogFLnF+2N4DUPwDMQt6yJdllIb+Y706NdkrA2Z
jfJDq5WmVnf6i4gaqTzs4GVAj5HW9jOV9ti/DqGz+CTQXB1LN1lCDIVqG34XnTwb
G9LjQfkCggEAZwYAt4tTtgJGWNFDlW+wT/sZIm3bX7ncpD4+Ll0w+2s4nPXFTfaj
/4zHgkIP1t5rx2HODdlGYDS8jZpow7HDE0LN3sFgienWf5808QtDhWWLrkCLoPEe
mdl3FeJFtgby6EaTODjMPM8kEKlvACp5E6BhsIMEQc7EYNrtNvjOFKtj3go+DWfu
EeusQB3dGI/0h+UnS0WcOSbb7RkYbphJ9ZDdBNMTpQi7+ga6l9pP0XOrWwJYo2Gq
yPrl0j4oJ69C54hF+RQvjIg0pT5dKSacJTYtUnn5dkcFwDFe/yMbinbhcCynwAXJ
zqC9g4U3cCk44bbDdENPVr4IOox13NND+QKCAQEAilm2oMZoP3WGkBMTSzJl6OGd
F8NnE95noleknNFYuThhCT6T4Z1s28VpxXV7d0DTNOtXj+TzeZq4jrwkgOSZbif0
8ky4gRZmm0iFwvAu8ZXk1olHbhMZnCOfh0Qhd4bU2tSoWgWVIAQWEHUhDI7Q1rsX
s4sCjYHKuNMEKdfYvxtKeiunoFqdmT65hwM9o3TfvJfm/RChb7i/nVruXQ6IhPEM
9WYZS7hlKyqVBESJuonR15biy7Xov5ELl6A821cskZO3vTwtlBSeCDiqaeVLpKR3
aYwf5YZo7v+N8KBSLEdLNjoKK4PfXUdczD7uOUllbd4/MRgCn4EmFvmpljGiEQ==
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
-----END CERTIFICATE-----
"""
self.Resp = {
"disable": "disable server redis-ro/redis-ro0",
"set-server-agent": "set server redis-ro/redis-ro0 agent up",
"set-server-health": "set server redis-ro/redis-ro0 health stopping",
"set-server-state": "set server redis-ro/redis-ro0 state drain",
"set-server-weight": "set server redis-ro/redis-ro0 weight 10",
"frontends": "show stat",
"info": "show info",
"sessions": "show sess",
"servers": "show stat",
"show-ssl-crt-lists": "show ssl crt-list",
"show-ssl-crt-list": "show ssl crt-list -n /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist",
"show-ssl-certs": "show ssl cert",
"show-ssl-cert": "show ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem",
"add-to-crt-list": "add ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem",
"del-from-crt-list": "del ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem",
"new-ssl-cert": "new ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem",
"update-ssl-cert": "set ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem <<\n%s" % self.pem_cert_content,
"del-ssl-cert": "del ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem",
"commit-ssl-cert": "commit ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem",
"abort-ssl-cert": "abort ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem",
}
self.Resp = dict([(k, v + "\r\n") for k, v in self.Resp.items()])
def test_setServerAgent(self):
"""Test 'set server agent' command"""
args = {"backend": "redis-ro", "server": "redis-ro0", "value": "up"}
cmdOutput = cmds.setServerAgent(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["set-server-agent"])
def test_setServerHealth(self):
"""Test 'set server health' command"""
args = {"backend": "redis-ro", "server": "redis-ro0", "value": "stopping"}
cmdOutput = cmds.setServerHealth(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["set-server-health"])
def test_setServerState(self):
"""Test 'set server state' command"""
args = {"backend": "redis-ro", "server": "redis-ro0", "value": "drain"}
cmdOutput = cmds.setServerState(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["set-server-state"])
def test_setServerWeight(self):
"""Test 'set server weight' command"""
args = {"backend": "redis-ro", "server": "redis-ro0", "value": "10"}
cmdOutput = cmds.setServerWeight(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["set-server-weight"])
def test_showFrontends(self):
"""Test 'frontends/backends' commands"""
args = {}
cmdOutput = cmds.showFrontends(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["frontends"])
def test_showInfo(self):
"""Test 'show info' command"""
cmdOutput = cmds.showInfo().getCmd()
self.assertEqual(cmdOutput, self.Resp["info"])
def test_showSessions(self):
"""Test 'show sess' command"""
cmdOutput = cmds.showSessions().getCmd()
self.assertEqual(cmdOutput, self.Resp["sessions"])
def test_showServers(self):
"""Test 'show stat' command"""
args = {"backend": "redis-ro"}
cmdOutput = cmds.showServers(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["servers"])
def test_showSslCrtLists(self):
"""Test 'show ssl crt-list' command"""
cmdOutput = cmds.showSslCrtLists().getCmd()
self.assertEqual(cmdOutput, self.Resp["show-ssl-crt-lists"])
def test_showSslCrtList(self):
"""Test 'show ssl crt-list <crt-list>' command"""
args = {
"crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist",
}
cmdOutput = cmds.showSslCrtList(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["show-ssl-crt-list"])
def test_showSslCerts(self):
"""Test 'show ssl cert' command"""
cmdOutput = cmds.showSslCerts().getCmd()
self.assertEqual(cmdOutput, self.Resp["show-ssl-certs"])
def test_showSslCert(self):
"""Test 'show ssl cert <certfile>' command"""
args = {
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem"
}
cmdOutput = cmds.showSslCert(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["show-ssl-cert"])
def test_addToSslCrtList(self):
"""Test 'add ssl crt-list <crt-list> <certfile>' command"""
args = {
"crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist",
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem"
}
cmdOutput = cmds.addToSslCrtList(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["add-to-crt-list"])
def test_delFromSslCrtList(self):
"""Test 'del ssl crt-list <crt-list> <certfile>' command"""
args = {
"crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist",
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem"
}
cmdOutput = cmds.delFromSslCrtList(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["del-from-crt-list"])
def test_newSslCrt(self):
"""Test 'new ssl cert <certfile>' command"""
args = {
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem",
}
cmdOutput = cmds.newSslCrt(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["new-ssl-cert"])
def test_updateSslCrt(self):
"""Test 'set ssl cert <certfile> <payload>' command"""
args = {
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem",
"payload": "%s" % self.pem_cert_content
}
cmdOutput = cmds.updateSslCrt(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["update-ssl-cert"])
def test_delSslCrt(self):
"""Test 'del ssl cert <certfile>' command"""
args = {
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem",
}
cmdOutput = cmds.delSslCrt(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["del-ssl-cert"])
def test_commitSslCrt(self):
"""Test 'commit ssl cert <certfile>' command"""
args = {
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem",
}
cmdOutput = cmds.commitSslCrt(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["commit-ssl-cert"])
def test_abortSslCrt(self):
"""Test 'abort ssl cert <certfile>' command"""
args = {
"certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem",
}
cmdOutput = cmds.abortSslCrt(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["abort-ssl-cert"])
if __name__ == '__main__':
unittest.main()
@@ -1,59 +0,0 @@
# pylint: disable=locally-disabled, too-few-public-methods, no-self-use, invalid-name, broad-except
"""test_conn.py - Unittests related to connections to HAProxy."""
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from haproxy import conn
import unittest
from socket import AF_INET, AF_UNIX
class SimpleConnMock(object):
"""Simple socket mock."""
def __init__(self, stype, stream):
self.stype = stype
self.stream = stream
def connect(self, addr):
"""Mocked socket.connect method."""
pass
class TestConnection(unittest.TestCase):
"""Tests different aspects of haproxyctl's connections to HAProxy."""
def testConnSimple(self):
"""Tests that connection to non-protocol path works and fallsback to UNIX socket."""
sfile = "/some/path/to/socket.sock"
c = conn.HaPConn(sfile, socket_module=SimpleConnMock)
addr, stype = c.sfile
self.assertEqual(sfile, addr)
self.assertEqual(stype, AF_UNIX)
def testConnUnixString(self):
"""Tests that unix:// protocol works and connects to a socket."""
sfile = "unix:///some/path/to/socket.socket"
c = conn.HaPConn(sfile, socket_module=SimpleConnMock)
addr, stype = c.sfile
self.assertEqual("/some/path/to/socket.socket", addr)
self.assertEqual(stype, AF_UNIX)
def testConnTCPString(self):
"""Tests that tcp:// protocol works and connects to an IP."""
sfile = "tcp://1.2.3.4:8080"
c = conn.HaPConn(sfile, socket_module=SimpleConnMock)
addr, stype = c.sfile
ip, port = addr
self.assertEqual("1.2.3.4", ip)
self.assertEqual(8080, port)
self.assertEqual(stype, AF_INET)
def testConnTCPStringNoPort(self):
"""Tests that passing a tcp:// address with no port, raises an Exception."""
sfile = "tcp://1.2.3.4"
# Not using assertRaises because we still support 2.6
try:
conn.HaPConn(sfile, socket_module=SimpleConnMock)
raise Exception('Connection should have thrown an exception')
except conn.HapError:
pass
if __name__ == '__main__':
unittest.main()
@@ -4,7 +4,6 @@ import sys
import argparse
import traceback
sys.path.append(os.path.join(os.path.dirname(__file__), 'lib'))
from haproxy.conn import HaPConn
from haproxy import cmds
@@ -9,7 +9,6 @@ import OpenSSL
import json
from typing import List
sys.path.append(os.path.join(os.path.dirname(__file__), 'lib'))
from haproxy.conn import HaPConn
from haproxy import cmds