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
commit d7e16d5512
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1117 additions and 105 deletions

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"

View file

@ -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 = []

View file

@ -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:

View file

@ -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-----
MIIGNjCCBR6gAwIBAgITAPoWnilNUBNcAb8iJ2dgK1eXeTANBgkqhkiG9w0BAQsF
ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMTAyMDMw
ODQ2MTBaFw0yMTA1MDQwODQ2MTBaMBoxGDAWBgNVBAMTD3Rlc3QuYW5kZW1hbi5k
ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL7DSlOfRdoKZdX825O4
Q+uEN85NYR/SJtSLDfaaRebanbDzxp90PEIHCqZyf0q7Zz5eF6qd2ycldtJSVk8b
lVOyJjPIOLUrUAeF6I07b/AOBO/8DU9G3lARSOQkPmC80ahGAW3F1eaccf08qncW
CGxKKXmeL9mbAsA4k6+6pIq8YRBqMCE2bkRQ/scAa8pL7ms5hceONWfqjHC12zIp
yavvnfNVZ6z7QlwHEh3Rajk1IaHLyE7+9+oQ3zXqFtM6sBvXlvVhwsizgkH3ZodN
81ycvHoP1MWqHGHX0klREQ9qRrHuSuqHsjJHX8gtbqI2Z9DVOUUEunbIkImTwqYj
e5tp7g4RQJUgAdsauyN02NTdeUeci+JDvA3FHJpAtA7tDXIeNcyPjRho17i4VUIc
Yasu5JDF0iSPDT/Srxt6EsDntDFDco1HXMsFqUhMbY2+gUWC3P0n98VWSO+BCtAd
Fbc4+N3QEM8RnQKI86WHR/vnVDoigOhALupXa6czjLGMjaSLDI0nyJ5M81r8ZuBZ
Wu2Q6HTikNmoWl3w6x+9WvY6TQd9OpCjQUu13UMVAco8CGEOj0ZqhhLTccX8dxPK
/01bXMtFRivJfe6vML+O0N54JbI5caXmaEdcEuazAVJWt1ZPGFTMjiw/O0S6Hb0V
YJKXqjJs9t95O5MpL9W4YvGxAgMBAAGjggJrMIICZzAOBgNVHQ8BAf8EBAMCBaAw
HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
VR0OBBYEFHQLXiD/GxQD11ocGiFauejS5RRmMB8GA1UdIwQYMBaAFMDMA0a5WCDM
XHJw8+EuyyCm9Wg6MHcGCCsGAQUFBwEBBGswaTAyBggrBgEFBQcwAYYmaHR0cDov
L29jc3Auc3RnLWludC14MS5sZXRzZW5jcnlwdC5vcmcwMwYIKwYBBQUHMAKGJ2h0
dHA6Ly9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAaBgNVHREEEzAR
gg90ZXN0LmFuZGVtYW4uZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC
3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcw
ggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdQAW6GnB0ZXq18P4lxrj8HYB94zhtp0x
qFIYtoN/MagVCAAAAXdnSPbpAAAEAwBGMEQCICAST5iJD7DVrcKRvu9rvNVVnkOW
hAYUgihWr/1Gu6VdAiAcRcZYBP0hIHmFExM9ehJ+J7YmqM35SyiC7s0chsNdHQB2
AN2ZNPyl5ySAyVZofYE0mQhJskn3tWnYx7yrP1zB825kAAABd2dI+N0AAAQDAEcw
RQIgaaUndm8O3+nCl5OHTf6rOdi9VF9szVckdgDargdWKkgCIQCAjW4UvuMIv4Bt
c6auowPcpdqHjL8XRcztJA3XUGRGHTANBgkqhkiG9w0BAQsFAAOCAQEABza4/ocY
J/XwN8PP+Ane7fVerqL7mRfhzJhxz4mbCPfv4Drq3kUu9fnhR/vaGgdaNdnO83a9
PUBCm6FCPMcVwX0uKDJ9J4Xj+SVjnVu4+7uhS5LyygtaegoBZyMb5ppxWH1n5r47
10ug+KptERFf1datb8/jsEVF7rYCtPXBygjfGAbGuCxViakr4BNcOBPNL+MusfvP
qpH8kEyPAIwHX02XvvpLTy77qiyTpQSuFOusOJptNNqBUeBehqpf8FHn01fnKkcW
pKmFJ2e2VSnTZIBJvD58HMR+WNAEp7tHffHk2z/mPPtdRdxW5Zieoe5+6+HDtwgG
+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()

View file

@ -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)

View file

@ -0,0 +1,577 @@
#!/usr/bin/env python3
# Sync ssl certificates from a yaml file into haproxy memory
import os
import sys
import argparse
import yaml
import base64
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
class SyncWithTarget:
""" Base class for sync objects to a target """
def __init__(self, socket='/var/run/haproxy.socket'):
self.socket = socket
def _execute_remote_cmd(self, command_class, **command_args):
con = HaPConn(self.socket)
if con:
command_obj = command_class(**command_args)
result = con.sendCmd(command_obj, objectify=True)
con.close()
return result
def _calc_diff(self):
""" return needed operations to get remote object in sync """
raise Exception("need to be implemented!")
def diff_list(self, first: List, second: List):
second = set(second)
return [item for item in first if item not in second]
class Diff(SyncWithTarget):
""" Represents a full diff to sync with remote """
def __init__(self, crt_lists=None):
super().__init__()
if crt_lists is None:
crt_lists = []
self._crt_lists = crt_lists
self._diff = self._calc_diff()
self._status = self._get_status()
self._transactions = self._get_transactions()
@property
def diff(self):
return self._diff
@property
def crt_lists(self):
return self._crt_lists
@property
def transactions(self):
return self._transactions
@property
def status(self):
return self._status
def _calc_diff(self):
result = {}
for crt_list in self:
result[crt_list.frontend_id] = crt_list.diff
return result
def abort(self, output_format):
""" Abort transactions"""
aborted = []
for certfile in self.transactions:
certfile = certfile.replace('*/', "/")
output = self._execute_remote_cmd(cmds.abortSslCrt, certfile=certfile)
aborted.append({
"cert": certfile,
"output": output,
})
if output_format == 'json':
print(json.dumps({'abort': aborted}))
if output_format == 'raw':
for item in aborted:
print(f"ABORT transaction: {item['cert']}")
print(f" {repr(item['output'])}")
def _get_transactions(self):
""" get open transactions"""
return self._execute_remote_cmd(cmds.showSslCerts)['transaction']
def _get_status(self):
status = {}
crt_list: CertList
for crt_list in self.crt_lists:
status[crt_list.frontend_id] = {
"frontend_name": crt_list.frontend_name,
"path": crt_list.path,
"local_certs": crt_list.local,
"local_default": crt_list.local_default,
"remote_certs": crt_list.remote,
"remote_default": crt_list.remote_default,
}
cert: Cert
status[crt_list.frontend_id]['certs'] = {}
for cert in crt_list.certs:
status[crt_list.frontend_id]['certs'][cert.cert_id] = {
'path': cert.path,
'local': cert.local,
'remote': cert.remote,
}
return status
def show_status(self, output_format):
""" Shows current local and remote state """
if output_format == 'json':
print(json.dumps(self.status))
if output_format == 'raw':
print("## STATUS ##")
for frontend_id, crt_list in self.status.items():
print(f"CRT_LIST: {crt_list['path']}")
print(f" FRONTEND NAME: {crt_list['frontend_name']}")
print(f" FRONTEND ID: {frontend_id}")
print(f" LOCAL CERTS: {crt_list['local_certs']}")
print(f" REMOTE CERTS: {crt_list['remote_certs']}")
print(f" LOCAL DEFAULT: {crt_list['local_default']}")
print(f" REMOTE DEFAULT: {crt_list['remote_default']}")
for cert_id, cert in crt_list['certs'].items():
print()
print(f" CERT: {cert['path']}")
print(f" LOCAL: {cert['local']}")
print(f" REMOTE: {cert['remote']}")
print()
def show_diff(self, output_format):
""" Shows what will be synced to target """
if output_format == 'json':
print(json.dumps(self.diff))
if output_format == 'raw':
print("## DIFF ##")
for frontend_id, diff in self.diff.items():
print(f"CRT LIST: {diff['path']}")
print(f" FRONTEND NAME: {diff['frontend_name']}")
print(f" FRONTEND ID: {diff['frontend_id']}")
for update in diff['update']:
print(f" CERT UPDATE:")
print(f" Cert: {update['certfile']}")
print(f" Serial: {update['meta']['Serial']}")
print(f" Issuer: {update['meta']['Issuer']}")
print(f" Subject: {update['meta']['Subject']}")
else:
if not diff['update']:
print(f" CERT UPDATE: []")
print(f" CERT ADD : {diff['add']}")
print(f" CERT DEL : {diff['del']}")
def show_transactions(self, output_format):
if output_format == 'json':
print(json.dumps({'transactions': self.transactions}))
if output_format == 'raw':
print("## OPEN TRANSACTIONS ##")
for cert in self.transactions:
print(cert)
def sync(self, output_format):
""" Sync to target """
sync = {}
certs_to_delete = []
for frontend_id, diff in self.diff.items():
sync[frontend_id] = {
'frontend_name': diff['frontend_name'],
'frontend_id': diff['frontend_id'],
'path': diff['path'],
'add': [],
'remove': [],
'update': [],
'del': []
}
# update cert content
for cert in diff['update']:
messages = []
if cert['certfile'] in diff['add']:
output = self._execute_remote_cmd(cmds.newSslCrt, certfile=cert['certfile'])
messages.append(output)
output = self._execute_remote_cmd(cmds.updateSslCrt, certfile=cert['certfile'], payload=cert['pem'])
messages.append(output)
output = self._execute_remote_cmd(cmds.commitSslCrt, certfile=cert['certfile'])
messages.append(output)
sync[frontend_id]['update'].append({
'cert': cert['certfile'],
'messages': messages
})
# add to crt-list
for cert in diff['add']:
messages = []
output = self._execute_remote_cmd(cmds.addToSslCrtList, crt_list=diff['path'], certfile=cert)
messages.append(output)
sync[frontend_id]['add'].append({
'cert': cert,
'messages': messages
})
# remove from crt-list
for cert in diff['del']:
messages = []
output = self._execute_remote_cmd(cmds.delFromSslCrtList, crt_list=diff['path'], certfile=cert)
messages.append(output)
certs_to_delete.append(cert.split(":")[0])
sync[frontend_id]['remove'].append({
'cert': cert,
'messages': messages
})
# delete unused certs operation - haproxy does not allow to delete certs in use
for cert in certs_to_delete:
messages = []
output = self._execute_remote_cmd(cmds.delSslCrt, certfile=cert)
messages.append(output)
sync[frontend_id]['del'].append({
'cert': cert,
'messages': messages
})
if output_format == 'json':
print(json.dumps(self.diff))
if output_format == 'raw':
print("## SYNC ##")
for frontend_id, crt_list in sync.items():
print(f"CRT-LIST: {crt_list['path']}")
print(f" FRONTEND NAME: {crt_list['frontend_name']}")
print(f" FRONTEND ID: {crt_list['frontend_id']}")
for cert in crt_list['update']:
print(f" UPDATE: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
for cert in crt_list['add']:
print(f" ADD: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
for cert in crt_list['remove']:
print(f" REMOVE: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
for cert in crt_list['del']:
print(f"\n DEL: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
print()
def __iter__(self):
return iter(self._crt_lists)
def __str__(self):
return self.status
class CertList(SyncWithTarget):
""" Represents a haproxy ssl-crt-list """
def __init__(self, path, frontend_id=None, frontend_name=None, certs=None, default_cert=None):
super().__init__()
if certs is None:
certs = []
self._path = path
self._certs = certs
self._frontend_name = frontend_name
self._frontend_id = frontend_id
self._local_default = default_cert
self._local = self._get_local_state()
self._remote_ln = self._get_remote_state(cmds.showSslCrtList, crt_list=self._path)
self._remote = [cert_ln.split(":")[0] for cert_ln in self._remote_ln]
self._diff = self._calc_diff()
@property
def path(self):
return self._path
@property
def frontend_name(self):
return self._frontend_name
@property
def frontend_id(self):
return self._frontend_id
@property
def certs(self):
return self._certs
@property
def local_default(self):
return self._local_default
@property
def remote_default(self):
return next(iter(self._remote), None)
@property
def local(self):
return self._local
@property
def remote_ln(self):
""" Certs with line number"""
return self._remote_ln
@property
def remote(self):
"""
if default certs are different return remote certs with line numbers, so they are deleted in the crt list.
This ensures that the default cert is always on top.
"""
if self._local_default is not None and self.local_default != self.remote_default:
return self._remote_ln
return self._remote
@property
def diff(self):
return self._diff
def _calc_diff(self):
""" return needed operations to get remote object in sync """
diff = {
'frontend_name': self.frontend_name,
'frontend_id': self.frontend_id,
'path': self.path,
'add': [],
'del': [],
'update': []
}
# skip when there is no remote crt list
if self.remote is None:
return diff
# certs to add, delete and update on the remote target
diff['add'] = self.diff_list(self.local, self.remote)
diff['del'] = self.diff_list(self.remote, self.local)
diff['update'] = [cert.diff for cert in self.certs if cert.diff]
return diff
def _get_local_state(self):
return [f"{repr(cert)}" for cert in self._certs]
def _get_remote_state(self, command_class, **command_args):
crt_list_data = self._execute_remote_cmd(command_class, **command_args)
return crt_list_data.get('certs', None)
def __iter__(self):
return iter(self._local)
class Cert(SyncWithTarget):
""" Represents a haproxy ssl-cert """
def __init__(self, path, pem, cert_id=None):
super().__init__()
self._path = path
self._pem = pem
self._cert_id = cert_id
self._local = self._get_local_state()
self._remote = self._get_remote_state(cmds.showSslCert, certfile=self._path)
self._diff = self._calc_diff()
@property
def path(self):
return self._path
@property
def cert_id(self):
return self._cert_id
@property
def pem(self):
return self._pem.replace("\n\n", "\n")
@property
def local(self):
return self._local
@property
def remote(self):
return self._remote
@property
def diff(self):
return self._diff
def __repr__(self):
return self._path
def _get_cert_data(self, dump=False, encoding='utf-8'):
result = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, self.pem)
if dump:
result = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, result).decode(encoding)
return result
def _glue(self, components):
return "".join("/{0:s}={1:s}".format(name.decode(), value.decode()) for name, value in components)
def _get_local_state(self):
cert_obj = self._get_cert_data()
return {
"Serial": '%.2x'.upper() % cert_obj.get_serial_number(),
"Subject": self._glue(cert_obj.get_subject().get_components()),
"Issuer": self._glue(cert_obj.get_issuer().get_components())
}
def _get_remote_state(self, command_class, **command_args):
cert_data = self._execute_remote_cmd(command_class, **command_args)
if 'error' in cert_data:
return cert_data
if cert_data['Status'] == 'Empty':
return {'Status': cert_data['Status']}
return {
"Serial": cert_data.get('Serial', None),
"Subject": cert_data.get('Subject', None),
"Issuer": cert_data.get('Issuer', None),
}
def _calc_diff(self):
result = {}
if self._remote != self._local:
result['certfile'] = self.path
result['pem'] = self.pem
result['meta'] = self.local
return result
def dict_from_yaml(path):
with open(path, 'r') as yaml_file:
data = yaml.load(yaml_file, Loader=yaml.SafeLoader)
return data
def skip_frontend(frontend_id, frontend):
filter_frontend_names = list(filter(None, args.frontends.split(",")))
filter_frontend_ids = list(filter(None, args.frontend_ids.split(",")))
if not filter_frontend_ids and not filter_frontend_names:
return False
if filter_frontend_ids and frontend_id in filter_frontend_ids:
return False
if filter_frontend_names and frontend['name'] in filter_frontend_names:
return False
return True
def get_cert_data(cert, dump=False, encoding='utf-8'):
if os.path.isfile(cert):
cert = open(cert).read()
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
if dump:
cert = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, cert).decode(encoding)
return cert
def base64_decode(base64_str, encoding='utf-8'):
if base64_str:
base64_bytes = base64_str.encode(encoding)
message_bytes = base64.b64decode(base64_bytes)
message = message_bytes.decode(encoding)
return message
return ''
def get_args():
# noinspection PyTypeChecker
parser = argparse.ArgumentParser(
description="""
Sync ssl certificates into HAProxys memory with certificates read from a configfile. If no frontend filter is
given, all certificates will be synced.""",
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
'command',
choices=['status', 'diff', 'sync', 'transactions', 'abort'],
nargs='+',
help="Execute one or more operations."
)
parser.add_argument(
'--config',
help='Path to the ssl certificate information configfile.',
default="/usr/local/etc/haproxy/sslCerts.yaml"
)
parser.add_argument(
'--frontends',
help='Attempt action on a list of frontend names, specified as a comma separated list.',
default=""
)
parser.add_argument(
'--frontend-ids',
help='Attempt action on a list of frontend ids, specified as a comma separated list.',
default=""
)
parser.add_argument(
'--output',
help='Specify output format.',
choices=['json', 'raw'],
default="raw"
)
return parser.parse_args()
def get_crt_lists_from_config(configfile):
""" Get ssl crt-list with certificates from configfile"""
config = dict_from_yaml(configfile)
crt_lists = []
for frontend_id, frontend in config['frontends'].items():
if skip_frontend(frontend_id, frontend):
continue
certs = []
default_cert = None
for cert_id, cert_data in frontend['certs'].items():
crt = base64_decode(cert_data['crt'])
key = base64_decode(cert_data['key'])
ca = base64_decode(cert_data['ca'])
full_cert = crt + key + ca
if cert_data['default']:
default_cert = cert_data['path']
certs.append(Cert(path=cert_data['path'], pem=full_cert, cert_id=cert_id))
params = {
'path': frontend['crt_list_path'],
'frontend_id': frontend_id,
'frontend_name': frontend['name'],
'certs': certs,
'default_cert': default_cert
}
crt_lists.append(CertList(**params))
return crt_lists
args = get_args()
crt_lists = get_crt_lists_from_config(args.config)
diff = Diff(crt_lists=crt_lists)
""" Sync ssl certs from configfile to HaProxy """
if "status" in args.command:
diff.show_status(args.output)
if "diff" in args.command:
diff.show_diff(args.output)
if "abort" in args.command:
diff.abort(args.output)
if "transactions" in args.command:
diff.show_transactions(args.output)
if "sync" in args.command:
diff.sync(args.output)

View file

@ -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

View file

@ -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

View file

@ -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 %}