From e9c9d1a1e4f1bf876892b6e58f7288d36180889f Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Tue, 30 May 2023 09:04:34 +0000 Subject: T5241: Support netns for veth and dummy interfaces Add netns configuration for dummy and virtual-ethernet interfaces Change Interface class to get/set data to netns --- .../interfaces-virtual-ethernet.xml.in | 1 + python/vyos/ifconfig/control.py | 14 +++- python/vyos/ifconfig/interface.py | 83 ++++++++++++++++------ python/vyos/util.py | 8 ++- python/vyos/validate.py | 68 ++++++------------ src/conf_mode/interfaces-dummy.py | 5 +- 6 files changed, 107 insertions(+), 72 deletions(-) diff --git a/interface-definitions/interfaces-virtual-ethernet.xml.in b/interface-definitions/interfaces-virtual-ethernet.xml.in index 1daa764d4..5f205f354 100644 --- a/interface-definitions/interfaces-virtual-ethernet.xml.in +++ b/interface-definitions/interfaces-virtual-ethernet.xml.in @@ -21,6 +21,7 @@ #include #include #include + #include #include #include #include diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index 7a6b36e7c..915c1d2f9 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors +# Copyright 2019-2023 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 @@ -49,6 +49,18 @@ class Control(Section): return popen(command, self.debug) def _cmd(self, command): + import re + if 'netns' in self.config: + # This command must be executed from default netns 'ip link set dev X netns X' + # exclude set netns cmd from netns to avoid: + # failed to run command: ip netns exec ns01 ip link set dev veth20 netns ns01 + pattern = r'ip link set dev (\S+) netns (\S+)' + matches = re.search(pattern, command) + if matches and matches.group(2) == self.config['netns']: + # Command already includes netns and matches desired namespace: + command = command + else: + command = f'ip netns exec {self.config["netns"]} {command}' return cmd(command, self.debug) def _get_command(self, config, name): diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 7754488c4..85fa90653 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 VyOS maintainers and contributors +# Copyright 2019-2023 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 @@ -35,6 +35,7 @@ from vyos.template import render from vyos.util import mac2eui64 from vyos.util import dict_search from vyos.util import read_file +from vyos.util import run from vyos.util import get_interface_config from vyos.util import get_interface_namespace from vyos.util import is_systemd_service_active @@ -59,6 +60,15 @@ from netaddr import mac_unix_expanded link_local_prefix = 'fe80::/64' + +def _interface_exists_in_netns(interface_name, netns): + from vyos.util import rc_cmd + rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') + if rc == 0: + return True + return False + + class Interface(Control): # This is the class which will be used to create # self.operational, it allows subclasses, such as @@ -137,9 +147,6 @@ class Interface(Control): 'validate': assert_mtu, 'shellcmd': 'ip link set dev {ifname} mtu {value}', }, - 'netns': { - 'shellcmd': 'ip link set dev {ifname} netns {value}', - }, 'vrf': { 'convert': lambda v: f'master {v}' if v else 'nomaster', 'shellcmd': 'ip link set dev {ifname} {value}', @@ -270,8 +277,11 @@ class Interface(Control): } @classmethod - def exists(cls, ifname): - return os.path.exists(f'/sys/class/net/{ifname}') + def exists(cls, ifname, netns=None) -> bool: + cmd = f'ip link show dev {ifname}' + if netns: + cmd = f'ip netns exec {netns} {cmd}' + return run(cmd) == 0 @classmethod def get_config(cls): @@ -339,7 +349,12 @@ class Interface(Control): self.vrrp = VRRP(ifname) def _create(self): + # Do not create interface that already exist or exists in netns + netns = self.config.get('netns', None) + if self.exists(f'{self.ifname}', netns=netns): + return cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) + if 'netns' in self.config: cmd = f'ip netns exec {self.config["netns"]} {cmd}' self._cmd(cmd) def remove(self): @@ -374,6 +389,9 @@ class Interface(Control): # after interface removal no other commands should be allowed # to be called and instead should raise an Exception: cmd = 'ip link del dev {ifname}'.format(**self.config) + # for delete we can't get data from self.config{'netns'} + netns = get_interface_namespace(self.config['ifname']) + if netns: cmd = f'ip netns exec {netns} {cmd}' return self._cmd(cmd) def _set_vrf_ct_zone(self, vrf): @@ -381,6 +399,10 @@ class Interface(Control): Add/Remove rules in nftables to associate traffic in VRF to an individual conntack zone """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + if vrf: # Get routing table ID for VRF vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get( @@ -533,13 +555,9 @@ class Interface(Control): if not os.path.exists(f'/run/netns/{netns}'): return None - # As a PoC we only allow 'dummy' interfaces - if 'dum' not in self.ifname: - return None - # Check if interface realy exists in namespace - if get_interface_namespace(self.ifname) != None: - self._cmd(f'ip netns exec {get_interface_namespace(self.ifname)} ip link del dev {self.ifname}') + if _interface_exists_in_netns(self.ifname, netns): + self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}') return def set_netns(self, netns): @@ -551,7 +569,8 @@ class Interface(Control): >>> Interface('dum0').set_netns('foo') """ - self.set_interface('netns', netns) + cmd = f'ip link set dev {self.ifname} netns {netns}' + self._cmd(cmd) def set_vrf(self, vrf): """ @@ -586,6 +605,10 @@ class Interface(Control): return self.set_interface('arp_cache_tmo', tmo) def _cleanup_mss_rules(self, table, ifname): + # Don't allow for netns yet + if 'netns' in self.config: + return None + commands = [] results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n") for line in results: @@ -605,6 +628,10 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_tcp_ipv4_mss(1340) """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + self._cleanup_mss_rules('raw', self.ifname) nft_prefix = 'nft add rule raw VYOS_TCP_MSS' base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn' @@ -739,6 +766,11 @@ class Interface(Control): As per RFC3074. """ + + # Don't allow for netns yet + if 'netns' in self.config: + return None + if value == 'strict': value = 1 elif value == 'loose': @@ -1101,8 +1133,9 @@ class Interface(Control): self.set_dhcp(True) elif addr == 'dhcpv6': self.set_dhcpv6(True) - elif not is_intf_addr_assigned(self.ifname, addr): - tmp = f'ip addr add {addr} dev {self.ifname}' + elif not is_intf_addr_assigned(self.ifname, addr, self.config.get('netns')): + exec_within_netns = f'ip netns exec {self.config.get("netns")}' if self.config.get('netns') else '' + tmp = f'{exec_within_netns} ip addr add {addr} dev {self.ifname}' # Add broadcast address for IPv4 if is_ipv4(addr): tmp += ' brd +' @@ -1147,8 +1180,9 @@ class Interface(Control): self.set_dhcp(False) elif addr == 'dhcpv6': self.set_dhcpv6(False) - elif is_intf_addr_assigned(self.ifname, addr): - self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') + elif is_intf_addr_assigned(self.ifname, addr, netns=self.config.get('netns')): + exec_within_netns = f'ip netns exec {self.config.get("netns")}' if self.config.get('netns') else '' + self._cmd(f'{exec_within_netns} ip addr del "{addr}" dev "{self.ifname}"') else: return False @@ -1168,8 +1202,12 @@ class Interface(Control): self.set_dhcp(False) self.set_dhcpv6(False) + _netns = get_interface_namespace(self.config['ifname']) + netns_exec = f'ip netns exec {_netns} ' if _netns else '' + cmd = netns_exec + cmd += f'ip addr flush dev {self.ifname}' # flush all addresses - self._cmd(f'ip addr flush dev "{self.ifname}"') + self._cmd(cmd) def add_to_bridge(self, bridge_dict): """ @@ -1308,6 +1346,11 @@ class Interface(Control): # - https://man7.org/linux/man-pages/man8/tc-mirred.8.html # Depening if we are the source or the target interface of the port # mirror we need to setup some variables. + + # Don't allow for netns yet + if 'netns' in self.config: + return None + source_if = self._config['ifname'] mirror_config = None @@ -1412,8 +1455,8 @@ class Interface(Control): # Since the interface is pushed onto a separate logical stack # Configure NETNS if dict_search('netns', config) != None: - self.set_netns(config.get('netns', '')) - return + if not _interface_exists_in_netns(self.ifname, self.config['netns']): + self.set_netns(config.get('netns', '')) else: self.del_netns(config.get('netns', '')) diff --git a/python/vyos/util.py b/python/vyos/util.py index 61ce59324..d83287fd2 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -906,14 +906,16 @@ def get_bridge_fdb(interface): tmp = loads(cmd(f'bridge -j fdb show dev {interface}')) return tmp -def get_interface_config(interface): +def get_interface_config(interface, netns=None): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. """ - if not os.path.exists(f'/sys/class/net/{interface}'): + from vyos.util import run + netns_exec = f'ip netns exec {netns}' if netns else '' + if run(f'{netns_exec} test -e /sys/class/net/{interface}') != 0: return None from json import loads - tmp = loads(cmd(f'ip -d -j link show {interface}'))[0] + tmp = loads(cmd(f'{netns_exec} ip -d -j link show {interface}'))[0] return tmp def get_interface_address(interface): diff --git a/python/vyos/validate.py b/python/vyos/validate.py index d18785aaf..d3e6e9087 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -43,57 +43,31 @@ def _are_same_ip(one, two): s_two = AF_INET if is_ipv4(two) else AF_INET6 return inet_pton(f_one, one) == inet_pton(f_one, two) -def is_intf_addr_assigned(intf, address) -> bool: - """ - Verify if the given IPv4/IPv6 address is assigned to specific interface. +def is_intf_addr_assigned(ifname, addr, netns=None): + """Verify if the given IPv4/IPv6 address is assigned to specific interface. It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR address 192.0.2.1/24. """ - from vyos.template import is_ipv4 - - from netifaces import ifaddresses - from netifaces import AF_INET - from netifaces import AF_INET6 - - # check if the requested address type is configured at all - # { - # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], - # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}], - # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] - # } - try: - addresses = ifaddresses(intf) - except ValueError as e: - print(e) - return False - - # determine IP version (AF_INET or AF_INET6) depending on passed address - addr_type = AF_INET if is_ipv4(address) else AF_INET6 - - # Check every IP address on this interface for a match - netmask = None - if '/' in address: - address, netmask = address.split('/') - for ip in addresses.get(addr_type, []): - # ip can have the interface name in the 'addr' field, we need to remove it - # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} - ip_addr = ip['addr'].split('%')[0] - - if not _are_same_ip(address, ip_addr): - continue - - # we do not have a netmask to compare against, they are the same - if not netmask: - return True - - prefixlen = '' - if is_ipv4(ip_addr): - prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')]) - else: - prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split('/')[0].split(':') if _]) + import json + import jmespath + from vyos.util import rc_cmd + from ipaddress import ip_interface - if str(prefixlen) == netmask: - return True + within_netns = f'ip netns exec {netns}' if netns else '' + rc, out = rc_cmd(f'{within_netns} ip --json address show dev {ifname}') + if rc == 0: + json_out = json.loads(out) + addresses = jmespath.search("[].addr_info[].{family: family, address: local, prefixlen: prefixlen}", json_out) + for address_info in addresses: + family = address_info['family'] + address = address_info['address'] + prefixlen = address_info['prefixlen'] + # Remove the interface name if present in the given address + if '%' in addr: + addr = addr.split('%')[0] + interface = ip_interface(f"{address}/{prefixlen}") + if ip_interface(addr) == interface or address == addr: + return True return False diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e771581e1..a02ddeb51 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -55,7 +55,10 @@ def generate(dummy): return None def apply(dummy): - d = DummyIf(dummy['ifname']) + if 'netns' in dummy: + d = DummyIf(ifname=dummy['ifname'], netns=dummy['netns']) + else: + d = DummyIf(dummy['ifname']) # Remove dummy interface if 'deleted' in dummy: -- cgit v1.2.3 From 725b6e0454427b8c2b3a507c61d6192ca7522a7e Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 3 Sep 2023 14:33:22 +0200 Subject: netns: T5241: provide is_netns_interface utility helper --- python/vyos/ifconfig/interface.py | 14 +++----------- python/vyos/utils/network.py | 4 ++-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 20ea66953..e24485132 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -38,6 +38,7 @@ from vyos.utils.dict import dict_search from vyos.utils.file import read_file from vyos.utils.network import get_interface_config from vyos.utils.network import get_interface_namespace +from vyos.utils.network import is_netns_interface from vyos.utils.process import is_systemd_service_active from vyos.utils.process import run from vyos.template import is_ipv4 @@ -61,15 +62,6 @@ from netaddr import mac_unix_expanded link_local_prefix = 'fe80::/64' - -def _interface_exists_in_netns(interface_name, netns): - from vyos.util import rc_cmd - rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') - if rc == 0: - return True - return False - - class Interface(Control): # This is the class which will be used to create # self.operational, it allows subclasses, such as @@ -572,7 +564,7 @@ class Interface(Control): return None # Check if interface realy exists in namespace - if _interface_exists_in_netns(self.ifname, netns): + if is_netns_interface(self.ifname, netns): self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}') return @@ -1514,7 +1506,7 @@ class Interface(Control): # Since the interface is pushed onto a separate logical stack # Configure NETNS if dict_search('netns', config) != None: - if not _interface_exists_in_netns(self.ifname, self.config['netns']): + if not is_netns_interface(self.ifname, self.config['netns']): self.set_netns(config.get('netns', '')) else: self.del_netns(config.get('netns', '')) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 2f181d8d9..fa86795eb 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -40,9 +40,9 @@ def interface_exists(interface) -> bool: import os return os.path.exists(f'/sys/class/net/{interface}') -def interface_exists_in_netns(interface_name, netns): +def is_netns_interface(interface, netns): from vyos.utils.process import rc_cmd - rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') + rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface}') if rc == 0: return True return False -- cgit v1.2.3 From 114f8a9a66e49449e09ac3a1721db42626e54212 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 3 Sep 2023 14:34:08 +0200 Subject: netns: T5241: use common interface_exists() helper --- python/vyos/utils/network.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index fa86795eb..8b4385cfb 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -78,8 +78,7 @@ def get_interface_config(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. """ - import os - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): return None from json import loads from vyos.utils.process import cmd @@ -90,8 +89,7 @@ def get_interface_address(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. """ - import os - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): return None from json import loads from vyos.utils.process import cmd @@ -141,8 +139,7 @@ def is_wwan_connected(interface): def get_bridge_fdb(interface): """ Returns the forwarding database entries for a given interface """ - import os - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): return None from json import loads from vyos.utils.process import cmd -- cgit v1.2.3 From 00a9f6a39f556ee6eb8ac669dd86f5095c21b22f Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 3 Sep 2023 14:34:39 +0200 Subject: netns: T5241: improve get_interface_namespace() robustness --- python/vyos/utils/network.py | 78 +++++++++++++++----------------------------- python/vyos/utils/process.py | 2 +- 2 files changed, 28 insertions(+), 52 deletions(-) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 8b4385cfb..4706dbfb7 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -96,23 +96,23 @@ def get_interface_address(interface): tmp = loads(cmd(f'ip --detail --json addr show dev {interface}'))[0] return tmp -def get_interface_namespace(iface): +def get_interface_namespace(interface: str): """ Returns wich netns the interface belongs to """ from json import loads from vyos.utils.process import cmd - # Check if netns exist - tmp = loads(cmd(f'ip --json netns ls')) - if len(tmp) == 0: - return None - for ns in tmp: + # Bail out early if netns does not exist + tmp = cmd(f'ip --json netns ls') + if not tmp: return None + + for ns in loads(tmp): netns = f'{ns["name"]}' # Search interface in each netns data = loads(cmd(f'ip netns exec {netns} ip --json link show')) for tmp in data: - if iface == tmp["ifname"]: + if interface == tmp["ifname"]: return netns def is_wwan_connected(interface): @@ -271,57 +271,33 @@ def is_addr_assigned(ip_address, vrf=None) -> bool: return False -def is_intf_addr_assigned(intf, address) -> bool: +def is_intf_addr_assigned(ifname: str, addr: str, netns: str=None) -> bool: """ Verify if the given IPv4/IPv6 address is assigned to specific interface. It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR address 192.0.2.1/24. """ - from vyos.template import is_ipv4 - - from netifaces import ifaddresses - from netifaces import AF_INET - from netifaces import AF_INET6 - - # check if the requested address type is configured at all - # { - # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], - # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}], - # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] - # } - try: - addresses = ifaddresses(intf) - except ValueError as e: - print(e) - return False - - # determine IP version (AF_INET or AF_INET6) depending on passed address - addr_type = AF_INET if is_ipv4(address) else AF_INET6 - - # Check every IP address on this interface for a match - netmask = None - if '/' in address: - address, netmask = address.split('/') - for ip in addresses.get(addr_type, []): - # ip can have the interface name in the 'addr' field, we need to remove it - # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} - ip_addr = ip['addr'].split('%')[0] - - if not _are_same_ip(address, ip_addr): - continue - - # we do not have a netmask to compare against, they are the same - if not netmask: - return True + import json + import jmespath - prefixlen = '' - if is_ipv4(ip_addr): - prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')]) - else: - prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split('/')[0].split(':') if _]) + from vyos.utils.process import rc_cmd + from ipaddress import ip_interface - if str(prefixlen) == netmask: - return True + netns_cmd = f'ip netns exec {netns}' if netns else '' + rc, out = rc_cmd(f'{netns_cmd} ip --json address show dev {ifname}') + if rc == 0: + json_out = json.loads(out) + addresses = jmespath.search("[].addr_info[].{family: family, address: local, prefixlen: prefixlen}", json_out) + for address_info in addresses: + family = address_info['family'] + address = address_info['address'] + prefixlen = address_info['prefixlen'] + # Remove the interface name if present in the given address + if '%' in addr: + addr = addr.split('%')[0] + interface = ip_interface(f"{address}/{prefixlen}") + if ip_interface(addr) == interface or address == addr: + return True return False diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index e09c7d86d..c2ef98140 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -170,7 +170,7 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None, (1, 'Device "eth99" does not exist.') """ out, code = popen( - command, flag, + command.lstrip(), flag, stdout=stdout, stderr=stderr, input=input, timeout=timeout, env=env, shell=shell, -- cgit v1.2.3 From 6c32e17a75199f9acf9e2c33b3480fc27257622d Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 3 Sep 2023 15:20:45 +0200 Subject: netns: T5241: simplify network namespace handling --- python/vyos/ifconfig/interface.py | 75 +++++++++++++++++++++------------------ src/conf_mode/interfaces-dummy.py | 7 ++-- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index e24485132..53256abb7 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -285,7 +285,7 @@ class Interface(Control): } @classmethod - def exists(cls, ifname, netns=None) -> bool: + def exists(cls, ifname: str, netns: str=None) -> bool: cmd = f'ip link show dev {ifname}' if netns: cmd = f'ip netns exec {netns} {cmd}' @@ -361,8 +361,9 @@ class Interface(Control): netns = self.config.get('netns', None) if self.exists(f'{self.ifname}', netns=netns): return + cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) - if 'netns' in self.config: cmd = f'ip netns exec {self.config["netns"]} {cmd}' + if 'netns' in self.config: cmd = f'ip netns exec {netns} {cmd}' self._cmd(cmd) def remove(self): @@ -398,7 +399,7 @@ class Interface(Control): # to be called and instead should raise an Exception: cmd = 'ip link del dev {ifname}'.format(**self.config) # for delete we can't get data from self.config{'netns'} - netns = get_interface_namespace(self.config['ifname']) + netns = get_interface_namespace(self.ifname) if netns: cmd = f'ip netns exec {netns} {cmd}' return self._cmd(cmd) @@ -554,33 +555,30 @@ class Interface(Control): if prev_state == 'up': self.set_admin_state('up') - def del_netns(self, netns): - """ - Remove interface from given NETNS. - """ - - # If NETNS does not exist then there is nothing to delete + def del_netns(self, netns: str) -> bool: + """ Remove interface from given network namespace """ + # If network namespace does not exist then there is nothing to delete if not os.path.exists(f'/run/netns/{netns}'): - return None + return False - # Check if interface realy exists in namespace + # Check if interface exists in network namespace if is_netns_interface(self.ifname, netns): self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}') - return + return True + return False - def set_netns(self, netns): + def set_netns(self, netns: str) -> bool: """ - Add interface from given NETNS. + Add interface from given network namespace Example: >>> from vyos.ifconfig import Interface >>> Interface('dum0').set_netns('foo') """ + self._cmd(f'ip link set dev {self.ifname} netns {netns}') + return True - cmd = f'ip link set dev {self.ifname} netns {netns}' - self._cmd(cmd) - - def set_vrf(self, vrf): + def set_vrf(self, vrf: str) -> bool: """ Add/Remove interface from given VRF instance. @@ -592,10 +590,11 @@ class Interface(Control): tmp = self.get_interface('vrf') if tmp == vrf: - return None + return False self.set_interface('vrf', vrf) self._set_vrf_ct_zone(vrf) + return True def set_arp_cache_tmo(self, tmo): """ @@ -613,10 +612,6 @@ class Interface(Control): return self.set_interface('arp_cache_tmo', tmo) def _cleanup_mss_rules(self, table, ifname): - # Don't allow for netns yet - if 'netns' in self.config: - return None - commands = [] results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n") for line in results: @@ -660,6 +655,10 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_tcp_mss(1320) """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + self._cleanup_mss_rules('ip6 raw', self.ifname) nft_prefix = 'nft add rule ip6 raw VYOS_TCP_MSS' base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn' @@ -774,7 +773,6 @@ class Interface(Control): As per RFC3074. """ - # Don't allow for netns yet if 'netns' in self.config: return None @@ -818,6 +816,10 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_ipv6_source_validation('strict') """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + self._cleanup_ipv6_source_validation_rules(self.ifname) nft_prefix = f'nft add rule ip6 raw vyos_rpfilter iifname "{self.ifname}"' if mode == 'strict': @@ -1167,14 +1169,17 @@ class Interface(Control): if addr in self._addr: return False + # get interface network namespace if specified + netns = self.config.get('netns', None) + # add to interface if addr == 'dhcp': self.set_dhcp(True) elif addr == 'dhcpv6': self.set_dhcpv6(True) - elif not is_intf_addr_assigned(self.ifname, addr, self.config.get('netns')): - exec_within_netns = f'ip netns exec {self.config.get("netns")}' if self.config.get('netns') else '' - tmp = f'{exec_within_netns} ip addr add {addr} dev {self.ifname}' + elif not is_intf_addr_assigned(self.ifname, addr, netns=netns): + netns_cmd = f'ip netns exec {netns}' if netns else '' + tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}' # Add broadcast address for IPv4 if is_ipv4(addr): tmp += ' brd +' @@ -1214,14 +1219,17 @@ class Interface(Control): if not addr: raise ValueError() + # get interface network namespace if specified + netns = self.config.get('netns', None) + # remove from interface if addr == 'dhcp': self.set_dhcp(False) elif addr == 'dhcpv6': self.set_dhcpv6(False) - elif is_intf_addr_assigned(self.ifname, addr, netns=self.config.get('netns')): - exec_within_netns = f'ip netns exec {self.config.get("netns")}' if self.config.get('netns') else '' - self._cmd(f'{exec_within_netns} ip addr del "{addr}" dev "{self.ifname}"') + elif is_intf_addr_assigned(self.ifname, addr, netns=netns): + netns_cmd = f'ip netns exec {netns}' if netns else '' + self._cmd(f'{netns_cmd} ip addr del {addr} dev {self.ifname}') else: return False @@ -1241,10 +1249,9 @@ class Interface(Control): self.set_dhcp(False) self.set_dhcpv6(False) - _netns = get_interface_namespace(self.config['ifname']) - netns_exec = f'ip netns exec {_netns} ' if _netns else '' - cmd = netns_exec - cmd += f'ip addr flush dev {self.ifname}' + netns = get_interface_namespace(self.ifname) + netns_cmd = f'ip netns exec {netns}' if netns else '' + cmd = f'{netns_cmd} ip addr flush dev {self.ifname}' # flush all addresses self._cmd(cmd) diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index a02ddeb51..db768b94d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -55,10 +55,7 @@ def generate(dummy): return None def apply(dummy): - if 'netns' in dummy: - d = DummyIf(ifname=dummy['ifname'], netns=dummy['netns']) - else: - d = DummyIf(dummy['ifname']) + d = DummyIf(**dummy) # Remove dummy interface if 'deleted' in dummy: -- cgit v1.2.3 From 5c2b49092ca1fc56cf198ca89630ec97fddd64b3 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Tue, 5 Sep 2023 19:37:04 +0200 Subject: smoketest: T5241: re-work netns assertions and provide common utility helper --- python/vyos/utils/network.py | 8 ++- smoketest/scripts/cli/test_interfaces_netns.py | 79 ------------------------ smoketest/scripts/cli/test_netns.py | 83 ++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 80 deletions(-) delete mode 100755 smoketest/scripts/cli/test_interfaces_netns.py create mode 100755 smoketest/scripts/cli/test_netns.py diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 4706dbfb7..abc382766 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -42,11 +42,17 @@ def interface_exists(interface) -> bool: def is_netns_interface(interface, netns): from vyos.utils.process import rc_cmd - rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface}') + rc, out = rc_cmd(f'sudo ip netns exec {netns} ip link show dev {interface}') if rc == 0: return True return False +def get_netns_all() -> list: + from json import loads + from vyos.utils.process import cmd + tmp = loads(cmd('ip --json netns ls')) + return [ netns['name'] for netns in tmp ] + def get_vrf_members(vrf: str) -> list: """ Get list of interface VRF members diff --git a/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_interfaces_netns.py deleted file mode 100755 index b8bebb221..000000000 --- a/smoketest/scripts/cli/test_interfaces_netns.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/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 netifaces import interfaces -from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.configsession import ConfigSession -from vyos.configsession import ConfigSessionError -from vyos.ifconfig import Interface -from vyos.ifconfig import Section -from vyos.utils.process import cmd - -base_path = ['netns'] -namespaces = ['mgmt', 'front', 'back', 'ams-ix'] - -class NETNSTest(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self._interfaces = ['dum10', 'dum12', 'dum50'] - - def test_create_netns(self): - for netns in namespaces: - base = base_path + ['name', netns] - self.cli_set(base) - - # commit changes - self.cli_commit() - - netns_list = cmd('ip netns ls') - - # Verify NETNS configuration - for netns in namespaces: - self.assertTrue(netns in netns_list) - - - def test_netns_assign_interface(self): - netns = 'foo' - self.cli_set(['netns', 'name', netns]) - - # Set - for iface in self._interfaces: - self.cli_set(['interfaces', 'dummy', iface, 'netns', netns]) - - # commit changes - self.cli_commit() - - netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') - - for iface in self._interfaces: - self.assertTrue(iface in netns_iface_list) - - # Delete - for iface in self._interfaces: - self.cli_delete(['interfaces', 'dummy', iface, 'netns', netns]) - - # commit changes - self.cli_commit() - - netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') - - for iface in self._interfaces: - self.assertNotIn(iface, netns_iface_list) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_netns.py b/smoketest/scripts/cli/test_netns.py new file mode 100755 index 000000000..fd04dd520 --- /dev/null +++ b/smoketest/scripts/cli/test_netns.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Interface +from vyos.ifconfig import Section +from vyos.utils.process import cmd +from vyos.utils.network import is_netns_interface +from vyos.utils.network import get_netns_all + +base_path = ['netns'] +interfaces = ['dum10', 'dum12', 'dum50'] + +class NetNSTest(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + self.cli_delete(base_path) + # commit changes + self.cli_commit() + + # There should be no network namespace remaining + tmp = cmd('ip netns ls') + self.assertFalse(tmp) + + super(NetNSTest, self).tearDown() + + def test_netns_create(self): + namespaces = ['mgmt', 'front', 'back'] + for netns in namespaces: + self.cli_set(base_path + ['name', netns]) + + # commit changes + self.cli_commit() + + # Verify NETNS configuration + for netns in namespaces: + self.assertIn(netns, get_netns_all()) + + def test_netns_interface(self): + netns = 'foo' + self.cli_set(base_path + ['name', netns]) + + # Set + for iface in interfaces: + self.cli_set(['interfaces', 'dummy', iface, 'netns', netns]) + + # commit changes + self.cli_commit() + + for interface in interfaces: + self.assertTrue(is_netns_interface(interface, netns)) + + # Delete + for interface in interfaces: + self.cli_delete(['interfaces', 'dummy', interface]) + + # commit changes + self.cli_commit() + + netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') + + for interface in interfaces: + self.assertFalse(is_netns_interface(interface, netns)) + +if __name__ == '__main__': + unittest.main(verbosity=2) -- cgit v1.2.3