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):