diff options
-rw-r--r-- | interface-definitions/netns.xml.in | 10 | ||||
-rwxr-xr-x | python/vyos/ifconfig/interface.py | 42 | ||||
-rw-r--r-- | python/vyos/util.py | 18 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_netns.py | 83 | ||||
-rwxr-xr-x | src/conf_mode/netns.py | 118 |
5 files changed, 254 insertions, 17 deletions
diff --git a/interface-definitions/netns.xml.in b/interface-definitions/netns.xml.in index 88451737a..80de805fb 100644 --- a/interface-definitions/netns.xml.in +++ b/interface-definitions/netns.xml.in @@ -9,13 +9,13 @@ <tagNode name="name"> <properties> <help>Network namespace name</help> + <constraint> + <regex>^[a-zA-Z0-9-_]{1,100}</regex> + </constraint> + <constraintErrorMessage>Netns name must be alphanumeric and can contain hyphens and underscores.</constraintErrorMessage> </properties> <children> - <leafNode name="descriprion"> - <properties> - <help>Network namespace description</help> - </properties> - </leafNode> + #include <include/interface/description.xml.i> </children> </tagNode> </children> diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 50da2553a..bcb692697 100755 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -37,6 +37,7 @@ from vyos.util import mac2eui64 from vyos.util import dict_search from vyos.util import read_file from vyos.util import get_interface_config +from vyos.util import get_interface_namespace from vyos.util import is_systemd_service_active from vyos.template import is_ipv4 from vyos.template import is_ipv6 @@ -515,19 +516,33 @@ 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 + 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}') + return + def set_netns(self, netns): """ - Add/Remove interface from given NETNS. + Add interface from given NETNS. Example: >>> from vyos.ifconfig import Interface >>> Interface('dum0').set_netns('foo') """ - #tmp = self.get_interface('netns') - #if tmp == netns: - # return None - self.set_interface('netns', netns) def set_vrf(self, vrf): @@ -1371,6 +1386,16 @@ class Interface(Control): if mac: self.set_mac(mac) + # If interface is connected to NETNS we don't have to check all other + # settings like MTU/IPv6/sysctl values, etc. + # 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 + else: + self.del_netns(config.get('netns', '')) + # Update interface description self.set_alias(config.get('description', '')) @@ -1423,13 +1448,6 @@ class Interface(Control): # checked before self.set_vrf(config.get('vrf', '')) - # If interface attached to NETNS we shouldn't check all other settings - # As interface placed to separate logical stack - # Configure NETNS - if dict_search('netns', config) != None: - self.set_netns(config.get('netns', '')) - return - # Configure MSS value for IPv4 TCP connections tmp = dict_search('ip.adjust_mss', config) value = tmp if (tmp != None) else '0' diff --git a/python/vyos/util.py b/python/vyos/util.py index 9aa1f98d2..6c375e1a6 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -794,6 +794,24 @@ def get_interface_address(interface): tmp = loads(cmd(f'ip -d -j addr show {interface}'))[0] return tmp +def get_interface_namespace(iface): + """ + Returns wich netns the interface belongs to + """ + from json import loads + # Check if netns exist + tmp = loads(cmd(f'ip --json netns ls')) + if len(tmp) == 0: + return None + + for ns in tmp: + namespace = f'{ns["name"]}' + # Search interface in each netns + data = loads(cmd(f'ip netns exec {namespace} ip -j link show')) + for compare in data: + if iface == compare["ifname"]: + return namespace + def get_all_vrfs(): """ Return a dictionary of all system wide known VRF instances """ from json import loads diff --git a/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_interfaces_netns.py new file mode 100755 index 000000000..9975a6b09 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_netns.py @@ -0,0 +1,83 @@ +#!/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 <http://www.gnu.org/licenses/>. + +import re +import os +import json +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.util 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/src/conf_mode/netns.py b/src/conf_mode/netns.py new file mode 100755 index 000000000..0924eb616 --- /dev/null +++ b/src/conf_mode/netns.py @@ -0,0 +1,118 @@ +#!/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 <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from tempfile import NamedTemporaryFile + +from vyos.config import Config +from vyos.configdict import node_changed +from vyos.ifconfig import Interface +from vyos.util import call +from vyos.util import dict_search +from vyos.util import get_interface_config +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +def netns_interfaces(c, match): + """ + get NETNS bound interfaces + """ + matched = [] + old_level = c.get_level() + c.set_level(['interfaces']) + section = c.get_config_dict([], get_first_key=True) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'netns' in interface: + v = interface.get('netns', '') + if v == match: + matched.append(name) + + c.set_level(old_level) + return matched + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['netns'] + netns = conf.get_config_dict(base, get_first_key=True, + no_tag_node_value_mangle=True) + + # determine which NETNS has been removed + for name in node_changed(conf, base + ['name']): + if 'netns_remove' not in netns: + netns.update({'netns_remove' : {}}) + + netns['netns_remove'][name] = {} + # get NETNS bound interfaces + interfaces = netns_interfaces(conf, name) + if interfaces: netns['netns_remove'][name]['interface'] = interfaces + + return netns + +def verify(netns): + # ensure NETNS is not assigned to any interface + if 'netns_remove' in netns: + for name, config in netns['netns_remove'].items(): + if 'interface' in config: + raise ConfigError(f'Can not remove NETNS "{name}", it still has '\ + f'member interfaces!') + + if 'name' in netns: + for name, config in netns['name'].items(): + print(name) + + return None + + +def generate(netns): + if not netns: + return None + + return None + + +def apply(netns): + + for tmp in (dict_search('netns_remove', netns) or []): + if os.path.isfile(f'/run/netns/{tmp}'): + call(f'ip netns del {tmp}') + + if 'name' in netns: + for name, config in netns['name'].items(): + if not os.path.isfile(f'/run/netns/{name}'): + call(f'ip netns add {name}') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |