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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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/conf_mode') 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/conf_mode') 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/conf_mode') 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 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/conf_mode') 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