diff --git a/.gitignore b/.gitignore index 3687d0f27..855a6c393 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .idea +venv /*/*/work diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/MaintenanceController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/MaintenanceController.php new file mode 100644 index 000000000..0a2e180da --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/MaintenanceController.php @@ -0,0 +1,170 @@ +getData( + ["server_status_list"], + ["rowCount", "current", "searchPhrase", "sort"] + ); + } + + /** + * set server weight + * @return array|mixed + */ + public function serverWeightAction() + { + return $this->saveData( + ["server_weight"], + ["backend", "server", "weight"] + ); + } + + /** + * set server administrative state + * @return array|mixed + */ + public function serverStateAction() + { + return $this->saveData( + ["server_state"], + ["backend", "server", "state"] + ); + } + + /** + * set server administrative state for multiple servers + * @return array|mixed + */ + public function serverStateBulkAction() + { + return $this->saveData( + ["server_state_bulk"], + ["server_ids", "state"] + ); + } + + /** + * set server weight for multiple servers + * @return array|mixed + */ + public function serverWeightBulkAction() + { + return $this->saveData( + ["server_weight_bulk"], + ["server_ids", "weight"] + ); + } + + /** + * Execute a backend command securely + * @param array $command + * @param array $arguments + * @return string + */ + protected function safeBackendCmd(array $command, array $arguments = []) + { + $backend = new Backend(); + + foreach ($arguments as $name) { + $val = $this->request->getPost($name); + if (is_array($val) and $name == 'sort') { + $sort = key(array_slice($val, 0, 1)); + $sort_dir = $val[$sort]; + $command[] = $sort; + $command[] = $sort_dir; + continue; + } + $command[] = $val; + } + + $command = array_map(function ($value) { + return escapeshellarg(empty($value = trim($value)) ? null : $value); + }, $command); + + return trim($backend->configdRun("haproxy " . join(" ", $command))); + } + + /** + * Executes a backend command to get data + * @param array $command + * @param array $arguments + * @return string|string[] + */ + protected function getData(array $command, array $arguments = []) + { + if ($this->request->isPost()) { + return $this->safeBackendCmd($command, $arguments); + } + return ["status" => "unavailable"]; + } + + /** + * Executes a backend command to save data + * @param array $command + * @param array $arguments + * @return array|string[] + */ + protected function saveData(array $command, array $arguments = []) + { + if ($this->request->isPost()) { + if ($error = $this->safeBackendCmd($command, $arguments)) { + return [ + "status" => "error", + "message" => $error + ]; + } else { + return ["status" => "ok"]; + } + } + return [ + "status" => 'unavailable', + "message" => 'only accept POST Requests.' + ]; + } +} diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/MaintenanceController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/MaintenanceController.php new file mode 100644 index 000000000..d5a073cc7 --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/MaintenanceController.php @@ -0,0 +1,45 @@ +view->pick('OPNsense/HAProxy/maintenance'); + } +} diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml index 750fe00af..8d12e55ab 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml @@ -28,7 +28,10 @@ - + + + + diff --git a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt new file mode 100644 index 000000000..33332c27b --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt @@ -0,0 +1,286 @@ +{# + +Copyright (C) 2021 Andreas Stuerz +OPNsense® is Copyright © 2014 – 2016 by Deciso B.V. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +#} + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ lang._('id') }}{{ lang._('Proxy') }}{{ lang._('Server') }}{{ lang._('Address') }}{{ lang._('Status') }}{{ lang._('Check Status') }}{{ lang._('Weight') }}{{ lang._('Sessions') }}{{ lang._('Bytes in') }}{{ lang._('Bytes out') }}{{ lang._('Active') }}{{ lang._('Downtime') }}{{ lang._('Last Change') }}{{ lang._('Commands') }}
+ + + + +
+
+
+ +{{ partial("layout_partials/base_dialog_processing") }} diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/__init__.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/__init__.py new file mode 100644 index 000000000..c7f37eafa --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/__init__.py @@ -0,0 +1,3 @@ +"""haproxy lib for socket commands. +Based on: https://github.com/neurogeek/haproxyctl""" +__version__ = "1.0" diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py new file mode 100644 index 000000000..0316bd99e --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py @@ -0,0 +1,237 @@ +# 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 = [] + 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 getResult(self, res): + """Returns raw results gathered from HAProxy""" + if res == '\n': + res = None + 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 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: + 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=',') + + 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 = [] + + reader = self.getDict(res) + for row in reader: + # 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 diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py new file mode 100644 index 000000000..962a15cf5 --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py @@ -0,0 +1,83 @@ +# 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( + '(?Punix://|tcp://)(?P[^:]+):*(?P[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('ASCII') + 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() diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/const.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/const.py new file mode 100644 index 000000000..ebd60d8c8 --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/const.py @@ -0,0 +1,7 @@ +"""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://' diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/__init__.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py new file mode 100644 index 000000000..032954583 --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py @@ -0,0 +1,73 @@ +# 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.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 = 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"]) + + 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"]) + + 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"]) + + 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"]) + + def test_showFrontends(self): + """Test 'frontends/backends' commands""" + args = {} + cmdFrontends = cmds.showFrontends(**args).getCmd() + self.assertEqual(cmdFrontends, self.Resp["frontends"]) + + def test_showInfo(self): + """Test 'show info' command""" + cmdShowInfo = cmds.showInfo().getCmd() + self.assertEqual(cmdShowInfo, self.Resp["info"]) + + def test_showSessions(self): + """Test 'show info' command""" + cmdShowInfo = cmds.showSessions().getCmd() + self.assertEqual(cmdShowInfo, self.Resp["sessions"]) + + def test_showServers(self): + """Test 'show info' command""" + args = {"backend": "redis-ro"} + cmdShowInfo = cmds.showServers(**args).getCmd() + self.assertEqual(cmdShowInfo, self.Resp["servers"]) + +if __name__ == '__main__': + unittest.main() diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_conn.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_conn.py new file mode 100644 index 000000000..fc6aac966 --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_conn.py @@ -0,0 +1,59 @@ +# 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() \ No newline at end of file diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py new file mode 100755 index 000000000..fc42c7c14 --- /dev/null +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +import os +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 + +SOCKET = '/var/run/haproxy.socket' +VALID_COMMANDS = { + "set-server-agent": cmds.setServerAgent, + "set-server-health": cmds.setServerHealth, + "set-server-state": cmds.setServerState, + "set-server-weight": cmds.setServerWeight, + "show-frontends": cmds.showFrontends, + "show-backends": cmds.showBackends, + "show-info": cmds.showInfo, + "show-sessions": cmds.showSessions, + "show-servers": cmds.showServers, +} + +def get_args(): + parser = argparse.ArgumentParser(description='Send haproxy commands via socket.') + parser.add_argument( + 'command', + choices=list(VALID_COMMANDS), + help='The command to execute via haproxy socket' + ) + parser.add_argument( + '--backend', + help='Attempt action on given backend.', + default=None + ) + parser.add_argument( + '--server', + help='Attempt action on given server.', + default=None + ) + 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', + default=None + ) + parser.add_argument( + '--value', + help='Specify value for a set command.', + default=None + ) + parser.add_argument( + '--output', + help='Specify output format.', + choices=['json', 'bootstrap'], + default=None + ) + parser.add_argument( + '--page-rows', + help='Limit output to the specified numbers of rows per page.', + default=None + ) + parser.add_argument( + '--page', + help='Output page number.', + default=None + ) + parser.add_argument( + '--search', + help='Search for string.', + default=None + ) + parser.add_argument( + '--sort-col', + help='Sort output on this column.', + default=None + ) + parser.add_argument( + '--sort-dir', + help='Sort output in this direction.', + default=None + ) + parser.add_argument( + '--debug', + type=bool, + help='Show debug output.', + default=False + ) + + return parser.parse_args() + +args = get_args() +command_class = VALID_COMMANDS.get(args.command, None) +command_args = {key: val for key, val in vars(args).items() if key != "command"} + +try: + if args.server_ids: + # bulk + command_bulk_args = command_args + command_bulk_args.pop('server_ids', None) + for server_id in args.server_ids.split(","): + command_bulk_args.update({ + 'backend': server_id.split("/")[0], + 'server': server_id.split("/")[1] + }) + con = HaPConn(SOCKET) + if con: + result = con.sendCmd(command_class(**command_bulk_args), objectify=False) + if result: + print(f"{server_id}: {result.strip()}") + con.close() + + else: + # single + con = HaPConn(SOCKET) + if con: + result = con.sendCmd(command_class(**command_args), objectify=False) + if result: + print(result.strip()) + else: + print(f"Could not open socket {SOCKET}") + +except Exception as exc: + print(f"While talking to {SOCKET}: {exc}") + if args['debug']: + tb = traceback.format_exc() + print(tb) diff --git a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf index 02896ebe0..ce1ef790b 100644 --- a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf +++ b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf @@ -45,3 +45,33 @@ command:/usr/local/opnsense/scripts/OPNsense/HAProxy/queryStats.php parameters:%s type:script_output message:requesting haproxy statistics + +[server_status_list] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +parameters: show-servers --output bootstrap --page-rows %s --page %s --search %s --sort-col %s --sort-dir %s +type:script_output +message:show server status list + +[server_state] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +parameters: set-server-state --backend %s --server %s --value %s +type:script_output +message:change haproxy server state + +[server_weight] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +parameters: set-server-weight --backend %s --server %s --value %s +type:script_output +message:change haproxy server weight + +[server_state_bulk] +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +parameters: set-server-state --server-ids %s --value %s +type:script_output +message:change haproxy state for multiple server + +[server_weight_bulk] +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 \ No newline at end of file