diff options
author | Christian Poessinger <christian.poessinger@rohde-schwarz.com> | 2021-12-06 18:49:30 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-06 18:49:30 +0100 |
commit | 84455522574afb9286803f4e192a3d99c259618c (patch) | |
tree | 01ab634d8e5a9c40061f2b8150245319c44746a4 | |
parent | 5b3ff8574c8265cf308b225c1a39503712f0b21d (diff) | |
parent | 47584e030f3341b265e826643951648982cb20d0 (diff) | |
download | vyos-1x-84455522574afb9286803f4e192a3d99c259618c.tar.gz vyos-1x-84455522574afb9286803f4e192a3d99c259618c.zip |
Merge pull request #1077 from sever-sever/T3829
netns: T3829: Ability to configure network namespaces
-rw-r--r-- | interface-definitions/include/interface/netns.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/interfaces-dummy.xml.in | 1 | ||||
-rw-r--r-- | interface-definitions/netns.xml.in | 23 | ||||
-rw-r--r-- | op-mode-definitions/show-netns.xml.in | 13 | ||||
-rwxr-xr-x | python/vyos/ifconfig/interface.py | 43 | ||||
-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 |
8 files changed, 313 insertions, 0 deletions
diff --git a/interface-definitions/include/interface/netns.xml.i b/interface-definitions/include/interface/netns.xml.i new file mode 100644 index 000000000..39f9118fa --- /dev/null +++ b/interface-definitions/include/interface/netns.xml.i @@ -0,0 +1,14 @@ +<!-- include start from interface/netns.xml.i --> +<leafNode name="netns"> + <properties> + <help>Network namespace name</help> + <valueHelp> + <format>text</format> + <description>Network namespace name</description> + </valueHelp> + <completionHelp> + <path>netns name</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 2bc88c1a7..4d4c44160 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -27,6 +27,7 @@ #include <include/interface/source-validation.xml.i> </children> </node> + #include <include/interface/netns.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> diff --git a/interface-definitions/netns.xml.in b/interface-definitions/netns.xml.in new file mode 100644 index 000000000..80de805fb --- /dev/null +++ b/interface-definitions/netns.xml.in @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="netns" owner="${vyos_conf_scripts_dir}/netns.py"> + <properties> + <help>Network namespace</help> + <priority>299</priority> + </properties> + <children> + <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> + #include <include/interface/description.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-netns.xml.in b/op-mode-definitions/show-netns.xml.in new file mode 100644 index 000000000..8d5072d4e --- /dev/null +++ b/op-mode-definitions/show-netns.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="netns"> + <properties> + <help>Show network namespace information</help> + </properties> + <command>ip netns ls</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 58d130ef6..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 @@ -135,6 +136,9 @@ 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}', @@ -512,6 +516,35 @@ 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 interface from given NETNS. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('dum0').set_netns('foo') + """ + + self.set_interface('netns', netns) + def set_vrf(self, vrf): """ Add/Remove interface from given VRF instance. @@ -1353,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', '')) diff --git a/python/vyos/util.py b/python/vyos/util.py index d8e83ab8d..157b26bf7 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) |