From dac5ad318b00853591121078bce5c849db23c810 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 3 Mar 2020 16:51:44 +0000 Subject: ifconfig: T2057: generic interface option setting this patch allows to get or change many interface options (mtu, arp settings, ...) using get_interface / set_interface functions --- python/vyos/ifconfig.py | 547 ++++++++++++++++++++++++++++++------------------ python/vyos/validate.py | 53 +++++ 2 files changed, 396 insertions(+), 204 deletions(-) diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index d5b511f82..beeafa420 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -66,13 +66,194 @@ interface "{{ intf }}" { """ -class Interface: +class Control: + _command_get = {} + _command_set = {} + + def _debug_msg(self, msg): + if os.path.isfile('/tmp/vyos.ifconfig.debug'): + print('DEBUG/{:<6} {}'.format(self.config['ifname'], msg)) + + def _cmd(self, command): + p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) + tmp = p.communicate()[0].strip() + self._debug_msg("cmd '{}'".format(command)) + if tmp.decode(): + self._debug_msg("returned:\n{}".format(tmp.decode())) + + # do we need some error checking code here? + return tmp.decode() + + def _get_command(self, config, name): + """ + Using the defined names, set data write to sysfs. + """ + cmd = self._command_get[name]['shellcmd'].format(**config) + return self._cmd(cmd) + + def _set_command(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + if not value: + return None + + # the code can pass int as int + value = str(value) + + validate = self._command_set[name].get('validate', None) + if validate: + validate(value) + + config = {**config, **{'value': value}} + + convert = self._command_set[name].get('convert', None) + if convert: + value = convert(value) + + cmd = self._command_set[name]['shellcmd'].format(**config) + return self._cmd(cmd) + + _sysfs_get = {} + _sysfs_set = {} + + def _read_sysfs(self, filename): + """ + Provide a single primitive w/ error checking for reading from sysfs. + """ + value = None + with open(filename, 'r') as f: + value = f.read().rstrip('\n') + + self._debug_msg("read '{}' < '{}'".format(value, filename)) + return value + + def _write_sysfs(self, filename, value): + """ + Provide a single primitive w/ error checking for writing to sysfs. + """ + self._debug_msg("write '{}' > '{}'".format(value, filename)) + if os.path.isfile(filename): + with open(filename, 'w') as f: + f.write(str(value)) + return True + return False + + def _get_sysfs(self, config, name): + """ + Using the defined names, get data write from sysfs. + """ + filename = self._sysfs_get[name]['location'].format(config) + if not filename: + return None + return self._read_sysfs(filename) + + def _set_sysfs(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + if not value: + return None + + # the code can pass int as int + value = str(value) + + validate = self._sysfs_set[name].get('validate', None) + if validate: + validate(value) + + config = {**config, **{'value': value}} + + convert = self._sysfs_set[name].get('convert', None) + if convert: + value = convert(value) + + commited = self._write_sysfs(self._sysfs_set[name]['location'].format(**config), value) + if not commited: + errmsg = self._sysfs_set.get('errormsg','') + if errmsg: + raise TypeError(errmsg.format(**config)) + return commited + + def get_interface(self, name): + if name in self._sysfs_get: + return self._get_sysfs(self.config, name) + if name in self._command_get: + return self._get_command(self.config, name) + raise KeyError(f'{name} is not a attribute of the interface we can get') + + def set_interface(self, name, value): + if name in self._sysfs_set: + return self._set_sysfs(self.config, name, value) + if name in self._command_set: + return self._set_command(self.config, name, value) + raise KeyError(f'{name} is not a attribute of the interface we can set') + + +class Interface(Control): options = [] required = [] default = { 'type': '', } + _command_set = { + 'mac': { + 'validate': assert_mac, + 'shellcmd': 'ip link set dev {ifname} address {value}', + }, + } + + _sysfs_get = { + 'mtu': { + 'location': '/sys/class/net/{ifname}/mtu', + }, + } + + _sysfs_set = { + 'alias': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mtu': { + 'validate': assert_mtu, + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'arp_cache_tmo': { + 'convert': lambda tmo: (int(tmo) * 1000), + 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms', + }, + 'arp_filter': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter', + }, + 'arp_accept': { + 'validate': lambda arp: assert_range(arp,0,2), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept', + }, + 'arp_announce': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce', + }, + 'arp_ignore': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', + }, + 'proxy_arp': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', + }, + 'proxy_arp_pvlan': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan', + }, + # link_detect vs link_filter name weirdness + 'link_detect': { + 'validate': lambda link: assert_range(link,0,3), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter', + }, + } + def __init__(self, ifname, **kargs): """ This is the base interface class which supports basic IP/MAC address @@ -142,41 +323,6 @@ class Interface: cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) self._cmd(cmd) - def _debug_msg(self, msg): - if os.path.isfile('/tmp/vyos.ifconfig.debug'): - print('DEBUG/{:<6} {}'.format(self.config['ifname'], msg)) - - def _cmd(self, command): - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - self._debug_msg("cmd '{}'".format(command)) - if tmp.decode(): - self._debug_msg("returned:\n{}".format(tmp.decode())) - - # do we need some error checking code here? - return tmp.decode() - - def _read_sysfs(self, filename): - """ - Provide a single primitive w/ error checking for reading from sysfs. - """ - value = None - with open(filename, 'r') as f: - value = f.read().rstrip('\n') - - self._debug_msg("read '{}' < '{}'".format(value, filename)) - return value - - def _write_sysfs(self, filename, value): - """ - Provide a single primitive w/ error checking for writing to sysfs. - """ - self._debug_msg("write '{}' > '{}'".format(value, filename)) - with open(filename, 'w') as f: - f.write(str(value)) - - return None - def remove(self): """ Remove interface from operating system. Removing the interface @@ -235,8 +381,7 @@ class Interface: >>> Interface('eth0').get_mtu() '1500' """ - return self._read_sysfs('/sys/class/net/{}/mtu' - .format(self.config['ifname'])) + return self.get_interface('mtu') def set_mtu(self, mtu): """ @@ -248,11 +393,7 @@ class Interface: >>> Interface('eth0').get_mtu() '1400' """ - if mtu < 68 or mtu > 9000: - raise ValueError('Invalid MTU size: "{}"'.format(mru)) - - return self._write_sysfs('/sys/class/net/{}/mtu' - .format(self.config['ifname']), mtu) + return self.set_interface('mtu', mtu) def set_mac(self, mac): """ @@ -262,33 +403,7 @@ class Interface: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') """ - # on interface removal (ethernet) an empty string is passed - ignore it - if not mac: - return None - - # a mac address consits out of 6 octets - octets = len(mac.split(':')) - if octets != 6: - raise ValueError('wrong number of MAC octets: {} '.format(octets)) - - # validate against the first mac address byte if it's a multicast - # address - if int(mac.split(':')[0], 16) & 1: - raise ValueError('{} is a multicast MAC address'.format(mac)) - - # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(int(i, 16) for i in mac.split(':')) == 0: - raise ValueError('00:00:00:00:00:00 is not a valid MAC address') - - # check for VRRP mac address - if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': - raise ValueError('{} is a VRRP MAC address'.format(mac)) - - # Assemble command executed on system. Unfortunately there is no way - # of altering the MAC address via sysfs - cmd = 'ip link set dev {} address {}'.format(self.config['ifname'], mac) - return self._cmd(cmd) - + self.set_interface('mac', mac) def set_arp_cache_tmo(self, tmo): """ @@ -299,8 +414,7 @@ class Interface: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_arp_cache_tmo(40) """ - return self._write_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' - .format(self.config['ifname']), (int(tmo) * 1000)) + return self.set_interface('arp_cache_tmo', tmo) def set_arp_filter(self, arp_filter): """ @@ -320,11 +434,7 @@ class Interface: particular interfaces. Only for more complex setups like load- balancing, does this behaviour cause problems. """ - if int(arp_filter) >= 0 and int(arp_filter) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_filter' - .format(self.config['ifname']), arp_filter) - else: - raise ValueError("Value out of range") + return self.set_interface('arp_filter', arp_filter) def set_arp_accept(self, arp_accept): """ @@ -340,11 +450,7 @@ class Interface: gratuitous arp frame, the arp table will be updated regardless if this setting is on or off. """ - if int(arp_accept) >= 0 and int(arp_accept) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_accept' - .format(self.config['ifname']), arp_accept) - else: - raise ValueError("Value out of range") + return self.set_interface('arp_accept', arp_accept) def set_arp_announce(self, arp_announce): """ @@ -365,11 +471,7 @@ class Interface: receiving answer from the resolved target while decreasing the level announces more valid sender's information. """ - if int(arp_announce) >= 0 and int(arp_announce) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_announce' - .format(self.config['ifname']), arp_announce) - else: - raise ValueError("Value out of range") + return self.set_interface('arp_announce', arp_announce) def set_arp_ignore(self, arp_ignore): """ @@ -381,11 +483,7 @@ class Interface: 1 - reply only if the target IP address is local address configured on the incoming interface """ - if int(arp_ignore) >= 0 and int(arp_ignore) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_ignore' - .format(self.config['ifname']), arp_ignore) - else: - raise ValueError("Value out of range") + return self.set_interface('arp_ignore', arp_ignore) def set_link_detect(self, link_filter): """ @@ -407,14 +505,9 @@ class Interface: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_link_detect(1) """ - sysfs_file = '/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self.config['ifname']) - if os.path.exists(sysfs_file): - if int(link_filter) >= 0 and int(link_filter) <= 2: - return self._write_sysfs(sysfs_file, link_filter) - else: - raise ValueError("Value out of range") + return self.set_interface('link_detect', link_filter) - def set_alias(self, ifalias=None): + def set_alias(self, ifalias=''): """ Set interface alias name used by e.g. SNMP @@ -426,12 +519,7 @@ class Interface: >>> Interface('eth0').set_ifalias('') """ - if not ifalias: - # clear interface alias - ifalias = '\0' - - self._write_sysfs('/sys/class/net/{}/ifalias' - .format(self.config['ifname']), ifalias) + self.set_interface('alias', ifalias) def get_state(self): """ @@ -473,11 +561,7 @@ class Interface: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_proxy_arp(1) """ - if int(enable) >= 0 and int(enable) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp' - .format(self.config['ifname']), enable) - else: - raise ValueError("Value out of range") + self.set_interface('proxy_arp', enable) def set_proxy_arp_pvlan(self, enable): """ @@ -503,11 +587,7 @@ class Interface: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_proxy_arp_pvlan(1) """ - if int(enable) >= 0 and int(enable) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan' - .format(self.config['ifname']), enable) - else: - raise ValueError("Value out of range") + self.set_interface('proxy_arp_pvlan', enable) def get_addr(self): """ @@ -899,6 +979,20 @@ class STPIf(Interface): A spanning-tree capable interface. This applies only to bridge port member interfaces! """ + _sysfs_set = {**Interface._sysfs_set, **{ + 'path_cost': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/path_cost', + 'errormsg': '{ifname} is not a bridge port member' + }, + 'path_priority': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/priority', + 'errormsg': '{ifname} is not a bridge port member' + }, + }} default = { 'type': 'stp', @@ -916,12 +1010,7 @@ class STPIf(Interface): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_path_cost(4) """ - if not os.path.isfile('/sys/class/net/{}/brport/path_cost' - .format(self.config['ifname'])): - raise TypeError('{} is not a bridge port member'.format(self.config['ifname'])) - - return self._write_sysfs('/sys/class/net/{}/brport/path_cost' - .format(self.config['ifname']), cost) + self.set_interface('path_cost', cost) def set_path_priority(self, priority): """ @@ -932,13 +1021,7 @@ class STPIf(Interface): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_path_priority(4) """ - if not os.path.isfile('/sys/class/net/{}/brport/priority' - .format(self.config['ifname'])): - raise TypeError('{} is not a bridge port member'.format(self.config['ifname'])) - - return self._write_sysfs('/sys/class/net/{}/brport/priority' - .format(self.config['ifname']), priority) - + self.set_interface('path_priority', priority) class BridgeIf(Interface): """ @@ -950,6 +1033,53 @@ class BridgeIf(Interface): The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. """ + _sysfs_set = {**Interface._sysfs_set, **{ + 'ageing_time': { + 'validate': assert_positive, + 'convert': lambda time: int(time) * 100, + 'location': '/sys/class/net/{ifname}/bridge/ageing_time', + }, + 'forward_delay': { + 'validate': assert_positive, + 'convert': lambda time: int(time) * 100, + 'location': '/sys/class/net/{ifname}/bridge/forward_delay', + }, + 'hello_time': { + 'validate': assert_positive, + 'convert': lambda time: int(time) * 100, + 'location': '/sys/class/net/{ifname}/bridge/hello_time', + }, + 'max_age': { + 'validate': assert_positive, + 'convert': lambda time: int(time) * 100, + 'location': '/sys/class/net/{ifname}/bridge/max_age', + }, + 'priority': { + 'validate': assert_positive, + 'convert': lambda time: int(time) * 100, + 'location': '/sys/class/net/{ifname}/bridge/priority', + }, + 'stp': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifconfig}/bridge/stp_state', + }, + 'multicast_querier': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', + }, + }} + + _command_set = {**Interface._command_set, **{ + 'add_port': { + 'validate': assert_boolean, + 'shellcmd': 'ip link set dev {value} master {ifname}', + }, + 'del_port': { + 'validate': assert_boolean, + 'shellcmd': 'ip link set dev {value} nomaster', + }, + }} + default = { 'type': 'bridge', } @@ -966,9 +1096,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').ageing_time(2) """ - time = int(time) * 100 - return self._write_sysfs('/sys/class/net/{}/bridge/ageing_time' - .format(self.config['ifname']), time) + self.set_interface('ageing_time', time) def set_forward_delay(self, time): """ @@ -979,8 +1107,8 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').forward_delay(15) """ - return self._write_sysfs('/sys/class/net/{}/bridge/forward_delay' - .format(self.config['ifname']), (int(time) * 100)) + self.set_interface('forward_delay', time) + def set_hello_time(self, time): """ @@ -991,8 +1119,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').set_hello_time(2) """ - return self._write_sysfs('/sys/class/net/{}/bridge/hello_time' - .format(self.config['ifname']), (int(time) * 100)) + self.set_interface('hello_time', time) def set_max_age(self, time): """ @@ -1003,8 +1130,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').set_max_age(30) """ - return self._write_sysfs('/sys/class/net/{}/bridge/max_age' - .format(self.config['ifname']), (int(time) * 100)) + self.set_interface('max_age', time) def set_priority(self, priority): """ @@ -1014,8 +1140,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').set_priority(8192) """ - return self._write_sysfs('/sys/class/net/{}/bridge/priority' - .format(self.config['ifname']), priority) + self.set_interface('priority', time) def set_stp(self, state): """ @@ -1025,12 +1150,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').set_stp(1) """ - - if int(state) >= 0 and int(state) <= 1: - return self._write_sysfs('/sys/class/net/{}/bridge/stp_state' - .format(self.config['ifname']), state) - else: - raise ValueError("Value out of range") + self.set_interface('stp', state) def set_multicast_querier(self, enable): @@ -1046,11 +1166,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').set_multicast_querier(1) """ - if int(enable) >= 0 and int(enable) <= 1: - return self._write_sysfs('/sys/class/net/{}/bridge/multicast_querier' - .format(self.config['ifname']), enable) - else: - raise ValueError("Value out of range") + self.set_interface('multicast_querier', enable) def add_port(self, interface): @@ -1062,8 +1178,7 @@ class BridgeIf(Interface): >>> BridgeIf('br0').add_port('eth0') >>> BridgeIf('br0').add_port('eth1') """ - cmd = 'ip link set dev {} master {}'.format(interface, self.config['ifname']) - return self._cmd(cmd) + return self.set_interface('add_port', interface) def del_port(self, interface): """ @@ -1073,8 +1188,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').del_port('eth1') """ - cmd = 'ip link set dev {} nomaster'.format(interface) - return self._cmd(cmd) + return self.set_interface('del_port', interface) class VLANIf(Interface): """ @@ -1197,6 +1311,29 @@ class EthernetIf(VLANIf): Abstraction of a Linux Ethernet Interface """ + _command_set = {**Interface._command_set, **{ + 'gro': { + 'validate': lambda v: assert_list(v,['on','off']), + 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}', + }, + 'gso': { + 'validate': lambda v: assert_list(v,['on','off']), + 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}', + }, + 'sg': { + 'validate': lambda v: assert_list(v,['on','off']), + 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}', + }, + 'tso': { + 'validate': lambda v: assert_list(v,['on','off']), + 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}', + }, + 'ufo': { + 'validate': lambda v: assert_list(v,['on','off']), + 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}', + }, + }} + default = { 'type': 'ethernet', } @@ -1340,11 +1477,7 @@ class EthernetIf(VLANIf): >>> i = EthernetIf('eth0') >>> i.set_gro('on') """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} gro {}'.format(self.config['ifname'], state) - return self._cmd(cmd) + return self.set_interface('gro', state) def set_gso(self, state): @@ -1354,11 +1487,7 @@ class EthernetIf(VLANIf): >>> i = EthernetIf('eth0') >>> i.set_gso('on') """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} gso {}'.format(self.config['ifname'], state) - return self._cmd(cmd) + return self.set_interface('gso', state) def set_sg(self, state): @@ -1368,11 +1497,7 @@ class EthernetIf(VLANIf): >>> i = EthernetIf('eth0') >>> i.set_sg('on') """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} sg {}'.format(self.config['ifname'], state) - return self._cmd(cmd) + return self.set_interface('sg', state) def set_tso(self, state): @@ -1382,11 +1507,7 @@ class EthernetIf(VLANIf): >>> i = EthernetIf('eth0') >>> i.set_tso('on') """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} tso {}'.format(self.config['ifname'], state) - return self._cmd(cmd) + return self.set_interface('tso', state) def set_ufo(self, state): @@ -1453,6 +1574,45 @@ class BondIf(VLANIf): monitoring may be performed. """ + _sysfs_set = {**Interface._sysfs_set, **{ + 'bond_hash_policy': { + 'validate': lambda v: assert_list(v,['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']), + 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy', + }, + 'bond_miimon': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/miimon' + }, + 'bond_arp_interval': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/arp_interval' + }, + 'bond_arp_ip_target': { + # XXX: no validation of the IP + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + }, + 'bond_add_port': { + 'location': '/sys/class/net/{ifname}+{value}/bonding/slaves', + }, + 'bond_del_port': { + 'location': '/sys/class/net/{ifname}-{value}/bonding/slaves', + }, + 'bond_primary': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/bonding/primary', + }, + 'bond_mode': { + 'validate': lambda v: assert_list(v,['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']), + 'location': '/sys/class/net/{ifname}/bonding/mode', + }, + }} + + _sysfs_get = {**Interface._sysfs_get, **{ + 'bond_arp_ip_target': { + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + } + }} + default = { 'type': 'bond', } @@ -1503,10 +1663,7 @@ class BondIf(VLANIf): >>> from vyos.ifconfig import BondIf >>> BondIf('bond0').set_hash_policy('layer2+3') """ - if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']: - raise ValueError("Value out of range") - return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' - .format(self.config['ifname']), mode) + self.set_interface('bond_hash_policy', mode) def set_arp_interval(self, interval): """ @@ -1538,11 +1695,9 @@ class BondIf(VLANIf): inspected for link failures. A value of zero disables MII link monitoring. A value of 100 is a good starting point. """ - return self._write_sysfs('/sys/class/net/{}/bonding/miimon' - .format(self.config['ifname']), interval) + return self.set_interface('bond_miimon', interval) else: - return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval' - .format(self.config['ifname']), interval) + return self.set_interface('bond_arp_interval', interval) def get_arp_ip_target(self): """ @@ -1560,8 +1715,7 @@ class BondIf(VLANIf): >>> BondIf('bond0').get_arp_ip_target() '192.0.2.1' """ - return self._read_sysfs('/sys/class/net/{}/bonding/arp_ip_target' - .format(self.config['ifname'])) + return self.get_interface('bond_arp_ip_target') def set_arp_ip_target(self, target): """ @@ -1580,8 +1734,7 @@ class BondIf(VLANIf): >>> BondIf('bond0').get_arp_ip_target() '192.0.2.1' """ - return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target' - .format(self.config['ifname']), target) + return self.set_interface('bond_arp_ip_target', target) def add_port(self, interface): """ @@ -1596,9 +1749,7 @@ class BondIf(VLANIf): # interface is in 'up' state, the following Kernel error will be thrown: # bond0: eth1 is up - this may be due to an out of date ifenslave. Interface(interface).set_state('down') - - return self._write_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self.config['ifname']), '+' + interface) + return self.set_interface('bond_add_port', interface) def del_port(self, interface): """ @@ -1608,8 +1759,7 @@ class BondIf(VLANIf): >>> from vyos.ifconfig import BondIf >>> BondIf('bond0').del_port('eth1') """ - return self._write_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self.config['ifname']), '-' + interface) + return self.set_interface('bond_del_port', interface) def get_slaves(self): """ @@ -1646,12 +1796,7 @@ class BondIf(VLANIf): >>> from vyos.ifconfig import BondIf >>> BondIf('bond0').set_primary('eth2') """ - if not interface: - # reset primary interface - interface = '\0' - - return self._write_sysfs('/sys/class/net/{}/bonding/primary' - .format(self.config['ifname']), interface) + return self.set_interface('bond_primary', interface) def set_mode(self, mode): """ @@ -1668,13 +1813,7 @@ class BondIf(VLANIf): >>> from vyos.ifconfig import BondIf >>> BondIf('bond0').set_mode('802.3ad') """ - if not mode in [ - 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', - '802.3ad', 'balance-tlb', 'balance-alb']: - raise ValueError("Value out of range") - - return self._write_sysfs('/sys/class/net/{}/bonding/mode' - .format(self.config['ifname']), mode) + return self.set_interface('bond_mode', mode) class WireGuardIf(Interface): options = ['port', 'private-key', 'pubkey', 'psk', 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 33c495d91..4dca82dd8 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -168,3 +168,56 @@ def is_subnet_connected(subnet, primary=False): return True return False + + +def assert_boolean(b): + if int(b) not in (0, 1): + raise ValueError(f'Value {b} out of range') + + +def assert_range(value, lower=0, count=3): + if int(value) not in range(lower,lower+count): + raise ValueError("Value out of range") + + +def assert_list(s, l): + if s not in l: + o = ' or '.join([f'"{n}"' for n in l]) + raise ValueError(f'state must be {o}, got {s}') + + +def assert_number(n): + if not n.isnumeric(): + raise ValueError(f'{n} must be a number') + + +def assert_positive(n, smaller=0): + assert_number(n) + if int(n) < smaller: + raise ValueError(f'{n} is smaller than {limit}') + + +def assert_mtu(mtu, min=68, max=9000): + assert_number(mtu) + if int(mtu) < min or int(mtu) > max: + raise ValueError(f'Invalid MTU size: "{mtu}"') + + +def assert_mac(m): + octets = [int(i, 16) for i in m.split(':')] + + # a mac address consits out of 6 octets + if len(octets) != 6: + raise ValueError(f'wrong number of MAC octets: {octets}') + + # validate against the first mac address byte if it's a multicast + # address + if octets[0] & 1: + raise ValueError(f'{m} is a multicast MAC address') + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(octets) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + if octets[:5] == (0, 0, 94, 0, 1): + raise ValueError(f'{m} is a VRRP MAC address') -- cgit v1.2.3 From 665d1c5bdb24aa0aef79405dc2f2962b930fb9b3 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 3 Mar 2020 20:01:56 +0100 Subject: vrf: T31: initial support for a VRF backend in XML/Python This is a work in progress to complete T31 whoever thought it was less than 1 hour of work was ..... optimistic. Only VRF vreation and show is supported right now. No interface can be bound to any one VRF. --- interface-definitions/include/interface-vrf.xml.i | 12 ++ interface-definitions/vrf.xml.in | 58 +++++++ op-mode-definitions/show-vrf.xml | 24 +++ python/vyos/vrf.py | 23 +++ src/completion/list_vrf.py | 27 ++++ src/conf_mode/vrf.py | 186 ++++++++++++++++++++++ src/validators/interface-name | 29 ++++ 7 files changed, 359 insertions(+) create mode 100644 interface-definitions/include/interface-vrf.xml.i create mode 100644 interface-definitions/vrf.xml.in create mode 100644 op-mode-definitions/show-vrf.xml create mode 100644 python/vyos/vrf.py create mode 100755 src/completion/list_vrf.py create mode 100755 src/conf_mode/vrf.py create mode 100755 src/validators/interface-name diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i new file mode 100644 index 000000000..7e880e6ee --- /dev/null +++ b/interface-definitions/include/interface-vrf.xml.i @@ -0,0 +1,12 @@ + + + VRF instance name + + vrf name + + + + + VRF name not allowed or to long + + diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in new file mode 100644 index 000000000..e270e8b90 --- /dev/null +++ b/interface-definitions/vrf.xml.in @@ -0,0 +1,58 @@ + + + + + VRF configuration + + 210 + + + + + Disable services running on the default VRF from other VRF (ssh, bgp, ...) + + + + + + Enable binding across all VRF domains for IPv4 + + + + + + + Virtual Routing and Forwarding + + + + VRF name not allowed or to long + + name + the vrf name must not contain '/' and be 16 characters or less + + + + + + The routing table to associate to this VRF + + + + Invalid kernel table number + + number + the VRF must be a number between 1 and 2^31-1 + + + + + + Description of the VRF role + + + + + + + \ No newline at end of file diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml new file mode 100644 index 000000000..fb2fddd49 --- /dev/null +++ b/op-mode-definitions/show-vrf.xml @@ -0,0 +1,24 @@ + + + + + + + Show VRF information + + ${vyos_completion_dir}/list_vrf.py -e + + + + Show VRF information for an interface + + + + + ${vyos_completion_dir}/list_vrf.py -e "$4" + + + + + + diff --git a/python/vyos/vrf.py b/python/vyos/vrf.py new file mode 100644 index 000000000..99e4cb7d1 --- /dev/null +++ b/python/vyos/vrf.py @@ -0,0 +1,23 @@ +# Copyright 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import json +import subprocess + + +def list_vrfs(): + command = 'ip -j -br link show type vrf' + answer = json.loads(subprocess.check_output(command.split()).decode()) + return [_ for _ in answer if _] diff --git a/src/completion/list_vrf.py b/src/completion/list_vrf.py new file mode 100755 index 000000000..210b3c9a4 --- /dev/null +++ b/src/completion/list_vrf.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import argparse +import vyos.vrf + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-e", "--extensive", action="store_true", + help="provide detailed vrf informatio") +parser.add_argument('interface', metavar='I', type=str, nargs='?', + help='interface to display') + +args = parser.parse_args() + +if args.extensive: + print('{:16} {:7} {:17} {}'.format('interface', 'state', 'mac', 'flags')) + print('{:16} {:7} {:17} {}'.format('---------', '-----', '---', '-----')) + for vrf in vyos.vrf.list_vrfs(): + name = vrf['ifname'] + if args.interface and name != args.interface: + continue + state = vrf['operstate'].lower() + mac = vrf['address'].lower() + info = ','.join([_.lower() for _ in vrf['flags']]) + print(f'{name:16} {state:7} {mac:17} {info}') +else: + print(" ".join([vrf['ifname'] for vrf in vyos.vrf.list_vrfs()])) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py new file mode 100755 index 000000000..9896c7c85 --- /dev/null +++ b/src/conf_mode/vrf.py @@ -0,0 +1,186 @@ +#!/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 copy import deepcopy +from vyos.config import Config +from vyos import ConfigError +from vyos import vrf + + +# https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt + + +def sysctl(name, value): + os.system('sysctl -wq {}={}'.format(name, value)) + +def interfaces_with_vrf (match, effective): + matched = [] + config = Config() + section = config.get_config_dict('interfaces', effective) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'vrf' in interface: + v = interface.get('vrf', '') + if v == match: + matched.append(name) + return matched + +def get_config(): + command = { + 'bind':{}, + 'vrf':[], + 'int': {}, # per vrf name list of interfaces which will have it + } + + config = Config() + + old = {} + new = {} + + if config.exists_effective('vrf'): + old = deepcopy(config.get_config_dict('vrf', True)) + + if config.exists('vrf'): + new = deepcopy(config.get_config_dict('vrf', False)) + + integer = lambda _: '1' if _ else '0' + command['bind']['ipv4'] = integer('ipv4' not in new.get('disable-bind-to-all', {})) + command['bind']['ipv6'] = integer('ipv6' not in new.get('disable-bind-to-all', {})) + + old_names = old.get('name', []) + new_names = new.get('name', []) + all_names = list(set(old_names) | set(new_names)) + del_names = list(set(old_names).difference(new_names)) + mod_names = list(set(old_names).intersection(new_names)) + add_names = list(set(new_names).difference(old_names)) + + for name in all_names: + v = { + 'name': name, + 'action': 'miss', + 'table': -1, + 'check': -1, + } + + if name in new_names: + v['table'] = new.get('name', {}).get(name, {}).get('table', -1) + v['check'] = old.get('name', {}).get(name, {}).get('table', -1) + + if name in add_names: + v['action'] = 'add' + elif name in del_names: + v['action'] = 'delete' + elif name in mod_names: + if v['table'] != -1: + if v['check'] == -1: + v['action'] = 'add' + else: + v['action'] = 'modify' + + command['vrf'].append(v) + + for v in vrf.list_vrfs(): + name = v['ifname'] + command['int'][name] = interfaces_with_vrf(name,False) + + return command + + +def verify(command): + for v in command['vrf']: + action = v['action'] + name = v['name'] + if action == 'modify' and v['table'] != v['check']: + raise ConfigError(f'set vrf name {name}: modification of vrf table is not supported yet') + if action == 'delete' and name in command['int']: + interface = ', '.join(command['int'][name]) + if interface: + raise ConfigError(f'delete vrf name {name}: can not delete vrf as it is used on {interface}') + + return command + + +def generate(command): + return command + + +def apply(command): + # set the default VRF global behaviour + sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) + sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) + + errors = [] + for v in command['vrf']: + name = v['name'] + action = v['action'] + table = v['table'] + + errors.append(f'could not {action} vrf {name}') + + if action == 'miss': + continue + + if action == 'delete': + if os.system(f'sudo ip link delete dev {name}'): + continue + errors.pop() + continue + + if action == 'modify': + # > uname -a + # Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux + # > ip link add my-vrf type vrf table 100 + # > ip link set my-vrf type vrf table 200 + # RTNETLINK answers: Operation not supported + # so require to remove vrf and change all existing the interfaces + + if os.system(f'sudo ip link delete dev {name}'): + continue + action = 'add' + + if action == 'add': + commands = [ + f'sudo ip link add {name} type vrf table {table}', + f'sudo ip link set dev {name} up', + f'sudo ip -4 rule add oif {name} lookup {table}', + f'sudo ip -4 rule add iif {name} lookup {table}', + f'sudo ip -6 rule add oif {name} lookup {table}', + f'sudo ip -6 rule add iif {name} lookup {table}', + ] + + for command in commands: + if os.system(command): + errors[-1] += ' ('+command+')' + continue + errors.pop() + + if errors: + raise ConfigError(', '.join(errors)) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/validators/interface-name b/src/validators/interface-name new file mode 100755 index 000000000..49a833f39 --- /dev/null +++ b/src/validators/interface-name @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import sys +import re + +if len(sys.argv) == 2: + # https://unix.stackexchange.com/questions/451368/allowed-chars-in-linux-network-interface-names + pattern = "^([^/\s]{1,16}$)$" + if re.match(pattern, sys.argv[1]): + sys.exit(0) + else: + sys.exit(1) + -- cgit v1.2.3 From 45904fcd80beaed93e3737d0f78b7f081234fc07 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:10:23 +0100 Subject: vrf: T31: use embedded regex on 'vrf name' instead of python script --- interface-definitions/vrf.xml.in | 8 ++++---- src/validators/interface-name | 29 ----------------------------- 2 files changed, 4 insertions(+), 33 deletions(-) delete mode 100755 src/validators/interface-name diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index e270e8b90..1fb878f44 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -22,14 +22,14 @@ - Virtual Routing and Forwarding + VRF instance name - + [^/\s]{1,16}$ - VRF name not allowed or to long + VRF instance name must be 16 characters or less name - the vrf name must not contain '/' and be 16 characters or less + Instance name diff --git a/src/validators/interface-name b/src/validators/interface-name deleted file mode 100755 index 49a833f39..000000000 --- a/src/validators/interface-name +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# - -import sys -import re - -if len(sys.argv) == 2: - # https://unix.stackexchange.com/questions/451368/allowed-chars-in-linux-network-interface-names - pattern = "^([^/\s]{1,16}$)$" - if re.match(pattern, sys.argv[1]): - sys.exit(0) - else: - sys.exit(1) - -- cgit v1.2.3 From c3bba9873dd6412d14b073d06c04278de25fed67 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:10:30 +0100 Subject: vrf: T31: reuse interface-description.xml.i for instance description --- interface-definitions/vrf.xml.in | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 1fb878f44..717a283aa 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -46,11 +46,7 @@ - - - Description of the VRF role - - + #include -- cgit v1.2.3 From 8fa0754f3aa8e4df18b0448e970abc01f348366f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:10:32 +0100 Subject: vrf: T31: improve help for routing table --- interface-definitions/vrf.xml.in | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 717a283aa..a6c67e9dd 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -2,7 +2,7 @@ - VRF configuration + Virtual Routing and Forwarding 210 @@ -35,14 +35,14 @@ - The routing table to associate to this VRF + Routing table associated with this instance Invalid kernel table number - number - the VRF must be a number between 1 and 2^31-1 + 1-2147483647 + Routing table ID -- cgit v1.2.3 From b34ec45126f9ef9b285e46bbf3f917bd926e7f4b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:10:15 +0100 Subject: xml: include: description: adjust help message --- interface-definitions/include/interface-description.xml.i | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface-definitions/include/interface-description.xml.i b/interface-definitions/include/interface-description.xml.i index 7a7a37871..961533e26 100644 --- a/interface-definitions/include/interface-description.xml.i +++ b/interface-definitions/include/interface-description.xml.i @@ -1,9 +1,9 @@ - Interface description + Interface specific description .{1,256}$ - Interface description too long (limit 256 characters) + Description too long (limit 256 characters) -- cgit v1.2.3 From d93efd69083c153c1586dd08d65f1d10769820b7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:10:49 +0100 Subject: vrf: T31: make 'show vrf' command behave like other 'show interface commands' - remove the additional depth for querying discrete VRF names - retrieve available VRF names from via from CLI rather then invoking an external script --- op-mode-definitions/show-vrf.xml | 22 ++++++++++------------ src/completion/list_vrf.py | 27 --------------------------- src/op_mode/show_vrf.py | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 39 deletions(-) delete mode 100755 src/completion/list_vrf.py create mode 100755 src/op_mode/show_vrf.py diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml index fb2fddd49..360153d8e 100644 --- a/op-mode-definitions/show-vrf.xml +++ b/op-mode-definitions/show-vrf.xml @@ -6,19 +6,17 @@ Show VRF information - ${vyos_completion_dir}/list_vrf.py -e - - - - Show VRF information for an interface - - - - - ${vyos_completion_dir}/list_vrf.py -e "$4" - - + ${vyos_op_scripts_dir}/show_vrf.py -e + + + Show information on specific VRF instance + + vrf name + + + ${vyos_op_scripts_dir}/show_vrf.py -e "$3" + diff --git a/src/completion/list_vrf.py b/src/completion/list_vrf.py deleted file mode 100755 index 210b3c9a4..000000000 --- a/src/completion/list_vrf.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import vyos.vrf - -parser = argparse.ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("-e", "--extensive", action="store_true", - help="provide detailed vrf informatio") -parser.add_argument('interface', metavar='I', type=str, nargs='?', - help='interface to display') - -args = parser.parse_args() - -if args.extensive: - print('{:16} {:7} {:17} {}'.format('interface', 'state', 'mac', 'flags')) - print('{:16} {:7} {:17} {}'.format('---------', '-----', '---', '-----')) - for vrf in vyos.vrf.list_vrfs(): - name = vrf['ifname'] - if args.interface and name != args.interface: - continue - state = vrf['operstate'].lower() - mac = vrf['address'].lower() - info = ','.join([_.lower() for _ in vrf['flags']]) - print(f'{name:16} {state:7} {mac:17} {info}') -else: - print(" ".join([vrf['ifname'] for vrf in vyos.vrf.list_vrfs()])) diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py new file mode 100755 index 000000000..210b3c9a4 --- /dev/null +++ b/src/op_mode/show_vrf.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import argparse +import vyos.vrf + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-e", "--extensive", action="store_true", + help="provide detailed vrf informatio") +parser.add_argument('interface', metavar='I', type=str, nargs='?', + help='interface to display') + +args = parser.parse_args() + +if args.extensive: + print('{:16} {:7} {:17} {}'.format('interface', 'state', 'mac', 'flags')) + print('{:16} {:7} {:17} {}'.format('---------', '-----', '---', '-----')) + for vrf in vyos.vrf.list_vrfs(): + name = vrf['ifname'] + if args.interface and name != args.interface: + continue + state = vrf['operstate'].lower() + mac = vrf['address'].lower() + info = ','.join([_.lower() for _ in vrf['flags']]) + print(f'{name:16} {state:7} {mac:17} {info}') +else: + print(" ".join([vrf['ifname'] for vrf in vyos.vrf.list_vrfs()])) -- cgit v1.2.3 From c26fb9bc15bd625b11ae1674bddf8daf168b0f76 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:12:18 +0100 Subject: vrf: T31: no need to use sudo calls in vrf.py All configuration mode scripts are already run with sudo. --- src/conf_mode/vrf.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 9896c7c85..4dddc7de9 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -152,18 +152,18 @@ def apply(command): # RTNETLINK answers: Operation not supported # so require to remove vrf and change all existing the interfaces - if os.system(f'sudo ip link delete dev {name}'): + if os.system(f'ip link delete dev {name}'): continue action = 'add' if action == 'add': commands = [ - f'sudo ip link add {name} type vrf table {table}', - f'sudo ip link set dev {name} up', - f'sudo ip -4 rule add oif {name} lookup {table}', - f'sudo ip -4 rule add iif {name} lookup {table}', - f'sudo ip -6 rule add oif {name} lookup {table}', - f'sudo ip -6 rule add iif {name} lookup {table}', + f'ip link add {name} type vrf table {table}', + f'ip link set dev {name} up', + f'ip -4 rule add oif {name} lookup {table}', + f'ip -4 rule add iif {name} lookup {table}', + f'ip -6 rule add oif {name} lookup {table}', + f'ip -6 rule add iif {name} lookup {table}', ] for command in commands: -- cgit v1.2.3 From e2a46280601aee4846776ebad08ed367aa115460 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:32:58 +0100 Subject: vrf: T31: reduce script complexity Keep it simple and stupid :) --- src/conf_mode/vrf.py | 217 ++++++++++++++++++++++----------------------------- 1 file changed, 95 insertions(+), 122 deletions(-) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 4dddc7de9..75b19da23 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,21 +18,33 @@ import os from sys import exit from copy import deepcopy +from subprocess import check_call, CalledProcessError from vyos.config import Config +from vyos.configdict import list_diff from vyos import ConfigError from vyos import vrf +default_config_data = { + 'vrf_add': [], + 'vrf_existing': [], + 'vrf_remove': [] +} -# https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt - +def _cmd(command): + """ + Run any arbitrary command on the system + """ + try: + check_call(command.split()) + except CalledProcessError as e: + pass + raise ConfigError(f'Error operationg on VRF: {e}') -def sysctl(name, value): - os.system('sysctl -wq {}={}'.format(name, value)) -def interfaces_with_vrf (match, effective): +def interfaces_with_vrf(match): matched = [] config = Config() - section = config.get_config_dict('interfaces', effective) + section = config.get_config_dict('interfaces') for type in section: interfaces = section[type] for name in interfaces: @@ -43,137 +55,98 @@ def interfaces_with_vrf (match, effective): matched.append(name) return matched -def get_config(): - command = { - 'bind':{}, - 'vrf':[], - 'int': {}, # per vrf name list of interfaces which will have it - } - - config = Config() - - old = {} - new = {} - if config.exists_effective('vrf'): - old = deepcopy(config.get_config_dict('vrf', True)) - - if config.exists('vrf'): - new = deepcopy(config.get_config_dict('vrf', False)) - - integer = lambda _: '1' if _ else '0' - command['bind']['ipv4'] = integer('ipv4' not in new.get('disable-bind-to-all', {})) - command['bind']['ipv6'] = integer('ipv6' not in new.get('disable-bind-to-all', {})) - - old_names = old.get('name', []) - new_names = new.get('name', []) - all_names = list(set(old_names) | set(new_names)) - del_names = list(set(old_names).difference(new_names)) - mod_names = list(set(old_names).intersection(new_names)) - add_names = list(set(new_names).difference(old_names)) - - for name in all_names: - v = { - 'name': name, - 'action': 'miss', - 'table': -1, - 'check': -1, +def get_config(): + conf = Config() + vrf_config = deepcopy(default_config_data) + + cfg_base = ['vrf'] + if not conf.exists(cfg_base): + # get all currently effetive VRFs and mark them for deletion + vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) + return vrf_config + + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) + act_vrf = conf.list_nodes(cfg_base + ['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(cfg_base + ['name']): + vrf_inst = { + 'description' : '\0', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True - if name in new_names: - v['table'] = new.get('name', {}).get(name, {}).get('table', -1) - v['check'] = old.get('name', {}).get(name, {}).get('table', -1) + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) - if name in add_names: - v['action'] = 'add' - elif name in del_names: - v['action'] = 'delete' - elif name in mod_names: - if v['table'] != -1: - if v['check'] == -1: - v['action'] = 'add' - else: - v['action'] = 'modify' + # find member interfaces of this particulat VRF + vrf_inst['members'] = interfaces_with_vrf(name) - command['vrf'].append(v) + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) - for v in vrf.list_vrfs(): - name = v['ifname'] - command['int'][name] = interfaces_with_vrf(name,False) + return vrf_config - return command +def verify(vrf_config): + # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_add']: + if vrf['table_mod']: + raise ConfigError('VRF routing table id modification is not possible') -def verify(command): - for v in command['vrf']: - action = v['action'] - name = v['name'] - if action == 'modify' and v['table'] != v['check']: - raise ConfigError(f'set vrf name {name}: modification of vrf table is not supported yet') - if action == 'delete' and name in command['int']: - interface = ', '.join(command['int'][name]) - if interface: - raise ConfigError(f'delete vrf name {name}: can not delete vrf as it is used on {interface}') + # add test to see if routing table already exists or not? - return command + return None -def generate(command): - return command +def generate(vrf_config): + return None +def apply(vrf_config): + # https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt -def apply(command): # set the default VRF global behaviour - sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) - sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) - - errors = [] - for v in command['vrf']: - name = v['name'] - action = v['action'] - table = v['table'] - - errors.append(f'could not {action} vrf {name}') - - if action == 'miss': - continue - - if action == 'delete': - if os.system(f'sudo ip link delete dev {name}'): - continue - errors.pop() - continue - - if action == 'modify': - # > uname -a - # Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux - # > ip link add my-vrf type vrf table 100 - # > ip link set my-vrf type vrf table 200 - # RTNETLINK answers: Operation not supported - # so require to remove vrf and change all existing the interfaces - - if os.system(f'ip link delete dev {name}'): - continue - action = 'add' - - if action == 'add': - commands = [ - f'ip link add {name} type vrf table {table}', - f'ip link set dev {name} up', - f'ip -4 rule add oif {name} lookup {table}', - f'ip -4 rule add iif {name} lookup {table}', - f'ip -6 rule add oif {name} lookup {table}', - f'ip -6 rule add iif {name} lookup {table}', - ] - - for command in commands: - if os.system(command): - errors[-1] += ' ('+command+')' - continue - errors.pop() - - if errors: - raise ConfigError(', '.join(errors)) + #sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) + #sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) + + for vrf_name in vrf_config['vrf_remove']: + if os.path.isdir(f'/sys/class/net/{vrf_name}'): + _cmd(f'ip link delete dev {vrf_name}') + + for vrf in vrf_config['vrf_add']: + name = vrf['name'] + table = vrf['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + _cmd(f'ip link add {name} type vrf table {table}') + _cmd(f'ip link set dev {name} up') + _cmd(f'ip -4 rule add oif {name} lookup {table}') + _cmd(f'ip -4 rule add iif {name} lookup {table}') + _cmd(f'ip -6 rule add oif {name} lookup {table}') + _cmd(f'ip -6 rule add iif {name} lookup {table}') + + # set VRF description for e.g. SNMP monitoring + with open(f'/sys/class/net/{name}/ifalias', 'w') as f: + f.write(vrf['description']) + + return None if __name__ == '__main__': try: -- cgit v1.2.3 From bb705d1a90cad14f07b5e61c44365868283feee9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:38:01 +0100 Subject: vrf: T31: remove superfluous vyos.vrf library functions vyos.vrf.list_vrfs() was only used in one function thus building a library is no longer needed. If it is needed in the future it should be placed into a library again. --- python/vyos/vrf.py | 23 ----------------------- src/conf_mode/vrf.py | 1 - src/op_mode/show_vrf.py | 27 ++++++++++++++++++++++++--- 3 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 python/vyos/vrf.py diff --git a/python/vyos/vrf.py b/python/vyos/vrf.py deleted file mode 100644 index 99e4cb7d1..000000000 --- a/python/vyos/vrf.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2020 VyOS maintainers and contributors -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . - -import json -import subprocess - - -def list_vrfs(): - command = 'ip -j -br link show type vrf' - answer = json.loads(subprocess.check_output(command.split()).decode()) - return [_ for _ in answer if _] diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 75b19da23..c2bbc72b3 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -22,7 +22,6 @@ from subprocess import check_call, CalledProcessError from vyos.config import Config from vyos.configdict import list_diff from vyos import ConfigError -from vyos import vrf default_config_data = { 'vrf_add': [], diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py index 210b3c9a4..ec894d572 100755 --- a/src/op_mode/show_vrf.py +++ b/src/op_mode/show_vrf.py @@ -1,7 +1,28 @@ #!/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 argparse -import vyos.vrf + +from subprocess import check_output +from json import loads + +def list_vrfs(): + command = 'ip -j -br link show type vrf' + answer = loads(check_output(command.split()).decode()) + return [_ for _ in answer if _] parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() @@ -15,7 +36,7 @@ args = parser.parse_args() if args.extensive: print('{:16} {:7} {:17} {}'.format('interface', 'state', 'mac', 'flags')) print('{:16} {:7} {:17} {}'.format('---------', '-----', '---', '-----')) - for vrf in vyos.vrf.list_vrfs(): + for vrf in list_vrfs(): name = vrf['ifname'] if args.interface and name != args.interface: continue @@ -24,4 +45,4 @@ if args.extensive: info = ','.join([_.lower() for _ in vrf['flags']]) print(f'{name:16} {state:7} {mac:17} {info}') else: - print(" ".join([vrf['ifname'] for vrf in vyos.vrf.list_vrfs()])) + print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) -- cgit v1.2.3 From 5bf9dfd17096af6e7cf06e8e20eb16e8e55b9177 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:51:37 +0100 Subject: vrf: T31: support add/remove of interfaces from vrf --- interface-definitions/include/interface-vrf.xml.i | 8 +-- python/vyos/ifconfig.py | 28 +++++++- src/conf_mode/vrf.py | 81 +++++++++++++++-------- 3 files changed, 83 insertions(+), 34 deletions(-) diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i index 7e880e6ee..355e7f0f3 100644 --- a/interface-definitions/include/interface-vrf.xml.i +++ b/interface-definitions/include/interface-vrf.xml.i @@ -1,12 +1,12 @@ VRF instance name + + text + VRF instance name + vrf name - - - - VRF name not allowed or to long diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index beeafa420..430b3c4e4 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -202,6 +202,12 @@ class Interface(Control): 'validate': assert_mac, 'shellcmd': 'ip link set dev {ifname} address {value}', }, + 'add_vrf': { + 'shellcmd': 'ip link set dev {ifname} master {value}', + }, + 'del_vrf': { + 'shellcmd': 'ip link set dev {ifname} nomaster {value}', + }, } _sysfs_get = { @@ -345,7 +351,7 @@ class Interface(Control): self.del_addr(addr) # --------------------------------------------------------------------- - # A code refactoring is required as this type check is present as + # A code refactoring is required as this type check is present as # Interface implement behaviour for one of it's sub-class. # It is required as the current pattern for vlan is: @@ -405,6 +411,26 @@ class Interface(Control): """ self.set_interface('mac', mac) + def add_vrf(self, vrf): + """ + Add interface to given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').add_vrf('foo') + """ + self.set_interface('add_vrf', vrf) + + def del_vrf(self, vrf): + """ + Remove interface from given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').del_vrf('foo') + """ + self.set_interface('del_vrf', vrf) + def set_arp_cache_tmo(self, tmo): """ Set ARP cache timeout value in seconds. Internal Kernel representation diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index c2bbc72b3..ad2b72a5b 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -24,6 +24,7 @@ from vyos.configdict import list_diff from vyos import ConfigError default_config_data = { + 'deleted': False, 'vrf_add': [], 'vrf_existing': [], 'vrf_remove': [] @@ -63,49 +64,71 @@ def get_config(): if not conf.exists(cfg_base): # get all currently effetive VRFs and mark them for deletion vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) - return vrf_config - - # Determine vrf interfaces (currently effective) - to determine which - # vrf interface is no longer present and needs to be removed - eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) - act_vrf = conf.list_nodes(cfg_base + ['name']) - vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) - - # read in individual VRF definition and build up - # configuration - for name in conf.list_nodes(cfg_base + ['name']): + else: + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) + act_vrf = conf.list_nodes(cfg_base + ['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(cfg_base + ['name']): + vrf_inst = { + 'description' : '\0', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False + } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True + + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) + + # find member interfaces of this particulat VRF + vrf_inst['members'] = interfaces_with_vrf(name) + + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) + + # check VRFs which need to be removed as they are not allowed to have + # interfaces attached + tmp = [] + for name in vrf_config['vrf_remove']: vrf_inst = { - 'description' : '\0', 'members': [], 'name' : name, - 'table' : '', - 'table_mod': False } - conf.set_level(cfg_base + ['name', name]) - - if conf.exists(['table']): - # VRF table can't be changed on demand, thus we need to read in the - # current and the effective routing table number - act_table = conf.return_value(['table']) - eff_table = conf.return_effective_value(['table']) - vrf_inst['table'] = act_table - if eff_table and eff_table != act_table: - vrf_inst['table_mod'] = True - - if conf.exists(['description']): - vrf_inst['description'] = conf.return_value(['description']) # find member interfaces of this particulat VRF vrf_inst['members'] = interfaces_with_vrf(name) - # append individual VRF configuration to global configuration list - vrf_config['vrf_add'].append(vrf_inst) + # append individual VRF configuration to temporary configuration list + tmp.append(vrf_inst) + # replace values in vrf_remove with list of dictionaries + # as we need it in verify() - we can't delete a VRF with members attached + vrf_config['vrf_remove'] = tmp return vrf_config def verify(vrf_config): # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_remove']: + if len(vrf['members']) > 0: + raise ConfigError('VRF "{}" can not be deleted as it has active members'.format(vrf['name'])) + + # routing table id can't be changed - OS restriction for vrf in vrf_config['vrf_add']: if vrf['table_mod']: raise ConfigError('VRF routing table id modification is not possible') -- cgit v1.2.3 From 93f7ae7f1ed1e218ef64d2582d11ac0ed769a438 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 19:53:58 +0100 Subject: vrf: T31: rename 'vrf disable-bind-to-all ipv4' to 'vrf bind-to-all' By default the scope of the port bindings for unbound sockets is limited to the default VRF. That is, it will not be matched by packets arriving on interfaces enslaved to an l3mdev and processes may bind to the same port if they bind to an l3mdev. TCP & UDP services running in the default VRF context (ie., not bound to any VRF device) can work across all VRF domains by enabling the 'vrf bind-to-all' option. --- interface-definitions/vrf.xml.in | 15 ++++----------- src/conf_mode/vrf.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index a6c67e9dd..f1895598e 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -7,19 +7,12 @@ 210 - + - Disable services running on the default VRF from other VRF (ssh, bgp, ...) + Enable binding services to all VRFs + - - - - - Enable binding across all VRF domains for IPv4 - - - - + VRF instance name diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index ad2b72a5b..e31285dde 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -24,6 +24,7 @@ from vyos.configdict import list_diff from vyos import ConfigError default_config_data = { + 'bind_to_all': 0, 'deleted': False, 'vrf_add': [], 'vrf_existing': [], @@ -40,7 +41,6 @@ def _cmd(command): pass raise ConfigError(f'Error operationg on VRF: {e}') - def interfaces_with_vrf(match): matched = [] config = Config() @@ -55,7 +55,6 @@ def interfaces_with_vrf(match): matched.append(name) return matched - def get_config(): conf = Config() vrf_config = deepcopy(default_config_data) @@ -65,6 +64,11 @@ def get_config(): # get all currently effetive VRFs and mark them for deletion vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) else: + + # Should services be allowed to bind to all VRFs? + if conf.exists(['bind-to-all']): + vrf_config['bind_to_all'] = 1 + # Determine vrf interfaces (currently effective) - to determine which # vrf interface is no longer present and needs to be removed eff_vrf = conf.list_effective_nodes(cfg_base + ['name']) @@ -121,7 +125,6 @@ def get_config(): vrf_config['vrf_remove'] = tmp return vrf_config - def verify(vrf_config): # ensure VRF is not assigned to any interface for vrf in vrf_config['vrf_remove']: @@ -137,7 +140,6 @@ def verify(vrf_config): return None - def generate(vrf_config): return None @@ -145,8 +147,9 @@ def apply(vrf_config): # https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt # set the default VRF global behaviour - #sysctl('net.ipv4.tcp_l3mdev_accept', command['bind']['ipv4']) - #sysctl('net.ipv4.udp_l3mdev_accept', command['bind']['ipv4']) + bind_all = vrf_config['bind_to_all'] + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') for vrf_name in vrf_config['vrf_remove']: if os.path.isdir(f'/sys/class/net/{vrf_name}'): -- cgit v1.2.3 From 39dfcfb5077a4024c454f9cd127fc1f65529f591 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 20:55:04 +0100 Subject: vrf: T31: create iproute2 table to name mapping reference --- src/conf_mode/vrf.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index e31285dde..91d8f8432 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import os +import jinja2 from sys import exit from copy import deepcopy @@ -23,6 +24,21 @@ from vyos.config import Config from vyos.configdict import list_diff from vyos import ConfigError +config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vrf.py ### +# +# Routing table ID to name mapping reference + +# id vrf name comment +{% for vrf in vrf_add -%} +{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} +{% endfor -%} + +""" + default_config_data = { 'bind_to_all': 0, 'deleted': False, @@ -141,6 +157,11 @@ def verify(vrf_config): return None def generate(vrf_config): + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(vrf_config) + with open(config_file, 'w') as f: + f.write(config_text) + return None def apply(vrf_config): -- cgit v1.2.3 From d8808f46955fdaed1d2e86ec10b078db66e4639e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:28:03 +0100 Subject: vrf: T31: prior to the v4.8 kernel iif and oif rules are needed .. we run on 4.19 thus this is no longer needed. --- src/conf_mode/vrf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 91d8f8432..8036703f1 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -181,12 +181,11 @@ def apply(vrf_config): table = vrf['table'] if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table _cmd(f'ip link add {name} type vrf table {table}') + # Start VRf _cmd(f'ip link set dev {name} up') - _cmd(f'ip -4 rule add oif {name} lookup {table}') - _cmd(f'ip -4 rule add iif {name} lookup {table}') - _cmd(f'ip -6 rule add oif {name} lookup {table}') - _cmd(f'ip -6 rule add iif {name} lookup {table}') # set VRF description for e.g. SNMP monitoring with open(f'/sys/class/net/{name}/ifalias', 'w') as f: -- cgit v1.2.3 From 5b69a581831ba431d6b56077ad6340925a73a371 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:29:31 +0100 Subject: vrf: T31: adding unreachable routes to the routing tables --- src/conf_mode/vrf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 8036703f1..242fc7ccb 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -186,6 +186,11 @@ def apply(vrf_config): _cmd(f'ip link add {name} type vrf table {table}') # Start VRf _cmd(f'ip link set dev {name} up') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') # set VRF description for e.g. SNMP monitoring with open(f'/sys/class/net/{name}/ifalias', 'w') as f: -- cgit v1.2.3 From 3d231292c8beaa00d40f922c01ca4191b2b89da1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:30:13 +0100 Subject: vrf: T31: reorder routing table lookups Linux routing uses rules to find tables - routing targets are then looked up in those tables. If the lookup got a matching route, the process ends. TL;DR; first table with a matching entry wins! You can see your routing table lookup rules using "ip rule", sadly the local lookup is hit before any VRF lookup. Pinging an addresses from the VRF will usually find a hit in the local table, and never reach the VRF routing table - this is usually not what you want. Thus we will re-arrange the tables and move the local lookup furhter down once VRFs are enabled. --- src/conf_mode/vrf.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 242fc7ccb..a39366126 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -196,6 +196,34 @@ def apply(vrf_config): with open(f'/sys/class/net/{name}/ifalias', 'w') as f: f.write(vrf['description']) + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup furhter down once VRFs + # are enabled. + + # set "normal" non VRF table lookups + add_pref = '0' + del_pref = '32765' + + # Lookup table is adjusted if we are in VRF mode + if vrf_config['vrf_add']: + add_pref = '32765' + del_pref = '0' + + # Configure table lookups + _cmd(f'ip -4 rule add pref {add_pref} table local') + _cmd(f'ip -4 rule del pref {del_pref}') + _cmd(f'ip -6 rule add pref {add_pref} table local') + _cmd(f'ip -6 rule del pref {del_pref}') + return None if __name__ == '__main__': -- cgit v1.2.3 From 94527843ecbf92d48e367b6b5e255da4e958d0be Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:42:45 +0100 Subject: templates: T2099: make op-mode path completion helper working --- scripts/build-command-op-templates | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index 0383c8982..689d19ece 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -111,7 +111,7 @@ def get_properties(p): for i in lists: comp_exprs.append("echo \"{0}\"".format(i.text)) for i in paths: - comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) + comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\"".format(i.text)) for i in scripts: comp_exprs.append("{0}".format(i.text)) comp_help = " && ".join(comp_exprs) -- cgit v1.2.3 From 153d1535d954f59cc48ed26f6cc552703b8cbd73 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Mar 2020 21:53:48 +0100 Subject: vrf: T31: enable vrf support for dummy interface --- interface-definitions/interfaces-dummy.xml.in | 1 + src/conf_mode/interfaces-dummy.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 39809a610..5229e602a 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -19,6 +19,7 @@ #include #include #include + #include diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e79e6222d..10cae5d7d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -18,6 +18,7 @@ import os from copy import deepcopy from sys import exit +from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff @@ -30,7 +31,8 @@ default_config_data = { 'deleted': False, 'description': '', 'disable': False, - 'intf': '' + 'intf': '', + 'vrf': '' } def get_config(): @@ -69,9 +71,17 @@ def get_config(): act_addr = conf.return_values('address') dummy['address_remove'] = list_diff(eff_addr, act_addr) + # retrieve VRF instance + if conf.exists('vrf'): + dummy['vrf'] = conf.return_value('vrf') + return dummy def verify(dummy): + vrf_name = dummy['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + return None def generate(dummy): @@ -95,6 +105,12 @@ def apply(dummy): for addr in dummy['address']: d.add_addr(addr) + # assign to VRF + if dummy['vrf']: + d.add_vrf(dummy['vrf']) + else: + d.del_vrf(dummy['vrf']) + # disable interface on demand if dummy['disable']: d.set_state('down') -- cgit v1.2.3