Merge pull request #2244 from andeman/haproxy_live_cert_update

net/haproxy: support runtime certificate updates
This commit is contained in:
Frank Wall
2021-02-23 16:53:31 +01:00
committed by GitHub
9 changed files with 1117 additions and 105 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ PLUGIN_NAME= haproxy
PLUGIN_VERSION= 2.26
PLUGIN_REVISION= 1
PLUGIN_COMMENT= Reliable, high performance TCP/HTTP load balancer
PLUGIN_DEPENDS= haproxy20
PLUGIN_DEPENDS= haproxy
PLUGIN_MAINTAINER= opnsense@moov.de
.include "../../Mk/plugins.mk"
@@ -1,12 +1,9 @@
# pylint: disable=locally-disabled, too-few-public-methods, no-self-use, invalid-name
"""cmds.py - Implementations of the different HAProxy commands"""
import re
import csv
import json
from io import StringIO
class Cmd():
"""Cmd - Command base class"""
req_args = []
@@ -42,44 +39,200 @@ class Cmd():
"""
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):
"""Set server agent command."""
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):
"""Set server health command."""
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):
"""Set server state command."""
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):
"""Set server weight command."""
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"""
@@ -107,22 +260,20 @@ class showFBEnds(Cmd):
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"
@@ -136,7 +287,6 @@ class showInfo(Cmd):
return resDict
class showSessions(Cmd):
"""Show sess HAProxy command"""
cmdTxt = "show sess\r\n"
@@ -145,7 +295,6 @@ class showSessions(Cmd):
def getResultObj(self, res):
return res.split('\n')
class baseStat(Cmd):
"""Base class for stats commands."""
@@ -158,64 +307,11 @@ class baseStat(Cmd):
csv_string = StringIO(res)
return csv.DictReader(csv_string, delimiter=',')
def getBootstrapOutput(self, **kwargs):
rows = kwargs['rows']
# search
if kwargs['search']:
filtered_rows = []
for row in rows:
def inner(row):
for k, v in row.items():
if kwargs['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[kwargs['sort_col']], reverse=True if kwargs['sort_dir'] == 'desc' else False)
# pager
total = len(rows)
pages = [rows[i:i + kwargs['page_rows']] for i in range(0, total, kwargs['page_rows'])]
if pages and (kwargs['page'] > len(pages) or kwargs['page'] < 1):
raise KeyError(f"Current page {kwargs['page']} does not exist. Available pages: {len(pages)}")
page = pages[kwargs['page'] - 1] if pages else []
return json.dumps({
"rows": page,
"total": total,
"rowCount": kwargs['page_rows'],
"current": kwargs['page']
})
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 getResult(self, res):
if self.args['output'] == 'json':
return json.dumps(self.getResultObj(res))
if self.args['output'] == 'bootstrap':
rows = self.getResultObj(res)
args = {
"rows": rows,
"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'],
}
return self.getBootstrapOutput(**args)
return self.getResultObj(res)
def getResultObj(self, res):
servers = []
@@ -70,7 +70,7 @@ class HaPConn(object):
output = self.sock.recv(const.HAP_BUFSIZE)
while output:
res += output.decode('ASCII')
res += output.decode('UTF-8')
output = self.sock.recv(const.HAP_BUFSIZE)
if objectify:
@@ -5,69 +5,288 @@ 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-----
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
-----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",
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"}
cmdSetServerAgent = cmds.setServerAgent(**args).getCmd()
self.assertEqual(cmdSetServerAgent, self.Resp["set-server-agent"])
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"}
cmdSetServerHealth = cmds.setServerHealth(**args).getCmd()
self.assertEqual(cmdSetServerHealth, self.Resp["set-server-health"])
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"}
cmdSetServerState = cmds.setServerState(**args).getCmd()
self.assertEqual(cmdSetServerState, self.Resp["set-server-state"])
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"}
cmdSetServerState = cmds.setServerWeight(**args).getCmd()
self.assertEqual(cmdSetServerState, self.Resp["set-server-weight"])
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 = {}
cmdFrontends = cmds.showFrontends(**args).getCmd()
self.assertEqual(cmdFrontends, self.Resp["frontends"])
cmdOutput = cmds.showFrontends(**args).getCmd()
self.assertEqual(cmdOutput, self.Resp["frontends"])
def test_showInfo(self):
"""Test 'show info' command"""
cmdShowInfo = cmds.showInfo().getCmd()
self.assertEqual(cmdShowInfo, self.Resp["info"])
cmdOutput = cmds.showInfo().getCmd()
self.assertEqual(cmdOutput, self.Resp["info"])
def test_showSessions(self):
"""Test 'show info' command"""
cmdShowInfo = cmds.showSessions().getCmd()
self.assertEqual(cmdShowInfo, self.Resp["sessions"])
"""Test 'show sess' command"""
cmdOutput = cmds.showSessions().getCmd()
self.assertEqual(cmdOutput, self.Resp["sessions"])
def test_showServers(self):
"""Test 'show info' command"""
"""Test 'show stat' command"""
args = {"backend": "redis-ro"}
cmdShowInfo = cmds.showServers(**args).getCmd()
self.assertEqual(cmdShowInfo, self.Resp["servers"])
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()
@@ -19,6 +19,17 @@ VALID_COMMANDS = {
"show-info": cmds.showInfo,
"show-sessions": cmds.showSessions,
"show-servers": cmds.showServers,
"show-ssl-crt-lists": cmds.showSslCrtLists,
"show-ssl-crt-list": cmds.showSslCrtList,
"show-ssl-certs": cmds.showSslCerts,
"show-ssl-cert": cmds.showSslCert,
"add-to-crt-list": cmds.addToSslCrtList,
"del-from-crt-list": cmds.delFromSslCrtList,
"new-ssl-cert": cmds.newSslCrt,
"update-ssl-cert": cmds.updateSslCrt,
"del-ssl-cert": cmds.delSslCrt,
"commit-ssl-cert": cmds.commitSslCrt,
"abort-ssl-cert": cmds.abortSslCrt,
}
def get_args():
@@ -40,7 +51,7 @@ def get_args():
)
parser.add_argument(
'--server-ids',
help='Attempt action on a list of server, specified as a comma seperated list e.g. back1/server1,back2/server3',
help='Attempt action on a list of server, specified as a comma separated list e.g. back1/server1,back2/server3',
default=None
)
parser.add_argument(
@@ -48,6 +59,21 @@ def get_args():
help='Specify value for a set command.',
default=None
)
parser.add_argument(
'--payload',
help='Specify payload for a update command. either string or filepath',
default=None
)
parser.add_argument(
'--crt-list',
help='Set a filepath for a crt-list.',
default=None
)
parser.add_argument(
'--certfile',
help='Set a filepath for a certificate.',
default=None
)
parser.add_argument(
'--output',
help='Specify output format.',
@@ -89,6 +115,14 @@ def get_args():
return parser.parse_args()
args = get_args()
if args.payload and os.path.isfile(args.payload):
with open(args.payload) as payload_file:
payload_content = ""
for line in payload_file:
if line.rstrip():
payload_content += line
args.payload = payload_content
command_class = VALID_COMMANDS.get(args.command, None)
command_args = {key: val for key, val in vars(args).items() if key != "command"}
@@ -108,7 +142,6 @@ try:
if result:
print(f"{server_id}: {result.strip()}")
con.close()
else:
# single
con = HaPConn(SOCKET)
File diff suppressed because it is too large Load Diff
@@ -75,3 +75,28 @@ command:/usr/local/opnsense/scripts/OPNsense/HAProxy/socketCommand.py
parameters: set-server-weight --server-ids %s --value %s
type:script_output
message:change haproxy weight for multiple server
[cert_diff]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py
parameters: diff --output json --frontends %s
type:script_output
message:Show diff between configured ssl certificates and certs from HAProxy memory for multiple frontends
[cert_sync]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py
parameters: sync --frontends %s --output json
type:script_output
message:Sync ssl certificates into HAProxy memory for multiple frontends
[cert_diff_bulk]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py diff --output json
parameters:
type:script_output
message:Show diff between configured ssl certificates and certs from HAProxy memory for all frontends
[cert_sync_bulk]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync --output json
parameters:
type:script_output
message:Sync ssl certificates into HAProxy memory for all frontends
description:Sync ssl certificates changes into HAProxy memory
@@ -1,2 +1,3 @@
haproxy.conf:/usr/local/etc/haproxy.conf
rc.conf.d:/etc/rc.conf.d/haproxy
sslCerts.yaml:/usr/local/etc/haproxy/sslCerts.yaml
@@ -0,0 +1,61 @@
#
# Automatically generated configuration.
# Do not edit this file manually.
#
# List all frontends with configured ssl certificates in config.xml
{# ################## #}
{# ##### Macros ##### #}
{# ################## #}
{% macro getCA(refId) -%}
{% set result = '{}' %}
{% for data in helpers.getNodeByTag('ca') if data.refid == refId %}
{{ data.crt -}}
{% else %}
{{ "{}" }}
{% endfor %}
{%- endmacro %}
{% macro getCert(refId, indent=4) -%}
{% for data in helpers.getNodeByTag('cert') if data.refid == refId %}
{% if data.caref %}
{% do data.update({'ca': getCA(data.caref)}) %}
{% else %}
{% do data.update({'ca': {} }) %}
{% endif %}
crt: {{ data.crt }}
key: {{ data.prv }}
ca: {{ data.ca }}
{% endfor %}
{%- endmacro %}
{# ################## #}
{# ##### Main ##### #}
{# ################## #}
{% set enabled_frontends = [] %}
{% set crt_list_template = "/tmp/haproxy/ssl/%s.certlist" %}
{% set cert_template = "/tmp/haproxy/ssl/%s.pem" %}
{% for frontend in helpers.toList('OPNsense.HAProxy.frontends.frontend') %}
{% set certs = [] %}
{% for cert in frontend.get('ssl_default_certificate', '').split(',') + frontend.get('ssl_certificates', '').split(',') if cert %}
{% do certs.append(cert) %}
{% endfor %}
{% do frontend.update({'certs': certs}) %}
{% if frontend.enabled == '1' and frontend.ssl_enabled == '1' and frontend.certs|length > 0 %}
{% do enabled_frontends.append(frontend) %}
{% endif %}
{% endfor %}
{% if helpers.exists('OPNsense.HAProxy.frontends') and enabled_frontends|length > 0 %}
frontends:
{% for frontend in enabled_frontends %}
"{{ frontend.id }}":
name: {{ frontend.name }}
crt_list_path: {{ crt_list_template % frontend.id }}
certs:
{% for cert_refid in frontend.certs %}
{{ cert_refid }}:
path: {{ cert_template % cert_refid }}
default: {{ "True" if frontend.ssl_default_certificate == cert_refid else "False" }}
{{ getCert(cert_refid) | indent( width=8, indentfirst=True) -}}
{% endfor %}
{% endfor %}
{% else %}
frontends: {}
{% endif %}