From c61f7557a6b595c5d8db7e6c303f275a3c0ab038 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 21 Mar 2021 18:27:36 +0100 Subject: isis: T3417: last byte of IS-IS network entity title must always be 0 (cherry picked from commit 19b16986515dcb58955e153025b24dc012faa574) --- src/conf_mode/protocols_isis.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index da91f3b11..eab580083 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -55,6 +55,11 @@ def verify(isis): if 'net' not in isis_config: raise ConfigError('ISIS net format iso is mandatory!') + # last byte in IS-IS area address must be 0 + tmp = isis_config['net'].split('.') + if int(tmp[-1]) != 0: + raise ConfigError('Last byte of IS-IS network entity title must always be 0!') + # If interface not set if 'interface' not in isis_config: raise ConfigError('ISIS interface is mandatory!') -- cgit v1.2.3 From 785af7cf6603a81adc432537bf97987f59d818a3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 22 Aug 2021 15:13:48 +0200 Subject: bridge: T3137: backport vlan features from 1.4 current --- interface-definitions/interfaces-bridge.xml.in | 15 ++- python/vyos/ifconfig/bridge.py | 130 ++++++++++++------------ smoketest/scripts/cli/test_interfaces_bridge.py | 53 +++++++++- src/conf_mode/interfaces-bridge.py | 91 +++++------------ src/validators/allowed-vlan | 19 ++++ 5 files changed, 172 insertions(+), 136 deletions(-) create mode 100755 src/validators/allowed-vlan (limited to 'src') diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 91ce00ba6..ddfc5ade4 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -86,6 +86,12 @@ #include #include #include + + + Enable VLAN aware bridge + + + Interval at which neighbor bridges are removed @@ -138,7 +144,7 @@ VLAN id range allowed on this interface (use '-' as delimiter) - ^([0-9]{1,4}-[0-9]{1,4})|([0-9]{1,4})$ + not a valid VLAN ID value or range @@ -172,6 +178,12 @@ 32 + + + Port is isolated (also known as Private-VLAN) + + + @@ -196,7 +208,6 @@ - #include #include diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index aadef0c09..27073b266 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors +# Copyright 2019-2021 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 @@ -22,6 +22,7 @@ from vyos.validate import assert_positive from vyos.util import cmd from vyos.util import dict_search from vyos.configdict import get_vlan_ids +from vyos.configdict import list_diff @Interface.register class BridgeIf(Interface): @@ -33,7 +34,6 @@ class BridgeIf(Interface): The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. """ - iftype = 'bridge' definition = { **Interface.definition, @@ -267,21 +267,37 @@ class BridgeIf(Interface): for member in (tmp or []): if member in interfaces(): self.del_port(member) - vlan_filter = 0 - vlan_del = set() - vlan_add = set() + # enable/disable Vlan Filter + vlan_filter = '1' if 'enable_vlan' in config else '0' + self.set_vlan_filter(vlan_filter) ifname = config['ifname'] + if int(vlan_filter): + add_vlan = [] + cur_vlan_ids = get_vlan_ids(ifname) + + tmp = dict_search('vif', config) + if tmp: + for vif, vif_config in tmp.items(): + add_vlan.append(vif) + + # Remove redundant VLANs from the system + for vlan in list_diff(cur_vlan_ids, add_vlan): + cmd = f'bridge vlan del dev {ifname} vid {vlan} self' + self._cmd(cmd) + + for vlan in add_vlan: + cmd = f'bridge vlan add dev {ifname} vid {vlan} self' + self._cmd(cmd) + + # VLAN of bridge parent interface is always 1 + # VLAN 1 is the default VLAN for all unlabeled packets + cmd = f'bridge vlan add dev {ifname} vid 1 pvid untagged self' + self._cmd(cmd) + tmp = dict_search('member.interface', config) if tmp: - if self.get_vlan_filter(): - bridge_vlan_ids = get_vlan_ids(ifname) - # Delete VLAN ID for the bridge - if 1 in bridge_vlan_ids: - bridge_vlan_ids.remove(1) - for vlan in bridge_vlan_ids: - vlan_del.add(str(vlan)) for interface, interface_config in tmp.items(): # if interface does yet not exist bail out early and @@ -296,9 +312,15 @@ class BridgeIf(Interface): # not have any addresses configured by CLI so just flush any # remaining ones lower.flush_addrs() + # enslave interface port to bridge self.add_port(interface) + # always set private-vlan/port isolation + tmp = dict_search('isolated', interface_config) + value = 'on' if (tmp != None) else 'off' + lower.set_port_isolation(value) + # set bridge port path cost if 'cost' in interface_config: value = interface_config.get('cost') @@ -309,61 +331,39 @@ class BridgeIf(Interface): value = interface_config.get('priority') lower.set_path_priority(value) - tmp = dict_search('native_vlan_removed', interface_config) - - for vlan_id in (tmp or []): - cmd = f'bridge vlan del dev {interface} vid {vlan_id}' - self._cmd(cmd) - cmd = f'bridge vlan add dev {interface} vid 1 pvid untagged master' - self._cmd(cmd) - vlan_del.add(vlan_id) - vlan_add.add(1) - - tmp = dict_search('allowed_vlan_removed', interface_config) - - for vlan_id in (tmp or []): - cmd = f'bridge vlan del dev {interface} vid {vlan_id}' - self._cmd(cmd) - vlan_del.add(vlan_id) - - if 'native_vlan' in interface_config: - vlan_filter = 1 - cmd = f'bridge vlan del dev {interface} vid 1' - self._cmd(cmd) - vlan_id = interface_config['native_vlan'] - if int(vlan_id) != 1: - if 1 in vlan_add: - vlan_add.remove(1) - vlan_del.add(1) - cmd = f'bridge vlan add dev {interface} vid {vlan_id} pvid untagged master' - self._cmd(cmd) - vlan_add.add(vlan_id) - if vlan_id in vlan_del: - vlan_del.remove(vlan_id) - - if 'allowed_vlan' in interface_config: - vlan_filter = 1 - if 'native_vlan' not in interface_config: - cmd = f'bridge vlan del dev {interface} vid 1' + if int(vlan_filter): + add_vlan = [] + native_vlan_id = None + allowed_vlan_ids= [] + cur_vlan_ids = get_vlan_ids(interface) + + if 'native_vlan' in interface_config: + vlan_id = interface_config['native_vlan'] + add_vlan.append(vlan_id) + native_vlan_id = vlan_id + + if 'allowed_vlan' in interface_config: + for vlan in interface_config['allowed_vlan']: + vlan_range = vlan.split('-') + if len(vlan_range) == 2: + for vlan_add in range(int(vlan_range[0]),int(vlan_range[1]) + 1): + add_vlan.append(str(vlan_add)) + allowed_vlan_ids.append(str(vlan_add)) + else: + add_vlan.append(vlan) + allowed_vlan_ids.append(vlan) + + # Remove redundant VLANs from the system + for vlan in list_diff(cur_vlan_ids, add_vlan): + cmd = f'bridge vlan del dev {interface} vid {vlan} master' self._cmd(cmd) - vlan_del.add(1) - for vlan in interface_config['allowed_vlan']: + + for vlan in allowed_vlan_ids: cmd = f'bridge vlan add dev {interface} vid {vlan} master' self._cmd(cmd) - vlan_add.add(vlan) - if vlan in vlan_del: - vlan_del.remove(vlan) - - for vlan in vlan_del: - cmd = f'bridge vlan del dev {ifname} vid {vlan} self' - self._cmd(cmd) - - for vlan in vlan_add: - cmd = f'bridge vlan add dev {ifname} vid {vlan} self' - self._cmd(cmd) - - # enable/disable Vlan Filter - self.set_vlan_filter(vlan_filter) - + # Setting native VLAN to system + if native_vlan_id: + cmd = f'bridge vlan add dev {interface} vid {native_vlan_id} pvid untagged master' + self._cmd(cmd) super().update(config) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 4014c1a4c..2152dba72 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -63,6 +63,32 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): super().tearDown() + def test_isolated_interfaces(self): + # Add member interfaces to bridge and set STP cost/priority + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['stp']) + + # assign members to bridge interface + for member in self._members: + base_member = base + ['member', 'interface', member] + self.cli_set(base_member + ['isolated']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + tmp = get_interface_config(interface) + # STP must be enabled as configured above + self.assertEqual(1, tmp['linkinfo']['info_data']['stp_state']) + + # validate member interface configuration + for member in self._members: + tmp = get_interface_config(member) + # Isolated must be enabled as configured above + self.assertTrue(tmp['linkinfo']['info_slave_data']['isolated']) + + def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority for interface in self._interfaces: @@ -97,12 +123,34 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): cost += 1 priority += 1 + + def test_vif_8021q_interfaces(self): + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['enable-vlan']) + super().test_vif_8021q_interfaces() + + def test_vif_8021q_lower_up_down(self): + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['enable-vlan']) + super().test_vif_8021q_lower_up_down() + + def test_vif_8021q_mtu_limits(self): + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['enable-vlan']) + super().test_vif_8021q_mtu_limits() + def test_bridge_vlan_filter(self): + vif_vlan = 2 # Add member interface to bridge and set VLAN filter for interface in self._interfaces: base = self._base_path + [interface] - self.cli_set(base + ['vif', '1', 'address', '192.0.2.1/24']) - self.cli_set(base + ['vif', '2', 'address', '192.0.3.1/24']) + self.cli_set(base + ['enable-vlan']) + self.cli_set(base + ['address', '192.0.2.1/24']) + self.cli_set(base + ['vif', str(vif_vlan), 'address', '192.0.3.1/24']) + self.cli_set(base + ['vif', str(vif_vlan), 'mtu', self._mtu]) vlan_id = 101 allowed_vlan = 2 @@ -174,6 +222,7 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: self.cli_delete(self._base_path + [interface, 'member']) + def test_bridge_vlan_members(self): # T2945: ensure that VIFs are not dropped from bridge vifs = ['300', '400'] diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 5b0046a72..4d3ebc587 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -18,7 +18,6 @@ import os from sys import exit from netifaces import interfaces -import re from vyos.config import Config from vyos.configdict import get_interface_dict @@ -41,26 +40,6 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -def helper_check_removed_vlan(conf,bridge,key,key_mangling): - key_update = re.sub(key_mangling[0], key_mangling[1], key) - if dict_search('member.interface', bridge): - for interface in bridge['member']['interface']: - tmp = leaf_node_changed(conf, ['member', 'interface',interface,key]) - if tmp: - if 'member' in bridge: - if 'interface' in bridge['member']: - if interface in bridge['member']['interface']: - bridge['member']['interface'][interface].update({f'{key_update}_removed': tmp }) - else: - bridge['member']['interface'].update({interface: {f'{key_update}_removed': tmp }}) - else: - bridge['member'].update({ 'interface': {interface: {f'{key_update}_removed': tmp }}}) - else: - bridge.update({'member': { 'interface': {interface: {f'{key_update}_removed': tmp }}}}) - - return bridge - - def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -80,12 +59,6 @@ def get_config(config=None): bridge['member'].update({'interface_remove': tmp }) else: bridge.update({'member': {'interface_remove': tmp }}) - - - # determine which members vlan have been removed - - bridge = helper_check_removed_vlan(conf,bridge,'native-vlan',('-', '_')) - bridge = helper_check_removed_vlan(conf,bridge,'allowed-vlan',('-', '_')) if dict_search('member.interface', bridge): # XXX: T2665: we need a copy of the dict keys for iteration, else we will get: @@ -99,7 +72,6 @@ def get_config(config=None): # the default dictionary is not properly paged into the dict (see T2665) # thus we will ammend it ourself default_member_values = defaults(base + ['member', 'interface']) - vlan_aware = False for interface,interface_config in bridge['member']['interface'].items(): bridge['member']['interface'][interface] = dict_merge( default_member_values, bridge['member']['interface'][interface]) @@ -120,19 +92,11 @@ def get_config(config=None): # Bridge members must not have an assigned address tmp = has_address_configured(conf, interface) if tmp: bridge['member']['interface'][interface].update({'has_address' : ''}) - + # VLAN-aware bridge members must not have VLAN interface configuration - if 'native_vlan' in interface_config: - vlan_aware = True - - if 'allowed_vlan' in interface_config: - vlan_aware = True - - - if vlan_aware: - tmp = has_vlan_subinterface_configured(conf,interface) - if tmp: - if tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''}) + tmp = has_vlan_subinterface_configured(conf,interface) + if 'enable_vlan' in bridge and tmp: + bridge['member']['interface'][interface].update({'has_vlan' : ''}) return bridge @@ -142,8 +106,8 @@ def verify(bridge): verify_dhcpv6(bridge) verify_vrf(bridge) - - vlan_aware = False + + ifname = bridge['ifname'] if dict_search('member.interface', bridge): for interface, interface_config in bridge['member']['interface'].items(): @@ -166,31 +130,24 @@ def verify(bridge): if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') - - if 'has_vlan' in interface_config: - raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') - - # VLAN-aware bridge members must not have VLAN interface configuration - if 'native_vlan' in interface_config: - vlan_aware = True - - if 'allowed_vlan' in interface_config: - vlan_aware = True - - if vlan_aware and 'wlan' in interface: - raise ConfigError(error_msg + 'VLAN aware cannot be set!') - - if 'allowed_vlan' in interface_config: - for vlan in interface_config['allowed_vlan']: - if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan): - vlan_range = vlan.split('-') - if int(vlan_range[0]) <1 and int(vlan_range[0])>4094: - raise ConfigError('VLAN ID must be between 1 and 4094') - if int(vlan_range[1]) <1 and int(vlan_range[1])>4094: - raise ConfigError('VLAN ID must be between 1 and 4094') - else: - if int(vlan) <1 and int(vlan)>4094: - raise ConfigError('VLAN ID must be between 1 and 4094') + + if 'enable_vlan' in bridge: + if 'has_vlan' in interface_config: + raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + + if 'wlan' in interface: + raise ConfigError(error_msg + 'VLAN aware cannot be set!') + else: + for option in ['allowed_vlan', 'native_vlan']: + if option in interface_config: + raise ConfigError('Can not use VLAN options on non VLAN aware bridge') + + if 'enable_vlan' in bridge: + if dict_search('vif.1', bridge): + raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface') + else: + if dict_search('vif', bridge): + raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"') return None diff --git a/src/validators/allowed-vlan b/src/validators/allowed-vlan new file mode 100755 index 000000000..11389390b --- /dev/null +++ b/src/validators/allowed-vlan @@ -0,0 +1,19 @@ +#! /usr/bin/python3 + +import sys +import re + +if __name__ == '__main__': + if len(sys.argv)>1: + allowed_vlan = sys.argv[1] + if re.search('[0-9]{1,4}-[0-9]{1,4}', allowed_vlan): + for tmp in allowed_vlan.split('-'): + if int(tmp) not in range(1, 4095): + sys.exit(1) + else: + if int(allowed_vlan) not in range(1, 4095): + sys.exit(1) + else: + sys.exit(2) + + sys.exit(0) -- cgit v1.2.3 From b4b2c91127289c7b62afb24304054d57357a48c5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 25 Aug 2021 14:55:10 +0200 Subject: op-mode: frr: T1514: add possibility to restart isis daemon --- op-mode-definitions/restart-frr.xml.in | 6 ++++++ src/op_mode/restart_frr.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/op-mode-definitions/restart-frr.xml.in b/op-mode-definitions/restart-frr.xml.in index a5ba5b11f..475bd1ee8 100644 --- a/op-mode-definitions/restart-frr.xml.in +++ b/op-mode-definitions/restart-frr.xml.in @@ -20,6 +20,12 @@ sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bgpd + + + Restart Intermediate System to Intermediate System (IS-IS) routing daemon + + sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon isisd + Restart Open Shortest Path First (OSPF) routing daemon diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index d1b66b33f..0b2322478 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -155,7 +155,7 @@ def _check_args_daemon(daemons): # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() -- cgit v1.2.3 From 2d5199e75d02c29ce924f932de5c9b012d2b11fd Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 24 Aug 2021 09:17:58 -0500 Subject: T3773: delete the original "show system integrity" command (cherry picked from commit 059307f924c604eb2bdeab19a2db8ce6d8e09f90) --- op-mode-definitions/show-system.xml.in | 6 --- src/op_mode/show_system_integrity.py | 70 ---------------------------------- 2 files changed, 76 deletions(-) delete mode 100755 src/op_mode/show_system_integrity.py (limited to 'src') diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 5e9bf719e..18a28868d 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -55,12 +55,6 @@ ${vyos_op_scripts_dir}/show_cpu.py - - - Checks overall system integrity - - sudo ${vyos_op_scripts_dir}/show_system_integrity.py - Show messages in kernel ring buffer diff --git a/src/op_mode/show_system_integrity.py b/src/op_mode/show_system_integrity.py deleted file mode 100755 index c34d41e80..000000000 --- a/src/op_mode/show_system_integrity.py +++ /dev/null @@ -1,70 +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 os -import re -import json -from datetime import datetime, timedelta - -version_file = r'/usr/share/vyos/version.json' - - -def _get_sys_build_version(): - if not os.path.exists(version_file): - return None - buf = open(version_file, 'r').read() - j = json.loads(buf) - if not 'built_on' in j: - return None - return datetime.strptime(j['built_on'], '%a %d %b %Y %H:%M %Z') - - -def _check_pkgs(build_stamp): - pkg_diffs = { - 'buildtime': str(build_stamp), - 'pkg': {} - } - - pkg_info = os.listdir('/var/lib/dpkg/info/') - for file in pkg_info: - if re.search('\.list$', file): - fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime - dt_str = (datetime.utcfromtimestamp( - fts).strftime('%Y-%m-%d %H:%M:%S')) - fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') - if fdt > build_stamp: - pkg_diffs['pkg'].update( - {str(re.sub('\.list', '', file)): str(fdt)}) - - if len(pkg_diffs['pkg']) != 0: - return pkg_diffs - else: - return None - - -if __name__ == '__main__': - built_date = _get_sys_build_version() - if not built_date: - sys.exit(1) - pkgs = _check_pkgs(built_date) - if pkgs: - print ( - "The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime']) - for k, v in pkgs['pkg'].items(): - print ("installed: " + v + '\t' + k) -- cgit v1.2.3 From 8c7741cbdb1a38561bb0c82e5c8aff2109224c2e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 25 Aug 2021 21:12:58 +0200 Subject: frr: T3217: Abbility to save routing configs (cherry picked from commit d9d923ea4e0bbe0cc154dc2fbdd626585b5d7449) --- python/vyos/frr.py | 54 +++++++++++++++++++++++++++++++++++++++--- src/conf_mode/protocols_rip.py | 3 ++- 2 files changed, 53 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/python/vyos/frr.py b/python/vyos/frr.py index 3bab64301..df6849472 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -68,15 +68,27 @@ Apply the new configuration: import tempfile import re from vyos import util +from vyos.util import chown +from vyos.util import cmd import logging +from logging.handlers import SysLogHandler +import os LOG = logging.getLogger(__name__) +DEBUG = os.path.exists('/tmp/vyos.frr.debug') +if DEBUG: + LOG.setLevel(logging.DEBUG) + ch = SysLogHandler(address='/dev/log') + ch2 = logging.StreamHandler() + LOG.addHandler(ch) + LOG.addHandler(ch2) _frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd'] path_vtysh = '/usr/bin/vtysh' path_frr_reload = '/usr/lib/frr/frr-reload.py' +path_config = '/run/frr' class FrrError(Exception): @@ -175,21 +187,42 @@ def reload_configuration(config, daemon=None): f.write(config) f.flush() + LOG.debug(f'reload_configuration: Reloading config using temporary file: {f.name}') cmd = f'{path_frr_reload} --reload' if daemon: cmd += f' --daemon {daemon}' + + if DEBUG: + cmd += f' --debug --stdout' + cmd += f' {f.name}' + LOG.debug(f'reload_configuration: Executing command against frr-reload: "{cmd}"') output, code = util.popen(cmd, stderr=util.STDOUT) f.close() + for i, e in enumerate(output.split('\n')): + LOG.debug(f'frr-reload output: {i:3} {e}') if code == 1: - raise CommitError(f'Configuration FRR failed while commiting code: {repr(output)}') + raise CommitError('FRR configuration failed while running commit. Please ' \ + 'enable debugging to examine logs.\n\n\n' \ + 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \ + 'and "sudo systemctl stop vyos-configd"') elif code: raise OSError(code, output) return output +def save_configuration(): + """Save FRR configuration to /run/frr/config/frr.conf + It save configuration on each commit. T3217 + """ + + cmd(f'{path_vtysh} -n -w') + + return + + def execute(command): """ Run commands inside vtysh command: str containing commands to execute inside a vtysh session @@ -382,6 +415,11 @@ class FRRConfig: raise ValueError( 'The config element needs to be a string or list type object') + if config: + LOG.debug(f'__init__: frr library initiated with initial config') + for i, e in enumerate(self.config): + LOG.debug(f'__init__: initial {i:3} {e}') + def load_configuration(self, daemon=None): '''Load the running configuration from FRR into the config object daemon: str with name of the FRR Daemon to load configuration from or @@ -390,9 +428,16 @@ class FRRConfig: Using this overwrites the current loaded config objects and replaces the original loaded config ''' self.imported_config = get_configuration(daemon=daemon) - LOG.debug(f'load_configuration: Configuration loaded from FRR: {self.imported_config}') + if daemon: + LOG.debug(f'load_configuration: Configuration loaded from FRR daemon {daemon}') + else: + LOG.debug(f'load_configuration: Configuration loaded from FRR integrated config') + self.original_config = self.imported_config.split('\n') self.config = self.original_config.copy() + + for i, e in enumerate(self.imported_config.split('\n')): + LOG.debug(f'load_configuration: loaded {i:3} {e}') return def test_configuration(self): @@ -408,6 +453,8 @@ class FRRConfig: None to use the consolidated config ''' LOG.debug('commit_configuration: Commiting configuration') + for i, e in enumerate(self.config): + LOG.debug(f'commit_configuration: new_config {i:3} {e}') reload_configuration('\n'.join(self.config), daemon=daemon) def modify_section(self, start_pattern, replacement=[], stop_pattern=r'\S+', remove_stop_mark=False, count=0): @@ -459,7 +506,8 @@ class FRRConfig: start = _find_first_element(self.config, before_pattern) if start < 0: return False - + for i, e in enumerate(addition, start=start): + LOG.debug(f'add_before: add {i:3} {e}') self.config[start:start] = addition return True diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 8ddd705f2..f36abbf90 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -125,7 +125,7 @@ def get_config(config=None): conf.set_level(base) - # Get distribute list interface + # Get distribute list interface for dist_iface in conf.list_nodes('distribute-list interface'): # Set level 'distribute-list interface ethX' conf.set_level(base + ['distribute-list', 'interface', dist_iface]) @@ -301,6 +301,7 @@ def apply(rip): if os.path.exists(config_file): call(f'vtysh -d ripd -f {config_file}') + call('sudo vtysh --writeconfig --noerror') os.remove(config_file) else: print("File {0} not found".format(config_file)) -- cgit v1.2.3 From e4db4a23ff94a77bb62a40580018d4c884a13e12 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 25 Aug 2021 21:20:30 +0200 Subject: isis: T3779: backport entire 1.4 (current) featureset As IS-IS is a new feature and the CLI configuration changed from 1.3 -> 1.4 (required by T3417) it makes sense to synchronize the CLI configuration for both versions. This means backporting the CLI from 1.4 -> 1.3 to not confuse the userbase already with a brand new feature. As 1.3.0-epa1 is on the way and should not contain any CLI changes afterwards, this is the perfect time. --- data/templates/frr/isisd.frr.tmpl | 72 +- data/templates/frr/route-map.frr.tmpl | 5 + interface-definitions/include/bfd.xml.i | 8 + .../include/isis-redistribute-ipv4.xml.i | 56 -- .../include/isis/default-information-level.xml.i | 32 + interface-definitions/include/isis/metric.xml.i | 14 + interface-definitions/include/isis/passive.xml.i | 8 + .../include/isis/protocol-common-config.xml.i | 769 ++++++++++++++++++++ .../include/isis/redistribute-level-1-2.xml.i | 20 + interface-definitions/include/route-map.xml.i | 18 + interface-definitions/protocols-isis.xml.in | 772 +-------------------- smoketest/configs/isis-small | 105 +++ smoketest/scripts/cli/test_protocols_isis.py | 170 +++++ src/conf_mode/protocols_isis.py | 263 ++++--- src/migration-scripts/isis/0-to-1 | 59 ++ 15 files changed, 1412 insertions(+), 959 deletions(-) create mode 100644 data/templates/frr/route-map.frr.tmpl create mode 100644 interface-definitions/include/bfd.xml.i delete mode 100644 interface-definitions/include/isis-redistribute-ipv4.xml.i create mode 100644 interface-definitions/include/isis/default-information-level.xml.i create mode 100644 interface-definitions/include/isis/metric.xml.i create mode 100644 interface-definitions/include/isis/passive.xml.i create mode 100644 interface-definitions/include/isis/protocol-common-config.xml.i create mode 100644 interface-definitions/include/isis/redistribute-level-1-2.xml.i create mode 100644 interface-definitions/include/route-map.xml.i create mode 100644 smoketest/configs/isis-small create mode 100755 smoketest/scripts/cli/test_protocols_isis.py create mode 100755 src/migration-scripts/isis/0-to-1 (limited to 'src') diff --git a/data/templates/frr/isisd.frr.tmpl b/data/templates/frr/isisd.frr.tmpl index 8a813d9cb..6cfa076d0 100644 --- a/data/templates/frr/isisd.frr.tmpl +++ b/data/templates/frr/isisd.frr.tmpl @@ -1,5 +1,5 @@ ! -router isis {{ process }} +router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} net {{ net }} {% if dynamic_hostname is defined %} hostname dynamic @@ -13,8 +13,15 @@ router isis {{ process }} {% if set_overload_bit is defined %} set-overload-bit {% endif %} -{% if domain_password is defined and domain_password.plaintext_password is defined and domain_password.plaintext_password is not none %} +{% if domain_password is defined and domain_password is not none %} +{% if domain_password.md5 is defined and domain_password.md5 is not none %} + domain-password md5 {{ domain_password.plaintext_password }} +{% elif domain_password.plaintext_password is defined and domain_password.plaintext_password is not none %} domain-password clear {{ domain_password.plaintext_password }} +{% endif %} +{% endif %} +{% if log_adjacency_changes is defined %} + log-adjacency-changes {% endif %} {% if lsp_gen_interval is defined and lsp_gen_interval is not none %} lsp-gen-interval {{ lsp_gen_interval }} @@ -95,47 +102,61 @@ router isis {{ process }} {% if spf_delay_ietf is defined and spf_delay_ietf.init_delay is defined and spf_delay_ietf.init_delay is not none %} spf-delay-ietf init-delay {{ spf_delay_ietf.init_delay }} {% endif %} -{% if area_password is defined and area_password.md5 is defined and area_password.md5 is not none %} +{% if area_password is defined and area_password is not none %} +{% if area_password.md5 is defined and area_password.md5 is not none %} area-password md5 {{ area_password.md5 }} -{% elif area_password is defined and area_password.plaintext_password is defined and area_password.plaintext_password is not none %} +{% elif area_password.plaintext_password is defined and area_password.plaintext_password is not none %} area-password clear {{ area_password.plaintext_password }} +{% endif %} {% endif %} {% if default_information is defined and default_information.originate is defined and default_information.originate is not none %} -{% for level in default_information.originate.ipv4 if default_information.originate.ipv4 is defined %} - default-information originate ipv4 {{ level | replace('_', '-') }} -{% endfor %} -{% for level in default_information.originate.ipv6 if default_information.originate.ipv6 is defined %} - default-information originate ipv6 {{ level | replace('_', '-') }} always +{% for afi, afi_config in default_information.originate.items() %} +{% for level, level_config in afi_config.items() %} + default-information originate {{ afi }} {{ level | replace('_', '-') }} {{ 'always' if level_config.always is defined }} {{ 'route-map ' ~ level_config.route_map if level_config.route_map is defined }} {{ 'metric ' ~ level_config.metric if level_config.metric is defined }} +{% endfor %} {% endfor %} {% endif %} -{% if redistribute is defined and redistribute.ipv4 is defined and redistribute.ipv4 is not none %} -{% for protocol in redistribute.ipv4 %} -{% for level, level_config in redistribute.ipv4[protocol].items() %} -{% if level_config.metric is defined and level_config.metric is not none %} +{% if redistribute is defined %} +{% if redistribute.ipv4 is defined and redistribute.ipv4 is not none %} +{% for protocol, protocol_options in redistribute.ipv4.items() %} +{% for level, level_config in protocol_options.items() %} +{% if level_config.metric is defined and level_config.metric is not none %} redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} metric {{ level_config.metric }} -{% elif level_config.route_map is defined and level_config.route_map is not none %} +{% elif level_config.route_map is defined and level_config.route_map is not none %} redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} route-map {{ level_config.route_map }} -{% else %} +{% else %} redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} -{% endif %} +{% endif %} +{% endfor %} {% endfor %} -{% endfor %} +{% endif %} +{% if redistribute.ipv6 is defined and redistribute.ipv6 is not none %} +{% for protocol, protocol_options in redistribute.ipv6.items() %} +{% for level, level_config in protocol_options.items() %} +{% if level_config.metric is defined and level_config.metric is not none %} + redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} metric {{ level_config.metric }} +{% elif level_config.route_map is defined and level_config.route_map is not none %} + redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} route-map {{ level_config.route_map }} +{% else %} + redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} +{% endif %} +{% endfor %} +{% endfor %} +{% endif %} {% endif %} {% if level is defined and level is not none %} -{% if level == 'level-1' %} - is-type level-1 -{% elif level == 'level-2' %} +{% if level == 'level-2' %} is-type level-2-only -{% elif level == 'level-1-2' %} - is-type level-1-2 +{% else %} + is-type {{ level }} {% endif %} {% endif %} ! {% if interface is defined and interface is not none %} {% for iface, iface_config in interface.items() %} -interface {{ iface }} - ip router isis {{ process }} - ipv6 router isis {{ process }} +interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} + ip router isis VyOS + ipv6 router isis VyOS {% if iface_config.bfd is defined %} isis bfd {% endif %} @@ -174,3 +195,4 @@ interface {{ iface }} {% endif %} {% endfor %} {% endif %} +! \ No newline at end of file diff --git a/data/templates/frr/route-map.frr.tmpl b/data/templates/frr/route-map.frr.tmpl new file mode 100644 index 000000000..6b33cc126 --- /dev/null +++ b/data/templates/frr/route-map.frr.tmpl @@ -0,0 +1,5 @@ +! +{% if route_map is defined and route_map is not none %} +ip protocol {{ protocol }} route-map {{ route_map }} +{% endif %} +! diff --git a/interface-definitions/include/bfd.xml.i b/interface-definitions/include/bfd.xml.i new file mode 100644 index 000000000..2bc3664e1 --- /dev/null +++ b/interface-definitions/include/bfd.xml.i @@ -0,0 +1,8 @@ + + + + Enable Bidirectional Forwarding Detection (BFD) + + + + diff --git a/interface-definitions/include/isis-redistribute-ipv4.xml.i b/interface-definitions/include/isis-redistribute-ipv4.xml.i deleted file mode 100644 index 774086a81..000000000 --- a/interface-definitions/include/isis-redistribute-ipv4.xml.i +++ /dev/null @@ -1,56 +0,0 @@ - - - - Redistribute into level-1 - - - - - Metric for redistributed routes - - u32:0-16777215 - ISIS default metric - - - - - - - - - Route map reference - - policy route-map - - - - - - - - Redistribute into level-2 - - - - - Metric for redistributed routes - - u32:0-16777215 - ISIS default metric - - - - - - - - - Route map reference - - policy route-map - - - - - - diff --git a/interface-definitions/include/isis/default-information-level.xml.i b/interface-definitions/include/isis/default-information-level.xml.i new file mode 100644 index 000000000..5ade72a4b --- /dev/null +++ b/interface-definitions/include/isis/default-information-level.xml.i @@ -0,0 +1,32 @@ + + + + Distribute default route into level-1 + + + + + Always advertise default route + + + + #include + #include + + + + + Distribute default route into level-2 + + + + + Always advertise default route + + + + #include + #include + + + diff --git a/interface-definitions/include/isis/metric.xml.i b/interface-definitions/include/isis/metric.xml.i new file mode 100644 index 000000000..30e2cdc10 --- /dev/null +++ b/interface-definitions/include/isis/metric.xml.i @@ -0,0 +1,14 @@ + + + + Set default metric for circuit + + u32:0-16777215 + Default metric value + + + + + + + diff --git a/interface-definitions/include/isis/passive.xml.i b/interface-definitions/include/isis/passive.xml.i new file mode 100644 index 000000000..6d05f8cc7 --- /dev/null +++ b/interface-definitions/include/isis/passive.xml.i @@ -0,0 +1,8 @@ + + + + Configure passive mode for interface + + + + diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i new file mode 100644 index 000000000..84e2f7bb2 --- /dev/null +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -0,0 +1,769 @@ + + + + Configure the authentication password for an area + + + + + Plain-text authentication type + + txt + Level-wide password + + + + + + MD5 authentication type + + txt + Level-wide password + + + + + + + + Control distribution of default information + + + + + Distribute a default route + + + + + Distribute default route for IPv4 + + + #include + + + + + Distribute default route for IPv6 + + + #include + + + + + + + + + Set the authentication password for a routing domain + + + + + Plain-text authentication type + + txt + Level-wide password + + + + + + MD5 authentication type + + txt + Level-wide password + + + + + + + + Dynamic hostname for IS-IS + + + + + + IS-IS level number + + level-1 level-1-2 level-2 + + + level-1 + Act as a station router + + + level-1-2 + Act as both a station and an area router + + + level-2 + Act as an area router + + + ^(level-1|level-1-2|level-2)$ + + + + + + Log adjacency state changes + + + + + + Minimum interval between regenerating same LSP + + u32:1-120 + Minimum interval in seconds + + + + + + + + + Configure the maximum size of generated LSPs + + u32:128-4352 + Maximum size of generated LSPs + + + + + + 1497 + + + + LSP refresh interval + + u32:1-65235 + LSP refresh interval in seconds + + + + + + + + + Maximum LSP lifetime + + u32:350-65535 + LSP lifetime in seconds + + + + + + + + + Use old-style (ISO 10589) or new-style packet formats + + narrow transition wide + + + narrow + Use old style of TLVs with narrow metric + + + transition + Send and accept both styles of TLVs during transition + + + wide + Use new style of TLVs to carry wider metric + + + ^(narrow|transition|wide)$ + + + + + + A Network Entity Title for this process (ISO only) + + XX.XXXX. ... .XXX.XX + Network entity title (NET) + + + [a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2} + + + + + + Use the RFC 6232 purge-originator + + + + + + Show IS-IS neighbor adjacencies + + + + + Enable MPLS traffic engineering extensions + + + + + + + MPLS traffic engineering router ID + + ipv4 + IPv4 address + + + + + + + + + + + Segment-Routing (SPRING) settings + + + + + Enable segment-routing functionality + + + + + + Global block label range + + + + + The lower bound of the global block + + u32:16-1048575 + MPLS label value + + + + + + + + + The upper bound of the global block + + u32:16-1048575 + MPLS label value + + + + + + + + + + + + Maximum MPLS labels allowed for this router + + u32:1-16 + MPLS label depth + + + + + + + + + Static IPv4/IPv6 prefix segment/label mapping + + ipv4net + IPv4 prefix segment + + + ipv6net + IPv6 prefix segment + + + + + + + + + + Specify the absolute value of prefix segment/label ID + + + + + Specify the absolute value of prefix segment/label ID + + u32:16-1048575 + The absolute segment/label ID value + + + + + + + + + Request upstream neighbor to replace segment/label with explicit null label + + + + + + Do not request penultimate hop popping for segment/label + + + + + + + + Specify the index value of prefix segment/label ID + + + + + Specify the index value of prefix segment/label ID + + u32:0-65535 + The index segment/label ID value + + + + + + + + + Request upstream neighbor to replace segment/label with explicit null label + + + + + + Do not request penultimate hop popping for segment/label + + + + + + + + + + + + Redistribute information from another routing protocol + + + + + Redistribute IPv4 routes + + + + + Border Gateway Protocol (BGP) + + + #include + + + + + Redistribute connected routes into IS-IS + + + #include + + + + + Redistribute kernel routes into IS-IS + + + #include + + + + + Redistribute OSPF routes into IS-IS + + + #include + + + + + Redistribute RIP routes into IS-IS + + + #include + + + + + Redistribute static routes into IS-IS + + + #include + + + + + + + Redistribute IPv6 routes + + + + + Redistribute BGP routes into IS-IS + + + #include + + + + + Redistribute connected routes into IS-IS + + + #include + + + + + Redistribute kernel routes into IS-IS + + + #include + + + + + Redistribute OSPFv3 routes into IS-IS + + + #include + + + + + Redistribute RIPng routes into IS-IS + + + #include + + + + + Redistribute static routes into IS-IS + + + #include + + + + + + + + + Set attached bit to identify as L1/L2 router for inter-area traffic + + + + + + Set overload bit to avoid any transit traffic + + + + + + IETF SPF delay algorithm + + + + + Delay used while in QUIET state + + u32:0-60000 + Delay used while in QUIET state (in ms) + + + + + + + + + Delay used while in SHORT_WAIT state + + u32:0-60000 + Delay used while in SHORT_WAIT state (in ms) + + + + + + + + + Delay used while in LONG_WAIT + + u32:0-60000 + Delay used while in LONG_WAIT state in ms + + + + + + + + + Time with no received IGP events before considering IGP stable + + u32:0-60000 + Time with no received IGP events before considering IGP stable in ms + + + + + + + + + Maximum duration needed to learn all the events related to a single failure + + u32:0-60000 + Maximum duration needed to learn all the events related to a single failure in ms + + + + + + + + + + + Minimum interval between SPF calculations + + u32:1-120 + Interval in seconds + + + + + + + + + Interface params + + + + + + #include + + + Configure circuit type for interface + + level-1 level-1-2 level-2-only + + + level-1 + Level-1 only adjacencies are formed + + + level-1-2 + Level-1-2 adjacencies are formed + + + level-2-only + Level-2 only adjacencies are formed + + + ^(level-1|level-1-2|level-2-only)$ + + + + + + Add padding to IS-IS hello packets + + + + + + Set Hello interval + + u32:1-600 + Set Hello interval + + + + + + + + + Set Hello interval + + u32:2-100 + Set multiplier for Hello holding time + + + + + + + #include + + + Set network type + + + + + point-to-point network type + + + + + + #include + + + Configure the authentication password for a circuit + + + + + Plain-text authentication type + + txt + Circuit password + + + + + + + + Set priority for Designated Router election + + u32:0-127 + Priority value + + + + + + + + + Set PSNP interval + + u32:0-127 + PSNP interval in seconds + + + + + + + + + Disable three-way handshake + + + + + +#include + \ No newline at end of file diff --git a/interface-definitions/include/isis/redistribute-level-1-2.xml.i b/interface-definitions/include/isis/redistribute-level-1-2.xml.i new file mode 100644 index 000000000..abb85274f --- /dev/null +++ b/interface-definitions/include/isis/redistribute-level-1-2.xml.i @@ -0,0 +1,20 @@ + + + + Redistribute into level-1 + + + #include + #include + + + + + Redistribute into level-2 + + + #include + #include + + + diff --git a/interface-definitions/include/route-map.xml.i b/interface-definitions/include/route-map.xml.i new file mode 100644 index 000000000..88092b7d4 --- /dev/null +++ b/interface-definitions/include/route-map.xml.i @@ -0,0 +1,18 @@ + + + + Specify route-map name to use + + policy route-map + + + txt + Route map name + + + ^[-_a-zA-Z0-9.]+$ + + Name of route-map can only contain alpha-numeric letters, hyphen and underscores + + + diff --git a/interface-definitions/protocols-isis.xml.in b/interface-definitions/protocols-isis.xml.in index 624c72a4c..e0bc47bb9 100644 --- a/interface-definitions/protocols-isis.xml.in +++ b/interface-definitions/protocols-isis.xml.in @@ -2,781 +2,15 @@ - + Intermediate System to Intermediate System (IS-IS) 610 - - text(TAG) - ISO Routing area tag - - - - Configure the authentication password for an area - - - - - Plain-text authentication type - - txt - Level-wide password - - - - - - MD5 authentication type - - txt - Level-wide password - - - - - - - - Control distribution of default information - - - - - Distribute a default route - - - - - Distribute default route for IPv4 - - - - - Distribute default route into level-1 - - - - - - Distribute default route into level-2 - - - - - - - - Distribute default route for IPv6 - - - - - Distribute default route into level-1 - - always - - - always - Always advertise default route - - - - - - Distribute default route into level-2 - - always - - - always - Always advertise default route - - - - - - - - - - - - Set the authentication password for a routing domain - - - - - Plain-text authentication type - - txt - Level-wide password - - - - - - - - - Dynamic hostname for IS-IS - - - - - - IS-IS level number - - level-1 level-1-2 level-2 - - - level-1 - Act as a station router - - - level-1-2 - Act as both a station and an area router - - - level-2 - Act as an area router - - - ^(level-1|level-1-2|level-2)$ - - - - - - Minimum interval between regenerating same LSP - - u32:1-120 - Minimum interval in seconds - - - - - - - - - Configure the maximum size of generated LSPs - - u32:128-4352 - Maximum size of generated LSPs - - - - - - - - - LSP refresh interval - - u32:1-65235 - LSP refresh interval in seconds - - - - - - - - - Maximum LSP lifetime - - u32:350-65535 - LSP lifetime in seconds - - - - - - - - - Use old-style (ISO 10589) or new-style packet formats - - narrow transition wide - - - narrow - Use old style of TLVs with narrow metric - - - transition - Send and accept both styles of TLVs during transition - - - wide - Use new style of TLVs to carry wider metric - - - ^(narrow|transition|wide)$ - - - - - - A Network Entity Title for this process (ISO only) - - XX.XXXX. ... .XXX.XX - Network entity title (NET) - - - [a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2} - - - - - - Use the RFC 6232 purge-originator - - - - - - Show IS-IS neighbor adjacencies - - - - - Enable MPLS traffic engineering extensions - - - - - - - MPLS traffic engineering router ID - - ipv4 - IPv4 address - - - - - - - - - - - Segment-Routing (SPRING) settings - - - - - Enable segment-routing functionality - - - - - - Global block label range - - - - - The lower bound of the global block - - u32:16-1048575 - MPLS label value - - - - - - - - - The upper bound of the global block - - u32:16-1048575 - MPLS label value - - - - - - - - - - - - Maximum MPLS labels allowed for this router - - u32:1-16 - MPLS label depth - - - - - - - - - Static IPv4/IPv6 prefix segment/label mapping - - ipv4net - IPv4 prefix segment - - - ipv6net - IPv6 prefix segment - - - - - - - - - - Specify the absolute value of prefix segment/label ID - - - - - Specify the absolute value of prefix segment/label ID - - u32:16-1048575 - The absolute segment/label ID value - - - - - - - - - Request upstream neighbor to replace segment/label with explicit null label - - - - - - Do not request penultimate hop popping for segment/label - - - - - - - - Specify the index value of prefix segment/label ID - - - - - Specify the index value of prefix segment/label ID - - u32:0-65535 - The index segment/label ID value - - - - - - - - - Request upstream neighbor to replace segment/label with explicit null label - - - - - - Do not request penultimate hop popping for segment/label - - - - - - - - - - - - Redistribute information from another routing protocol - - - - - Redistribute IPv4 routes - - - - - Border Gateway Protocol (BGP) - - - #include - - - - - Redistribute connected routes into IS-IS - - - #include - - - - - Redistribute kernel routes into IS-IS - - - #include - - - - - Redistribute OSPF routes into IS-IS - - - #include - - - - - Redistribute RIP routes into IS-IS - - - #include - - - - - Redistribute static routes into IS-IS - - - #include - - - - - - - - - Set attached bit to identify as L1/L2 router for inter-area traffic - - - - - - Set overload bit to avoid any transit traffic - - - - - - IETF SPF delay algorithm - - - - - Delay used while in QUIET state - - u32:0-60000 - Delay used while in QUIET state (in ms) - - - - - - - - - Delay used while in SHORT_WAIT state - - u32:0-60000 - Delay used while in SHORT_WAIT state (in ms) - - - - - - - - - Delay used while in LONG_WAIT - - u32:0-60000 - Delay used while in LONG_WAIT state (in ms) - - - - - - - - - Time with no received IGP events before considering IGP stable - - u32:0-60000 - Time with no received IGP events before considering IGP stable (in ms) - - - - - - - - - Maximum duration needed to learn all the events related to a single failure - - u32:0-60000 - Maximum duration needed to learn all the events related to a single failure (in ms) - - - - - - - - - - - Minimum interval between SPF calculations - - u32:1-120 - Minimum interval between consecutive SPFs in seconds - - - - - - - - - - Interface params - - - - - - - - Enable BFD support - - - - - - Configure circuit type for interface - - level-1 level-1-2 level-2-only - - - level-1 - Level-1 only adjacencies are formed - - - level-1-2 - Level-1-2 adjacencies are formed - - - level-2-only - Level-2 only adjacencies are formed - - - ^(level-1|level-1-2|level-2-only)$ - - - - - - Add padding to IS-IS hello packets - - - - - - Set Hello interval - - u32:1-600 - Set Hello interval - - - - - - - - - Set Hello interval - - u32:2-100 - Set multiplier for Hello holding time - - - - - - - - - Set default metric for circuit - - u32:0-16777215 - Default metric value - - - - - - - - - Set network type - - - - - point-to-point network type - - - - - - - - Configure the passive mode for interface - - - - - - Configure the authentication password for a circuit - - - - - Plain-text authentication type - - txt - Circuit password - - - - - - - - Set priority for Designated Router election - - u32:0-127 - Priority value - - - - - - - - - Set PSNP interval in seconds - - u32:0-127 - Priority value - - - - - - - - - Disable three-way handshake - - - - - + #include - + diff --git a/smoketest/configs/isis-small b/smoketest/configs/isis-small new file mode 100644 index 000000000..2c42ac9c4 --- /dev/null +++ b/smoketest/configs/isis-small @@ -0,0 +1,105 @@ +interfaces { + dummy dum0 { + address 203.0.113.1/24 + } + ethernet eth0 { + duplex auto + speed auto + } + ethernet eth1 { + address 192.0.2.1/24 + duplex auto + speed auto + } + ethernet eth2 { + duplex auto + speed auto + } + ethernet eth3 { + duplex auto + speed auto + } +} +policy { + prefix-list EXPORT-ISIS { + rule 10 { + action permit + prefix 203.0.113.0/24 + } + } + route-map EXPORT-ISIS { + rule 10 { + action permit + match { + ip { + address { + prefix-list EXPORT-ISIS + } + } + } + } + } +} +protocols { + isis FOO { + interface eth1 { + bfd + } + net 49.0001.1921.6800.1002.00 + redistribute { + ipv4 { + connected { + level-2 { + route-map EXPORT-ISIS + } + } + } + } + } +} +system { + config-management { + commit-revisions 200 + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos.io + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ + plaintext-password "" + } + level admin + } + } + ntp { + server 0.pool.ntp.org { + } + server 1.pool.ntp.org { + } + server 2.pool.ntp.org { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } + time-zone Europe/Berlin +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@1:conntrack-sync@1:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@18:ipoe-server@1:ipsec@5:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@7:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@20:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.0-rc1 + diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py new file mode 100755 index 000000000..482162b0e --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import process_named_running + +PROCESS_NAME = 'isisd' +base_path = ['protocols', 'isis'] + +domain = 'VyOS' +net = '49.0001.1921.6800.1002.00' + +class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + cls._interfaces = Section.interfaces('ethernet') + + # call base-classes classmethod + super(cls, cls).setUpClass() + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + def isis_base_config(self): + self.cli_set(base_path + ['net', net]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + + def test_isis_01_redistribute(self): + prefix_list = 'EXPORT-ISIS' + route_map = 'EXPORT-ISIS' + rule = '10' + + self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit']) + self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24']) + self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit']) + self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'match', 'ip', 'address', 'prefix-list', prefix_list]) + + self.cli_set(base_path) + + # verify() - net id and interface are mandatory + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.isis_base_config() + self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) + self.cli_set(base_path + ['log-adjacency-changes']) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router isis {domain}') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' log-adjacency-changes', tmp) + self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp) + + for interface in self._interfaces: + tmp = self.getFRRconfig(f'interface {interface}') + self.assertIn(f' ip router isis {domain}', tmp) + self.assertIn(f' ipv6 router isis {domain}', tmp) + + self.cli_delete(['policy', 'route-map', route_map]) + self.cli_delete(['policy', 'prefix-list', prefix_list]) + + def test_isis_02_zebra_route_map(self): + # Implemented because of T3328 + route_map = 'foo-isis-in' + + self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + + self.isis_base_config() + self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) + self.cli_set(base_path + ['route-map', route_map]) + + # commit changes + self.cli_commit() + + # Verify FRR configuration + zebra_route_map = f'ip protocol isis route-map {route_map}' + frrconfig = self.getFRRconfig(zebra_route_map) + self.assertIn(zebra_route_map, frrconfig) + + # Remove the route-map again + self.cli_delete(base_path + ['route-map']) + # commit changes + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig(zebra_route_map) + self.assertNotIn(zebra_route_map, frrconfig) + + self.cli_delete(['policy', 'route-map', route_map]) + + def test_isis_03_default_information(self): + metric = '50' + route_map = 'default-foo-' + + self.isis_base_config() + for afi in ['ipv4', 'ipv6']: + for level in ['level-1', 'level-2']: + self.cli_set(base_path + ['default-information', 'originate', afi, level, 'always']) + self.cli_set(base_path + ['default-information', 'originate', afi, level, 'metric', metric]) + self.cli_set(base_path + ['default-information', 'originate', afi, level, 'route-map', route_map + level + afi]) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router isis {domain}') + self.assertIn(f' net {net}', tmp) + + for afi in ['ipv4', 'ipv6']: + for level in ['level-1', 'level-2']: + route_map_name = route_map + level + afi + self.assertIn(f' default-information originate {afi} {level} always route-map {route_map_name} metric {metric}', tmp) + + def test_isis_04_password(self): + password = 'foo' + + self.isis_base_config() + + self.cli_set(base_path + ['area-password', 'plaintext-password', password]) + self.cli_set(base_path + ['area-password', 'md5', password]) + self.cli_set(base_path + ['domain-password', 'plaintext-password', password]) + self.cli_set(base_path + ['domain-password', 'md5', password]) + + # verify() - can not use both md5 and plaintext-password for area-password + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['area-password', 'md5', password]) + + # verify() - can not use both md5 and plaintext-password for domain-password + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['domain-password', 'md5', password]) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router isis {domain}') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' domain-password clear {password}', tmp) + self.assertIn(f' area-password clear {password}', tmp) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index eab580083..0c179b724 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -19,12 +19,16 @@ import os from sys import exit from vyos.config import Config +from vyos.configdict import dict_merge from vyos.configdict import node_changed -from vyos import ConfigError -from vyos.util import call +from vyos.configverify import verify_common_route_maps +from vyos.configverify import verify_interface_exists +from vyos.ifconfig import Interface from vyos.util import dict_search -from vyos.template import render +from vyos.util import get_interface_config from vyos.template import render_to_string +from vyos.xml import defaults +from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() @@ -34,131 +38,172 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'isis'] - isis = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + base = ['protocols', 'isis'] + isis = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) + + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + isis['interface_removed'] = list(interfaces_removed) + + # Bail out early if configuration tree does not exist + if not conf.exists(base): + isis.update({'deleted' : ''}) + return isis + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + # XXX: Note that we can not call defaults(base), as defaults does not work + # on an instance of a tag node. + default_values = defaults(base) + # merge in default values + isis = dict_merge(default_values, isis) + + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify(). + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = conf.get_config_dict(['policy']) + # Merge policy dict into "regular" config dict + isis = dict_merge(tmp, isis) return isis def verify(isis): # bail out early - looks like removal from running config - if not isis: + if not isis or 'deleted' in isis: return None - for process, isis_config in isis.items(): - # If more then one isis process is defined (Frr only supports one) - # http://docs.frrouting.org/en/latest/isisd.html#isis-router - if len(isis) > 1: - raise ConfigError('Only one isis process can be defined') - - # If network entity title (net) not defined - if 'net' not in isis_config: - raise ConfigError('ISIS net format iso is mandatory!') - - # last byte in IS-IS area address must be 0 - tmp = isis_config['net'].split('.') - if int(tmp[-1]) != 0: - raise ConfigError('Last byte of IS-IS network entity title must always be 0!') - - # If interface not set - if 'interface' not in isis_config: - raise ConfigError('ISIS interface is mandatory!') - - # If md5 and plaintext-password set at the same time - if 'area_password' in isis_config: - if {'md5', 'plaintext_password'} <= set(isis_config['encryption']): - raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!') - - # If one param from delay set, but not set others - if 'spf_delay_ietf' in isis_config: - required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn'] - exist_timers = [] - for elm_timer in required_timers: - if elm_timer in isis_config['spf_delay_ietf']: - exist_timers.append(elm_timer) - - exist_timers = set(required_timers).difference(set(exist_timers)) - if len(exist_timers) > 0: - raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-')) - - # If Redistribute set, but level don't set - if 'redistribute' in isis_config: - proc_level = isis_config.get('level','').replace('-','_') - for proto, proto_config in isis_config.get('redistribute', {}).get('ipv4', {}).items(): + if 'net' not in isis: + raise ConfigError('Network entity is mandatory!') + + # last byte in IS-IS area address must be 0 + tmp = isis['net'].split('.') + if int(tmp[-1]) != 0: + raise ConfigError('Last byte of IS-IS network entity title must always be 0!') + + verify_common_route_maps(isis) + + # If interface not set + if 'interface' not in isis: + raise ConfigError('Interface used for routing updates is mandatory!') + + for interface in isis['interface']: + verify_interface_exists(interface) + # Interface MTU must be >= configured lsp-mtu + mtu = Interface(interface).get_mtu() + area_mtu = isis['lsp_mtu'] + # Recommended maximum PDU size = interface MTU - 3 bytes + recom_area_mtu = mtu - 3 + if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu: + raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \ + f'current area MTU is {area_mtu}! \n' \ + f'Recommended area lsp-mtu {recom_area_mtu} or less ' \ + '(calculated on MTU size).') + + # If md5 and plaintext-password set at the same time + for password in ['area_password', 'domain_password']: + if password in isis: + if {'md5', 'plaintext_password'} <= set(isis[password]): + tmp = password.replace('_', '-') + raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!') + + # If one param from delay set, but not set others + if 'spf_delay_ietf' in isis: + required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn'] + exist_timers = [] + for elm_timer in required_timers: + if elm_timer in isis['spf_delay_ietf']: + exist_timers.append(elm_timer) + + exist_timers = set(required_timers).difference(set(exist_timers)) + if len(exist_timers) > 0: + raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-')) + + # If Redistribute set, but level don't set + if 'redistribute' in isis: + proc_level = isis.get('level','').replace('-','_') + for afi in ['ipv4', 'ipv6']: + if afi not in isis['redistribute']: + continue + + for proto, proto_config in isis['redistribute'][afi].items(): if 'level_1' not in proto_config and 'level_2' not in proto_config: - raise ConfigError('Redistribute level-1 or level-2 should be specified in \"protocols isis {} redistribute ipv4 {}\"'.format(process, proto)) - for redistribute_level in proto_config.keys(): - if proc_level and proc_level != 'level_1_2' and proc_level != redistribute_level: - raise ConfigError('\"protocols isis {0} redistribute ipv4 {2} {3}\" cannot be used with \"protocols isis {0} level {1}\"'.format(process, proc_level, proto, redistribute_level)) - - # Segment routing checks - if dict_search('segment_routing', isis_config): - if dict_search('segment_routing.global_block', isis_config): - high_label_value = dict_search('segment_routing.global_block.high_label_value', isis_config) - low_label_value = dict_search('segment_routing.global_block.low_label_value', isis_config) - # If segment routing global block high value is blank, throw error - if low_label_value and not high_label_value: - raise ConfigError('Segment routing global block high value must not be left blank') - # If segment routing global block low value is blank, throw error - if high_label_value and not low_label_value: - raise ConfigError('Segment routing global block low value must not be left blank') - # If segment routing global block low value is higher than the high value, throw error - if int(low_label_value) > int(high_label_value): - raise ConfigError('Segment routing global block low value must be lower than high value') - - if dict_search('segment_routing.local_block', isis_config): - high_label_value = dict_search('segment_routing.local_block.high_label_value', isis_config) - low_label_value = dict_search('segment_routing.local_block.low_label_value', isis_config) - # If segment routing local block high value is blank, throw error - if low_label_value and not high_label_value: - raise ConfigError('Segment routing local block high value must not be left blank') - # If segment routing local block low value is blank, throw error - if high_label_value and not low_label_value: - raise ConfigError('Segment routing local block low value must not be left blank') - # If segment routing local block low value is higher than the high value, throw error - if int(low_label_value) > int(high_label_value): - raise ConfigError('Segment routing local block low value must be lower than high value') + raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \ + f'"protocols isis {process} redistribute {afi} {proto}"!') + + for redistr_level, redistr_config in proto_config.items(): + if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level: + raise ConfigError(f'"protocols isis {process} redistribute {afi} {proto} {redistr_level}" ' \ + f'can not be used with \"protocols isis {process} level {proc_level}\"') + + # Segment routing checks + if dict_search('segment_routing.global_block', isis): + high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) + low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) + + # If segment routing global block high value is blank, throw error + if (low_label_value and not high_label_value) or (high_label_value and not low_label_value): + raise ConfigError('Segment routing global block requires both low and high value!') + + # If segment routing global block low value is higher than the high value, throw error + if int(low_label_value) > int(high_label_value): + raise ConfigError('Segment routing global block low value must be lower than high value') + + if dict_search('segment_routing.local_block', isis): + high_label_value = dict_search('segment_routing.local_block.high_label_value', isis) + low_label_value = dict_search('segment_routing.local_block.low_label_value', isis) + + # If segment routing local block high value is blank, throw error + if (low_label_value and not high_label_value) or (high_label_value and not low_label_value): + raise ConfigError('Segment routing local block requires both high and low value!') + + # If segment routing local block low value is higher than the high value, throw error + if int(low_label_value) > int(high_label_value): + raise ConfigError('Segment routing local block low value must be lower than high value') return None def generate(isis): - if not isis: - isis['new_frr_config'] = '' + if not isis or 'deleted' in isis: + isis['frr_isisd_config'] = '' + isis['frr_zebra_config'] = '' return None - # only one ISIS process is supported, so we can directly send the first key - # of the config dict - process = list(isis.keys())[0] - isis[process]['process'] = process - - isis['new_frr_config'] = render_to_string('frr/isisd.frr.tmpl', - isis[process]) - + isis['protocol'] = 'isis' # required for frr/route-map.frr.tmpl + isis['frr_zebra_config'] = render_to_string('frr/route-map.frr.tmpl', isis) + isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis) return None def apply(isis): + isis_daemon = 'isisd' + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(daemon='isisd') - frr_cfg.modify_section(r'interface \S+', '') - frr_cfg.modify_section(f'router isis \S+', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['new_frr_config']) - frr_cfg.commit_configuration(daemon='isisd') - - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if isis['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(daemon='isisd') - - # Debugging - ''' - print('') - print('--------- DEBUGGING ----------') - print(f'Existing config:\n{frr_cfg["original_config"]}\n\n') - print(f'Replacement config:\n{isis["new_frr_config"]}\n\n') - print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n') - ''' + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)') + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + + frr_cfg.load_configuration(isis_daemon) + frr_cfg.modify_section(f'^router isis VyOS$', '') + + for key in ['interface', 'interface_removed']: + if key not in isis: + continue + for interface in isis[key]: + frr_cfg.modify_section(f'^interface {interface}$', '') + + frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config']) + frr_cfg.commit_configuration(isis_daemon) + + # Save configuration to /run/frr/config/frr.conf + frr.save_configuration() return None diff --git a/src/migration-scripts/isis/0-to-1 b/src/migration-scripts/isis/0-to-1 new file mode 100755 index 000000000..93cbbbed5 --- /dev/null +++ b/src/migration-scripts/isis/0-to-1 @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 . + +# T3417: migrate IS-IS tagNode to node as we can only have one IS-IS process + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['protocols', 'isis'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# Only one IS-IS process is supported, thus this operation is save +isis_base = base + config.list_nodes(base) + +# We need a temporary copy of the config +tmp_base = ['protocols', 'isis2'] +config.copy(isis_base, tmp_base) + +# Now it's save to delete the old configuration +config.delete(base) + +# Rename temporary copy to new final config (IS-IS domain key is static and no +# longer required to be set via CLI) +config.rename(tmp_base, 'isis') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) -- cgit v1.2.3 From a515212f4efb08846df04405f31a828edcd63552 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 30 Aug 2021 21:29:22 +0200 Subject: ethernet: T3619: fix VyOS 1.2 -> 1.3 performance degradation An analysis of the code base from VyOS 1.2 -> 1.3 -> 1.4 revealed the following "root-cause" VyOS 1.2 uses the "old" node.def file format for: * Generic Segmentation Offloading * Generic Receive Offloading So if any of the above settings is available on the configuration CLI, the node.def file will be executed - this is how it works. By default, this CLI option is not enabled in VyOS 1.2 - but the Linux Kernel enables offloading "under the hood" by default for GRO, GSO... which will boost the performance for users magically. With the rewrite in VyOS 1.3 of all the interface related code T1579, and especially T1637 this was moved to a new approach. There is now only one handler script which is called whenever a user changes something under the interfaces ethernet tree. The Full CLI configuration is assembled by get_interface_dict() - a wrapper for get_config_dict() which abstracts and works for all of our interface types - single source design. The problem now comes into play when the gathered configuration is actually written to the hardware, as there is no GSO, GRO or foo-offloading setting defined - we behave as instructed and disable the offloading. So the real bug originates from VyOS 1.2 and the old Vyatta codebase, but the recent XML Python rewrites brought that one up to light. Solution: A configuration migration script will be provided starting with VyOS 1.3 which will read in the CLI configuration of the ethernet interfaces and if not enabled, will query the adapter if offloading is supported at all, and if so, will enable the CLI nodes. One might say that this will "blow" the CLI configuration but it only represents the truth - which was masked in VyOS 1.2. --- src/migration-scripts/interfaces/20-to-21 | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100755 src/migration-scripts/interfaces/20-to-21 (limited to 'src') diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 new file mode 100755 index 000000000..9210330d6 --- /dev/null +++ b/src/migration-scripts/interfaces/20-to-21 @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 . + +# T3619: mirror Linux Kernel defaults for ethernet offloading options into VyOS +# CLI. See https://phabricator.vyos.net/T3619#102254 for all the details. + +from sys import argv + +from vyos.ethtool import Ethtool +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['interfaces', 'ethernet'] +config = ConfigTree(config_file) + +if not config.exists(base): + exit(0) + +for ifname in config.list_nodes(base): + eth = Ethtool(ifname) + + # If GRO is enabled by the Kernel - we reflect this on the CLI. If GRO is + # enabled via CLI but not supported by the NIC - we remove it from the CLI + configured = config.exists(base + [ifname, 'offload', 'gro']) + enabled, fixed = eth.get_generic_receive_offload() + if configured and fixed: + config.delete(base + [ifname, 'offload', 'gro']) + elif enabled and not fixed: + config.set(base + [ifname, 'offload', 'gro']) + + # If GSO is enabled by the Kernel - we reflect this on the CLI. If GSO is + # enabled via CLI but not supported by the NIC - we remove it from the CLI + configured = config.exists(base + [ifname, 'offload', 'gso']) + enabled, fixed = eth.get_generic_segmentation_offload() + if configured and fixed: + config.delete(base + [ifname, 'offload', 'gso']) + elif enabled and not fixed: + config.set(base + [ifname, 'offload', 'gso']) + + # If LRO is enabled by the Kernel - we reflect this on the CLI. If LRO is + # enabled via CLI but not supported by the NIC - we remove it from the CLI + configured = config.exists(base + [ifname, 'offload', 'lro']) + enabled, fixed = eth.get_large_receive_offload() + if configured and fixed: + config.delete(base + [ifname, 'offload', 'lro']) + elif enabled and not fixed: + config.set(base + [ifname, 'offload', 'lro']) + + # If SG is enabled by the Kernel - we reflect this on the CLI. If SG is + # enabled via CLI but not supported by the NIC - we remove it from the CLI + configured = config.exists(base + [ifname, 'offload', 'sg']) + enabled, fixed = eth.get_scatter_gather() + if configured and fixed: + config.delete(base + [ifname, 'offload', 'sg']) + elif enabled and not fixed: + config.set(base + [ifname, 'offload', 'sg']) + + # If TSO is enabled by the Kernel - we reflect this on the CLI. If TSO is + # enabled via CLI but not supported by the NIC - we remove it from the CLI + configured = config.exists(base + [ifname, 'offload', 'tso']) + enabled, fixed = eth.get_tcp_segmentation_offload() + if configured and fixed: + config.delete(base + [ifname, 'offload', 'tso']) + elif enabled and not fixed: + config.set(base + [ifname, 'offload', 'tso']) + + # If UFO is enabled by the Kernel - we reflect this on the CLI. If UFO is + # enabled via CLI but not supported by the NIC - we remove it from the CLI + configured = config.exists(base + [ifname, 'offload', 'ufo']) + enabled, fixed = eth.get_udp_fragmentation_offload() + if configured and fixed: + config.delete(base + [ifname, 'offload', 'ufo']) + elif enabled and not fixed: + config.set(base + [ifname, 'offload', 'ufo']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3 From f5e46ee6cc2b6c1c1869e26beca4ccd5bf52b62f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 30 Aug 2021 21:36:52 +0200 Subject: ethernet: T3787: remove deprecated UDP fragmentation offloading option Deprecated in the Linux Kernel by commit 08a00fea6de277df12ccfadc21 ("net: Remove references to NETIF_F_UFO from ethtool."). --- interface-definitions/interfaces-ethernet.xml.in | 6 ----- python/vyos/ethtool.py | 3 --- python/vyos/ifconfig/ethernet.py | 28 ------------------------ src/migration-scripts/interfaces/20-to-21 | 12 ++++------ 4 files changed, 4 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index ec20bca8d..27d555552 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -101,12 +101,6 @@ - - - Enable UDP Fragmentation Offloading - - - diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index a81ddac31..397be6bb2 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -122,9 +122,6 @@ class Ethtool: def get_tcp_segmentation_offload(self): return self._get_generic('tcp-segmentation-offload') - def get_udp_fragmentation_offload(self): - return self._get_generic('udp-fragmentation-offload') - def get_rx_buffer(self): # Configuration of RX ring-buffers is not supported on every device, # thus when it's impossible return None diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index cb03a006c..a6c7f5f25 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -72,11 +72,6 @@ class EthernetIf(Interface): 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v), # 'shellcmd': 'ethtool -K {ifname} tso {value}', }, - 'ufo': { - 'validate': lambda v: assert_list(v, ['on', 'off']), - 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v), - # 'shellcmd': 'ethtool -K {ifname} ufo {value}', - }, }} _sysfs_set = {**Interface._sysfs_set, **{ @@ -339,26 +334,6 @@ class EthernetIf(Interface): print('Adapter does not support changing tcp-segmentation-offload settings!') return False - def set_ufo(self, state): - """ - Enable UDP fragmentation offloading. State can be either True or False. - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_udp_offload(True) - """ - if not isinstance(state, bool): - raise ValueError('Value out of range') - - enabled, fixed = self.ethtool.get_udp_fragmentation_offload() - if enabled != state: - if not fixed: - return self.set_interface('gro', 'on' if state else 'off') - else: - print('Adapter does not support changing udp-fragmentation-offload settings!') - return False - def set_ring_buffer(self, b_type, b_size): """ Example: @@ -404,9 +379,6 @@ class EthernetIf(Interface): # TSO (TCP segmentation offloading) self.set_tso(dict_search('offload.tso', config) != None) - # UDP fragmentation offloading - self.set_ufo(dict_search('offload.ufo', config) != None) - # Set physical interface speed and duplex if {'speed', 'duplex'} <= set(config): speed = config.get('speed') diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index 9210330d6..4b0e70d35 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -15,7 +15,8 @@ # along with this program. If not, see . # T3619: mirror Linux Kernel defaults for ethernet offloading options into VyOS -# CLI. See https://phabricator.vyos.net/T3619#102254 for all the details. +# CLI. See https://phabricator.vyos.net/T3619#102254 for all the details. +# T3787: Remove deprecated UDP fragmentation offloading option from sys import argv @@ -84,14 +85,9 @@ for ifname in config.list_nodes(base): elif enabled and not fixed: config.set(base + [ifname, 'offload', 'tso']) - # If UFO is enabled by the Kernel - we reflect this on the CLI. If UFO is - # enabled via CLI but not supported by the NIC - we remove it from the CLI - configured = config.exists(base + [ifname, 'offload', 'ufo']) - enabled, fixed = eth.get_udp_fragmentation_offload() - if configured and fixed: + # Remove deprecated UDP fragmentation offloading option + if config.exists(base + [ifname, 'offload', 'ufo']): config.delete(base + [ifname, 'offload', 'ufo']) - elif enabled and not fixed: - config.set(base + [ifname, 'offload', 'ufo']) try: with open(file_name, 'w') as f: -- cgit v1.2.3 From 11fdcb7cdd078e67b460b6863f8fd9785c33dc2d Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 30 Aug 2021 15:43:02 +0000 Subject: tunnel: T3786: Add checks for source any and not key (cherry picked from commit 5c29377fa91595088118419275f6d05b1fbfbd1d) --- src/conf_mode/interfaces-tunnel.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index e5958e9ae..a7207f94e 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -74,6 +74,12 @@ def verify(tunnel): verify_tunnel(tunnel) + # If tunnel source address any and key not set + if tunnel['encapsulation'] in ['gre'] and \ + tunnel['source_address'] == '0.0.0.0' and \ + dict_search('parameters.ip.key', tunnel) == None: + raise ConfigError('Tunnel parameters ip key must be set!') + verify_mtu_ipv6(tunnel) verify_address(tunnel) verify_vrf(tunnel) -- cgit v1.2.3 From 688f9810fde3947db66ff7e4c0ea21bf9708feec Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 31 Aug 2021 12:20:05 +0200 Subject: ssh: T3789: add custom validator for base64 encoded CLI data SSH keys used for remote login are supplied as base64 encoded data on the CLI. The key is not validated, thus an invalid copy/pasted key will render the login useless. This commit adds a custom and re-usable validator which check if the data is properly base64 encoded. (cherry picked from commit 00efce716912680354d47a2dca9769cd8c5c89ae) --- interface-definitions/system-login.xml.in | 5 ++++- src/validators/base64 | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 src/validators/base64 (limited to 'src') diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index fb34b7199..3c2c7dfa5 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -52,7 +52,10 @@ - Public key value (base64-encoded) + Public key value (Base64 encoded) + + + diff --git a/src/validators/base64 b/src/validators/base64 new file mode 100755 index 000000000..e2b1e730d --- /dev/null +++ b/src/validators/base64 @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 base64 +from sys import argv + +if __name__ == '__main__': + if len(argv) != 2: + exit(1) + try: + base64.b64decode(argv[1]) + except: + exit(1) + exit(0) -- cgit v1.2.3 From 031817eecb14280e3f421cb9c391ab29dbc2fa60 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 31 Aug 2021 11:58:46 +0200 Subject: ethernet: T3514: bail out early on invalid adapter speed/duplex setting Ethernet adapters have a discrete set of available speed and duplex settings. Instead of passing every value down to ethtool and let it decide, we can do this early in the VyOS verify() function for ethernet interfaces. (cherry picked from commit 91892e431349ca0edb5e3e3023e4f340ab9b777f) --- src/conf_mode/interfaces-ethernet.py | 48 +++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 349b0e7a3..a7e01e279 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -63,32 +63,20 @@ def verify(ethernet): ifname = ethernet['ifname'] verify_interface_exists(ifname) + ethtool = Ethtool(ifname) # No need to check speed and duplex keys as both have default values. if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')): raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured') - verify_mtu(ethernet) - verify_mtu_ipv6(ethernet) - verify_dhcpv6(ethernet) - verify_address(ethernet) - verify_vrf(ethernet) - verify_eapol(ethernet) - verify_mirror(ethernet) + if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto': + # We need to verify if the requested speed and duplex setting is + # supported by the underlaying NIC. + speed = ethernet['speed'] + duplex = ethernet['duplex'] + if not ethtool.check_speed_duplex(speed, duplex): + raise ConfigError(f'Adapter does not support speed "{speed}" and duplex "{duplex}"!') - # verify offloading capabilities - if dict_search('offload.rps', ethernet) != None: - if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'): - raise ConfigError('Interface does not suport RPS!') - - driver = EthernetIf(ifname).get_driver_name() - # T3342 - Xen driver requires special treatment - if driver == 'vif': - if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None: - raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\ - 'for MTU size larger then 1500 bytes') - - ethtool = Ethtool(ifname) if 'ring_buffer' in ethernet: max_rx = ethtool.get_rx_buffer() if not max_rx: @@ -108,6 +96,26 @@ def verify(ethernet): raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\ f'size of "{max_tx}" bytes!') + verify_mtu(ethernet) + verify_mtu_ipv6(ethernet) + verify_dhcpv6(ethernet) + verify_address(ethernet) + verify_vrf(ethernet) + verify_eapol(ethernet) + verify_mirror(ethernet) + + # verify offloading capabilities + if dict_search('offload.rps', ethernet) != None: + if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'): + raise ConfigError('Interface does not suport RPS!') + + driver = EthernetIf(ifname).get_driver_name() + # T3342 - Xen driver requires special treatment + if driver == 'vif': + if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None: + raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\ + 'for MTU size larger then 1500 bytes') + if {'is_bond_member', 'mac'} <= set(ethernet): print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' f'is a member of bond "{is_bond_member}"'.format(**ethernet)) -- cgit v1.2.3 From 2bfd809e9ae198d95b9fcb556440637fdcc4005c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 31 Aug 2021 18:15:47 +0200 Subject: ethernet: T2241: check if interface supports changing speed/duplex settings Not all interface drivers have the ability to change the speed and duplex settings. Known drivers with this limitation are vmxnet3, virtio_net and xen_netfront. If this driver is detected, an error will be presented to the user. (cherry picked from commit cc742d48579e4f76e5d3230d87e22f71f76f9301) --- python/vyos/ethtool.py | 15 +++++++++++++++ src/conf_mode/interfaces-ethernet.py | 3 ++- src/migration-scripts/interfaces/20-to-21 | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index 55b7b776f..fb2e49c1d 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -13,7 +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 re + from vyos.util import popen class Ethtool: @@ -41,8 +43,18 @@ class Ethtool: # } _speed_duplex = { } _ring_buffers = { } + _driver_name = None def __init__(self, ifname): + # Get driver used for interface + sysfs_file = f'/sys/class/net/{ifname}/device/driver/module' + if os.path.exists(sysfs_file): + link = os.readlink(sysfs_file) + self._driver_name = os.path.basename(link) + + if not self._driver_name: + raise ValueError(f'Could not determine driver for interface {ifname}!') + # Build a dictinary of supported link-speed and dupley settings. out, err = popen(f'ethtool {ifname}') reading = False @@ -142,6 +154,9 @@ class Ethtool: if duplex not in ['full', 'half']: raise ValueError(f'Value "{duplex}" for duplex is invalid!') + if self._driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']: + return False + if speed in self._speed_duplex: if duplex in self._speed_duplex[speed]: return True diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index a7e01e279..57e05d4ea 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -75,7 +75,8 @@ def verify(ethernet): speed = ethernet['speed'] duplex = ethernet['duplex'] if not ethtool.check_speed_duplex(speed, duplex): - raise ConfigError(f'Adapter does not support speed "{speed}" and duplex "{duplex}"!') + raise ConfigError(f'Adapter does not support changing speed and duplex '\ + f'settings to: {speed}/{duplex}!') if 'ring_buffer' in ethernet: max_rx = ethtool.get_rx_buffer() diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index 4b0e70d35..bd89dcdb4 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -89,6 +89,21 @@ for ifname in config.list_nodes(base): if config.exists(base + [ifname, 'offload', 'ufo']): config.delete(base + [ifname, 'offload', 'ufo']) + # Also while processing the interface configuration, not all adapters support + # changing the speed and duplex settings. If the desired speed and duplex + # values do not work for the NIC driver, we change them back to the default + # value of "auto" - which will be applied if the CLI node is deleted. + speed_path = base + [ifname, 'speed'] + duplex_path = base + [ifname, 'duplex'] + # speed and duplex must always be set at the same time if not set to "auto" + if config.exists(speed_path) and config.exists(duplex_path): + speed = config.return_value(speed_path) + duplex = config.return_value(duplex_path) + if speed != 'auto' and duplex != 'auto': + if not eth.check_speed_duplex(speed, duplex): + config.delete(speed_path) + config.delete(duplex_path) + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From de3c476ccb60d66a844f9e12a9ca2963ae2206e6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 31 Aug 2021 21:50:05 +0200 Subject: ethernet: T3163: only change ring-buffer settings if required Only update the RX/TX ring-buffer settings if they are different from the ones currently programmed to the hardware. There is no need to write the same value to the hardware again - this could cause traffic disruption on some NICs. (cherry picked from commit 29082959e0efc02462fba8560d6726096e8743e9) --- python/vyos/ethtool.py | 29 ++++++++++++++++++++++------- python/vyos/ifconfig/ethernet.py | 15 ++++++++++----- src/conf_mode/interfaces-ethernet.py | 4 ++-- 3 files changed, 34 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index e803e28a1..f5796358d 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -43,6 +43,7 @@ class Ethtool: # } _speed_duplex = { } _ring_buffers = { } + _ring_buffers_max = { } _driver_name = None _auto_negotiation = None @@ -99,10 +100,20 @@ class Ethtool: 'fixed' : fixed } - out, err = popen(f'ethtool -g {ifname}') + out, err = popen(f'ethtool --show-ring {ifname}') # We are only interested in line 2-5 which contains the device maximum # ringbuffers for line in out.splitlines()[2:6]: + if ':' in line: + key, value = [s.strip() for s in line.strip().split(":", 1)] + key = key.lower().replace(' ', '_') + # T3645: ethtool version used on Debian Bullseye changed the + # output format from 0 -> n/a. As we are only interested in the + # tx/rx keys we do not care about RX Mini/Jumbo. + if value.isdigit(): + self._ring_buffers_max[key] = int(value) + # Now we wan't to get the current RX/TX ringbuffer values - used for + for line in out.splitlines()[7:11]: if ':' in line: key, value = [s.strip() for s in line.strip().split(":", 1)] key = key.lower().replace(' ', '_') @@ -143,15 +154,19 @@ class Ethtool: def get_tcp_segmentation_offload(self): return self._get_generic('tcp-segmentation-offload') - def get_rx_buffer(self): - # Configuration of RX ring-buffers is not supported on every device, + def get_ring_buffer_max(self, rx_tx): + # Configuration of RX/TX ring-buffers is not supported on every device, # thus when it's impossible return None - return self._ring_buffers.get('rx', None) + if rx_tx not in ['rx', 'tx']: + ValueError('Ring-buffer type must be either "rx" or "tx"') + return self._ring_buffers_max.get(rx_tx, None) - def get_tx_buffer(self): - # Configuration of TX ring-buffers is not supported on every device, + def get_ring_buffer(self, rx_tx): + # Configuration of RX/TX ring-buffers is not supported on every device, # thus when it's impossible return None - return self._ring_buffers.get('tx', None) + if rx_tx not in ['rx', 'tx']: + ValueError('Ring-buffer type must be either "rx" or "tx"') + return self._ring_buffers.get(rx_tx, None) def check_speed_duplex(self, speed, duplex): """ Check if the passed speed and duplex combination is supported by diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index d6e42db99..5974a3d8f 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -317,21 +317,26 @@ class EthernetIf(Interface): print('Adapter does not support changing tcp-segmentation-offload settings!') return False - def set_ring_buffer(self, b_type, b_size): + def set_ring_buffer(self, rx_tx, size): """ Example: >>> from vyos.ifconfig import EthernetIf >>> i = EthernetIf('eth0') >>> i.set_ring_buffer('rx', '4096') """ + current_size = self.ethtool.get_ring_buffer(rx_tx) + if current_size == size: + # bail out early if nothing is about to change + return None + ifname = self.config['ifname'] - cmd = f'ethtool -G {ifname} {b_type} {b_size}' + cmd = f'ethtool --set-ring {ifname} {rx_tx} {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(f'could not set "{b_type}" ring-buffer for {ifname}') + print(f'could not set "{rx_tx}" ring-buffer for {ifname}') return output def update(self, config): @@ -370,8 +375,8 @@ class EthernetIf(Interface): # 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]) + for rx_tx, size in config['ring_buffer'].items(): + self.set_ring_buffer(rx_tx, size) # call base class first super().update(config) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 57e05d4ea..f3f3fede8 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -79,11 +79,11 @@ def verify(ethernet): f'settings to: {speed}/{duplex}!') if 'ring_buffer' in ethernet: - max_rx = ethtool.get_rx_buffer() + max_rx = ethtool.get_ring_buffer_max('rx') if not max_rx: raise ConfigError('Driver does not support RX ring-buffer configuration!') - max_tx = ethtool.get_tx_buffer() + max_tx = ethtool.get_ring_buffer_max('tx') if not max_tx: raise ConfigError('Driver does not support TX ring-buffer configuration!') -- cgit v1.2.3 From b0d4112bd6073e4a947869c3bd80f8e87783fbfa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 31 Aug 2021 23:03:01 +0200 Subject: vyos.ethtool: T3163: purify code to read and change flow-control settings It makes no sense to have a parser for the ethtool values in ethtool.py and ethernet.py - one instance ios more then enough! (cherry picked from commit 0229645c8248decb5664056df8aa5cd5dff41802) --- python/vyos/ethtool.py | 23 +++++++++++++++++ python/vyos/ifconfig/ethernet.py | 42 ++++++++----------------------- src/conf_mode/interfaces-ethernet.py | 4 +++ src/migration-scripts/interfaces/20-to-21 | 8 ++++++ 4 files changed, 45 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index f5796358d..87b9d7dd0 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -46,6 +46,8 @@ class Ethtool: _ring_buffers_max = { } _driver_name = None _auto_negotiation = None + _flow_control = None + _flow_control_enabled = None def __init__(self, ifname): # Get driver used for interface @@ -123,6 +125,15 @@ class Ethtool: if value.isdigit(): self._ring_buffers[key] = int(value) + # Get current flow control settings, but this is not supported by + # all NICs (e.g. vmxnet3 does not support is) + out, err = popen(f'ethtool --show-pause {ifname}') + if len(out.splitlines()) > 1: + self._flow_control = True + # read current flow control setting, this returns: + # ['Autonegotiate:', 'on'] + self._flow_control_enabled = out.splitlines()[1].split()[-1] + def _get_generic(self, feature): """ Generic method to read self._features and return a tuple for feature @@ -186,5 +197,17 @@ class Ethtool: return True return False + def check_flow_control(self): + """ Check if the NIC supports flow-control """ + if self._driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']: + return False + return self._flow_control + + def get_flow_control(self): + if self._flow_control_enabled == None: + raise ValueError('Interface does not support changing '\ + 'flow-control settings!') + return self._flow_control_enabled + def get_auto_negotiation(self): return self._auto_negotiation diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 5974a3d8f..cb07693c3 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -122,38 +122,16 @@ class EthernetIf(Interface): 'flow control settings!') return - # Get current flow control settings: - cmd = f'ethtool --show-pause {ifname}' - output, code = self._popen(cmd) - if code == 76: - # the interface does not support it - return '' - if code: - # never fail here as it prevent vyos to boot - print(f'unexpected return code {code} from {cmd}') - return '' - - # The above command returns - with tabs: - # - # Pause parameters for eth0: - # Autonegotiate: on - # RX: off - # TX: off - if re.search("Autonegotiate:\ton", output): - if enable == "on": - # flowcontrol is already enabled - no need to re-enable it again - # this will prevent the interface from flapping as applying the - # flow-control settings will take the interface down and bring - # it back up every time. - return '' - - # Assemble command executed on system. Unfortunately there is no way - # to change this setting via sysfs - cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}' - output, code = self._popen(cmd) - if code: - print(f'could not set flowcontrol for {ifname}') - return output + current = self.ethtool.get_flow_control() + if current != enable: + # Assemble command executed on system. Unfortunately there is no way + # to change this setting via sysfs + cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}' + output, code = self._popen(cmd) + if code: + print(f'Could not set flowcontrol for {ifname}') + return output + return None def set_speed_duplex(self, speed, duplex): """ diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index f3f3fede8..6e0d8c4e8 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -78,6 +78,10 @@ def verify(ethernet): raise ConfigError(f'Adapter does not support changing speed and duplex '\ f'settings to: {speed}/{duplex}!') + if 'disable_flow_control' in ethernet: + if not ethtool.check_flow_control(): + raise ConfigError('Adapter does not support changing flow-control settings!') + if 'ring_buffer' in ethernet: max_rx = ethtool.get_ring_buffer_max('rx') if not max_rx: diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index bd89dcdb4..0bd858760 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -104,6 +104,14 @@ for ifname in config.list_nodes(base): config.delete(speed_path) config.delete(duplex_path) + # Also while processing the interface configuration, not all adapters support + # changing disabling flow-control - or change this setting. If disabling + # flow-control is not supported by the NIC, we remove the setting from CLI + flow_control_path = base + [ifname, 'disable-flow-control'] + if config.exists(flow_control_path): + if not eth.check_flow_control(): + config.delete(flow_control_path) + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From 90031f21dc66e28f8883cb58af3f07c35b61d273 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Thu, 2 Sep 2021 11:36:38 +0000 Subject: sstp-server: T2661: Delete CA certificate redundancy check --- data/templates/accel-ppp/sstp.config.tmpl | 2 ++ src/conf_mode/vpn_sstp.py | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index 7ca7b1c1e..d48e9ab0d 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -29,7 +29,9 @@ disable verbose=1 ifname=sstp%d accept=ssl +{% if ssl.ca_cert_file is defined and ssl.ca_cert_file is not none %} ssl-ca-file={{ ssl.ca_cert_file }} +{% endif %} ssl-pemfile={{ ssl.cert_file }} ssl-keyfile={{ ssl.key_file }} diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 47367f125..11925dfa4 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -57,9 +57,7 @@ def verify(sstp): # SSL certificate checks # tmp = dict_search('ssl.ca_cert_file', sstp) - if not tmp: - raise ConfigError(f'SSL CA certificate file required!') - else: + if tmp: if not os.path.isfile(tmp): raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!') -- cgit v1.2.3 From 3834f62915830af92dd006a8606b3cce75cbb483 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 2 Sep 2021 14:05:33 +0200 Subject: op-mode: T1376: speed up tab-completion for DHCP pool listing Commit 9f20bee81c ("T1376: improve show_dhcp and show_dhcpv6") added the tab completion helper to list the availbale IP pools to query. This was done by calling a python script which then called cli-shell-api which resulted in a penalty by the Python interpreter startup. This can be solved by directly using the cli-shell-api wrapper available as in op-mode - as also seen for DHCPv6. (cherry picked from commit b1ff7baaf3c52c8c364955632fcece2da7033b10) --- op-mode-definitions/dhcp.xml.in | 4 ++-- src/op_mode/show_dhcp.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index 1dacbd5ba..6f0c25110 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -22,7 +22,7 @@ Show DHCP server leases for a specific pool - + service dhcp-server shared-network-name sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6 @@ -57,7 +57,7 @@ Show DHCP server statistics for a specific pool - + service dhcp-server shared-network-name sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6 diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index ff1e3cc56..9f65b1bd6 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -178,7 +178,7 @@ if __name__ == '__main__': group = parser.add_mutually_exclusive_group() group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") - group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") + group.add_argument("--allowed", type=str, choices=["sort", "state"], help="Show allowed values for argument") parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") @@ -189,11 +189,7 @@ if __name__ == '__main__': conf = Config() - if args.allowed == 'pool': - if conf.exists_effective('service dhcp-server'): - print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name"))) - exit(0) - elif args.allowed == 'sort': + if args.allowed == 'sort': print(' '.join(lease_display_fields.keys())) exit(0) elif args.allowed == 'state': -- cgit v1.2.3 From aa7d7beea87c37ce5717ed89c0aba4388f0c3673 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 2 Sep 2021 16:08:57 +0200 Subject: login: T3792: bugfix for usernames containing a hyphen While migrating to get_config_dict() in commit e8a1c291b1 ("login: radius: T3192: migrate to get_config_dict()") the user-name was not excluded from mangling (no_tag_node_value_mangle=True). This resulted in a username "vyos-user" from CLI to be actually created as "vyos_user" on the system. This commit also adds respective Smoketests to prevent this in the future. (cherry picked from commit 658de9ea0fbe91e593f9cf0a8c434791282af100) --- smoketest/scripts/cli/test_system_login.py | 41 +++++++++++++++++++++++++++--- src/conf_mode/system-login.py | 2 +- 2 files changed, 39 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index 8327235fb..af3a5851c 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -31,7 +31,19 @@ from vyos.util import read_file from vyos.template import inc_ip base_path = ['system', 'login'] -users = ['vyos1', 'vyos2'] +users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice'] + +ssh_pubkey = """ +AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF +TlXYfVt6Ls2NZZsUSwHbnhK4BKDrPvVZMW/LycjQPzWW6TGtk6UbZP1WqdviQ9hP +jsEeKJSTKciMSvQpjBWyEQQPXSKYQC7ryQQilZDqnJgzqwzejKEe+nhhOdBvjuZc +uukxjT69E0UmWAwLxzvfiurwiQaC7tG+PwqvtfHOPL3i6yRO2C5ORpFarx8PeGDS +IfIXJCr3LoUbLHeuE7T2KaOKQcX0UsWJ4CoCapRLpTVYPDB32BYfgq7cW1Sal1re +EGH2PzuXBklinTBgCHA87lHjpwDIAqdmvMj7SXIW9LxazLtP+e37sexE7xEs0cpN +l68txdDbY2P2Kbz5mqGFfCvBYKv9V2clM5vyWNy/Xp5TsCis89nn83KJmgFS7sMx +pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr +TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk= +""" class TestSystemLogin(VyOSUnitTestSHIM.TestCase): def tearDown(self): @@ -42,6 +54,8 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_add_linux_system_user(self): + # We are not allowed to re-use a username already taken by the Linux + # base system system_user = 'backup' self.cli_set(base_path + ['user', system_user, 'authentication', 'plaintext-password', system_user]) @@ -75,9 +89,30 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): (stdout, stderr) = proc.communicate() # stdout is something like this: - # b'Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux\n' + # b'Linux LR1.wue3 5.10.61-amd64-vyos #1 SMP Fri Aug 27 08:55:46 UTC 2021 x86_64 GNU/Linux\n' self.assertTrue(len(stdout) > 40) + def test_system_user_ssh_key(self): + ssh_user = 'ssh-test_user' + public_keys = 'vyos' + type = 'ssh-rsa' + + self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'key', ssh_pubkey.replace('\n','')]) + + # check validate() - missing type for public-key + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'type', type]) + + self.cli_commit() + + # Check that SSH key was written properly + tmp = cmd(f'sudo cat /home/{ssh_user}/.ssh/authorized_keys') + key = f'{type} ' + ssh_pubkey.replace('\n','') + self.assertIn(key, tmp) + + self.cli_delete(base_path + ['user', ssh_user]) + def test_radius_kernel_features(self): # T2886: RADIUS requires some Kernel options to be present kernel = platform.release() @@ -201,4 +236,4 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertTrue(tmp) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 59ea1d34b..78830931d 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -59,7 +59,7 @@ def get_config(config=None): conf = Config() base = ['system', 'login'] login = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) + no_tag_node_value_mangle=True, get_first_key=True) # users no longer existing in the running configuration need to be deleted local_users = get_local_users() -- cgit v1.2.3 From 8d47a10b472b595661cd97f2b0b837ebf03f3ffd Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 2 Sep 2021 14:38:58 +0000 Subject: nipsec: T3093: Delete temporarily generated code This code was generated before to rewrite IPSec to XML style And this was rewriten/fixed and used in the next 1.4 releases So we realy don't need it in 1.3 as we use old nodes for it. --- Makefile | 2 - interface-definitions/vpn_ipsec.xml.in | 1167 -------------------------------- src/conf_mode/vpn_ipsec.py | 67 -- 3 files changed, 1236 deletions(-) delete mode 100644 interface-definitions/vpn_ipsec.xml.in delete mode 100755 src/conf_mode/vpn_ipsec.py (limited to 'src') diff --git a/Makefile b/Makefile index ce7b18e65..83020d59e 100644 --- a/Makefile +++ b/Makefile @@ -45,8 +45,6 @@ interface_definitions: $(config_xml_obj) rm -f $(TMPL_DIR)/policy/node.def rm -f $(TMPL_DIR)/system/node.def rm -f $(TMPL_DIR)/vpn/node.def - rm -f $(TMPL_DIR)/vpn/ipsec/node.def - rm -rf $(TMPL_DIR)/vpn/nipsec # XXX: T3781: migrate back to old iptables NAT implementation as we can not use nft # which requires Kernel 5.10 for proper prefix translation support. Kernel 5.10 diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in deleted file mode 100644 index 426d7e71c..000000000 --- a/interface-definitions/vpn_ipsec.xml.in +++ /dev/null @@ -1,1167 +0,0 @@ - - - - - - - VPN IP security (IPsec) parameters - - - - - Set auto-update interval for IPsec daemon - - u32:30-65535 - Auto-update interval (s) - - - - - - - - - Option to disable requirement for unique IDs in the Security Database - - - - - - Name of Encapsulating Security Payload (ESP) group - - - - - ESP compression - - disable enable - - - disable - Disable ESP compression (default) - - - enable - Enable ESP compression - - - ^(disable|enable)$ - - - - - - ESP lifetime - - u32:30-86400 - ESP lifetime in seconds (default 3600) - - - - - - - - - ESP mode - - tunnel transport - - - tunnel - Tunnel mode (default) - - - transport - Transport mode - - - ^(tunnel|transport)$ - - - - - - ESP Perfect Forward Secrecy - - enable dh-group1 dh-group2 dh-group5 dh-group14 dh-group15 dh-group16 dh-group17 dh-group18 dh-group19 dh-group20 dh-group21 dh-group22 dh-group23 dh-group24 dh-group25 dh-group26 dh-group27 dh-group28 dh-group29 dh-group30 dh-group31 dh-group32 disable - - - enable - Enable PFS. Use ike-groups dh-group (default) - - - dh-group1 - Enable PFS. Use Diffie-Hellman group 1 (modp768) - - - dh-group2 - Enable PFS. Use Diffie-Hellman group 2 (modp1024) - - - dh-group5 - Enable PFS. Use Diffie-Hellman group 5 (modp1536) - - - dh-group14 - Enable PFS. Use Diffie-Hellman group 14 (modp2048) - - - dh-group15 - Enable PFS. Use Diffie-Hellman group 15 (modp3072) - - - dh-group16 - Enable PFS. Use Diffie-Hellman group 16 (modp4096) - - - dh-group17 - Enable PFS. Use Diffie-Hellman group 17 (modp6144) - - - dh-group18 - Enable PFS. Use Diffie-Hellman group 18 (modp8192) - - - dh-group19 - Enable PFS. Use Diffie-Hellman group 19 (ecp256) - - - dh-group20 - Enable PFS. Use Diffie-Hellman group 20 (ecp384) - - - dh-group21 - Enable PFS. Use Diffie-Hellman group 21 (ecp521) - - - dh-group22 - Enable PFS. Use Diffie-Hellman group 22 (modp1024s160) - - - dh-group23 - Enable PFS. Use Diffie-Hellman group 23 (modp2048s224) - - - dh-group24 - Enable PFS. Use Diffie-Hellman group 24 (modp2048s256) - - - dh-group25 - Enable PFS. Use Diffie-Hellman group 25 (ecp192) - - - dh-group26 - Enable PFS. Use Diffie-Hellman group 26 (ecp224) - - - dh-group27 - Enable PFS. Use Diffie-Hellman group 27 (ecp224bp) - - - dh-group28 - Enable PFS. Use Diffie-Hellman group 28 (ecp256bp) - - - dh-group29 - Enable PFS. Use Diffie-Hellman group 29 (ecp384bp) - - - dh-group30 - Enable PFS. Use Diffie-Hellman group 30 (ecp512bp) - - - dh-group31 - Enable PFS. Use Diffie-Hellman group 31 (curve25519) - - - dh-group32 - Enable PFS. Use Diffie-Hellman group 32 (curve448) - - - disable - Disable PFS - - - ^(enable|dh-group1|dh-group2|dh-group5|dh-group14|dh-group15|dh-group16|dh-group17|dh-group18|dh-group19|dh-group20|dh-group21|dh-group22|dh-group23|dh-group24|dh-group25|dh-group26|dh-group27|dh-group28|dh-group29|dh-group30|dh-group31|dh-group32|disable)$ - - - - - - ESP-group proposal [REQUIRED] - - u32:1-65535 - ESP-group proposal number - - - - #include - #include - - - - - - - Name of Internet Key Exchange (IKE) group - - - - - close-action_help - - none hold clear restart - - - none - Set action to none (default) - - - hold - Set action to hold - - - clear - Set action to clear - - - restart - Set action to restart - - - ^(none|hold|clear|restart)$ - - - - - - Dead Peer Detection (DPD) - - - - - Keep-alive failure action - - hold clear restart - - - hold - Set action to hold (default) - - - clear - Set action to clear - - - restart - Set action to restart - - - ^(hold|clear|restart)$ - - - - - - Keep-alive interval - - u32:2-86400 - Keep-alive interval in seconds (default 30) - - - - - - - - - Dead-Peer-Detection keep-alive timeout (IKEv1 only) - - u32:2-86400 - Keep-alive timeout in seconds (default 120) - - - - - - - - - - - ikev2-reauth_help - - yes no - - - yes - Enable remote host re-autentication during an IKE rekey. Currently broken due to a strong swan bug - - - no - Disable remote host re-authenticaton during an IKE rekey. (Default) - - - ^(yes|no)$ - - - - - - Key Exchange Version - - ikev1 ikev2 - - - ikev1 - Use IKEv1 for Key Exchange [DEFAULT] - - - ikev2 - Use IKEv2 for Key Exchange - - - ^(ikev1|ikev2)$ - - - - - - IKE lifetime - - u32:30-86400 - IKE lifetime in seconds (default 28800) - - - - - - - - - Enable MOBIKE Support. MOBIKE is only available for IKEv2. - - enable disable - - - enable - Enable MOBIKE (default for IKEv2) - - - disable - Disable MOBIKE - - - ^(enable|disable)$ - - - - - - IKEv1 Phase 1 Mode Selection - - main aggressive - - - main - Use Main mode for Key Exchanges in the IKEv1 Protocol (Recommended Default) - - - aggressive - Use Aggressive mode for Key Exchanges in the IKEv1 protocol - We do not recommend users to use aggressive mode as it is much more insecure compared to Main mode. - - - ^(main|aggressive)$ - - - - - - proposal_help - - u32:1-65535 - IKE-group proposal - - - - - - dh-grouphelp - - 1 2 5 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 - - - 1 - Diffie-Hellman group 1 (modp768) - - - 2 - Diffie-Hellman group 2 (modp1024) - - - 5 - Diffie-Hellman group 5 (modp1536) - - - 14 - Diffie-Hellman group 14 (modp2048) - - - 15 - Diffie-Hellman group 15 (modp3072) - - - 16 - Diffie-Hellman group 16 (modp4096) - - - 17 - Diffie-Hellman group 17 (modp6144) - - - 18 - Diffie-Hellman group 18 (modp8192) - - - 19 - Diffie-Hellman group 19 (ecp256) - - - 20 - Diffie-Hellman group 20 (ecp384) - - - 21 - Diffie-Hellman group 21 (ecp521) - - - 22 - Diffie-Hellman group 22 (modp1024s160) - - - 23 - Diffie-Hellman group 23 (modp2048s224) - - - 24 - Diffie-Hellman group 24 (modp2048s256) - - - 25 - Diffie-Hellman group 25 (ecp192) - - - 26 - Diffie-Hellman group 26 (ecp224) - - - 27 - Diffie-Hellman group 27 (ecp224bp) - - - 28 - Diffie-Hellman group 28 (ecp256bp) - - - 29 - Diffie-Hellman group 29 (ecp384bp) - - - 30 - Diffie-Hellman group 30 (ecp512bp) - - - 31 - Diffie-Hellman group 31 (curve25519) - - - 32 - Diffie-Hellman group 32 (curve448) - - - ^(1|2|5|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)$ - - - - #include - #include - - - - - - - Sets to include an additional configuration directive file for strongSwan. Use an absolute path to specify the included file - - - - - Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file. - - - - - Interface to use for VPN [REQUIRED] - - - - - IPsec interface [REQUIRED] - - - - - - - - - - - IPsec logging - - - - - strongSwan Logger Level - - u32:0-2 - Logger Verbosity Level (default 0) - - - - - - - - - Log mode. To see what each log mode exactly does, please refer to the strongSwan documentation - - dmn mgr ike chd job cfg knl net asn enc lib esp tls tnc imc imv pts any - - - dmn - Debug log option for strongSwan - - - mgr - Debug log option for strongSwan - - - ike - Debug log option for strongSwan - - - chd - Debug log option for strongSwan - - - job - Debug log option for strongSwan - - - cfg - Debug log option for strongSwan - - - knl - Debug log option for strongSwan - - - net - Debug log option for strongSwan - - - asn - Debug log option for strongSwan - - - enc - Debug log option for strongSwan - - - lib - Debug log option for strongSwan - - - esp - Debug log option for strongSwan - - - tls - Debug log option for strongSwan - - - tnc - Debug log option for strongSwan - - - imc - Debug log option for strongSwan - - - imv - Debug log option for strongSwan - - - pts - Debug log option for strongSwan - - - any - Debug log option for strongSwan - - - ^(dmn|mgr|ike|chd|job|cfg|knl|net|asn|enc|lib|esp|tls|tnc|imc|imv|pts|any)$ - - - - - - - - - Network Address Translation (NAT) networks - - - - - NAT networks to allow - - ipv4net - NAT networks to allow - - - - - - - - - NAT networks to exclude from allowed-networks - - ipv4net - NAT networks to exclude from allowed-networks - - - - - - - - - - - - - - Network Address Translation (NAT) traversal - - disable enable - - - disable - Disable NAT-T - - - enable - Enable NAT-T - - - ^(disable|enable)$ - - - - - - Global IPsec settings - - - - - Do not automatically install routes to remote networks - - - - - - - - VPN IPSec Profile - - - - - Authentication [REQUIRED] - - - - - Authentication mode - - - - - Use pre-shared secret key - - - - - - - - Pre-shared secret key - - txt - Pre-shared secret key - - - - - - - - DMVPN crypto configuration - - - - - bind_child_help - - - - - - - - Esp group name [REQUIRED] - - vpn ipsec esp-group - - - - - - Ike group name [REQUIRED] - - vpn ipsec ike-group - - - - - - - - Site to site VPN - - - - - VPN peer - - ipv4 - IPv4 address of the peer - - - ipv6 - IPv6 address of the peer - - - txt - Hostname of the peer - - - <@text> - ID of the peer - - - - - - Peer authentication [REQUIRED] - - - - - ID for peer authentication - - txt - ID used for peer authentication - - - - - - Authentication mode - - pre-shared-secret rsa x509 - - - pre-shared-secret - pre-shared-secret_description - - - rsa - rsa_description - - - x509 - x509_description - - - ^(pre-shared-secret|rsa|x509)$ - - - - - - Pre-shared secret key - - txt - Pre-shared secret key - - - - - - ID for remote authentication - - txt - ID used for peer authentication - - - - - - RSA key name - - - - - Use certificate common name as ID - - - - - - X.509 certificate - - - #include - #include - - - File containing the X.509 Certificate Revocation List (CRL) - - txt - File in /config/auth - - - - - - Key file and password to open it - - - - - File containing the private key for the X.509 certificate for this host - - txt - File in /config/auth - - - - - - Password that protects the private key - - txt - Password that protects the private key - - - - - - - - - - - - Connection type - - initiate respond - - - initiate - initiate_description - - - respond - respond_description - - - ^(initiate|respond)$ - - - - - - Defult ESP group name - - - - - VPN peer description - - - - - - DHCP interface to listen on - - - - - - Force UDP Encapsulation for ESP Payloads - - enable disable - - - enable - This endpoint will force UDP encapsulation for this peer - - - disable - This endpoint will not force UDP encapsulation for this peer - - - ^(enable|disable)$ - - - - - - Internet Key Exchange (IKE) group name [REQUIRED] - - vpn ipsec ike-group - - - - - - Re-authentication of the remote peer during an IKE re-key. IKEv2 option only - - yes no inherit - - - yes - Enable remote host re-autentication during an IKE re-key. Currently broken due to a strong swan bug - - - no - Disable remote host re-authenticaton during an IKE re-key. - - - inherit - Inherit the reauth configuration form your IKE-group (Default) - - - ^(yes|no|inherit)$ - - - - - - IPv4 or IPv6 address of a local interface to use for VPN - - any - - - ipv4 - IPv4 address of a local interface for VPN - - - ipv6 - IPv6 address of a local interface for VPN - - - any - Allow any IPv4 address present on the system to be used for VPN - - - - - ^(any)$ - - - - - - Peer tunnel [REQUIRED] - - u32 - Peer tunnel [REQUIRED] - - - - - - Option to allow NAT networks - - enable disable - - - enable - Enable NAT networks - - - disable - Disable NAT networks (default) - - - ^(enable|disable)$ - - - - - - Option to allow public networks - - enable disable - - - enable - Enable public networks - - - disable - Disable public networks (default) - - - ^(enable|disable)$ - - - - #include - - - ESP group name - - vpn ipsec esp-group - - - - - - Local parameters for interesting traffic - - - - - Any TCP or UDP port - - port name - Named port (any name in /etc/services, e.g., http) - - - u32:1-65535 - Numbered port - - - - - - Local IPv4 or IPv6 prefix - - ipv4 - Local IPv4 prefix - - - ipv6 - Local IPv6 prefix - - - - - - - - - - - - Protocol to encrypt - - - - - - Remote parameters for interesting traffic - - - - - Any TCP or UDP port - - port name - Named port (any name in /etc/services, e.g., http) - - - u32:1-65535 - Numbered port - - - - - - Remote IPv4 or IPv6 prefix - - ipv4 - Remote IPv4 prefix - - - ipv6 - Remote IPv6 prefix - - - - - - - - - - - - - - Virtual tunnel interface [REQUIRED] - - - - - VTI tunnel interface associated with this configuration [REQUIRED] - - - - - ESP group name [REQUIRED] - - vpn ipsec esp-group - - - - - - - - - - - - - - diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py deleted file mode 100755 index 969266c30..000000000 --- a/src/conf_mode/vpn_ipsec.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 os - -from sys import exit - -from vyos.config import Config -from vyos.template import render -from vyos.util import call -from vyos.util import dict_search -from vyos import ConfigError -from vyos import airbag -from pprint import pprint -airbag.enable() - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['vpn', 'nipsec'] - if not conf.exists(base): - return None - - # retrieve common dictionary keys - ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - return ipsec - -def verify(ipsec): - if not ipsec: - return None - -def generate(ipsec): - if not ipsec: - return None - - return ipsec - -def apply(ipsec): - if not ipsec: - return None - - pprint(ipsec) - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) -- cgit v1.2.3 From 4a8bf1ee1e1e5b6fee1850b5cb82085c0edd75ab Mon Sep 17 00:00:00 2001 From: Brandon Stepler Date: Thu, 29 Jul 2021 14:30:00 -0400 Subject: configd: T3694: always set script.argv Several scripts imported by vyos-configd (including src/conf_mode/protocols_static.py) rely on argv for operating on VRFs. Always setting script.argv in src/services/vyos-configd ensures those scripts will operate on the default VRF when called with no arguments. Otherwise, a stale argv might cause those scripts to operate on the last modified VRF instead of the default VRF. (cherry picked from commit 3341c591ad1190f39ff3ffd475eddf5d95aef763) --- src/services/vyos-configd | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 6f770b696..670b6e66a 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -133,8 +133,7 @@ def explicit_print(path, mode, msg): logger.critical("error explicit_print") def run_script(script, config, args) -> int: - if args: - script.argv = args + script.argv = args config.set_level([]) try: c = script.get_config(config) @@ -208,7 +207,7 @@ def process_node_data(config, data) -> int: return R_ERROR_DAEMON script_name = None - args = None + args = [] res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data) if res.group(1): @@ -221,7 +220,7 @@ def process_node_data(config, data) -> int: return R_ERROR_DAEMON if res.group(3): args = res.group(3).split() - args.insert(0, f'{script_name}.py') + args.insert(0, f'{script_name}.py') if script_name not in include_set: return R_PASS -- cgit v1.2.3 From a654886f23aada50b4f1a951c7c45a98f962341c Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 2 Sep 2021 18:58:11 +0000 Subject: tunnel: T3788: Add check keys for ipip and sit Keys are not allowed with ipip and sit tunnels (cherry picked from commit 7e84566dedfdc532ffe05b404005daa6f21df567) --- src/conf_mode/interfaces-tunnel.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index a7207f94e..ccc4bad3d 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -80,6 +80,11 @@ def verify(tunnel): dict_search('parameters.ip.key', tunnel) == None: raise ConfigError('Tunnel parameters ip key must be set!') + # Keys are not allowed with ipip and sit tunnels + if tunnel['encapsulation'] in ['ipip', 'sit']: + if dict_search('parameters.ip.key', tunnel) != None: + raise ConfigError('Keys are not allowed with ipip and sit tunnels!') + verify_mtu_ipv6(tunnel) verify_address(tunnel) verify_vrf(tunnel) -- cgit v1.2.3 From 537cd313d0076008e66fde728576ad328bfc0dcc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 4 Sep 2021 11:38:19 +0200 Subject: op-mode: import cleanup in "show interfaces" script (cherry picked from commit 5bde11aceffd3d7fca99e582b16555fc0c584410) --- src/op_mode/show_interfaces.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 79bb8e2a6..bfb5d68a2 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2017, 2019 VyOS maintainers and contributors +# Copyright 2017-2021 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 @@ -19,9 +19,7 @@ import os import re import sys import glob -import datetime import argparse -import netifaces from vyos.ifconfig import Section from vyos.ifconfig import Interface -- cgit v1.2.3 From 5548dee213dc14b83322bfdcf32d089f5cb169eb Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 4 Sep 2021 11:39:01 +0200 Subject: op-mode: T3619: bugfix "show interfaces" for VLANs Commit 31169fa8a7 ("vyos.ifconfig: T3619: only set offloading options if supported by NIC") always instantiated an object of the Ethtool class for an ethernet object - this is right as a real ethernet interface is managed by Ethtool. Unfortunately the script used for "show interface" determindes the "base class" for an interface by its name, so eth0 -> Ethernet, eth0.10 -> Ethernet. This assumption is incorrect as a VLAN interface can not have the physical parameters changed of its underlaying interface. This can only be done for eth0. There is no need for the op-mode script to determine the implementation class for an interface at this level, as we are only interested in the state of the interface and it's IP addresses - which is a common operation valid for every interface on VyOS. (cherry picked from commit 27e53fbcd843c3aad27db9e97f9060ae6dfcc5ee) --- src/op_mode/show_interfaces.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index bfb5d68a2..17b52b5df 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -60,10 +60,9 @@ def filtered_interfaces(ifnames, iftypes, vif, vrrp): if ifnames and ifname not in ifnames: continue - # return the class which can handle this interface name - klass = Section.klass(ifname) - # connect to the interface - interface = klass(ifname, create=False, debug=False) + # As we are only "reading" from the interface - we must use the + # generic base class which exposes all the data via a common API + interface = Interface(ifname, create=False, debug=False) if iftypes and interface.definition['section'] not in iftypes: continue -- cgit v1.2.3 From 4a2700aba8108bd7ef60821872ae1433b518a6d9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 4 Sep 2021 12:37:23 +0200 Subject: op-mode: T3619: bugfix "show interfaces X detail" Commit 27e53fbc ("op-mode: T3619: bugfix "show interfaces" for VLANs") fixed the op-mode command for the "show interfaces" operation, but if a user was interested in all the ethernet or bridge interfaces, the command "show interfaces detail" did not yield any output. The filtered_interfaces() function was further generalized to only operate on base components and call itself recusively if required. (cherry picked from commit 5e1f76d16332a917bfd99c6f2bffcd73e61d934d) --- src/op_mode/show_interfaces.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 17b52b5df..aef2d8060 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -52,11 +52,12 @@ def filtered_interfaces(ifnames, iftypes, vif, vrrp): ifnames: a list of interfaces names to consider, empty do not filter return an instance of the interface class """ - allnames = Section.interfaces() + if isinstance(iftypes, list): + for iftype in iftypes: + yield from filtered_interfaces(ifnames, iftype, vif, vrrp) - vrrp_interfaces = VRRP.active_interfaces() if vrrp else [] - - for ifname in allnames: + for ifname in Section.interfaces(iftypes): + # Bail out early if interface name not part of our search list if ifnames and ifname not in ifnames: continue @@ -64,14 +65,14 @@ def filtered_interfaces(ifnames, iftypes, vif, vrrp): # generic base class which exposes all the data via a common API interface = Interface(ifname, create=False, debug=False) - if iftypes and interface.definition['section'] not in iftypes: - continue - + # VLAN interfaces have a '.' in their name by convention if vif and not '.' in ifname: continue - if vrrp and ifname not in vrrp_interfaces: - continue + if vrrp: + vrrp_interfaces = VRRP.active_interfaces() + if ifname not in vrrp_interfaces: + continue yield interface -- cgit v1.2.3 From 30ca5a07498693d820b3728951a184e02cfa61f9 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sat, 4 Sep 2021 13:23:55 +0700 Subject: T3697: do not try to restart charon if it's not required The root cause is that the ipsec-settings.py script is run _twice_: first from "vpn ipsec options", then from the top level "vpn" node. The case when it's not required is when: * "vpn ipsec" configuration doesn't exist yet * user configured it with "vpn ipsec options" * the ipsec-settings.py script is run first time, from "vpn ipsec options" Trying to restart charon at that stage leads to a deadlock. --- interface-definitions/ipsec-settings.xml.in | 2 +- src/conf_mode/ipsec-settings.py | 39 ++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/interface-definitions/ipsec-settings.xml.in b/interface-definitions/ipsec-settings.xml.in index bc54baa27..dbf6625fb 100644 --- a/interface-definitions/ipsec-settings.xml.in +++ b/interface-definitions/ipsec-settings.xml.in @@ -4,7 +4,7 @@ - + Global IPsec settings diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 7ca2d9b44..771f635a0 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -18,7 +18,9 @@ import re import os from time import sleep -from sys import exit + +# Top level import so that configd can override it +from sys import argv from vyos.config import Config from vyos import ConfigError @@ -216,6 +218,20 @@ def generate(data): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) +def is_charon_responsive(): + # Check if charon responds to strokes + # + # Sometimes it takes time to fully initialize, + # so waiting for the process to come to live isn't always enough + # + # There's no official "no-op" stroke so we use the "memusage" stroke as a substitute + from os import system + res = system("ipsec stroke memusage >&/dev/null") + if res == 0: + return True + else: + return False + def restart_ipsec(): try: # Restart the IPsec daemon when it's running. @@ -223,17 +239,28 @@ def restart_ipsec(): # there's a chance that this script will run before charon is up, # so we can't assume it's running and have to check and wait if needed. - # First, wait for charon to get started by the old ipsec.pl script. + # But before everything else, there's a catch! + # This script is run from _two_ places: "vpn ipsec options" and the top level "vpn" node + # When IPsec isn't set up yet, and a user wants to commit an IPsec config with some + # "vpn ipsec settings", this script will first be called before StrongSWAN is started by vpn-config.pl! + # Thus if this script is run from "settings" _and_ charon is unresponsive, + # we shouldn't wait for it, else there will be a deadlock. + # We indicate that by running the script under vyshim from "vpn ipsec options" (which sets a variable named "argv") + # and running it without configd from "vpn ipsec" + if "from-options" in argv: + if not is_charon_responsive(): + return + + # If we got this far, then we actually need to restart StrongSWAN + + # First, wait for charon to get started by the old vpn-config.pl script. from time import sleep, time from os import system now = time() while True: if (time() - now) > 60: raise OSError("Timeout waiting for the IPsec process to become responsive") - # There's no oficial "no-op" stroke, - # so we use memusage to check if charon is alive and responsive - res = system("ipsec stroke memusage >&/dev/null") - if res == 0: + if is_charon_responsive(): break sleep(5) -- cgit v1.2.3 From 2ecf7a9f9cbe9359457bd23b4a0c45f3763123c7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 5 Sep 2021 17:56:28 +0200 Subject: name-server: T3804: merge "system name-servers-dhcp" into "system name-server" We have "set system name-server " to specify a name-server IP address we wan't to use. We also have "set system name-servers-dhcp " which does the same, but the name-server in question is retrieved via DHCP. Both CLI nodes are combined under "set system name-server " to keep things as they are in real life - we need a name-server. --- interface-definitions/dns-domain-name.xml.in | 25 +++++++------- src/conf_mode/host_name.py | 49 ++++++++++++++++------------ src/migration-scripts/system/20-to-21 | 48 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 34 deletions(-) create mode 100755 src/migration-scripts/system/20-to-21 (limited to 'src') diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index ff632e1d1..2b1644609 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -1,37 +1,34 @@ - - Domain Name Servers (DNS) used by the system (resolv.conf) + System Domain Name Servers (DNS) 400 + + + ipv4 - Domain Name Server (DNS) address + Domain Name Server IPv4 address ipv6 - Domain Name Server (DNS) address + Domain Name Server IPv6 address + + + txt + Use Domain Name Server from DHCP interface + - - - Interfaces whose DHCP client nameservers will be used by the system (resolv.conf) - 400 - - - - - - System host name (default: vyos) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index f4c75c257..a7135911d 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -14,10 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -conf-mode script for 'system host-name' and 'system domain-name'. -""" - import re import sys import copy @@ -25,10 +21,13 @@ import copy import vyos.util import vyos.hostsd_client -from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, call, process_named_running - +from vyos.config import Config +from vyos.ifconfig import Section +from vyos.template import is_ip +from vyos.util import cmd +from vyos.util import call +from vyos.util import process_named_running from vyos import airbag airbag.enable() @@ -37,7 +36,7 @@ default_config_data = { 'domain_name': '', 'domain_search': [], 'nameserver': [], - 'nameservers_dhcp_interfaces': [], + 'nameservers_dhcp_interfaces': {}, 'static_host_mapping': {} } @@ -51,29 +50,37 @@ def get_config(config=None): hosts = copy.deepcopy(default_config_data) - hosts['hostname'] = conf.return_value("system host-name") + hosts['hostname'] = conf.return_value(['system', 'host-name']) # This may happen if the config is not loaded yet, # e.g. if run by cloud-init if not hosts['hostname']: hosts['hostname'] = default_config_data['hostname'] - if conf.exists("system domain-name"): - hosts['domain_name'] = conf.return_value("system domain-name") + if conf.exists(['system', 'domain-name']): + hosts['domain_name'] = conf.return_value(['system', 'domain-name']) hosts['domain_search'].append(hosts['domain_name']) - for search in conf.return_values("system domain-search domain"): + for search in conf.return_values(['system', 'domain-search', 'domain']): hosts['domain_search'].append(search) - hosts['nameserver'] = conf.return_values("system name-server") + if conf.exists(['system', 'name-server']): + for ns in conf.return_values(['system', 'name-server']): + if is_ip(ns): + hosts['nameserver'].append(ns) + else: + tmp = '' + if_type = Section.section(ns) + if conf.exists(['interfaces', if_type, ns, 'address']): + tmp = conf.return_values(['interfaces', if_type, ns, 'address']) - hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp") + hosts['nameservers_dhcp_interfaces'].update({ ns : tmp }) # system static-host-mapping - for hn in conf.list_nodes('system static-host-mapping host-name'): + for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']): hosts['static_host_mapping'][hn] = {} - hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet') - hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias') + hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet']) + hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias']) return hosts @@ -103,8 +110,10 @@ def verify(hosts): if not hostname_regex.match(a) and len(a) != 0: raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"') - # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't - # exist or doesn't have address dhcp(v6) + for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items(): + # Warnin user if interface does not have DHCP or DHCPv6 configured + if not set(interface_config).intersection(['dhcp', 'dhcpv6']): + print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!') return None diff --git a/src/migration-scripts/system/20-to-21 b/src/migration-scripts/system/20-to-21 new file mode 100755 index 000000000..1728995de --- /dev/null +++ b/src/migration-scripts/system/20-to-21 @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 . + +# T3795: merge "system name-servers-dhcp" into "system name-server" + +import os + +from sys import argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['system', 'name-servers-dhcp'] +config = ConfigTree(config_file) +if not config.exists(base): + # Nothing to do + exit(0) + +for interface in config.return_values(base): + config.set(['system', 'name-server'], value=interface, replace=False) + +config.delete(base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3 From c14bb9ab38112837c3e1e0cafcb3b8f19cfba1c0 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 6 Sep 2021 08:23:24 +0200 Subject: wwan: T3620: op-mode: not all commands supported by all modems - add info message (cherry picked from commit 10814c4d3360598262e991e4b20768dfcde91d75) --- op-mode-definitions/show-interfaces-wwan.xml.in | 4 ++-- src/op_mode/show_wwan.py | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/op-mode-definitions/show-interfaces-wwan.xml.in b/op-mode-definitions/show-interfaces-wwan.xml.in index d57e17a13..7e5f49ba6 100644 --- a/op-mode-definitions/show-interfaces-wwan.xml.in +++ b/op-mode-definitions/show-interfaces-wwan.xml.in @@ -68,9 +68,9 @@ sudo ${vyos_op_scripts_dir}/show_wwan.py --interface=$4 --sim - + - Show WWAN module information summary + Show WWAN module detailed information summary mmcli --modem ${4#wwan} diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py index 249dda2a5..529b5bd0f 100755 --- a/src/op_mode/show_wwan.py +++ b/src/op_mode/show_wwan.py @@ -34,13 +34,17 @@ required = parser.add_argument_group('Required arguments') required.add_argument("--interface", help="WWAN interface name, e.g. wwan0", required=True) def qmi_cmd(device, command, silent=False): - tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}') - tmp = tmp.replace(f'[{cdc}] ', '') - if not silent: - # skip first line as this only holds the info headline - for line in tmp.splitlines()[1:]: - print(line.lstrip()) - return tmp + try: + tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}') + tmp = tmp.replace(f'[{cdc}] ', '') + if not silent: + # skip first line as this only holds the info headline + for line in tmp.splitlines()[1:]: + print(line.lstrip()) + return tmp + except: + print('Command not supported by Modem') + exit(1) if __name__ == '__main__': args = parser.parse_args() -- cgit v1.2.3 From 591eee82296b69b9d8ed49ca28683d0f016c85b8 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 5 Sep 2021 04:43:29 -0500 Subject: T3803: add source-address option to the op mode ping CLI. (cherry picked from commit e211cdbb375dba13af33d6ad6c3addab707f2870) --- src/op_mode/ping.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index 2144ab53c..60bbc0c78 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -62,8 +62,8 @@ options = { }, 'interface': { 'ping': '{command} -I {value}', - 'type': ' ', - 'help': 'Interface to use as source for ping' + 'type': '', + 'help': 'Source interface' }, 'interval': { 'ping': '{command} -i {value}', @@ -115,6 +115,10 @@ options = { 'type': '', 'help': 'Number of bytes to send' }, + 'source-address': { + 'ping': '{command} -I {value}', + 'type': ' ', + }, 'ttl': { 'ping': '{command} -t {value}', 'type': '', @@ -234,4 +238,4 @@ if __name__ == '__main__': # print(f'{command} {host}') os.system(f'{command} {host}') - \ No newline at end of file + -- cgit v1.2.3 From a5093308eab24ae4f746f52ec8c283a93a1654f9 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 6 Sep 2021 18:06:14 +0000 Subject: tunnel: T2920: Add checks tun with same source addr and keys 2 tunnels with the same local-address should has different keys Check existing tunnels (source-address key) with new tunnel. --- src/conf_mode/interfaces-tunnel.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index ccc4bad3d..22a9f0e18 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -18,6 +18,7 @@ import os from sys import exit from netifaces import interfaces +from ipaddress import IPv4Address from vyos.config import Config from vyos.configdict import dict_merge @@ -31,6 +32,7 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vrf from vyos.configverify import verify_tunnel from vyos.ifconfig import Interface +from vyos.ifconfig import Section from vyos.ifconfig import TunnelIf from vyos.template import is_ipv4 from vyos.template import is_ipv6 @@ -80,6 +82,27 @@ def verify(tunnel): dict_search('parameters.ip.key', tunnel) == None: raise ConfigError('Tunnel parameters ip key must be set!') + if tunnel['encapsulation'] in ['gre', 'gretap']: + if dict_search('parameters.ip.key', tunnel) != None: + # Check pairs tunnel source-address/encapsulation/key with exists tunnels. + # Prevent the same key for 2 tunnels with same source-address/encap. T2920 + for tunnel_if in Section.interfaces('tunnel'): + tunnel_cfg = get_interface_config(tunnel_if) + exist_encap = tunnel_cfg['linkinfo']['info_kind'] + exist_source_address = tunnel_cfg['address'] + exist_key = tunnel_cfg['linkinfo']['info_data']['ikey'] + new_source_address = tunnel['source_address'] + # Convert tunnel key to ip key, format "ip -j link show" + # 1 => 0.0.0.1, 999 => 0.0.3.231 + orig_new_key = int(tunnel['parameters']['ip']['key']) + new_key = IPv4Address(orig_new_key) + new_key = str(new_key) + if tunnel['encapsulation'] == exist_encap and \ + new_source_address == exist_source_address and \ + new_key == exist_key: + raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ + f'is already used for tunnel "{tunnel_if}"!') + # Keys are not allowed with ipip and sit tunnels if tunnel['encapsulation'] in ['ipip', 'sit']: if dict_search('parameters.ip.key', tunnel) != None: -- cgit v1.2.3 From c6039b9a82fe8a1752dc82a9834faf3a85b5dd38 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 6 Sep 2021 21:17:42 +0200 Subject: ifconfig: T3806: "ipv6 address no_default_link_local" required for MTU < 1280 This commit also extends the smoketest to verify that the exception for this error is raised. (cherry picked from commit 84a429b41175b95634ec9492e0cf3a564a47abdd) --- python/vyos/configverify.py | 24 ++++++++++++------------ smoketest/scripts/cli/base_interfaces_test.py | 10 +++++++++- src/conf_mode/interfaces-ethernet.py | 15 +++++++-------- 3 files changed, 28 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index cff673a6e..ce7e76eb4 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -67,22 +67,22 @@ def verify_mtu_ipv6(config): min_mtu = 1280 if int(config['mtu']) < min_mtu: interface = config['ifname'] - error_msg = f'IPv6 address will be configured on interface "{interface}" ' \ - f'thus the minimum MTU requirement is {min_mtu}!' + error_msg = f'IPv6 address will be configured on interface "{interface}",\n' \ + f'the required minimum MTU is {min_mtu}!' - for address in (dict_search('address', config) or []): - if address in ['dhcpv6'] or is_ipv6(address): - raise ConfigError(error_msg) + if 'address' in config: + for address in config['address']: + if address in ['dhcpv6'] or is_ipv6(address): + raise ConfigError(error_msg) - tmp = dict_search('ipv6.address', config) - if tmp and 'no_default_link_local' not in tmp: - raise ConfigError('link-local ' + error_msg) + tmp = dict_search('ipv6.address.no_default_link_local', config) + if tmp == None: raise ConfigError('link-local ' + error_msg) - if tmp and 'autoconf' in tmp: - raise ConfigError(error_msg) + tmp = dict_search('ipv6.address.autoconf', config) + if tmp != None: raise ConfigError(error_msg) - if tmp and 'eui64' in tmp: - raise ConfigError(error_msg) + tmp = dict_search('ipv6.address.eui64', config) + if tmp != None: raise ConfigError(error_msg) def verify_tunnel(config): """ diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 947162889..4acde99d3 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -246,11 +246,19 @@ class BasicInterfaceTest: for intf in self._interfaces: base = self._base_path + [intf] self.cli_set(base + ['mtu', self._mtu]) - self.cli_set(base + ['ipv6', 'address', 'no-default-link-local']) for option in self._options.get(intf, []): self.cli_set(base + option.split()) + # check validate() - can not set low MTU if 'no-default-link-local' + # is not set on CLI + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + for intf in self._interfaces: + base = self._base_path + [intf] + self.cli_set(base + ['ipv6', 'address', 'no-default-link-local']) + # commit interface changes self.cli_commit() diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 6e0d8c4e8..17f58b285 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -62,6 +62,13 @@ def verify(ethernet): ifname = ethernet['ifname'] verify_interface_exists(ifname) + verify_mtu(ethernet) + verify_mtu_ipv6(ethernet) + verify_dhcpv6(ethernet) + verify_address(ethernet) + verify_vrf(ethernet) + verify_eapol(ethernet) + verify_mirror(ethernet) ethtool = Ethtool(ifname) # No need to check speed and duplex keys as both have default values. @@ -101,14 +108,6 @@ def verify(ethernet): raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\ f'size of "{max_tx}" bytes!') - verify_mtu(ethernet) - verify_mtu_ipv6(ethernet) - verify_dhcpv6(ethernet) - verify_address(ethernet) - verify_vrf(ethernet) - verify_eapol(ethernet) - verify_mirror(ethernet) - # verify offloading capabilities if dict_search('offload.rps', ethernet) != None: if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'): -- cgit v1.2.3 From d9f20383323a9dbebcef4d4393f692dff716700c Mon Sep 17 00:00:00 2001 From: Paul Lettington Date: Fri, 3 Sep 2021 23:39:22 +0100 Subject: login: T971 allow quoting in public-keys options This patch allows the use of `"` in ssh public-key options which unlocks the ability to set the `from` option in a way that sshd will accept to limit what hosts a user can connect from. (cherry picked from commit 6b52387190f8213e7e02060e894c6ddd4fb7cb3d) --- src/conf_mode/system-login.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 78830931d..8aa43dd32 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -246,7 +246,9 @@ def apply(login): # XXX: Should we deny using root at all? home_dir = getpwnam(user).pw_dir render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl', - user_config, permission=0o600, user=user, group='users') + user_config, permission=0o600, + formater=lambda _: _.replace(""", '"'), + user=user, group='users') except Exception as e: raise ConfigError(f'Adding user "{user}" raised exception: "{e}"') -- cgit v1.2.3 From 451a7d6d97ee48d715e410617bdbb7149537c41a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 8 Sep 2021 14:34:41 +0200 Subject: openvpn: T3805: use vyos.util.makedir() to create system directories (cherry picked from commit 84e912ab2f583864e637c2df137f62f3d4cbeb14) --- src/conf_mode/interfaces-openvpn.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 0a420f7bf..8da299914 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -40,6 +40,7 @@ from vyos.util import call from vyos.util import chown from vyos.util import chmod_600 from vyos.util import dict_search +from vyos.util import makedir from vyos.validate import is_addr_assigned from vyos import ConfigError @@ -425,6 +426,10 @@ def verify(openvpn): def generate(openvpn): interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) + # create base config directory on demand + makedir(directory, user, group) + # enforce proper permissions on /run/openvpn + chown(directory, user, group) # we can't know in advance which clients have been removed, # thus all client configs will be removed and re-added on demand @@ -436,9 +441,7 @@ def generate(openvpn): return None # create client config directory on demand - if not os.path.exists(ccd_dir): - os.makedirs(ccd_dir, 0o755) - chown(ccd_dir, user, group) + makedir(ccd_dir, user, group) # Fix file permissons for keys fix_permissions = [] -- cgit v1.2.3 From c593bf7f597735b4b95c3923bb6ea6fc2c2ae346 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 8 Sep 2021 14:35:20 +0200 Subject: openvpn: T3805: drop privileges using systemd - required for rtnetlink (cherry picked from commit 2647edc30f1e02840cae62fde8b44345d35ac720) --- data/templates/openvpn/server.conf.tmpl | 2 -- src/conf_mode/interfaces-openvpn.py | 3 --- src/etc/systemd/system/openvpn@.service.d/override.conf | 4 ++++ 3 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index b2d0716c2..50bb49134 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -7,8 +7,6 @@ # verb 3 -user {{ daemon_user }} -group {{ daemon_group }} dev-type {{ device_type }} dev {{ ifname }} persist-key diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 8da299914..c3620d690 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -80,9 +80,6 @@ def get_config(config=None): openvpn = get_interface_dict(conf, base) openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) - openvpn['daemon_user'] = user - openvpn['daemon_group'] = group - return openvpn def verify(openvpn): diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf index 7946484a3..03fe6b587 100644 --- a/src/etc/systemd/system/openvpn@.service.d/override.conf +++ b/src/etc/systemd/system/openvpn@.service.d/override.conf @@ -7,3 +7,7 @@ WorkingDirectory= WorkingDirectory=/run/openvpn ExecStart= ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +User=openvpn +Group=openvpn +AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE +CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE -- cgit v1.2.3 From 07840977834816b69fa3b366817d90f44b5dc7a7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 10 Sep 2021 16:46:55 +0200 Subject: ethernet: T3802: use only one implementation for get_driver_name() Move the two implementations to get the driver name of a NIC from ethernet.py and ethtool.py to only ethtool.py. --- python/vyos/ethtool.py | 13 ++++++++----- python/vyos/ifconfig/ethernet.py | 28 +++------------------------- src/conf_mode/interfaces-ethernet.py | 2 +- 3 files changed, 12 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index 7e46969cf..4efc3a234 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -134,6 +134,12 @@ class Ethtool: # ['Autonegotiate:', 'on'] self._flow_control_enabled = out.splitlines()[1].split()[-1] + def get_auto_negotiation(self): + return self._auto_negotiation + + def get_driver_name(self): + return self._driver_name + def _get_generic(self, feature): """ Generic method to read self._features and return a tuple for feature @@ -189,7 +195,7 @@ class Ethtool: if duplex not in ['full', 'half']: raise ValueError(f'Value "{duplex}" for duplex is invalid!') - if self._driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']: + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: return False if speed in self._speed_duplex: @@ -199,7 +205,7 @@ class Ethtool: def check_flow_control(self): """ Check if the NIC supports flow-control """ - if self._driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']: + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: return False return self._flow_control @@ -208,6 +214,3 @@ class Ethtool: raise ValueError('Interface does not support changing '\ 'flow-control settings!') return self._flow_control_enabled - - def get_auto_negotiation(self): - return self._auto_negotiation diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 47d3b6b4d..50e865203 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -81,25 +81,6 @@ class EthernetIf(Interface): super().__init__(ifname, **kargs) self.ethtool = Ethtool(ifname) - def get_driver_name(self): - """ - Return the driver name used by NIC. Some NICs don't support all - features e.g. changing link-speed, duplex - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.get_driver_name() - 'vmxnet3' - """ - ifname = self.config['ifname'] - sysfs_file = f'/sys/class/net/{ifname}/device/driver/module' - if os.path.exists(sysfs_file): - link = os.readlink(sysfs_file) - return os.path.basename(link) - else: - return None - def set_flow_control(self, enable): """ Changes the pause parameters of the specified Ethernet device. @@ -117,8 +98,7 @@ class EthernetIf(Interface): raise ValueError("Value out of range") if not self.ethtool.check_flow_control(): - self._debug_msg(f'{driver_name} driver does not support changing '\ - 'flow control settings!') + self._debug_msg(f'NIC driver does not support changing flow control settings!') return False current = self.ethtool.get_flow_control() @@ -152,10 +132,8 @@ class EthernetIf(Interface): if duplex not in ['auto', 'full', 'half']: raise ValueError("Value out of range (duplex)") - driver_name = self.get_driver_name() - if driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']: - self._debug_msg(f'{driver_name} driver does not support changing '\ - 'speed/duplex settings!') + if not self.ethtool.check_speed_duplex(speed, duplex): + self._debug_msg(f'NIC driver does not support changing speed/duplex settings!') return # Get current speed and duplex settings: diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 17f58b285..de851262b 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -113,7 +113,7 @@ def verify(ethernet): if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'): raise ConfigError('Interface does not suport RPS!') - driver = EthernetIf(ifname).get_driver_name() + driver = ethtool.get_driver_name() # T3342 - Xen driver requires special treatment if driver == 'vif': if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None: -- cgit v1.2.3 From f9d56f2feaf64f078ee019ecfbe470ddefcfe064 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 10 Sep 2021 22:50:27 +0200 Subject: frr: T1514: refactor restart script and drop duplicated code (cherry picked from commit d39567c977c84f1c16998947e16d397edbb015be) --- src/op_mode/restart_frr.py | 131 ++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 0b2322478..109c8dd7b 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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,16 +13,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# -import sys +import os import argparse import logging -from logging.handlers import SysLogHandler -from pathlib import Path import psutil +from logging.handlers import SysLogHandler +from shutil import rmtree + from vyos.util import call +from vyos.util import ask_yes_no +from vyos.util import process_named_running +from vyos.util import makedir # some default values watchfrr = '/usr/lib/frr/watchfrr.sh' @@ -40,40 +43,45 @@ logger.setLevel(logging.INFO) def _check_safety(): try: # print warning - answer = input("WARNING: This is a potentially unsafe function! You may lose the connection to the router or active configuration after running this command. Use it at your own risk! Continue? [y/N]: ") - if not answer.lower() == "y": - logger.error("User aborted command") + if not ask_yes_no('WARNING: This is a potentially unsafe function!\n' \ + 'You may lose the connection to the router or active configuration after\n' \ + 'running this command. Use it at your own risk!\n\n' + 'Continue?'): return False # check if another restart process already running if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1: - logger.error("Another restart_frr.py already running") - answer = input("Another restart_frr.py process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ") - if not answer.lower() == "y": + message = 'Another restart_frr.py process is already running!' + logger.error(message) + if not ask_yes_no(f'\n{message} It is unsafe to continue.\n\n' \ + 'Do you want to process anyway?'): return False # check if watchfrr.sh is running - for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']): - if 'bash' in process.info['name'] and watchfrr in process.info['cmdline']: - logger.error("Another {} already running".format(watchfrr)) - answer = input("Another {} process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(watchfrr)) - if not answer.lower() == "y": - return False + tmp = os.path.basename(watchfrr) + if process_named_running(tmp): + message = f'Another {tmp} process is already running.' + logger.error(message) + if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ + 'Do you want to process anyway?'): + return False # check if vtysh is running - for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']): - if 'vtysh' in process.info['name']: - logger.error("The vtysh is running by another task") - answer = input("The vtysh is running by another task. It is unsafe to continue. Do you want to process anyway? [y/N]: ") - if not answer.lower() == "y": - return False + if process_named_running('vtysh'): + message = 'vtysh process is executed by another task.' + logger.error(message) + if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ + 'Do you want to process anyway?'): + return False # check if temporary directory exists - if Path(frrconfig_tmp).exists(): - logger.error("The temporary directory \"{}\" already exists".format(frrconfig_tmp)) - answer = input("The temporary directory \"{}\" already exists. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(frrconfig_tmp)) - if not answer.lower() == "y": + if os.path.exists(frrconfig_tmp): + message = f'Temporary directory "{frrconfig_tmp}" already exists!' + logger.error(message) + if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ + 'Do you want to process anyway?'): return False + except: logger.error("Something goes wrong in _check_safety()") return False @@ -84,72 +92,47 @@ def _check_safety(): # write active config to file def _write_config(): # create temporary directory - Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True) + makedir(frrconfig_tmp) # save frr.conf to it - command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) + command = f'{vtysh} -n -w --config_dir {frrconfig_tmp} 2> /dev/null' return_code = call(command) - if not return_code == 0: - logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code)) + if return_code != 0: + logger.error(f'Failed to save active config: "{command}" returned exit code: {return_code}') return False - logger.info("Active config saved to {}".format(frrconfig_tmp)) + logger.info(f'Active config saved to {frrconfig_tmp}') return True # clear and remove temporary directory def _cleanup(): - tmpdir = Path(frrconfig_tmp) - try: - if tmpdir.exists(): - for file in tmpdir.iterdir(): - file.unlink() - tmpdir.rmdir() - except: - logger.error("Failed to remove temporary directory {}".format(frrconfig_tmp)) - print("Failed to remove temporary directory {}".format(frrconfig_tmp)) - -# check if daemon is running -def _daemon_check(daemon): - command = "{} print_status {}".format(watchfrr, daemon) - return_code = call(command) - if not return_code == 0: - logger.error("Daemon \"{}\" is not running".format(daemon)) - return False - - # return True if all checks were passed - return True + if os.path.isdir(frrconfig_tmp): + rmtree(frrconfig_tmp) # restart daemon def _daemon_restart(daemon): - command = "{} restart {}".format(watchfrr, daemon) + command = f'{watchfrr} restart {daemon}' return_code = call(command) if not return_code == 0: - logger.error("Failed to restart daemon \"{}\"".format(daemon)) + logger.error(f'Failed to restart daemon "{daemon}"!') return False # return True if restarted successfully - logger.info("Daemon \"{}\" restarted".format(daemon)) + logger.info(f'Daemon "{daemon}" restarted!') return True # reload old config def _reload_config(daemon): if daemon != '': - command = "{} -n -b --config_dir {} -d {} 2> /dev/null".format(vtysh, frrconfig_tmp, daemon) + command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} -d {daemon} 2> /dev/null' else: - command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) + command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} 2> /dev/null' return_code = call(command) if not return_code == 0: - logger.error("Failed to reinstall configuration") + logger.error('Failed to re-install configuration!') return False # return True if restarted successfully - logger.info("Configuration reinstalled successfully") - return True - -# check all daemons if they are running -def _check_args_daemon(daemons): - for daemon in daemons: - if not _daemon_check(daemon): - return False + logger.info('Configuration re-installed successfully!') return True # define program arguments @@ -159,19 +142,18 @@ cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf # parse arguments cmd_args = cmd_args_parser.parse_args() - # main logic # restart daemon if cmd_args.action == 'restart': # check if it is safe to restart FRR if not _check_safety(): print("\nOne of the safety checks was failed or user aborted command. Exiting.") - sys.exit(1) + exit(1) if not _write_config(): print("Failed to save active config") _cleanup() - sys.exit(1) + exit(1) # a little trick to make further commands more clear if not cmd_args.daemon: @@ -179,19 +161,20 @@ if cmd_args.action == 'restart': # check all daemons if they are running if cmd_args.daemon != ['']: - if not _check_args_daemon(cmd_args.daemon): - print("Warning: some of listed daemons are not running") + for daemon in cmd_args.daemon: + if not process_named_running(daemon): + print('WARNING: some of listed daemons are not running!') # run command to restart daemon for daemon in cmd_args.daemon: if not _daemon_restart(daemon): - print("Failed to restart daemon: {}".format(daemon)) + print('Failed to restart daemon: {daemon}') _cleanup() - sys.exit(1) + exit(1) # reinstall old configuration _reload_config(daemon) # cleanup after all actions _cleanup() -sys.exit(0) +exit(0) -- cgit v1.2.3 From b88a9fa6c70c7b15d20396e71a694008e6e31625 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sat, 11 Sep 2021 21:22:23 -0500 Subject: Fix inconsistent capitalization in the show version output --- python/vyos/airbag.py | 8 ++++---- src/op_mode/show_version.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index a20f44207..3c7a144b7 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -125,14 +125,14 @@ def _intercepting_exceptions(_singleton=[False]): # if the key before the value has not time, syslog takes that as the source of the message FAULT = """\ -Report Time: {date} -Image Version: VyOS {version} -Release Train: {release_train} +Report time: {date} +Image version: VyOS {version} +Release train: {release_train} Built by: {built_by} Built on: {built_on} Build UUID: {build_uuid} -Build Commit ID: {build_git} +Build commit ID: {build_git} Architecture: {system_arch} Boot via: {boot_via} diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py index 5bbc2e1f1..7962e1e7b 100755 --- a/src/op_mode/show_version.py +++ b/src/op_mode/show_version.py @@ -32,12 +32,12 @@ parser.add_argument("-j", "--json", action="store_true", help="Produce JSON outp version_output_tmpl = """ Version: VyOS {{version}} -Release Train: {{release_train}} +Release train: {{release_train}} Built by: {{built_by}} Built on: {{built_on}} Build UUID: {{build_uuid}} -Build Commit ID: {{build_git}} +Build commit ID: {{build_git}} Architecture: {{system_arch}} Boot via: {{boot_via}} -- cgit v1.2.3 From b8bb9f5863d223e31383ca2e264ea0fea9d32dbe Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 12 Sep 2021 11:41:16 +0700 Subject: T3822: set the OpenVPN key file owner to openvpn:openvpn --- src/conf_mode/interfaces-openvpn.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index c3620d690..3cfb2b742 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -477,6 +477,7 @@ def generate(openvpn): # Fixup file permissions for file in fix_permissions: chmod_600(file) + chown(file, 'openvpn', 'openvpn') return None -- cgit v1.2.3 From 842bc6d6fd682029eb543d92dfb23d4334d71b96 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 14 Sep 2021 18:26:42 +0200 Subject: openvpn: T3822: fix certificate permissions Commit b8bb9f586 ("T3822: set the OpenVPN key file owner to openvpn:openvpn") changed the permissions only for file present in the "fix_permissions" list. The list did not contain all required certificates - this has been fixed. --- src/conf_mode/interfaces-openvpn.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 3cfb2b742..5d537dadf 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -440,14 +440,17 @@ def generate(openvpn): # create client config directory on demand makedir(ccd_dir, user, group) - # Fix file permissons for keys - fix_permissions = [] - - tmp = dict_search('shared_secret_key_file', openvpn) - if tmp: fix_permissions.append(openvpn['shared_secret_key_file']) - - tmp = dict_search('tls.key_file', openvpn) - if tmp: fix_permissions.append(tmp) + # Fix file permissons for site2site shared secret + if dict_search('shared_secret_key_file', openvpn): + chmod_600(openvpn['shared_secret_key_file']) + chown(openvpn['shared_secret_key_file'], user, group) + + # Fix file permissons for TLS certificate and keys + for tls in ['auth_file', 'ca_cert_file', 'cert_file', 'crl_file', + 'crypt_file', 'dh_file', 'key_file']: + if dict_search(f'tls.{tls}', openvpn): + chmod_600(openvpn['tls'][tls]) + chown(openvpn['tls'][tls], user, group) # Generate User/Password authentication file if 'authentication' in openvpn: @@ -474,11 +477,6 @@ def generate(openvpn): render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) - # Fixup file permissions - for file in fix_permissions: - chmod_600(file) - chown(file, 'openvpn', 'openvpn') - return None def apply(openvpn): -- cgit v1.2.3 From 184f25819fa43fc892b97c0044813b8aa56855b4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 14 Sep 2021 19:50:52 +0200 Subject: dhcpv6-pd: T421: disable wide dhcpv6 client debug messages (cherry picked from commit 6b48900358ce9b01eaa78e3a086e95a26064f0df) --- src/systemd/dhcp6c@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service index 9a97ee261..fdd6d7d88 100644 --- a/src/systemd/dhcp6c@.service +++ b/src/systemd/dhcp6c@.service @@ -9,7 +9,7 @@ StartLimitIntervalSec=0 WorkingDirectory=/run/dhcp6c Type=forking PIDFile=/run/dhcp6c/dhcp6c.%i.pid -ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i +ExecStart=/usr/sbin/dhcp6c -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i Restart=on-failure RestartSec=20 -- cgit v1.2.3 From 24f17e0e41bb0bfd4d42e5b335d03ed1b9b1c634 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 18 Sep 2021 11:26:14 +0200 Subject: validator: T2417: bugfix on Python3 f'ormat strings Commit 3639a5610b590a ("validator: T2417: try to make the code clearer") introduced Python3 f'ormatted strings but missed the "f" keyword. (cherry picked from commit dda9f655f94968b07043887a03e3bba176eb94d5) --- src/validators/script | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/validators/script b/src/validators/script index 2665ec1f6..1d8a27e5c 100755 --- a/src/validators/script +++ b/src/validators/script @@ -1,8 +1,6 @@ #!/usr/bin/env python3 # -# numeric value validator -# -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -23,7 +21,6 @@ import shlex import vyos.util - if __name__ == '__main__': if len(sys.argv) < 2: sys.exit('Please specify script file to check') @@ -35,11 +32,11 @@ if __name__ == '__main__': sys.exit(f'File {script} does not exist') if not (os.path.isfile(script) and os.access(script, os.X_OK)): - sys.exit('File {script} is not an executable file') + sys.exit(f'File {script} is not an executable file') # File outside the config dir is just a warning if not vyos.util.file_is_persistent(script): sys.exit( - 'Warning: file {path} is outside the / config directory\n' + f'Warning: file {path} is outside the / config directory\n' 'It will not be automatically migrated to a new image on system update' ) -- cgit v1.2.3 From 482aaf1cee85487c14a183770d23ceda4611d1c6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 18 Sep 2021 21:27:47 +0200 Subject: dhcp-server: T1968: allow multiple static-routes to be configured vyos@vyos# show service dhcp-server shared-network-name LAN { subnet 10.0.0.0/24 { default-router 10.0.0.1 dns-server 194.145.150.1 lease 88 range 0 { start 10.0.0.100 stop 10.0.0.200 } static-route 192.168.10.0/24 { next-hop 10.0.0.2 } static-route 192.168.20.0/24 { router 10.0.0.2 } } } (cherry picked from commit a4440bd589db645eb99f343a8163e188a700774c) --- data/templates/dhcp-server/dhcpd.conf.tmpl | 10 ++-- interface-definitions/dhcp-server.xml.in | 29 +++++------ smoketest/scripts/cli/test_service_dhcp-server.py | 3 +- src/conf_mode/dhcp_server.py | 7 +-- src/migration-scripts/dhcp-server/5-to-6 | 61 +++++++++++++++++++++++ 5 files changed, 85 insertions(+), 25 deletions(-) create mode 100755 src/migration-scripts/dhcp-server/5-to-6 (limited to 'src') diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index ff2e31998..58be7984d 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -114,9 +114,13 @@ shared-network {{ network | replace('_','-') }} { {% if subnet_config.default_router and subnet_config.default_router is not none %} {% set static_default_route = ', ' + '0.0.0.0/0' | isc_static_route(subnet_config.default_router) %} {% endif %} -{% if subnet_config.static_route.router is defined and subnet_config.static_route.router is not none and subnet_config.static_route.destination_subnet is defined and subnet_config.static_route.destination_subnet is not none %} - option rfc3442-static-route {{ subnet_config.static_route.destination_subnet | isc_static_route(subnet_config.static_route.router) }}{{ static_default_route }}; - option windows-static-route {{ subnet_config.static_route.destination_subnet | isc_static_route(subnet_config.static_route.router) }}; +{% if subnet_config.static_route is defined and subnet_config.static_route is not none %} +{% set rfc3442_routes = [] %} +{% for route, route_options in subnet_config.static_route.items() %} +{% set rfc3442_routes = rfc3442_routes.append(route | isc_static_route(route_options.next_hop)) %} +{% endfor %} + option rfc3442-static-route {{ rfc3442_routes | join(', ') }}{{ static_default_route }}; + option windows-static-route {{ rfc3442_routes | join(', ') }}; {% endif %} {% endif %} {% if subnet_config.ip_forwarding is defined %} diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index bafd6f6a2..c0f72dd86 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -357,26 +357,21 @@ - + - Classless static route + Classless static route destination subnet [REQUIRED] + + ipv4net + IPv4 address and prefix length + + + + - - - Destination subnet [REQUIRED] - - ipv4net - IPv4 address and prefix length - - - - - - - + - IP address of router to be used to reach the destination subnet [REQUIRED] + IP address of router to be used to reach the destination subnet ipv4 IPv4 address of router @@ -387,7 +382,7 @@ - + Additional subnet parameters for DHCP server. You must diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 815bd333a..40977bb04 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -123,8 +123,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['wpad-url', wpad]) self.cli_set(pool + ['server-identifier', server_identifier]) - self.cli_set(pool + ['static-route', 'destination-subnet', '10.0.0.0/24']) - self.cli_set(pool + ['static-route', 'router', '192.0.2.1']) + self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index cdee72e09..8d6cef8b7 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -159,9 +159,10 @@ def verify(dhcp): 'lease subnet must be configured.') for subnet, subnet_config in network_config['subnet'].items(): - if 'static_route' in subnet_config and len(subnet_config['static_route']) != 2: - raise ConfigError('Missing DHCP static-route parameter(s):\n' \ - 'destination-subnet | router must be defined!') + if 'static_route' in subnet_config: + for route, route_option in subnet_config['static_route'].items(): + if 'next_hop' not in route_option: + raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') # Check if DHCP address range is inside configured subnet declaration if 'range' in subnet_config: diff --git a/src/migration-scripts/dhcp-server/5-to-6 b/src/migration-scripts/dhcp-server/5-to-6 new file mode 100755 index 000000000..4cd2ec07a --- /dev/null +++ b/src/migration-scripts/dhcp-server/5-to-6 @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'dhcp-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# Run this for every instance if 'shared-network-name' +for network in config.list_nodes(base): + base_network = base + [network] + + if not config.exists(base_network + ['subnet']): + continue + + # Run this for every specified 'subnet' + for subnet in config.list_nodes(base_network + ['subnet']): + base_subnet = base_network + ['subnet', subnet] + + if config.exists(base_subnet + ['static-route']): + prefix = config.return_value(base_subnet + ['static-route', 'destination-subnet']) + router = config.return_value(base_subnet + ['static-route', 'router']) + config.delete(base_subnet + ['static-route']) + + config.set(base_subnet + ['static-route', prefix, 'next-hop'], value=router) + config.set_tag(base_subnet + ['static-route']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3 From abad387fcaf700a32f8fc85183d617fcfbb0b8f4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 18 Sep 2021 21:48:53 +0200 Subject: dhcp-server: T3838: rename dns-server to name-server node IPv4 DHCP uses "dns-server" to specify one or more name-servers for a given pool. In order to use the same CLI syntax this should be renamed to name-server, which is already the case for DHCPv6. (cherry picked from commit e2f9f4f4e8b2e961a58d935d09798ddb4e1e0460) --- data/templates/dhcp-server/dhcpd.conf.tmpl | 4 +-- interface-definitions/dhcp-server.xml.in | 14 +------- interface-definitions/dhcpv6-server.xml.in | 42 ++-------------------- interface-definitions/dns-forwarding.xml.in | 19 +--------- .../include/accel-ppp/name-server.xml.i | 20 ----------- .../include/name-server-ipv4-ipv6.xml.i | 20 +++++++++++ .../include/name-server-ipv4.xml.i | 15 ++++++++ .../include/name-server-ipv6.xml.i | 15 ++++++++ interface-definitions/interfaces-openvpn.xml.in | 18 +--------- interface-definitions/service_ipoe-server.xml.in | 2 +- interface-definitions/service_pppoe-server.xml.in | 2 +- interface-definitions/service_router-advert.xml.in | 14 +------- interface-definitions/vpn_l2tp.xml.in | 2 +- interface-definitions/vpn_openconnect.xml.in | 2 +- interface-definitions/vpn_pptp.xml.in | 14 +------- interface-definitions/vpn_sstp.xml.in | 2 +- smoketest/scripts/cli/test_service_dhcp-server.py | 16 ++++----- src/migration-scripts/dhcp-server/5-to-6 | 7 ++++ 18 files changed, 80 insertions(+), 148 deletions(-) delete mode 100644 interface-definitions/include/accel-ppp/name-server.xml.i create mode 100644 interface-definitions/include/name-server-ipv4-ipv6.xml.i create mode 100644 interface-definitions/include/name-server-ipv4.xml.i create mode 100644 interface-definitions/include/name-server-ipv6.xml.i (limited to 'src') diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index 58be7984d..f64192acf 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -88,8 +88,8 @@ shared-network {{ network | replace('_','-') }} { {% if network_config.subnet is defined and network_config.subnet is not none %} {% for subnet, subnet_config in network_config.subnet.items() %} subnet {{ subnet | address_from_cidr }} netmask {{ subnet | netmask_from_cidr }} { -{% if subnet_config.dns_server is defined and subnet_config.dns_server is not none %} - option domain-name-servers {{ subnet_config.dns_server | join(', ') }}; +{% if subnet_config.name_server is defined and subnet_config.name_server is not none %} + option domain-name-servers {{ subnet_config.name_server | join(', ') }}; {% endif %} {% if subnet_config.domain_search is defined and subnet_config.domain_search is not none %} option domain-search "{{ subnet_config.domain_search | join('", "') }}"; diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index c0f72dd86..3a1eee60e 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -117,19 +117,7 @@ - - - DNS server IPv4 address - - ipv4 - DNS server IPv4 address - - - - - - - + #include Client Domain Name diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index 95b1e5602..58181872b 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -14,19 +14,7 @@ Additional global parameters for DHCPv6 server - - - IPv6 address of a Recursive DNS Server - - ipv6 - IPv6 address of DNS name server - - - - - - - + #include @@ -70,19 +58,7 @@ #include - - - IPv6 address of a Recursive DNS Server - - ipv6 - IPv6 address of DNS name server - - - - - - - + #include @@ -194,19 +170,7 @@ - - - IPv6 address of a Recursive DNS Server - - ipv6 - IPv6 address of DNS name server - - - - - - - + #include NIS domain name for client to use diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 9edd18a66..5d6e25a27 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -142,24 +142,7 @@ 3600 - - - Domain Name Servers (DNS) addresses [OPTIONAL] - - ipv4 - Domain Name Server (DNS) IPv4 address - - - ipv6 - Domain Name Server (DNS) IPv6 address - - - - - - - - + #include Local addresses from which to send DNS queries diff --git a/interface-definitions/include/accel-ppp/name-server.xml.i b/interface-definitions/include/accel-ppp/name-server.xml.i deleted file mode 100644 index e744b384f..000000000 --- a/interface-definitions/include/accel-ppp/name-server.xml.i +++ /dev/null @@ -1,20 +0,0 @@ - - - - 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/include/name-server-ipv4-ipv6.xml.i b/interface-definitions/include/name-server-ipv4-ipv6.xml.i new file mode 100644 index 000000000..14973234b --- /dev/null +++ b/interface-definitions/include/name-server-ipv4-ipv6.xml.i @@ -0,0 +1,20 @@ + + + + Domain Name Servers (DNS) addresses + + ipv4 + Domain Name Server (DNS) IPv4 address + + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + + diff --git a/interface-definitions/include/name-server-ipv4.xml.i b/interface-definitions/include/name-server-ipv4.xml.i new file mode 100644 index 000000000..0cf884e03 --- /dev/null +++ b/interface-definitions/include/name-server-ipv4.xml.i @@ -0,0 +1,15 @@ + + + + Domain Name Servers (DNS) addresses + + ipv4 + Domain Name Server (DNS) IPv4 address + + + + + + + + diff --git a/interface-definitions/include/name-server-ipv6.xml.i b/interface-definitions/include/name-server-ipv6.xml.i new file mode 100644 index 000000000..d4517c4c6 --- /dev/null +++ b/interface-definitions/include/name-server-ipv6.xml.i @@ -0,0 +1,15 @@ + + + + Domain Name Servers (DNS) addresses + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 40f8fe65c..51e81390c 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -554,23 +554,7 @@ - - - Domain Name Server (DNS) - - ipv4 - DNS server IPv4 address - - - ipv6 - DNS server IPv6 address - - - - - - - + #include Route to be pushed to all clients diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index 7c575ba77..b19acab56 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -111,7 +111,7 @@ - #include + #include #include diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index 955c104f7..712e6549e 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -59,7 +59,7 @@ #include - #include + #include interface(s) to listen on diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index e18b27f1b..0f4009f5c 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service_router-advert.xml.in @@ -135,19 +135,7 @@ - - - IPv6 address of recursive DNS server - - ipv6 - IPv6 address of DNS name server - - - - - - - + #include Hosts use the administered (stateful) protocol for autoconfiguration of other (non-address) information diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 787298284..8bcede159 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -22,7 +22,7 @@ #include - #include + #include L2TP Network Server (LNS) diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in index b345b560e..f35b1ebbd 100644 --- a/interface-definitions/vpn_openconnect.xml.in +++ b/interface-definitions/vpn_openconnect.xml.in @@ -190,7 +190,7 @@ - #include + #include diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn_pptp.xml.in index 91c8cd76f..9b84a00c1 100644 --- a/interface-definitions/vpn_pptp.xml.in +++ b/interface-definitions/vpn_pptp.xml.in @@ -21,19 +21,7 @@ - - - Domain Name Server (DNS) propagated to client - - ipv4 - Domain Name Server (DNS) IPv4 address - - - - - - - + #include #include diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index 840e237cc..5406ede41 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -27,7 +27,7 @@ #include #include - #include + #include Client IP pools and gateway setting diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 40977bb04..37e016778 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -59,8 +59,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['dns-server', dns_1]) - self.cli_set(pool + ['dns-server', dns_2]) + self.cli_set(pool + ['name-server', dns_1]) + self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['domain-name', domain_name]) # check validate() - No DHCP address range or active static-mapping set @@ -108,8 +108,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['dns-server', dns_1]) - self.cli_set(pool + ['dns-server', dns_2]) + self.cli_set(pool + ['name-server', dns_1]) + self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['domain-name', domain_name]) self.cli_set(pool + ['ip-forwarding']) self.cli_set(pool + ['smtp-server', smtp_server]) @@ -201,8 +201,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['dns-server', dns_1]) - self.cli_set(pool + ['dns-server', dns_2]) + self.cli_set(pool + ['name-server', dns_1]) + self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['domain-name', domain_name]) # check validate() - No DHCP address range or active static-mapping set @@ -261,7 +261,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['dns-server', dns_1]) + self.cli_set(pool + ['name-server', dns_1]) self.cli_set(pool + ['domain-name', domain_name]) self.cli_set(pool + ['lease', lease_time]) diff --git a/src/migration-scripts/dhcp-server/5-to-6 b/src/migration-scripts/dhcp-server/5-to-6 index 4cd2ec07a..7f447ac17 100755 --- a/src/migration-scripts/dhcp-server/5-to-6 +++ b/src/migration-scripts/dhcp-server/5-to-6 @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# T1968: allow multiple static-routes to be configured +# T3838: rename dns-server -> name-server import sys from vyos.configtree import ConfigTree @@ -45,6 +47,7 @@ for network in config.list_nodes(base): for subnet in config.list_nodes(base_network + ['subnet']): base_subnet = base_network + ['subnet', subnet] + # T1968: allow multiple static-routes to be configured if config.exists(base_subnet + ['static-route']): prefix = config.return_value(base_subnet + ['static-route', 'destination-subnet']) router = config.return_value(base_subnet + ['static-route', 'router']) @@ -53,6 +56,10 @@ for network in config.list_nodes(base): config.set(base_subnet + ['static-route', prefix, 'next-hop'], value=router) config.set_tag(base_subnet + ['static-route']) + # T3838: rename dns-server -> name-server + if config.exists(base_subnet + ['dns-server']): + config.rename(base_subnet + ['dns-server'], 'name-server') + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From 81dbce734c207a0fce836bf2a5d283910509f4ff Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 19 Sep 2021 10:51:15 +0200 Subject: dhcp-server: T3672: only one failover peer is supported (cherry picked from commit a8ccf72c222caad8cd7aaca9bca773be39e87f5c) --- data/templates/dhcp-server/dhcpd.conf.tmpl | 37 ++++------- interface-definitions/dhcp-server.xml.in | 98 ++++++++++++++---------------- src/conf_mode/dhcp_server.py | 36 +++++------ src/migration-scripts/dhcp-server/5-to-6 | 25 ++++++-- 4 files changed, 97 insertions(+), 99 deletions(-) (limited to 'src') diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index f64192acf..23917b303 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -35,32 +35,25 @@ option wpad-url code 252 = text; {% endfor %} {% endif %} -{% if shared_network_name is defined and shared_network_name is not none %} -{% for network, network_config in shared_network_name.items() if network_config.disable is not defined %} -{% if network_config.subnet is defined and network_config.subnet is not none %} -{% for subnet, subnet_config in network_config.subnet.items() %} -{% if subnet_config.failover is defined and subnet_config.failover is defined and subnet_config.failover.name is defined and subnet_config.failover.name is not none %} -# Failover configuration for {{ subnet }} -failover peer "{{ subnet_config.failover.name }}" { -{% if subnet_config.failover.status == 'primary' %} +{% if failover is defined and failover is not none %} +{% set dhcp_failover_name = 'VyOS-DHCP-failover-peer' %} +# DHCP failover configuration +failover peer "{{ dhcp_failover_name }}" { +{% if failover.status == 'primary' %} primary; mclt 1800; split 128; -{% elif subnet_config.failover.status == 'secondary' %} +{% elif failover.status == 'secondary' %} secondary; -{% endif %} - address {{ subnet_config.failover.local_address }}; +{% endif %} + address {{ failover.source_address }}; port 520; - peer address {{ subnet_config.failover.peer_address }}; + peer address {{ failover.remote }}; peer port 520; max-response-delay 30; max-unacked-updates 10; load balance max seconds 3; } -{% endif %} -{% endfor %} -{% endif %} -{% endfor %} {% endif %} {% if listen_address is defined and listen_address is not none %} @@ -182,23 +175,17 @@ shared-network {{ network | replace('_','-') }} { } {% endfor %} {% endif %} -{% if subnet_config.failover is defined and subnet_config.failover.name is defined and subnet_config.failover.name is not none %} pool { - failover peer "{{ subnet_config.failover.name }}"; +{% if subnet_config.enable_failover is defined %} + failover peer "{{ dhcp_failover_name }}"; deny dynamic bootp clients; +{% endif %} {% if subnet_config.range is defined and subnet_config.range is not none %} {% for range, range_options in subnet_config.range.items() %} range {{ range_options.start }} {{ range_options.stop }}; {% endfor %} {% endif %} } -{% else %} -{% if subnet_config.range is defined and subnet_config.range is not none %} -{% for range, range_options in subnet_config.range.items() %} - range {{ range_options.start }} {{ range_options.stop }}; -{% endfor %} -{% endif %} -{% endif %} } {% endfor %} {% endif %} diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 3a1eee60e..10384947a 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -16,6 +16,46 @@ + + + DHCP failover configuration + + + #include + + + IPv4 remote address used for connectio + + ipv4 + IPv4 address of failover peer + + + + + + + + + Failover hierarchy + + primary secondary + + + primary + Configure this server to be the primary node + + + secondary + Configure this server to be the secondary node + + + ^(primary|secondary)$ + + Invalid DHCP failover peer status + + + + Additional global parameters for DHCP server. You must @@ -128,6 +168,12 @@ #include + + + Enable DHCP failover support for this subnet + + + IP address to exclude from DHCP lease range @@ -141,58 +187,6 @@ - - - DHCP failover parameters - - - - - IP address for failover peer to connect [REQUIRED] - - ipv4 - IPv4 address to exclude from lease range - - - - - - - - - DHCP failover peer name [REQUIRED] - - [-_a-zA-Z0-9.]+ - - Invalid failover peer name. May only contain letters, numbers and .-_ - - - - - IP address of failover peer [REQUIRED] - - ipv4 - IPv4 address of failover peer - - - - - - - - - DHCP failover peer status (primary|secondary) [REQUIRED] - - primary secondary - - - ^(primary|secondary)$ - - Invalid DHCP failover peer status - - - - Enable IP forwarding on client diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 8d6cef8b7..5b3809017 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -148,9 +148,9 @@ def verify(dhcp): 'At least one DHCP shared network must be configured.') # Inspect shared-network/subnet - failover_names = [] listen_ok = False subnets = [] + failover_ok = False # A shared-network requires a subnet definition for network, network_config in dhcp['shared_network_name'].items(): @@ -159,11 +159,19 @@ def verify(dhcp): 'lease subnet must be configured.') for subnet, subnet_config in network_config['subnet'].items(): + # All delivered static routes require a next-hop to be set if 'static_route' in subnet_config: for route, route_option in subnet_config['static_route'].items(): if 'next_hop' not in route_option: raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') + # DHCP failover needs at least one subnet that uses it + if 'enable_failover' in subnet_config: + if 'failover' not in dhcp: + raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \ + 'Failover is not configured globally!') + failover_ok = True + # Check if DHCP address range is inside configured subnet declaration if 'range' in subnet_config: networks = [] @@ -192,23 +200,6 @@ def verify(dhcp): tmp = IPRange(range_config['start'], range_config['stop']) networks.append(tmp) - if 'failover' in subnet_config: - for key in ['local_address', 'peer_address', 'name', 'status']: - if key not in subnet_config['failover']: - raise ConfigError(f'Missing DHCP failover parameter "{key}"!') - - # Failover names must be uniquie - if subnet_config['failover']['name'] in failover_names: - name = subnet_config['failover']['name'] - raise ConfigError(f'DHCP failover names must be unique:\n' \ - f'{name} has already been configured!') - failover_names.append(subnet_config['failover']['name']) - - # Failover requires start/stop ranges for pool - if 'range' not in subnet_config: - raise ConfigError(f'DHCP failover requires at least one start-stop range to be configured\n'\ - f'within shared-network "{network}, {subnet}" for using failover!') - # Exclude addresses must be in bound if 'exclude' in subnet_config: for exclude in subnet_config['exclude']: @@ -252,6 +243,15 @@ def verify(dhcp): if net.overlaps(net2): raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!') + if 'failover' in dhcp: + if not failover_ok: + raise ConfigError('DHCP failover must be enabled for at least one subnet!') + + for key in ['source_address', 'remote', 'status']: + if key not in dhcp['failover']: + tmp = key.replace('_', '-') + raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') + for address in (dict_search('listen_address', dhcp) or []): if is_addr_assigned(address): listen_ok = True diff --git a/src/migration-scripts/dhcp-server/5-to-6 b/src/migration-scripts/dhcp-server/5-to-6 index 7f447ac17..39bbb9f50 100755 --- a/src/migration-scripts/dhcp-server/5-to-6 +++ b/src/migration-scripts/dhcp-server/5-to-6 @@ -29,16 +29,16 @@ file_name = sys.argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['service', 'dhcp-server', 'shared-network-name'] +base = ['service', 'dhcp-server'] config = ConfigTree(config_file) -if not config.exists(base): +if not config.exists(base + ['shared-network-name']): # Nothing to do exit(0) # Run this for every instance if 'shared-network-name' -for network in config.list_nodes(base): - base_network = base + [network] +for network in config.list_nodes(base + ['shared-network-name']): + base_network = base + ['shared-network-name', network] if not config.exists(base_network + ['subnet']): continue @@ -60,6 +60,23 @@ for network in config.list_nodes(base): if config.exists(base_subnet + ['dns-server']): config.rename(base_subnet + ['dns-server'], 'name-server') + + # T3672: ISC DHCP server only supports one failover peer + if config.exists(base_subnet + ['failover']): + # There can only be one failover configuration, if none is present + # we add the first one + if not config.exists(base + ['failover']): + local = config.return_value(base_subnet + ['failover', 'local-address']) + remote = config.return_value(base_subnet + ['failover', 'peer-address']) + status = config.return_value(base_subnet + ['failover', 'status']) + + config.set(base + ['failover', 'remote'], value=remote) + config.set(base + ['failover', 'source-address'], value=local) + config.set(base + ['failover', 'status'], value=status) + + config.delete(base_subnet + ['failover']) + config.set(base_subnet + ['enable-failover']) + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From a672c7c012b85cd2950403400900453aa318613b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 19 Sep 2021 11:32:04 +0200 Subject: dhcp-server: T3672: re-add missing "name" CLI option This option is mandatory and must be user configurable as it needs to match on both sides. (cherry picked from commit 2985035bcb2f3732e15a41e3c2ee6c6c93a6836e) --- data/templates/dhcp-server/dhcpd.conf.tmpl | 5 ++--- interface-definitions/dhcp-server.xml.in | 9 +++++++++ src/conf_mode/dhcp_server.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index 23917b303..9aeaafcc2 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -36,9 +36,8 @@ option wpad-url code 252 = text; {% endif %} {% if failover is defined and failover is not none %} -{% set dhcp_failover_name = 'VyOS-DHCP-failover-peer' %} # DHCP failover configuration -failover peer "{{ dhcp_failover_name }}" { +failover peer "{{ failover.name }}" { {% if failover.status == 'primary' %} primary; mclt 1800; @@ -177,7 +176,7 @@ shared-network {{ network | replace('_','-') }} { {% endif %} pool { {% if subnet_config.enable_failover is defined %} - failover peer "{{ dhcp_failover_name }}"; + failover peer "{{ failover.name }}"; deny dynamic bootp clients; {% endif %} {% if subnet_config.range is defined and subnet_config.range is not none %} diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 10384947a..598be74b4 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -34,6 +34,15 @@ + + + Peer name used to identify connection + + [-_a-zA-Z0-9.]+ + + Invalid failover peer name. May only contain letters, numbers and .-_ + + Failover hierarchy diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 5b3809017..28f2a4ca5 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -247,7 +247,7 @@ def verify(dhcp): if not failover_ok: raise ConfigError('DHCP failover must be enabled for at least one subnet!') - for key in ['source_address', 'remote', 'status']: + for key in ['name', 'remote', 'source_address', 'status']: if key not in dhcp['failover']: tmp = key.replace('_', '-') raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') -- cgit v1.2.3 From 404b4c7b7b4f3063bc2bb608a32833d6cf23d834 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Tue, 23 Feb 2021 17:38:05 +0000 Subject: dhcp-server: T2927: Add empty args if does not possible to determine variables (cherry picked from commit 2f8b33a26e63e5b9ac4e697b9312f2238d6241f3) --- data/templates/dhcp-server/dhcpd.conf.tmpl | 18 +++++++++--------- src/system/on-dhcp-event.sh | 25 ++++++++++++------------- 2 files changed, 21 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index a2d5cb242..d774b4827 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -8,16 +8,12 @@ on release { set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", "", ClientIp, "", ""); } on expiry { set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", "", ClientIp, "", ""); } {% endif %} @@ -209,11 +205,15 @@ shared-network {{ network | replace('_','-') }} { on commit { set shared-networkname = "{{ network | replace('_','-') }}"; {% if hostfile_update is defined %} - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); set ClientIp = binary-to-ascii(10, 8, ".", leased-address); set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name, "empty_hostname"); + if not (ClientName = "empty_hostname") { + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); + } else { + log(concat("Hostname is not defined for client with IP: ", ClientIP, " MAC: ", ClientMac)); + } {% endif %} } } diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index a062dc810..49e53d7e1 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -21,21 +21,20 @@ client_mac=$4 domain=$5 hostsd_client="/usr/bin/vyos-hostsd-client" -if [ -z "$client_name" ]; then - logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" - client_name=$(echo "client-"$client_mac | tr : -) -fi - -if [ "$domain" == "..YYZ!" ]; then - client_fqdn_name=$client_name - client_search_expr=$client_name -else - client_fqdn_name=$client_name.$domain - client_search_expr="$client_name\\.$domain" -fi - case "$action" in commit) # add mapping for new lease + if [ -z "$client_name" ]; then + logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" + client_name=$(echo "client-"$client_mac | tr : -) + fi + + if [ "$domain" == "..YYZ!" ]; then + client_fqdn_name=$client_name + client_search_expr=$client_name + else + client_fqdn_name=$client_name.$domain + client_search_expr="$client_name\\.$domain" + fi $hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply exit 0 ;; -- cgit v1.2.3 From 9b0c1c0c3125257baee184eea7ad55d7dda57680 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 19 Sep 2021 17:04:49 +0200 Subject: dhcp-server: T3672: migrate failover name option Commit 2985035b (dhcp-server: T3672: re-add missing "name" CLI option) unfortunately did not add the name option to the migration script. (cherry picked from commit e83a113360ba18043edcf7f70689c7042dee2b37) --- src/migration-scripts/dhcp-server/5-to-6 | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/migration-scripts/dhcp-server/5-to-6 b/src/migration-scripts/dhcp-server/5-to-6 index 39bbb9f50..aefe84737 100755 --- a/src/migration-scripts/dhcp-server/5-to-6 +++ b/src/migration-scripts/dhcp-server/5-to-6 @@ -69,10 +69,12 @@ for network in config.list_nodes(base + ['shared-network-name']): local = config.return_value(base_subnet + ['failover', 'local-address']) remote = config.return_value(base_subnet + ['failover', 'peer-address']) status = config.return_value(base_subnet + ['failover', 'status']) + name = config.return_value(base_subnet + ['failover', 'name']) config.set(base + ['failover', 'remote'], value=remote) config.set(base + ['failover', 'source-address'], value=local) config.set(base + ['failover', 'status'], value=status) + config.set(base + ['failover', 'name'], value=name) config.delete(base_subnet + ['failover']) config.set(base_subnet + ['enable-failover']) -- cgit v1.2.3 From 9788c48435bb8ce74883138d305743a6a565910a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 21 Sep 2021 20:05:52 +0200 Subject: vrrp: keepalived: T2720: adjust to Jinja2 trim_blocks feature This is a successor to commit a2ac9fac16e ("vyos.template: T2720: always enable Jinja2 trim_blocks feature"). It only shifts the whitespaces / indents inside the keepalived configuration file. (cherry picked from commit c1ac0630cfe0ee65569fbe435cc006ade20fed22) --- data/templates/vrrp/keepalived.conf.tmpl | 103 ++++++++++++++----------------- src/conf_mode/vrrp.py | 7 ++- 2 files changed, 52 insertions(+), 58 deletions(-) (limited to 'src') diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl index d51522e45..13619ca69 100644 --- a/data/templates/vrrp/keepalived.conf.tmpl +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -10,8 +10,7 @@ global_defs { } {% for group in groups %} - -{% if group.health_check_script %} +{% if group.health_check_script is defined and group.health_check_script is not none %} vrrp_script healthcheck_{{ group.name }} { script "{{ group.health_check_script }}" interval {{ group.health_check_interval }} @@ -19,87 +18,77 @@ vrrp_script healthcheck_{{ group.name }} { rise 1 } -{% endif %} +{% endif %} vrrp_instance {{ group.name }} { - {% if group.description %} - # {{ group.description }} - {% endif %} - + {{ '# ' ~ group.description if group.description is defined }} state BACKUP interface {{ group.interface }} virtual_router_id {{ group.vrid }} priority {{ group.priority }} advert_int {{ group.advertise_interval }} - - {% if group.preempt %} +{% if group.preempt is defined and group.preempt is not none %} preempt_delay {{ group.preempt_delay }} - {% else %} +{% else %} nopreempt - {% endif %} - - {% if group.peer_address %} +{% endif %} +{% if group.peer_address is defined and group.peer_address is not none %} unicast_peer { {{ group.peer_address }} } - {% endif %} - - {% if group.hello_source %} - {% if group.peer_address %} - unicast_src_ip {{ group.hello_source }} - {% else %} - mcast_src_ip {{ group.hello_source }} - {% endif %} - {% endif %} - - {% if group.use_vmac and group.peer_address %} - use_vmac {{group.interface}}v{{group.vrid}} - vmac_xmit_base - {% elif group.use_vmac %} - use_vmac {{group.interface}}v{{group.vrid}} - {% endif %} - - {% if group.auth_password %} - authentication { +{% endif %} +{% if group.hello_source is defined and group.hello_source is not none %} +{% if group.peer_address is defined and group.peer_address is not none %} + unicast_src_ip {{ group.hello_source }} +{% else %} + mcast_src_ip {{ group.hello_source }} +{% endif %} +{% endif %} +{% if group.use_vmac is defined and group.peer_address is defined %} + use_vmac {{ group.interface }}v{{ group.vrid }} + vmac_xmit_base +{% elif group.use_vmac is defined %} + use_vmac {{ group.interface }}v{{ group.vrid }} +{% endif %} +{% if group.auth_password is defined and group.auth_password is not none %} + authentication { auth_pass "{{ group.auth_password }}" auth_type {{ group.auth_type }} - } - {% endif %} - + } +{% endif %} +{% if group.virtual_addresses is defined and group.virtual_addresses is not none %} virtual_ipaddress { - {% for addr in group.virtual_addresses %} +{% for addr in group.virtual_addresses %} {{ addr }} - {% endfor %} +{% endfor %} } - - {% if group.virtual_addresses_excluded %} +{% endif %} +{% if group.virtual_addresses_excluded is defined and group.virtual_addresses_excluded is not none %} virtual_ipaddress_excluded { - {% for addr in group.virtual_addresses_excluded %} +{% for addr in group.virtual_addresses_excluded %} {{ addr }} - {% endfor %} +{% endfor %} } - {% endif %} - - {% if group.health_check_script %} +{% endif %} +{% if group.health_check_script is defined and group.health_check_script is not none %} track_script { healthcheck_{{ group.name }} } - {% endif %} +{% endif %} } - {% endfor %} -{% for sync_group in sync_groups %} +{% if sync_groups is defined and sync_groups is not none %} +{% for sync_group in sync_groups %} vrrp_sync_group {{ sync_group.name }} { - group { - {% for member in sync_group.members %} - {{ member }} - {% endfor %} - } - - {% if sync_group.conntrack_sync %} + group { +{% for member in sync_group.members %} + {{ member }} +{% endfor %} + } +{% if sync_group.conntrack_sync %} notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}" notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}" notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}" - {% endif %} +{% endif %} } - -{% endfor %} +{% endfor %} +{% endif %} diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 3ccc7d66b..4cee87003 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -17,7 +17,12 @@ import os from sys import exit -from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address +from ipaddress import ip_address +from ipaddress import ip_interface +from ipaddress import IPv4Interface +from ipaddress import IPv6Interface +from ipaddress import IPv4Address +from ipaddress import IPv6Address from json import dumps from pathlib import Path -- cgit v1.2.3 From 65398e5c8aedf2f206bb706e97aa828e409d07b3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 21 Sep 2021 20:29:36 +0200 Subject: vrrp: keepalived: T616: move configuration to volatile /run directory Move keepalived configuration from /etc/keepalived to /run/keepalived. (cherry picked from commit b243795eba1b36cadd81c3149e833bdf5c5bea70) --- data/templates/vrrp/keepalived.conf.tmpl | 3 +-- python/vyos/ifconfig/vrrp.py | 8 ++++---- smoketest/scripts/cli/test_ha_vrrp.py | 6 ++---- src/conf_mode/vrrp.py | 5 ++++- src/etc/systemd/system/keepalived.service.d/override.conf | 10 ++++++++++ src/system/keepalived-fifo.py | 14 +++++++------- 6 files changed, 28 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl index 13619ca69..c9835049a 100644 --- a/data/templates/vrrp/keepalived.conf.tmpl +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -5,7 +5,7 @@ global_defs { dynamic_interfaces script_user root - notify_fifo /run/keepalived_notify_fifo + notify_fifo /run/keepalived/keepalived_notify_fifo notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py } @@ -16,7 +16,6 @@ vrrp_script healthcheck_{{ group.name }} { interval {{ group.health_check_interval }} fall {{ group.health_check_count }} rise 1 - } {% endif %} diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index b522cc1ab..481b0284a 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -32,14 +32,14 @@ class VRRPNoData(VRRPError): class VRRP(object): _vrrp_prefix = '00:00:5E:00:01:' location = { - 'pid': '/run/keepalived.pid', - 'fifo': '/run/keepalived_notify_fifo', + 'pid': '/run/keepalived/keepalived.pid', + 'fifo': '/run/keepalived/keepalived_notify_fifo', 'state': '/tmp/keepalived.data', 'stats': '/tmp/keepalived.stats', 'json': '/tmp/keepalived.json', 'daemon': '/etc/default/keepalived', - 'config': '/etc/keepalived/keepalived.conf', - 'vyos': '/run/keepalived_config.dict', + 'config': '/run/keepalived/keepalived.conf', + 'vyos': '/run/keepalived/keepalived_config.dict', } _signal = { diff --git a/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index 03618c7d8..9c8d26699 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -14,22 +14,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os -import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError +from vyos.ifconfig.vrrp import VRRP from vyos.util import cmd from vyos.util import process_named_running from vyos.util import read_file - from vyos.template import inc_ip PROCESS_NAME = 'keepalived' -KEEPALIVED_CONF = '/etc/keepalived/keepalived.conf' +KEEPALIVED_CONF = VRRP.location['config'] base_path = ['high-availability', 'vrrp'] vrrp_interface = 'eth1' diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 4cee87003..cee6a9ba2 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -30,6 +30,7 @@ import vyos.config from vyos import ConfigError from vyos.util import call +from vyos.util import makedir from vyos.template import render from vyos.ifconfig.vrrp import VRRP @@ -136,7 +137,9 @@ def get_config(config=None): sync_groups.append(sync_group) # create a file with dict with proposed configuration - with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file: + dirname = os.path.dirname(VRRP.location['vyos']) + makedir(dirname) + with open(VRRP.location['vyos'] + ".temp", 'w') as dict_file: dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) return (vrrp_groups, sync_groups) diff --git a/src/etc/systemd/system/keepalived.service.d/override.conf b/src/etc/systemd/system/keepalived.service.d/override.conf index 9fcabf652..e338b90a2 100644 --- a/src/etc/systemd/system/keepalived.service.d/override.conf +++ b/src/etc/systemd/system/keepalived.service.d/override.conf @@ -1,2 +1,12 @@ +[Unit] +ConditionPathExists= +ConditionPathExists=/run/keepalived/keepalived.conf +After= +After=vyos-router.service + [Service] KillMode=process +ExecStart= +ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork $DAEMON_ARGS +PIDFile= +PIDFile=/run/keepalived/keepalived.pid diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index 7e2076820..1e749207b 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# import os import time @@ -22,11 +21,12 @@ import argparse import threading import re import json -from pathlib import Path -from queue import Queue import logging + +from queue import Queue from logging.handlers import SysLogHandler +from vyos.ifconfig.vrrp import VRRP from vyos.util import cmd # configure logging @@ -60,7 +60,7 @@ class KeepalivedFifo: def _config_load(self): try: # read the dictionary file with configuration - with open('/run/keepalived_config.dict', 'r') as dict_file: + with open(VRRP.location['vyos'], 'r') as dict_file: vrrp_config_dict = json.load(dict_file) self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}} # save VRRP instances to the new dictionary @@ -93,8 +93,8 @@ class KeepalivedFifo: # create FIFO pipe def pipe_create(self): - if Path(self.pipe_path).exists(): - logger.info("PIPE already exist: {}".format(self.pipe_path)) + if os.path.exists(self.pipe_path): + logger.info(f"PIPE already exist: {self.pipe_path}") else: os.mkfifo(self.pipe_path) -- cgit v1.2.3 From 260f383221ea1b23e644b0c50f45eeb300e9bc24 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 21 Sep 2021 22:33:07 +0200 Subject: vrrp: keepalived: T616: drop /etc/default/keepalived This is a follow-up commit to 65398e5c8 ("vrrp: keepalived: T616: move configuration to volatile /run directory") as it makes no sense to store a static /etc/default/keepalived file marked as "Autogenerated by VyOS" that only enabled the SNMP option to keepalived. Better pass the --snmp switch via the systemd override file and drop all other references/files. --- data/templates/vrrp/daemon.tmpl | 5 ----- python/vyos/ifconfig/vrrp.py | 1 - src/conf_mode/vrrp.py | 1 - src/etc/systemd/system/keepalived.service.d/override.conf | 2 +- 4 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 data/templates/vrrp/daemon.tmpl (limited to 'src') diff --git a/data/templates/vrrp/daemon.tmpl b/data/templates/vrrp/daemon.tmpl deleted file mode 100644 index c9dbea72d..000000000 --- a/data/templates/vrrp/daemon.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -# Autogenerated by VyOS -# Options to pass to keepalived - -# DAEMON_ARGS are appended to the keepalived command-line -DAEMON_ARGS="--snmp" diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index 481b0284a..3d6f4d7c6 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -37,7 +37,6 @@ class VRRP(object): 'state': '/tmp/keepalived.data', 'stats': '/tmp/keepalived.stats', 'json': '/tmp/keepalived.json', - 'daemon': '/etc/default/keepalived', 'config': '/run/keepalived/keepalived.conf', 'vyos': '/run/keepalived/keepalived_config.dict', } diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index cee6a9ba2..55c4cc67a 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -231,7 +231,6 @@ def generate(data): render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', {"groups": vrrp_groups, "sync_groups": sync_groups}) - render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {}) return None diff --git a/src/etc/systemd/system/keepalived.service.d/override.conf b/src/etc/systemd/system/keepalived.service.d/override.conf index e338b90a2..c18ae0c29 100644 --- a/src/etc/systemd/system/keepalived.service.d/override.conf +++ b/src/etc/systemd/system/keepalived.service.d/override.conf @@ -7,6 +7,6 @@ After=vyos-router.service [Service] KillMode=process ExecStart= -ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork $DAEMON_ARGS +ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp PIDFile= PIDFile=/run/keepalived/keepalived.pid -- cgit v1.2.3 From 1121bed93cf79b838babf73852a456820b865305 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 22 Sep 2021 08:31:53 +0200 Subject: vrrp: keepalived: T616: bugfix for invalid os.unlink() Commit 260f3832 ("vrrp: keepalived: T616: drop /etc/default/keepalived") dropped the old daemon configuration but there was one line of code that tried to delete the file which was no longer present. This resulted in: KeyError: 'daemon' --- src/conf_mode/vrrp.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src') diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 55c4cc67a..71f3ddb84 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -245,19 +245,14 @@ def apply(data): print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err)) if not VRRP.is_running(): - print("Starting the VRRP process") ret = call("systemctl restart keepalived.service") else: - print("Reloading the VRRP process") ret = call("systemctl reload keepalived.service") if ret != 0: raise ConfigError("keepalived failed to start") else: - # VRRP is removed in the commit - print("Stopping the VRRP process") call("systemctl stop keepalived.service") - os.unlink(VRRP.location['daemon']) return None -- cgit v1.2.3 From bfe59076f8075083920143cfb4ae22617aa0c663 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 26 Sep 2021 10:57:56 +0200 Subject: op-mode: reboot/poweroff: T3857: send wall message to all users (cherry picked from commit 0ee26592772a14e829d9d1f8e64f9db875f31a63) --- src/op_mode/powerctrl.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index c000d7d06..ffcb865a8 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -92,37 +92,40 @@ def cancel_shutdown(): try: run('/sbin/shutdown -c --no-wall') except OSError as e: - exit("Could not cancel a reboot or poweroff: %s" % e) + exit(f'Could not cancel a reboot or poweroff: {e}') - message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow) + mode = output['MODE'] + message = f'Scheduled {mode} has been cancelled {timenow}' run(f'wall {message} > /dev/null 2>&1') else: print("Reboot or poweroff is not scheduled") def execute_shutdown(time, reboot=True, ask=True): + action = "reboot" if reboot else "poweroff" if not ask: - action = "reboot" if reboot else "poweroff" - if not ask_yes_no("Are you sure you want to %s this system?" % action): + if not ask_yes_no(f"Are you sure you want to {action} this system?"): exit(0) - - action = "-r" if reboot else "-P" + action_cmd = "-r" if reboot else "-P" if len(time) == 0: # T870 legacy reboot job support chk_vyatta_based_reboots() ### - out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) + out = cmd(f'/sbin/shutdown {action_cmd} now', stderr=STDOUT) print(out.split(",", 1)[0]) return elif len(time) == 1: # Assume the argument is just time ts = parse_time(time[0]) if ts: - cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + cmd(f'/sbin/shutdown {action_cmd} {time[0]}', stderr=STDOUT) + # Inform all other logged in users about the reboot/shutdown + wall_msg = f'System {action} is scheduled {time[0]}' + cmd(f'/usr/bin/wall "{wall_msg}"') else: - exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + exit(f'Invalid time "{time[0]}". The valid format is HH:MM') elif len(time) == 2: # Assume it's date and time ts = parse_time(time[0]) @@ -131,14 +134,18 @@ def execute_shutdown(time, reboot=True, ask=True): t = datetime.combine(ds, ts) td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + + cmd(f'/sbin/shutdown {action_cmd} {t2}', stderr=STDOUT) + # Inform all other logged in users about the reboot/shutdown + wall_msg = f'System {action} is scheduled {time[1]} {time[0]}' + cmd(f'/usr/bin/wall "{wall_msg}"') else: if not ts: - exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + exit(f'Invalid time "{time[0]}". The valid format is HH:MM') else: - exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) + exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]') else: - exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") + exit('Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM') check_shutdown() -- cgit v1.2.3 From aa1362cb4fbb53c6b45e0739b143940f6ec5d85f Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 26 Sep 2021 10:11:23 -0500 Subject: T3866: ignore interfaces without "address" in DNS forwarding migration --- src/migration-scripts/dns-forwarding/1-to-2 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 index 8c4f4b5c7..ba10c26f2 100755 --- a/src/migration-scripts/dns-forwarding/1-to-2 +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -67,8 +67,14 @@ if config.exists(base + ['listen-on']): # retrieve corresponding interface addresses in CIDR format # those need to be converted in pure IP addresses without network information path = ['interfaces', section, intf, 'address'] - for addr in config.return_values(path): - listen_addr.append( ip_interface(addr).ip ) + try: + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) + except: + # Some interface types do not use "address" option (e.g. OpenVPN) + # and may not even have a fixed address + print("Could not retrieve the address of the interface {} from the config".format(intf)) + print("You will need to update your DNS forwarding configuration manually") for addr in listen_addr: config.set(base + ['listen-address'], value=addr, replace=False) -- cgit v1.2.3 From 2224130742d2867bb12b81deac8972d17920d9d7 Mon Sep 17 00:00:00 2001 From: zsdc Date: Tue, 28 Sep 2021 12:50:30 +0300 Subject: dhclient: T3852: Fixed dhclient processes search Backported commits: 13abffe43b2a5c41bb4ec4675c227f6cf1f868da 01158a8eaa574c48c726c20693479e4aa6e18ee6 This allows finding all running dhclient processes properly. --- .../dhclient-enter-hooks.d/02-vyos-stopdhclient | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient index 939055a63..f737148dc 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient +++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient @@ -2,26 +2,35 @@ if [ -z ${CONTROLLED_STOP} ] ; then # stop dhclient for this interface, if it is not current one # get PID for current dhclient - current_dhclient=`ps --no-headers --format ppid --pid $$ | awk '{ print $1 }'` + current_dhclient=`ps --no-headers --format ppid --pid $$ | awk '{ print \$1 }'` # get PID for master process (current can be a fork) - master_dhclient=`ps --no-headers --format ppid --pid $current_dhclient | awk '{ print $1 }'` + master_dhclient=`ps --no-headers --format ppid --pid $current_dhclient | awk '{ print \$1 }'` # get IP version for current dhclient - ipversion_arg=`ps --no-headers --format args --pid $current_dhclient | awk '{ print $2 }'` + ipversion_arg=`ps --no-headers --format args --pid $current_dhclient | awk 'match(\$0, /\s-(4|6)\s/, IPV) { printf("%s", IPV[1]) }'` # get list of all dhclient running for current interface - dhclients_pids=(`pgrep -f "dhclient $ipversion_arg.* $interface(\s|$)"`) + if [[ $ipversion_arg == "6" ]]; then + dhclients_pids=(`pgrep -f "dhclient.*\s-6\s.*\s$interface(\s|$)"`) + else + dhclients_pids=(`ps --no-headers --format pid,args -C dhclient | awk "{ if(match(\\$0, /\s${interface}(\s|$)/) && !match(\\$0, /\s-6\s/)) printf(\"%s\n\", \\$1) }"`) + fi logmsg info "Current dhclient PID: $current_dhclient, Parent PID: $master_dhclient, IP version: $ipversion_arg, All dhclients for interface $interface: ${dhclients_pids[@]}" # stop all dhclients for current interface, except current one for dhclient in ${dhclients_pids[@]}; do if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then - logmsg info "Stopping dhclient with PID: ${dhclient}" # get path to PID-file of dhclient process - local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match($0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'` + local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'` # stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill - dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile + logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: $dhclient_pidfile" + if [[ -e $dhclient_pidfile ]]; then + dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile + else + logmsg error "PID file $dhclient_pidfile does not exists, killing dhclient with SIGTERM signal" + kill -s 15 ${dhclient} + fi fi done fi -- cgit v1.2.3 From 3c73edd96568b77aa0efc60a70babeea5d5515b4 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Fri, 16 Apr 2021 09:04:22 -0500 Subject: vrrp: T3877: backport handlers to solve "default rfc3768-compatibility" issue Do not create rfc3768-compatibility interfaces by default because of wrong Jinja2 syntax. Backporting the entire system makes it easier in the future to additional bugfixes. --- data/templates/vrrp/keepalived.conf.tmpl | 124 +++++------ interface-definitions/vrrp.xml.in | 5 + src/conf_mode/vrrp.py | 342 +++++++++++-------------------- src/system/keepalived-fifo.py | 81 ++++---- 4 files changed, 235 insertions(+), 317 deletions(-) (limited to 'src') diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl index 6b0f8e58e..7b8f7cb09 100644 --- a/data/templates/vrrp/keepalived.conf.tmpl +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -12,84 +12,94 @@ global_defs { notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py } -{% for group in groups %} -{% if group.health_check_script is defined and group.health_check_script is not none %} -vrrp_script healthcheck_{{ group.name }} { - script "{{ group.health_check_script }}" - interval {{ group.health_check_interval }} - fall {{ group.health_check_count }} +{% if group is defined and group is not none %} +{% for name, group_config in group.items() if group_config.disable is not defined %} +{% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none %} +vrrp_script healthcheck_{{ name }} { + script "{{ group_config.health_check.script }}" + interval {{ group_config.health_check.interval }} + fall {{ group_config.health_check.failure_count }} rise 1 } -{% endif %} - -vrrp_instance {{ group.name }} { - {{ '# ' ~ group.description if group.description is defined }} +{% endif %} +vrrp_instance {{ name }} { +{% if group_config.description is defined and group_config.description is not none %} + # {{ group_config.description }} +{% endif %} state BACKUP - interface {{ group.interface }} - virtual_router_id {{ group.vrid }} - priority {{ group.priority }} - advert_int {{ group.advertise_interval }} -{% if group.preempt is defined and group.preempt is not none %} - preempt_delay {{ group.preempt_delay }} -{% else %} + interface {{ group_config.interface }} + virtual_router_id {{ group_config.vrid }} + priority {{ group_config.priority }} + advert_int {{ group_config.advertise_interval }} +{% if group_config.no_preempt is not defined and group_config.preempt_delay is defined and group_config.preempt_delay is not none %} + preempt_delay {{ group_config.preempt_delay }} +{% elif group_config.no_preempt is defined %} nopreempt -{% endif %} -{% if group.peer_address is defined and group.peer_address is not none %} - unicast_peer { {{ group.peer_address }} } -{% endif %} -{% if group.hello_source is defined and group.hello_source is not none %} -{% if group.peer_address is defined and group.peer_address is not none %} - unicast_src_ip {{ group.hello_source }} -{% else %} - mcast_src_ip {{ group.hello_source }} {% endif %} -{% endif %} -{% if group.use_vmac is defined and group.peer_address is defined %} - use_vmac {{ group.interface }}v{{ group.vrid }} +{% if group_config.peer_address is defined and group_config.peer_address is not none %} + unicast_peer { {{ group_config.peer_address }} } +{% endif %} +{% if group_config.hello_source_address is defined and group_config.hello_source_address is not none %} +{% if group_config.peer_address is defined and group_config.peer_address is not none %} + unicast_src_ip {{ group_config.hello_source_address }} +{% else %} + mcast_src_ip {{ group_config.hello_source_address }} +{% endif %} +{% endif %} +{% if group_config.rfc3768_compatibility is defined and group_config.peer_address is defined %} + use_vmac {{ group_config.interface }}v{{ group_config.vrid }} vmac_xmit_base -{% elif group.use_vmac is defined %} - use_vmac {{ group.interface }}v{{ group.vrid }} -{% endif %} -{% if group.auth_password is defined and group.auth_password is not none %} +{% elif group_config.rfc3768_compatibility is defined %} + use_vmac {{ group_config.interface }}v{{ group_config.vrid }} +{% endif %} +{% if group_config.authentication is defined and group_config.authentication is not none %} authentication { - auth_pass "{{ group.auth_password }}" - auth_type {{ group.auth_type }} + auth_pass "{{ group_config.authentication.password }}" +{% if group_config.authentication.type == 'plaintext-password' %} + auth_type PASS +{% else %} + auth_type {{ group_config.authentication.type | upper }} +{% endif %} } -{% endif %} -{% if group.virtual_addresses is defined and group.virtual_addresses is not none %} +{% endif %} +{% if group_config.virtual_address is defined and group_config.virtual_address is not none %} virtual_ipaddress { -{% for addr in group.virtual_addresses %} +{% for addr in group_config.virtual_address %} {{ addr }} -{% endfor %} +{% endfor %} } -{% endif %} -{% if group.virtual_addresses_excluded is defined and group.virtual_addresses_excluded is not none %} +{% endif %} +{% if group_config.virtual_address_excluded is defined and group_config.virtual_address_excluded is not none %} virtual_ipaddress_excluded { -{% for addr in group.virtual_addresses_excluded %} +{% for addr in group_config.virtual_address_excluded %} {{ addr }} -{% endfor %} +{% endfor %} } -{% endif %} -{% if group.health_check_script is defined and group.health_check_script is not none %} +{% endif %} +{% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none %} track_script { - healthcheck_{{ group.name }} + healthcheck_{{ name }} } -{% endif %} +{% endif %} } -{% endfor %} +{% endfor %} +{% endif %} -{% if sync_groups is defined and sync_groups is not none %} -{% for sync_group in sync_groups %} -vrrp_sync_group {{ sync_group.name }} { +{% if sync_group is defined and sync_group is not none %} +{% for name, group_config in sync_group.items() if group_config.disable is not defined %} +vrrp_sync_group {{ name }} { group { -{% for member in sync_group.members %} +{% if group_config.member is defined and group_config.member is not none %} +{% for member in group_config.member %} {{ member }} -{% endfor %} +{% endfor %} +{% endif %} } -{% if sync_group.conntrack_sync %} - notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}" - notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}" - notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}" +{% if conntrack_sync_group is defined and conntrack_sync_group == name %} +{% set vyos_helper = "/usr/libexec/vyos/vyos-vrrp-conntracksync.sh" %} + notify_master "{{ vyos_helper }} master {{ name }}" + notify_backup "{{ vyos_helper }} backup {{ name }}" + notify_fault "{{ vyos_helper }} fault {{ name }}" {% endif %} } {% endfor %} diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in index 594a32120..b58cf735c 100644 --- a/interface-definitions/vrrp.xml.in +++ b/interface-definitions/vrrp.xml.in @@ -35,6 +35,7 @@ + 1 @@ -94,6 +95,7 @@ + 3 @@ -102,6 +104,7 @@ + 60 @@ -164,6 +167,7 @@ + 0 @@ -176,6 +180,7 @@ + 100 diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 71f3ddb84..eaf348774 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -17,246 +17,140 @@ import os from sys import exit -from ipaddress import ip_address from ipaddress import ip_interface from ipaddress import IPv4Interface from ipaddress import IPv6Interface -from ipaddress import IPv4Address -from ipaddress import IPv6Address -from json import dumps -from pathlib import Path - -import vyos.config - -from vyos import ConfigError -from vyos.util import call -from vyos.util import makedir -from vyos.template import render +from vyos.config import Config +from vyos.configdict import dict_merge from vyos.ifconfig.vrrp import VRRP - +from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): - vrrp_groups = [] - sync_groups = [] - if config: - config = config + conf = config else: - config = vyos.config.Config() - - # Get the VRRP groups - for group_name in config.list_nodes("high-availability vrrp group"): - config.set_level("high-availability vrrp group {0}".format(group_name)) - - # Retrieve the values - group = {"preempt": True, "use_vmac": False, "disable": False} - - if config.exists("disable"): - group["disable"] = True - - group["name"] = group_name - group["vrid"] = config.return_value("vrid") - group["interface"] = config.return_value("interface") - group["description"] = config.return_value("description") - group["advertise_interval"] = config.return_value("advertise-interval") - group["priority"] = config.return_value("priority") - group["hello_source"] = config.return_value("hello-source-address") - group["peer_address"] = config.return_value("peer-address") - group["sync_group"] = config.return_value("sync-group") - group["preempt_delay"] = config.return_value("preempt-delay") - group["virtual_addresses"] = config.return_values("virtual-address") - group["virtual_addresses_excluded"] = config.return_values("virtual-address-excluded") - - group["auth_password"] = config.return_value("authentication password") - group["auth_type"] = config.return_value("authentication type") - - group["health_check_script"] = config.return_value("health-check script") - group["health_check_interval"] = config.return_value("health-check interval") - group["health_check_count"] = config.return_value("health-check failure-count") - - group["master_script"] = config.return_value("transition-script master") - group["backup_script"] = config.return_value("transition-script backup") - group["fault_script"] = config.return_value("transition-script fault") - group["stop_script"] = config.return_value("transition-script stop") - group["script_mode_force"] = config.exists("transition-script mode-force") - - if config.exists("no-preempt"): - group["preempt"] = False - if config.exists("rfc3768-compatibility"): - group["use_vmac"] = True - - # Substitute defaults where applicable - if not group["advertise_interval"]: - group["advertise_interval"] = 1 - if not group["priority"]: - group["priority"] = 100 - if not group["preempt_delay"]: - group["preempt_delay"] = 0 - if not group["health_check_interval"]: - group["health_check_interval"] = 60 - if not group["health_check_count"]: - group["health_check_count"] = 3 - - # FIXUP: translate our option for auth type to keepalived's syntax - # for simplicity - if group["auth_type"]: - if group["auth_type"] == "plaintext-password": - group["auth_type"] = "PASS" - else: - group["auth_type"] = "AH" - - vrrp_groups.append(group) - - config.set_level("") - - # Get the sync group used for conntrack-sync - conntrack_sync_group = None - if config.exists("service conntrack-sync failover-mechanism vrrp"): - conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group") - - # Get the sync groups - for sync_group_name in config.list_nodes("high-availability vrrp sync-group"): - config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name)) - - sync_group = {"conntrack_sync": False} - sync_group["name"] = sync_group_name - sync_group["members"] = config.return_values("member") - if conntrack_sync_group: - if conntrack_sync_group == sync_group_name: - sync_group["conntrack_sync"] = True - - # add transition script configuration - sync_group["master_script"] = config.return_value("transition-script master") - sync_group["backup_script"] = config.return_value("transition-script backup") - sync_group["fault_script"] = config.return_value("transition-script fault") - sync_group["stop_script"] = config.return_value("transition-script stop") - - sync_groups.append(sync_group) - - # create a file with dict with proposed configuration - dirname = os.path.dirname(VRRP.location['vyos']) - makedir(dirname) - with open(VRRP.location['vyos'] + ".temp", 'w') as dict_file: - dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) - - return (vrrp_groups, sync_groups) - - -def verify(data): - vrrp_groups, sync_groups = data - - for group in vrrp_groups: - # Check required fields - if not group["vrid"]: - raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"])) - if not group["interface"]: - raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"])) - if not group["virtual_addresses"]: - raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"])) - - if group["auth_password"] and (not group["auth_type"]): - raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"])) - - # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction - - # XXX: filter on map object is destructive, so we force it to list. - # Additionally, filter objects always evaluate to True, empty or not, - # so we force them to lists as well. - vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"])) - vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) - vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) - - if vaddrs4 and vaddrs6: - raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"])) - - if vaddrs4: - if group["hello_source"]: - hsa = ip_address(group["hello_source"]) - if isinstance(hsa, IPv6Address): - raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"])) - if group["peer_address"]: - pa = ip_address(group["peer_address"]) - if isinstance(pa, IPv6Address): - raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"])) - - if vaddrs6: - if group["hello_source"]: - hsa = ip_address(group["hello_source"]) - if isinstance(hsa, IPv4Address): - raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"])) - if group["peer_address"]: - pa = ip_address(group["peer_address"]) - if isinstance(pa, IPv4Address): - raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"])) - - # Warn the user about the deprecated mode-force option - if group["script_mode_force"]: - print("""Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.""") - print("""It's no longer necessary, so you can safely remove it from your config now.""") - - # Disallow same VRID on multiple interfaces - _groups = sorted(vrrp_groups, key=(lambda x: x["interface"])) - count = len(_groups) - 1 - index = 0 - while (index < count): - if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]): - raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format( - _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"])) - else: - index += 1 + conf = Config() + + base = ['high-availability', 'vrrp'] + if not conf.exists(base): + return None + + vrrp = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + if 'group' in vrrp: + default_values = defaults(base + ['group']) + for group in vrrp['group']: + vrrp['group'][group] = dict_merge(default_values, vrrp['group'][group]) + + ## Get the sync group used for conntrack-sync + conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group'] + if conf.exists(conntrack_path): + vrrp['conntrack_sync_group'] = conf.return_value(conntrack_path) + + import pprint + pprint.pprint(vrrp) + return vrrp + +def verify(vrrp): + if not vrrp: + return None + + used_vrid_if = [] + if 'group' in vrrp: + for group, group_config in vrrp['group'].items(): + # Check required fields + if 'vrid' not in group_config: + raise ConfigError(f'VRID is required but not set in VRRP group "{group}"') + + if 'interface' not in group_config: + raise ConfigError(f'Interface is required but not set in VRRP group "{group}"') + + if 'virtual_address' not in group_config: + raise ConfigError(f'Virtual IP address is required but not set in VRRP group "{group}"') + + if 'authentication' in group_config: + if not {'password', 'type'} <= set(group_config['authentication']): + raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') + + # We can not use a VRID once per interface + interface = group_config['interface'] + vrid = group_config['vrid'] + tmp = {'interface': interface, 'vrid': vrid} + if tmp in used_vrid_if: + raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!') + used_vrid_if.append(tmp) + + # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction + + # XXX: filter on map object is destructive, so we force it to list. + # Additionally, filter objects always evaluate to True, empty or not, + # so we force them to lists as well. + vaddrs = list(map(lambda i: ip_interface(i), group_config['virtual_address'])) + vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) + vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) + + if vaddrs4 and vaddrs6: + raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \ + 'Create individual groups for IPv4 and IPv6!') + if vaddrs4: + if 'hello_source_address' in group_config: + if is_ipv6(group_config['hello_source_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!') + + if 'peer_address' in group_config: + if is_ipv6(group_config['peer_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') + + if vaddrs6: + if 'hello_source_address' in group_config: + if is_ipv4(group_config['hello_source_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!') + + if 'peer_address' in group_config: + if is_ipv4(group_config['peer_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') + + + # Warn the user about the deprecated mode-force option + if 'transition_script' in group_config and 'mode_force' in group_config['transition_script']: + print('Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.') + print('It is no longer necessary, so you can safely remove it from your config now.') # Check sync groups - vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups)) - - for sync_group in sync_groups: - for m in sync_group["members"]: - if not (m in vrrp_group_names): - raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m)) - - -def generate(data): - vrrp_groups, sync_groups = data - - # Remove disabled groups from the sync group member lists - for sync_group in sync_groups: - for member in sync_group["members"]: - g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0] - if g["disable"]: - print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"])) - # Filter out disabled groups - vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - - render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', - {"groups": vrrp_groups, "sync_groups": sync_groups}) + if 'sync_group' in vrrp: + for sync_group, sync_config in vrrp['sync_group'].items(): + if 'member' in sync_config: + for member in sync_config['member']: + if member not in vrrp['group']: + raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\ + 'but it does not exist!') + +def generate(vrrp): + if not vrrp: + return None + + render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', vrrp) return None +def apply(vrrp): + service_name = 'keepalived.service' + if not vrrp: + call(f'systemctl stop {service_name}') + return None -def apply(data): - vrrp_groups, sync_groups = data - if vrrp_groups: - # safely rename a temporary file with configuration dict - try: - dict_file = Path("{}.temp".format(VRRP.location['vyos'])) - dict_file.rename(Path(VRRP.location['vyos'])) - except Exception as err: - print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err)) - - if not VRRP.is_running(): - ret = call("systemctl restart keepalived.service") - else: - ret = call("systemctl reload keepalived.service") - - if ret != 0: - raise ConfigError("keepalived failed to start") - else: - call("systemctl stop keepalived.service") - + call(f'systemctl restart {service_name}') return None - if __name__ == '__main__': try: c = get_config() @@ -264,5 +158,5 @@ if __name__ == '__main__': generate(c) apply(c) except ConfigError as e: - print("VRRP error: {0}".format(str(e))) + print(e) exit(1) diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index 1e749207b..1fba0d75b 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -27,6 +27,7 @@ from queue import Queue from logging.handlers import SysLogHandler from vyos.ifconfig.vrrp import VRRP +from vyos.configquery import ConfigTreeQuery from vyos.util import cmd # configure logging @@ -37,17 +38,20 @@ logs_handler_syslog.setFormatter(logs_format) logger.addHandler(logs_handler_syslog) logger.setLevel(logging.DEBUG) +mdns_running_file = '/run/mdns_vrrp_active' +mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns-repeater.py' # class for all operations class KeepalivedFifo: # init - read command arguments def __init__(self): - logger.info("Starting FIFO pipe for Keepalived") + logger.info('Starting FIFO pipe for Keepalived') # define program arguments cmd_args_parser = argparse.ArgumentParser(description='Create FIFO pipe for keepalived and process notify events', add_help=False) cmd_args_parser.add_argument('PIPE', help='path to the FIFO pipe') # parse arguments cmd_args = cmd_args_parser.parse_args() + self._config_load() self.pipe_path = cmd_args.PIPE @@ -59,33 +63,34 @@ class KeepalivedFifo: # load configuration def _config_load(self): try: - # read the dictionary file with configuration - with open(VRRP.location['vyos'], 'r') as dict_file: - vrrp_config_dict = json.load(dict_file) + base = ['high-availability', 'vrrp'] + conf = ConfigTreeQuery() + if not conf.exists(base): + raise ValueError() + + # Read VRRP configuration directly from CLI + vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}} - # save VRRP instances to the new dictionary - for vrrp_group in vrrp_config_dict['vrrp_groups']: - self.vrrp_config['vrrp_groups'][vrrp_group['name']] = { - 'STOP': vrrp_group.get('stop_script'), - 'FAULT': vrrp_group.get('fault_script'), - 'BACKUP': vrrp_group.get('backup_script'), - 'MASTER': vrrp_group.get('master_script') - } - # save VRRP sync groups to the new dictionary - for sync_group in vrrp_config_dict['sync_groups']: - self.vrrp_config['sync_groups'][sync_group['name']] = { - 'STOP': sync_group.get('stop_script'), - 'FAULT': sync_group.get('fault_script'), - 'BACKUP': sync_group.get('backup_script'), - 'MASTER': sync_group.get('master_script') - } - logger.debug("Loaded configuration: {}".format(self.vrrp_config)) + for key in ['group', 'sync_group']: + if key not in vrrp_config_dict: + continue + for group, group_config in vrrp_config_dict[key].items(): + if 'transition_script' not in group_config: + continue + self.vrrp_config['vrrp_groups'][group] = { + 'STOP': group_config['transition_script'].get('stop'), + 'FAULT': group_config['transition_script'].get('fault'), + 'BACKUP': group_config['transition_script'].get('backup'), + 'MASTER': group_config['transition_script'].get('master'), + } + logger.info(f'Loaded configuration: {self.vrrp_config}') except Exception as err: - logger.error("Unable to load configuration: {}".format(err)) + logger.error(f'Unable to load configuration: {err}') # run command def _run_command(self, command): - logger.debug("Running the command: {}".format(command)) + logger.debug(f'Running the command: {command}') try: cmd(command) except OSError as err: @@ -94,13 +99,13 @@ class KeepalivedFifo: # create FIFO pipe def pipe_create(self): if os.path.exists(self.pipe_path): - logger.info(f"PIPE already exist: {self.pipe_path}") + logger.info(f'PIPE already exist: {self.pipe_path}') else: os.mkfifo(self.pipe_path) # process message from pipe def pipe_process(self): - logger.debug("Message processing start") + logger.debug('Message processing start') regex_notify = re.compile(r'^(?P\w+) "(?P[\w-]+)" (?P\w+) (?P\d+)$', re.MULTILINE) while self.stopme.is_set() is False: # wait for a new message event from pipe_wait @@ -111,16 +116,19 @@ class KeepalivedFifo: # get all messages from queue and try to process them while self.message_queue.empty() is not True: message = self.message_queue.get() - logger.debug("Received message: {}".format(message)) + logger.debug(f'Received message: {message}') notify_message = regex_notify.search(message) # try to process a message if it looks valid if notify_message: n_type = notify_message.group('type') n_name = notify_message.group('name') n_state = notify_message.group('state') - logger.info("{} {} changed state to {}".format(n_type, n_name, n_state)) + logger.info(f'{n_type} {n_name} changed state to {n_state}') # check and run commands for VRRP instances if n_type == 'INSTANCE': + if os.path.exists(mdns_running_file): + cmd(mdns_update_command) + if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]: n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state) if n_script: @@ -128,6 +136,9 @@ class KeepalivedFifo: # check and run commands for VRRP sync groups # currently, this is not available in VyOS CLI if n_type == 'GROUP': + if os.path.exists(mdns_running_file): + cmd(mdns_update_command) + if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]: n_script = self.vrrp_config['sync_groups'][n_name].get(n_state) if n_script: @@ -135,16 +146,16 @@ class KeepalivedFifo: # mark task in queue as done self.message_queue.task_done() except Exception as err: - logger.error("Error processing message: {}".format(err)) - logger.debug("Terminating messages processing thread") + logger.error(f'Error processing message: {err}') + logger.debug('Terminating messages processing thread') # wait for messages def pipe_wait(self): - logger.debug("Message reading start") + logger.debug('Message reading start') self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK) while self.stopme.is_set() is False: # sleep a bit to not produce 100% CPU load - time.sleep(0.1) + time.sleep(0.250) try: # try to read a message from PIPE message = os.read(self.pipe_read, 500) @@ -157,21 +168,19 @@ class KeepalivedFifo: except Exception as err: # ignore the "Resource temporarily unavailable" error if err.errno != 11: - logger.error("Error receiving message: {}".format(err)) + logger.error(f'Error receiving message: {err}') - logger.debug("Closing FIFO pipe") + logger.debug('Closing FIFO pipe') os.close(self.pipe_read) - # handle SIGTERM signal to allow finish all messages processing def sigterm_handle(signum, frame): - logger.info("Ending processing: Received SIGTERM signal") + logger.info('Ending processing: Received SIGTERM signal') fifo.stopme.set() thread_wait_message.join() fifo.message_event.set() thread_process_message.join() - signal.signal(signal.SIGTERM, sigterm_handle) # init our class -- cgit v1.2.3 From 0537e9f7eb6d6a3fadb8b28245a9450821e601ac Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 1 Oct 2021 15:00:53 +0200 Subject: vrrp: T3877: remove debug output --- src/conf_mode/vrrp.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index eaf348774..c906bdfcd 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -57,8 +57,6 @@ def get_config(config=None): if conf.exists(conntrack_path): vrrp['conntrack_sync_group'] = conf.return_value(conntrack_path) - import pprint - pprint.pprint(vrrp) return vrrp def verify(vrrp): -- cgit v1.2.3 From 8e6c48563d1612916bd7fcc665d70bfa77ec5667 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 2 Oct 2021 12:02:57 +0200 Subject: dns: forwarding: T3882: remove deprecated code to work with PowerDNS 4.5 --- data/templates/dns-forwarding/recursor.conf.tmpl | 3 +-- src/conf_mode/dns_forwarding.py | 15 --------------- 2 files changed, 1 insertion(+), 17 deletions(-) (limited to 'src') diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl index 8799718b0..d460775c0 100644 --- a/data/templates/dns-forwarding/recursor.conf.tmpl +++ b/data/templates/dns-forwarding/recursor.conf.tmpl @@ -10,8 +10,7 @@ threads=1 allow-from={{ allow_from | join(',') }} log-common-errors=yes non-local-bind=yes -query-local-address={{ source_address_v4 | join(',') }} -query-local-address6={{ source_address_v6 | join(',') }} +query-local-address={{ source_address | join(',') }} lua-config-file=recursor.conf.lua # cache-size diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index c44e6c974..06366362a 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -66,21 +66,6 @@ def get_config(config=None): if conf.exists(base_nameservers_dhcp): dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)}) - # Split the source_address property into separate IPv4 and IPv6 lists - # NOTE: In future versions of pdns-recursor (> 4.4.0), this logic can be removed - # as both IPv4 and IPv6 addresses can be specified in a single setting. - source_address_v4 = [] - source_address_v6 = [] - - for source_address in dns['source_address']: - if is_ipv6(source_address): - source_address_v6.append(source_address) - else: - source_address_v4.append(source_address) - - dns.update({'source_address_v4': source_address_v4}) - dns.update({'source_address_v6': source_address_v6}) - return dns def verify(dns): -- cgit v1.2.3 From d4c5b4a87fc61310b9bb5579145e1c5978ca1f60 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 4 Oct 2021 19:44:23 +0200 Subject: T3889: Revert "dhcpv6-pd: T421: disable wide dhcpv6 client debug messages" This reverts commit 184f25819fa43fc892b97c0044813b8aa56855b4. --- src/systemd/dhcp6c@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service index fdd6d7d88..9a97ee261 100644 --- a/src/systemd/dhcp6c@.service +++ b/src/systemd/dhcp6c@.service @@ -9,7 +9,7 @@ StartLimitIntervalSec=0 WorkingDirectory=/run/dhcp6c Type=forking PIDFile=/run/dhcp6c/dhcp6c.%i.pid -ExecStart=/usr/sbin/dhcp6c -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i +ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i Restart=on-failure RestartSec=20 -- cgit v1.2.3 From fac3b8fe86700c581fc8b73574a3b9c79a530bb3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 8 Oct 2021 21:17:52 +0200 Subject: tunnel: T3893: harden logic when validating tunnel parameters Different types of tunnels have different keys set in get_interface_config(). Thus it should be properly verified (by e.g. using dict_search()) that the key in question esits to not raise KeyError. (cherry picked from commit 5aadf673497b93e2d4ad304e567de1cd571f9e25) --- src/conf_mode/interfaces-tunnel.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 22a9f0e18..5fa165190 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -88,18 +88,17 @@ def verify(tunnel): # Prevent the same key for 2 tunnels with same source-address/encap. T2920 for tunnel_if in Section.interfaces('tunnel'): tunnel_cfg = get_interface_config(tunnel_if) - exist_encap = tunnel_cfg['linkinfo']['info_kind'] - exist_source_address = tunnel_cfg['address'] - exist_key = tunnel_cfg['linkinfo']['info_data']['ikey'] + # no match on encapsulation - bail out + if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: + continue new_source_address = tunnel['source_address'] # Convert tunnel key to ip key, format "ip -j link show" # 1 => 0.0.0.1, 999 => 0.0.3.231 - orig_new_key = int(tunnel['parameters']['ip']['key']) - new_key = IPv4Address(orig_new_key) + orig_new_key = dict_search('parameters.ip.key', tunnel) + new_key = IPv4Address(int(orig_new_key)) new_key = str(new_key) - if tunnel['encapsulation'] == exist_encap and \ - new_source_address == exist_source_address and \ - new_key == exist_key: + if dict_search('address', tunnel_cfg) == new_source_address and \ + dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key: raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ f'is already used for tunnel "{tunnel_if}"!') -- cgit v1.2.3 From 67b3dd6b4715fef266eb47e68623944f8be617e0 Mon Sep 17 00:00:00 2001 From: Ross Dougherty Date: Wed, 20 Oct 2021 23:50:51 +1100 Subject: dhclient hooks: T3920: avoid 'too many args' error when no vrf --- src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index edb7c7b27..f060c6ee8 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -19,7 +19,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then for router in $old_routers; do # check if we are bound to a VRF local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//') - if [ -n $vrf_name ]; then + if [ "$vrf_name" != "*" ]; then vrf="vrf $vrf_name" fi -- cgit v1.2.3 From 2ffd79535058f39e033871f187f3b69186aa6c0d Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Wed, 20 Oct 2021 17:49:15 +0000 Subject: dhcpv6-server: T3918: Fix subnets verify raise ConfigError (cherry picked from commit ead10909ba9104733930bb3f59c90610138bd047) --- src/conf_mode/dhcpv6_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 175300bb0..e6a2e4486 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -128,7 +128,7 @@ def verify(dhcpv6): # Subnets must be unique if subnet in subnets: - raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!') subnets.append(subnet) # DHCPv6 requires at least one configured address range or one static mapping -- cgit v1.2.3 From 1312068cb9743dd4d16edd37dbed9c142724997e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 20 Oct 2021 21:20:17 +0200 Subject: tunnel: T3921: bugfix KeyError for source-address --- src/conf_mode/interfaces-tunnel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 5fa165190..4db564e6d 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -78,7 +78,7 @@ def verify(tunnel): # If tunnel source address any and key not set if tunnel['encapsulation'] in ['gre'] and \ - tunnel['source_address'] == '0.0.0.0' and \ + dict_search('source_address', tunnel) == '0.0.0.0' and \ dict_search('parameters.ip.key', tunnel) == None: raise ConfigError('Tunnel parameters ip key must be set!') @@ -91,7 +91,7 @@ def verify(tunnel): # no match on encapsulation - bail out if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: continue - new_source_address = tunnel['source_address'] + new_source_address = dict_search('source_address', tunnel) # Convert tunnel key to ip key, format "ip -j link show" # 1 => 0.0.0.1, 999 => 0.0.3.231 orig_new_key = dict_search('parameters.ip.key', tunnel) -- cgit v1.2.3 From c1015d8ce0013719eb898b60b14ffec192b8141c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 21 Oct 2021 19:38:38 +0200 Subject: tunnel: T3925: dhcp-interface was of no use - use source-interface instead --- interface-definitions/interfaces-tunnel.xml.in | 15 -------- python/vyos/configverify.py | 7 ++-- smoketest/configs/tunnel-broker | 2 +- smoketest/scripts/cli/test_interfaces_tunnel.py | 20 ----------- src/migration-scripts/interfaces/21-to-22 | 46 +++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 41 deletions(-) create mode 100755 src/migration-scripts/interfaces/21-to-22 (limited to 'src') diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index df9b58992..2c15abec7 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -54,21 +54,6 @@ - - - dhcp interface - - interface - DHCP interface that supplies the local IP address for this tunnel - - - - - - - - - Encapsulation of this tunnel interface diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index ce7e76eb4..3aece499e 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -95,15 +95,12 @@ def verify_tunnel(config): raise ConfigError('Must configure the tunnel encapsulation for '\ '{ifname}!'.format(**config)) - if 'source_address' not in config and 'dhcp_interface' not in config: - raise ConfigError('source-address is mandatory for tunnel') + if 'source_address' not in config and 'source_interface' not in config: + raise ConfigError('source-address or source-interface required for tunnel!') if 'remote' not in config and config['encapsulation'] != 'gre': raise ConfigError('remote-ip address is mandatory for tunnel') - if {'source_address', 'dhcp_interface'} <= set(config): - raise ConfigError('Can not use both source-address and dhcp-interface') - if config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: error_ipv6 = 'Encapsulation mode requires IPv6' if 'source_address' in config and not is_ipv6(config['source_address']): diff --git a/smoketest/configs/tunnel-broker b/smoketest/configs/tunnel-broker index d4a5c2dfc..03ac0db41 100644 --- a/smoketest/configs/tunnel-broker +++ b/smoketest/configs/tunnel-broker @@ -56,7 +56,7 @@ interfaces { tunnel tun100 { address 172.16.0.1/30 encapsulation gre-bridge - local-ip 192.0.2.0 + dhcp-interface eth0 remote-ip 192.0.2.100 } tunnel tun200 { diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index 3aed498b4..ff8778828 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -156,26 +156,6 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(self._base_path + [interface]) self.cli_commit() - def test_tunnel_verify_local_dhcp(self): - # We can not use source-address and dhcp-interface at the same time - - interface = f'tun1020' - local_if_addr = f'10.0.0.1/24' - - self.cli_set(self._base_path + [interface, 'address', local_if_addr]) - self.cli_set(self._base_path + [interface, 'encapsulation', 'gre']) - self.cli_set(self._base_path + [interface, 'source-address', self.local_v4]) - self.cli_set(self._base_path + [interface, 'remote', remote_ip4]) - self.cli_set(self._base_path + [interface, 'dhcp-interface', 'eth0']) - - # source-address and dhcp-interface can not be used at the same time - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_delete(self._base_path + [interface, 'dhcp-interface']) - - # Check if commit is ok - self.cli_commit() - def test_tunnel_parameters_gre(self): interface = f'tun1030' gre_key = '10' diff --git a/src/migration-scripts/interfaces/21-to-22 b/src/migration-scripts/interfaces/21-to-22 new file mode 100755 index 000000000..098102102 --- /dev/null +++ b/src/migration-scripts/interfaces/21-to-22 @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 . + +from sys import argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'tunnel'] + +if not config.exists(base): + exit(0) + +for interface in config.list_nodes(base): + path = base + [interface, 'dhcp-interface'] + if config.exists(path): + tmp = config.return_value(path) + config.delete(path) + config.set(base + [interface, 'source-interface'], value=tmp) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3 From f04799dfb638567f72ebf6cd9d1d1d5ba614fdb5 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 21 Oct 2021 16:58:32 +0000 Subject: dhcp: T3626: Prevent to disable only one configured network (cherry picked from commit 9c825a3457a88a4eebc6475f92332822e5102889) --- src/conf_mode/dhcp_server.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 28f2a4ca5..71b71879c 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -151,9 +151,15 @@ def verify(dhcp): listen_ok = False subnets = [] failover_ok = False + shared_networks = len(dhcp['shared_network_name']) + disabled_shared_networks = 0 + # A shared-network requires a subnet definition for network, network_config in dhcp['shared_network_name'].items(): + if 'disable' in network_config: + disabled_shared_networks += 1 + if 'subnet' not in network_config: raise ConfigError(f'No subnets defined for {network}. At least one\n' \ 'lease subnet must be configured.') @@ -243,6 +249,10 @@ def verify(dhcp): if net.overlaps(net2): raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!') + # Prevent 'disable' for shared-network if only one network is configured + if (shared_networks - disabled_shared_networks) < 1: + raise ConfigError(f'At least one shared network must be active!') + if 'failover' in dhcp: if not failover_ok: raise ConfigError('DHCP failover must be enabled for at least one subnet!') -- cgit v1.2.3 From 28d1f0252a10fe71abc8fdda1509295eeb97ee7c Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 21 Oct 2021 17:25:47 +0000 Subject: dhcp-server: T3610: Allow configuration for non-primary ip address (cherry picked from commit 78cfb949cc6bceab744271cf23f269276b178182) --- src/conf_mode/dhcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 71b71879c..a8cef5ebf 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -232,7 +232,7 @@ def verify(dhcp): # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if 'disable' not in network_config: - if is_subnet_connected(subnet, primary=True): + if is_subnet_connected(subnet, primary=False): listen_ok = True # Subnets must be non overlapping -- cgit v1.2.3 From 3af310cb76d96d08151e4cdc83abcfe15484a556 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Fri, 22 Oct 2021 14:41:24 +0000 Subject: sstp: T2566: Fix verify section for pool ipv6 only --- src/conf_mode/vpn_sstp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 11925dfa4..070009722 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -50,7 +50,7 @@ def verify(sstp): verify_accel_ppp_base_service(sstp) - if not sstp['client_ip_pool']: + if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: raise ConfigError('Client IP subnet required') # -- cgit v1.2.3 From 0e0565b83909784abcf5858f8ea178ded98debb0 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 26 Oct 2021 20:34:48 +0000 Subject: op-mode: T3942: Add feature generate IPSec debug-archive --- .../generate-ipsec-debug-archive.xml.in | 20 ++++++++++++ src/op_mode/generate_ipsec_debug_archive.sh | 36 ++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 op-mode-definitions/generate-ipsec-debug-archive.xml.in create mode 100755 src/op_mode/generate_ipsec_debug_archive.sh (limited to 'src') diff --git a/op-mode-definitions/generate-ipsec-debug-archive.xml.in b/op-mode-definitions/generate-ipsec-debug-archive.xml.in new file mode 100644 index 000000000..ecd7108c4 --- /dev/null +++ b/op-mode-definitions/generate-ipsec-debug-archive.xml.in @@ -0,0 +1,20 @@ + + + + + + + Generate IPsec archives + + + + + Generate IPSec debug-archive + + ${vyos_op_scripts_dir}/generate_ipsec_debug_archive.sh + + + + + + diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh new file mode 100755 index 000000000..53d0a6eaa --- /dev/null +++ b/src/op_mode/generate_ipsec_debug_archive.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Collecting IPSec Debug Information + +DATE=`date +%d-%m-%Y` + +a_CMD=( + "sudo ipsec status" + "sudo swanctl -L" + "sudo swanctl -l" + "sudo swanctl -P" + "sudo ip x sa show" + "sudo ip x policy show" + "sudo ip tunnel show" + "sudo ip address" + "sudo ip rule show" + "sudo ip route" + "sudo ip route show table 220" + ) + + +echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt +date >> /tmp/ipsec-status-${DATE}.txt + +# Execute all DEBUG commands and save it to file +for cmd in "${a_CMD[@]}"; do + echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt + ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null +done + +# Collect charon logs, build .tgz archive +sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \ +sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null +sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt + +echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz" -- cgit v1.2.3 From 64994acb6f106626f94743a3e47057f613a0d2fb Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 27 Oct 2021 21:54:23 +0200 Subject: vrrp: T3944: reload daemon instead of restart when already running This prevents a failover from MASTER -> BACKUP when changing any MASTER related configuration. (cherry picked from commit 2c82c9acbde2ccca9c7bb5e646a45fd646463afe) --- src/conf_mode/vrrp.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index c906bdfcd..ad38adaec 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -28,6 +28,7 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import is_systemd_service_running from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -146,7 +147,12 @@ def apply(vrrp): call(f'systemctl stop {service_name}') return None - call(f'systemctl restart {service_name}') + # XXX: T3944 - reload keepalived configuration if service is already running + # to not cause any service disruption when applying changes. + if is_systemd_service_running(service_name): + call(f'systemctl reload {service_name}') + else: + call(f'systemctl restart {service_name}') return None if __name__ == '__main__': -- cgit v1.2.3 From 75facc61c3e66a8ddd3f02000338df15621425d4 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 28 Oct 2021 16:50:23 +0000 Subject: IPSec: T3941: Fix uptime for tunnels sa op-mode The current uptime for tunnels is getting from parent SA That is incorrect as we should get value from child SA --- src/op_mode/show_ipsec_sa.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py index 8b8f11947..503366dd8 100755 --- a/src/op_mode/show_ipsec_sa.py +++ b/src/op_mode/show_ipsec_sa.py @@ -57,10 +57,7 @@ for sa in sas: else: state = "down" - if state == "up": - uptime = vyos.util.seconds_to_human(parent_sa["established"].decode()) - else: - uptime = "N/A" + uptime = "N/A" remote_host = parent_sa["remote-host"].decode() remote_id = parent_sa["remote-id"].decode() @@ -88,6 +85,8 @@ for sa in sas: # Remove B from <1K values pkts_str = re.sub(r'B', r'', pkts_str) + uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) + enc = isa["encr-alg"].decode() if "encr-keysize" in isa: key_size = isa["encr-keysize"].decode() -- cgit v1.2.3 From 02090ae9927e96650c5e615f39631081dbbc6a1b Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Fri, 29 Oct 2021 07:46:33 +0000 Subject: ipsec: T3643: Fix for show tunnels with state down The current op-mode for "show vpn ipsec sa" shows only tunnels which established (parent SA) and installed (child SA) If tunnel not installed it can't show correct information about this tunnel, in that case it can shows only parent sa state Get codebase for "show_ipsec_sa.py" (op-mode) from 1.4 branch where it was fixed. --- src/op_mode/show_ipsec_sa.py | 167 +++++++++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 77 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py index 503366dd8..beb632fa8 100755 --- a/src/op_mode/show_ipsec_sa.py +++ b/src/op_mode/show_ipsec_sa.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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 @@ -29,35 +29,22 @@ def convert(text): def alphanum_key(key): return [convert(c) for c in re.split('([0-9]+)', str(key))] -try: - session = vici.Session() - sas = session.list_sas() -except PermissionError: - print("You do not have a permission to connect to the IPsec daemon") - sys.exit(1) -except ConnectionRefusedError: - print("IPsec is not runing") - sys.exit(1) -except Exception as e: - print("An error occured: {0}".format(e)) - sys.exit(1) - -sa_data = [] - -for sa in sas: - # list_sas() returns a list of single-item dicts - for peer in sa: - parent_sa = sa[peer] - child_sas = parent_sa["child-sas"] - installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} - - # parent_sa["state"] = IKE state, child_sas["state"] = ESP state +def format_output(conns, sas): + sa_data = [] + + for peer, parent_conn in conns.items(): + if peer not in sas: + continue + + parent_sa = sas[peer] + child_sas = parent_sa['child-sas'] + installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} + + state = 'down' + uptime = 'N/A' + if parent_sa["state"] == b"ESTABLISHED" and installed_sas: state = "up" - else: - state = "down" - - uptime = "N/A" remote_host = parent_sa["remote-host"].decode() remote_id = parent_sa["remote-id"].decode() @@ -66,53 +53,79 @@ for sa in sas: remote_id = "N/A" # The counters can only be obtained from the child SAs - if not installed_sas: - data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] - sa_data.append(data) - else: - for csa in installed_sas: - isa = installed_sas[csa] - csa_name = isa['name'] - csa_name = csa_name.decode() - - bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) - bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) - bytes_str = "{0}/{1}".format(bytes_in, bytes_out) - - pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) - pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) - pkts_str = "{0}/{1}".format(pkts_in, pkts_out) - # Remove B from <1K values - pkts_str = re.sub(r'B', r'', pkts_str) - - uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) - - enc = isa["encr-alg"].decode() - if "encr-keysize" in isa: - key_size = isa["encr-keysize"].decode() - else: - key_size = "" - if "integ-alg" in isa: - hash = isa["integ-alg"].decode() - else: - hash = "" - if "dh-group" in isa: - dh_group = isa["dh-group"].decode() - else: - dh_group = "" - - proposal = enc - if key_size: - proposal = "{0}_{1}".format(proposal, key_size) - if hash: - proposal = "{0}/{1}".format(proposal, hash) - if dh_group: - proposal = "{0}/{1}".format(proposal, dh_group) - - data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] + for child_conn in parent_conn['children']: + if child_conn not in installed_sas: + data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] sa_data.append(data) - -headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] -sa_data = sorted(sa_data, key=alphanum_key) -output = tabulate.tabulate(sa_data, headers) -print(output) + continue + + isa = installed_sas[child_conn] + csa_name = isa['name'] + csa_name = csa_name.decode() + + bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) + bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) + bytes_str = "{0}/{1}".format(bytes_in, bytes_out) + + pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) + pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) + pkts_str = "{0}/{1}".format(pkts_in, pkts_out) + # Remove B from <1K values + pkts_str = re.sub(r'B', r'', pkts_str) + + uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) + + enc = isa["encr-alg"].decode() + if "encr-keysize" in isa: + key_size = isa["encr-keysize"].decode() + else: + key_size = "" + if "integ-alg" in isa: + hash = isa["integ-alg"].decode() + else: + hash = "" + if "dh-group" in isa: + dh_group = isa["dh-group"].decode() + else: + dh_group = "" + + proposal = enc + if key_size: + proposal = "{0}_{1}".format(proposal, key_size) + if hash: + proposal = "{0}/{1}".format(proposal, hash) + if dh_group: + proposal = "{0}/{1}".format(proposal, dh_group) + + data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] + sa_data.append(data) + return sa_data + +if __name__ == '__main__': + try: + session = vici.Session() + conns = {} + sas = {} + + for conn in session.list_conns(): + for key in conn: + conns[key] = conn[key] + + for sa in session.list_sas(): + for key in sa: + sas[key] = sa[key] + + headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] + sa_data = format_output(conns, sas) + sa_data = sorted(sa_data, key=alphanum_key) + output = tabulate.tabulate(sa_data, headers) + print(output) + except PermissionError: + print("You do not have a permission to connect to the IPsec daemon") + sys.exit(1) + except ConnectionRefusedError: + print("IPsec is not runing") + sys.exit(1) + except Exception as e: + print("An error occured: {0}".format(e)) + sys.exit(1) -- cgit v1.2.3 From 0c30c4736581df1e50bf42159b3041b3dedfa4da Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 13:48:15 +0100 Subject: console: T3954: bugfix RuntimeError: dictionary keys changed during iteration (cherry picked from commit f227987ccf41e01d4ddafb6db7b36ecf13148c78) --- interface-definitions/system-console.xml.in | 1 + src/conf_mode/system_console.py | 70 ++++++++++++++++++----------- 2 files changed, 45 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/interface-definitions/system-console.xml.in b/interface-definitions/system-console.xml.in index 88f7f82a9..2897e5e97 100644 --- a/interface-definitions/system-console.xml.in +++ b/interface-definitions/system-console.xml.in @@ -74,6 +74,7 @@ ^(1200|2400|4800|9600|19200|38400|57600|115200)$ + 115200 diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 33a546bd3..19b252513 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -18,9 +18,14 @@ import os import re from vyos.config import Config -from vyos.util import call, read_file, write_file +from vyos.configdict import dict_merge +from vyos.util import call +from vyos.util import read_file +from vyos.util import write_file from vyos.template import render -from vyos import ConfigError, airbag +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag airbag.enable() by_bus_dir = '/dev/serial/by-bus' @@ -36,21 +41,27 @@ def get_config(config=None): console = conf.get_config_dict(base, get_first_key=True) # bail out early if no serial console is configured - if 'device' not in console.keys(): + if 'device' not in console: return console # convert CLI values to system values - for device in console['device'].keys(): - # no speed setting has been configured - use default value - if not 'speed' in console['device'][device].keys(): - tmp = { 'speed': '' } - if device.startswith('hvc'): - tmp['speed'] = 38400 - else: - tmp['speed'] = 115200 + default_values = defaults(base + ['device']) + for device, device_config in console['device'].items(): + if 'speed' not in device_config and device.startswith('hvc'): + # XEN console has a different default console speed + console['device'][device]['speed'] = 38400 + else: + # Merge in XML defaults - the proper way to do it + console['device'][device] = dict_merge(default_values, + console['device'][device]) + + return console - console['device'][device].update(tmp) +def verify(console): + if not console or 'device' not in console: + return None + for device in console['device']: if device.startswith('usb'): # It is much easiert to work with the native ttyUSBn name when using # getty, but that name may change across reboots - depending on the @@ -58,13 +69,13 @@ def get_config(config=None): # to its dynamic device file - and create a new dict entry for it. by_bus_device = f'{by_bus_dir}/{device}' if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): - tmp = os.path.basename(os.readlink(by_bus_device)) - # updating the dict must come as last step in the loop! - console['device'][tmp] = console['device'].pop(device) + device = os.path.basename(os.readlink(by_bus_device)) - return console + # If the device name still starts with usbXXX no matching tty was found + # and it can not be used as a serial interface + if device.startswith('usb'): + raise ConfigError(f'Device {device} does not support beeing used as tty') -def verify(console): return None def generate(console): @@ -76,20 +87,29 @@ def generate(console): call(f'systemctl stop {basename}') os.unlink(os.path.join(root, basename)) - if not console: + if not console or 'device' not in console: return None - for device in console['device'].keys(): + for device, device_config in console['device'].items(): + if device.startswith('usb'): + # It is much easiert to work with the native ttyUSBn name when using + # getty, but that name may change across reboots - depending on the + # amount of connected devices. We will resolve the fixed device name + # to its dynamic device file - and create a new dict entry for it. + by_bus_device = f'{by_bus_dir}/{device}' + if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): + device = os.path.basename(os.readlink(by_bus_device)) + config_file = base_dir + f'/serial-getty@{device}.service' getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service' - render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device]) + render(config_file, 'getty/serial-getty.service.tmpl', device_config) os.symlink(config_file, getty_wants_symlink) # GRUB # For existing serial line change speed (if necessary) # Only applys to ttyS0 - if 'ttyS0' not in console['device'].keys(): + if 'ttyS0' not in console['device']: return None speed = console['device']['ttyS0']['speed'] @@ -98,7 +118,6 @@ def generate(console): return None lines = read_file(grub_config).split('\n') - p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$') write = False newlines = [] @@ -122,9 +141,8 @@ def generate(console): return None def apply(console): - # reset screen blanking + # Reset screen blanking call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux /dev/tty1 2>&1') - # Reload systemd manager configuration call('systemctl daemon-reload') @@ -136,11 +154,11 @@ def apply(console): call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux /dev/tty1 2>&1') # Start getty process on configured serial interfaces - for device in console['device'].keys(): + for device in console['device']: # Only start console if it exists on the running system. If a user # detaches a USB serial console and reboots - it should not fail! if os.path.exists(f'/dev/{device}'): - call(f'systemctl start serial-getty@{device}.service') + call(f'systemctl restart serial-getty@{device}.service') return None -- cgit v1.2.3 From 60775392123a0253863ab7af5accd3b61285d84e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 13:48:22 +0100 Subject: console: udev: T3954: adjust rule script to new systemd-udev version We can no longer use bash veriable string code vor string manipulation. Move to a more robust "cut" implementation. (cherry picked from commit 513e951f3e1358ec6ff5424d03e8f4e9aa7c3388) --- src/etc/udev/rules.d/90-vyos-serial.rules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules index 3f10f4924..5cca89e89 100644 --- a/src/etc/udev/rules.d/90-vyos-serial.rules +++ b/src/etc/udev/rules.d/90-vyos-serial.rules @@ -22,7 +22,7 @@ IMPORT{builtin}="path_id", IMPORT{builtin}="usb_id" # (tr -d -) does the replacement # - Replace the first group after ":" to represent the bus relation (sed -e 0,/:/s//b/) indicated by "b" # - Replace the next group after ":" to represent the port relation (sed -e 0,/:/s//p/) indicated by "p" -ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" -ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" LABEL="serial_end" -- cgit v1.2.3 From 893dd69d975c309bfd09976e776c5fa9a5932ef9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 14:42:04 +0100 Subject: netflow: T3953: use warning if "netflow source-ip" does not exist instead of error (cherry picked from commit 17215846b512851e7df8cdfcfc06c18b1d27f763) --- src/conf_mode/flow_accounting_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 9cae29481..0a4559ade 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -306,7 +306,7 @@ def verify(config): source_ip_presented = True break if not source_ip_presented: - raise ConfigError("Your \"netflow source-ip\" does not exist in the system") + print("Warning: your \"netflow source-ip\" does not exist in the system") # check if engine-id compatible with selected protocol version if config['netflow']['engine-id']: -- cgit v1.2.3 From e482377b29df05e60dbdb31d6276ae2030ffa2f9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 14:50:47 +0100 Subject: tunnel: T3956: GRE key check must not be run on our own interface instance --- src/conf_mode/interfaces-tunnel.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 4db564e6d..2798d321f 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -87,6 +87,10 @@ def verify(tunnel): # Check pairs tunnel source-address/encapsulation/key with exists tunnels. # Prevent the same key for 2 tunnels with same source-address/encap. T2920 for tunnel_if in Section.interfaces('tunnel'): + # It makes no sense to run the test for re-used GRE keys on our + # own interface we are currently working on + if tunnel['ifname'] == tunnel_if: + continue tunnel_cfg = get_interface_config(tunnel_if) # no match on encapsulation - bail out if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: -- cgit v1.2.3 From 0c2384114e8abbd9c883a57729564c70c0a86eec Mon Sep 17 00:00:00 2001 From: zsdc Date: Mon, 25 Oct 2021 21:44:00 +0300 Subject: dhclient: T3940: Added lease file argument to the `dhclient -x` call When `dhclient` with the `-x` option is used to stop running DHCP client with a lease file that is not the same as in the new `dhclient` process, it requires a `-lf` argument with a path to the old lease file to find information about old/active leases and process them according to instructions and config. This commit adds the option to the `02-vyos-stopdhclient` hook, which allows to properly process `dhclient` instances started in different ways. (cherry picked from commit 393970f9ee5b3dfc58e0e999d3d5941a198b2c6f) --- src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient index f737148dc..ae6bf9f16 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient +++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient @@ -23,10 +23,12 @@ if [ -z ${CONTROLLED_STOP} ] ; then if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then # get path to PID-file of dhclient process local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'` + # get path to lease-file of dhclient process + local dhclient_leasefile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-lf (/\\\S*leases) .*", LF) { print LF[1] }'` # stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill - logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: $dhclient_pidfile" + logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: ${dhclient_pidfile}, Leases file: ${dhclient_leasefile}" if [[ -e $dhclient_pidfile ]]; then - dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile + dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile -lf $dhclient_leasefile else logmsg error "PID file $dhclient_pidfile does not exists, killing dhclient with SIGTERM signal" kill -s 15 ${dhclient} -- cgit v1.2.3 From e2bab09cc31ed41149409bafe0a19568f244a963 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 7 Nov 2021 20:26:47 +0100 Subject: T3912: use a more informative default login banner (cherry picked from commit 5d39a113bdef82e201aa43f848217c30db2f6fd9) --- src/conf_mode/system-login-banner.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index 569010735..6a8dac318 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -22,12 +22,15 @@ from vyos import airbag airbag.enable() motd=""" -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. +Welcome to VyOS! -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. +Check out project news at https://blog.vyos.io +and feel free to report bugs at https://phabricator.vyos.net + +You can change this banner using "set system login banner post-login" command. + +VyOS is a free software distribution that includes multiple components, +you can check individual component licenses under /usr/share/doc/*/copyright """ -- cgit v1.2.3 From 37c3ebc8aba14ba7605fbbb9c4013cbd2513400d Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Fri, 26 Mar 2021 11:25:44 -0500 Subject: http api: T3412: use FastAPI as web framework; support application/json Replace the Flask micro-framework with FastAPI, in order to support extensions to the API and OpenAPI 3.* generation. This change will remain backwards compatible with previous versions. Notably, the multipart forms version of requests remain supported; in addition application/json requests are now natively supported. (cherry picked from commit 0125fff200efe3259aa25953e7505f69679261f8) --- data/templates/https/nginx.default.tmpl | 4 +- debian/control | 1 + src/services/vyos-http-api-server | 571 ++++++++++++++++++++++---------- src/systemd/vyos-http-api.service | 3 +- 4 files changed, 392 insertions(+), 187 deletions(-) (limited to 'src') diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index 26d0b5d73..625ef4486 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -41,9 +41,11 @@ server { ssl_protocols TLSv1.2 TLSv1.3; # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image|generate|show) { + location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc) { {% if server.api %} proxy_pass http://localhost:{{ server.api.port }}; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 600; proxy_buffering off; {% else %} diff --git a/debian/control b/debian/control index 87a0258d2..8cafd8257 100644 --- a/debian/control +++ b/debian/control @@ -141,6 +141,7 @@ Depends: usbutils, vyatta-bash, vyatta-cfg, + vyos-http-api-tools, vyos-utils, wide-dhcpv6-client, wireguard-tools, diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 703628558..8069d7146 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 +#!/usr/share/vyos-http-api-tools/bin/python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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 @@ -19,25 +19,37 @@ import os import sys import grp +import copy import json +import logging import traceback import threading -import signal +from typing import List, Union, Callable, Dict -import vyos.config - -from flask import Flask, request -from waitress import serve +import uvicorn +from fastapi import FastAPI, Depends, Request, Response, HTTPException +from fastapi.responses import HTMLResponse +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute +from pydantic import BaseModel, StrictStr, validator -from functools import wraps +import vyos.config from vyos.configsession import ConfigSession, ConfigSessionError - DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' CFG_GROUP = 'vyattacfg' -app = Flask(__name__) +debug = True + +logger = logging.getLogger(__name__) +logs_handler = logging.StreamHandler() +logger.addHandler(logs_handler) + +if debug: + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) # Giant lock! lock = threading.Lock() @@ -56,55 +68,310 @@ def check_auth(key_list, key): def error(code, msg): resp = {"success": False, "error": msg, "data": None} - return json.dumps(resp), code + resp = json.dumps(resp) + return HTMLResponse(resp, status_code=code) def success(data): resp = {"success": True, "data": data, "error": None} - return json.dumps(resp) - -def get_command(f): - @wraps(f) - def decorated_function(*args, **kwargs): - cmd = request.form.get("data") - if not cmd: - return error(400, "Non-empty data field is required") - try: - cmd = json.loads(cmd) - except Exception as e: - return error(400, "Failed to parse JSON: {0}".format(e)) - return f(cmd, *args, **kwargs) - - return decorated_function - -def auth_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - key = request.form.get("key") - api_keys = app.config['vyos_keys'] - id = check_auth(api_keys, key) - if not id: - return error(401, "Valid API key is required") - return f(*args, **kwargs) - - return decorated_function - -@app.route('/configure', methods=['POST']) -@get_command -@auth_required -def configure_op(commands): - session = app.config['vyos_session'] + resp = json.dumps(resp) + return HTMLResponse(resp) + +# Pydantic models for validation +# Pydantic will cast when possible, so use StrictStr +# validators added as needed for additional constraints +# schema_extra adds anotations to OpenAPI, to add examples + +class ApiModel(BaseModel): + key: StrictStr + +class BaseConfigureModel(BaseModel): + op: StrictStr + path: List[StrictStr] + value: StrictStr = None + + @validator("path", pre=True, always=True) + def check_non_empty(cls, path): + assert len(path) > 0 + return path + +class ConfigureModel(ApiModel): + op: StrictStr + path: List[StrictStr] + value: StrictStr = None + + @validator("path", pre=True, always=True) + def check_non_empty(cls, path): + assert len(path) > 0 + return path + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "set | delete | comment", + "path": ['config', 'mode', 'path'], + } + } + +class ConfigureListModel(ApiModel): + commands: List[BaseConfigureModel] + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "commands": "list of commands", + } + } + +class RetrieveModel(ApiModel): + op: StrictStr + path: List[StrictStr] + configFormat: StrictStr = None + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "returnValue | returnValues | exists | showConfig", + "path": ['config', 'mode', 'path'], + "configFormat": "json (default) | json_ast | raw", + + } + } + +class ConfigFileModel(ApiModel): + op: StrictStr + file: StrictStr = None + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "save | load", + "file": "filename", + } + } + +class ImageModel(ApiModel): + op: StrictStr + url: StrictStr = None + name: StrictStr = None + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "add | delete", + "url": "imagelocation", + "name": "imagename", + } + } + +class GenerateModel(ApiModel): + op: StrictStr + path: List[StrictStr] + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "generate", + "path": ["op", "mode", "path"], + } + } + +class ShowModel(ApiModel): + op: StrictStr + path: List[StrictStr] + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "show", + "path": ["op", "mode", "path"], + } + } + +class Success(BaseModel): + success: bool + data: Union[str, bool, Dict] + error: str + +class Error(BaseModel): + success: bool = False + data: Union[str, bool, Dict] + error: str + +responses = { + 200: {'model': Success}, + 400: {'model': Error}, + 422: {'model': Error, 'description': 'Validation Error'}, + 500: {'model': Error} +} + +def auth_required(data: ApiModel): + key = data.key + api_keys = app.state.vyos_keys + id = check_auth(api_keys, key) + if not id: + raise HTTPException(status_code=401, detail="Valid API key is required") + app.state.vyos_id = id + +# override Request and APIRoute classes in order to convert form request to json; +# do all explicit validation here, for backwards compatability of error messages; +# the explicit validation may be dropped, if desired, in favor of native +# validation by FastAPI/Pydantic, as is used for application/json requests +class MultipartRequest(Request): + ERR_MISSING_KEY = False + ERR_MISSING_DATA = False + ERR_NOT_JSON = False + ERR_NOT_DICT = False + ERR_NO_OP = False + ERR_NO_PATH = False + ERR_EMPTY_PATH = False + ERR_PATH_NOT_LIST = False + ERR_VALUE_NOT_STRING = False + ERR_PATH_NOT_LIST_OF_STR = False + offending_command = {} + exception = None + async def body(self) -> bytes: + if not hasattr(self, "_body"): + forms = {} + merge = {} + body = await super().body() + self._body = body + + form_data = await self.form() + if form_data: + logger.debug("processing form data") + for k, v in form_data.multi_items(): + forms[k] = v + + if 'data' not in forms: + self.ERR_MISSING_DATA = True + else: + try: + tmp = json.loads(forms['data']) + except json.JSONDecodeError as e: + self.ERR_NOT_JSON = True + self.exception = e + tmp = {} + if isinstance(tmp, list): + merge['commands'] = tmp + else: + merge = tmp + + if 'commands' in merge: + cmds = merge['commands'] + else: + cmds = copy.deepcopy(merge) + cmds = [cmds] + + for c in cmds: + if not isinstance(c, dict): + self.ERR_NOT_DICT = True + self.offending_command = c + elif 'op' not in c: + self.ERR_NO_OP = True + self.offending_command = c + elif 'path' not in c: + self.ERR_NO_PATH = True + self.offending_command = c + elif not c['path']: + self.ERR_EMPTY_PATH = True + self.offending_command = c + elif not isinstance(c['path'], list): + self.ERR_PATH_NOT_LIST = True + self.offending_command = c + elif not all(isinstance(el, str) for el in c['path']): + self.ERR_PATH_NOT_LIST_OF_STR = True + self.offending_command = c + elif 'value' in c and not isinstance(c['value'], str): + self.ERR_VALUE_NOT_STRING = True + self.offending_command = c + + if 'key' not in forms and 'key' not in merge: + self.ERR_MISSING_KEY = True + if 'key' in forms and 'key' not in merge: + merge['key'] = forms['key'] + + new_body = json.dumps(merge) + new_body = new_body.encode() + self._body = new_body + + return self._body + +class MultipartRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + request = MultipartRequest(request.scope, request.receive) + endpoint = request.url.path + try: + response: Response = await original_route_handler(request) + except HTTPException as e: + return error(e.status_code, e.detail) + except Exception as e: + if request.ERR_MISSING_KEY: + return error(422, "Valid API key is required") + if request.ERR_MISSING_DATA: + return error(422, "Non-empty data field is required") + if request.ERR_NOT_JSON: + return error(400, "Failed to parse JSON: {0}".format(request.exception)) + if endpoint == '/configure': + if request.ERR_NOT_DICT: + return error(400, "Malformed command \"{0}\": any command must be a dict".format(json.dumps(request.offending_command))) + if request.ERR_NO_OP: + return error(400, "Malformed command \"{0}\": missing \"op\" field".format(json.dumps(request.offending_command))) + if request.ERR_NO_PATH: + return error(400, "Malformed command \"{0}\": missing \"path\" field".format(json.dumps(request.offending_command))) + if request.ERR_EMPTY_PATH: + return error(400, "Malformed command \"{0}\": empty path".format(json.dumps(request.offending_command))) + if request.ERR_PATH_NOT_LIST: + return error(400, "Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(request.offending_command))) + if request.ERR_VALUE_NOT_STRING: + return error(400, "Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(request.offending_command))) + if request.ERR_PATH_NOT_LIST_OF_STR: + return error(400, "Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(request.offending_command))) + if endpoint in ('/retrieve','/generate','/show'): + if request.ERR_NO_OP or request.ERR_NO_PATH: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") + if endpoint in ('/config-file', '/image'): + if request.ERR_NO_OP: + return error(400, "Missing required field \"op\"") + + raise e + + return response + + return custom_route_handler + +app = FastAPI(debug=True, + title="VyOS API", + version="0.1.0", + responses={**responses}, + dependencies=[Depends(auth_required)]) + +app.router.route_class = MultipartRoute + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request, exc): + return error(400, str(exc.errors()[0])) + +@app.post('/configure') +def configure_op(data: Union[ConfigureModel, ConfigureListModel]): + session = app.state.vyos_session env = session.get_session_env() config = vyos.config.Config(session_env=env) - strict_field = request.form.get("strict") - if strict_field == "true": - strict = True - else: - strict = False - # Allow users to pass just one command - if not isinstance(commands, list): - commands = [commands] + if not isinstance(data, ConfigureListModel): + data = [data] + else: + data = data.commands # We don't want multiple people/apps to be able to commit at once, # or modify the shared session while someone else is doing the same, @@ -114,53 +381,25 @@ def configure_op(commands): status = 200 error_msg = None try: - for c in commands: - # What we've got may not even be a dict - if not isinstance(c, dict): - raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c))) - - # Missing op or path is a show stopper - if not ('op' in c): - raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) - if not ('path' in c): - raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c))) - - # Missing value is fine, substitute for empty string - if 'value' in c: - value = c['value'] - else: - value = "" - - op = c['op'] - path = c['path'] - - if not path: - raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c))) - - # Type checking - if not isinstance(path, list): - raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c))) + for c in data: + op = c.op + path = c.path - if not isinstance(value, str): - raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c))) - - # Account for the case when value field is present and set to null - if not value: + if c.value: + value = c.value + else: value = "" - # For vyos.configsessios calls that have no separate value arguments, + # For vyos.configsession calls that have no separate value arguments, # and for type checking too - try: - cfg_path = " ".join(path + [value]).strip() - except TypeError: - raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c))) + cfg_path = " ".join(path + [value]).strip() if op == 'set': # XXX: it would be nice to do a strict check for "path already exists", # but there's probably no way to do that session.set(path, value=value) elif op == 'delete': - if strict and not config.exists(cfg_path): + if app.state.vyos_strict and not config.exists(cfg_path): raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path)) session.delete(path, value=value) elif op == 'comment': @@ -169,16 +408,16 @@ def configure_op(commands): raise ConfigSessionError("\"{0}\" is not a valid operation".format(op)) # end for session.commit() - print("Configuration modified via HTTP API using key \"{0}\"".format(id)) + logger.info(f"Configuration modified via HTTP API using key '{app.state.vyos_id}'") except ConfigSessionError as e: session.discard() status = 400 - if app.config['vyos_debug']: - print(traceback.format_exc(), file=sys.stderr) + if app.state.vyos_debug: + logger.critical(f"ConfigSessionError:\n {traceback.format_exc()}") error_msg = str(e) except Exception as e: session.discard() - print(traceback.format_exc(), file=sys.stderr) + logger.critical(traceback.format_exc()) status = 500 # Don't give the details away to the outer world @@ -188,22 +427,17 @@ def configure_op(commands): if status != 200: return error(status, error_msg) - else: - return success(None) -@app.route('/retrieve', methods=['POST']) -@get_command -@auth_required -def retrieve_op(command): - session = app.config['vyos_session'] + return success(None) + +@app.post("/retrieve") +def retrieve_op(data: RetrieveModel): + session = app.state.vyos_session env = session.get_session_env() config = vyos.config.Config(session_env=env) - try: - op = command['op'] - path = " ".join(command['path']) - except KeyError: - return error(400, "Missing required field. \"op\" and \"path\" fields are required") + op = data.op + path = " ".join(data.path) try: if op == 'returnValue': @@ -214,10 +448,10 @@ def retrieve_op(command): res = config.exists(path) elif op == 'showConfig': config_format = 'json' - if 'configFormat' in command: - config_format = command['configFormat'] + if data.configFormat: + config_format = data.configFormat - res = session.show_config(path=command['path']) + res = session.show_config(path=data.path) if config_format == 'json': config_tree = vyos.configtree.ConfigTree(res) res = json.loads(config_tree.to_json()) @@ -233,33 +467,28 @@ def retrieve_op(command): except ConfigSessionError as e: return error(400, str(e)) except Exception as e: - print(traceback.format_exc(), file=sys.stderr) + logger.critical(traceback.format_exc()) return error(500, "An internal error occured. Check the logs for details.") return success(res) -@app.route('/config-file', methods=['POST']) -@get_command -@auth_required -def config_file_op(command): - session = app.config['vyos_session'] +@app.post('/config-file') +def config_file_op(data: ConfigFileModel): + session = app.state.vyos_session - try: - op = command['op'] - except KeyError: - return error(400, "Missing required field \"op\"") + op = data.op try: if op == 'save': - try: - path = command['file'] - except KeyError: + if data.file: + path = data.file + else: path = '/config/config.boot' res = session.save_config(path) elif op == 'load': - try: - path = command['file'] - except KeyError: + if data.file: + path = data.file + else: return error(400, "Missing required field \"file\"") res = session.migrate_and_load_config(path) res = session.commit() @@ -268,33 +497,28 @@ def config_file_op(command): except ConfigSessionError as e: return error(400, str(e)) except Exception as e: - print(traceback.format_exc(), file=sys.stderr) + logger.critical(traceback.format_exc()) return error(500, "An internal error occured. Check the logs for details.") return success(res) -@app.route('/image', methods=['POST']) -@get_command -@auth_required -def image_op(command): - session = app.config['vyos_session'] +@app.post('/image') +def image_op(data: ImageModel): + session = app.state.vyos_session - try: - op = command['op'] - except KeyError: - return error(400, "Missing required field \"op\"") + op = data.op try: if op == 'add': - try: - url = command['url'] - except KeyError: + if data.url: + url = data.url + else: return error(400, "Missing required field \"url\"") res = session.install_image(url) elif op == 'delete': - try: - name = command['name'] - except KeyError: + if data.name: + name = data.name + else: return error(400, "Missing required field \"name\"") res = session.remove_image(name) else: @@ -302,26 +526,17 @@ def image_op(command): except ConfigSessionError as e: return error(400, str(e)) except Exception as e: - print(traceback.format_exc(), file=sys.stderr) + logger.critical(traceback.format_exc()) return error(500, "An internal error occured. Check the logs for details.") return success(res) +@app.post('/generate') +def generate_op(data: GenerateModel): + session = app.state.vyos_session -@app.route('/generate', methods=['POST']) -@get_command -@auth_required -def generate_op(command): - session = app.config['vyos_session'] - - try: - op = command['op'] - path = command['path'] - except KeyError: - return error(400, "Missing required field. \"op\" and \"path\" fields are required") - - if not isinstance(path, list): - return error(400, "Malformed command: \"path\" field must be a list of strings") + op = data.op + path = data.path try: if op == 'generate': @@ -331,25 +546,17 @@ def generate_op(command): except ConfigSessionError as e: return error(400, str(e)) except Exception as e: - print(traceback.format_exc(), file=sys.stderr) + logger.critical(traceback.format_exc()) return error(500, "An internal error occured. Check the logs for details.") return success(res) -@app.route('/show', methods=['POST']) -@get_command -@auth_required -def show_op(command): - session = app.config['vyos_session'] +@app.post('/show') +def show_op(data: ShowModel): + session = app.state.vyos_session - try: - op = command['op'] - path = command['path'] - except KeyError: - return error(400, "Missing required field. \"op\" and \"path\" fields are required") - - if not isinstance(path, list): - return error(400, "Malformed command: \"path\" field must be a list of strings") + op = data.op + path = data.path try: if op == 'show': @@ -359,14 +566,11 @@ def show_op(command): except ConfigSessionError as e: return error(400, str(e)) except Exception as e: - print(traceback.format_exc(), file=sys.stderr) + logger.critical(traceback.format_exc()) return error(500, "An internal error occured. Check the logs for details.") return success(res) -def shutdown(): - raise KeyboardInterrupt - if __name__ == '__main__': # systemd's user and group options don't work, do it by hand here, # else no one else will be able to commit @@ -380,21 +584,20 @@ if __name__ == '__main__': try: server_config = load_server_config() except Exception as e: - print("Failed to load the HTTP API server config: {0}".format(e)) + logger.critical("Failed to load the HTTP API server config: {0}".format(e)) session = ConfigSession(os.getpid()) - app.config['vyos_session'] = session - app.config['vyos_keys'] = server_config['api_keys'] - app.config['vyos_debug'] = server_config['debug'] - - def sig_handler(signum, frame): - shutdown() + app.state.vyos_session = session + app.state.vyos_keys = server_config['api_keys'] - signal.signal(signal.SIGTERM, sig_handler) + app.state.vyos_debug = True if server_config['debug'] == 'true' else False + app.state.vyos_strict = True if server_config['strict'] == 'true' else False try: - serve(app, host=server_config["listen_address"], - port=server_config["port"]) + uvicorn.run(app, host=server_config["listen_address"], + port=int(server_config["port"]), + proxy_headers=True) except OSError as e: - print(f"OSError {e}") + logger.critical(f"OSError {e}") + sys.exit(1) diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service index 4fa68b4ff..ba5df5984 100644 --- a/src/systemd/vyos-http-api.service +++ b/src/systemd/vyos-http-api.service @@ -5,9 +5,8 @@ Requires=vyos-router.service [Service] ExecStartPre=/usr/libexec/vyos/init/vyos-config -ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server +ExecStart=/usr/libexec/vyos/services/vyos-http-api-server Type=idle -KillMode=process SyslogIdentifier=vyos-http-api SyslogFacility=daemon -- cgit v1.2.3 From 8b8c2b36f85072def2bbb59040305e19b187a399 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sat, 12 Jun 2021 11:43:29 -0500 Subject: http-api: T3616: update for strict content-type check in FastAPI 0.65.2 FastAPI 0.65.2 checks content-type request header before assuming JSON, closing a well-known loophole. This requires a modification of the code providing backwards compatibility of multipart forms. (cherry picked from commit 3a9041e2d4d4a48ba7c01439e69c5f86a4a850c2) --- src/services/vyos-http-api-server | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'src') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 8069d7146..cbf321dc8 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,6 +32,9 @@ from fastapi.responses import HTMLResponse from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute from pydantic import BaseModel, StrictStr, validator +from starlette.datastructures import FormData, MutableHeaders +from starlette.formparsers import FormParser, MultiPartParser +from multipart.multipart import parse_options_header import vyos.config @@ -236,6 +239,35 @@ class MultipartRequest(Request): ERR_PATH_NOT_LIST_OF_STR = False offending_command = {} exception = None + + @property + def orig_headers(self): + self._orig_headers = super().headers + return self._orig_headers + + @property + def headers(self): + self._headers = super().headers.mutablecopy() + self._headers['content-type'] = 'application/json' + return self._headers + + async def form(self) -> FormData: + if not hasattr(self, "_form"): + assert ( + parse_options_header is not None + ), "The `python-multipart` library must be installed to use form parsing." + content_type_header = self.orig_headers.get("Content-Type") + content_type, options = parse_options_header(content_type_header) + if content_type == b"multipart/form-data": + multipart_parser = MultiPartParser(self.orig_headers, self.stream()) + self._form = await multipart_parser.parse() + elif content_type == b"application/x-www-form-urlencoded": + form_parser = FormParser(self.orig_headers, self.stream()) + self._form = await form_parser.parse() + else: + self._form = FormData() + return self._form + async def body(self) -> bytes: if not hasattr(self, "_body"): forms = {} -- cgit v1.2.3 From f87f6c249535453b8bd3718dc7cdc84dcbbdbe13 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 5 Aug 2021 11:24:47 -0500 Subject: http-api: T2768: example using GraphQL for high-level config operations (cherry picked from commit b168b4cc7da456f14714d917cdc7a1c6b8df9af5) --- data/templates/https/nginx.default.tmpl | 2 +- python/vyos/defaults.py | 5 +- src/services/api/graphql/graphql/__init__.py | 0 src/services/api/graphql/graphql/directives.py | 17 ++++++ src/services/api/graphql/graphql/mutations.py | 60 ++++++++++++++++++++++ .../api/graphql/graphql/schema/dhcp_server.graphql | 35 +++++++++++++ .../graphql/schema/interface_ethernet.graphql | 18 +++++++ .../api/graphql/graphql/schema/schema.graphql | 15 ++++++ src/services/api/graphql/recipes/__init__.py | 0 src/services/api/graphql/recipes/dhcp_server.py | 13 +++++ .../api/graphql/recipes/interface_ethernet.py | 13 +++++ src/services/api/graphql/recipes/recipe.py | 49 ++++++++++++++++++ .../api/graphql/recipes/templates/dhcp_server.tmpl | 9 ++++ .../recipes/templates/interface_ethernet.tmpl | 5 ++ src/services/api/graphql/state.py | 4 ++ src/services/vyos-http-api-server | 27 ++++++++++ 16 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 src/services/api/graphql/graphql/__init__.py create mode 100644 src/services/api/graphql/graphql/directives.py create mode 100644 src/services/api/graphql/graphql/mutations.py create mode 100644 src/services/api/graphql/graphql/schema/dhcp_server.graphql create mode 100644 src/services/api/graphql/graphql/schema/interface_ethernet.graphql create mode 100644 src/services/api/graphql/graphql/schema/schema.graphql create mode 100644 src/services/api/graphql/recipes/__init__.py create mode 100644 src/services/api/graphql/recipes/dhcp_server.py create mode 100644 src/services/api/graphql/recipes/interface_ethernet.py create mode 100644 src/services/api/graphql/recipes/recipe.py create mode 100644 src/services/api/graphql/recipes/templates/dhcp_server.tmpl create mode 100644 src/services/api/graphql/recipes/templates/interface_ethernet.tmpl create mode 100644 src/services/api/graphql/state.py (limited to 'src') diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index 625ef4486..d25e5193a 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -41,7 +41,7 @@ server { ssl_protocols TLSv1.2 TLSv1.3; # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc) { + location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) { {% if server.api %} proxy_pass http://localhost:{{ server.api.port }}; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index ca5e02834..dacdbdef2 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -23,7 +23,10 @@ directories = { "migrate": "/opt/vyatta/etc/config-migrate/migrate", "log": "/var/log/vyatta", "templates": "/usr/share/vyos/templates/", - "certbot": "/config/auth/letsencrypt" + "certbot": "/config/auth/letsencrypt", + "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", + "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/" + } cfg_group = 'vyattacfg' diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py new file mode 100644 index 000000000..651421c35 --- /dev/null +++ b/src/services/api/graphql/graphql/directives.py @@ -0,0 +1,17 @@ +from ariadne import SchemaDirectiveVisitor, ObjectType +from . mutations import make_resolver + +class DataDirective(SchemaDirectiveVisitor): + """ + Class providing implementation of 'generate' directive in schema. + + """ + def visit_field_definition(self, field, object_type): + name = f'{field.type}' + # field.type contains the return value of the mutation; trim value + # to produce canonical name + name = name.replace('Result', '', 1) + + func = make_resolver(name) + field.resolve = func + return field diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py new file mode 100644 index 000000000..98c665c9a --- /dev/null +++ b/src/services/api/graphql/graphql/mutations.py @@ -0,0 +1,60 @@ + +from importlib import import_module +from typing import Any, Dict +from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from graphql import GraphQLResolveInfo +from makefun import with_signature + +from .. import state + +mutation = ObjectType("Mutation") + +def make_resolver(mutation_name): + """Dynamically generate a resolver for the mutation named in the + schema by 'mutation_name'. + + Dynamic generation is provided using the package 'makefun' (via the + decorator 'with_signature'), which provides signature-preserving + function wrappers; it provides several improvements over, say, + functools.wraps. + + :raise Exception: + encapsulating ConfigErrors, or internal errors + """ + class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1) + func_base_name = convert_camel_case_to_snake(class_name) + resolver_name = f'resolve_create_{func_base_name}' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' + + @mutation.field(mutation_name) + @convert_kwargs_to_snake_case + @with_signature(func_sig, func_name=resolver_name) + async def func_impl(*args, **kwargs): + try: + if 'data' not in kwargs: + return { + "success": False, + "errors": ['missing data'] + } + + data = kwargs['data'] + session = state.settings['app'].state.vyos_session + + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + k = klass(session, data) + k.configure() + + return { + "success": True, + "data": data + } + except Exception as error: + return { + "success": False, + "errors": [str(error)] + } + + return func_impl + + diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql new file mode 100644 index 000000000..a7ee75d40 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -0,0 +1,35 @@ +input dhcpServerConfigInput { + sharedNetworkName: String + subnet: String + defaultRouter: String + dnsServer: String + domainName: String + lease: Int + range: Int + start: String + stop: String + dnsForwardingAllowFrom: String + dnsForwardingCacheSize: Int + dnsForwardingListenAddress: String +} + +type dhcpServerConfig { + sharedNetworkName: String + subnet: String + defaultRouter: String + dnsServer: String + domainName: String + lease: Int + range: Int + start: String + stop: String + dnsForwardingAllowFrom: String + dnsForwardingCacheSize: Int + dnsForwardingListenAddress: String +} + +type createDhcpServerResult { + data: dhcpServerConfig + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql new file mode 100644 index 000000000..fdcf97bad --- /dev/null +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -0,0 +1,18 @@ +input interfaceEthernetConfigInput { + interface: String + address: String + replace: Boolean = true + description: String +} + +type interfaceEthernetConfig { + interface: String + address: String + description: String +} + +type createInterfaceEthernetResult { + data: interfaceEthernetConfig + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql new file mode 100644 index 000000000..8a5e17962 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -0,0 +1,15 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + _dummy: String +} + +directive @generate on FIELD_DEFINITION + +type Mutation { + createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate + createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate +} diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py new file mode 100644 index 000000000..3edb3028e --- /dev/null +++ b/src/services/api/graphql/recipes/dhcp_server.py @@ -0,0 +1,13 @@ + +from . recipe import Recipe + +class DhcpServer(Recipe): + def __init__(self, session, command_file): + super().__init__(session, command_file) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self.data = transform_data(self.data) + # super().configure() diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py new file mode 100644 index 000000000..f88f5924f --- /dev/null +++ b/src/services/api/graphql/recipes/interface_ethernet.py @@ -0,0 +1,13 @@ + +from . recipe import Recipe + +class InterfaceEthernet(Recipe): + def __init__(self, session, command_file): + super().__init__(session, command_file) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self.data = transform_data(self.data) + # super().configure() diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py new file mode 100644 index 000000000..8fbb9e0bf --- /dev/null +++ b/src/services/api/graphql/recipes/recipe.py @@ -0,0 +1,49 @@ +from ariadne import convert_camel_case_to_snake +import vyos.defaults +from vyos.template import render + +class Recipe(object): + def __init__(self, session, data): + self._session = session + self.data = data + self._name = convert_camel_case_to_snake(type(self).__name__) + + @property + def data(self): + return self.__data + + @data.setter + def data(self, data): + if isinstance(data, dict): + self.__data = data + else: + raise ValueError("data must be of type dict") + + def configure(self): + session = self._session + data = self.data + func_base_name = self._name + + tmpl_file = f'{func_base_name}.tmpl' + cmd_file = f'/tmp/{func_base_name}.cmds' + tmpl_dir = vyos.defaults.directories['api_templates'] + + try: + render(cmd_file, tmpl_file, data, location=tmpl_dir) + commands = [] + with open(cmd_file) as f: + lines = f.readlines() + for line in lines: + commands.append(line.split()) + for cmd in commands: + if cmd[0] == 'set': + session.set(cmd[1:]) + elif cmd[0] == 'delete': + session.delete(cmd[1:]) + else: + raise ValueError('Operation must be "set" or "delete"') + session.commit() + except Exception as error: + raise error + + diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl new file mode 100644 index 000000000..629ce83c1 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl @@ -0,0 +1,9 @@ +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }} +set service dns forwarding allow-from {{ dns_forwarding_allow_from }} +set service dns forwarding cache-size {{ dns_forwarding_cache_size }} +set service dns forwarding listen-address {{ dns_forwarding_listen_address }} diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl new file mode 100644 index 000000000..d9d7ed691 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl @@ -0,0 +1,5 @@ +{% if replace %} +delete interfaces ethernet {{ interface }} address +{% endif %} +set interfaces ethernet {{ interface }} address {{ address }} +set interfaces ethernet {{ interface }} description {{ description }} diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py new file mode 100644 index 000000000..63db9f4ef --- /dev/null +++ b/src/services/api/graphql/state.py @@ -0,0 +1,4 @@ + +def init(): + global settings + settings = {} diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cbf321dc8..cb4ce4072 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -36,10 +36,16 @@ from starlette.datastructures import FormData, MutableHeaders from starlette.formparsers import FormParser, MultiPartParser from multipart.multipart import parse_options_header +from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers +from ariadne.asgi import GraphQL + import vyos.config +import vyos.defaults from vyos.configsession import ConfigSession, ConfigSessionError +import api.graphql.state + DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' CFG_GROUP = 'vyattacfg' @@ -603,6 +609,25 @@ def show_op(data: ShowModel): return success(res) +### +# GraphQL integration +### + +api.graphql.state.init() + +from api.graphql.graphql.mutations import mutation +from api.graphql.graphql.directives import DataDirective + +api_schema_dir = vyos.defaults.directories['api_schema'] + +type_defs = load_schema_from_path(api_schema_dir) + +schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + +app.add_route('/graphql', GraphQL(schema, debug=True)) + +### + if __name__ == '__main__': # systemd's user and group options don't work, do it by hand here, # else no one else will be able to commit @@ -626,6 +651,8 @@ if __name__ == '__main__': app.state.vyos_debug = True if server_config['debug'] == 'true' else False app.state.vyos_strict = True if server_config['strict'] == 'true' else False + api.graphql.state.settings['app'] = app + try: uvicorn.run(app, host=server_config["listen_address"], port=int(server_config["port"]), -- cgit v1.2.3 From 9756b870d503eab00714b21c66bf30de47878f6e Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 5 Aug 2021 11:25:31 -0500 Subject: http-api: T2768: add README.graphql (cherry picked from commit 5b69aad5bfe1fd1dfc51afb1d4b6323028009deb) --- src/services/api/graphql/README.graphql | 116 ++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/services/api/graphql/README.graphql (limited to 'src') diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql new file mode 100644 index 000000000..a04138010 --- /dev/null +++ b/src/services/api/graphql/README.graphql @@ -0,0 +1,116 @@ + +Example using GraphQL mutations to configure a DHCP server: + +This assumes that the http-api is running: + +'set service https api' + +One can configure an address on an interface, and configure the DHCP server +to run with that address as default router by requesting these 'mutations' +in the GraphQL playground: + +mutation { + createInterfaceEthernet (data: {interface: "eth1", + address: "192.168.0.1/24", + description: "BOB"}) { + success + errors + data { + address + } + } +} + +mutation { + createDhcpServer(data: {sharedNetworkName: "BOB", + subnet: "192.168.0.0/24", + defaultRouter: "192.168.0.1", + dnsServer: "192.168.0.1", + domainName: "vyos.net", + lease: 86400, + range: 0, + start: "192.168.0.9", + stop: "192.168.0.254", + dnsForwardingAllowFrom: "192.168.0.0/24", + dnsForwardingCacheSize: 0, + dnsForwardingListenAddress: "192.168.0.1"}) { + success + errors + data { + defaultRouter + } + } +} + +The GraphQL playground will be found at: + +https://{{ host_address }}/graphql + +An equivalent curl command to the first example above would be: + +curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}' + +Note that the 'mutation' term is prefaced by 'query' in the curl command. + +What's here: + +services +├── api +│   └── graphql +│   ├── graphql +│   │   ├── directives.py +│   │   ├── __init__.py +│   │   ├── mutations.py +│   │   └── schema +│   │   ├── dhcp_server.graphql +│   │   ├── interface_ethernet.graphql +│   │   └── schema.graphql +│   ├── recipes +│   │   ├── dhcp_server.py +│   │   ├── __init__.py +│   │   ├── interface_ethernet.py +│   │   ├── recipe.py +│   │   └── templates +│   │   ├── dhcp_server.tmpl +│   │   └── interface_ethernet.tmpl +│   └── state.py +├── vyos-configd +├── vyos-hostsd +└── vyos-http-api-server + +The GraphQL library that we are using, Ariadne, advertises itself as a +'schema-first' implementation: define the schema; define resolvers +(handlers) for declared Query and Mutation types (Subscription types are not +currently used). + +In the current approach to a high-level API, we consider the +Jinja2-templated collection of configuration mode 'set'/'delete' commands as +the Ur-data; the GraphQL schema is produced from those files, located in +'api/graphql/recipes/templates'. + +Resolvers for the schema Mutation fields are dynamically generated using a +'directive' added to the respective schema field. The directive, +'@generate', is handled by the class 'DataDirective' in +'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in +'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate +wrapper in 'api/graphql/recipes', with base class doing the (overridable) +configuration steps of calling all defined 'set'/'delete' commands. + +Integrating the above with vyos-http-api-server is ~10 lines of code. + +What needs to be done: + +• automate generation of schema and wrappers from templated configuration +commands + +• investigate whether the subclassing provided by the named wrappers in +'api/graphql/recipes' is sufficient for use cases which need to modify data + +• encapsulate the manipulation of 'canonical names' which transforms the +prefixed camel-case schema names to various snake-case file/function names + +• consider mechanism for migration of templates: offline vs. on-the-fly + +• define the naming convention for those schema fields that refer to +configuration mode parameters: e.g. how much of the path is needed as prefix +to uniquely define the term -- cgit v1.2.3 From 7a85ac845fe5f88cf72472650c371e3fad03941a Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 28 Mar 2021 16:24:33 -0500 Subject: http-api: T3440: give uvicorn time to initialize before starting Nginx (cherry picked from commit 889e16a77517549fb833a90d047455533be02f06) --- src/conf_mode/http-api.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 472eb77e4..7e4b117c8 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -19,6 +19,7 @@ import sys import os import json +import time from copy import deepcopy import vyos.defaults @@ -34,11 +35,6 @@ config_file = '/etc/vyos/http-api.conf' vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] -# XXX: this model will need to be extended for tag nodes -dependencies = [ - 'https.py', -] - def get_config(config=None): http_api = deepcopy(vyos.defaults.api_data) x = http_api.get('api_keys') @@ -103,8 +99,10 @@ def apply(http_api): else: call('systemctl stop vyos-http-api.service') - for dep in dependencies: - cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) + # Let uvicorn settle before restarting Nginx + time.sleep(2) + + cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError) if __name__ == '__main__': try: -- cgit v1.2.3 From 66ff05703ed260c744290fb604dbf31a0dfcd1da Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 7 Nov 2021 14:47:40 -0600 Subject: http-api: T2768: update dhcp-server example for migration 5-to-6 (cherry picked from commit dc9a2821d063a96681d6cb1d962618829b71937d) --- src/services/api/graphql/README.graphql | 2 +- src/services/api/graphql/graphql/schema/dhcp_server.graphql | 4 ++-- src/services/api/graphql/recipes/templates/dhcp_server.tmpl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index a04138010..580c0eb7f 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -25,7 +25,7 @@ mutation { createDhcpServer(data: {sharedNetworkName: "BOB", subnet: "192.168.0.0/24", defaultRouter: "192.168.0.1", - dnsServer: "192.168.0.1", + nameServer: "192.168.0.1", domainName: "vyos.net", lease: 86400, range: 0, diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index a7ee75d40..9f741a0a5 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -2,7 +2,7 @@ input dhcpServerConfigInput { sharedNetworkName: String subnet: String defaultRouter: String - dnsServer: String + nameServer: String domainName: String lease: Int range: Int @@ -17,7 +17,7 @@ type dhcpServerConfig { sharedNetworkName: String subnet: String defaultRouter: String - dnsServer: String + nameServer: String domainName: String lease: Int range: Int diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl index 629ce83c1..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl +++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl @@ -1,5 +1,5 @@ set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }} set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }} set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }} set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }} -- cgit v1.2.3 From c4fb141f115f2fa8ce0463585c0aeaaa22a4251a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 8 Nov 2021 17:33:27 +0100 Subject: T3912: remove duplicate "Welcome to VyOS!" already shown by pre-login (cherry picked from commit 73be449b1cd09f3ca86400753630fb4804fbeca7) --- src/conf_mode/system-login-banner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index 6a8dac318..a40d932e0 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -22,8 +22,6 @@ from vyos import airbag airbag.enable() motd=""" -Welcome to VyOS! - Check out project news at https://blog.vyos.io and feel free to report bugs at https://phabricator.vyos.net -- cgit v1.2.3 From ef392ba9715290a5b8e8b619fc19e708fe7e442b Mon Sep 17 00:00:00 2001 From: Marek Isalski Date: Fri, 6 Aug 2021 14:44:48 +0100 Subject: l2tp: T3724: allow setting accel-ppp l2tp host-name (cherry picked from commit 3d00140453b3967370c77ddd9dac4af223a7ddce) --- data/templates/accel-ppp/l2tp.config.tmpl | 3 +++ interface-definitions/vpn_l2tp.xml.in | 5 +++++ src/conf_mode/vpn_l2tp.py | 2 ++ 3 files changed, 10 insertions(+) (limited to 'src') diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index 070a966b7..a2a2382fa 100644 --- a/data/templates/accel-ppp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -57,6 +57,9 @@ bind={{ outside_addr }} {% if lns_shared_secret %} secret={{ lns_shared_secret }} {% endif %} +{% if lns_host_name %} +host-name={{ lns_host_name }} +{% endif %} [client-ip-range] 0.0.0.0/0 diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 8bcede159..ff3219866 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -33,6 +33,11 @@ Tunnel password used to authenticate the client (LAC) + + + Sent to the client (LAC) in the Host-Name attribute + + diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index e970d2ef5..86aa9af09 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -291,6 +291,8 @@ def get_config(config=None): # LNS secret if conf.exists(['lns', 'shared-secret']): l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + if conf.exists(['lns', 'host-name']): + l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name']) if conf.exists(['ccp-disable']): l2tp['ccp_disable'] = True -- cgit v1.2.3 From 8e8d3d8e6dfec86c210a1b5874e839ad8adaef4b Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sat, 13 Nov 2021 13:04:41 -0600 Subject: graphql: T3993: move schema generation to bindings.py; clean up for linting (cherry picked from commit 9e2694b24b06d928240522322c9a6d60c7a7d290) --- src/services/api/graphql/bindings.py | 14 ++++++++++++ src/services/vyos-http-api-server | 44 +++++++++++++++--------------------- 2 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 src/services/api/graphql/bindings.py (limited to 'src') diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py new file mode 100644 index 000000000..1403841b4 --- /dev/null +++ b/src/services/api/graphql/bindings.py @@ -0,0 +1,14 @@ +import vyos.defaults +from . graphql.mutations import mutation +from . graphql.directives import DataDirective + +from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers + +def generate_schema(): + api_schema_dir = vyos.defaults.directories['api_schema'] + + type_defs = load_schema_from_path(api_schema_dir) + + schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + + return schema diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cb4ce4072..aa7ac6708 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,16 +32,13 @@ from fastapi.responses import HTMLResponse from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute from pydantic import BaseModel, StrictStr, validator -from starlette.datastructures import FormData, MutableHeaders +from starlette.datastructures import FormData from starlette.formparsers import FormParser, MultiPartParser from multipart.multipart import parse_options_header -from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers from ariadne.asgi import GraphQL import vyos.config -import vyos.defaults - from vyos.configsession import ConfigSession, ConfigSessionError import api.graphql.state @@ -69,11 +66,11 @@ def load_server_config(): return config def check_auth(key_list, key): - id = None + key_id = None for k in key_list: if k['key'] == key: - id = k['id'] - return id + key_id = k['id'] + return key_id def error(code, msg): resp = {"success": False, "error": msg, "data": None} @@ -223,10 +220,10 @@ responses = { def auth_required(data: ApiModel): key = data.key api_keys = app.state.vyos_keys - id = check_auth(api_keys, key) - if not id: + key_id = check_auth(api_keys, key) + if not key_id: raise HTTPException(status_code=401, detail="Valid API key is required") - app.state.vyos_id = id + app.state.vyos_id = key_id # override Request and APIRoute classes in order to convert form request to json; # do all explicit validation here, for backwards compatability of error messages; @@ -613,16 +610,11 @@ def show_op(data: ShowModel): # GraphQL integration ### -api.graphql.state.init() - -from api.graphql.graphql.mutations import mutation -from api.graphql.graphql.directives import DataDirective +from api.graphql.bindings import generate_schema -api_schema_dir = vyos.defaults.directories['api_schema'] - -type_defs = load_schema_from_path(api_schema_dir) +api.graphql.state.init() -schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) +schema = generate_schema() app.add_route('/graphql', GraphQL(schema, debug=True)) @@ -640,16 +632,16 @@ if __name__ == '__main__': try: server_config = load_server_config() - except Exception as e: - logger.critical("Failed to load the HTTP API server config: {0}".format(e)) + except Exception as err: + logger.critical(f"Failed to load the HTTP API server config: {err}") - session = ConfigSession(os.getpid()) + config_session = ConfigSession(os.getpid()) - app.state.vyos_session = session + app.state.vyos_session = config_session app.state.vyos_keys = server_config['api_keys'] - app.state.vyos_debug = True if server_config['debug'] == 'true' else False - app.state.vyos_strict = True if server_config['strict'] == 'true' else False + app.state.vyos_debug = bool(server_config['debug'] == 'true') + app.state.vyos_strict = bool(server_config['strict'] == 'true') api.graphql.state.settings['app'] = app @@ -657,6 +649,6 @@ if __name__ == '__main__': uvicorn.run(app, host=server_config["listen_address"], port=int(server_config["port"]), proxy_headers=True) - except OSError as e: - logger.critical(f"OSError {e}") + except OSError as err: + logger.critical(f"OSError {err}") sys.exit(1) -- cgit v1.2.3 From 419f81a0c39740de0ff61ce25325ebea76c4a395 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 14 Nov 2021 19:03:25 -0600 Subject: graphql: T3993: add config file save/load (cherry picked from commit 8915a19f7761253b7bdf6ca847069539ee33851d) --- src/services/api/graphql/README.graphql | 24 +++++++++++ src/services/api/graphql/bindings.py | 4 +- src/services/api/graphql/graphql/directives.py | 17 +++++++- src/services/api/graphql/graphql/mutations.py | 49 ++++++++++++++++++++++ .../api/graphql/graphql/schema/config_file.graphql | 27 ++++++++++++ .../api/graphql/graphql/schema/schema.graphql | 3 ++ src/services/api/graphql/recipes/config_file.py | 16 +++++++ src/services/api/graphql/recipes/recipe.py | 19 +++++++++ 8 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/services/api/graphql/graphql/schema/config_file.graphql create mode 100644 src/services/api/graphql/recipes/config_file.py (limited to 'src') diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index 580c0eb7f..c91b70782 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -42,6 +42,30 @@ mutation { } } +mutation { + saveConfigFile(data: {fileName: "/config/config.boot"}) { + success + errors + data { + fileName + } + } +} + +N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to save to +/config/config.boot; to save to an alternative path, specify fileName. + +mutation { + loadConfigFile(data: {fileName: "/home/vyos/config.boot"}) { + success + errors + data { + fileName + } + } +} + + The GraphQL playground will be found at: https://{{ host_address }}/graphql diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 1403841b4..c123f68d8 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -1,6 +1,6 @@ import vyos.defaults from . graphql.mutations import mutation -from . graphql.directives import DataDirective +from . graphql.directives import DataDirective, ConfigFileDirective from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers @@ -9,6 +9,6 @@ def generate_schema(): type_defs = load_schema_from_path(api_schema_dir) - schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective, "configfile": ConfigFileDirective}) return schema diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 651421c35..85d514de4 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,5 +1,5 @@ from ariadne import SchemaDirectiveVisitor, ObjectType -from . mutations import make_resolver +from . mutations import make_resolver, make_config_file_resolver class DataDirective(SchemaDirectiveVisitor): """ @@ -15,3 +15,18 @@ class DataDirective(SchemaDirectiveVisitor): func = make_resolver(name) field.resolve = func return field + +class ConfigFileDirective(SchemaDirectiveVisitor): + """ + Class providing implementation of 'configfile' directive in schema. + + """ + def visit_field_definition(self, field, object_type): + name = f'{field.type}' + # field.type contains the return value of the mutation; trim value + # to produce canonical name + name = name.replace('Result', '', 1) + + func = make_config_file_resolver(name) + field.resolve = func + return field diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 98c665c9a..2eb0a0b4a 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -57,4 +57,53 @@ def make_resolver(mutation_name): return func_impl +def make_config_file_resolver(mutation_name): + op = '' + if 'save' in mutation_name: + op = 'save' + elif 'load' in mutation_name: + op = 'load' + class_name = mutation_name.replace('save', '', 1).replace('load', '', 1) + func_base_name = convert_camel_case_to_snake(class_name) + resolver_name = f'resolve_{func_base_name}' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' + + @mutation.field(mutation_name) + @convert_kwargs_to_snake_case + @with_signature(func_sig, func_name=resolver_name) + async def func_impl(*args, **kwargs): + try: + if 'data' not in kwargs: + return { + "success": False, + "errors": ['missing data'] + } + + data = kwargs['data'] + session = state.settings['app'].state.vyos_session + + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + k = klass(session, data) + if op == 'save': + k.save() + elif op == 'load': + k.load() + else: + return { + "success": False, + "errors": ["Input must be saveConfigFile | loadConfigFile"] + } + + return { + "success": True, + "data": data + } + except Exception as error: + return { + "success": False, + "errors": [str(error)] + } + + return func_impl diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql new file mode 100644 index 000000000..3096cf743 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -0,0 +1,27 @@ +input saveConfigFileInput { + fileName: String +} + +type saveConfigFile { + fileName: String +} + +type saveConfigFileResult { + data: saveConfigFile + success: Boolean! + errors: [String] +} + +input loadConfigFileInput { + fileName: String! +} + +type loadConfigFile { + fileName: String! +} + +type loadConfigFileResult { + data: loadConfigFile + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 8a5e17962..70fe0d726 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -8,8 +8,11 @@ type Query { } directive @generate on FIELD_DEFINITION +directive @configfile on FIELD_DEFINITION type Mutation { createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate + saveConfigFile(data: saveConfigFileInput) : saveConfigFileResult @configfile + loadConfigFile(data: loadConfigFileInput) : loadConfigFileResult @configfile } diff --git a/src/services/api/graphql/recipes/config_file.py b/src/services/api/graphql/recipes/config_file.py new file mode 100644 index 000000000..850e5326e --- /dev/null +++ b/src/services/api/graphql/recipes/config_file.py @@ -0,0 +1,16 @@ + +from . recipe import Recipe + +class ConfigFile(Recipe): + def __init__(self, session, command_file): + super().__init__(session, command_file) + + # Define any custom processing of parameters here by overriding + # save/load: + # + # def save(self): + # self.data = transform_data(self.data) + # super().save() + # def load(self): + # self.data = transform_data(self.data) + # super().load() diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py index 8fbb9e0bf..91d8bd67a 100644 --- a/src/services/api/graphql/recipes/recipe.py +++ b/src/services/api/graphql/recipes/recipe.py @@ -46,4 +46,23 @@ class Recipe(object): except Exception as error: raise error + def save(self): + session = self._session + data = self.data + if 'file_name' not in data or not data['file_name']: + data['file_name'] = '/config/config.boot' + try: + session.save_config(data['file_name']) + except Exception as error: + raise error + + def load(self): + session = self._session + data = self.data + + try: + session.load_config(data['file_name']) + session.commit() + except Exception as error: + raise error -- cgit v1.2.3 From 0105450e7d103559455c3646091dc40983e68d61 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 17 Nov 2021 18:53:43 +0100 Subject: T3912: add additional newline after "Welcome to VyOS" (cherry picked from commit 77eca49bffede005f546b7d9d3660bf2e32c7e8e) --- src/conf_mode/system-login-banner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index a40d932e0..2220d7b66 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -37,7 +37,7 @@ PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' default_config_data = { - 'issue': 'Welcome to VyOS - \n \l\n', + 'issue': 'Welcome to VyOS - \\n \\l\n\n', 'issue_net': 'Welcome to VyOS\n', 'motd': motd } -- cgit v1.2.3 From 50a1b4a1170182864760613216b68322f165a749 Mon Sep 17 00:00:00 2001 From: zsdc Date: Mon, 4 Oct 2021 10:40:31 +0300 Subject: OpenVPN: T3350: Changed custom options for OpenVPN processing Custom OpenVPN options moved back to the command line from a configuration file. This should keep full compatibility with the `crux` branch, and allows to avoid mistakes with parsing options that contain `--` in the middle. The only smart part of this - handling a `push` option. Because of internal changes in OpenVPN, previously it did not require an argument in the double-quotes, but after version update in `equuleus` and `sagitta` old syntax became invalid. So, all the `push` options are processed to add quotes. The solution is still not complete, because if a single config line contains `push` with other options, it will not work, but it is better than nothing. (cherry picked from commit 3fd2ff423b6c6e992b2ed531c7ba99fb9e1a2123) --- data/templates/openvpn/server.conf.tmpl | 13 ------------- data/templates/openvpn/service-override.conf.tmpl | 20 ++++++++++++++++++++ src/conf_mode/interfaces-openvpn.py | 12 ++++++++++++ .../system/openvpn@.service.d/10-override.conf | 13 +++++++++++++ .../systemd/system/openvpn@.service.d/override.conf | 13 ------------- 5 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 data/templates/openvpn/service-override.conf.tmpl create mode 100644 src/etc/systemd/system/openvpn@.service.d/10-override.conf delete mode 100644 src/etc/systemd/system/openvpn@.service.d/override.conf (limited to 'src') diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index c96b57fb8..c2b0c2ef9 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -257,16 +257,3 @@ auth {{ hash }} auth-user-pass {{ auth_user_pass_file }} auth-retry nointeract {% endif %} - -{% if openvpn_option is defined and openvpn_option is not none %} -# -# Custom options added by user (not validated) -# -{% for option in openvpn_option %} -{% for argument in option.split('--') %} -{% if argument is defined and argument != '' %} ---{{ argument }} -{% endif %} -{% endfor %} -{% endfor %} -{% endif %} diff --git a/data/templates/openvpn/service-override.conf.tmpl b/data/templates/openvpn/service-override.conf.tmpl new file mode 100644 index 000000000..069bdbd08 --- /dev/null +++ b/data/templates/openvpn/service-override.conf.tmpl @@ -0,0 +1,20 @@ +[Service] +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +{%- if openvpn_option is defined and openvpn_option is not none %} +{% for option in openvpn_option %} +{# Remove the '--' prefix from variable if it is presented #} +{% if option.startswith('--') %} +{% set option = option.split('--', maxsplit=1)[1] %} +{% endif %} +{# Workaround to pass '--push' options properly. Previously openvpn accepted this option without values in double-quotes #} +{# But now it stopped doing this, so we need to add them for compatibility #} +{# HOWEVER! This is a raw option and we do not promise that this or any other trick will work for all the cases. #} +{# Using 'openvpn-option' you take all responsibility for compatibility for yourself. #} +{% if option.startswith('push') and not (option.startswith('push "') and option.endswith('"')) %} +{% set option = 'push \"%s\"'|format(option.split('push ', maxsplit=1)[1]) %} +{% endif %} + --{{ option }} +{%- endfor %} +{% endif %} + diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 5d537dadf..4e3c19be2 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -51,6 +51,7 @@ user = 'openvpn' group = 'openvpn' cfg_file = '/run/openvpn/{ifname}.conf' +service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf' def checkCertHeader(header, filename): """ @@ -434,6 +435,11 @@ def generate(openvpn): if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) + # Remove systemd directories with overrides + service_dir = os.path.dirname(service_file.format(**openvpn)) + if os.path.isdir(service_dir): + rmtree(service_dir, ignore_errors=True) + if 'deleted' in openvpn or 'disable' in openvpn: return None @@ -477,6 +483,12 @@ def generate(openvpn): render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) + # Render 20-override.conf for OpenVPN service + render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn, + formater=lambda _: _.replace(""", '"'), user=user, group=group) + # Reload systemd services config to apply an override + call(f'systemctl daemon-reload') + return None def apply(openvpn): diff --git a/src/etc/systemd/system/openvpn@.service.d/10-override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf new file mode 100644 index 000000000..03fe6b587 --- /dev/null +++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf @@ -0,0 +1,13 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/openvpn +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +User=openvpn +Group=openvpn +AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE +CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf deleted file mode 100644 index 03fe6b587..000000000 --- a/src/etc/systemd/system/openvpn@.service.d/override.conf +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -After= -After=vyos-router.service - -[Service] -WorkingDirectory= -WorkingDirectory=/run/openvpn -ExecStart= -ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid -User=openvpn -Group=openvpn -AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE -CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE -- cgit v1.2.3 From b618790b9e5ab51e5d4f65e6756fedca70882cba Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 15 Nov 2021 21:19:51 +0100 Subject: openvpn: T3995: implement systemd reload support (cherry picked from commit eceaa3a787929f5a514b9c45da52936c0d4d4a54) --- src/conf_mode/interfaces-openvpn.py | 4 ++-- src/etc/systemd/system/openvpn@.service.d/10-override.conf | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 4e3c19be2..ae35ed3c4 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -493,10 +493,10 @@ def generate(openvpn): def apply(openvpn): interface = openvpn['ifname'] - call(f'systemctl stop openvpn@{interface}.service') # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: + call(f'systemctl stop openvpn@{interface}.service') for cleanup_file in glob(f'/run/openvpn/{interface}.*'): if os.path.isfile(cleanup_file): os.unlink(cleanup_file) @@ -508,7 +508,7 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - call(f'systemctl start openvpn@{interface}.service') + call(f'systemctl reload-or-restart openvpn@{interface}.service') conf = VTunIf.get_config() conf['device_type'] = openvpn['device_type'] diff --git a/src/etc/systemd/system/openvpn@.service.d/10-override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf index 03fe6b587..775a2d7ba 100644 --- a/src/etc/systemd/system/openvpn@.service.d/10-override.conf +++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf @@ -7,6 +7,7 @@ WorkingDirectory= WorkingDirectory=/run/openvpn ExecStart= ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +ExecReload=/bin/kill -HUP $MAINPID User=openvpn Group=openvpn AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE -- cgit v1.2.3 From a12079f7cb7f8c10bfb309375c3397852502ed78 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 17 Nov 2021 21:42:26 +0100 Subject: snmp: T3996: fix invalid IPv6 localhost handling when using listen-address We need to use a temporary variable when validating the tuple if address is used. If not the else branch will always add the tuple to the list of addresses used for listen-address. (cherry picked from commit d13b91462487e090b32c0d1ecf9139a2271b4837) --- smoketest/scripts/cli/test_service_snmp.py | 33 ++++++++++++++++++++++-------- src/conf_mode/snmp.py | 19 ++++++++++------- 2 files changed, 36 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index 008271102..e15d186bc 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -22,6 +22,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.template import is_ipv4 +from vyos.template import address_from_cidr from vyos.util import read_file from vyos.util import process_named_running @@ -36,16 +37,29 @@ def get_config_value(key): return tmp[0] class TestSNMPService(VyOSUnitTestSHIM.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + # delete testing SNMP config self.cli_delete(base_path) + self.cli_commit() def test_snmp_basic(self): + dummy_if = 'dum7312' + dummy_addr = '100.64.0.1/32' + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr]) + # Check if SNMP can be configured and service runs clients = ['192.0.2.1', '2001:db8::1'] networks = ['192.0.2.128/25', '2001:db8:babe::/48'] - listen = ['127.0.0.1', '::1'] + listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)] + port = '5000' for auth in ['ro', 'rw']: community = 'VyOS' + auth @@ -56,7 +70,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['community', community, 'network', network]) for addr in listen: - self.cli_set(base_path + ['listen-address', addr]) + self.cli_set(base_path + ['listen-address', addr, 'port', port]) self.cli_set(base_path + ['contact', 'maintainers@vyos.io']) self.cli_set(base_path + ['location', 'qemu']) @@ -68,16 +82,18 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): # thus we need to transfor this into a proper list config = get_config_value('agentaddress') expected = 'unix:/run/snmpd.socket' + self.assertIn(expected, config) + for addr in listen: if is_ipv4(addr): - expected += ',udp:{}:161'.format(addr) + expected = f'udp:{addr}:{port}' else: - expected += ',udp6:[{}]:161'.format(addr) - - self.assertTrue(expected in config) + expected = f'udp6:[{addr}]:{port}' + self.assertIn(expected, config) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(['interfaces', 'dummy', dummy_if]) def test_snmpv3_sha(self): @@ -86,7 +102,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002']) self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) - # check validate() - a view must be created before this can be comitted + # check validate() - a view must be created before this can be committed with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -152,4 +168,3 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': unittest.main(verbosity=2) - diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 3990e5735..0fbe90cce 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,13 +20,17 @@ from sys import exit from vyos.config import Config from vyos.configverify import verify_vrf -from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random +from vyos.snmpv3_hashgen import plaintext_to_md5 +from vyos.snmpv3_hashgen import plaintext_to_sha1 +from vyos.snmpv3_hashgen import random from vyos.template import render from vyos.template import is_ipv4 -from vyos.util import call, chmod_755 +from vyos.util import call +from vyos.util import chmod_755 from vyos.validate import is_addr_assigned from vyos.version import get_version_data -from vyos import ConfigError, airbag +from vyos import ConfigError +from vyos import airbag airbag.enable() config_file_client = r'/etc/snmp/snmp.conf' @@ -401,19 +405,20 @@ def verify(snmp): addr = listen[0] port = listen[1] + tmp = None if is_ipv4(addr): # example: udp:127.0.0.1:161 - listen = 'udp:' + addr + ':' + port + tmp = f'udp:{addr}:{port}' elif snmp['ipv6_enabled']: # example: udp6:[::1]:161 - listen = 'udp6:' + '[' + addr + ']' + ':' + port + tmp = f'udp6:[{addr}]:{port}' # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist if is_addr_assigned(addr): - snmp['listen_on'].append(listen) + if tmp: snmp['listen_on'].append(tmp) else: - print('WARNING: SNMP listen address {0} not configured!'.format(addr)) + print(f'WARNING: SNMP listen address {addr} not configured!') verify_vrf(snmp) -- cgit v1.2.3 From a032d73f1d405f3bae269791e9064026faa491d9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 18 Nov 2021 17:56:57 +0100 Subject: wwan: T3795: make connect and disconnect op-mode commands aware to WWAN interfaces --- python/vyos/util.py | 16 +++++++++ src/op_mode/connect_disconnect.py | 68 +++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/python/vyos/util.py b/python/vyos/util.py index 9f01d504d..1834b78bd 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -717,3 +717,19 @@ def is_systemd_service_running(service): Copied from: https://unix.stackexchange.com/a/435317 """ tmp = cmd(f'systemctl show --value -p SubState {service}') return bool((tmp == 'running')) + +def is_wwan_connected(interface): + """ Determine if a given WWAN interface, e.g. wwan0 is connected to the + carrier network or not """ + import json + + if not interface.startswith('wwan'): + raise ValueError(f'Specified interface "{interface}" is not a WWAN interface') + + modem = interface.lstrip('wwan') + + tmp = cmd(f'mmcli --modem {modem} --output-json') + tmp = json.loads(tmp) + + # return True/False if interface is in connected state + return dict_search('modem.generic.state', tmp) == 'connected' diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index a773aa28e..ffc574362 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -17,21 +17,19 @@ import os import argparse -from sys import exit from psutil import process_iter -from time import strftime, localtime, time from vyos.util import call +from vyos.util import DEVNULL +from vyos.util import is_wwan_connected -def check_interface(interface): +def check_ppp_interface(interface): if not os.path.isfile(f'/etc/ppp/peers/{interface}'): - print(f'Interface {interface}: invalid!') + print(f'Interface {interface} does not exist!') exit(1) def check_ppp_running(interface): - """ - Check if ppp process is running in the interface in question - """ + """ Check if PPP process is running in the interface in question """ for p in process_iter(): if "pppd" in p.name(): if interface in p.cmdline(): @@ -40,32 +38,46 @@ def check_ppp_running(interface): return False def connect(interface): - """ - Connect PPP interface - """ - check_interface(interface) + """ Connect dialer interface """ - # Check if interface is already dialed - if os.path.isdir(f'/sys/class/net/{interface}'): - print(f'Interface {interface}: already connected!') - elif check_ppp_running(interface): - print(f'Interface {interface}: connection is beeing established!') + if interface.startswith('ppp'): + check_ppp_interface(interface) + # Check if interface is already dialed + if os.path.isdir(f'/sys/class/net/{interface}'): + print(f'Interface {interface}: already connected!') + elif check_ppp_running(interface): + print(f'Interface {interface}: connection is beeing established!') + else: + print(f'Interface {interface}: connecting...') + call(f'systemctl restart ppp@{interface}.service') + elif interface.startswith('wwan'): + if is_wwan_connected(interface): + print(f'Interface {interface}: already connected!') + else: + call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py') else: - print(f'Interface {interface}: connecting...') - call(f'systemctl restart ppp@{interface}.service') + print(f'Unknown interface {interface}, can not connect. Aborting!') def disconnect(interface): - """ - Disconnect PPP interface - """ - check_interface(interface) + """ Disconnect dialer interface """ - # Check if interface is already down - if not check_ppp_running(interface): - print(f'Interface {interface}: connection is already down') + if interface.startswith('ppp'): + check_ppp_interface(interface) + + # Check if interface is already down + if not check_ppp_running(interface): + print(f'Interface {interface}: connection is already down') + else: + print(f'Interface {interface}: disconnecting...') + call(f'systemctl stop ppp@{interface}.service') + elif interface.startswith('wwan'): + if not is_wwan_connected(interface): + print(f'Interface {interface}: connection is already down') + else: + modem = interface.lstrip('wwan') + call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL) else: - print(f'Interface {interface}: disconnecting...') - call(f'systemctl stop ppp@{interface}.service') + print(f'Unknown interface {interface}, can not disconnect. Aborting!') def main(): parser = argparse.ArgumentParser() -- cgit v1.2.3 From 4747e944233de14b5c66ca3d7004d1174554681a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 18 Nov 2021 17:58:10 +0100 Subject: wwan: T3795: do not fail config-load when signal is missing --- src/conf_mode/interfaces-wwan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index 31c599145..cb46b3723 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists from vyos.configverify import verify_vrf from vyos.ifconfig import WWANIf from vyos.util import cmd +from vyos.util import call from vyos.util import dict_search +from vyos.util import DEVNULL from vyos.template import render from vyos import ConfigError from vyos import airbag @@ -89,7 +91,7 @@ def apply(wwan): options += ',user={user},password={password}'.format(**wwan['authentication']) command = f'{base_cmd} --simple-connect="{options}"' - cmd(command) + call(command, stdout=DEVNULL) w.update(wwan) return None -- cgit v1.2.3 From eb6247e4b464c36fa7441627b221d0db39429251 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 18 Nov 2021 17:58:44 +0100 Subject: wwan: T3795: periodically check if WWAN connection needs a reconnect --- debian/vyos-1x.install | 1 + src/etc/cron.d/check-wwan | 1 + src/helpers/vyos-check-wwan.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 src/etc/cron.d/check-wwan create mode 100755 src/helpers/vyos-check-wwan.py (limited to 'src') diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index c075db898..0c0c203ea 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,3 +1,4 @@ +etc/cron.d etc/dhcp etc/netplug etc/ppp diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan new file mode 100644 index 000000000..28190776f --- /dev/null +++ b/src/etc/cron.d/check-wwan @@ -0,0 +1 @@ +*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py new file mode 100755 index 000000000..c6e6c54b7 --- /dev/null +++ b/src/helpers/vyos-check-wwan.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 . + +from vyos.configquery import VbashOpRun +from vyos.configquery import ConfigTreeQuery + +from vyos.util import is_wwan_connected +from vyos.util import call + +conf = ConfigTreeQuery() +dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'), + get_first_key=True) + +for interface, interface_config in dict.items(): + if not is_wwan_connected(interface): + if 'disable' in interface_config: + # do not restart this interface as it's disabled by the user + continue + + #op = VbashOpRun() + #op.run(['connect', 'interface', interface]) + call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py') + +exit(0) -- cgit v1.2.3 From e7c3137708652bea9c0f8b5fe703717bb6f49d84 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Fri, 19 Nov 2021 15:37:39 -0600 Subject: http-api: T4003: fix output when no tty attached to stdout, e.g., api (cherry picked from commit 82ea3b4f3c12023ce17f1062785b6238f457673d) --- src/op_mode/show_interfaces.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index aef2d8060..281d25e30 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -85,10 +85,8 @@ def split_text(text, used=0): used: number of characted already used in the screen """ no_tty = call('tty -s') - if no_tty: - return text.split() - returned = cmd('stty size') + returned = cmd('stty size') if not no_tty else '' if len(returned) == 2: rows, columns = [int(_) for _ in returned] else: -- cgit v1.2.3