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 <stephenfin@redhat.com>

Closes-Bug: #1677238
Change-Id: I18f7b3424a6c447ee89df1f0326ece75f2333bf2
This commit is contained in:
Rodolfo Alonso Hernandez 2017-07-17 15:27:53 +01:00
parent 72b27d0e86
commit dff9093ab6
26 changed files with 824 additions and 73 deletions

3
.stestr.conf Normal file
View File

@ -0,0 +1,3 @@
[DEFAULT]
test_path=${OS_TEST_PATH:-.}
top_dir=./

View File

@ -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.")

View File

@ -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()

View File

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

View File

@ -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
"""

View File

@ -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})

View File

View File

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

View File

@ -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<state>\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<mac>([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<mtu>\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))

View File

@ -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],
)

View File

View File

@ -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])

19
os_vif/utils.py Normal file
View File

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

View File

@ -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

View File

@ -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

16
tox.ini
View File

@ -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

View File

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

View File

@ -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')

View File

@ -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

View File

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