From e41ae4d952e276d8497d38f5761806c14ea542d2 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Wed, 9 Sep 2020 06:45:40 +0000 Subject: openconnect: T2036: Move CLI commands under vpn openconnect --- data/templates/ocserv/ocserv_config.tmpl | 2 +- data/templates/ocserv/radius_conf.tmpl | 2 +- data/templates/ocserv/radius_servers.tmpl | 2 +- interface-definitions/vpn_anyconnect.xml.in | 258 -------------------------- interface-definitions/vpn_openconnect.xml.in | 258 ++++++++++++++++++++++++++ op-mode-definitions/anyconnect.xml | 20 -- op-mode-definitions/openconnect.xml | 20 ++ smoketest/scripts/cli/test_vpn_anyconnect.py | 58 ------ smoketest/scripts/cli/test_vpn_openconnect.py | 58 ++++++ src/conf_mode/vpn_anyconnect.py | 135 -------------- src/conf_mode/vpn_openconnect.py | 135 ++++++++++++++ src/op_mode/anyconnect-control.py | 67 ------- src/op_mode/openconnect-control.py | 67 +++++++ 13 files changed, 541 insertions(+), 541 deletions(-) delete mode 100644 interface-definitions/vpn_anyconnect.xml.in create mode 100644 interface-definitions/vpn_openconnect.xml.in delete mode 100644 op-mode-definitions/anyconnect.xml create mode 100644 op-mode-definitions/openconnect.xml delete mode 100755 smoketest/scripts/cli/test_vpn_anyconnect.py create mode 100755 smoketest/scripts/cli/test_vpn_openconnect.py delete mode 100755 src/conf_mode/vpn_anyconnect.py create mode 100755 src/conf_mode/vpn_openconnect.py delete mode 100755 src/op_mode/anyconnect-control.py create mode 100755 src/op_mode/openconnect-control.py diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl index 6aaeff693..328af0c0d 100644 --- a/data/templates/ocserv/ocserv_config.tmpl +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -1,4 +1,4 @@ -### generated by vpn_anyconnect.py ### +### generated by vpn_openconnect.py ### tcp-port = {{ listen_ports.tcp }} udp-port = {{ listen_ports.udp }} diff --git a/data/templates/ocserv/radius_conf.tmpl b/data/templates/ocserv/radius_conf.tmpl index 2d19306a0..1712d83ef 100644 --- a/data/templates/ocserv/radius_conf.tmpl +++ b/data/templates/ocserv/radius_conf.tmpl @@ -1,4 +1,4 @@ -### generated by cpn_anyconnect.py ### +### generated by vpn_openconnect.py ### nas-identifier VyOS {% for srv in server %} {% if not "disable" in server[srv] %} diff --git a/data/templates/ocserv/radius_servers.tmpl b/data/templates/ocserv/radius_servers.tmpl index ba21fa074..7bacac992 100644 --- a/data/templates/ocserv/radius_servers.tmpl +++ b/data/templates/ocserv/radius_servers.tmpl @@ -1,4 +1,4 @@ -### generated by cpn_anyconnect.py ### +### generated by vpn_openconnect.py ### # server key {% for srv in server %} {% if not "disable" in server[srv] %} diff --git a/interface-definitions/vpn_anyconnect.xml.in b/interface-definitions/vpn_anyconnect.xml.in deleted file mode 100644 index e74326986..000000000 --- a/interface-definitions/vpn_anyconnect.xml.in +++ /dev/null @@ -1,258 +0,0 @@ - - - - - - - SSL VPN AnyConnect - 901 - - - - - Authentication for remote access SSL VPN Server - - - - - Authentication mode used by this server - - local - Use local username/password configuration - - - radius - Use RADIUS server for user autentication - - - (local|radius) - - - local radius - - - - - - Local user authentication for SSL VPN server - - - - - User name for authentication - - - - - Option to disable a SSL VPN Server user - - - - - - Password for authentication - - - - - - - #include - - - - - Session timeout - - 1-30 - Session timeout in seconds (default: 2) - - - - - Timeout must be between 1 and 30 seconds - - 2 - - - - - - - - SSL Certificate, SSL Key and CA (/config/auth) - - - - - tcp port number to accept connections (default: 443) - - 1-65535 - Numeric IP port (default: 443) - - - - - - 443 - - - - udp port number to accept connections (default: 443) - - 1-65535 - Numeric IP port (default: 443) - - - - - - 443 - - - - - - SSL Certificate, SSL Key and CA (/config/auth) - - - - - Certificate Authority certificate - - - - - file - File in /config/auth directory - - - - - - - - - Server Certificate - - file - File in /config/auth directory - - - - - - - - - Privat Key of the Server Certificate - - file - File in /config/auth directory - - - - - - - - - - - Network settings - - - - - Route to be pushed to the client - - ipv4net - IPv4 network and prefix length - - - ipv6net - IPv6 network and prefix length - - - - - - - - - - Client IP pools settings - - - - - Client IP subnet (CIDR notation) - - ipv4net - IPv4 address and prefix length - - - - - Not a valid CIDR formatted prefix - - - - - - - Pool of client IPv6 addresses - - - - - Pool of addresses used to assign to clients - - ipv6net - IPv6 address and prefix length - - - - - - - - - Prefix length used for individual client - - <48-128> - Client prefix length (default: 64) - - - - - - 64 - - - - - - Domain Name Server (DNS) propagated to client - - ipv4 - Domain Name Server (DNS) IPv4 address - - - ipv6 - Domain Name Server (DNS) IPv6 address - - - - - - - - - - - - - - - diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in new file mode 100644 index 000000000..16fe660a9 --- /dev/null +++ b/interface-definitions/vpn_openconnect.xml.in @@ -0,0 +1,258 @@ + + + + + + + SSL VPN OpenConnect, AnyConnect compatible server + 901 + + + + + Authentication for remote access SSL VPN Server + + + + + Authentication mode used by this server + + local + Use local username/password configuration + + + radius + Use RADIUS server for user autentication + + + (local|radius) + + + local radius + + + + + + Local user authentication for SSL VPN server + + + + + User name for authentication + + + + + Option to disable a SSL VPN Server user + + + + + + Password for authentication + + + + + + + #include + + + + + Session timeout + + 1-30 + Session timeout in seconds (default: 2) + + + + + Timeout must be between 1 and 30 seconds + + 2 + + + + + + + + SSL Certificate, SSL Key and CA (/config/auth) + + + + + tcp port number to accept connections (default: 443) + + 1-65535 + Numeric IP port (default: 443) + + + + + + 443 + + + + udp port number to accept connections (default: 443) + + 1-65535 + Numeric IP port (default: 443) + + + + + + 443 + + + + + + SSL Certificate, SSL Key and CA (/config/auth) + + + + + Certificate Authority certificate + + + + + file + File in /config/auth directory + + + + + + + + + Server Certificate + + file + File in /config/auth directory + + + + + + + + + Privat Key of the Server Certificate + + file + File in /config/auth directory + + + + + + + + + + + Network settings + + + + + Route to be pushed to the client + + ipv4net + IPv4 network and prefix length + + + ipv6net + IPv6 network and prefix length + + + + + + + + + + Client IP pools settings + + + + + Client IP subnet (CIDR notation) + + ipv4net + IPv4 address and prefix length + + + + + Not a valid CIDR formatted prefix + + + + + + + Pool of client IPv6 addresses + + + + + Pool of addresses used to assign to clients + + ipv6net + IPv6 address and prefix length + + + + + + + + + Prefix length used for individual client + + <48-128> + Client prefix length (default: 64) + + + + + + 64 + + + + + + Domain Name Server (DNS) propagated to client + + ipv4 + Domain Name Server (DNS) IPv4 address + + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + + + + + + + + diff --git a/op-mode-definitions/anyconnect.xml b/op-mode-definitions/anyconnect.xml deleted file mode 100644 index 7e8cdd35b..000000000 --- a/op-mode-definitions/anyconnect.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - show anyconnect-server information - - - - - Show active anyconnect server sessions - - ${vyos_op_scripts_dir}/anyconnect-control.py --action="show_sessions" - - - - - - diff --git a/op-mode-definitions/openconnect.xml b/op-mode-definitions/openconnect.xml new file mode 100644 index 000000000..9b82b114e --- /dev/null +++ b/op-mode-definitions/openconnect.xml @@ -0,0 +1,20 @@ + + + + + + + show openconnect-server information + + + + + Show active openconnect server sessions + + ${vyos_op_scripts_dir}/openconnect-control.py --action="show_sessions" + + + + + + diff --git a/smoketest/scripts/cli/test_vpn_anyconnect.py b/smoketest/scripts/cli/test_vpn_anyconnect.py deleted file mode 100755 index dd8ab1609..000000000 --- a/smoketest/scripts/cli/test_vpn_anyconnect.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 re -import os -import unittest - -from psutil import process_iter -from vyos.configsession import ConfigSession, ConfigSessionError -from vyos.util import read_file - -OCSERV_CONF = '/run/ocserv/ocserv.conf' -base_path = ['vpn', 'anyconnect'] -cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' -cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key' - -class TestVpnAnyconnect(unittest.TestCase): - def setUp(self): - self.session = ConfigSession(os.getpid()) - - def tearDown(self): - # Delete vpn anyconnect configuration - self.session.delete(base_path) - self.session.commit() - - del self.session - - def test_vpn(self): - user = 'vyos_user' - password = 'vyos_pass' - self.session.delete(base_path) - self.session.set(base_path + ["authentication", "local-users", "username", user, "password", password]) - self.session.set(base_path + ["authentication", "mode", "local"]) - self.session.set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"]) - self.session.set(base_path + ["ssl", "ca-cert-file", cert]) - self.session.set(base_path + ["ssl", "cert-file", cert]) - self.session.set(base_path + ["ssl", "key-file", cert_key]) - - self.session.commit() - - # Check for running process - self.assertTrue("ocserv-main" in (p.name() for p in process_iter())) - -if __name__ == '__main__': - unittest.main() diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py new file mode 100755 index 000000000..d2b82d686 --- /dev/null +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 re +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +OCSERV_CONF = '/run/ocserv/ocserv.conf' +base_path = ['vpn', 'openconnect'] +cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' +cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key' + +class TestVpnOpenconnect(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + # Delete vpn openconnect configuration + self.session.delete(base_path) + self.session.commit() + + del self.session + + def test_vpn(self): + user = 'vyos_user' + password = 'vyos_pass' + self.session.delete(base_path) + self.session.set(base_path + ["authentication", "local-users", "username", user, "password", password]) + self.session.set(base_path + ["authentication", "mode", "local"]) + self.session.set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"]) + self.session.set(base_path + ["ssl", "ca-cert-file", cert]) + self.session.set(base_path + ["ssl", "cert-file", cert]) + self.session.set(base_path + ["ssl", "key-file", cert_key]) + + self.session.commit() + + # Check for running process + self.assertTrue("ocserv-main" in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/src/conf_mode/vpn_anyconnect.py b/src/conf_mode/vpn_anyconnect.py deleted file mode 100755 index 158e1a117..000000000 --- a/src/conf_mode/vpn_anyconnect.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 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 vyos.config import Config -from vyos.configdict import dict_merge -from vyos.xml import defaults -from vyos.template import render -from vyos.util import call -from vyos import ConfigError -from crypt import crypt, mksalt, METHOD_SHA512 - -from vyos import airbag -airbag.enable() - -cfg_dir = '/run/ocserv' -ocserv_conf = cfg_dir + '/ocserv.conf' -ocserv_passwd = cfg_dir + '/ocpasswd' -radius_cfg = cfg_dir + '/radiusclient.conf' -radius_servers = cfg_dir + '/radius_servers' - - -# Generate hash from user cleartext password -def get_hash(password): - return crypt(password, mksalt(METHOD_SHA512)) - - -def get_config(): - conf = Config() - base = ['vpn', 'anyconnect'] - if not conf.exists(base): - return None - - ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - default_values = defaults(base) - ocserv = dict_merge(default_values, ocserv) - return ocserv - - -def verify(ocserv): - if ocserv is None: - return None - - # Check authentication - if "authentication" in ocserv: - if "mode" in ocserv["authentication"]: - if "local" in ocserv["authentication"]["mode"]: - if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('Anyconect mode local required at leat one user') - else: - for user in ocserv["authentication"]["local_users"]["username"]: - if not "password" in ocserv["authentication"]["local_users"]["username"][user]: - raise ConfigError(f'password required for user {user}') - else: - raise ConfigError('anyconnect authentication mode required') - else: - raise ConfigError('anyconnect authentication credentials required') - - # Check ssl - if "ssl" in ocserv: - req_cert = ['ca_cert_file', 'cert_file', 'key_file'] - for cert in req_cert: - if not cert in ocserv["ssl"]: - raise ConfigError('anyconnect ssl {0} required'.format(cert.replace('_', '-'))) - else: - raise ConfigError('anyconnect ssl required') - - # Check network settings - if "network_settings" in ocserv: - if "push_route" in ocserv["network_settings"]: - # Replace default route - if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]: - ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") - ocserv["network_settings"]["push_route"].append("default") - else: - ocserv["network_settings"]["push_route"] = "default" - else: - raise ConfigError('anyconnect network settings required') - - -def generate(ocserv): - if not ocserv: - return None - - if "radius" in ocserv["authentication"]["mode"]: - # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) - # Render radius servers - render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) - else: - if "local_users" in ocserv["authentication"]: - for user in ocserv["authentication"]["local_users"]["username"]: - ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) - # Render local users - render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) - - # Render config - render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) - - - -def apply(ocserv): - if not ocserv: - call('systemctl stop ocserv.service') - for file in [ocserv_conf, ocserv_passwd]: - if os.path.exists(file): - os.unlink(file) - else: - call('systemctl restart ocserv.service') - - -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/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py new file mode 100755 index 000000000..af8604972 --- /dev/null +++ b/src/conf_mode/vpn_openconnect.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 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 vyos.config import Config +from vyos.configdict import dict_merge +from vyos.xml import defaults +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from crypt import crypt, mksalt, METHOD_SHA512 + +from vyos import airbag +airbag.enable() + +cfg_dir = '/run/ocserv' +ocserv_conf = cfg_dir + '/ocserv.conf' +ocserv_passwd = cfg_dir + '/ocpasswd' +radius_cfg = cfg_dir + '/radiusclient.conf' +radius_servers = cfg_dir + '/radius_servers' + + +# Generate hash from user cleartext password +def get_hash(password): + return crypt(password, mksalt(METHOD_SHA512)) + + +def get_config(): + conf = Config() + base = ['vpn', 'openconnect'] + if not conf.exists(base): + return None + + ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + default_values = defaults(base) + ocserv = dict_merge(default_values, ocserv) + return ocserv + + +def verify(ocserv): + if ocserv is None: + return None + + # Check authentication + if "authentication" in ocserv: + if "mode" in ocserv["authentication"]: + if "local" in ocserv["authentication"]["mode"]: + if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at leat one user') + else: + for user in ocserv["authentication"]["local_users"]["username"]: + if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + raise ConfigError(f'password required for user {user}') + else: + raise ConfigError('openconnect authentication mode required') + else: + raise ConfigError('openconnect authentication credentials required') + + # Check ssl + if "ssl" in ocserv: + req_cert = ['ca_cert_file', 'cert_file', 'key_file'] + for cert in req_cert: + if not cert in ocserv["ssl"]: + raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-'))) + else: + raise ConfigError('openconnect ssl required') + + # Check network settings + if "network_settings" in ocserv: + if "push_route" in ocserv["network_settings"]: + # Replace default route + if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]: + ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") + ocserv["network_settings"]["push_route"].append("default") + else: + ocserv["network_settings"]["push_route"] = "default" + else: + raise ConfigError('openconnect network settings required') + + +def generate(ocserv): + if not ocserv: + return None + + if "radius" in ocserv["authentication"]["mode"]: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + else: + if "local_users" in ocserv["authentication"]: + for user in ocserv["authentication"]["local_users"]["username"]: + ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) + # Render local users + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) + + # Render config + render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) + + + +def apply(ocserv): + if not ocserv: + call('systemctl stop ocserv.service') + for file in [ocserv_conf, ocserv_passwd]: + if os.path.exists(file): + os.unlink(file) + else: + call('systemctl restart ocserv.service') + + +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/op_mode/anyconnect-control.py b/src/op_mode/anyconnect-control.py deleted file mode 100755 index 6382016b7..000000000 --- a/src/op_mode/anyconnect-control.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 sys -import argparse -import json - -from vyos.config import Config -from vyos.util import popen, run, DEVNULL -from tabulate import tabulate - -occtl = '/usr/bin/occtl' -occtl_socket = '/run/ocserv/occtl.socket' - -def show_sessions(): - out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL) - if code: - sys.exit('Cannot get anyconnect users information') - else: - headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"] - sessions = json.loads(out) - ses_list = [] - for ses in sessions: - ses_list.append([ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]]) - if len(ses_list) > 0: - print(tabulate(ses_list, headers)) - else: - print("No active anyconnect sessions") - -def is_ocserv_configured(): - if not Config().exists_effective('vpn anyconnect'): - print("vpn anyconnect server is not configured") - sys.exit(1) - -def main(): - #parese args - parser = argparse.ArgumentParser() - parser.add_argument('--action', help='Control action', required=True) - parser.add_argument('--selector', help='Selector username|ifname|sid', required=False) - parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False) - args = parser.parse_args() - - - # Check is IPoE configured - is_ocserv_configured() - - if args.action == "restart": - run("systemctl restart ocserv") - sys.exit(0) - elif args.action == "show_sessions": - show_sessions() - -if __name__ == '__main__': - main() diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py new file mode 100755 index 000000000..ef9fe618c --- /dev/null +++ b/src/op_mode/openconnect-control.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 sys +import argparse +import json + +from vyos.config import Config +from vyos.util import popen, run, DEVNULL +from tabulate import tabulate + +occtl = '/usr/bin/occtl' +occtl_socket = '/run/ocserv/occtl.socket' + +def show_sessions(): + out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL) + if code: + sys.exit('Cannot get openconnect users information') + else: + headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"] + sessions = json.loads(out) + ses_list = [] + for ses in sessions: + ses_list.append([ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]]) + if len(ses_list) > 0: + print(tabulate(ses_list, headers)) + else: + print("No active openconnect sessions") + +def is_ocserv_configured(): + if not Config().exists_effective('vpn openconnect'): + print("vpn openconnect server is not configured") + sys.exit(1) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Control action', required=True) + parser.add_argument('--selector', help='Selector username|ifname|sid', required=False) + parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False) + args = parser.parse_args() + + + # Check is Openconnect server configured + is_ocserv_configured() + + if args.action == "restart": + run("systemctl restart ocserv") + sys.exit(0) + elif args.action == "show_sessions": + show_sessions() + +if __name__ == '__main__': + main() -- cgit v1.2.3 From c3d170b17e39e94e6f53e4afd8d0468d35e9d8fc Mon Sep 17 00:00:00 2001 From: sever-sever Date: Thu, 10 Sep 2020 07:16:39 +0000 Subject: op-mode: T2856: Fix broken pipe in show version all --- op-mode-definitions/show-version.xml | 2 +- src/op_mode/show_version.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/op-mode-definitions/show-version.xml b/op-mode-definitions/show-version.xml index aae5bb008..905a4865c 100644 --- a/op-mode-definitions/show-version.xml +++ b/op-mode-definitions/show-version.xml @@ -18,7 +18,7 @@ Show system version and versions of all packages - ${vyos_op_scripts_dir}/show_version.py --all + echo "Package versions:"; dpkg -l | awk '$0~/>/{exit}1' diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py index d0d5c6785..5bbc2e1f1 100755 --- a/src/op_mode/show_version.py +++ b/src/op_mode/show_version.py @@ -27,7 +27,6 @@ from sys import exit from vyos.util import call parser = argparse.ArgumentParser() -parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions") parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") @@ -65,9 +64,5 @@ if __name__ == '__main__': tmpl = Template(version_output_tmpl) print(tmpl.render(version_data)) - if args.all: - print("Package versions:") - call("dpkg -l") - if args.funny: print(vyos.limericks.get_random()) -- cgit v1.2.3 From 4100b09f45a86bd2b5f6a2cf7a6b3ca84cb9ae84 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 10 Sep 2020 08:19:36 -0400 Subject: show log:T2872:"Show log" for nat and openvpn got inter-mixed The possible completion of the "show log" shows only nat and the description shows for openvpn. Corrected the duplicate entry --- op-mode-definitions/show-log.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-mode-definitions/show-log.xml b/op-mode-definitions/show-log.xml index 0c4da647b..b00e4cfec 100644 --- a/op-mode-definitions/show-log.xml +++ b/op-mode-definitions/show-log.xml @@ -135,7 +135,7 @@ egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr) - + Show log for OpenVPN -- cgit v1.2.3 From d49845421dbd8d0f470b7122022543eb45d10b7a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 00:15:29 +0200 Subject: ifconfig: T2863: only use IPv6 link-local address if interface has MAC address With VyOS 1.2 the default WireGuard behavior is used. This means that when a WireGuard interface is added to the system, there is no "MAC" address - also there is no IPv6 link-local address assigned by the Kernel to this particular interface. With implementation of T2653 all interfaces now receive an IPv6 address - which is also valid for WireGuard interfaces - unfortunately this logic relies on the interface MAC address - and as there is none, the link-local address will be always the same. The logic behind is coded here [1]. We generate an IPv6 link-local address even when there is no "MAC" address. The behavior/functionality (as with VyOS 1.2) must be restored to not have a link-local IPv6 address at all. Any user can add any IPv6 link-local address manually by issuing: set interfaces wireguard wg01 address fe80::ff:1/64. Change vyos.ifconfig.add_ipv6_eui64_address to only add the EUI64-based link-local address if a MAC address is available. [1] https://github.com/vyos/vyos-1x/blob/3077158391ceee4ce04c27dec33f629529727c36/python/vyos/ifconfig/interface.py#L468 --- python/vyos/ifconfig/interface.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index ef2336c17..ffe69f61b 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -464,10 +464,13 @@ class Interface(Control): Calculate the EUI64 from the interface's MAC, then assign it with the given prefix to the interface. """ - - eui64 = mac2eui64(self.get_mac(), prefix) - prefixlen = prefix.split('/')[1] - self.add_addr(f'{eui64}/{prefixlen}') + # T2863: only add a link-local IPv6 address if the interface returns + # a MAC address. This is not the case on e.g. WireGuard interfaces. + mac = self.get_mac() + if mac: + eui64 = mac2eui64(mac, prefix) + prefixlen = prefix.split('/')[1] + self.add_addr(f'{eui64}/{prefixlen}') def del_ipv6_eui64_address(self, prefix): """ -- cgit v1.2.3 From 2ea89c1b3bd51523501b6d35bd1983407c3bca09 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 09:03:07 +0200 Subject: vyos.util: add missing ConfigError import in check_kmod() --- python/vyos/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/vyos/util.py b/python/vyos/util.py index 84aa16791..79e11a86d 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -654,6 +654,7 @@ def get_bridge_member_config(conf, br, intf): def check_kmod(k_mod): """ Common utility function to load required kernel modules on demand """ + from vyos import ConfigError if isinstance(k_mod, str): k_mod = k_mod.split() for module in k_mod: -- cgit v1.2.3 From 6bb9c21b75ab04d1c96dee3ae8769f25337aed93 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 09:10:38 +0200 Subject: Revert "Debian: T2843: drop wireguard-modules dependency with kernel 5.8" This reverts commit bd076f694a763991a0b0d3a7bb0fa5d194d56d7c. --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index d9663d07b..0777ecc52 100644 --- a/debian/control +++ b/debian/control @@ -110,6 +110,7 @@ Depends: vyos-utils, wide-dhcpv6-client, wireguard-tools, + wireguard-modules, wireless-regdb, wpasupplicant (>= 0.6.7) Description: VyOS configuration scripts and data -- cgit v1.2.3 From 5f7f976d15be664e9ac29a46dc33cc3a9c3572fd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 10:30:24 +0200 Subject: configd: T2582: add .gitignore --- src/shim/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/shim/.gitignore diff --git a/src/shim/.gitignore b/src/shim/.gitignore new file mode 100644 index 000000000..d33538138 --- /dev/null +++ b/src/shim/.gitignore @@ -0,0 +1,2 @@ +/mkjson/obj/ +/vyshim -- cgit v1.2.3 From 56bb811e1ce626aa783ffafe9fe8952da9bda82d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 10:32:26 +0200 Subject: qat: T2857: cleanup configuration script --- src/conf_mode/intel_qat.py | 145 +++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py index 1e5101a9f..86dbccaf0 100755 --- a/src/conf_mode/intel_qat.py +++ b/src/conf_mode/intel_qat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 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 @@ -13,94 +13,87 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# -# -import sys import os import re +from sys import exit + from vyos.config import Config -from vyos import ConfigError from vyos.util import popen, run - +from vyos import ConfigError from vyos import airbag airbag.enable() -# Define for recovering -gl_ipsec_conf = None +qat_init_script = '/etc/init.d/qat_service' def get_config(config=None): - if config: - c = config - else: - c = Config() - config_data = { - 'qat_conf' : None, - 'ipsec_conf' : None, - 'openvpn_conf' : None, - } - - if c.exists('system acceleration qat'): - config_data['qat_conf'] = True - - if c.exists('vpn ipsec '): - gl_ipsec_conf = True - config_data['ipsec_conf'] = True - - if c.exists('interfaces openvpn'): - config_data['openvpn_conf'] = True - - return config_data - -# Control configured VPN service which can use QAT -def vpn_control(action): - # XXX: Should these commands report failure - if action == 'restore' and gl_ipsec_conf: - return run('ipsec start') - return run(f'ipsec {action}') - -def verify(c): - # Check if QAT service installed - if not os.path.exists('/etc/init.d/qat_service'): - raise ConfigError("Warning: QAT init file not found") - - if c['qat_conf'] == None: - return - - # Check if QAT device exist - output, err = popen('lspci -nn', decode='utf-8') - if not err: - data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) - #If QAT devices found - if not data: - print("\t No QAT acceleration device found") - sys.exit(1) - -def apply(c): - if c['ipsec_conf']: + if config: + conf = config + else: + conf = Config() + + data = {} + + if conf.exists(['system', 'acceleration', 'qat']): + data.update({'qat_enable' : ''}) + + if conf.exists(['vpn', 'ipsec']): + data.update({'ipsec' : ''}) + + if conf.exists(['interfaces', 'openvpn']): + data.update({'openvpn' : ''}) + + return data + + +def vpn_control(action, force_ipsec=False): + # XXX: Should these commands report failure? + if action == 'restore' and force_ipsec: + return run('ipsec start') + + return run(f'ipsec {action}') + + +def verify(qat): + if 'qat_enable' not in qat: + return + + # Check if QAT service installed + if not os.path.exists(qat_init_script): + raise ConfigError('QAT init script not found') + + # Check if QAT device exist + output, err = popen('lspci -nn', decode='utf-8') + if not err: + data = re.findall( + '(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) + # If QAT devices found + if not data: + raise ConfigError('No QAT acceleration device found') + +def apply(qat): # Shutdown VPN service which can use QAT - vpn_control('stop') + if 'ipsec' in qat: + vpn_control('stop') + + # Enable/Disable QAT service + if 'qat_enable' in qat: + run(f'{qat_init_script} start') + else: + run(f'{qat_init_script} stop') - # Disable QAT service - if c['qat_conf'] == None: - run('/etc/init.d/qat_service stop') - if c['ipsec_conf']: - vpn_control('start') - return + # Recover VPN service + if 'ipsec' in qat: + vpn_control('start') - # Run qat init.d script - run('/etc/init.d/qat_service start') - if c['ipsec_conf']: - # Recovery VPN service - vpn_control('start') if __name__ == '__main__': - try: - c = get_config() - verify(c) - apply(c) - except ConfigError as e: - print(e) - vpn_control('restore') - sys.exit(1) + try: + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + vpn_control('restore', force_ipsec=('ipsec' in c)) + exit(1) -- cgit v1.2.3 From 5136870ca682b2f32e5ccfc68251cba5664623f3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 10:32:56 +0200 Subject: qat: T2857: add very limited smoketest --- .../scripts/cli/test_system_acceleration_qat.py | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100755 smoketest/scripts/cli/test_system_acceleration_qat.py diff --git a/smoketest/scripts/cli/test_system_acceleration_qat.py b/smoketest/scripts/cli/test_system_acceleration_qat.py new file mode 100755 index 000000000..c937c810e --- /dev/null +++ b/smoketest/scripts/cli/test_system_acceleration_qat.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Francois Mertz fireboxled@gmail.com +# +# 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 +import unittest + +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError + +base_path = ['system', 'acceleration', 'qat'] + +class TestSystemLCD(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_basic(self): + """ Check if configuration script is in place and that the config + script throws an error as QAT device is not present in Qemu. This *must* + be extended with QAT autodetection once run on a QAT enabled device """ + + # configure some system display + self.session.set(base_path) + + # An error must be thrown if QAT device could not be found + with self.assertRaises(ConfigSessionError): + self.session.commit() + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.3 From 25136d9a9501dcc40c31f9db8e90be3eb5569d24 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 13:17:03 +0200 Subject: ddclient: T2858: migrate to get_config_dict() --- data/templates/dynamic-dns/ddclient.conf.tmpl | 73 ++++----- interface-definitions/dns-dynamic.xml.in | 1 + src/conf_mode/dynamic_dns.py | 217 +++++++------------------- 3 files changed, 99 insertions(+), 192 deletions(-) diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl index 9c7219230..6fbbb50c3 100644 --- a/data/templates/dynamic-dns/ddclient.conf.tmpl +++ b/data/templates/dynamic-dns/ddclient.conf.tmpl @@ -3,44 +3,47 @@ daemon=1m syslog=yes ssl=yes -{% for interface in interfaces -%} +{% for iface in interface %} +# ddclient configuration for interface "{{ iface }}" +{% if interface[iface].use_web is defined and interface[iface].use_web is not none %} +{% set web_skip = ", web-skip='" + interface[iface].use_web.skip + "'" if interface[iface].use_web.skip is defined else '' %} +use=web, web='{{ interface[iface].use_web.url }}'{{ web_skip }} +{% else %} +use=if, if={{ iface }} +{% endif %} -# -# ddclient configuration for interface "{{ interface.interface }}": -# -{% if interface.web_url -%} -use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %} -{% else -%} -use=if, if={{ interface.interface }} -{% endif -%} - -{% for rfc in interface.rfc2136 -%} -{% for record in rfc.record %} -# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} -server={{ rfc.server }} +{% if interface[iface].rfc2136 is defined and interface[iface].rfc2136 is not none %} +{% for rfc2136, config in interface[iface].rfc2136.items() %} +{% for dns_record in config.record if config.record is defined %} +# RFC2136 dynamic DNS configuration for {{ rfc2136 }}, {{ config.zone }}, {{ dns_record }} +server={{ config.server }} protocol=nsupdate -password={{ rfc.keyfile }} -ttl={{ rfc.ttl }} -zone={{ rfc.zone }} -{{ record }} -{% endfor -%} -{% endfor -%} +password={{ config.keyfile }} +ttl={{ config.ttl }} +zone={{ config.zone }} +{{ dns_record }} + +{% endfor %} +{% endfor %} +{% endif %} -{% for srv in interface.service %} -{% for host in srv.host %} -# DynDNS provider configuration for {{ host }} -protocol={{ srv.protocol }}, +{% if interface[iface].service is defined and interface[iface].service is not none %} +{% for service, config in interface[iface].service.items() %} +{% for dns_record in config.host_name %} +# DynDNS provider configuration for {{ service }}, {{ dns_record }} +protocol={{ config.protocol }}, max-interval=28d, -login={{ srv.login }}, -password='{{ srv.password }}', -{% if srv.server -%} -server={{ srv.server }}, -{% endif -%} -{% if srv.zone -%} -zone={{ srv.zone }}, -{% endif -%} -{{ host }} -{% endfor %} -{% endfor %} +login={{ config.login }}, +password='{{ config.password }}', +{% if config.server %} +server={{ config.server }}, +{% endif %} +{% if config.zone %} +zone={{ config.zone }}, +{% endif %} +{{ dns_record }} +{% endfor %} +{% endfor %} +{% endif %} {% endfor %} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 143c04ef6..34a31a7c5 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -58,6 +58,7 @@ + 600 diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 57c910a68..93e995b78 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -17,14 +17,13 @@ import os from sys import exit -from copy import deepcopy -from stat import S_IRUSR, S_IWUSR from vyos.config import Config -from vyos import ConfigError -from vyos.util import call +from vyos.configdict import dict_merge from vyos.template import render - +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() @@ -45,197 +44,101 @@ default_service_protocol = { 'zoneedit': 'zoneedit1' } -default_config_data = { - 'interfaces': [], - 'deleted': False -} - def get_config(config=None): - dyndns = deepcopy(default_config_data) if config: conf = config else: conf = Config() - base_level = ['service', 'dns', 'dynamic'] + base_level = ['service', 'dns', 'dynamic'] if not conf.exists(base_level): - dyndns['deleted'] = True - return dyndns - - for interface in conf.list_nodes(base_level + ['interface']): - node = { - 'interface': interface, - 'rfc2136': [], - 'service': [], - 'web_skip': '', - 'web_url': '' - } - - # set config level to e.g. "service dns dynamic interface eth0" - conf.set_level(base_level + ['interface', interface]) - # Handle RFC2136 - Dynamic Updates in the Domain Name System - for rfc2136 in conf.list_nodes(['rfc2136']): - rfc = { - 'name': rfc2136, - 'keyfile': '', - 'record': [], - 'server': '', - 'ttl': '600', - 'zone': '' - } - - # set config level - conf.set_level(base_level + ['interface', interface, 'rfc2136', rfc2136]) - - if conf.exists(['key']): - rfc['keyfile'] = conf.return_value(['key']) - - if conf.exists(['record']): - rfc['record'] = conf.return_values(['record']) - - if conf.exists(['server']): - rfc['server'] = conf.return_value(['server']) - - if conf.exists(['ttl']): - rfc['ttl'] = conf.return_value(['ttl']) - - if conf.exists(['zone']): - rfc['zone'] = conf.return_value(['zone']) - - node['rfc2136'].append(rfc) - - # set config level to e.g. "service dns dynamic interface eth0" - conf.set_level(base_level + ['interface', interface]) - # Handle DynDNS service providers - for service in conf.list_nodes(['service']): - srv = { - 'provider': service, - 'host': [], - 'login': '', - 'password': '', - 'protocol': '', - 'server': '', - 'custom' : False, - 'zone' : '' - } - - # set config level - conf.set_level(base_level + ['interface', interface, 'service', service]) - - # preload protocol from default service mapping - if service in default_service_protocol.keys(): - srv['protocol'] = default_service_protocol[service] - else: - srv['custom'] = True - - if conf.exists(['login']): - srv['login'] = conf.return_value(['login']) - - if conf.exists(['host-name']): - srv['host'] = conf.return_values(['host-name']) - - if conf.exists(['protocol']): - srv['protocol'] = conf.return_value(['protocol']) - - if conf.exists(['password']): - srv['password'] = conf.return_value(['password']) - - if conf.exists(['server']): - srv['server'] = conf.return_value(['server']) - - if conf.exists(['zone']): - srv['zone'] = conf.return_value(['zone']) - elif srv['provider'] == 'cloudflare': - # default populate zone entry with bar.tld if - # host-name is foo.bar.tld - srv['zone'] = srv['host'][0].split('.',1)[1] - - node['service'].append(srv) - - # Set config back to appropriate level for these options - conf.set_level(base_level + ['interface', interface]) - - # Additional settings in CLI - if conf.exists(['use-web', 'skip']): - node['web_skip'] = conf.return_value(['use-web', 'skip']) - - if conf.exists(['use-web', 'url']): - node['web_url'] = conf.return_value(['use-web', 'url']) - - # set config level back to top level - conf.set_level(base_level) - - dyndns['interfaces'].append(node) + return None + + dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + for interface in dyndns['interface']: + if 'service' in dyndns['interface'][interface]: + # 'Autodetect' protocol used by DynDNS service + for service in dyndns['interface'][interface]['service']: + if service in default_service_protocol: + dyndns['interface'][interface]['service'][service].update( + {'protocol' : default_service_protocol.get(service)}) + else: + dyndns['interface'][interface]['service'][service].update( + {'custom': ''}) + + if 'rfc2136' in dyndns['interface'][interface]: + default_values = defaults(base_level + ['interface', 'rfc2136']) + for rfc2136 in dyndns['interface'][interface]['rfc2136']: + dyndns['interface'][interface]['rfc2136'][rfc2136] = dict_merge( + default_values, dyndns['interface'][interface]['rfc2136'][rfc2136]) return dyndns def verify(dyndns): # bail out early - looks like removal from running config - if dyndns['deleted']: + if not dyndns: return None # A 'node' corresponds to an interface - for node in dyndns['interfaces']: + if 'interface' not in dyndns: + return None + for interface in dyndns['interface']: # RFC2136 - configuration validation - for rfc2136 in node['rfc2136']: - if not rfc2136['record']: - raise ConfigError('Set key for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + if 'rfc2136' in dyndns['interface'][interface]: + for rfc2136, config in dyndns['interface'][interface]['rfc2136'].items(): - if not rfc2136['zone']: - raise ConfigError('Set zone for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + for tmp in ['record', 'zone', 'server', 'key']: + if tmp not in config: + raise ConfigError(f'"{tmp}" required for rfc2136 based ' + f'DynDNS service on "{interface}"') - if not rfc2136['keyfile']: - raise ConfigError('Set keyfile for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) - else: - if not os.path.isfile(rfc2136['keyfile']): - raise ConfigError('Keyfile for service "{0}" to send DDNS updates for interface "{1}" does not exist'.format(rfc2136['name'], node['interface'])) - - if not rfc2136['server']: - raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + if not os.path.isfile(config['key']): + raise ConfigError(f'"key"-file not found for rfc2136 based ' + f'DynDNS service on "{interface}"') # DynDNS service provider - configuration validation - for service in node['service']: - if not service['host']: - raise ConfigError('Set host-name for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + if 'service' in dyndns['interface'][interface]: + for service, config in dyndns['interface'][interface]['service'].items(): + error_msg = f'required for DynDNS service "{service}" on "{interface}"' + if 'host_name' not in config: + raise ConfigError(f'"host-name" {error_msg}') - if not service['login']: - raise ConfigError('Set login for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + if 'login' not in config: + raise ConfigError(f'"login" (username) {error_msg}') - if not service['password']: - raise ConfigError('Set password for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + if 'password' not in config: + raise ConfigError(f'"password" {error_msg}') - if service['custom'] is True: - if not service['protocol']: - raise ConfigError('Set protocol for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + if 'zone' in config: + if service != 'cloudflare': + raise ConfigError(f'"zone" option only supported with CloudFlare') - if not service['server']: - raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + if 'custom' in config: + if 'protocol' not in config: + raise ConfigError(f'"protocol" {error_msg}') - if service['zone']: - if service['provider'] != 'cloudflare': - raise ConfigError('Zone option not allowed for "{0}", it can only be used for CloudFlare'.format(service['provider'])) + if 'server' not in config: + raise ConfigError(f'"server" {error_msg}') return None def generate(dyndns): # bail out early - looks like removal from running config - if dyndns['deleted']: + if not dyndns: return None - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) - - # Config file must be accessible only by its owner - os.chmod(config_file, S_IRUSR | S_IWUSR) - + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, trim_blocks=True, permission=0o600) return None def apply(dyndns): - if dyndns['deleted']: + if not dyndns: call('systemctl stop ddclient.service') if os.path.exists(config_file): os.unlink(config_file) - else: call('systemctl restart ddclient.service') -- cgit v1.2.3 From 54c08da5a77e325b024415805fc2586afa1b0e8c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 15:32:57 +0200 Subject: bonding: T2877: support configuration of minimum number of active links Specifies the minimum number of links that must be active before asserting carrier. It is similar to the Cisco EtherChannel min-links feature. This allows setting the minimum number of member ports that must be up (link-up state) before marking the bond device as up (carrier on). This is useful for situations where higher level services such as clustering want to ensure a minimum number of low bandwidth links are active before switchover. This option only affects 802.3ad mode. The default value is 0. This will cause carrier to be asserted (for 802.3ad mode) whenever there is an active aggregator, regardless of the number of available links in that aggregator. Note that, because an aggregator cannot be active without at least one available link, setting this option to 0 or to 1 has the exact same effect. --- interface-definitions/interfaces-bonding.xml.in | 13 +++++++++++ python/vyos/ifconfig/bond.py | 31 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 7d658f6a0..b28be387b 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -99,6 +99,19 @@ #include + + + Minimum number of member interfaces required up before enabling bond + + <0-16> + Minimum number of member interfaces required up before enabling bond + + + + + + 0 + Bonding mode diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 64407401b..67dcd2b69 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -52,6 +52,10 @@ class BondIf(Interface): 'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']), 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy', }, + 'bond_min_links': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/min_links', + }, 'bond_miimon': { 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/bonding/miimon' @@ -130,6 +134,29 @@ class BondIf(Interface): """ self.set_interface('bond_hash_policy', mode) + def set_min_links(self, number): + """ + Specifies the minimum number of links that must be active before + asserting carrier. It is similar to the Cisco EtherChannel min-links + feature. This allows setting the minimum number of member ports that + must be up (link-up state) before marking the bond device as up + (carrier on). This is useful for situations where higher level services + such as clustering want to ensure a minimum number of low bandwidth + links are active before switchover. This option only affect 802.3ad + mode. + + The default value is 0. This will cause carrier to be asserted (for + 802.3ad mode) whenever there is an active aggregator, regardless of the + number of available links in that aggregator. Note that, because an + aggregator cannot be active without at least one available link, + setting this option to 0 or to 1 has the exact same effect. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_min_links('0') + """ + self.set_interface('bond_min_links', number) + def set_arp_interval(self, interval): """ Specifies the ARP link monitoring frequency in milliseconds. @@ -347,6 +374,10 @@ class BondIf(Interface): value = config.get('hash_policy') if value: self.set_hash_policy(value) + # Minimum number of member interfaces + value = config.get('min_links') + if value: self.set_min_links(value) + # Some interface options can only be changed if the interface is # administratively down if self.get_admin_state() == 'down': -- cgit v1.2.3 From 7f15ef3d994f4ba6bb7ef7e1c47957a9deb7378f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 15:45:01 +0200 Subject: bonding: T2878: new op-mode command: show interfaces bonding bond0 detail vyos@vyos:~$ show interfaces bonding bond5 detail Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011) Bonding Mode: IEEE 802.3ad Dynamic link aggregation Transmit Hash Policy: layer2 (0) MII Status: down MII Polling Interval (ms): 100 Up Delay (ms): 0 Down Delay (ms): 0 802.3ad info LACP rate: slow Min links: 0 Aggregator selection policy (ad_select): stable Slave Interface: eth1 MII Status: down Speed: Unknown Duplex: Unknown Link Failure Count: 0 Permanent HW addr: 00:50:56:bf:ef:aa Slave queue ID: 0 Aggregator ID: 1 Actor Churn State: churned Partner Churn State: churned Actor Churned Count: 1 Partner Churned Count: 1 Slave Interface: eth2 MII Status: down Speed: Unknown Duplex: Unknown Link Failure Count: 0 Permanent HW addr: 00:50:56:bf:19:26 Slave queue ID: 0 Aggregator ID: 2 Actor Churn State: churned Partner Churn State: churned Actor Churned Count: 1 Partner Churned Count: 1 --- op-mode-definitions/show-interfaces-bonding.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/op-mode-definitions/show-interfaces-bonding.xml b/op-mode-definitions/show-interfaces-bonding.xml index 568b215af..c1c76b059 100644 --- a/op-mode-definitions/show-interfaces-bonding.xml +++ b/op-mode-definitions/show-interfaces-bonding.xml @@ -19,6 +19,12 @@ ${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief + + + Show detailed interface information + + if [ -f "/proc/net/bonding/$4" ]; then cat "/proc/net/bonding/$4"; else echo "Interface $4 does not exist!"; fi + Show specified virtual network interface (vif) information -- cgit v1.2.3 From d6fde0b09d14cccb7691bff4135ab5a265cf6971 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 16:22:27 +0200 Subject: bandwidth-test: T2841: enable IPv6 reception which can receive both on IPv4/IPv6 --- op-mode-definitions/monitor-bandwidth-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-mode-definitions/monitor-bandwidth-test.xml b/op-mode-definitions/monitor-bandwidth-test.xml index d1e459b17..5959e05f2 100644 --- a/op-mode-definitions/monitor-bandwidth-test.xml +++ b/op-mode-definitions/monitor-bandwidth-test.xml @@ -11,7 +11,7 @@ Wait for bandwidth test connections (port TCP/5001) - iperf -s + /usr/bin/iperf -V -s -- cgit v1.2.3 From 8ae88b5ba5cbc34b9992ccdde4229d44cfe56225 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Sep 2020 17:28:42 +0200 Subject: op-mode: T2841: support IPv6 for "monitor bandwidth-test initiate" --- op-mode-definitions/monitor-bandwidth-test.xml | 2 +- src/op_mode/monitor_bandwidth_test.sh | 30 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 src/op_mode/monitor_bandwidth_test.sh diff --git a/op-mode-definitions/monitor-bandwidth-test.xml b/op-mode-definitions/monitor-bandwidth-test.xml index 5959e05f2..5b36b1da5 100644 --- a/op-mode-definitions/monitor-bandwidth-test.xml +++ b/op-mode-definitions/monitor-bandwidth-test.xml @@ -20,7 +20,7 @@ <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> - iperf -c $4 + ${vyos_op_scripts_dir}/monitor_bandwidth_test.sh "$4" diff --git a/src/op_mode/monitor_bandwidth_test.sh b/src/op_mode/monitor_bandwidth_test.sh new file mode 100755 index 000000000..6da0291c5 --- /dev/null +++ b/src/op_mode/monitor_bandwidth_test.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# +# Copyright (C) 2020 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 . + +if ipaddrcheck --is-ipv6 $1; then + # Set address family to IPv6 when an IPv6 address was specified + OPT="-V" +elif [[ $(dig $1 AAAA +short | grep -v '\.$' | wc -l) -gt 0 ]]; then + # CNAME is also part of the dig answer thus we must remove any + # CNAME response and only shot the AAAA response(s), this is done + # by grep -v '\.$' + + # Set address family to IPv6 when FQDN has at least one AAAA record + OPT="-V" +fi + +/usr/bin/iperf $OPT -c $1 + -- cgit v1.2.3 From b82871584b2a087b5b690f8eace1e99b7c948cf3 Mon Sep 17 00:00:00 2001 From: sever-sever Date: Mon, 14 Sep 2020 07:27:48 +0000 Subject: op-mode: T2874: Add new utill for mtu-check --- op-mode-definitions/force-mtu-host.xml | 34 ++++++++++++++++++++++ src/op_mode/force_mtu_host.sh | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 op-mode-definitions/force-mtu-host.xml create mode 100755 src/op_mode/force_mtu_host.sh diff --git a/op-mode-definitions/force-mtu-host.xml b/op-mode-definitions/force-mtu-host.xml new file mode 100644 index 000000000..b92179f11 --- /dev/null +++ b/op-mode-definitions/force-mtu-host.xml @@ -0,0 +1,34 @@ + + + + + + + Show MTU max value for remote host protocol TCP + + + + + IP address of the remote host + + <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> + + + ${vyos_op_scripts_dir}/force_mtu_host.sh $4 + + + + Source interface + + + + + ${vyos_op_scripts_dir}/force_mtu_host.sh $4 $6 + + + + + + + + diff --git a/src/op_mode/force_mtu_host.sh b/src/op_mode/force_mtu_host.sh new file mode 100755 index 000000000..02955c729 --- /dev/null +++ b/src/op_mode/force_mtu_host.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# Module: vyos-show-ram.sh +# Displays memory usage information in minimalistic format +# +# Copyright (C) 2020 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 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 . + +target=$1 +interface=$2 + +# IPv4 header 20 byte + TCP header 20 byte +ipv4_overhead=40 + +# IPv6 headter 40 byte + TCP header 20 byte +ipv6_overhead=60 + +# If no arguments +if [[ $# -eq 0 ]] ; then + echo "Target host not defined" + exit 1 +fi + +# If one argument, it's ip address. If 2, the second arg "interface" +if [[ $# -eq 1 ]] ; then + mtu=$(sudo nmap -T4 --script path-mtu -F $target | grep "PMTU" | awk {'print $NF'}) +elif [[ $# -eq 2 ]]; then + mtu=$(sudo nmap -T4 -e $interface --script path-mtu -F $target | grep "PMTU" | awk {'print $NF'}) +fi + +tcpv4_mss=$(($mtu-$ipv4_overhead)) +tcpv6_mss=$(($mtu-$ipv6_overhead)) + +echo " +Recommended maximum values (or less) for target $target: +--- +MTU: $mtu +TCP-MSS: $tcpv4_mss +TCP-MSS_IPv6: $tcpv6_mss +" + -- cgit v1.2.3 From bf1d6fff80eebb579f2c33b1352a7162b8474730 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 14 Sep 2020 15:41:22 -0500 Subject: configd: T2885: print commit errors to config session terminal --- src/services/vyos-configd | 19 +++++++++++++++++++ src/shim/vyshim.c | 10 ++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 75f84d3df..579605e8c 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -62,6 +62,8 @@ configd_env_file = '/etc/default/vyos-configd-env' active_string = '' session_string = '' +session_tty = None + def key_name_from_file_name(f): return os.path.splitext(f)[0] @@ -105,6 +107,13 @@ conf_mode_scripts = dict(zip(imports, modules)) exclude_set = {key_name_from_file_name(f) for f in filenames if f not in include} include_set = {key_name_from_file_name(f) for f in filenames if f in include} +def explicit_print(t, m): + try: + with open(t, 'w') as f: + f.write(m) + f.flush() + except Exception: + pass def run_script(script, config) -> int: config.set_level([]) @@ -115,6 +124,7 @@ def run_script(script, config) -> int: script.apply(c) except ConfigError as e: logger.critical(e) + explicit_print(session_tty, e) return R_ERROR_COMMIT except Exception: return R_ERROR_DAEMON @@ -132,6 +142,15 @@ def initialization(socket): session_string = socket.recv().decode() resp = "session" socket.send(resp.encode()) + pid_string = socket.recv().decode() + resp = "pid" + socket.send(resp.encode()) + + logger.debug(f"config session pid is {pid_string}") + try: + session_tty = os.readlink(f"/proc/{pid_string}/fd/1") + except FileNotFoundError: + session_tty = None configsource = ConfigSourceString(running_config_text=active_string, session_config_text=session_string) diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c index 8b6feab99..196e3221e 100644 --- a/src/shim/vyshim.c +++ b/src/shim/vyshim.c @@ -162,6 +162,10 @@ int initialization(void* Requester) double prev_time_value, time_value; double time_diff; + char *pid_val = getenv("VYATTA_CONFIG_TMP"); + strsep(&pid_val, "_"); + debug_print("config session pid: %s\n", pid_val); + debug_print("Sending init announcement\n"); char *init_announce = mkjson(MKJSON_OBJ, 1, MKJSON_STRING, "type", "init"); @@ -219,6 +223,12 @@ int initialization(void* Requester) free(session_str); + debug_print("Sending config session pid\n"); + zmq_send(Requester, pid_val, strlen(pid_val), 0); + zmq_recv(Requester, buffer, 16, 0); + debug_print("Received pid receipt\n"); + + return 0; } -- cgit v1.2.3 From f8a6fa6a5a574851292e77e08cff16cdf6195334 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 15 Sep 2020 18:52:18 +0200 Subject: vyos.configdict: T2515: leaf_node_changed() should return list or None --- python/vyos/configdict.py | 6 +++--- src/conf_mode/interfaces-l2tpv3.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index e8c0aa5b3..bfc70b772 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -148,8 +148,8 @@ def T2665_default_dict_cleanup(dict): def leaf_node_changed(conf, path): """ Check if a leaf node was altered. If it has been altered - values has been - changed, or it was added/removed, we will return the old value. If nothing - has been changed, None is returned + changed, or it was added/removed, we will return a list containing the old + value(s). If nothing has been changed, None is returned """ from vyos.configdiff import get_config_diff D = get_config_diff(conf, key_mangling=('-', '_')) @@ -157,7 +157,7 @@ def leaf_node_changed(conf, path): (new, old) = D.get_value_diff(path) if new != old: if isinstance(old, str): - return old + return [old] elif isinstance(old, list): if isinstance(new, str): new = [new] diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 8250a3df8..144cee5fe 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -56,10 +56,11 @@ def get_config(config=None): # To delete an l2tpv3 interface we need the current tunnel and session-id if 'deleted' in l2tpv3: tmp = leaf_node_changed(conf, ['tunnel-id']) - l2tpv3.update({'tunnel_id': tmp}) + # leaf_node_changed() returns a list + l2tpv3.update({'tunnel_id': tmp[0]}) tmp = leaf_node_changed(conf, ['session-id']) - l2tpv3.update({'session_id': tmp}) + l2tpv3.update({'session_id': tmp[0]}) return l2tpv3 -- cgit v1.2.3 From 98d95b677867c27064d84033dc451ba04c9a2b7b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 15 Sep 2020 18:54:09 +0200 Subject: bonding: T2515: preserve interface admin state when removing from bond Removing a member from a bond/LACP will turn the physical interface always in admin-down state. This is invalid, the interface should be placed into the state configured on the VyOS CLI. Smoketest on bond interfaces is extended to check this behavior. --- python/vyos/ifconfig/bond.py | 22 ++++++++----- smoketest/scripts/cli/test_interfaces_bonding.py | 24 ++++++++++++++ src/conf_mode/interfaces-bonding.py | 42 +++++++++++++----------- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 67dcd2b69..c33cf30bf 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors +# Copyright 2019-2020 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 @@ -381,9 +381,14 @@ class BondIf(Interface): # Some interface options can only be changed if the interface is # administratively down if self.get_admin_state() == 'down': - # Delete bond member port(s) + # Remove ALL bond member interfaces for interface in self.get_slaves(): self.del_port(interface) + # Removing an interface from a bond will always place the underlaying + # physical interface in admin-down state! If physical interface is + # not disabled, re-enable it. + if not vyos_dict_search(f'member.interface_remove.{interface}.disable', config): + Interface(interface).set_admin_state('up') # Bonding policy/mode value = config.get('mode') @@ -391,13 +396,12 @@ class BondIf(Interface): # Add (enslave) interfaces to bond value = vyos_dict_search('member.interface', config) - if value: - for interface in value: - # if we've come here we already verified the interface - # does not have an addresses configured so just flush - # any remaining ones - Interface(interface).flush_addrs() - self.add_port(interface) + for interface in (value or []): + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + self.add_port(interface) # Primary device interface - must be set after 'mode' value = config.get('primary') diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index e3d3b25ee..b165883b9 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -20,6 +20,7 @@ import unittest from base_interfaces_test import BasicInterfaceTest from vyos.ifconfig import Section +from vyos.ifconfig.interface import Interface from vyos.configsession import ConfigSessionError from vyos.util import read_file @@ -57,5 +58,28 @@ class BondingInterfaceTest(BasicInterfaceTest.BaseTest): slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split() self.assertListEqual(slaves, self._members) + def test_remove_member(self): + """ T2515: when removing a bond member the interface must be admin-up again """ + + # configure member interfaces + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.session.set(self._base_path + [interface] + option.split()) + + self.session.commit() + + # remove single bond member port + for interface in self._interfaces: + remove_member = self._members[0] + self.session.delete(self._base_path + [interface, 'member', 'interface', remove_member]) + + self.session.commit() + + # removed member port must be admin-up + for interface in self._interfaces: + remove_member = self._members[0] + state = Interface(remove_member).get_admin_state() + self.assertEqual('up', state) + if __name__ == '__main__': unittest.main() diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 16e6e4f6e..a9679b47c 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -29,6 +29,7 @@ from vyos.configverify import verify_source_interface from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.ifconfig import BondIf +from vyos.ifconfig import Section from vyos.validate import is_member from vyos.validate import has_address_configured from vyos import ConfigError @@ -69,31 +70,33 @@ def get_config(config=None): # into a dictionary - we will use this to add additional information # later on for wach member if 'member' in bond and 'interface' in bond['member']: - # first convert it to a list if only one member is given - if isinstance(bond['member']['interface'], str): - bond['member']['interface'] = [bond['member']['interface']] - - tmp={} - for interface in bond['member']['interface']: - tmp.update({interface: {}}) - - bond['member']['interface'] = tmp + # convert list if member interfaces to a dictionary + bond['member']['interface'] = dict.fromkeys( + bond['member']['interface'], {}) if 'mode' in bond: bond['mode'] = get_bond_mode(bond['mode']) tmp = leaf_node_changed(conf, ['mode']) - if tmp: - bond.update({'shutdown_required': ''}) + if tmp: bond.update({'shutdown_required': {}}) # determine which members have been removed - tmp = leaf_node_changed(conf, ['member', 'interface']) - if tmp: - bond.update({'shutdown_required': ''}) - if 'member' in bond: - bond['member'].update({'interface_remove': tmp }) - else: - bond.update({'member': {'interface_remove': tmp }}) + interfaces_removed = leaf_node_changed(conf, ['member', 'interface']) + if interfaces_removed: + bond.update({'shutdown_required': {}}) + if 'member' not in bond: + bond.update({'member': {}}) + + tmp = {} + for interface in interfaces_removed: + section = Section.section(interface) # this will be 'ethernet' for 'eth0' + if conf.exists(['insterfaces', section, interface, 'disable']): + tmp.update({interface : {'disable': ''}}) + else: + tmp.update({interface : {}}) + + # also present the interfaces to be removed from the bond as dictionary + bond['member'].update({'interface_remove': tmp}) if 'member' in bond and 'interface' in bond['member']: for interface, interface_config in bond['member']['interface'].items(): @@ -109,8 +112,7 @@ def get_config(config=None): # bond members must not have an assigned address tmp = has_address_configured(conf, interface) - if tmp: - interface_config.update({'has_address' : ''}) + if tmp: interface_config.update({'has_address' : ''}) return bond -- cgit v1.2.3 From 40ca599350731a743a0d999205df10829017a783 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 15 Sep 2020 18:55:46 +0200 Subject: completion: T2238: add license --- src/completion/list_interfaces.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index e27281433..b19b90156 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -1,16 +1,28 @@ #!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 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 sys import argparse from vyos.ifconfig import Section - def matching(feature): for section in Section.feature(feature): for intf in Section.interfaces(section): yield intf - parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") -- cgit v1.2.3 From 7faa60cd31d6dfaafbf60086edba890751dd7002 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 18:55:24 +0200 Subject: smoketest: T2886: RADIUS requires CONFIG_AUDIT Kernel option --- smoketest/scripts/system/test_kernel_options.py | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 smoketest/scripts/system/test_kernel_options.py diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py new file mode 100755 index 000000000..861132127 --- /dev/null +++ b/smoketest/scripts/system/test_kernel_options.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 +import platform +import unittest + +kernel = platform.release() +with open(f'/boot/config-{kernel}') as f: + config = f.read() + +class TestKernelModules(unittest.TestCase): + + def test_radius_auth_t2886(self): + # T2886 - RADIUS authentication - check for statically compiled + # options (=y) + for option in ['CONFIG_AUDIT', 'CONFIG_HAVE_ARCH_AUDITSYSCALL', + 'CONFIG_AUDITSYSCALL', 'CONFIG_AUDIT_WATCH', + 'CONFIG_AUDIT_TREE', 'CONFIG_AUDIT_ARCH']: + self.asserIn(f'{option}=y', config) + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.3 From 6ac916ecd58cf1515dd5b9b47283d5528d0c265b Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 16 Sep 2020 11:24:16 -0500 Subject: configd: T2885: fix output of error string to config session --- src/services/vyos-configd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 579605e8c..642952936 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -111,6 +111,7 @@ def explicit_print(t, m): try: with open(t, 'w') as f: f.write(m) + f.write("\n") f.flush() except Exception: pass @@ -124,7 +125,7 @@ def run_script(script, config) -> int: script.apply(c) except ConfigError as e: logger.critical(e) - explicit_print(session_tty, e) + explicit_print(session_tty, str(e)) return R_ERROR_COMMIT except Exception: return R_ERROR_DAEMON @@ -132,6 +133,7 @@ def run_script(script, config) -> int: return R_SUCCESS def initialization(socket): + global session_tty # Reset config strings: active_string = '' session_string = '' -- cgit v1.2.3 From eb8d51724236607baa91cc29d1d084dbc0e1762a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 19:46:01 +0200 Subject: wireless: T2887: fix template renderint on ht and vht options --- data/templates/wifi/hostapd.conf.tmpl | 147 +++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 54 deletions(-) diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index a7efee6d5..b00515d76 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -124,38 +124,58 @@ ieee80211w=2 # DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) # 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) # L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) -{% set output = '' %} -{% set output = output + '[40-INTOLERANT]' if capabilities.ht.fourtymhz_incapable is defined else '' %} -{% set output = output + '[DELAYED-BA]' if capabilities.ht.delayed_block_ack is defined else '' %} -{% set output = output + '[DSSS_CCK-40]' if capabilities.ht.dsss_cck_40 is defined else '' %} -{% set output = output + '[GF]' if capabilities.ht.greenfield is defined else '' %} -{% set output = output + '[LDPC]' if capabilities.ht.ldpc is defined else '' %} -{% set output = output + '[LSIG-TXOP-PROT]' if capabilities.ht.lsig_protection is defined else '' %} -{% set output = output + '[TX-STBC]' if capabilities.ht.stbc.tx is defined else '' %} -{% set output = output + '[RX-STBC-' + capabilities.ht.stbc.rx | upper + ']' if capabilities.ht.stbc.tx is defined else '' %} -{% set output = output + '[MAX-AMSDU-' + capabilities.ht.max_amsdu + ']' if capabilities.ht.max_amsdu is defined else '' %} -{% set output = output + '[SMPS-' + capabilities.ht.smps | upper + ']' if capabilities.ht.smps is defined else '' %} +{% set output = namespace(value='') %} + +{% if capabilities.ht.fourtymhz_incapable is defined %} +{% set output.value = output.value + '[40-INTOLERANT]' %} +{% endif %} +{% if capabilities.ht.delayed_block_ack is defined %} +{% set output.value = output.value + '[DELAYED-BA]' %} +{% endif %} +{% if capabilities.ht.dsss_cck_40 is defined %} +{% set output.value = output.value + '[DSSS_CCK-40]' %} +{% endif %} +{% if capabilities.ht.greenfield is defined %} +{% set output.value = output.value + '[GF]' %} +{% endif %} +{% if capabilities.ht.ldpc is defined %} +{% set output.value = output.value + '[LDPC]' %} +{% endif %} +{% if capabilities.ht.lsig_protection is defined %} +{% set output.value = output.value + '[LSIG-TXOP-PROT]' %} +{% endif %} +{% if capabilities.ht.stbc is defined and capabilities.ht.stbc.tx is defined %} +{% set output.value = output.value + '[TX-STBC]' %} +{% endif %} +{% if capabilities.ht.stbc is defined and capabilities.ht.stbc.rx is defined %} +{% set output.value = output.value + '[RX-STBC-' + capabilities.ht.stbc.rx | upper + ']' %} +{% endif %} +{% if capabilities.ht.max_amsdu is defined %} +{% set output.value = output.value + '[MAX-AMSDU-' + capabilities.ht.max_amsdu + ']' %} +{% endif %} +{% if capabilities.ht.smps is defined %} +{% set output.value = output.value + '[SMPS-' + capabilities.ht.smps | upper + ']' %} +{% endif %} {% if capabilities.ht.channel_set_width is defined %} {% for csw in capabilities.ht.channel_set_width %} -{% set output = output + '[' + csw | upper + ']' %} +{% set output.value = output.value + '[' + csw | upper + ']' %} {% endfor %} {% endif %} {% if capabilities.ht.short_gi is defined %} {% for short_gi in capabilities.ht.short_gi %} -{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% set output.value = output.value + '[SHORT-GI-' + short_gi | upper + ']' %} {% endfor %} {% endif %} -ht_capab={{ output }} +ht_capab={{ output.value }} -{% if capabilities.ht.auto_powersave is defined %} +{% if capabilities.ht.auto_powersave is defined %} # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] # Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) uapsd_advertisement_enabled=1 -{% endif %} - +{% endif %} {% endif %} # Required for full HT and VHT functionality @@ -286,14 +306,14 @@ require_ht=1 # 0 = Tx antenna pattern might change during the lifetime of an association # 1 = Tx antenna pattern does not change during the lifetime of an -{% if capabilities.vht.center_channel_freq.freq_1 is defined %} +{% if capabilities.vht.center_channel_freq is defined and capabilities.vht.center_channel_freq.freq_1 is defined %} # center freq = 5 GHz + (5 * index) # So index 42 gives center freq 5.210 GHz # which is channel 42 in 5G band vht_oper_centr_freq_seg0_idx={{ capabilities.vht.center_channel_freq.freq_1 }} {% endif %} -{% if capabilities.vht.center_channel_freq.freq_2 is defined %} +{% if capabilities.vht.center_channel_freq is defined and capabilities.vht.center_channel_freq.freq_2 is defined %} # center freq = 5 GHz + (5 * index) # So index 159 gives center freq 5.795 GHz # which is channel 159 in 5G band @@ -304,55 +324,74 @@ vht_oper_centr_freq_seg1_idx={{ capabilities.vht.center_channel_freq.freq_2 }} vht_oper_chwidth={{ capabilities.vht.channel_set_width }} {% endif %} -{% set output = '' %} -{% set output = output + '[TX-STBC-2BY1]' if capabilities.vht.stbc.tx is defined else '' %} -{% set output = output + '[RXLDPC]' if capabilities.vht.ldpc is defined else '' %} -{% set output = output + '[VHT-TXOP-PS]' if capabilities.vht.tx_powersave is defined else '' %} -{% set output = output + '[HTC-VHT]' if capabilities.vht.vht_cf is defined else '' %} -{% set output = output + '[RX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} -{% set output = output + '[TX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} - -{% set output = output + '[RX-STBC-' + capabilities.vht.stbc.rx + ']' if capabilities.vht.stbc.rx is defined else '' %} -{% set output = output + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' if capabilities.vht.max_mpdu is defined else '' %} -{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} -{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} - -{% set output = output + '[VHT160]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '2' else '' %} -{% set output = output + '[VHT160-80PLUS80]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '3' else '' %} -{% set output = output + '[VHT-LINK-ADAPT2]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' else '' %} -{% set output = output + '[VHT-LINK-ADAPT3]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both' else '' %} - -{% if capabilities.vht.short_gi is defined %} -{% for short_gi in capabilities.vht.short_gi %} -{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} -{% endfor %} +{% set output = namespace(value='') %} +{% if capabilities.vht.stbc is defined and capabilities.vht.stbc.tx is defined %} +{% set output.value = output.value + '[TX-STBC-2BY1]' %} {% endif %} - -{% if capabilities.vht.beamform %} -{% for beamform in capabilities.vht.beamform %} -{% set output = output + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %} -{% set output = output + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %} -{% set output = output + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer' else '' %} -{% set output = output + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee' else '' %} -{% endfor %} +{% if capabilities.vht.stbc is defined and capabilities.vht.stbc.rx is defined %} +{% set output.value = output.value + '[RX-STBC-' + capabilities.vht.stbc.rx + ']' %} +{% endif %} +{% if capabilities.vht.ldpc is defined %} +{% set output.value = output.value + '[RXLDPC]' %} +{% endif %} +{% if capabilities.vht.tx_powersave is defined %} +{% set output.value = output.value + '[VHT-TXOP-PS]' %} +{% endif %} +{% if capabilities.vht.vht_cf is defined %} +{% set output.value = output.value + '[HTC-VHT]' %} +{% endif %} +{% if capabilities.vht.antenna_pattern_fixed is defined %} +{% set output.value = output.value + '[RX-ANTENNA-PATTERN]' %} +{% endif %} +{% if capabilities.vht.antenna_pattern_fixed is defined %} +{% set output.value = output.value + '[TX-ANTENNA-PATTERN]' %} +{% endif %} +{% if capabilities.vht.max_mpdu is defined %} +{% set output.value = output.value + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' %} +{% endif %} +{% if capabilities.vht.max_mpdu_exp is defined %} +{% set output.value = output.value + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' %} {% endif %} +{% if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '2' %} +{% set output.value = output.value + '[VHT160]' %} +{% endif %} +{% if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '3' %} +{% set output.value = output.value + '[VHT160-80PLUS80]' %} +{% endif %} +{% if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' %} +{% set output.value = output.value + '[VHT-LINK-ADAPT2]' %} +{% endif %} +{% if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both' %} +{% set output.value = output.value + '[VHT-LINK-ADAPT3]' %} +{% endif %} + +{% for short_gi in capabilities.vht.short_gi if capabilities.vht.short_gi is defined %} +{% set output.value = output.value + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} + +{% for beamform in capabilities.vht.beamform if capabilities.vht.beamform is defined %} +{% set output.value = output.value + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %} +{% set output.value = output.value + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %} +{% set output.value = output.value + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer' else '' %} +{% set output.value = output.value + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee' else '' %} +{% endfor %} {% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 %} {% if capabilities.vht.beamform %} {% if beamform == 'single-user-beamformer' %} {% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 6 %} -{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %} -{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% set output.value = output.value + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% set output.value = output.value + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %} {% endif %} {% endif %} {% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 5 %} -{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %} -{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %} +{% set output.value = output.value + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %} +{% set output.value = output.value + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %} {% endif %} {% endif %} {% endif %} -vht_capab={{ output }} +vht_capab={{ output.value }} {% endif %} # ieee80211n: Whether IEEE 802.11n (HT) is enabled -- cgit v1.2.3 From 1a16cd01141d4e8e4c7cfe14fd30caacdfe8b376 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 21:16:06 +0200 Subject: wireless: T1627: "capabilities ht max_amsdu" is not a multi node VyOS 1.2 confirmed it was a regular node - copy/paste error. --- interface-definitions/interfaces-wireless.xml.in | 1 - 1 file changed, 1 deletion(-) diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 6f0ec9e71..a85614cc2 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -110,7 +110,6 @@ (3839|7935) - -- cgit v1.2.3 From 857c4c10248a4f74b9b73d302ed9776cb818ff1b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 21:25:40 +0200 Subject: wireless: T2887: hostapd template cleanup --- data/templates/wifi/hostapd.conf.tmpl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index b00515d76..136942ca8 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -341,13 +341,10 @@ vht_oper_chwidth={{ capabilities.vht.channel_set_width }} {% set output.value = output.value + '[HTC-VHT]' %} {% endif %} {% if capabilities.vht.antenna_pattern_fixed is defined %} -{% set output.value = output.value + '[RX-ANTENNA-PATTERN]' %} -{% endif %} -{% if capabilities.vht.antenna_pattern_fixed is defined %} -{% set output.value = output.value + '[TX-ANTENNA-PATTERN]' %} +{% set output.value = output.value + '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]' %} {% endif %} {% if capabilities.vht.max_mpdu is defined %} -{% set output.value = output.value + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' %} +{% set output.value = output.value + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' %} {% endif %} {% if capabilities.vht.max_mpdu_exp is defined %} {% set output.value = output.value + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' %} -- cgit v1.2.3 From aca23987aaa42bebe8950cf1a36ea3f0e4ee47a9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 21:27:02 +0200 Subject: wireless: T2887: Jinja2 can not work on keys starting with a number ... an error would be presented: jinja2.exceptions.TemplateSyntaxError: expected token 'end of statement block', got 'mhz_incapable', thus we simply rename the key before rendering the template. --- src/conf_mode/interfaces-wireless.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 9861f72db..c6c843e7b 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -33,6 +33,7 @@ from vyos.configverify import verify_vrf from vyos.ifconfig import WiFiIf from vyos.template import render from vyos.util import call +from vyos.util import vyos_dict_search from vyos import ConfigError from vyos import airbag airbag.enable() @@ -213,6 +214,11 @@ def generate(wifi): mac.dialect = mac_unix_expanded wifi['mac'] = str(mac) + # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number + if '40mhz_incapable' in (vyos_dict_search('capabilities.ht', wifi) or []): + wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable'] + del wifi['capabilities']['ht']['40mhz_incapable'] + # render appropriate new config files depending on access-point or station mode if wifi['type'] == 'access-point': render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) -- cgit v1.2.3 From 0802505dcec58b4d50f058e20e0584929225546c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 21:38:44 +0200 Subject: wireless: T1627: "capabilities ht smps" is not a multi node --- interface-definitions/interfaces-wireless.xml.in | 1 - 1 file changed, 1 deletion(-) diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index a85614cc2..a0caf810f 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -149,7 +149,6 @@ (static|dynamic) - -- cgit v1.2.3 From de5cfd35ba14b75b58da653643472de93577b434 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 21:54:50 +0200 Subject: wireless: T2887: add smoketest for hostapd --- data/templates/wifi/hostapd.conf.tmpl | 4 -- smoketest/scripts/cli/test_interfaces_wireless.py | 87 +++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index 136942ca8..132c4ce40 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -51,10 +51,6 @@ ssid={{ ssid }} # (default: 0, i.e., not set) # Please note that some drivers do not use this value from hostapd and the # channel will need to be configured separately with iwconfig. -# -# If CONFIG_ACS build option is enabled, the channel can be selected -# automatically at run time by setting channel=acs_survey or channel=0, both of -# which will enable the ACS survey based algorithm. channel={{ channel }} {% endif %} diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index fae233244..691f633b7 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -15,11 +15,19 @@ # along with this program. If not, see . import os +import re import unittest from base_interfaces_test import BasicInterfaceTest from psutil import process_iter + from vyos.util import check_kmod +from vyos.util import read_file + +def get_config_value(interface, key): + tmp = read_file(f'/run/hostapd/{interface}.conf') + tmp = re.findall(r'\n?{}=+(.*)'.format(key), tmp) + return tmp[0] class WirelessInterfaceTest(BasicInterfaceTest.BaseTest): def setUp(self): @@ -53,6 +61,85 @@ class WirelessInterfaceTest(BasicInterfaceTest.BaseTest): else: self.assertTrue(False) + def test_hostapd_config(self): + """ Check if hostapd config is properly generated """ + + # Only set the hostapd (access-point) options + interface = 'wlan0' + phy = 'phy0' + ssid = 'ssid' + channel = '1' + + self.session.set(self._base_path + [interface, 'physical-device', phy]) + self.session.set(self._base_path + [interface, 'ssid', ssid]) + self.session.set(self._base_path + [interface, 'type', 'access-point']) + self.session.set(self._base_path + [interface, 'channel', channel]) + # auto-powersave is special + self.session.set(self._base_path + [interface, 'capabilities', 'ht', 'auto-powersave']) + + ht_opt = { + # VyOS CLI option hostapd - ht_capab setting + '40mhz-incapable' : '[40-INTOLERANT]', + 'delayed-block-ack' : '[DELAYED-BA]', + 'greenfield' : '[GF]', + 'ldpc' : '[LDPC]', + 'lsig-protection' : '[LSIG-TXOP-PROT]', + 'channel-set-width ht40+' : '[HT40+]', + 'stbc tx' : '[TX-STBC]', + 'stbc rx 123' : '[RX-STBC-123]', + 'max-amsdu 7935' : '[MAX-AMSDU-7935]', + 'smps static' : '[SMPS-STATIC]', + } + for key in ht_opt: + self.session.set(self._base_path + [interface, 'capabilities', 'ht'] + key.split()) + + vht_opt = { + # VyOS CLI option hostapd - ht_capab setting + 'stbc tx' : '[TX-STBC-2BY1]', + 'stbc rx 12' : '[RX-STBC-12]', + 'ldpc' : '[RXLDPC]', + 'tx-powersave' : '[VHT-TXOP-PS]', + 'vht-cf' : '[HTC-VHT]', + 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]', + 'max-mpdu 11454' : '[MAX-MPDU-11454]', + 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2][VHT160]', + 'link-adaptation both' : '[VHT-LINK-ADAPT3]', + 'short-gi 80' : '[SHORT-GI-80]', + 'short-gi 160' : '[SHORT-GI-160]', + } + for key in vht_opt: + self.session.set(self._base_path + [interface, 'capabilities', 'vht'] + key.split()) + + self.session.commit() + + # + # Validate Config + # + + # ssid + tmp = get_config_value(interface, 'ssid') + self.assertEqual(ssid, tmp) + + # channel + tmp = get_config_value(interface, 'channel') + self.assertEqual(channel, tmp) + + # auto-powersave is special + tmp = get_config_value(interface, 'uapsd_advertisement_enabled') + self.assertEqual('1', tmp) + + tmp = get_config_value(interface, 'ht_capab') + for key, value in ht_opt.items(): + self.assertIn(value, tmp) + + tmp = get_config_value(interface, 'vht_capab') + for key, value in vht_opt.items(): + self.assertIn(value, tmp) + + # Check for running process + self.assertIn('hostapd', (p.name() for p in process_iter())) + + if __name__ == '__main__': check_kmod('mac80211_hwsim') unittest.main() -- cgit v1.2.3 From 43359c2b824d53b5f82dea937f817139cbf77f8f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 16 Sep 2020 22:12:26 +0200 Subject: smoketest: T2886: typo, use assertIn over asserIn --- smoketest/scripts/system/test_kernel_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py index 861132127..8c96d96fb 100755 --- a/smoketest/scripts/system/test_kernel_options.py +++ b/smoketest/scripts/system/test_kernel_options.py @@ -30,7 +30,7 @@ class TestKernelModules(unittest.TestCase): for option in ['CONFIG_AUDIT', 'CONFIG_HAVE_ARCH_AUDITSYSCALL', 'CONFIG_AUDITSYSCALL', 'CONFIG_AUDIT_WATCH', 'CONFIG_AUDIT_TREE', 'CONFIG_AUDIT_ARCH']: - self.asserIn(f'{option}=y', config) + self.assertIn(f'{option}=y', config) if __name__ == '__main__': unittest.main() -- cgit v1.2.3 From 2fc157115cb358afba89f92a761c6617159624fc Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Wed, 16 Sep 2020 20:03:19 +0000 Subject: ethernet: T2891: Add ethernet ring-buffer CLI commands --- interface-definitions/interfaces-ethernet.xml.in | 31 ++++++++++++++++++++++++ python/vyos/ifconfig/ethernet.py | 21 ++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index e8f3f09f1..0aef0d332 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -268,6 +268,37 @@ auto + + + Shared buffer between the device driver and NIC + + + + + RX ring buffer + + 80-16384 + ring buffer size + + + + + + + + + TX ring buffer + + 80-16384 + ring buffer size + + + + + + + + #include #include diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 17c1bd64d..d4014a4dc 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -253,6 +253,22 @@ class EthernetIf(Interface): """ return self.set_interface('ufo', state) + def set_ring_buffer(self, b_type, b_size): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_ring_buffer('rx', '4096') + """ + cmd = '/sbin/ethtool -G {0} {1} {2}'.format(self.config['ifname'], b_type, b_size) + output, code = self._popen(cmd) + # ethtool error codes: + # 80 - value already setted + # 81 - does not possible to set value + if code and code != 80: + print('could not set {0} ring-buffer for {1}'.format(b_type, self.config['ifname'])) + return output + def update(self, config): """ General helper function which works on a dictionary retrived by @@ -298,6 +314,11 @@ class EthernetIf(Interface): duplex = config.get('duplex') self.set_speed_duplex(speed, duplex) + # Set interface ring buffer + if 'ring_buffer' in config: + for b_type in config['ring_buffer']: + self.set_ring_buffer(b_type, config['ring_buffer'][b_type]) + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as -- cgit v1.2.3 From de6dd336243b90a79023d7c84645cd75b5798f41 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 17 Sep 2020 21:29:52 +0200 Subject: smoketest: bond: T2894: vlan interfaces must not remove bond members A regression showed that when adding a vif to a bond the bond interface will loose all its member interfaces. This is - of course - super bad! --- smoketest/scripts/cli/test_interfaces_bonding.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index b165883b9..9ad8568ee 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -58,6 +58,14 @@ class BondingInterfaceTest(BasicInterfaceTest.BaseTest): slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split() self.assertListEqual(slaves, self._members) + def test_8021q_vlan(self): + """ Testcase for 802.1q VLAN interfaces """ + super().test_8021q_vlan() + + for interface in self._interfaces: + slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split() + self.assertListEqual(slaves, self._members) + def test_remove_member(self): """ T2515: when removing a bond member the interface must be admin-up again """ -- cgit v1.2.3 From 14c754f8bd6c96165d8ad3745c19c80a562910e1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 17 Sep 2020 21:34:56 +0200 Subject: smoketest: bond: T2894: extend comments --- smoketest/scripts/cli/test_interfaces_bonding.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 9ad8568ee..ac5e01e50 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -59,7 +59,8 @@ class BondingInterfaceTest(BasicInterfaceTest.BaseTest): self.assertListEqual(slaves, self._members) def test_8021q_vlan(self): - """ Testcase for 802.1q VLAN interfaces """ + """ Testcase for 802.1q VLAN interfaces created on top of a lacp / bond + interface. This is the testcase for T2894 """ super().test_8021q_vlan() for interface in self._interfaces: @@ -67,7 +68,9 @@ class BondingInterfaceTest(BasicInterfaceTest.BaseTest): self.assertListEqual(slaves, self._members) def test_remove_member(self): - """ T2515: when removing a bond member the interface must be admin-up again """ + """ T2515: when removing a bond member the previously enslaved/member + interface must be in its former admin-up/down state. Here we ensure that + it is admin-up as it was admin-up before. """ # configure member interfaces for interface in self._interfaces: -- cgit v1.2.3 From d1c9ee33f25e45cea0d01f9685f99c960ed4d7f8 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 19 Sep 2020 21:08:40 +0200 Subject: ifconfig: T2653: convert VLAN interfaces do discrete class Instead of using an Adapter pattern to make interfaces VLAN-aware, create a derived class named VLANIf to represent a VLAN. This change was necessary to eliminate mixed code in Interfaces class which was VLAN - free, but recently gained some VLAN specific code for set_admin_state(). In addition this "autoresolves" the issue in T2894 as a bond vlan interface will no longer change the lower interface. --- python/vyos/ifconfig/bond.py | 3 - python/vyos/ifconfig/control.py | 6 +- python/vyos/ifconfig/dummy.py | 2 - python/vyos/ifconfig/ethernet.py | 2 - python/vyos/ifconfig/geneve.py | 2 - python/vyos/ifconfig/input.py | 2 - python/vyos/ifconfig/interface.py | 190 +++++++++++++++++++++++++++--------- python/vyos/ifconfig/l2tpv3.py | 3 - python/vyos/ifconfig/loopback.py | 2 - python/vyos/ifconfig/macvlan.py | 4 - python/vyos/ifconfig/operational.py | 5 +- python/vyos/ifconfig/tunnel.py | 1 - python/vyos/ifconfig/vlan.py | 142 --------------------------- python/vyos/ifconfig/vrrp.py | 4 +- python/vyos/ifconfig/vti.py | 2 - python/vyos/ifconfig/vtun.py | 2 - python/vyos/ifconfig/vxlan.py | 1 - python/vyos/ifconfig/wireguard.py | 2 +- python/vyos/ifconfig/wireless.py | 5 - 19 files changed, 153 insertions(+), 227 deletions(-) delete mode 100644 python/vyos/ifconfig/vlan.py diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index c33cf30bf..9108fc180 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -16,15 +16,12 @@ import os from vyos.ifconfig.interface import Interface -from vyos.ifconfig.vlan import VLAN - from vyos.util import cmd from vyos.util import vyos_dict_search from vyos.validate import assert_list from vyos.validate import assert_positive @Interface.register -@VLAN.enable class BondIf(Interface): """ The Linux bonding driver provides a method for aggregating multiple network diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index a6fc8ac6c..43136f361 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -13,8 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - import os + from inspect import signature from inspect import _empty @@ -30,9 +30,9 @@ class Control(Section): _signature = {} def __init__(self, **kargs): - # some commands (such as operation comands - show interfaces, etc.) + # some commands (such as operation comands - show interfaces, etc.) # need to query the interface statistics. If the interface - # code is used and the debugging is enabled, the screen output + # code is used and the debugging is enabled, the screen output # will include both the command but also the debugging for that command # to prevent this, debugging can be explicitely disabled diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py index 43614cd1c..19ef9d304 100644 --- a/python/vyos/ifconfig/dummy.py +++ b/python/vyos/ifconfig/dummy.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - from vyos.ifconfig.interface import Interface - @Interface.register class DummyIf(Interface): """ diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index d4014a4dc..1d48941f9 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -17,13 +17,11 @@ import os import re from vyos.ifconfig.interface import Interface -from vyos.ifconfig.vlan import VLAN from vyos.validate import assert_list from vyos.util import run from vyos.util import vyos_dict_search @Interface.register -@VLAN.enable class EthernetIf(Interface): """ Abstraction of a Linux Ethernet Interface diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index dd0658668..0a13043cc 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -14,10 +14,8 @@ # License along with this library. If not, see . from copy import deepcopy - from vyos.ifconfig.interface import Interface - @Interface.register class GeneveIf(Interface): """ diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py index bfab36335..a6e566d87 100644 --- a/python/vyos/ifconfig/input.py +++ b/python/vyos/ifconfig/input.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - from vyos.ifconfig.interface import Interface - @Interface.register class InputIf(Interface): default = { diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index ffe69f61b..0774235b6 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -86,10 +86,6 @@ class Interface(Control): 'shellcmd': 'ip -json link show dev {ifname}', 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', }, - 'vlan_protocol': { - 'shellcmd': 'ip -json -details link show dev {ifname}', - 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), - }, } _command_set = { @@ -562,17 +558,6 @@ class Interface(Control): """ self.set_interface('alias', ifalias) - def get_vlan_protocol(self): - """ - Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0.10').get_vlan_protocol() - '802.1Q' - """ - return self.get_interface('vlan_protocol') - def get_admin_state(self): """ Get interface administrative state. Function will return 'up' or 'down' @@ -594,17 +579,6 @@ class Interface(Control): >>> Interface('eth0').get_admin_state() 'down' """ - # A VLAN interface can only be placed in admin up state when - # the lower interface is up, too - if self.get_vlan_protocol(): - lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] - with open(lower_interface, 'r') as f: - flags = f.read() - # If parent is not up - bail out as we can not bring up the VLAN. - # Flags are defined in kernel source include/uapi/linux/if.h - if not int(flags, 16) & 1: - return None - if state == 'up': self._admin_state_down_cnt -= 1 if self._admin_state_down_cnt < 1: @@ -1031,33 +1005,161 @@ class Interface(Control): self.add_to_bridge(bridge) # remove no longer required 802.1ad (Q-in-Q VLANs) + ifname = config['ifname'] for vif_s_id in config.get('vif_s_remove', {}): - self.del_vlan(vif_s_id) + vif_s_ifname = f'{ifname}.{vif_s_id}' + VLANIf(vif_s_ifname).remove() # create/update 802.1ad (Q-in-Q VLANs) - ifname = config['ifname'] - for vif_s_id, vif_s in config.get('vif_s', {}).items(): - tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) - s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) - vif_s['ifname'] = f'{ifname}.{vif_s_id}' - s_vlan.update(vif_s) + for vif_s_id, vif_s_config in config.get('vif_s', {}).items(): + tmp = deepcopy(VLANIf.get_config()) + tmp['ethertype'] = get_ethertype(vif_s_config.get('ethertype', '0x88A8')) + tmp['source_interface'] = ifname + tmp['vlan_id'] = vif_s_id + + vif_s_ifname = f'{ifname}.{vif_s_id}' + vif_s_config['ifname'] = vif_s_ifname + s_vlan = VLANIf(vif_s_ifname, **tmp) + s_vlan.update(vif_s_config) # remove no longer required client VLAN (vif-c) - for vif_c_id in vif_s.get('vif_c_remove', {}): - s_vlan.del_vlan(vif_c_id) + for vif_c_id in vif_s_config.get('vif_c_remove', {}): + vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}' + VLANIf(vif_c_ifname).remove() # create/update client VLAN (vif-c) interface - for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): - c_vlan = s_vlan.add_vlan(vif_c_id) - vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}' - c_vlan.update(vif_c) + for vif_c_id, vif_c_config in vif_s_config.get('vif_c', {}).items(): + tmp = deepcopy(VLANIf.get_config()) + tmp['source_interface'] = vif_s_ifname + tmp['vlan_id'] = vif_c_id + + vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}' + vif_c_config['ifname'] = vif_c_ifname + c_vlan = VLANIf(vif_c_ifname, **tmp) + c_vlan.update(vif_c_config) # remove no longer required 802.1q VLAN interfaces for vif_id in config.get('vif_remove', {}): - self.del_vlan(vif_id) + vif_ifname = f'{ifname}.{vif_id}' + VLANIf(vif_ifname).remove() # create/update 802.1q VLAN interfaces - for vif_id, vif in config.get('vif', {}).items(): - vlan = self.add_vlan(vif_id) - vif['ifname'] = f'{ifname}.{vif_id}' - vlan.update(vif) + for vif_id, vif_config in config.get('vif', {}).items(): + tmp = deepcopy(VLANIf.get_config()) + tmp['source_interface'] = ifname + tmp['vlan_id'] = vif_id + + vif_ifname = f'{ifname}.{vif_id}' + vif_config['ifname'] = vif_ifname + vlan = VLANIf(vif_ifname, **tmp) + vlan.update(vif_config) + + +class VLANIf(Interface): + """ Specific class which abstracts 802.1q and 802.1ad (Q-in-Q) VLAN interfaces """ + default = { + 'type': 'vlan', + 'source_interface': '', + 'vlan_id': '', + 'ethertype': '', + 'ingress_qos': '', + 'egress_qos': '', + } + + options = Interface.options + \ + ['source_interface', 'vlan_id', 'ethertype', 'ingress_qos', 'egress_qos'] + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # Do we have sub interfaces (VLANs)? As interfaces need to be deleted + # "in order" starting from Q-in-Q we delete them first. + for upper in glob(f'/sys/class/net/{self.ifname}/upper*'): + # an upper interface could be named: upper_bond0.1000.1100, thus + # we need top drop the upper_ prefix + vif_c = os.path.basename(upper) + vif_c = vif_c.replace('upper_', '') + VLANIf(vif_c).remove() + + super().remove() + + def _create(self): + # bail out early if interface already exists + if os.path.exists(f'/sys/class/net/{self.ifname}'): + return + + cmd = 'ip link add link {source_interface} name {ifname} type vlan id {vlan_id}' + if self.config['ethertype']: + cmd += ' proto {ethertype}' + if self.config['ingress_qos']: + cmd += ' ingress-qos-map {ingress_qos}' + if self.config['egress_qos']: + cmd += ' egress-qos-map {egress_qos}' + + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @staticmethod + def get_config(): + """ + MACsec interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACsecIf().get_config() + """ + config = deepcopy(__class__.default) + del config['type'] + return config + + def set_admin_state(self, state): + """ + Set interface administrative state to be 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_admin_state('down') + >>> Interface('eth0').get_admin_state() + 'down' + """ + # A VLAN interface can only be placed in admin up state when + # the lower interface is up, too + lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] + with open(lower_interface, 'r') as f: + flags = f.read() + # If parent is not up - bail out as we can not bring up the VLAN. + # Flags are defined in kernel source include/uapi/linux/if.h + if not int(flags, 16) & 1: + return None + + return super().set_admin_state(state) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 34147eb38..33740921e 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -13,12 +13,9 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - import os - from vyos.ifconfig.interface import Interface - @Interface.register class L2TPv3If(Interface): """ diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index c70e1773f..0e632d826 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - from vyos.ifconfig.interface import Interface - @Interface.register class LoopbackIf(Interface): """ diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index b068ce873..9c1d09c1c 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -14,13 +14,9 @@ # License along with this library. If not, see . from copy import deepcopy - from vyos.ifconfig.interface import Interface -from vyos.ifconfig.vlan import VLAN - @Interface.register -@VLAN.enable class MACVLANIf(Interface): """ Abstraction of a Linux MACvlan interface diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py index d585c1873..33e8614f0 100644 --- a/python/vyos/ifconfig/operational.py +++ b/python/vyos/ifconfig/operational.py @@ -14,20 +14,19 @@ # License along with this library. If not, see . import os + from time import time from datetime import datetime from functools import reduce - from tabulate import tabulate from vyos.ifconfig import Control - class Operational(Control): """ A class able to load Interface statistics """ - + cache_magic = 'XYZZYX' _stat_names = { diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 85c22b5b4..964ffe383 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -16,7 +16,6 @@ # https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/ # https://community.hetzner.com/tutorials/linux-setup-gre-tunnel - from copy import deepcopy from vyos.ifconfig.interface import Interface diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py deleted file mode 100644 index d68e8f6cd..000000000 --- a/python/vyos/ifconfig/vlan.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright 2019 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 . - - -import os -import re - -from vyos.ifconfig.interface import Interface - - -# This is an internal implementation class -class VLAN: - """ - This class handels the creation and removal of a VLAN interface. It serves - as base class for BondIf and EthernetIf. - """ - - _novlan_remove = lambda : None - - @classmethod - def enable (cls,adaptee): - adaptee._novlan_remove = adaptee.remove - adaptee.remove = cls.remove - adaptee.add_vlan = cls.add_vlan - adaptee.del_vlan = cls.del_vlan - adaptee.definition['vlan'] = True - return adaptee - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - ifname = self.config['ifname'] - - # Do we have sub interfaces (VLANs)? We apply a regex matching - # subinterfaces (indicated by a .) of a parent interface. - # - # As interfaces need to be deleted "in order" starting from Q-in-Q - # we delete them first. - vlan_ifs = [f for f in os.listdir(r'/sys/class/net') - if re.match(ifname + r'(?:\.\d+)(?:\.\d+)', f)] - - for vlan in vlan_ifs: - Interface(vlan).remove() - - # After deleting all Q-in-Q interfaces delete other VLAN interfaces - # which probably acted as parent to Q-in-Q or have been regular 802.1q - # interface. - vlan_ifs = [f for f in os.listdir(r'/sys/class/net') - if re.match(ifname + r'(?:\.\d+)', f)] - - for vlan in vlan_ifs: - # self.__class__ is already VLAN.enabled - self.__class__(vlan)._novlan_remove() - - # All subinterfaces are now removed, continue on the physical interface - self._novlan_remove() - - def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''): - """ - A virtual LAN (VLAN) is any broadcast domain that is partitioned and - isolated in a computer network at the data link layer (OSI layer 2). - Use this function to create a new VLAN interface on a given physical - interface. - - This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto - parameter is used to indicate VLAN type. - - A new object of type VLANIf is returned once the interface has been - created. - - @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN - interface - @param ingress_qos: Defines a mapping of VLAN header prio field to the - Linux internal packet priority on incoming frames. - @param ingress_qos: Defines a mapping of Linux internal packet priority - to VLAN header prio field but for outgoing frames. - - Example: - >>> from vyos.ifconfig import MACVLANIf - >>> i = MACVLANIf('eth0') - >>> i.add_vlan(10) - """ - vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) - if os.path.exists(f'/sys/class/net/{vlan_ifname}'): - return self.__class__(vlan_ifname) - - if ethertype: - self._ethertype = ethertype - ethertype = 'proto {}'.format(ethertype) - - # Optional ingress QOS mapping - opt_i = '' - if ingress_qos: - opt_i = 'ingress-qos-map ' + ingress_qos - # Optional egress QOS mapping - opt_e = '' - if egress_qos: - opt_e = 'egress-qos-map ' + egress_qos - - # create interface in the system - cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ - .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) - self._cmd(cmd) - - # return new object mapping to the newly created interface - # we can now work on this object for e.g. IP address setting - # or interface description and so on - return self.__class__(vlan_ifname) - - def del_vlan(self, vlan_id): - """ - Remove VLAN interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import MACVLANIf - >>> i = MACVLANIf('eth0.10') - >>> i.del_vlan() - """ - ifname = self.config['ifname'] - self.__class__(f'{ifname}.{vlan_id}')._novlan_remove() diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index 01a7cc7ab..d3e9d5df2 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -16,15 +16,13 @@ import os import json import signal + from time import time from time import sleep - from tabulate import tabulate -from vyos import airbag from vyos import util - class VRRPError(Exception): pass diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index 56ebe01d1..d0745898c 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - from vyos.ifconfig.interface import Interface - @Interface.register class VTIIf(Interface): default = { diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py index 60c178b9a..b25e32d63 100644 --- a/python/vyos/ifconfig/vtun.py +++ b/python/vyos/ifconfig/vtun.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - from vyos.ifconfig.interface import Interface - @Interface.register class VTunIf(Interface): default = { diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 18a500336..dba62b61a 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -18,7 +18,6 @@ from copy import deepcopy from vyos import ConfigError from vyos.ifconfig.interface import Interface - @Interface.register class VXLANIf(Interface): """ diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index fad4ef282..d8e89229d 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . - import os import time + from datetime import timedelta from hurry.filesize import size diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index a50346ffa..053566a1e 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -13,14 +13,9 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -import os - from vyos.ifconfig.interface import Interface -from vyos.ifconfig.vlan import VLAN - @Interface.register -@VLAN.enable class WiFiIf(Interface): """ Handle WIFI/WLAN interfaces. -- cgit v1.2.3 From 41a8c45c6b188646d6c6ac97bfaa2dffaffe1653 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 19 Sep 2020 21:57:08 +0200 Subject: smoketest: T2886: validate RADIUS configuration --- smoketest/scripts/cli/test_service_ssh.py | 2 +- smoketest/scripts/cli/test_system_login.py | 70 +++++++++++++++++++++++-- smoketest/scripts/system/test_kernel_options.py | 36 ------------- 3 files changed, 68 insertions(+), 40 deletions(-) delete mode 100755 smoketest/scripts/system/test_kernel_options.py diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 1038b8775..79850fe44 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -27,7 +27,7 @@ base_path = ['service', 'ssh'] def get_config_value(key): tmp = read_file(SSHD_CONF) - tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + tmp = re.findall(f'\n?{key}\s+(.*)', tmp) return tmp def is_service_running(): diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index 3c4b1fa28..48ae78ccf 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -16,11 +16,15 @@ import os import re +import platform import unittest +from platform import release as kernel_version from subprocess import Popen, PIPE -from vyos.configsession import ConfigSession, ConfigSessionError -import vyos.util as util + +from vyos.configsession import ConfigSession +from vyos.util import cmd +from vyos.util import read_file base_path = ['system', 'login'] users = ['vyos1', 'vyos2'] @@ -37,7 +41,7 @@ class TestSystemLogin(unittest.TestCase): self.session.commit() del self.session - def test_user(self): + def test_local_user(self): """ Check if user can be created and we can SSH to localhost """ self.session.set(['service', 'ssh', 'port', '22']) @@ -63,5 +67,65 @@ class TestSystemLogin(unittest.TestCase): # b'Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux\n' self.assertTrue(len(stdout) > 40) + def test_radius_kernel_features(self): + """ T2886: RADIUS requires some Kernel options to be present """ + kernel = platform.release() + kernel_config = read_file(f'/boot/config-{kernel}') + + # T2886 - RADIUS authentication - check for statically compiled + # options (=y) + for option in ['CONFIG_AUDIT', 'CONFIG_HAVE_ARCH_AUDITSYSCALL', + 'CONFIG_AUDITSYSCALL', 'CONFIG_AUDIT_WATCH', + 'CONFIG_AUDIT_TREE', 'CONFIG_AUDIT_ARCH']: + self.assertIn(f'{option}=y', kernel_config) + + def test_radius_config(self): + """ Verify generated RADIUS configuration files """ + + radius_key = 'VyOSsecretVyOS' + radius_server = '172.16.100.10' + radius_source = '127.0.0.1' + radius_port = '2000' + radius_timeout = '1' + + self.session.set(base_path + ['radius', 'server', radius_server, 'key', radius_key]) + self.session.set(base_path + ['radius', 'server', radius_server, 'port', radius_port]) + self.session.set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout]) + self.session.set(base_path + ['radius', 'source-address', radius_source]) + + self.session.commit() + + # this file must be read with higher permissions + pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf') + tmp = re.findall(r'\n?{}:{}\s+{}\s+{}\s+{}'.format(radius_server, + radius_port, radius_key, radius_timeout, + radius_source), pam_radius_auth_conf) + self.assertTrue(tmp) + + # required, static options + self.assertIn('priv-lvl 15', pam_radius_auth_conf) + self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf) + + # PAM + pam_common_account = read_file('/etc/pam.d/common-account') + self.assertIn('pam_radius_auth.so', pam_common_account) + + pam_common_auth = read_file('/etc/pam.d/common-auth') + self.assertIn('pam_radius_auth.so', pam_common_auth) + + pam_common_session = read_file('/etc/pam.d/common-session') + self.assertIn('pam_radius_auth.so', pam_common_session) + + pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive') + self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive) + + # NSS + nsswitch_conf = read_file('/etc/nsswitch.conf') + tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf) + self.assertTrue(tmp) + + tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf) + self.assertTrue(tmp) + if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py deleted file mode 100755 index 8c96d96fb..000000000 --- a/smoketest/scripts/system/test_kernel_options.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 -import platform -import unittest - -kernel = platform.release() -with open(f'/boot/config-{kernel}') as f: - config = f.read() - -class TestKernelModules(unittest.TestCase): - - def test_radius_auth_t2886(self): - # T2886 - RADIUS authentication - check for statically compiled - # options (=y) - for option in ['CONFIG_AUDIT', 'CONFIG_HAVE_ARCH_AUDITSYSCALL', - 'CONFIG_AUDITSYSCALL', 'CONFIG_AUDIT_WATCH', - 'CONFIG_AUDIT_TREE', 'CONFIG_AUDIT_ARCH']: - self.assertIn(f'{option}=y', config) - -if __name__ == '__main__': - unittest.main() -- cgit v1.2.3 From 31f6afc34ce95220963f0abcff3744d8a3291a80 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 19 Sep 2020 21:57:45 +0200 Subject: ifconfig: T2653: cleanup VLAN interface comments --- python/vyos/ifconfig/interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 0774235b6..be97b411b 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1077,8 +1077,7 @@ class VLANIf(Interface): Example: >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() + >>> VLANIf('eth0.10').remove """ # Do we have sub interfaces (VLANs)? As interfaces need to be deleted # "in order" starting from Q-in-Q we delete them first. @@ -1117,7 +1116,7 @@ class VLANIf(Interface): used by this class. Example: - >> dict = MACsecIf().get_config() + >> dict = VLANIf().get_config() """ config = deepcopy(__class__.default) del config['type'] @@ -1129,8 +1128,8 @@ class VLANIf(Interface): Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_admin_state('down') - >>> Interface('eth0').get_admin_state() + >>> Interface('eth0.10').set_admin_state('down') + >>> Interface('eth0.10').get_admin_state() 'down' """ # A VLAN interface can only be placed in admin up state when -- cgit v1.2.3 From 670536709b693f9b312a1f3da354d85eaf8a13b3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 19 Sep 2020 22:18:19 +0200 Subject: wifi: ifconfig: T2875: add_to_bridge() must be called after starting services hostapd/wpa_supplicant will control the admin state of an interface, thus we should re-add it to a bridge after we have launched those services. --- python/vyos/ifconfig/wireless.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 053566a1e..346577119 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -72,9 +72,22 @@ class WiFiIf(Interface): interface setup code and provide a single point of entry when workin on any interface. """ + # We can not call add_to_bridge() until wpa_supplicant is running, thus + # we will remove the key from the config dict and react to this specal + # case in thie derived class. + # re-add ourselves to any bridge we might have fallen out of + bridge_member = '' + if 'is_bridge_member' in config: + bridge_member = config['is_bridge_member'] + del config['is_bridge_member'] + # call base class first super().update(config) + # re-add ourselves to any bridge we might have fallen out of + if bridge_member: + self.add_to_bridge(bridge_member) + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as -- cgit v1.2.3 From e0797331774a02ca23e8363fbcfe5a49fb3ca2bd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 19 Sep 2020 22:32:18 +0200 Subject: dns: forwarding: T2900: restore proper Config() level in verify() Despite the fact that running verify on Config() is "bad" and "not as intended" the level of the configuration must match the keys that are checked by exits(). Re-set proper Config() level before querying the system nodes. --- src/conf_mode/dns_forwarding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 51631dc16..53bc37882 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -127,6 +127,7 @@ def verify(conf, dns): f'Error: No server configured for domain {domain}')) no_system_nameservers = False + conf.set_level([]) if dns['system'] and not ( conf.exists(['system', 'name-server']) or conf.exists(['system', 'name-servers-dhcp']) ): -- cgit v1.2.3