From dff9093ab682d798e6165840cfae234f2a5372f5 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Mon, 17 Jul 2017 15:27:53 +0100 Subject: [PATCH] Migrate from 'ip' commands to 'pyroute2' This patch migrates the use of command line 'ip' commands to pyroute2 library. A new class, 'IpCommand', is created to wrap the use of the library, implementing the functionalities needed in this project. The new wrapper class is defined in 'os_vif' and is used in 'vif_plug_linux_bridge' and 'vif_plug_ovs'. This patch also adds functional tests in 'os_vif'. The aim of these functional tests is to check 'pyroute2' implementation works correctly, by creating, modifying and deleting network interfaces. 'ip' commands are used to execute additional actions, not relying on the tested library to check its own results. Co-Authored-By: Stephen Finucane Closes-Bug: #1677238 Change-Id: I18f7b3424a6c447ee89df1f0326ece75f2333bf2 --- .stestr.conf | 3 + os_vif/exception.py | 14 ++ os_vif/internal/__init__.py | 25 +++ .../{common => internal/command}/__init__.py | 0 os_vif/internal/command/ip/__init__.py | 34 ++++ os_vif/internal/command/ip/api.py | 79 ++++++++ os_vif/internal/command/ip/impl_pyroute2.py | 94 +++++++++ os_vif/tests/functional/__init__.py | 0 os_vif/tests/functional/base.py | 112 +++++++++++ os_vif/tests/functional/internal/__init__.py | 0 .../functional/internal/command/__init__.py | 0 .../internal/command/ip/__init__.py | 0 .../internal/command/ip/test_impl_pyroute2.py | 183 ++++++++++++++++++ os_vif/tests/functional/privsep.py | 21 ++ os_vif/tests/unit/internal/__init__.py | 0 .../tests/unit/internal/command/__init__.py | 0 .../unit/internal/command/ip/__init__.py | 0 .../internal/command/ip/test_impl_pyroute2.py | 145 ++++++++++++++ os_vif/utils.py | 19 ++ requirements.txt | 1 + test-requirements.txt | 1 + tox.ini | 16 +- vif_plug_linux_bridge/linux_net.py | 24 +-- .../tests/unit/test_linux_net.py | 66 ++++--- vif_plug_ovs/linux_net.py | 20 +- vif_plug_ovs/tests/unit/test_linux_net.py | 40 ++-- 26 files changed, 824 insertions(+), 73 deletions(-) create mode 100644 .stestr.conf create mode 100644 os_vif/internal/__init__.py rename os_vif/{common => internal/command}/__init__.py (100%) create mode 100644 os_vif/internal/command/ip/__init__.py create mode 100644 os_vif/internal/command/ip/api.py create mode 100644 os_vif/internal/command/ip/impl_pyroute2.py create mode 100644 os_vif/tests/functional/__init__.py create mode 100644 os_vif/tests/functional/base.py create mode 100644 os_vif/tests/functional/internal/__init__.py create mode 100644 os_vif/tests/functional/internal/command/__init__.py create mode 100644 os_vif/tests/functional/internal/command/ip/__init__.py create mode 100644 os_vif/tests/functional/internal/command/ip/test_impl_pyroute2.py create mode 100644 os_vif/tests/functional/privsep.py create mode 100644 os_vif/tests/unit/internal/__init__.py create mode 100644 os_vif/tests/unit/internal/command/__init__.py create mode 100644 os_vif/tests/unit/internal/command/ip/__init__.py create mode 100644 os_vif/tests/unit/internal/command/ip/test_impl_pyroute2.py create mode 100644 os_vif/utils.py diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 00000000..88843b96 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-.} +top_dir=./ diff --git a/os_vif/exception.py b/os_vif/exception.py index f14d8e7a..1a21cf57 100644 --- a/os_vif/exception.py +++ b/os_vif/exception.py @@ -80,3 +80,17 @@ class UnplugException(ExceptionBase): class NetworkMissingPhysicalNetwork(ExceptionBase): msg_fmt = _("Physical network is missing for network %(network_uuid)s") + + +class NetworkInterfaceNotFound(ExceptionBase): + msg_fmt = _("Network interface %(interface)s not found") + + +class NetworkInterfaceTypeNotDefined(ExceptionBase): + msg_fmt = _("Network interface type %(type)s not defined") + + +class ExternalImport(ExceptionBase): + msg_fmt = _("Use of this module outside of os_vif is not allowed. It must " + "not be imported in os-vif plugins that are out of tree as it " + "is not a public interface of os-vif.") diff --git a/os_vif/internal/__init__.py b/os_vif/internal/__init__.py new file mode 100644 index 00000000..4cb3fd12 --- /dev/null +++ b/os_vif/internal/__init__.py @@ -0,0 +1,25 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import inspect +from os import path + +from os_vif import exception + +os_vif_root = path.dirname(path.dirname(path.dirname(__file__))) +frames_info = inspect.getouterframes(inspect.currentframe()) +for frame_info in frames_info[1:]: + importer_filename = inspect.getframeinfo(frame_info[0]).filename + if os_vif_root in importer_filename: + break +else: + raise exception.ExternalImport() diff --git a/os_vif/common/__init__.py b/os_vif/internal/command/__init__.py similarity index 100% rename from os_vif/common/__init__.py rename to os_vif/internal/command/__init__.py diff --git a/os_vif/internal/command/ip/__init__.py b/os_vif/internal/command/ip/__init__.py new file mode 100644 index 00000000..d533d222 --- /dev/null +++ b/os_vif/internal/command/ip/__init__.py @@ -0,0 +1,34 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from os_vif.internal.command.ip import api + + +def set(device, check_exit_code=None, state=None, mtu=None, address=None, + promisc=None): + """Method to set a parameter in an interface.""" + return api._get_impl().set(device, check_exit_code=check_exit_code, + state=state, mtu=mtu, address=address, + promisc=promisc) + + +def add(device, dev_type, check_exit_code=None, peer=None, link=None, + vlan_id=None): + """Method to add an interface.""" + return api._get_impl().add(device, dev_type, + check_exit_code=check_exit_code, peer=peer, + link=link, vlan_id=vlan_id) + + +def delete(device, check_exit_code=None): + """Method to delete an interface.""" + return api._get_impl().delete(device, check_exit_code=check_exit_code) diff --git a/os_vif/internal/command/ip/api.py b/os_vif/internal/command/ip/api.py new file mode 100644 index 00000000..434d6051 --- /dev/null +++ b/os_vif/internal/command/ip/api.py @@ -0,0 +1,79 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import six + +from oslo_log import log as logging +from oslo_utils import importutils + + +LOG = logging.getLogger(__name__) + + +impl_map = { + 'pyroute2': 'os_vif.internal.command.ip.impl_pyroute2', +} + + +def _get_impl(): + # NOTE(ralonsoh): currently there is only one implementation. No config + # options are exposed to the user. + pyroute2 = importutils.import_module(impl_map['pyroute2']) + return pyroute2.PyRoute2() + + +@six.add_metaclass(abc.ABCMeta) +class IpCommand(object): + + TYPE_VETH = 'veth' + TYPE_VLAN = 'vlan' + + @abc.abstractmethod + def set(self, device, check_exit_code=None, state=None, mtu=None, + address=None, promisc=None): + """Method to set a parameter in an interface. + + :param device: A network device (string) + :param check_exit_code: List of integers of allowed execution exit + codes + :param state: String network device state + :param mtu: Integer MTU value + :param address: String MAC address + :param promisc: Boolean promiscuous mode + :return: status of the command execution + """ + + @abc.abstractmethod + def add(self, device, dev_type, check_exit_code=None, peer=None, link=None, + vlan_id=None): + """Method to add an interface. + + :param device: A network device (string) + :param dev_type: String network device type (TYPE_VETH, TYPE_VLAN) + :param check_exit_code: List of integers of allowed execution exit + codes + :param peer: String peer name, for veth interfaces + :param link: String root network interface name, 'device' will be a + VLAN tagged virtual interface + :param vlan_id: Integer VLAN ID for VLAN devices + :return: status of the command execution + """ + + @abc.abstractmethod + def delete(self, device, check_exit_code=None): + """Method to delete an interface. + + :param device: A network device (string) + :param dev_type: String network device type (TYPE_VETH, TYPE_VLAN) + :return: status of the command execution + """ diff --git a/os_vif/internal/command/ip/impl_pyroute2.py b/os_vif/internal/command/ip/impl_pyroute2.py new file mode 100644 index 00000000..a229b917 --- /dev/null +++ b/os_vif/internal/command/ip/impl_pyroute2.py @@ -0,0 +1,94 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from oslo_utils import excutils +from pyroute2 import iproute +from pyroute2.netlink import exceptions as ipexc +from pyroute2.netlink.rtnl import ifinfmsg + +from os_vif import exception +from os_vif.internal.command.ip import api +from os_vif import utils + +LOG = logging.getLogger(__name__) + + +class PyRoute2(api.IpCommand): + + def _ip_link(self, ip, command, check_exit_code, **kwargs): + try: + LOG.debug('pyroute2 command %(command)s, arguments %(args)s' % + {'command': command, 'args': kwargs}) + return ip.link(command, **kwargs) + except ipexc.NetlinkError as e: + with excutils.save_and_reraise_exception() as ctx: + if e.code in check_exit_code: + LOG.error('NetlinkError was raised, code %s, message: %s' % + (e.code, str(e))) + ctx.reraise = False + + def set(self, device, check_exit_code=None, state=None, mtu=None, + address=None, promisc=None): + check_exit_code = check_exit_code or [] + ip = iproute.IPRoute() + idx = ip.link_lookup(ifname=device) + if not idx: + raise exception.NetworkInterfaceNotFound(interface=device) + idx = idx[0] + + args = {'index': idx} + if state: + args['state'] = state + if mtu: + args['mtu'] = mtu + if address: + args['address'] = address + if promisc is not None: + flags = ip.link('get', index=idx)[0]['flags'] + args['flags'] = (utils.set_mask(flags, ifinfmsg.IFF_PROMISC) + if promisc is True else + utils.unset_mask(flags, ifinfmsg.IFF_PROMISC)) + + if isinstance(check_exit_code, int): + check_exit_code = [check_exit_code] + + return self._ip_link(ip, 'set', check_exit_code, **args) + + def add(self, device, dev_type, check_exit_code=None, peer=None, link=None, + vlan_id=None): + check_exit_code = check_exit_code or [] + ip = iproute.IPRoute() + args = {'ifname': device, + 'kind': dev_type} + if self.TYPE_VLAN == dev_type: + args['vlan_id'] = vlan_id + idx = ip.link_lookup(ifname=link) + if 0 == len(idx): + raise exception.NetworkInterfaceNotFound(interface=link) + args['link'] = idx[0] + elif self.TYPE_VETH == dev_type: + args['peer'] = peer + else: + raise exception.NetworkInterfaceTypeNotDefined(type=dev_type) + + return self._ip_link(ip, 'add', check_exit_code, **args) + + def delete(self, device, check_exit_code=None): + check_exit_code = check_exit_code or [] + ip = iproute.IPRoute() + idx = ip.link_lookup(ifname=device) + if len(idx) == 0: + raise exception.NetworkInterfaceNotFound(interface=device) + idx = idx[0] + + return self._ip_link(ip, 'del', check_exit_code, **{'index': idx}) diff --git a/os_vif/tests/functional/__init__.py b/os_vif/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/functional/base.py b/os_vif/tests/functional/base.py new file mode 100644 index 00000000..bc4655ca --- /dev/null +++ b/os_vif/tests/functional/base.py @@ -0,0 +1,112 @@ +# Derived from: neutron/tests/functional/base.py +# neutron/tests/base.py +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import functools +import inspect +import os +import six +import string +import sys + +import eventlet.timeout +from os_vif import version as osvif_version +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import fileutils +from oslotest import base + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +def _get_test_log_path(): + return os.environ.get('OS_LOG_PATH', '/tmp') + + +# This is the directory from which infra fetches log files for functional tests +DEFAULT_LOG_DIR = os.path.join(_get_test_log_path(), 'osvif-functional-logs') + + +def _catch_timeout(f): + @functools.wraps(f) + def func(self, *args, **kwargs): + try: + return f(self, *args, **kwargs) + except eventlet.Timeout as e: + self.fail('Execution of this test timed out: %s' % e) + return func + + +class _CatchTimeoutMetaclass(abc.ABCMeta): + def __init__(cls, name, bases, dct): + super(_CatchTimeoutMetaclass, cls).__init__(name, bases, dct) + for name, method in inspect.getmembers( + # NOTE(ihrachys): we should use isroutine because it will catch + # both unbound methods (python2) and functions (python3) + cls, predicate=inspect.isroutine): + if name.startswith('test_'): + setattr(cls, name, _catch_timeout(method)) + + +def setup_logging(): + """Sets up the logging options for a log with supplied name.""" + product_name = "os_vif" + logging.setup(cfg.CONF, product_name) + LOG.info("Logging enabled!") + LOG.info("%(prog)s version %(version)s", + {'prog': sys.argv[0], 'version': osvif_version.__version__}) + LOG.debug("command line: %s", " ".join(sys.argv)) + + +def sanitize_log_path(path): + """Sanitize the string so that its log path is shell friendly""" + replace_map = string.maketrans(' ()', '-__') + return path.translate(replace_map) + + +# Test worker cannot survive eventlet's Timeout exception, which effectively +# kills the whole worker, with all test cases scheduled to it. This metaclass +# makes all test cases convert Timeout exceptions into unittest friendly +# failure mode (self.fail). +@six.add_metaclass(_CatchTimeoutMetaclass) +class BaseFunctionalTestCase(base.BaseTestCase): + """Base class for functional tests.""" + + def setUp(self): + super(BaseFunctionalTestCase, self).setUp() + logging.register_options(CONF) + setup_logging() + fileutils.ensure_tree(DEFAULT_LOG_DIR, mode=0o755) + log_file = sanitize_log_path( + os.path.join(DEFAULT_LOG_DIR, "%s.txt" % self.id())) + self.config(log_file=log_file) + + def config(self, **kw): + """Override some configuration values. + + The keyword arguments are the names of configuration options to + override and their values. + + If a group argument is supplied, the overrides are applied to + the specified configuration option group. + + All overrides are automatically cleared at the end of the current + test by the fixtures cleanup process. + """ + group = kw.pop('group', None) + for k, v in kw.items(): + CONF.set_override(k, v, group) diff --git a/os_vif/tests/functional/internal/__init__.py b/os_vif/tests/functional/internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/functional/internal/command/__init__.py b/os_vif/tests/functional/internal/command/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/functional/internal/command/ip/__init__.py b/os_vif/tests/functional/internal/command/ip/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/functional/internal/command/ip/test_impl_pyroute2.py b/os_vif/tests/functional/internal/command/ip/test_impl_pyroute2.py new file mode 100644 index 00000000..16353c7c --- /dev/null +++ b/os_vif/tests/functional/internal/command/ip/test_impl_pyroute2.py @@ -0,0 +1,183 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +from oslo_concurrency import processutils +from oslo_utils import excutils + +from os_vif.internal.command.ip import impl_pyroute2 +from os_vif.tests.functional import base +from os_vif.tests.functional import privsep + + +@privsep.os_vif_pctxt.entrypoint +def _execute_command(*args): + return processutils.execute(*args) + + +class ShellIpCommands(object): + + def add_device(self, device, dev_type, peer=None, link=None, + vlan_id=None): + if 'vlan' == dev_type: + _execute_command('ip', 'link', 'add', 'link', link, + 'name', device, 'type', dev_type, 'vlan', 'id', + vlan_id) + elif 'veth' == dev_type: + _execute_command('ip', 'link', 'add', device, 'type', dev_type, + 'peer', 'name', peer) + elif 'dummy' == dev_type: + _execute_command('ip', 'link', 'add', device, 'type', dev_type) + + def del_device(self, device): + if self.exist_device(device): + _execute_command('ip', 'link', 'del', device) + + def set_status_up(self, device): + _execute_command('ip', 'link', 'set', device, 'up') + + def set_status_down(self, device): + _execute_command('ip', 'link', 'set', device, 'down') + + def set_device_mtu(self, device, mtu): + _execute_command('ip', 'link', 'set', device, 'mtu', mtu) + + def show_device(self, device): + val, err = _execute_command('ip', 'link', 'show', device) + return val.splitlines() + + def exist_device(self, device): + try: + _execute_command('ip', 'link', 'show', device) + return True + except processutils.ProcessExecutionError as e: + with excutils.save_and_reraise_exception() as saved_exception: + if e.exit_code == 1: + saved_exception.reraise = False + return False + + def show_state(self, device): + regex = re.compile(r".*state (?P\w+)") + match = regex.match(self.show_device(device)[0]) + if match is None: + return + return match.group('state') + + def show_promisc(self, device): + regex = re.compile(r".*(PROMISC)") + match = regex.match(self.show_device(device)[0]) + return True if match else False + + def show_mac(self, device): + exp = r".*link/ether (?P([0-9A-Fa-f]{2}[:]){5}[0-9A-Fa-f]{2})" + regex = re.compile(exp) + match = regex.match(self.show_device(device)[1]) + if match is None: + return + return match.group('mac') + + def show_mtu(self, device): + regex = re.compile(r".*mtu (?P\d+)") + match = regex.match(self.show_device(device)[0]) + if match is None: + return + return int(match.group('mtu')) + + +@privsep.os_vif_pctxt.entrypoint +def _ip_cmd_set(*args, **kwargs): + impl_pyroute2.PyRoute2().set(*args, **kwargs) + + +@privsep.os_vif_pctxt.entrypoint +def _ip_cmd_add(*args, **kwargs): + impl_pyroute2.PyRoute2().add(*args, **kwargs) + + +@privsep.os_vif_pctxt.entrypoint +def _ip_cmd_delete(*args, **kwargs): + impl_pyroute2.PyRoute2().delete(*args, **kwargs) + + +class TestIpCommand(ShellIpCommands, base.BaseFunctionalTestCase): + + def setUp(self): + super(TestIpCommand, self).setUp() + + def test_set_state(self): + device1 = "test_dev_1" + device2 = "test_dev_2" + self.addCleanup(self.del_device, device1) + self.add_device(device1, 'veth', peer=device2) + _ip_cmd_set(device1, state='up') + _ip_cmd_set(device2, state='up') + self.assertEqual('UP', self.show_state(device1)) + self.assertEqual('UP', self.show_state(device2)) + _ip_cmd_set(device1, state='down') + _ip_cmd_set(device2, state='down') + self.assertEqual('DOWN', self.show_state(device1)) + self.assertEqual('DOWN', self.show_state(device2)) + + def test_set_mtu(self): + device = "test_dev_3" + self.addCleanup(self.del_device, device) + self.add_device(device, 'dummy') + _ip_cmd_set(device, mtu=1200) + self.assertEqual(1200, self.show_mtu(device)) + _ip_cmd_set(device, mtu=900) + self.assertEqual(900, self.show_mtu(device)) + + def test_set_address(self): + device = "test_dev_4" + address1 = "36:a7:e4:f9:01:01" + address2 = "36:a7:e4:f9:01:01" + self.addCleanup(self.del_device, device) + self.add_device(device, 'dummy') + _ip_cmd_set(device, address=address1) + self.assertEqual(address1, self.show_mac(device)) + _ip_cmd_set(device, address=address2) + self.assertEqual(address2, self.show_mac(device)) + + def test_set_promisc(self): + device = "test_dev_5" + self.addCleanup(self.del_device, device) + self.add_device(device, 'dummy') + _ip_cmd_set(device, promisc=True) + self.assertTrue(self.show_promisc(device)) + _ip_cmd_set(device, promisc=False) + self.assertFalse(self.show_promisc(device)) + + def test_add_vlan(self): + device = "test_dev_6" + link = "test_devlink" + self.addCleanup(self.del_device, device) + self.addCleanup(self.del_device, link) + self.add_device(link, 'dummy') + _ip_cmd_add(device, 'vlan', link=link, vlan_id=100) + self.assertTrue(self.exist_device(device)) + + def test_add_veth(self): + device = "test_dev_7" + peer = "test_devpeer" + self.addCleanup(self.del_device, device) + _ip_cmd_add(device, 'veth', peer=peer) + self.assertTrue(self.exist_device(device)) + self.assertTrue(self.exist_device(peer)) + + def test_delete(self): + device = "test_dev_8" + self.addCleanup(self.del_device, device) + self.add_device(device, 'dummy') + self.assertTrue(self.exist_device(device)) + _ip_cmd_delete(device) + self.assertFalse(self.exist_device(device)) diff --git a/os_vif/tests/functional/privsep.py b/os_vif/tests/functional/privsep.py new file mode 100644 index 00000000..115bb139 --- /dev/null +++ b/os_vif/tests/functional/privsep.py @@ -0,0 +1,21 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_privsep import capabilities as c +from oslo_privsep import priv_context + +os_vif_pctxt = priv_context.PrivContext( + 'os_vif', + cfg_section='os_vif_privileged', + pypath=__name__ + '.os_vif_pctxt', + capabilities=[c.CAP_NET_ADMIN], +) diff --git a/os_vif/tests/unit/internal/__init__.py b/os_vif/tests/unit/internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/unit/internal/command/__init__.py b/os_vif/tests/unit/internal/command/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/unit/internal/command/ip/__init__.py b/os_vif/tests/unit/internal/command/ip/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/os_vif/tests/unit/internal/command/ip/test_impl_pyroute2.py b/os_vif/tests/unit/internal/command/ip/test_impl_pyroute2.py new file mode 100644 index 00000000..0c905424 --- /dev/null +++ b/os_vif/tests/unit/internal/command/ip/test_impl_pyroute2.py @@ -0,0 +1,145 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from pyroute2 import iproute +from pyroute2.netlink import exceptions as ipexc +from pyroute2.netlink.rtnl import ifinfmsg + +from os_vif import exception +from os_vif.internal.command.ip import api as ip_api +from os_vif.tests.unit import base + + +class TestIpCommand(base.TestCase): + + ERROR_CODE = 40 + OTHER_ERROR_CODE = 50 + DEVICE = 'device' + MTU = 1500 + MAC = 'ca:fe:ca:fe:ca:fe' + UP = 'up' + TYPE_VETH = 'veth' + TYPE_VLAN = 'vlan' + LINK = 'device2' + VLAN_ID = 14 + + def setUp(self): + super(TestIpCommand, self).setUp() + self.ip = ip_api._get_impl() + self.ip_link_p = mock.patch.object(iproute.IPRoute, 'link') + self.ip_link = self.ip_link_p.start() + + def test_set(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[1]) as mock_link_lookup: + self.ip_link.return_value = [{'flags': 0x4000}] + self.ip.set(self.DEVICE, state=self.UP, mtu=self.MTU, + address=self.MAC, promisc=True) + mock_link_lookup.assert_called_once_with(ifname=self.DEVICE) + args = {'state': self.UP, + 'mtu': self.MTU, + 'address': self.MAC, + 'flags': 0x4000 | ifinfmsg.IFF_PROMISC} + calls = [mock.call('get', index=1), + mock.call('set', index=1, **args)] + self.ip_link.assert_has_calls(calls) + + def test_set_exit_code(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[1]) as mock_link_lookup: + self.ip_link.side_effect = ipexc.NetlinkError(self.ERROR_CODE, + msg="Error message") + + self.ip.set(self.DEVICE, check_exit_code=[self.ERROR_CODE]) + mock_link_lookup.assert_called_once_with(ifname=self.DEVICE) + self.ip_link.assert_called_once_with('set', index=1) + + self.assertRaises(ipexc.NetlinkError, self.ip.set, self.DEVICE, + check_exit_code=[self.OTHER_ERROR_CODE]) + + def test_set_no_interface_found(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[]) as mock_link_lookup: + self.assertRaises(exception.NetworkInterfaceNotFound, self.ip.set, + self.DEVICE) + mock_link_lookup.assert_called_once_with(ifname=self.DEVICE) + self.ip_link.assert_not_called() + + def test_add_veth(self): + self.ip.add(self.DEVICE, self.TYPE_VETH, peer='peer') + self.ip_link.assert_called_once_with( + 'add', ifname=self.DEVICE, kind=self.TYPE_VETH, peer='peer') + + def test_add_vlan(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[1]) as mock_link_lookup: + self.ip.add(self.DEVICE, self.TYPE_VLAN, link=self.LINK, + vlan_id=self.VLAN_ID) + mock_link_lookup.assert_called_once_with(ifname=self.LINK) + args = {'ifname': self.DEVICE, + 'kind': self.TYPE_VLAN, + 'vlan_id': self.VLAN_ID, + 'link': 1} + self.ip_link.assert_called_once_with('add', **args) + + def test_add_vlan_no_interface_found(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[]) as mock_link_lookup: + self.assertRaises(exception.NetworkInterfaceNotFound, self.ip.add, + self.DEVICE, self.TYPE_VLAN, link=self.LINK) + mock_link_lookup.assert_called_once_with(ifname=self.LINK) + self.ip_link.assert_not_called() + + def test_add_other_type(self): + self.assertRaises(exception.NetworkInterfaceTypeNotDefined, + self.ip.add, self.DEVICE, 'type_not_defined') + + def test_add_exit_code(self): + self.ip_link.side_effect = ipexc.NetlinkError(self.ERROR_CODE, + msg="Error message") + + self.ip.add(self.DEVICE, self.TYPE_VETH, peer='peer', + check_exit_code=[self.ERROR_CODE]) + self.ip_link.assert_called_once_with( + 'add', ifname=self.DEVICE, kind=self.TYPE_VETH, peer='peer') + + self.assertRaises(ipexc.NetlinkError, self.ip.add, self.DEVICE, + self.TYPE_VLAN, peer='peer', + check_exit_code=[self.OTHER_ERROR_CODE]) + + def test_delete(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[1]) as mock_link_lookup: + self.ip.delete(self.DEVICE) + mock_link_lookup.assert_called_once_with(ifname=self.DEVICE) + self.ip_link.assert_called_once_with('del', index=1) + + def test_delete_no_interface_found(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[]) as mock_link_lookup: + self.assertRaises(exception.NetworkInterfaceNotFound, + self.ip.delete, self.DEVICE) + mock_link_lookup.assert_called_once_with(ifname=self.DEVICE) + + def test_delete_exit_code(self): + with mock.patch.object(iproute.IPRoute, 'link_lookup', + return_value=[1]) as mock_link_lookup: + self.ip_link.side_effect = ipexc.NetlinkError(self.ERROR_CODE, + msg="Error message") + + self.ip.delete(self.DEVICE, check_exit_code=[self.ERROR_CODE]) + mock_link_lookup.assert_called_once_with(ifname=self.DEVICE) + self.ip_link.assert_called_once_with('del', index=1) + + self.assertRaises(ipexc.NetlinkError, self.ip.delete, self.DEVICE, + check_exit_code=[self.OTHER_ERROR_CODE]) diff --git a/os_vif/utils.py b/os_vif/utils.py new file mode 100644 index 00000000..d74f96bb --- /dev/null +++ b/os_vif/utils.py @@ -0,0 +1,19 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def set_mask(data, mask): + return data | mask + + +def unset_mask(data, mask, bit_size=32): + return data & ((2 ** bit_size - 1) ^ mask) diff --git a/requirements.txt b/requirements.txt index 2195aea2..6b2af3bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,5 +10,6 @@ oslo.log>=3.30.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.privsep>=1.23.0 # Apache-2.0 oslo.versionedobjects>=1.28.0 # Apache-2.0 +pyroute2>=0.4.21;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2) six>=1.10.0 # MIT stevedore>=1.20.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index f60eb0c9..d8adeded 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,6 +9,7 @@ reno>=2.5.0 # Apache-2.0 sphinx>=1.6.2 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 +stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT diff --git a/tox.ini b/tox.ini index 271d3e49..37dd9a84 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --slowest --testr-args='{posargs}' whitelist_externals = bash [tox:jenkins] @@ -22,6 +21,21 @@ commands = flake8 [testenv:venv] commands = {posargs} +[testenv:py27] +commands = + stestr run --black-regex ".tests.functional" --test-path="os_vif/tests" '{posargs}' + +[testenv:py35] +commands = + stestr run --black-regex ".tests.functional" '{posargs}' + +[testenv:functional] +basepython = python2.7 +setenv = + {[testenv]setenv} +commands = + stestr run --black-regex ".tests.unit" '{posargs}' + [testenv:cover] commands = coverage erase diff --git a/vif_plug_linux_bridge/linux_net.py b/vif_plug_linux_bridge/linux_net.py index 8e0f63ee..596b571d 100644 --- a/vif_plug_linux_bridge/linux_net.py +++ b/vif_plug_linux_bridge/linux_net.py @@ -21,6 +21,7 @@ import os +from os_vif.internal.command import ip as ip_lib from oslo_concurrency import lockutils from oslo_concurrency import processutils from oslo_log import log as logging @@ -28,6 +29,7 @@ from oslo_utils import excutils from vif_plug_linux_bridge import privsep + LOG = logging.getLogger(__name__) _IPTABLES_MANAGER = None @@ -40,8 +42,7 @@ def device_exists(device): def _set_device_mtu(dev, mtu): """Set the device MTU.""" if mtu: - processutils.execute('ip', 'link', 'set', dev, 'mtu', mtu, - check_exit_code=[0, 2, 254]) + ip_lib.set(dev, mtu=mtu, check_exit_code=[0, 2, 254]) else: LOG.debug("MTU not set on %(interface_name)s interface", {'interface_name': dev}) @@ -77,18 +78,14 @@ def _ensure_vlan_privileged(vlan_num, bridge_interface, mac_address, mtu): interface = 'vlan%s' % vlan_num if not device_exists(interface): LOG.debug('Starting VLAN interface %s', interface) - processutils.execute('ip', 'link', 'add', 'link', - bridge_interface, 'name', interface, 'type', - 'vlan', 'id', vlan_num, - check_exit_code=[0, 2, 254]) + ip_lib.add(interface, 'vlan', link=bridge_interface, + vlan_id=vlan_num, check_exit_code=[0, 2, 254]) # (danwent) the bridge will inherit this address, so we want to # make sure it is the value set from the NetworkManager if mac_address: - processutils.execute('ip', 'link', 'set', interface, - 'address', mac_address, - check_exit_code=[0, 2, 254]) - processutils.execute('ip', 'link', 'set', interface, 'up', - check_exit_code=[0, 2, 254]) + ip_lib.set(interface, address=mac_address, + check_exit_code=[0, 2, 254]) + ip_lib.set(interface, state='up', check_exit_code=[0, 2, 254]) # NOTE(vish): set mtu every time to ensure that changes to mtu get # propogated _set_device_mtu(interface, mtu) @@ -144,7 +141,7 @@ def _ensure_bridge_privileged(bridge, interface, net_attrs, gateway, # instead it inherits the MAC address of the first device on the # bridge, which will either be the vlan interface, or a # physical NIC. - processutils.execute('ip', 'link', 'set', bridge, 'up') + ip_lib.set(bridge, state='up') if interface: LOG.debug('Adding interface %(interface)s to bridge %(bridge)s', @@ -156,8 +153,7 @@ def _ensure_bridge_privileged(bridge, interface, net_attrs, gateway, msg = _('Failed to add interface: %s') % err raise Exception(msg) - out, err = processutils.execute('ip', 'link', 'set', - interface, 'up', check_exit_code=False) + ip_lib.set(interface, state='up') _set_device_mtu(interface, mtu) diff --git a/vif_plug_linux_bridge/tests/unit/test_linux_net.py b/vif_plug_linux_bridge/tests/unit/test_linux_net.py index f7d5fdfd..dabb2354 100644 --- a/vif_plug_linux_bridge/tests/unit/test_linux_net.py +++ b/vif_plug_linux_bridge/tests/unit/test_linux_net.py @@ -15,6 +15,7 @@ import os.path import testtools import fixtures +from os_vif.internal.command import ip as ip_lib from oslo_concurrency import lockutils from oslo_concurrency import processutils from oslo_config import cfg @@ -40,36 +41,35 @@ class LinuxNetTest(testtools.TestCase): group='oslo_concurrency') self.useFixture(log_fixture.get_logging_handle_error_fixture()) - @mock.patch.object(processutils, "execute") - def test_set_device_mtu(self, execute): + @mock.patch.object(ip_lib, "set") + def test_set_device_mtu(self, mock_ip_set): linux_net._set_device_mtu(dev='fakedev', mtu=1500) - expected = ['ip', 'link', 'set', 'fakedev', 'mtu', 1500] - execute.assert_called_with(*expected, check_exit_code=mock.ANY) + mock_ip_set.assert_called_once_with('fakedev', mtu=1500, + check_exit_code=[0, 2, 254]) @mock.patch.object(processutils, "execute") def test_set_device_invalid_mtu(self, mock_exec): linux_net._set_device_mtu(dev='fakedev', mtu=None) mock_exec.assert_not_called() - @mock.patch.object(processutils, "execute") + @mock.patch.object(ip_lib, "add") + @mock.patch.object(ip_lib, "set") @mock.patch.object(linux_net, "device_exists", return_value=False) @mock.patch.object(linux_net, "_set_device_mtu") - def test_ensure_vlan(self, mock_set_mtu, - mock_dev_exists, mock_exec): + def test_ensure_vlan(self, mock_set_mtu, mock_dev_exists, mock_ip_set, + mock_ip_add): linux_net._ensure_vlan_privileged(123, 'fake-bridge', mac_address='fake-mac', mtu=1500) self.assertTrue(mock_dev_exists.called) - calls = [mock.call('ip', 'link', 'add', 'link', - 'fake-bridge', 'name', 'vlan123', 'type', - 'vlan', 'id', 123, - check_exit_code=[0, 2, 254]), - mock.call('ip', 'link', 'set', 'vlan123', - 'address', 'fake-mac', - check_exit_code=[0, 2, 254]), - mock.call('ip', 'link', 'set', 'vlan123', 'up', - check_exit_code=[0, 2, 254])] - mock_exec.assert_has_calls(calls) + set_calls = [mock.call('vlan123', address='fake-mac', + check_exit_code=[0, 2, 254]), + mock.call('vlan123', state='up', + check_exit_code=[0, 2, 254])] + mock_ip_add.assert_called_once_with( + 'vlan123', 'vlan', link='fake-bridge', vlan_id=123, + check_exit_code=[0, 2, 254]) + mock_ip_set.assert_has_calls(set_calls) mock_set_mtu.assert_called_once_with('vlan123', 1500) @mock.patch.object(processutils, "execute") @@ -87,38 +87,43 @@ class LinuxNetTest(testtools.TestCase): with testtools.ExpectedException(ValueError): linux_net.ensure_bridge("br0", None, filtering=False) + @mock.patch.object(ip_lib, "set") @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", side_effect=[False, True]) - def test_ensure_bridge_concurrent_add(self, mock_dev_exists, mock_exec): + def test_ensure_bridge_concurrent_add(self, mock_dev_exists, mock_exec, + mock_ip_set): mock_exec.side_effect = [ValueError(), 0, 0, 0] linux_net.ensure_bridge("br0", None, filtering=False) calls = [mock.call('brctl', 'addbr', 'br0'), mock.call('brctl', 'setfd', 'br0', 0), - mock.call('brctl', 'stp', 'br0', "off"), - mock.call('ip', 'link', 'set', 'br0', "up")] + mock.call('brctl', 'stp', 'br0', "off")] mock_exec.assert_has_calls(calls) mock_dev_exists.assert_has_calls([mock.call("br0"), mock.call("br0")]) + mock_ip_set.assert_called_once_with('br0', state='up') + @mock.patch.object(ip_lib, "set") @mock.patch.object(linux_net, "_set_device_mtu") @mock.patch.object(os.path, "exists", return_value=False) @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=False) def test_ensure_bridge_mtu_not_called(self, mock_dev_exists, mock_exec, - mock_path_exists, mock_set_mtu): + mock_path_exists, mock_set_mtu, mock_ip_set): """This test validates that mtus are updated only if an interface is added to the bridge """ linux_net._ensure_bridge_privileged("fake-bridge", None, None, False, mtu=1500) mock_set_mtu.assert_not_called() + mock_ip_set.assert_called_once_with('fake-bridge', state='up') + @mock.patch.object(ip_lib, "set") @mock.patch.object(linux_net, "_set_device_mtu") @mock.patch.object(os.path, "exists", return_value=False) @mock.patch.object(processutils, "execute", return_value=("", "")) @mock.patch.object(linux_net, "device_exists", return_value=False) def test_ensure_bridge_mtu_order(self, mock_dev_exists, mock_exec, - mock_path_exists, mock_set_mtu): + mock_path_exists, mock_set_mtu, mock_ip_set): """This test validates that when adding an interface to a bridge, the interface mtu is updated first followed by the bridge. This is required to work around @@ -129,33 +134,38 @@ class LinuxNetTest(testtools.TestCase): calls = [mock.call('fake-interface', 1500), mock.call('fake-bridge', 1500)] mock_set_mtu.assert_has_calls(calls) + calls = [mock.call('fake-bridge', state = 'up'), + mock.call('fake-interface', state='up')] + mock_ip_set.assert_has_calls(calls) + @mock.patch.object(ip_lib, "set") @mock.patch.object(os.path, "exists", return_value=False) @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=False) def test_ensure_bridge_new_ipv4(self, mock_dev_exists, mock_exec, - mock_path_exists): + mock_path_exists, mock_ip_set): linux_net.ensure_bridge("br0", None, filtering=False) calls = [mock.call('brctl', 'addbr', 'br0'), mock.call('brctl', 'setfd', 'br0', 0), - mock.call('brctl', 'stp', 'br0', "off"), - mock.call('ip', 'link', 'set', 'br0', "up")] + mock.call('brctl', 'stp', 'br0', "off")] mock_exec.assert_has_calls(calls) mock_dev_exists.assert_called_once_with("br0") + mock_ip_set.assert_called_once_with('br0', state='up') + @mock.patch.object(ip_lib, "set") @mock.patch.object(os.path, "exists", return_value=True) @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=False) def test_ensure_bridge_new_ipv6(self, mock_dev_exists, mock_exec, - mock_path_exists): + mock_path_exists, mock_ip_set): linux_net.ensure_bridge("br0", None, filtering=False) calls = [mock.call('brctl', 'addbr', 'br0'), mock.call('brctl', 'setfd', 'br0', 0), mock.call('brctl', 'stp', 'br0', "off"), mock.call('tee', '/proc/sys/net/ipv6/conf/br0/disable_ipv6', - check_exit_code=[0, 1], process_input='1'), - mock.call('ip', 'link', 'set', 'br0', "up")] + check_exit_code=[0, 1], process_input='1')] mock_exec.assert_has_calls(calls) mock_dev_exists.assert_called_once_with("br0") + mock_ip_set.assert_called_once_with('br0', state='up') diff --git a/vif_plug_ovs/linux_net.py b/vif_plug_ovs/linux_net.py index c73c7842..b84a4663 100644 --- a/vif_plug_ovs/linux_net.py +++ b/vif_plug_ovs/linux_net.py @@ -24,6 +24,7 @@ import os import re import sys +from os_vif.internal.command import ip as ip_lib from oslo_concurrency import processutils from oslo_log import log as logging from oslo_utils import excutils @@ -117,8 +118,7 @@ def _delete_net_dev(dev): """Delete a network device only if it exists.""" if device_exists(dev): try: - processutils.execute('ip', 'link', 'delete', dev, - check_exit_code=[0, 2, 254]) + ip_lib.delete(dev, check_exit_code=[0, 2, 254]) LOG.debug("Net device removed: '%s'", dev) except processutils.ProcessExecutionError: with excutils.save_and_reraise_exception(): @@ -133,11 +133,10 @@ def create_veth_pair(dev1_name, dev2_name, mtu): for dev in [dev1_name, dev2_name]: _delete_net_dev(dev) - processutils.execute('ip', 'link', 'add', dev1_name, - 'type', 'veth', 'peer', 'name', dev2_name) + ip_lib.add(dev1_name, 'veth', peer=dev2_name) for dev in [dev1_name, dev2_name]: - processutils.execute('ip', 'link', 'set', dev, 'up') - processutils.execute('ip', 'link', 'set', dev, 'promisc', 'on') + ip_lib.set(dev, state='up') + ip_lib.set(dev, promisc='on') _update_device_mtu(dev, mtu) @@ -180,7 +179,8 @@ def delete_bridge(bridge, dev): if device_exists(bridge): if interface_in_bridge(bridge, dev): processutils.execute('brctl', 'delif', bridge, dev) - processutils.execute('ip', 'link', 'set', bridge, 'down') + + ip_lib.set(bridge, state='down') processutils.execute('brctl', 'delbr', bridge) @@ -213,14 +213,12 @@ def _update_device_mtu(dev, mtu, interface_type=None, timeout=120): @privsep.vif_plug.entrypoint def _set_device_mtu(dev, mtu): """Set the device MTU.""" - processutils.execute('ip', 'link', 'set', dev, 'mtu', mtu, - check_exit_code=[0, 2, 254]) + ip_lib.set(dev, mtu=mtu, check_exit_code=[0, 2, 254]) @privsep.vif_plug.entrypoint def set_interface_state(interface_name, port_state): - processutils.execute('ip', 'link', 'set', interface_name, port_state, - check_exit_code=[0, 2, 254]) + ip_lib.set(interface_name, state=port_state, check_exit_code=[0, 2, 254]) @privsep.vif_plug.entrypoint diff --git a/vif_plug_ovs/tests/unit/test_linux_net.py b/vif_plug_ovs/tests/unit/test_linux_net.py index d4fb5ff1..18dabbb5 100644 --- a/vif_plug_ovs/tests/unit/test_linux_net.py +++ b/vif_plug_ovs/tests/unit/test_linux_net.py @@ -15,6 +15,7 @@ import mock import os.path import testtools +from os_vif.internal.command import ip as ip_lib from oslo_concurrency import processutils from vif_plug_ovs import constants @@ -30,21 +31,21 @@ class LinuxNetTest(testtools.TestCase): privsep.vif_plug.set_client_mode(False) - @mock.patch.object(processutils, "execute") + @mock.patch.object(ip_lib, "set") @mock.patch.object(linux_net, "device_exists", return_value=True) - def test_ensure_bridge_exists(self, mock_dev_exists, mock_execute): + def test_ensure_bridge_exists(self, mock_dev_exists, mock_ip_set): linux_net.ensure_bridge("br0") - mock_execute.assert_has_calls([ - mock.call('ip', 'link', 'set', 'br0', 'up', - check_exit_code=[0, 2, 254])]) + mock_ip_set.assert_called_once_with('br0', state='up', + check_exit_code=[0, 2, 254]) mock_dev_exists.assert_has_calls([mock.call("br0")]) + @mock.patch.object(ip_lib, "set") @mock.patch.object(os.path, "exists", return_value=False) @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=False) def test_ensure_bridge_new_ipv4(self, mock_dev_exists, mock_execute, - mock_path_exists): + mock_path_exists, mock_ip_set): linux_net.ensure_bridge("br0") calls = [ @@ -54,17 +55,18 @@ class LinuxNetTest(testtools.TestCase): mock.call('brctl', 'setageing', 'br0', 0), mock.call('tee', '/sys/class/net/br0/bridge/multicast_snooping', check_exit_code=[0, 1], process_input='0'), - mock.call('ip', 'link', 'set', 'br0', 'up', - check_exit_code=[0, 2, 254]) ] mock_execute.assert_has_calls(calls) mock_dev_exists.assert_has_calls([mock.call("br0")]) + mock_ip_set.assert_called_once_with('br0', state='up', + check_exit_code=[0, 2, 254]) + @mock.patch.object(ip_lib, "set") @mock.patch.object(os.path, "exists", return_value=True) @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=False) def test_ensure_bridge_new_ipv6(self, mock_dev_exists, mock_execute, - mock_path_exists): + mock_path_exists, mock_ip_set): linux_net.ensure_bridge("br0") calls = [ @@ -76,11 +78,11 @@ class LinuxNetTest(testtools.TestCase): check_exit_code=[0, 1], process_input='0'), mock.call('tee', '/proc/sys/net/ipv6/conf/br0/disable_ipv6', check_exit_code=[0, 1], process_input='1'), - mock.call('ip', 'link', 'set', 'br0', 'up', - check_exit_code=[0, 2, 254]) ] mock_execute.assert_has_calls(calls) mock_dev_exists.assert_has_calls([mock.call("br0")]) + mock_ip_set.assert_called_once_with('br0', state='up', + check_exit_code=[0, 2, 254]) @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=False) @@ -93,34 +95,34 @@ class LinuxNetTest(testtools.TestCase): mock_dev_exists.assert_has_calls([mock.call("br0")]) mock_interface_br.assert_not_called() + @mock.patch.object(ip_lib, "set") @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=True) @mock.patch.object(linux_net, "interface_in_bridge", return_value=True) def test_delete_bridge_exists(self, mock_interface_br, mock_dev_exists, - mock_execute): + mock_execute, mock_ip_set): linux_net.delete_bridge("br0", "vnet1") calls = [ mock.call('brctl', 'delif', 'br0', 'vnet1'), - mock.call('ip', 'link', 'set', 'br0', 'down'), mock.call('brctl', 'delbr', 'br0')] mock_execute.assert_has_calls(calls) mock_dev_exists.assert_has_calls([mock.call("br0")]) mock_interface_br.assert_called_once_with("br0", "vnet1") + mock_ip_set.assert_called_once_with('br0', state='down') + @mock.patch.object(ip_lib, "set") @mock.patch.object(processutils, "execute") @mock.patch.object(linux_net, "device_exists", return_value=True) @mock.patch.object(linux_net, "interface_in_bridge", return_value=False) - def test_delete_interface_not_present(self, mock_interface_br, - mock_dev_exists, mock_execute): + def test_delete_interface_not_present(self, + mock_interface_br, mock_dev_exists, mock_execute, mock_ip_set): linux_net.delete_bridge("br0", "vnet1") - calls = [ - mock.call('ip', 'link', 'set', 'br0', 'down'), - mock.call('brctl', 'delbr', 'br0')] - mock_execute.assert_has_calls(calls) + mock_execute.assert_called_once_with('brctl', 'delbr', 'br0') mock_dev_exists.assert_has_calls([mock.call("br0")]) mock_interface_br.assert_called_once_with("br0", "vnet1") + mock_ip_set.assert_called_once_with('br0', state='down') @mock.patch.object(processutils, "execute") def test_add_bridge_port(self, mock_execute):