diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/dynamic_dns.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/high-availability.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 9 | ||||
-rwxr-xr-x | src/conf_mode/load-balancing-haproxy.py | 181 | ||||
-rwxr-xr-x | src/conf_mode/system-syslog.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 14 | ||||
-rwxr-xr-x | src/op_mode/interfaces.py | 34 | ||||
-rw-r--r-- | src/tests/test_util.py | 16 |
8 files changed, 239 insertions, 35 deletions
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 06a2f7e15..426e3d693 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -108,7 +108,8 @@ def verify(dyndns): raise ConfigError(f'"host-name" {error_msg}') if 'login' not in config: - raise ConfigError(f'"login" (username) {error_msg}') + if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'): + raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare') if 'password' not in config: raise ConfigError(f'"password" {error_msg}') diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 7a63f5b4b..e18b426b1 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -21,6 +21,7 @@ from ipaddress import ip_interface from ipaddress import IPv4Interface from ipaddress import IPv6Interface +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.ifconfig.vrrp import VRRP @@ -107,11 +108,16 @@ def verify(ha): raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') if 'health_check' in group_config: + health_check_types = ["script", "ping"] from vyos.utils.dict import check_mutually_exclusive_options try: - check_mutually_exclusive_options(group_config["health_check"], ["script", "ping"], required=True) + check_mutually_exclusive_options(group_config["health_check"], health_check_types, required=True) except ValueError as e: - raise ConfigError(f'Health check config is incorrect in VRRP group "{group}": {e}') + Warning(f'Health check configuration for VRRP group "{group}" will remain unused ' \ + f'until it has one of the following options: {health_check_types}') + # XXX: health check has default options so we need to remove it + # to avoid generating useless config statements in keepalived.conf + del group_config["health_check"] # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction # We also need to make sure VRID is not used twice on the same interface with the diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index b49c945cd..31cfab368 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -22,6 +22,7 @@ from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_eapol @@ -66,11 +67,17 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) base = ['interfaces', 'ethernet'] - _, ethernet = get_interface_dict(conf, base) + ifname, ethernet = get_interface_dict(conf, base) if 'deleted' not in ethernet: if pki: ethernet['pki'] = pki + tmp = is_node_changed(conf, base + [ifname, 'speed']) + if tmp: ethernet.update({'speed_duplex_changed': {}}) + + tmp = is_node_changed(conf, base + [ifname, 'duplex']) + if tmp: ethernet.update({'speed_duplex_changed': {}}) + return ethernet def verify(ethernet): diff --git a/src/conf_mode/load-balancing-haproxy.py b/src/conf_mode/load-balancing-haproxy.py new file mode 100755 index 000000000..b29fdffc7 --- /dev/null +++ b/src/conf_mode/load-balancing-haproxy.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from shutil import rmtree + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import call +from vyos.util import check_port_availability +from vyos.util import is_listen_port_bind_service +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key +from vyos.template import render +from vyos.xml import defaults +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 = r'/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'] + lb = conf.get_config_dict(base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True) + + if lb: + lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + if 'backend' in default_values: + del default_values['backend'] + if lb: + lb = dict_merge(default_values, lb) + + if 'backend' in lb: + for backend in lb['backend']: + default_balues_backend = defaults(base + ['backend']) + lb['backend'][backend] = dict_merge(default_balues_backend, lb['backend'][backend]) + + 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 '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}"') + +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) + + # SSL Certificates for frontend + for front, front_config in lb['service'].items(): + if 'ssl' in front_config: + cert_file_path = os.path.join(load_balancing_dir, 'cert.pem') + cert_key_path = os.path.join(load_balancing_dir, 'cert.pem.key') + ca_cert_file_path = os.path.join(load_balancing_dir, 'ca.pem') + + if 'certificate' in front_config['ssl']: + #cert_file_path = os.path.join(load_balancing_dir, 'cert.pem') + #cert_key_path = os.path.join(load_balancing_dir, 'cert.key') + cert_name = front_config['ssl']['certificate'] + pki_cert = lb['pki']['certificate'][cert_name] + + with open(cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + if 'private' in pki_cert and 'key' in pki_cert['private']: + with open(cert_key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + if 'ca_certificate' in front_config['ssl']: + ca_name = front_config['ssl']['ca_certificate'] + pki_ca_cert = lb['pki']['ca'][ca_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca_cert['certificate'])) + + # SSL Certificates for backend + for back, back_config in lb['backend'].items(): + if 'ssl' in back_config: + ca_cert_file_path = os.path.join(load_balancing_dir, 'ca.pem') + + if 'ca_certificate' in back_config['ssl']: + ca_name = back_config['ssl']['ca_certificate'] + pki_ca_cert = lb['pki']['ca'][ca_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca_cert['certificate'])) + + 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/system-syslog.py b/src/conf_mode/system-syslog.py index e646fb0ae..cf34bad2e 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -111,9 +111,9 @@ def verify(syslog): def generate(syslog): if not syslog: if os.path.exists(rsyslog_conf): - os.path.unlink(rsyslog_conf) + os.unlink(rsyslog_conf) if os.path.exists(logrotate_conf): - os.path.unlink(logrotate_conf) + os.unlink(logrotate_conf) return None @@ -126,9 +126,10 @@ def generate(syslog): return None def apply(syslog): + systemd_socket = 'syslog.socket' systemd_service = 'syslog.service' if not syslog: - call(f'systemctl stop {systemd_service}') + call(f'systemctl stop {systemd_service} {systemd_socket}') return None # we need to restart the service if e.g. the VRF name changed diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 68da70d7d..83021a3e6 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -17,6 +17,7 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.pki import wrap_certificate @@ -173,6 +174,19 @@ def verify(ocserv): users_wo_pswd.append(user) if users_wo_pswd: raise ConfigError(f'password required for users:\n{users_wo_pswd}') + + # Validate that if identity-based-config is configured all child config nodes are set + if 'identity_based_config' in ocserv["authentication"]: + if 'disabled' not in ocserv["authentication"]["identity_based_config"]: + Warning("Identity based configuration files is a 3rd party addition. Use at your own risk, this might break the ocserv daemon!") + if 'mode' not in ocserv["authentication"]["identity_based_config"]: + raise ConfigError('OpenConnect radius identity-based-config enabled but mode not selected') + elif 'group' in ocserv["authentication"]["identity_based_config"]["mode"] and "radius" not in ocserv["authentication"]["mode"]: + raise ConfigError('OpenConnect config-per-group must be used with radius authentication') + if 'directory' not in ocserv["authentication"]["identity_based_config"]: + raise ConfigError('OpenConnect identity-based-config enabled but directory not set') + if 'default_config' not in ocserv["authentication"]["identity_based_config"]: + raise ConfigError('OpenConnect identity-based-config enabled but default-config not set') else: raise ConfigError('openconnect authentication mode required') else: diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index dd87b5901..f38b95a71 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -277,6 +277,10 @@ def _get_counter_data(ifname: typing.Optional[str], res_intf['rx_bytes'] = _get_counter_val(cache['rx_bytes'], stats['rx_bytes']) res_intf['tx_packets'] = _get_counter_val(cache['tx_packets'], stats['tx_packets']) res_intf['tx_bytes'] = _get_counter_val(cache['tx_bytes'], stats['tx_bytes']) + res_intf['rx_dropped'] = _get_counter_val(cache['rx_dropped'], stats['rx_dropped']) + res_intf['tx_dropped'] = _get_counter_val(cache['tx_dropped'], stats['tx_dropped']) + res_intf['rx_over_errors'] = _get_counter_val(cache['rx_over_errors'], stats['rx_over_errors']) + res_intf['tx_carrier_errors'] = _get_counter_val(cache['tx_carrier_errors'], stats['tx_carrier_errors']) ret.append(res_intf) @@ -368,19 +372,23 @@ def _format_show_summary(data): @catch_broken_pipe def _format_show_counters(data: list): - formatting = '%-12s %10s %10s %10s %10s' - print(formatting % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) - - for intf in data: - print(formatting % ( - intf['ifname'], - intf['rx_packets'], - intf['rx_bytes'], - intf['tx_packets'], - intf['tx_bytes'] - )) - - return 0 + data_entries = [] + for entry in data: + interface = entry.get('ifname') + rx_packets = entry.get('rx_packets') + rx_bytes = entry.get('rx_bytes') + tx_packets = entry.get('tx_packets') + tx_bytes = entry.get('tx_bytes') + rx_dropped = entry.get('rx_dropped') + tx_dropped = entry.get('tx_dropped') + rx_errors = entry.get('rx_over_errors') + tx_errors = entry.get('tx_carrier_errors') + data_entries.append([interface, rx_packets, rx_bytes, tx_packets, tx_bytes, rx_dropped, tx_dropped, rx_errors, tx_errors]) + + headers = ['Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes', 'Rx Dropped', 'Tx Dropped', 'Rx Errors', 'Tx Errors'] + output = tabulate(data_entries, headers, numalign="left") + print (output) + return output def show(raw: bool, intf_name: typing.Optional[str], intf_type: typing.Optional[str], diff --git a/src/tests/test_util.py b/src/tests/test_util.py index d8b2b7940..473052bef 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -26,17 +26,3 @@ class TestVyOSUtil(TestCase): def test_sysctl_read(self): self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1') - - def test_camel_to_snake_case(self): - self.assertEqual(camel_to_snake_case('ConnectionTimeout'), - 'connection_timeout') - self.assertEqual(camel_to_snake_case('connectionTimeout'), - 'connection_timeout') - self.assertEqual(camel_to_snake_case('TCPConnectionTimeout'), - 'tcp_connection_timeout') - self.assertEqual(camel_to_snake_case('TCPPort'), - 'tcp_port') - self.assertEqual(camel_to_snake_case('UseHTTPProxy'), - 'use_http_proxy') - self.assertEqual(camel_to_snake_case('CustomerID'), - 'customer_id') |