From 90a4827284acd3cb072cdfeef323c522802c6449 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:55:11 +0200 Subject: haproxy: T6745: Rename `reverse-proxy` to `haproxy` --- src/conf_mode/load-balancing_haproxy.py | 206 ++++++++++++++++++++++ src/conf_mode/load-balancing_reverse-proxy.py | 206 ---------------------- src/conf_mode/pki.py | 2 +- src/migration-scripts/reverse-proxy/1-to-2 | 27 +++ src/op_mode/load-balancing_haproxy.py | 237 ++++++++++++++++++++++++++ src/op_mode/restart.py | 10 +- src/op_mode/reverseproxy.py | 237 -------------------------- 7 files changed, 476 insertions(+), 449 deletions(-) create mode 100644 src/conf_mode/load-balancing_haproxy.py delete mode 100755 src/conf_mode/load-balancing_reverse-proxy.py create mode 100755 src/migration-scripts/reverse-proxy/1-to-2 create mode 100755 src/op_mode/load-balancing_haproxy.py delete mode 100755 src/op_mode/reverseproxy.py (limited to 'src') diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py new file mode 100644 index 000000000..45042dd52 --- /dev/null +++ b/src/conf_mode/load-balancing_haproxy.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from sys import exit +from shutil import rmtree + +from vyos.config import Config +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate +from vyos.utils.dict import dict_search +from vyos.utils.process import call +from vyos.utils.network import check_port_availability +from vyos.utils.network import is_listen_port_bind_service +from vyos.pki import find_chain +from vyos.pki import load_certificate +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.template import render +from vyos.utils.file import write_file +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +load_balancing_dir = '/run/haproxy' +load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' +systemd_service = 'haproxy.service' +systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['load-balancing', 'haproxy'] + if not conf.exists(base): + return None + lb = conf.get_config_dict(base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + with_pki=True) + + return lb + +def verify(lb): + if not lb: + return None + + if 'backend' not in lb or 'service' not in lb: + raise ConfigError(f'"service" and "backend" must be configured!') + + for front, front_config in lb['service'].items(): + if 'port' not in front_config: + raise ConfigError(f'"{front} service port" must be configured!') + + # Check if bind address:port are used by another service + tmp_address = front_config.get('address', '0.0.0.0') + tmp_port = front_config['port'] + if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \ + not is_listen_port_bind_service(int(tmp_port), 'haproxy'): + raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') + + for back, back_config in lb['backend'].items(): + if 'http_check' in back_config: + http_check = back_config['http_check'] + if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: + raise ConfigError(f'"expect status" and "expect string" can not be configured together!') + + if 'health_check' in back_config: + if back_config['mode'] != 'tcp': + raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' + + f'health-check whilst in TCP mode!') + if 'http_check' in back_config: + raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!') + + if 'server' not in back_config: + raise ConfigError(f'"{back} server" must be configured!') + + for bk_server, bk_server_conf in back_config['server'].items(): + if 'address' not in bk_server_conf or 'port' not in bk_server_conf: + raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!') + + if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf): + raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') + + if 'ssl' in back_config: + if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): + raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') + + # Check if http-response-headers are configured in any frontend/backend where mode != http + for group in ['service', 'backend']: + for config_name, config in lb[group].items(): + if 'http_response_headers' in config and config['mode'] != 'http': + raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') + + for front, front_config in lb['service'].items(): + for cert in dict_search('ssl.certificate', front_config) or []: + verify_pki_certificate(lb, cert) + + for back, back_config in lb['backend'].items(): + tmp = dict_search('ssl.ca_certificate', back_config) + if tmp: verify_pki_ca_certificate(lb, tmp) + + +def generate(lb): + if not lb: + # Delete /run/haproxy/haproxy.cfg + config_files = [load_balancing_conf_file, systemd_override] + for file in config_files: + if os.path.isfile(file): + os.unlink(file) + # Delete old directories + if os.path.isdir(load_balancing_dir): + rmtree(load_balancing_dir, ignore_errors=True) + + return None + + # Create load-balance dir + if not os.path.isdir(load_balancing_dir): + os.mkdir(load_balancing_dir) + + loaded_ca_certs = {load_certificate(c['certificate']) + for c in lb['pki']['ca'].values()} if 'ca' in lb['pki'] else {} + + # SSL Certificates for frontend + for front, front_config in lb['service'].items(): + if 'ssl' not in front_config: + continue + + if 'certificate' in front_config['ssl']: + cert_names = front_config['ssl']['certificate'] + + for cert_name in cert_names: + pki_cert = lb['pki']['certificate'][cert_name] + cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem') + cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key') + + loaded_pki_cert = load_certificate(pki_cert['certificate']) + cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) + + write_file(cert_file_path, + '\n'.join(encode_certificate(c) for c in cert_full_chain)) + + if 'private' in pki_cert and 'key' in pki_cert['private']: + loaded_key = load_private_key(pki_cert['private']['key'], passphrase=None, wrap_tags=True) + key_pem = encode_private_key(loaded_key, passphrase=None) + write_file(cert_key_path, key_pem) + + # SSL Certificates for backend + for back, back_config in lb['backend'].items(): + if 'ssl' not in back_config: + continue + + if 'ca_certificate' in back_config['ssl']: + ca_name = back_config['ssl']['ca_certificate'] + ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem') + ca_chains = [] + + pki_ca_cert = lb['pki']['ca'][ca_name] + loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) + ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) + ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain)) + write_file(ca_cert_file_path, '\n'.join(ca_chains)) + + render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb) + render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb) + + return None + +def apply(lb): + call('systemctl daemon-reload') + if not lb: + call(f'systemctl stop {systemd_service}') + else: + call(f'systemctl reload-or-restart {systemd_service}') + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py deleted file mode 100755 index 17226efe9..000000000 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023-2024 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from sys import exit -from shutil import rmtree - -from vyos.config import Config -from vyos.configverify import verify_pki_certificate -from vyos.configverify import verify_pki_ca_certificate -from vyos.utils.dict import dict_search -from vyos.utils.process import call -from vyos.utils.network import check_port_availability -from vyos.utils.network import is_listen_port_bind_service -from vyos.pki import find_chain -from vyos.pki import load_certificate -from vyos.pki import load_private_key -from vyos.pki import encode_certificate -from vyos.pki import encode_private_key -from vyos.template import render -from vyos.utils.file import write_file -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -load_balancing_dir = '/run/haproxy' -load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' -systemd_service = 'haproxy.service' -systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['load-balancing', 'reverse-proxy'] - if not conf.exists(base): - return None - lb = conf.get_config_dict(base, - get_first_key=True, - key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - with_recursive_defaults=True, - with_pki=True) - - return lb - -def verify(lb): - if not lb: - return None - - if 'backend' not in lb or 'service' not in lb: - raise ConfigError(f'"service" and "backend" must be configured!') - - for front, front_config in lb['service'].items(): - if 'port' not in front_config: - raise ConfigError(f'"{front} service port" must be configured!') - - # Check if bind address:port are used by another service - tmp_address = front_config.get('address', '0.0.0.0') - tmp_port = front_config['port'] - if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \ - not is_listen_port_bind_service(int(tmp_port), 'haproxy'): - raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') - - for back, back_config in lb['backend'].items(): - if 'http_check' in back_config: - http_check = back_config['http_check'] - if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: - raise ConfigError(f'"expect status" and "expect string" can not be configured together!') - - if 'health_check' in back_config: - if back_config['mode'] != 'tcp': - raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' + - f'health-check whilst in TCP mode!') - if 'http_check' in back_config: - raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!') - - if 'server' not in back_config: - raise ConfigError(f'"{back} server" must be configured!') - - for bk_server, bk_server_conf in back_config['server'].items(): - if 'address' not in bk_server_conf or 'port' not in bk_server_conf: - raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!') - - if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf): - raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') - - if 'ssl' in back_config: - if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): - raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') - - # Check if http-response-headers are configured in any frontend/backend where mode != http - for group in ['service', 'backend']: - for config_name, config in lb[group].items(): - if 'http_response_headers' in config and config['mode'] != 'http': - raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') - - for front, front_config in lb['service'].items(): - for cert in dict_search('ssl.certificate', front_config) or []: - verify_pki_certificate(lb, cert) - - for back, back_config in lb['backend'].items(): - tmp = dict_search('ssl.ca_certificate', back_config) - if tmp: verify_pki_ca_certificate(lb, tmp) - - -def generate(lb): - if not lb: - # Delete /run/haproxy/haproxy.cfg - config_files = [load_balancing_conf_file, systemd_override] - for file in config_files: - if os.path.isfile(file): - os.unlink(file) - # Delete old directories - if os.path.isdir(load_balancing_dir): - rmtree(load_balancing_dir, ignore_errors=True) - - return None - - # Create load-balance dir - if not os.path.isdir(load_balancing_dir): - os.mkdir(load_balancing_dir) - - loaded_ca_certs = {load_certificate(c['certificate']) - for c in lb['pki']['ca'].values()} if 'ca' in lb['pki'] else {} - - # SSL Certificates for frontend - for front, front_config in lb['service'].items(): - if 'ssl' not in front_config: - continue - - if 'certificate' in front_config['ssl']: - cert_names = front_config['ssl']['certificate'] - - for cert_name in cert_names: - pki_cert = lb['pki']['certificate'][cert_name] - cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem') - cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key') - - loaded_pki_cert = load_certificate(pki_cert['certificate']) - cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) - - write_file(cert_file_path, - '\n'.join(encode_certificate(c) for c in cert_full_chain)) - - if 'private' in pki_cert and 'key' in pki_cert['private']: - loaded_key = load_private_key(pki_cert['private']['key'], passphrase=None, wrap_tags=True) - key_pem = encode_private_key(loaded_key, passphrase=None) - write_file(cert_key_path, key_pem) - - # SSL Certificates for backend - for back, back_config in lb['backend'].items(): - if 'ssl' not in back_config: - continue - - if 'ca_certificate' in back_config['ssl']: - ca_name = back_config['ssl']['ca_certificate'] - ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem') - ca_chains = [] - - pki_ca_cert = lb['pki']['ca'][ca_name] - loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) - ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) - ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain)) - write_file(ca_cert_file_path, '\n'.join(ca_chains)) - - render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb) - render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb) - - return None - -def apply(lb): - call('systemctl daemon-reload') - if not lb: - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - - return None - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 233d73ba8..45e0129a3 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -71,7 +71,7 @@ sync_search = [ }, { 'keys': ['certificate', 'ca_certificate'], - 'path': ['load_balancing', 'reverse_proxy'], + 'path': ['load_balancing', 'haproxy'], }, { 'keys': ['key'], diff --git a/src/migration-scripts/reverse-proxy/1-to-2 b/src/migration-scripts/reverse-proxy/1-to-2 new file mode 100755 index 000000000..61612bc36 --- /dev/null +++ b/src/migration-scripts/reverse-proxy/1-to-2 @@ -0,0 +1,27 @@ +# Copyright 2024 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +# T6745: Rename base node to haproxy + +from vyos.configtree import ConfigTree + +base = ['load-balancing', 'reverse-proxy'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + config.rename(base, 'haproxy') diff --git a/src/op_mode/load-balancing_haproxy.py b/src/op_mode/load-balancing_haproxy.py new file mode 100755 index 000000000..ae6734e16 --- /dev/null +++ b/src/op_mode/load-balancing_haproxy.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +import socket +import sys + +from tabulate import tabulate +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode + +socket_path = '/run/haproxy/admin.sock' +timeout = 5 + + +def _execute_haproxy_command(command): + """Execute a command on the HAProxy UNIX socket and retrieve the response. + + Args: + command (str): The command to be executed. + + Returns: + str: The response received from the HAProxy UNIX socket. + + Raises: + socket.error: If there is an error while connecting or communicating with the socket. + + Finally: + Closes the socket connection. + + Notes: + - HAProxy expects a newline character at the end of the command. + - The socket connection is established using the HAProxy UNIX socket. + - The response from the socket is received and decoded. + + Example: + response = _execute_haproxy_command('show stat') + print(response) + """ + try: + # HAProxy expects new line for command + command = f'{command}\n' + + # Connect to the HAProxy UNIX socket + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + # Set the socket timeout + sock.settimeout(timeout) + + # Send the command + sock.sendall(command.encode()) + + # Receive and decode the response + response = b'' + while True: + data = sock.recv(4096) + if not data: + break + response += data + response = response.decode() + + return (response) + + except socket.error as e: + print(f"Error: {e}") + + finally: + # Close the socket + sock.close() + + +def _convert_seconds(seconds): + """Convert seconds to days, hours, minutes, and seconds. + + Args: + seconds (int): The number of seconds to convert. + + Returns: + tuple: A tuple containing the number of days, hours, minutes, and seconds. + """ + minutes = seconds // 60 + hours = minutes // 60 + days = hours // 24 + + return days, hours % 24, minutes % 60, seconds % 60 + + +def _last_change_format(seconds): + """Format the time components into a string representation. + + Args: + seconds (int): The total number of seconds. + + Returns: + str: The formatted time string with days, hours, minutes, and seconds. + + Examples: + >>> _last_change_format(1434) + '23m54s' + >>> _last_change_format(93734) + '1d0h23m54s' + >>> _last_change_format(85434) + '23h23m54s' + """ + days, hours, minutes, seconds = _convert_seconds(seconds) + time_format = "" + + if days: + time_format += f"{days}d" + if hours: + time_format += f"{hours}h" + if minutes: + time_format += f"{minutes}m" + if seconds: + time_format += f"{seconds}s" + + return time_format + + +def _get_json_data(): + """Get haproxy data format JSON""" + return _execute_haproxy_command('show stat json') + + +def _get_raw_data(): + """Retrieve raw data from JSON and organize it into a dictionary. + + Returns: + dict: A dictionary containing the organized data categorized + into frontend, backend, and server. + """ + + data = json.loads(_get_json_data()) + lb_dict = {'frontend': [], 'backend': [], 'server': []} + + for key in data: + frontend = [] + backend = [] + server = [] + for entry in key: + obj_type = entry['objType'].lower() + position = entry['field']['pos'] + name = entry['field']['name'] + value = entry['value']['value'] + + dict_entry = {'pos': position, 'name': {name: value}} + + if obj_type == 'frontend': + frontend.append(dict_entry) + elif obj_type == 'backend': + backend.append(dict_entry) + elif obj_type == 'server': + server.append(dict_entry) + + if len(frontend) > 0: + lb_dict['frontend'].append(frontend) + if len(backend) > 0: + lb_dict['backend'].append(backend) + if len(server) > 0: + lb_dict['server'].append(server) + + return lb_dict + + +def _get_formatted_output(data): + """ + Format the data into a tabulated output. + + Args: + data (dict): The data to be formatted. + + Returns: + str: The tabulated output representing the formatted data. + """ + table = [] + headers = [ + "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change" + ] + + for key in data: + for item in data[key]: + row = [None] * len(headers) + + for element in item: + if 'pxname' in element['name']: + row[0] = element['name']['pxname'] + elif 'svname' in element['name']: + row[1] = element['name']['svname'] + elif 'status' in element['name']: + row[2] = element['name']['status'] + elif 'req_rate' in element['name']: + row[3] = element['name']['req_rate'] + elif 'rtime' in element['name']: + row[4] = f"{element['name']['rtime']} ms" + elif 'lastchg' in element['name']: + row[5] = _last_change_format(element['name']['lastchg']) + table.append(row) + + out = tabulate(table, headers, numalign="left") + return out + + +def show(raw: bool): + config = ConfigTreeQuery() + if not config.exists('load-balancing haproxy'): + raise vyos.opmode.UnconfiguredSubsystem('Haproxy is not configured') + + data = _get_raw_data() + if raw: + return data + else: + return _get_formatted_output(data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py index a83c8b9d8..3b0031f34 100755 --- a/src/op_mode/restart.py +++ b/src/op_mode/restart.py @@ -41,6 +41,10 @@ service_map = { 'systemd_service': 'pdns-recursor', 'path': ['service', 'dns', 'forwarding'], }, + 'haproxy': { + 'systemd_service': 'haproxy', + 'path': ['load-balancing', 'haproxy'], + }, 'igmp_proxy': { 'systemd_service': 'igmpproxy', 'path': ['protocols', 'igmp-proxy'], @@ -53,10 +57,6 @@ service_map = { 'systemd_service': 'avahi-daemon', 'path': ['service', 'mdns', 'repeater'], }, - 'reverse_proxy': { - 'systemd_service': 'haproxy', - 'path': ['load-balancing', 'reverse-proxy'], - }, 'router_advert': { 'systemd_service': 'radvd', 'path': ['service', 'router-advert'], @@ -83,10 +83,10 @@ services = typing.Literal[ 'dhcpv6', 'dns_dynamic', 'dns_forwarding', + 'haproxy', 'igmp_proxy', 'ipsec', 'mdns_repeater', - 'reverse_proxy', 'router_advert', 'snmp', 'ssh', diff --git a/src/op_mode/reverseproxy.py b/src/op_mode/reverseproxy.py deleted file mode 100755 index 19704182a..000000000 --- a/src/op_mode/reverseproxy.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023-2024 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import json -import socket -import sys - -from tabulate import tabulate -from vyos.configquery import ConfigTreeQuery - -import vyos.opmode - -socket_path = '/run/haproxy/admin.sock' -timeout = 5 - - -def _execute_haproxy_command(command): - """Execute a command on the HAProxy UNIX socket and retrieve the response. - - Args: - command (str): The command to be executed. - - Returns: - str: The response received from the HAProxy UNIX socket. - - Raises: - socket.error: If there is an error while connecting or communicating with the socket. - - Finally: - Closes the socket connection. - - Notes: - - HAProxy expects a newline character at the end of the command. - - The socket connection is established using the HAProxy UNIX socket. - - The response from the socket is received and decoded. - - Example: - response = _execute_haproxy_command('show stat') - print(response) - """ - try: - # HAProxy expects new line for command - command = f'{command}\n' - - # Connect to the HAProxy UNIX socket - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(socket_path) - - # Set the socket timeout - sock.settimeout(timeout) - - # Send the command - sock.sendall(command.encode()) - - # Receive and decode the response - response = b'' - while True: - data = sock.recv(4096) - if not data: - break - response += data - response = response.decode() - - return (response) - - except socket.error as e: - print(f"Error: {e}") - - finally: - # Close the socket - sock.close() - - -def _convert_seconds(seconds): - """Convert seconds to days, hours, minutes, and seconds. - - Args: - seconds (int): The number of seconds to convert. - - Returns: - tuple: A tuple containing the number of days, hours, minutes, and seconds. - """ - minutes = seconds // 60 - hours = minutes // 60 - days = hours // 24 - - return days, hours % 24, minutes % 60, seconds % 60 - - -def _last_change_format(seconds): - """Format the time components into a string representation. - - Args: - seconds (int): The total number of seconds. - - Returns: - str: The formatted time string with days, hours, minutes, and seconds. - - Examples: - >>> _last_change_format(1434) - '23m54s' - >>> _last_change_format(93734) - '1d0h23m54s' - >>> _last_change_format(85434) - '23h23m54s' - """ - days, hours, minutes, seconds = _convert_seconds(seconds) - time_format = "" - - if days: - time_format += f"{days}d" - if hours: - time_format += f"{hours}h" - if minutes: - time_format += f"{minutes}m" - if seconds: - time_format += f"{seconds}s" - - return time_format - - -def _get_json_data(): - """Get haproxy data format JSON""" - return _execute_haproxy_command('show stat json') - - -def _get_raw_data(): - """Retrieve raw data from JSON and organize it into a dictionary. - - Returns: - dict: A dictionary containing the organized data categorized - into frontend, backend, and server. - """ - - data = json.loads(_get_json_data()) - lb_dict = {'frontend': [], 'backend': [], 'server': []} - - for key in data: - frontend = [] - backend = [] - server = [] - for entry in key: - obj_type = entry['objType'].lower() - position = entry['field']['pos'] - name = entry['field']['name'] - value = entry['value']['value'] - - dict_entry = {'pos': position, 'name': {name: value}} - - if obj_type == 'frontend': - frontend.append(dict_entry) - elif obj_type == 'backend': - backend.append(dict_entry) - elif obj_type == 'server': - server.append(dict_entry) - - if len(frontend) > 0: - lb_dict['frontend'].append(frontend) - if len(backend) > 0: - lb_dict['backend'].append(backend) - if len(server) > 0: - lb_dict['server'].append(server) - - return lb_dict - - -def _get_formatted_output(data): - """ - Format the data into a tabulated output. - - Args: - data (dict): The data to be formatted. - - Returns: - str: The tabulated output representing the formatted data. - """ - table = [] - headers = [ - "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change" - ] - - for key in data: - for item in data[key]: - row = [None] * len(headers) - - for element in item: - if 'pxname' in element['name']: - row[0] = element['name']['pxname'] - elif 'svname' in element['name']: - row[1] = element['name']['svname'] - elif 'status' in element['name']: - row[2] = element['name']['status'] - elif 'req_rate' in element['name']: - row[3] = element['name']['req_rate'] - elif 'rtime' in element['name']: - row[4] = f"{element['name']['rtime']} ms" - elif 'lastchg' in element['name']: - row[5] = _last_change_format(element['name']['lastchg']) - table.append(row) - - out = tabulate(table, headers, numalign="left") - return out - - -def show(raw: bool): - config = ConfigTreeQuery() - if not config.exists('load-balancing reverse-proxy'): - raise vyos.opmode.UnconfiguredSubsystem('Reverse-proxy is not configured') - - data = _get_raw_data() - if raw: - return data - else: - return _get_formatted_output(data) - - -if __name__ == '__main__': - try: - res = vyos.opmode.run(sys.modules[__name__]) - if res: - print(res) - except (ValueError, vyos.opmode.Error) as e: - print(e) - sys.exit(1) -- cgit v1.2.3