diff --git a/etc/manila/rootwrap.d/share.filters b/etc/manila/rootwrap.d/share.filters index 12f32fdd1a..537286fffa 100644 --- a/etc/manila/rootwrap.d/share.filters +++ b/etc/manila/rootwrap.d/share.filters @@ -44,3 +44,9 @@ rm: CommandFilter, /usr/bin/rm, root # manila/share/drivers/glusterfs.py: 'gluster', '--xml', 'volume', 'info', '%s' # manila/share/drivers/glusterfs.py: 'gluster', 'volume', 'set', '%s', 'nfs.export-dir', '%s' gluster: CommandFilter, /usr/sbin/gluster, root + +# manila/network/linux/ip_lib.py: 'ip', 'netns', 'exec', '%s', '%s' +ip: CommandFilter, /sbin/ip, root + +# manila/network/linux/interface.py: 'ovs-vsctl', 'add-port', '%s', '%s' +ovs-vsctl: CommandFilter, /usr/bin/ovs-vsctl, root diff --git a/manila/exception.py b/manila/exception.py index e39a531af3..08eaf43b58 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -541,3 +541,7 @@ class VolumeSnapshotNotFound(NotFound): class InstanceNotFound(NotFound): message = _("Instance %(instance_id)s could not be found.") + + +class BridgeDoesNotExist(ManilaException): + message = _("Bridge %(bridge)s does not exist.") diff --git a/manila/network/linux/__init__.py b/manila/network/linux/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/network/linux/interface.py b/manila/network/linux/interface.py new file mode 100644 index 0000000000..ee29189193 --- /dev/null +++ b/manila/network/linux/interface.py @@ -0,0 +1,194 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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 netaddr +from oslo.config import cfg + +from manila import exception +from manila.network.linux import ip_lib +from manila.network.linux import ovs_lib +from manila.openstack.common import log as logging +from manila import utils + + +LOG = logging.getLogger(__name__) + +OPTS = [ + cfg.StrOpt('ovs_integration_bridge', + default='br-int', + help=_('Name of Open vSwitch bridge to use')), +] + +CONF = cfg.CONF +CONF.register_opts(OPTS) + + +class LinuxInterfaceDriver(object): + __metaclass__ = abc.ABCMeta + + # from linux IF_NAMESIZE + DEV_NAME_LEN = 14 + DEV_NAME_PREFIX = 'tap' + + def __init__(self): + self.conf = CONF + + def init_l3(self, device_name, ip_cidrs, namespace=None): + """Set the L3 settings for the interface using data from the port. + + ip_cidrs: list of 'X.X.X.X/YY' strings + """ + device = ip_lib.IPDevice(device_name, + namespace=namespace) + + previous = {} + for address in device.addr.list(scope='global', filters=['permanent']): + previous[address['cidr']] = address['ip_version'] + + # add new addresses + for ip_cidr in ip_cidrs: + + net = netaddr.IPNetwork(ip_cidr) + if ip_cidr in previous: + del previous[ip_cidr] + continue + + device.addr.add(net.version, ip_cidr, str(net.broadcast)) + + # clean up any old addresses + for ip_cidr, ip_version in previous.items(): + device.addr.delete(ip_version, ip_cidr) + + def check_bridge_exists(self, bridge): + if not ip_lib.device_exists(bridge): + raise exception.BridgeDoesNotExist(bridge=bridge) + + def get_device_name(self, port): + return (self.DEV_NAME_PREFIX + port['id'])[:self.DEV_NAME_LEN] + + @abc.abstractmethod + def plug(self, network_id, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None): + """Plug in the interface.""" + + @abc.abstractmethod + def unplug(self, device_name, bridge=None, namespace=None, prefix=None): + """Unplug the interface.""" + + +class OVSInterfaceDriver(LinuxInterfaceDriver): + """Driver for creating an internal interface on an OVS bridge.""" + + DEV_NAME_PREFIX = 'tap' + + def _get_tap_name(self, dev_name): + return dev_name + + def _ovs_add_port(self, bridge, device_name, port_id, mac_address, + internal=True): + cmd = ['ovs-vsctl', '--', '--may-exist', + 'add-port', bridge, device_name] + if internal: + cmd += ['--', 'set', 'Interface', device_name, 'type=internal'] + cmd += ['--', 'set', 'Interface', device_name, + 'external-ids:iface-id=%s' % port_id, + '--', 'set', 'Interface', device_name, + 'external-ids:iface-status=active', + '--', 'set', 'Interface', device_name, + 'external-ids:attached-mac=%s' % mac_address] + utils.execute(*cmd, run_as_root=True) + + def plug(self, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None): + """Plug in the interface.""" + if not bridge: + bridge = self.conf.ovs_integration_bridge + + self.check_bridge_exists(bridge) + ip = ip_lib.IPWrapper() + ns_dev = ip.device(device_name) + + if not ip_lib.device_exists(device_name, + namespace=namespace): + + tap_name = self._get_tap_name(device_name) + self._ovs_add_port(bridge, tap_name, port_id, mac_address) + ns_dev.link.set_address(mac_address) + + # Add an interface created by ovs to the namespace. + if namespace: + namespace_obj = ip.ensure_namespace(namespace) + namespace_obj.add_device_to_namespace(ns_dev) + + else: + LOG.warn(_("Device %s already exists"), device_name) + ns_dev.link.set_up() + + def unplug(self, device_name, bridge=None, namespace=None, prefix=None): + """Unplug the interface.""" + if not bridge: + bridge = self.conf.ovs_integration_bridge + + tap_name = self._get_tap_name(device_name) + self.check_bridge_exists(bridge) + ovs = ovs_lib.OVSBridge(bridge) + + try: + ovs.delete_port(tap_name) + except RuntimeError: + LOG.error(_("Failed unplugging interface '%s'"), + device_name) + + +class BridgeInterfaceDriver(LinuxInterfaceDriver): + """Driver for creating bridge interfaces.""" + + DEV_NAME_PREFIX = 'ns-' + + def plug(self, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None): + """Plugin the interface.""" + ip = ip_lib.IPWrapper() + if prefix: + tap_name = device_name.replace(prefix, 'tap') + else: + tap_name = device_name.replace(self.DEV_NAME_PREFIX, 'tap') + + if not ip_lib.device_exists(device_name, + namespace=namespace): + # Create ns_veth in a namespace if one is configured. + root_veth, ns_veth = ip.add_veth(tap_name, device_name, + namespace2=namespace) + ns_veth.link.set_address(mac_address) + + else: + ns_veth = ip.device(device_name) + root_veth = ip.device(tap_name) + LOG.warn(_("Device %s already exists"), device_name) + + root_veth.link.set_up() + ns_veth.link.set_up() + + def unplug(self, device_name, bridge=None, namespace=None, prefix=None): + """Unplug the interface.""" + device = ip_lib.IPDevice(device_name, namespace) + try: + device.link.delete() + LOG.debug(_("Unplugged interface '%s'"), device_name) + except RuntimeError: + LOG.error(_("Failed unplugging interface '%s'"), + device_name) diff --git a/manila/network/linux/ip_lib.py b/manila/network/linux/ip_lib.py new file mode 100644 index 0000000000..d1c0bada24 --- /dev/null +++ b/manila/network/linux/ip_lib.py @@ -0,0 +1,422 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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 netaddr + +from manila import utils + + +LOOPBACK_DEVNAME = 'lo' + + +class SubProcessBase(object): + def __init__(self, namespace=None): + self.namespace = namespace + + def _run(self, options, command, args): + if self.namespace: + return self._as_root(options, command, args) + else: + return self._execute(options, command, args) + + def _as_root(self, options, command, args, use_root_namespace=False): + namespace = self.namespace if not use_root_namespace else None + + return self._execute(options, command, args, namespace, as_root=True) + + @classmethod + def _execute(cls, options, command, args, namespace=None, as_root=False): + opt_list = ['-%s' % o for o in options] + if namespace: + ip_cmd = ['ip', 'netns', 'exec', namespace, 'ip'] + else: + ip_cmd = ['ip'] + total_cmd = ip_cmd + opt_list + [command] + list(args) + return utils.execute(*total_cmd, run_as_root=as_root)[0] + + +class IPWrapper(SubProcessBase): + def __init__(self, namespace=None): + super(IPWrapper, self).__init__(namespace=namespace) + self.netns = IpNetnsCommand(self) + + def device(self, name): + return IPDevice(name, self.namespace) + + def get_devices(self, exclude_loopback=False): + retval = [] + output = self._execute('o', 'link', ('list',), self.namespace) + for line in output.split('\n'): + if '<' not in line: + continue + tokens = line.split(':', 2) + if len(tokens) >= 3: + name = tokens[1].split('@', 1)[0].strip() + + if exclude_loopback and name == LOOPBACK_DEVNAME: + continue + + retval.append(IPDevice(name, self.namespace)) + return retval + + def add_tuntap(self, name, mode='tap'): + self._as_root('', 'tuntap', ('add', name, 'mode', mode)) + return IPDevice(name, self.namespace) + + def add_veth(self, name1, name2, namespace2=None): + args = ['add', name1, 'type', 'veth', 'peer', 'name', name2] + + if namespace2 is None: + namespace2 = self.namespace + else: + self.ensure_namespace(namespace2) + args += ['netns', namespace2] + + self._as_root('', 'link', tuple(args)) + + return (IPDevice(name1, self.namespace), IPDevice(name2, namespace2)) + + def ensure_namespace(self, name): + if not self.netns.exists(name): + ip = self.netns.add(name) + lo = ip.device(LOOPBACK_DEVNAME) + lo.link.set_up() + else: + ip = IPWrapper(name) + return ip + + def namespace_is_empty(self): + return not self.get_devices(exclude_loopback=True) + + def garbage_collect_namespace(self): + """Conditionally destroy the namespace if it is empty.""" + if self.namespace and self.netns.exists(self.namespace): + if self.namespace_is_empty(): + self.netns.delete(self.namespace) + return True + return False + + def add_device_to_namespace(self, device): + if self.namespace: + device.link.set_netns(self.namespace) + + @classmethod + def get_namespaces(cls): + output = cls._execute('', 'netns', ('list',)) + return [l.strip() for l in output.split('\n')] + + +class IPDevice(SubProcessBase): + def __init__(self, name, namespace=None): + super(IPDevice, self).__init__(namespace=namespace) + self.name = name + self.link = IpLinkCommand(self) + self.addr = IpAddrCommand(self) + self.route = IpRouteCommand(self) + + def __eq__(self, other): + return (other is not None and self.name == other.name + and self.namespace == other.namespace) + + def __str__(self): + return self.name + + +class IpCommandBase(object): + COMMAND = '' + + def __init__(self, parent): + self._parent = parent + + def _run(self, *args, **kwargs): + return self._parent._run(kwargs.get('options', []), self.COMMAND, args) + + def _as_root(self, *args, **kwargs): + return self._parent._as_root(kwargs.get('options', []), + self.COMMAND, + args, + kwargs.get('use_root_namespace', False)) + + +class IpDeviceCommandBase(IpCommandBase): + @property + def name(self): + return self._parent.name + + +class IpLinkCommand(IpDeviceCommandBase): + COMMAND = 'link' + + def set_address(self, mac_address): + self._as_root('set', self.name, 'address', mac_address) + + def set_mtu(self, mtu_size): + self._as_root('set', self.name, 'mtu', mtu_size) + + def set_up(self): + self._as_root('set', self.name, 'up') + + def set_down(self): + self._as_root('set', self.name, 'down') + + def set_netns(self, namespace): + self._as_root('set', self.name, 'netns', namespace) + self._parent.namespace = namespace + + def set_name(self, name): + self._as_root('set', self.name, 'name', name) + self._parent.name = name + + def set_alias(self, alias_name): + self._as_root('set', self.name, 'alias', alias_name) + + def delete(self): + self._as_root('delete', self.name) + + @property + def address(self): + return self.attributes.get('link/ether') + + @property + def state(self): + return self.attributes.get('state') + + @property + def mtu(self): + return self.attributes.get('mtu') + + @property + def qdisc(self): + return self.attributes.get('qdisc') + + @property + def qlen(self): + return self.attributes.get('qlen') + + @property + def alias(self): + return self.attributes.get('alias') + + @property + def attributes(self): + return self._parse_line(self._run('show', self.name, options='o')) + + def _parse_line(self, value): + if not value: + return {} + + device_name, settings = value.replace("\\", '').split('>', 1) + tokens = settings.split() + keys = tokens[::2] + values = [int(v) if v.isdigit() else v for v in tokens[1::2]] + + retval = dict(zip(keys, values)) + return retval + + +class IpAddrCommand(IpDeviceCommandBase): + COMMAND = 'addr' + + def add(self, ip_version, cidr, broadcast, scope='global'): + self._as_root('add', + cidr, + 'brd', + broadcast, + 'scope', + scope, + 'dev', + self.name, + options=[ip_version]) + + def delete(self, ip_version, cidr): + self._as_root('del', + cidr, + 'dev', + self.name, + options=[ip_version]) + + def flush(self): + self._as_root('flush', self.name) + + def list(self, scope=None, to=None, filters=None): + if filters is None: + filters = [] + + retval = [] + + if scope: + filters += ['scope', scope] + if to: + filters += ['to', to] + + for line in self._run('show', self.name, *filters).split('\n'): + line = line.strip() + if not line.startswith('inet'): + continue + parts = line.split() + if parts[0] == 'inet6': + version = 6 + scope = parts[3] + broadcast = '::' + else: + version = 4 + if parts[2] == 'brd': + broadcast = parts[3] + scope = parts[5] + else: + # sometimes output of 'ip a' might look like: + # inet 192.168.100.100/24 scope global eth0 + # and broadcast needs to be calculated from CIDR + broadcast = str(netaddr.IPNetwork(parts[1]).broadcast) + scope = parts[3] + + retval.append(dict(cidr=parts[1], + broadcast=broadcast, + scope=scope, + ip_version=version, + dynamic=('dynamic' == parts[-1]))) + return retval + + +class IpRouteCommand(IpDeviceCommandBase): + COMMAND = 'route' + + def add_gateway(self, gateway, metric=None): + args = ['replace', 'default', 'via', gateway] + if metric: + args += ['metric', metric] + args += ['dev', self.name] + self._as_root(*args) + + def delete_gateway(self, gateway): + self._as_root('del', + 'default', + 'via', + gateway, + 'dev', + self.name) + + def get_gateway(self, scope=None, filters=None): + if filters is None: + filters = [] + + retval = None + + if scope: + filters += ['scope', scope] + + route_list_lines = self._run('list', 'dev', self.name, + *filters).split('\n') + default_route_line = next((x.strip() for x in + route_list_lines if + x.strip().startswith('default')), None) + if default_route_line: + gateway_index = 2 + parts = default_route_line.split() + retval = dict(gateway=parts[gateway_index]) + metric_index = 4 + parts_has_metric = (len(parts) > metric_index) + if parts_has_metric: + retval.update(metric=int(parts[metric_index])) + + return retval + + def pullup_route(self, interface_name): + """Ensures that the route entry for the interface is before all + others on the same subnet. + """ + device_list = [] + device_route_list_lines = self._run('list', 'proto', 'kernel', + 'dev', interface_name).split('\n') + for device_route_line in device_route_list_lines: + try: + subnet = device_route_line.split()[0] + except Exception: + continue + subnet_route_list_lines = self._run('list', 'proto', 'kernel', + 'match', subnet).split('\n') + for subnet_route_line in subnet_route_list_lines: + i = iter(subnet_route_line.split()) + while(i.next() != 'dev'): + pass + device = i.next() + try: + while(i.next() != 'src'): + pass + src = i.next() + except Exception: + src = '' + if device != interface_name: + device_list.append((device, src)) + else: + break + + for (device, src) in device_list: + self._as_root('del', subnet, 'dev', device) + if (src != ''): + self._as_root('append', subnet, 'proto', 'kernel', + 'src', src, 'dev', device) + else: + self._as_root('append', subnet, 'proto', 'kernel', + 'dev', device) + + +class IpNetnsCommand(IpCommandBase): + COMMAND = 'netns' + + def add(self, name): + self._as_root('add', name, use_root_namespace=True) + return IPWrapper(name) + + def delete(self, name): + self._as_root('delete', name, use_root_namespace=True) + + def execute(self, cmds, addl_env={}, check_exit_code=True): + if not self._parent.namespace: + raise Exception(_('No namespace defined for parent')) + else: + env_params = [] + if addl_env: + env_params = (['env'] + + ['%s=%s' % pair for pair in addl_env.items()]) + total_cmd = ['ip', 'netns', 'exec', self._parent.namespace] + \ + env_params + list(cmds) + return utils.execute(*total_cmd, run_as_root=True, + check_exit_code=check_exit_code) + + def exists(self, name): + output = self._as_root('list', options='o', use_root_namespace=True) + + for line in output.split('\n'): + if name == line.strip(): + return True + return False + + +def device_exists(device_name, namespace=None): + try: + address = IPDevice(device_name, namespace).link.address + except Exception as e: + if 'does not exist' in str(e): + return False + raise + return bool(address) + + +def iproute_arg_supported(command, arg): + command += ['help'] + stdout, stderr = utils.execute(command, check_exit_code=False, + return_stderr=True) + return any(arg in line for line in stderr.split('\n')) diff --git a/manila/network/linux/ovs_lib.py b/manila/network/linux/ovs_lib.py new file mode 100644 index 0000000000..216a45aa70 --- /dev/null +++ b/manila/network/linux/ovs_lib.py @@ -0,0 +1,56 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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 manila.openstack.common import log as logging +from manila import utils + +LOG = logging.getLogger(__name__) + + +class OVSBridge: + def __init__(self, br_name): + self.br_name = br_name + self.re_id = self.re_compile_id() + + def re_compile_id(self): + external = 'external_ids\s*' + mac = 'attached-mac="(?P([a-fA-F\d]{2}:){5}([a-fA-F\d]{2}))"' + iface = 'iface-id="(?P[^"]+)"' + name = 'name\s*:\s"(?P[^"]*)"' + port = 'ofport\s*:\s(?P-?\d+)' + _re = ('%(external)s:\s{ ( %(mac)s,? | %(iface)s,? | . )* }' + ' \s+ %(name)s \s+ %(port)s' % {'external': external, + 'mac': mac, + 'iface': iface, 'name': name, + 'port': port}) + return re.compile(_re, re.M | re.X) + + def run_vsctl(self, args): + full_args = ["ovs-vsctl", "--timeout=2"] + args + try: + return utils.execute(*full_args, run_as_root=True) + except Exception as e: + LOG.error(_("Unable to execute %(cmd)s. Exception: %(exception)s"), + {'cmd': full_args, 'exception': e}) + + def reset_bridge(self): + self.run_vsctl(["--", "--if-exists", "del-br", self.br_name]) + self.run_vsctl(["add-br", self.br_name]) + + def delete_port(self, port_name): + self.run_vsctl(["--", "--if-exists", "del-port", self.br_name, + port_name]) diff --git a/manila/tests/network/linux/__init__.py b/manila/tests/network/linux/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/network/linux/test_interface.py b/manila/tests/network/linux/test_interface.py new file mode 100644 index 0000000000..289e6ba473 --- /dev/null +++ b/manila/tests/network/linux/test_interface.py @@ -0,0 +1,224 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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 manila.network.linux import interface +from manila.network.linux import ip_lib +from manila import test +from manila.tests import conf_fixture +from manila import utils + + +class BaseChild(interface.LinuxInterfaceDriver): + def plug(*args): + pass + + def unplug(*args): + pass + + +FakeSubnet = { + 'cidr': '192.168.1.1/24', + } + + +FakeAllocation = { + 'subnet': FakeSubnet, + 'ip_address': '192.168.1.2', + 'ip_version': 4, + } + + +FakePort = { + 'id': 'abcdef01-1234-5678-90ab-ba0987654321', + 'fixed_ips': [FakeAllocation], + 'device_id': 'cccccccc-cccc-cccc-cccc-cccccccccccc', + } + + +class TestBase(test.TestCase): + def setUp(self): + super(TestBase, self).setUp() + self.conf = conf_fixture.CONF + self.conf.register_opts(interface.OPTS) + self.ip_dev_p = mock.patch.object(ip_lib, 'IPDevice') + self.ip_dev = self.ip_dev_p.start() + self.ip_p = mock.patch.object(ip_lib, 'IPWrapper') + self.ip = self.ip_p.start() + self.device_exists_p = mock.patch.object(ip_lib, 'device_exists') + self.device_exists = self.device_exists_p.start() + + def tearDown(self): + self.ip_dev_p.stop() + self.ip_p.stop() + self.device_exists_p.stop() + super(TestBase, self).tearDown() + + +class TestABCDriver(TestBase): + def test_get_device_name(self): + bc = BaseChild() + device_name = bc.get_device_name(FakePort) + self.assertEqual('tapabcdef01-12', device_name) + + def test_l3_init(self): + addresses = [dict(ip_version=4, scope='global', + dynamic=False, cidr='172.16.77.240/24')] + self.ip_dev().addr.list = mock.Mock(return_value=addresses) + + bc = BaseChild() + ns = '12345678-1234-5678-90ab-ba0987654321' + bc.init_l3('tap0', ['192.168.1.2/24'], namespace=ns) + self.ip_dev.assert_has_calls( + [mock.call('tap0', namespace=ns), + mock.call().addr.list(scope='global', filters=['permanent']), + mock.call().addr.add(4, '192.168.1.2/24', '192.168.1.255'), + mock.call().addr.delete(4, '172.16.77.240/24')]) + + +class TestOVSInterfaceDriver(TestBase): + + def test_get_device_name(self): + br = interface.OVSInterfaceDriver() + device_name = br.get_device_name(FakePort) + self.assertEqual('tapabcdef01-12', device_name) + + def test_plug_no_ns(self): + self._test_plug() + + def test_plug_with_ns(self): + self._test_plug(namespace='01234567-1234-1234-99') + + def test_plug_alt_bridge(self): + self._test_plug(bridge='br-foo') + + def _test_plug(self, additional_expectation=[], bridge=None, + namespace=None): + + if not bridge: + bridge = 'br-int' + + def device_exists(dev, namespace=None): + return dev == bridge + + vsctl_cmd = ['ovs-vsctl', '--', '--may-exist', 'add-port', + bridge, 'tap0', '--', 'set', 'Interface', 'tap0', + 'type=internal', '--', 'set', 'Interface', 'tap0', + 'external-ids:iface-id=port-1234', '--', 'set', + 'Interface', 'tap0', + 'external-ids:iface-status=active', '--', 'set', + 'Interface', 'tap0', + 'external-ids:attached-mac=aa:bb:cc:dd:ee:ff'] + + with mock.patch.object(utils, 'execute') as execute: + ovs = interface.OVSInterfaceDriver() + self.device_exists.side_effect = device_exists + ovs.plug('port-1234', + 'tap0', + 'aa:bb:cc:dd:ee:ff', + bridge=bridge, + namespace=namespace) + execute.assert_called_once_with(*vsctl_cmd, run_as_root=True) + + expected = [mock.call(), + mock.call().device('tap0'), + mock.call().device().link.set_address('aa:bb:cc:dd:ee:ff')] + expected.extend(additional_expectation) + if namespace: + expected.extend( + [mock.call().ensure_namespace(namespace), + mock.call().ensure_namespace().add_device_to_namespace( + mock.ANY)]) + expected.extend([mock.call().device().link.set_up()]) + + self.ip.assert_has_calls(expected) + + def test_unplug(self, bridge=None): + if not bridge: + bridge = 'br-int' + with mock.patch('manila.network.linux.ovs_lib.OVSBridge') as ovs_br: + ovs = interface.OVSInterfaceDriver() + ovs.unplug('tap0') + ovs_br.assert_has_calls([mock.call(bridge), + mock.call().delete_port('tap0')]) + + +class TestBridgeInterfaceDriver(TestBase): + def test_get_device_name(self): + br = interface.BridgeInterfaceDriver() + device_name = br.get_device_name(FakePort) + self.assertEqual('ns-abcdef01-12', device_name) + + def test_plug_no_ns(self): + self._test_plug() + + def test_plug_with_ns(self): + self._test_plug(namespace='01234567-1234-1234-99') + + def _test_plug(self, namespace=None, mtu=None): + def device_exists(device, root_helper=None, namespace=None): + return device.startswith('brq') + + root_veth = mock.Mock() + ns_veth = mock.Mock() + + self.ip().add_veth = mock.Mock(return_value=(root_veth, ns_veth)) + + self.device_exists.side_effect = device_exists + br = interface.BridgeInterfaceDriver() + mac_address = 'aa:bb:cc:dd:ee:ff' + br.plug('port-1234', + 'ns-0', + mac_address, + namespace=namespace) + + ip_calls = [mock.call(), + mock.call().add_veth('tap0', 'ns-0', namespace2=namespace)] + ns_veth.assert_has_calls([mock.call.link.set_address(mac_address)]) + + self.ip.assert_has_calls(ip_calls) + + root_veth.assert_has_calls([mock.call.link.set_up()]) + ns_veth.assert_has_calls([mock.call.link.set_up()]) + + def test_plug_dev_exists(self): + self.device_exists.return_value = True + with mock.patch('manila.network.linux.interface.LOG.warn') as log: + br = interface.BridgeInterfaceDriver() + br.plug('port-1234', + 'tap0', + 'aa:bb:cc:dd:ee:ff') + self.ip_dev.assert_has_calls([]) + self.assertEqual(log.call_count, 1) + + def test_unplug_no_device(self): + self.device_exists.return_value = False + self.ip_dev().link.delete.side_effect = RuntimeError + with mock.patch('manila.network.linux.interface.LOG') as log: + br = interface.BridgeInterfaceDriver() + br.unplug('tap0') + [mock.call(), mock.call('tap0'), mock.call().link.delete()] + self.assertEqual(log.error.call_count, 1) + + def test_unplug(self): + self.device_exists.return_value = True + with mock.patch('manila.network.linux.interface.LOG.debug') as log: + br = interface.BridgeInterfaceDriver() + br.unplug('tap0') + log.assert_called_once() + + self.ip_dev.assert_has_calls([mock.call('tap0', None), + mock.call().link.delete()]) diff --git a/manila/tests/network/linux/test_ip_lib.py b/manila/tests/network/linux/test_ip_lib.py new file mode 100644 index 0000000000..995946439a --- /dev/null +++ b/manila/tests/network/linux/test_ip_lib.py @@ -0,0 +1,684 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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 manila.network.linux import ip_lib +from manila import test + +NETNS_SAMPLE = [ + '12345678-1234-5678-abcd-1234567890ab', + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + 'cccccccc-cccc-cccc-cccc-cccccccccccc'] + +LINK_SAMPLE = [ + '1: lo: mtu 16436 qdisc noqueue state UNKNOWN \\' + 'link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00', + '2: eth0: mtu 1500 qdisc mq state UP ' + 'qlen 1000\ link/ether cc:dd:ee:ff:ab:cd brd ff:ff:ff:ff:ff:ff' + '\ alias openvswitch', + '3: br-int: mtu 1500 qdisc noop state DOWN ' + '\ link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff', + '4: gw-ddc717df-49: mtu 1500 qdisc noop ' + 'state DOWN \ link/ether fe:dc:ba:fe:dc:ba brd ff:ff:ff:ff:ff:ff', + '5: eth0.50@eth0: mtu 1500 qdisc ' + ' noqueue master brq0b24798c-07 state UP mode DEFAULT' + '\ link/ether ab:04:49:b6:ab:a0 brd ff:ff:ff:ff:ff:ff'] + +ADDR_SAMPLE = (""" +2: eth0: mtu 1500 qdisc mq state UP qlen 1000 + link/ether dd:cc:aa:b9:76:ce brd ff:ff:ff:ff:ff:ff + inet 172.16.77.240/24 brd 172.16.77.255 scope global eth0 + inet6 2001:470:9:1224:5595:dd51:6ba2:e788/64 scope global temporary dynamic + valid_lft 14187sec preferred_lft 3387sec + inet6 2001:470:9:1224:fd91:272:581e:3a32/64 scope global temporary """ + """deprecated dynamic + valid_lft 14187sec preferred_lft 0sec + inet6 2001:470:9:1224:4508:b885:5fb:740b/64 scope global temporary """ + """deprecated dynamic + valid_lft 14187sec preferred_lft 0sec + inet6 2001:470:9:1224:dfcc:aaff:feb9:76ce/64 scope global dynamic + valid_lft 14187sec preferred_lft 3387sec + inet6 fe80::dfcc:aaff:feb9:76ce/64 scope link + valid_lft forever preferred_lft forever +""") + +ADDR_SAMPLE2 = (""" +2: eth0: mtu 1500 qdisc mq state UP qlen 1000 + link/ether dd:cc:aa:b9:76:ce brd ff:ff:ff:ff:ff:ff + inet 172.16.77.240/24 scope global eth0 + inet6 2001:470:9:1224:5595:dd51:6ba2:e788/64 scope global temporary dynamic + valid_lft 14187sec preferred_lft 3387sec + inet6 2001:470:9:1224:fd91:272:581e:3a32/64 scope global temporary """ + """deprecated dynamic + valid_lft 14187sec preferred_lft 0sec + inet6 2001:470:9:1224:4508:b885:5fb:740b/64 scope global temporary """ + """deprecated dynamic + valid_lft 14187sec preferred_lft 0sec + inet6 2001:470:9:1224:dfcc:aaff:feb9:76ce/64 scope global dynamic + valid_lft 14187sec preferred_lft 3387sec + inet6 fe80::dfcc:aaff:feb9:76ce/64 scope link + valid_lft forever preferred_lft forever +""") + +GATEWAY_SAMPLE1 = (""" +default via 10.35.19.254 metric 100 +10.35.16.0/22 proto kernel scope link src 10.35.17.97 +""") + +GATEWAY_SAMPLE2 = (""" +default via 10.35.19.254 metric 100 +""") + +GATEWAY_SAMPLE3 = (""" +10.35.16.0/22 proto kernel scope link src 10.35.17.97 +""") + +GATEWAY_SAMPLE4 = (""" +default via 10.35.19.254 +""") + +DEVICE_ROUTE_SAMPLE = ("10.0.0.0/24 scope link src 10.0.0.2") + +SUBNET_SAMPLE1 = ("10.0.0.0/24 dev qr-23380d11-d2 scope link src 10.0.0.1\n" + "10.0.0.0/24 dev tap1d7888a7-10 scope link src 10.0.0.2") +SUBNET_SAMPLE2 = ("10.0.0.0/24 dev tap1d7888a7-10 scope link src 10.0.0.2\n" + "10.0.0.0/24 dev qr-23380d11-d2 scope link src 10.0.0.1") + + +class TestSubProcessBase(test.TestCase): + def setUp(self): + super(TestSubProcessBase, self).setUp() + self.execute_p = mock.patch('manila.utils.execute') + self.execute = self.execute_p.start() + + def tearDown(self): + self.execute_p.stop() + super(TestSubProcessBase, self).tearDown() + + def test_execute_wrapper(self): + ip_lib.SubProcessBase._execute('o', 'link', ('list',)) + + self.execute.assert_called_once_with('ip', '-o', 'link', 'list', + run_as_root=False) + + def test_execute_wrapper_int_options(self): + ip_lib.SubProcessBase._execute([4], 'link', ('list',)) + + self.execute.assert_called_once_with('ip', '-4', 'link', 'list', + run_as_root=False) + + def test_execute_wrapper_no_options(self): + ip_lib.SubProcessBase._execute([], 'link', ('list',)) + + self.execute.assert_called_once_with('ip', 'link', 'list', + run_as_root=False) + + def test_run_no_namespace(self): + base = ip_lib.SubProcessBase() + base._run([], 'link', ('list',)) + self.execute.assert_called_once_with('ip', 'link', 'list', + run_as_root=False) + + def test_run_namespace(self): + base = ip_lib.SubProcessBase('ns') + base._run([], 'link', ('list',)) + self.execute.assert_called_once_with('ip', 'netns', 'exec', 'ns', + 'ip', 'link', 'list', + run_as_root=True) + + def test_as_root_namespace(self): + base = ip_lib.SubProcessBase('ns') + base._as_root([], 'link', ('list',)) + self.execute.assert_called_once_with('ip', 'netns', 'exec', 'ns', + 'ip', 'link', 'list', + run_as_root=True) + + +class TestIpWrapper(test.TestCase): + def setUp(self): + super(TestIpWrapper, self).setUp() + self.execute_p = mock.patch.object(ip_lib.IPWrapper, '_execute') + self.execute = self.execute_p.start() + + def tearDown(self): + self.execute_p.stop() + super(TestIpWrapper, self).tearDown() + + def test_get_devices(self): + self.execute.return_value = '\n'.join(LINK_SAMPLE) + retval = ip_lib.IPWrapper().get_devices() + self.assertEqual(retval, + [ip_lib.IPDevice('lo'), + ip_lib.IPDevice('eth0'), + ip_lib.IPDevice('br-int'), + ip_lib.IPDevice('gw-ddc717df-49'), + ip_lib.IPDevice('eth0.50')]) + + self.execute.assert_called_once_with('o', 'link', ('list',), None) + + def test_get_devices_malformed_line(self): + self.execute.return_value = '\n'.join(LINK_SAMPLE + ['gibberish']) + retval = ip_lib.IPWrapper().get_devices() + self.assertEqual(retval, + [ip_lib.IPDevice('lo'), + ip_lib.IPDevice('eth0'), + ip_lib.IPDevice('br-int'), + ip_lib.IPDevice('gw-ddc717df-49'), + ip_lib.IPDevice('eth0.50')]) + + self.execute.assert_called_once_with('o', 'link', ('list',), None) + + def test_get_namespaces(self): + self.execute.return_value = '\n'.join(NETNS_SAMPLE) + retval = ip_lib.IPWrapper.get_namespaces() + self.assertEqual(retval, + ['12345678-1234-5678-abcd-1234567890ab', + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + 'cccccccc-cccc-cccc-cccc-cccccccccccc']) + + self.execute.assert_called_once_with('', 'netns', ('list',)) + + def test_add_tuntap(self): + ip_lib.IPWrapper().add_tuntap('tap0') + self.execute.assert_called_once_with('', 'tuntap', + ('add', 'tap0', 'mode', 'tap'), + None, as_root=True) + + def test_add_veth(self): + ip_lib.IPWrapper().add_veth('tap0', 'tap1') + self.execute.assert_called_once_with('', 'link', + ('add', 'tap0', 'type', 'veth', + 'peer', 'name', 'tap1'), + None, as_root=True) + + def test_add_veth_with_namespaces(self): + ns2 = 'ns2' + with mock.patch.object(ip_lib.IPWrapper, 'ensure_namespace') as en: + ip_lib.IPWrapper().add_veth('tap0', 'tap1', namespace2=ns2) + en.assert_has_calls([mock.call(ns2)]) + self.execute.assert_called_once_with('', 'link', + ('add', 'tap0', 'type', 'veth', + 'peer', 'name', 'tap1', + 'netns', ns2), + None, as_root=True) + + def test_get_device(self): + dev = ip_lib.IPWrapper('ns').device('eth0') + self.assertEqual(dev.namespace, 'ns') + self.assertEqual(dev.name, 'eth0') + + def test_ensure_namespace(self): + with mock.patch.object(ip_lib, 'IPDevice') as ip_dev: + ip = ip_lib.IPWrapper() + with mock.patch.object(ip.netns, 'exists') as ns_exists: + ns_exists.return_value = False + ip.ensure_namespace('ns') + self.execute.assert_has_calls( + [mock.call([], 'netns', ('add', 'ns'), None, + as_root=True)]) + ip_dev.assert_has_calls([mock.call('lo', 'ns'), + mock.call().link.set_up()]) + + def test_ensure_namespace_existing(self): + with mock.patch.object(ip_lib, 'IpNetnsCommand') as ip_ns_cmd: + ip_ns_cmd.exists.return_value = True + ns = ip_lib.IPWrapper().ensure_namespace('ns') + self.assertFalse(self.execute.called) + self.assertEqual(ns.namespace, 'ns') + + def test_namespace_is_empty_no_devices(self): + ip = ip_lib.IPWrapper('ns') + with mock.patch.object(ip, 'get_devices') as get_devices: + get_devices.return_value = [] + + self.assertTrue(ip.namespace_is_empty()) + get_devices.assert_called_once_with(exclude_loopback=True) + + def test_namespace_is_empty(self): + ip = ip_lib.IPWrapper('ns') + with mock.patch.object(ip, 'get_devices') as get_devices: + get_devices.return_value = [mock.Mock()] + + self.assertFalse(ip.namespace_is_empty()) + get_devices.assert_called_once_with(exclude_loopback=True) + + def test_garbage_collect_namespace_does_not_exist(self): + with mock.patch.object(ip_lib, 'IpNetnsCommand') as ip_ns_cmd_cls: + ip_ns_cmd_cls.return_value.exists.return_value = False + ip = ip_lib.IPWrapper('ns') + with mock.patch.object(ip, 'namespace_is_empty') as mock_is_empty: + self.assertFalse(ip.garbage_collect_namespace()) + ip_ns_cmd_cls.assert_has_calls([mock.call().exists('ns')]) + self.assertNotIn(mock.call().delete('ns'), + ip_ns_cmd_cls.return_value.mock_calls) + self.assertEqual(mock_is_empty.mock_calls, []) + + def test_garbage_collect_namespace_existing_empty_ns(self): + with mock.patch.object(ip_lib, 'IpNetnsCommand') as ip_ns_cmd_cls: + ip_ns_cmd_cls.return_value.exists.return_value = True + + ip = ip_lib.IPWrapper('ns') + + with mock.patch.object(ip, 'namespace_is_empty') as mock_is_empty: + mock_is_empty.return_value = True + self.assertTrue(ip.garbage_collect_namespace()) + + mock_is_empty.assert_called_once_with() + expected = [mock.call().exists('ns'), + mock.call().delete('ns')] + ip_ns_cmd_cls.assert_has_calls(expected) + + def test_garbage_collect_namespace_existing_not_empty(self): + lo_device = mock.Mock() + lo_device.name = 'lo' + tap_device = mock.Mock() + tap_device.name = 'tap1' + + with mock.patch.object(ip_lib, 'IpNetnsCommand') as ip_ns_cmd_cls: + ip_ns_cmd_cls.return_value.exists.return_value = True + + ip = ip_lib.IPWrapper('ns') + + with mock.patch.object(ip, 'namespace_is_empty') as mock_is_empty: + mock_is_empty.return_value = False + + self.assertFalse(ip.garbage_collect_namespace()) + + mock_is_empty.assert_called_once_with() + expected = [mock.call(ip), + mock.call().exists('ns')] + self.assertEqual(ip_ns_cmd_cls.mock_calls, expected) + self.assertNotIn(mock.call().delete('ns'), + ip_ns_cmd_cls.mock_calls) + + def test_add_device_to_namespace(self): + dev = mock.Mock() + ip_lib.IPWrapper('ns').add_device_to_namespace(dev) + dev.assert_has_calls([mock.call.link.set_netns('ns')]) + + def test_add_device_to_namespace_is_none(self): + dev = mock.Mock() + ip_lib.IPWrapper().add_device_to_namespace(dev) + self.assertEqual(dev.mock_calls, []) + + +class TestIPDevice(test.TestCase): + def test_eq_same_name(self): + dev1 = ip_lib.IPDevice('tap0') + dev2 = ip_lib.IPDevice('tap0') + self.assertEqual(dev1, dev2) + + def test_eq_diff_name(self): + dev1 = ip_lib.IPDevice('tap0') + dev2 = ip_lib.IPDevice('tap1') + self.assertNotEqual(dev1, dev2) + + def test_eq_same_namespace(self): + dev1 = ip_lib.IPDevice('tap0', 'ns1') + dev2 = ip_lib.IPDevice('tap0', 'ns1') + self.assertEqual(dev1, dev2) + + def test_eq_diff_namespace(self): + dev1 = ip_lib.IPDevice('tap0', 'ns1') + dev2 = ip_lib.IPDevice('tap0', 'ns2') + self.assertNotEqual(dev1, dev2) + + def test_eq_other_is_none(self): + dev1 = ip_lib.IPDevice('tap0', 'ns1') + self.assertNotEqual(dev1, None) + + def test_str(self): + self.assertEqual(str(ip_lib.IPDevice('tap0')), 'tap0') + + +class TestIPCommandBase(test.TestCase): + def setUp(self): + super(TestIPCommandBase, self).setUp() + self.ip = mock.Mock() + self.ip.namespace = 'namespace' + self.ip_cmd = ip_lib.IpCommandBase(self.ip) + self.ip_cmd.COMMAND = 'foo' + + def test_run(self): + self.ip_cmd._run('link', 'show') + self.ip.assert_has_calls([mock.call._run([], 'foo', ('link', 'show'))]) + + def test_run_with_options(self): + self.ip_cmd._run('link', options='o') + self.ip.assert_has_calls([mock.call._run('o', 'foo', ('link', ))]) + + def test_as_root(self): + self.ip_cmd._as_root('link') + self.ip.assert_has_calls( + [mock.call._as_root([], 'foo', ('link', ), False)]) + + def test_as_root_with_options(self): + self.ip_cmd._as_root('link', options='o') + self.ip.assert_has_calls( + [mock.call._as_root('o', 'foo', ('link', ), False)]) + + +class TestIPDeviceCommandBase(test.TestCase): + def setUp(self): + super(TestIPDeviceCommandBase, self).setUp() + self.ip_dev = mock.Mock() + self.ip_dev.name = 'eth0' + self.ip_dev._execute = mock.Mock(return_value='executed') + self.ip_cmd = ip_lib.IpDeviceCommandBase(self.ip_dev) + self.ip_cmd.COMMAND = 'foo' + + def test_name_property(self): + self.assertEqual(self.ip_cmd.name, 'eth0') + + +class TestIPCmdBase(test.TestCase): + def setUp(self): + super(TestIPCmdBase, self).setUp() + self.parent = mock.Mock() + self.parent.name = 'eth0' + + def _assert_call(self, options, args): + self.parent.assert_has_calls([ + mock.call._run(options, self.command, args)]) + + def _assert_sudo(self, options, args, force_root_namespace=False): + self.parent.assert_has_calls( + [mock.call._as_root(options, self.command, args, + force_root_namespace)]) + + +class TestIpLinkCommand(TestIPCmdBase): + def setUp(self): + super(TestIpLinkCommand, self).setUp() + self.parent._run.return_value = LINK_SAMPLE[1] + self.command = 'link' + self.link_cmd = ip_lib.IpLinkCommand(self.parent) + + def test_set_address(self): + self.link_cmd.set_address('aa:bb:cc:dd:ee:ff') + self._assert_sudo([], ('set', 'eth0', 'address', 'aa:bb:cc:dd:ee:ff')) + + def test_set_mtu(self): + self.link_cmd.set_mtu(1500) + self._assert_sudo([], ('set', 'eth0', 'mtu', 1500)) + + def test_set_up(self): + self.link_cmd.set_up() + self._assert_sudo([], ('set', 'eth0', 'up')) + + def test_set_down(self): + self.link_cmd.set_down() + self._assert_sudo([], ('set', 'eth0', 'down')) + + def test_set_netns(self): + self.link_cmd.set_netns('foo') + self._assert_sudo([], ('set', 'eth0', 'netns', 'foo')) + self.assertEqual(self.parent.namespace, 'foo') + + def test_set_name(self): + self.link_cmd.set_name('tap1') + self._assert_sudo([], ('set', 'eth0', 'name', 'tap1')) + self.assertEqual(self.parent.name, 'tap1') + + def test_set_alias(self): + self.link_cmd.set_alias('openvswitch') + self._assert_sudo([], ('set', 'eth0', 'alias', 'openvswitch')) + + def test_delete(self): + self.link_cmd.delete() + self._assert_sudo([], ('delete', 'eth0')) + + def test_address_property(self): + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.address, 'cc:dd:ee:ff:ab:cd') + + def test_mtu_property(self): + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.mtu, 1500) + + def test_qdisc_property(self): + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.qdisc, 'mq') + + def test_qlen_property(self): + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.qlen, 1000) + + def test_alias_property(self): + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.alias, 'openvswitch') + + def test_state_property(self): + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.state, 'UP') + + def test_settings_property(self): + expected = {'mtu': 1500, + 'qlen': 1000, + 'state': 'UP', + 'qdisc': 'mq', + 'brd': 'ff:ff:ff:ff:ff:ff', + 'link/ether': 'cc:dd:ee:ff:ab:cd', + 'alias': 'openvswitch'} + self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1]) + self.assertEqual(self.link_cmd.attributes, expected) + self._assert_call('o', ('show', 'eth0')) + + +class TestIpAddrCommand(TestIPCmdBase): + def setUp(self): + super(TestIpAddrCommand, self).setUp() + self.parent.name = 'tap0' + self.command = 'addr' + self.addr_cmd = ip_lib.IpAddrCommand(self.parent) + + def test_add_address(self): + self.addr_cmd.add(4, '192.168.45.100/24', '192.168.45.255') + self._assert_sudo([4], + ('add', '192.168.45.100/24', 'brd', '192.168.45.255', + 'scope', 'global', 'dev', 'tap0')) + + def test_add_address_scoped(self): + self.addr_cmd.add(4, '192.168.45.100/24', '192.168.45.255', + scope='link') + self._assert_sudo([4], + ('add', '192.168.45.100/24', 'brd', '192.168.45.255', + 'scope', 'link', 'dev', 'tap0')) + + def test_del_address(self): + self.addr_cmd.delete(4, '192.168.45.100/24') + self._assert_sudo([4], + ('del', '192.168.45.100/24', 'dev', 'tap0')) + + def test_flush(self): + self.addr_cmd.flush() + self._assert_sudo([], ('flush', 'tap0')) + + def test_list(self): + expected = [ + dict(ip_version=4, scope='global', + dynamic=False, cidr='172.16.77.240/24', + broadcast='172.16.77.255'), + dict(ip_version=6, scope='global', + dynamic=True, cidr='2001:470:9:1224:5595:dd51:6ba2:e788/64', + broadcast='::'), + dict(ip_version=6, scope='global', + dynamic=True, cidr='2001:470:9:1224:fd91:272:581e:3a32/64', + broadcast='::'), + dict(ip_version=6, scope='global', + dynamic=True, cidr='2001:470:9:1224:4508:b885:5fb:740b/64', + broadcast='::'), + dict(ip_version=6, scope='global', + dynamic=True, cidr='2001:470:9:1224:dfcc:aaff:feb9:76ce/64', + broadcast='::'), + dict(ip_version=6, scope='link', + dynamic=False, cidr='fe80::dfcc:aaff:feb9:76ce/64', + broadcast='::')] + + test_cases = [ADDR_SAMPLE, ADDR_SAMPLE2] + + for test_case in test_cases: + self.parent._run = mock.Mock(return_value=test_case) + self.assertEqual(self.addr_cmd.list(), expected) + self._assert_call([], ('show', 'tap0')) + + def test_list_filtered(self): + expected = [ + dict(ip_version=4, scope='global', + dynamic=False, cidr='172.16.77.240/24', + broadcast='172.16.77.255')] + + test_cases = [ADDR_SAMPLE, ADDR_SAMPLE2] + + for test_case in test_cases: + output = '\n'.join(test_case.split('\n')[0:4]) + self.parent._run.return_value = output + self.assertEqual(self.addr_cmd.list('global', + filters=['permanent']), expected) + self._assert_call([], ('show', 'tap0', 'permanent', 'scope', + 'global')) + + +class TestIpRouteCommand(TestIPCmdBase): + def setUp(self): + super(TestIpRouteCommand, self).setUp() + self.parent.name = 'eth0' + self.command = 'route' + self.route_cmd = ip_lib.IpRouteCommand(self.parent) + + def test_add_gateway(self): + gateway = '192.168.45.100' + metric = 100 + self.route_cmd.add_gateway(gateway, metric) + self._assert_sudo([], + ('replace', 'default', 'via', gateway, + 'metric', metric, + 'dev', self.parent.name)) + + def test_del_gateway(self): + gateway = '192.168.45.100' + self.route_cmd.delete_gateway(gateway) + self._assert_sudo([], + ('del', 'default', 'via', gateway, + 'dev', self.parent.name)) + + def test_get_gateway(self): + test_cases = [{'sample': GATEWAY_SAMPLE1, + 'expected': {'gateway': '10.35.19.254', + 'metric': 100}}, + {'sample': GATEWAY_SAMPLE2, + 'expected': {'gateway': '10.35.19.254', + 'metric': 100}}, + {'sample': GATEWAY_SAMPLE3, + 'expected': None}, + {'sample': GATEWAY_SAMPLE4, + 'expected': {'gateway': '10.35.19.254'}}] + for test_case in test_cases: + self.parent._run = mock.Mock(return_value=test_case['sample']) + self.assertEqual(self.route_cmd.get_gateway(), + test_case['expected']) + + def test_pullup_route(self): + # interface is not the first in the list - requires + # deleting and creating existing entries + output = [DEVICE_ROUTE_SAMPLE, SUBNET_SAMPLE1] + + def pullup_side_effect(self, *args): + result = output.pop(0) + return result + + self.parent._run = mock.Mock(side_effect=pullup_side_effect) + self.route_cmd.pullup_route('tap1d7888a7-10') + self._assert_sudo([], ('del', '10.0.0.0/24', 'dev', 'qr-23380d11-d2')) + self._assert_sudo([], ('append', '10.0.0.0/24', 'proto', 'kernel', + 'src', '10.0.0.1', 'dev', 'qr-23380d11-d2')) + + def test_pullup_route_first(self): + # interface is first in the list - no changes + output = [DEVICE_ROUTE_SAMPLE, SUBNET_SAMPLE2] + + def pullup_side_effect(self, *args): + result = output.pop(0) + return result + + self.parent._run = mock.Mock(side_effect=pullup_side_effect) + self.route_cmd.pullup_route('tap1d7888a7-10') + # Check two calls - device get and subnet get + self.assertEqual(len(self.parent._run.mock_calls), 2) + + +class TestIpNetnsCommand(TestIPCmdBase): + def setUp(self): + super(TestIpNetnsCommand, self).setUp() + self.command = 'netns' + self.netns_cmd = ip_lib.IpNetnsCommand(self.parent) + + def test_add_namespace(self): + ns = self.netns_cmd.add('ns') + self._assert_sudo([], ('add', 'ns'), force_root_namespace=True) + self.assertEqual(ns.namespace, 'ns') + + def test_delete_namespace(self): + with mock.patch('manila.utils.execute'): + self.netns_cmd.delete('ns') + self._assert_sudo([], ('delete', 'ns'), force_root_namespace=True) + + def test_namespace_exists(self): + retval = '\n'.join(NETNS_SAMPLE) + self.parent._as_root.return_value = retval + self.assertTrue( + self.netns_cmd.exists('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')) + self._assert_sudo('o', ('list',), force_root_namespace=True) + + def test_namespace_doest_not_exist(self): + retval = '\n'.join(NETNS_SAMPLE) + self.parent._as_root.return_value = retval + self.assertFalse( + self.netns_cmd.exists('bbbbbbbb-1111-2222-3333-bbbbbbbbbbbb')) + self._assert_sudo('o', ('list',), force_root_namespace=True) + + def test_execute(self): + self.parent.namespace = 'ns' + with mock.patch('manila.utils.execute') as execute: + self.netns_cmd.execute(['ip', 'link', 'list']) + execute.assert_called_once_with('ip', 'netns', 'exec', 'ns', 'ip', + 'link', 'list', + run_as_root=True, + check_exit_code=True) + + def test_execute_env_var_prepend(self): + self.parent.namespace = 'ns' + with mock.patch('manila.utils.execute') as execute: + env = dict(FOO=1, BAR=2) + self.netns_cmd.execute(['ip', 'link', 'list'], env) + execute.assert_called_once_with( + 'ip', 'netns', 'exec', 'ns', 'env', 'FOO=1', 'BAR=2', + 'ip', 'link', 'list', + run_as_root=True, check_exit_code=True) + + +class TestDeviceExists(test.TestCase): + def test_device_exists(self): + with mock.patch.object(ip_lib.IPDevice, '_execute') as _execute: + _execute.return_value = LINK_SAMPLE[1] + self.assertTrue(ip_lib.device_exists('eth0')) + _execute.assert_called_once_with('o', 'link', ('show', 'eth0')) + + def test_device_does_not_exist(self): + with mock.patch.object(ip_lib.IPDevice, '_execute') as _execute: + _execute.return_value = '' + _execute.side_effect = RuntimeError('Device does not exist.') + self.assertFalse(ip_lib.device_exists('eth0')) diff --git a/manila/tests/network/linux/test_ovs_lib.py b/manila/tests/network/linux/test_ovs_lib.py new file mode 100644 index 0000000000..7cd85796ed --- /dev/null +++ b/manila/tests/network/linux/test_ovs_lib.py @@ -0,0 +1,64 @@ +# Copyright 2014 Mirantis Inc. +# +# 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 manila.network.linux import ovs_lib +from manila import test + + +class OVS_Lib_Test(test.TestCase): + """A test suite to excercise the OVS libraries.""" + + def setUp(self): + super(OVS_Lib_Test, self).setUp() + self.BR_NAME = "br-int" + self.TO = "--timeout=2" + + self.br = ovs_lib.OVSBridge(self.BR_NAME) + self.execute_p = mock.patch('manila.utils.execute') + self.execute = self.execute_p.start() + + def tearDown(self): + self.execute_p.stop() + super(OVS_Lib_Test, self).tearDown() + + def test_reset_bridge(self): + self.br.reset_bridge() + self.execute.assert_has_calls([mock.call("ovs-vsctl", self.TO, "--", + "--if-exists", "del-br", self.BR_NAME, run_as_root=True), + mock.call("ovs-vsctl", self.TO, "add-br", + self.BR_NAME, run_as_root=True)]) + + def test_delete_port(self): + pname = "tap5" + self.br.delete_port(pname) + self.execute.assert_called_once_with("ovs-vsctl", self.TO, "--", + "--if-exists", "del-port", self.BR_NAME, pname, + run_as_root=True) + + def test_port_id_regex(self): + result = ('external_ids : {attached-mac="fa:16:3e:23:5b:f2",' + ' iface-id="5c1321a7-c73f-4a77-95e6-9f86402e5c8f",' + ' iface-status=active}\nname :' + ' "dhc5c1321a7-c7"\nofport : 2\n') + match = self.br.re_id.search(result) + vif_mac = match.group('vif_mac') + vif_id = match.group('vif_id') + port_name = match.group('port_name') + ofport = int(match.group('ofport')) + self.assertEqual(vif_mac, 'fa:16:3e:23:5b:f2') + self.assertEqual(vif_id, '5c1321a7-c73f-4a77-95e6-9f86402e5c8f') + self.assertEqual(port_name, 'dhc5c1321a7-c7') + self.assertEqual(ofport, 2)