You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
982 lines
33 KiB
Python
982 lines
33 KiB
Python
# Copyright (c) 2015 Thales Services SAS
|
|
#
|
|
# 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
|
|
from concurrent import futures
|
|
import contextlib
|
|
import os
|
|
import random
|
|
import re
|
|
import select
|
|
import shlex
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
|
|
import fixtures
|
|
import netaddr
|
|
from neutron_lib import constants as n_const
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import uuidutils
|
|
import six
|
|
|
|
from neutron.agent.common import ovs_lib
|
|
from neutron.agent.linux import bridge_lib
|
|
from neutron.agent.linux import interface
|
|
from neutron.agent.linux import ip_lib
|
|
from neutron.agent.linux import iptables_firewall
|
|
from neutron.agent.linux import utils
|
|
from neutron.common import utils as common_utils
|
|
from neutron.conf.agent import common as config
|
|
from neutron.db import db_base_plugin_common
|
|
from neutron.plugins.ml2.drivers.linuxbridge.agent import \
|
|
linuxbridge_neutron_agent as linuxbridge_agent
|
|
from neutron.tests.common import base as common_base
|
|
from neutron.tests import tools
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
UNDEFINED = object()
|
|
|
|
NS_PREFIX = 'test-'
|
|
BR_PREFIX = 'test-br'
|
|
PORT_PREFIX = 'port'
|
|
VETH0_PREFIX = 'test-veth0'
|
|
VETH1_PREFIX = 'test-veth1'
|
|
PATCH_PREFIX = 'patch'
|
|
MACVTAP_PREFIX = 'macvtap'
|
|
|
|
# port name should be shorter than DEVICE_NAME_MAX_LEN because if this
|
|
# port is used to provide vlan connection between two linuxbridge
|
|
# agents then place for vlan ID is also required, Vlan ID can take max 4 digits
|
|
# and there is also additional "." in device name so it will in overall gives
|
|
# DEVICE_NAME_MAX_LEN = 15 chars
|
|
LB_DEVICE_NAME_MAX_LEN = 10
|
|
|
|
SS_SOURCE_PORT_PATTERN = re.compile(
|
|
r'^.*\s+\d+\s+.*:(?P<port>\d+)\s+[^\s]+:.*')
|
|
|
|
READ_TIMEOUT = int(
|
|
os.environ.get('OS_TEST_READ_TIMEOUT', 5))
|
|
|
|
CHILD_PROCESS_TIMEOUT = int(
|
|
os.environ.get('OS_TEST_CHILD_PROCESS_TIMEOUT', 20))
|
|
CHILD_PROCESS_SLEEP = float(
|
|
os.environ.get('OS_TEST_CHILD_PROCESS_SLEEP', 0.5))
|
|
|
|
TRANSPORT_PROTOCOLS = (n_const.PROTO_NAME_TCP, n_const.PROTO_NAME_UDP,
|
|
n_const.PROTO_NAME_SCTP)
|
|
|
|
OVS_MANAGER_TEST_PORT_FIRST = 6610
|
|
OVS_MANAGER_TEST_PORT_LAST = 6639
|
|
|
|
|
|
def increment_ip_cidr(ip_cidr, offset=1):
|
|
"""Increment ip_cidr offset times.
|
|
|
|
example: increment_ip_cidr("1.2.3.4/24", 2) ==> "1.2.3.6/24"
|
|
"""
|
|
net0 = netaddr.IPNetwork(ip_cidr)
|
|
net = netaddr.IPNetwork(ip_cidr)
|
|
net.value += offset
|
|
if not net0.network < net.ip < net0[-1]:
|
|
tools.fail(
|
|
'Incorrect ip_cidr,offset tuple (%s,%s): "incremented" ip_cidr is '
|
|
'outside ip_cidr' % (ip_cidr, offset))
|
|
return str(net)
|
|
|
|
|
|
def set_namespace_gateway(port_dev, gateway_ip):
|
|
"""Set gateway for the namespace associated to the port."""
|
|
if not port_dev.namespace:
|
|
tools.fail('tests should not change test machine gateway')
|
|
port_dev.route.add_gateway(gateway_ip)
|
|
|
|
|
|
def assert_ping(src_namespace, dst_ip, timeout=1, count=3):
|
|
ipversion = netaddr.IPAddress(dst_ip).version
|
|
ping_command = 'ping' if ipversion == 4 else 'ping6'
|
|
ns_ip_wrapper = ip_lib.IPWrapper(src_namespace)
|
|
ns_ip_wrapper.netns.execute([ping_command, '-c', count, '-W', timeout,
|
|
dst_ip])
|
|
|
|
|
|
def assert_async_ping(src_namespace, dst_ip, timeout=1, count=1, interval=1):
|
|
ipversion = netaddr.IPAddress(dst_ip).version
|
|
ping_command = 'ping' if ipversion == 4 else 'ping6'
|
|
ns_ip_wrapper = ip_lib.IPWrapper(src_namespace)
|
|
|
|
# See bug 1588731 for explanation why using -c count ping option
|
|
# cannot be used and it needs to be done using the following workaround.
|
|
for _index in range(count):
|
|
start_time = time.time()
|
|
ns_ip_wrapper.netns.execute([ping_command, '-c', '1', '-W', timeout,
|
|
dst_ip])
|
|
end_time = time.time()
|
|
diff = end_time - start_time
|
|
if 0 < diff < interval:
|
|
# wait at most "interval" seconds between individual pings
|
|
time.sleep(interval - diff)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def async_ping(namespace, ips, timeout=1, count=10):
|
|
with futures.ThreadPoolExecutor(max_workers=len(ips)) as executor:
|
|
fs = [executor.submit(assert_async_ping, namespace, ip, count=count,
|
|
timeout=timeout)
|
|
for ip in ips]
|
|
yield lambda: all(f.done() for f in fs)
|
|
futures.wait(fs)
|
|
for f in fs:
|
|
f.result()
|
|
|
|
|
|
def assert_no_ping(src_namespace, dst_ip, timeout=1, count=1):
|
|
try:
|
|
assert_ping(src_namespace, dst_ip, timeout, count)
|
|
except RuntimeError:
|
|
pass
|
|
else:
|
|
tools.fail("destination ip %(destination)s is replying to ping from "
|
|
"namespace %(ns)s, but it shouldn't" %
|
|
{'ns': src_namespace, 'destination': dst_ip})
|
|
|
|
|
|
def assert_arping(src_namespace, dst_ip, source=None, timeout=1, count=1):
|
|
"""Send arp request using arping executable.
|
|
|
|
NOTE: ARP protocol is used in IPv4 only. IPv6 uses Neighbour Discovery
|
|
Protocol instead.
|
|
"""
|
|
ns_ip_wrapper = ip_lib.IPWrapper(src_namespace)
|
|
arping_cmd = ['arping', '-c', count, '-w', timeout]
|
|
if source:
|
|
arping_cmd.extend(['-s', source])
|
|
arping_cmd.append(dst_ip)
|
|
ns_ip_wrapper.netns.execute(arping_cmd)
|
|
|
|
|
|
def assert_no_arping(src_namespace, dst_ip, source=None, timeout=1, count=1):
|
|
try:
|
|
assert_arping(src_namespace, dst_ip, source, timeout, count)
|
|
except RuntimeError:
|
|
pass
|
|
else:
|
|
tools.fail("destination ip %(destination)s is replying to arp from "
|
|
"namespace %(ns)s, but it shouldn't" %
|
|
{'ns': src_namespace, 'destination': dst_ip})
|
|
|
|
|
|
def _get_source_ports_from_ss_output(output):
|
|
ports = set()
|
|
for line in output.splitlines():
|
|
match = SS_SOURCE_PORT_PATTERN.match(line)
|
|
if match:
|
|
ports.add(int(match.group('port')))
|
|
return ports
|
|
|
|
|
|
def get_unused_port(used, start=1024, end=None):
|
|
if end is None:
|
|
port_range = utils.execute(
|
|
['sysctl', '-n', 'net.ipv4.ip_local_port_range'], run_as_root=True)
|
|
end = int(port_range.split()[0]) - 1
|
|
|
|
candidates = set(range(start, end + 1))
|
|
return random.choice(list(candidates - used))
|
|
|
|
|
|
def get_free_namespace_port(protocol, namespace=None, start=1024, end=None):
|
|
"""Return an unused port from given namespace
|
|
|
|
WARNING: This function returns a port that is free at the execution time of
|
|
this function. If this port is used later for binding then there
|
|
is a potential danger that port will be no longer free. It's up to
|
|
the programmer to handle error if port is already in use.
|
|
|
|
:param protocol: Return free port for given protocol. Supported protocols
|
|
are 'tcp' and 'udp'.
|
|
:param namespace: Namespace in which free port has to be returned.
|
|
:param start: The starting port number.
|
|
:param end: The ending port number (free port that is returned would be
|
|
between (start, end) values.
|
|
"""
|
|
if protocol == n_const.PROTO_NAME_TCP:
|
|
param = '-tna'
|
|
elif protocol == n_const.PROTO_NAME_UDP:
|
|
param = '-una'
|
|
else:
|
|
raise ValueError("Unsupported protocol %s" % protocol)
|
|
|
|
ip_wrapper = ip_lib.IPWrapper(namespace=namespace)
|
|
output = ip_wrapper.netns.execute(['ss', param], run_as_root=True)
|
|
used_ports = _get_source_ports_from_ss_output(output)
|
|
|
|
return get_unused_port(used_ports, start, end)
|
|
|
|
|
|
def create_patch_ports(source, destination):
|
|
"""Hook up two OVS bridges.
|
|
|
|
The result is two patch ports, each end connected to a bridge.
|
|
The two patch port names will start with 'patch-', followed by identical
|
|
four characters. For example patch-xyzw-fedora, and patch-xyzw-ubuntu,
|
|
where fedora and ubuntu are random strings.
|
|
|
|
:param source: Instance of OVSBridge
|
|
:param destination: Instance of OVSBridge
|
|
"""
|
|
common = common_utils.get_rand_name(max_length=4, prefix='')
|
|
prefix = '%s-%s-' % (PATCH_PREFIX, common)
|
|
|
|
source_name = common_utils.get_rand_device_name(prefix=prefix)
|
|
destination_name = common_utils.get_rand_device_name(prefix=prefix)
|
|
|
|
source.add_patch_port(source_name, destination_name)
|
|
destination.add_patch_port(destination_name, source_name)
|
|
|
|
|
|
def create_vlan_interface(
|
|
namespace, port_name, mac_address, ip_address, vlan_tag):
|
|
"""Create a VLAN interface in namespace with IP address.
|
|
|
|
:param namespace: Namespace in which VLAN interface should be created.
|
|
:param port_name: Name of the port to which VLAN should be added.
|
|
:param ip_address: IPNetwork instance containing the VLAN interface IP
|
|
address.
|
|
:param vlan_tag: VLAN tag for VLAN interface.
|
|
"""
|
|
ip_wrap = ip_lib.IPWrapper(namespace)
|
|
dev_name = "%s.%d" % (port_name, vlan_tag)
|
|
ip_wrap.add_vlan(dev_name, port_name, vlan_tag)
|
|
dev = ip_wrap.device(dev_name)
|
|
dev.addr.add(str(ip_address))
|
|
dev.link.set_address(mac_address)
|
|
dev.link.set_up()
|
|
|
|
return dev
|
|
|
|
|
|
class RootHelperProcess(subprocess.Popen):
|
|
def __init__(self, cmd, *args, **kwargs):
|
|
for arg in ('stdin', 'stdout', 'stderr'):
|
|
kwargs.setdefault(arg, subprocess.PIPE)
|
|
kwargs.setdefault('universal_newlines', True)
|
|
|
|
self.namespace = kwargs.pop('namespace', None)
|
|
self.cmd = cmd
|
|
if self.namespace is not None:
|
|
cmd = ['ip', 'netns', 'exec', self.namespace] + cmd
|
|
root_helper = config.get_root_helper(utils.cfg.CONF)
|
|
cmd = shlex.split(root_helper) + cmd
|
|
self.child_pid = None
|
|
LOG.debug("Spawning process %s", cmd)
|
|
super(RootHelperProcess, self).__init__(cmd, *args, **kwargs)
|
|
self._wait_for_child_process()
|
|
|
|
def kill(self, sig=signal.SIGKILL):
|
|
pid = self.child_pid or str(self.pid)
|
|
utils.execute(['kill', '-%d' % sig, pid], run_as_root=True)
|
|
|
|
def read_stdout(self, timeout=None):
|
|
return self._read_stream(self.stdout, timeout)
|
|
|
|
@staticmethod
|
|
def _read_stream(stream, timeout):
|
|
if timeout:
|
|
rready, _wready, _xready = select.select([stream], [], [], timeout)
|
|
if not rready:
|
|
raise RuntimeError('No output in %.2f seconds' % timeout)
|
|
return stream.readline()
|
|
|
|
def writeline(self, data):
|
|
self.stdin.write(data + os.linesep)
|
|
self.stdin.flush()
|
|
|
|
def _wait_for_child_process(self, timeout=CHILD_PROCESS_TIMEOUT,
|
|
sleep=CHILD_PROCESS_SLEEP):
|
|
def child_is_running():
|
|
child_pid = utils.get_root_helper_child_pid(
|
|
self.pid, self.cmd, run_as_root=True)
|
|
if utils.pid_invoked_with_cmdline(child_pid, self.cmd):
|
|
return True
|
|
|
|
common_utils.wait_until_true(
|
|
child_is_running,
|
|
timeout,
|
|
exception=RuntimeError("Process %s hasn't been spawned "
|
|
"in %d seconds" % (self.cmd, timeout)))
|
|
self.child_pid = utils.get_root_helper_child_pid(
|
|
self.pid, self.cmd, run_as_root=True)
|
|
|
|
@property
|
|
def is_running(self):
|
|
return self.poll() is None
|
|
|
|
|
|
class Pinger(object):
|
|
"""Class for sending ICMP packets asynchronously
|
|
|
|
The aim is to keep sending ICMP packets on background while executing other
|
|
code. After background 'ping' command is stopped, statistics are available.
|
|
|
|
Difference to assert_(no_)ping() functions located in this module is that
|
|
these methods send given count of ICMP packets while they wait for the
|
|
exit code of 'ping' command.
|
|
|
|
>>> pinger = Pinger('pinger_test', '192.168.0.2')
|
|
|
|
>>> pinger.start(); time.sleep(5); pinger.stop()
|
|
|
|
>>> pinger.sent, pinger.received
|
|
7 7
|
|
|
|
"""
|
|
|
|
stats_pattern = re.compile(
|
|
r'^(?P<trans>\d+) packets transmitted,.*(?P<recv>\d+) received.*$')
|
|
unreachable_pattern = re.compile(
|
|
r'.* Destination .* Unreachable')
|
|
TIMEOUT = 15
|
|
|
|
def __init__(self, namespace, address, count=None, timeout=1,
|
|
interval=None):
|
|
self.proc = None
|
|
self.namespace = namespace
|
|
self.address = address
|
|
self.count = count
|
|
self.timeout = timeout
|
|
self.destination_unreachable = False
|
|
self.sent = 0
|
|
self.received = 0
|
|
self.interval = interval
|
|
|
|
def _wait_for_death(self):
|
|
is_dead = lambda: self.proc.poll() is not None
|
|
common_utils.wait_until_true(
|
|
is_dead, timeout=self.TIMEOUT, exception=RuntimeError(
|
|
"Ping command hasn't ended after %d seconds." % self.TIMEOUT))
|
|
|
|
def _parse_stats(self):
|
|
for line in self.proc.stdout:
|
|
if (not self.destination_unreachable and
|
|
self.unreachable_pattern.match(line)):
|
|
self.destination_unreachable = True
|
|
continue
|
|
result = self.stats_pattern.match(line)
|
|
if result:
|
|
self.sent = int(result.group('trans'))
|
|
self.received = int(result.group('recv'))
|
|
break
|
|
else:
|
|
raise RuntimeError("Didn't find ping statistics.")
|
|
|
|
def start(self):
|
|
if self.proc and self.proc.is_running:
|
|
raise RuntimeError("This pinger has already a running process")
|
|
ip_version = common_utils.get_ip_version(self.address)
|
|
ping_exec = 'ping' if ip_version == 4 else 'ping6'
|
|
cmd = [ping_exec, self.address, '-W', str(self.timeout)]
|
|
if self.count:
|
|
cmd.extend(['-c', str(self.count)])
|
|
if self.interval:
|
|
cmd.extend(['-i', str(self.interval)])
|
|
self.proc = RootHelperProcess(cmd, namespace=self.namespace)
|
|
|
|
def stop(self):
|
|
if self.proc and self.proc.is_running:
|
|
self.proc.kill(signal.SIGINT)
|
|
self._wait_for_death()
|
|
self._parse_stats()
|
|
|
|
def wait(self):
|
|
if self.count:
|
|
self._wait_for_death()
|
|
self._parse_stats()
|
|
else:
|
|
raise RuntimeError("Pinger is running infinitely, use stop() "
|
|
"first")
|
|
|
|
|
|
class NetcatTester(object):
|
|
TCP = n_const.PROTO_NAME_TCP
|
|
UDP = n_const.PROTO_NAME_UDP
|
|
SCTP = n_const.PROTO_NAME_SCTP
|
|
VERSION_TO_ALL_ADDRESS = {
|
|
4: '0.0.0.0',
|
|
6: '::',
|
|
}
|
|
|
|
def __init__(self, client_namespace, server_namespace, address,
|
|
dst_port, protocol, server_address=None, src_port=None):
|
|
|
|
"""
|
|
Tool for testing connectivity on transport layer using netcat
|
|
executable.
|
|
|
|
The processes are spawned lazily.
|
|
|
|
:param client_namespace: Namespace in which netcat process that
|
|
connects to other netcat will be spawned
|
|
:param server_namespace: Namespace in which listening netcat process
|
|
will be spawned
|
|
:param address: Server address from client point of view
|
|
:param dst_port: Port on which netcat listens
|
|
:param protocol: Transport protocol, either 'tcp', 'udp' or 'sctp'
|
|
:param server_address: Address in server namespace on which netcat
|
|
should listen
|
|
:param src_port: Source port of netcat process spawned in client
|
|
namespace - packet will have src_port in TCP/UDP
|
|
header with this value
|
|
|
|
"""
|
|
self.client_namespace = client_namespace
|
|
self.server_namespace = server_namespace
|
|
self._client_process = None
|
|
self._server_process = None
|
|
self.address = address
|
|
self.dst_port = str(dst_port)
|
|
self.src_port = str(src_port) if src_port else None
|
|
if protocol not in TRANSPORT_PROTOCOLS:
|
|
raise ValueError("Unsupported protocol %s" % protocol)
|
|
self.protocol = protocol
|
|
ip_version = netaddr.IPAddress(address).version
|
|
self.server_address = (
|
|
server_address or self.VERSION_TO_ALL_ADDRESS[ip_version])
|
|
|
|
@property
|
|
def client_process(self):
|
|
if not self._client_process:
|
|
self.establish_connection()
|
|
return self._client_process
|
|
|
|
@property
|
|
def server_process(self):
|
|
if not self._server_process:
|
|
self._spawn_server_process()
|
|
return self._server_process
|
|
|
|
def _spawn_server_process(self):
|
|
self._server_process = self._spawn_nc_in_namespace(
|
|
self.server_namespace,
|
|
address=self.server_address,
|
|
listen=True)
|
|
|
|
@property
|
|
def is_established(self):
|
|
return bool(self._client_process and not self._client_process.poll())
|
|
|
|
def establish_connection(self):
|
|
if self.is_established:
|
|
raise RuntimeError('%(proto)s connection to %(ip_addr)s is already'
|
|
' established' %
|
|
{'proto': self.protocol,
|
|
'ip_addr': self.address})
|
|
|
|
if not self._server_process:
|
|
self._spawn_server_process()
|
|
self._client_process = self._spawn_nc_in_namespace(
|
|
self.client_namespace,
|
|
address=self.address)
|
|
if self.protocol == self.UDP:
|
|
# Create an ASSURED entry in conntrack table for UDP packets,
|
|
# that requires 3-way communication
|
|
# 1st transmission creates UNREPLIED
|
|
# 2nd transmission removes UNREPLIED
|
|
# 3rd transmission creates ASSURED
|
|
data = 'foo'
|
|
self.client_process.writeline(data)
|
|
self.server_process.read_stdout(READ_TIMEOUT)
|
|
self.server_process.writeline(data)
|
|
self.client_process.read_stdout(READ_TIMEOUT)
|
|
self.client_process.writeline(data)
|
|
self.server_process.read_stdout(READ_TIMEOUT)
|
|
|
|
def test_connectivity(self, respawn=False):
|
|
testing_string = uuidutils.generate_uuid()
|
|
if respawn:
|
|
self.stop_processes()
|
|
|
|
self.client_process.writeline(testing_string)
|
|
message = self.server_process.read_stdout(READ_TIMEOUT).strip()
|
|
self.server_process.writeline(message)
|
|
message = self.client_process.read_stdout(READ_TIMEOUT).strip()
|
|
|
|
return message == testing_string
|
|
|
|
def test_no_connectivity(self, respawn=False):
|
|
try:
|
|
return not self.test_connectivity(respawn)
|
|
except RuntimeError:
|
|
return True
|
|
|
|
def _spawn_nc_in_namespace(self, namespace, address, listen=False):
|
|
cmd = ['ncat', address, self.dst_port]
|
|
if self.protocol == self.UDP:
|
|
cmd.append('-u')
|
|
elif self.protocol == self.SCTP:
|
|
cmd.append('--sctp')
|
|
|
|
if listen:
|
|
cmd.append('-l')
|
|
if self.protocol in (self.TCP, self.SCTP):
|
|
cmd.append('-k')
|
|
else:
|
|
cmd.extend(['-w', '20'])
|
|
if self.src_port:
|
|
cmd.extend(['-p', self.src_port])
|
|
proc = RootHelperProcess(cmd, namespace=namespace)
|
|
return proc
|
|
|
|
def stop_processes(self):
|
|
for proc_attr in ('_client_process', '_server_process'):
|
|
proc = getattr(self, proc_attr)
|
|
if proc:
|
|
if proc.poll() is None:
|
|
proc.kill()
|
|
proc.wait()
|
|
setattr(self, proc_attr, None)
|
|
|
|
|
|
class NamespaceFixture(fixtures.Fixture):
|
|
"""Create a namespace.
|
|
|
|
:ivar ip_wrapper: created namespace
|
|
:type ip_wrapper: IPWrapper
|
|
:ivar name: created namespace name
|
|
:type name: str
|
|
"""
|
|
|
|
def __init__(self, prefix=NS_PREFIX):
|
|
super(NamespaceFixture, self).__init__()
|
|
self.prefix = prefix
|
|
|
|
def _setUp(self):
|
|
ip = ip_lib.IPWrapper()
|
|
self.name = self.prefix + uuidutils.generate_uuid()
|
|
self.ip_wrapper = ip.ensure_namespace(self.name)
|
|
self.addCleanup(self.destroy)
|
|
|
|
def destroy(self):
|
|
if self.ip_wrapper.netns.exists(self.name):
|
|
self.ip_wrapper.netns.delete(self.name)
|
|
|
|
|
|
class VethFixture(fixtures.Fixture):
|
|
"""Create a veth.
|
|
|
|
:ivar ports: created veth ports
|
|
:type ports: tuple of 2 IPDevice
|
|
"""
|
|
|
|
def _setUp(self):
|
|
ip_wrapper = ip_lib.IPWrapper()
|
|
|
|
self.ports = common_base.create_resource(
|
|
VETH0_PREFIX,
|
|
lambda name: ip_wrapper.add_veth(name, self.get_peer_name(name)))
|
|
|
|
self.addCleanup(self.destroy)
|
|
|
|
def destroy(self):
|
|
for port in self.ports:
|
|
ip_wrapper = ip_lib.IPWrapper(port.namespace)
|
|
if (ip_wrapper.netns.exists(port.namespace) or
|
|
port.namespace is None):
|
|
try:
|
|
ip_wrapper.del_veth(port.name)
|
|
break
|
|
except RuntimeError:
|
|
# NOTE(cbrandily): It seems a veth is automagically deleted
|
|
# when a namespace owning a veth endpoint is deleted.
|
|
pass
|
|
|
|
@staticmethod
|
|
def get_peer_name(name):
|
|
if name.startswith(VETH0_PREFIX):
|
|
return name.replace(VETH0_PREFIX, VETH1_PREFIX)
|
|
elif name.startswith(VETH1_PREFIX):
|
|
return name.replace(VETH1_PREFIX, VETH0_PREFIX)
|
|
else:
|
|
tools.fail('%s is not a valid VethFixture veth endpoint' % name)
|
|
|
|
|
|
class NamedVethFixture(VethFixture):
|
|
"""Create a veth with at least one specified name of a device
|
|
|
|
:ivar ports: created veth ports
|
|
:type ports: tuple of 2 IPDevice
|
|
"""
|
|
|
|
def __init__(self, veth0_prefix=VETH0_PREFIX, veth1_prefix=VETH1_PREFIX):
|
|
super(NamedVethFixture, self).__init__()
|
|
self.veth0_name = self.get_veth_name(veth0_prefix)
|
|
self.veth1_name = self.get_veth_name(veth1_prefix)
|
|
|
|
def _setUp(self):
|
|
ip_wrapper = ip_lib.IPWrapper()
|
|
self.ports = ip_wrapper.add_veth(self.veth0_name, self.veth1_name)
|
|
self.addCleanup(self.destroy)
|
|
|
|
@staticmethod
|
|
def get_veth_name(name):
|
|
if name.startswith(VETH0_PREFIX):
|
|
return common_utils.get_rand_device_name(VETH0_PREFIX)
|
|
if name.startswith(VETH1_PREFIX):
|
|
return common_utils.get_rand_device_name(VETH1_PREFIX)
|
|
return name
|
|
|
|
|
|
class MacvtapFixture(fixtures.Fixture):
|
|
"""Create a macvtap.
|
|
|
|
:param src_dev: source device for macvtap
|
|
:type src_dev: IPDevice
|
|
:param mode: mode of macvtap
|
|
:type mode: string
|
|
:ivar ip_dev: created macvtap
|
|
:type ip_dev: IPDevice
|
|
"""
|
|
def __init__(self, src_dev=None, mode=None, prefix=MACVTAP_PREFIX):
|
|
super(MacvtapFixture, self).__init__()
|
|
self.src_dev = src_dev
|
|
self.mode = mode
|
|
self.prefix = prefix
|
|
|
|
def _setUp(self):
|
|
ip_wrapper = ip_lib.IPWrapper()
|
|
self.ip_dev = common_base.create_resource(
|
|
self.prefix,
|
|
ip_wrapper.add_macvtap,
|
|
self.src_dev, mode=self.mode)
|
|
self.addCleanup(self.destroy)
|
|
|
|
def destroy(self):
|
|
if (ip_lib.network_namespace_exists(self.ip_dev.namespace) or
|
|
self.ip_dev.namespace is None):
|
|
try:
|
|
self.ip_dev.link.delete()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class PortFixture(fixtures.Fixture):
|
|
"""Create a port.
|
|
|
|
:ivar port: created port
|
|
:type port: IPDevice
|
|
:ivar bridge: port bridge
|
|
"""
|
|
|
|
def __init__(self, bridge=None, namespace=None, mac=None, port_id=None):
|
|
super(PortFixture, self).__init__()
|
|
self.bridge = bridge
|
|
self.namespace = namespace
|
|
self.mac = (
|
|
mac or db_base_plugin_common.DbBasePluginCommon._generate_mac())
|
|
self.port_id = port_id or uuidutils.generate_uuid()
|
|
|
|
@abc.abstractmethod
|
|
def _create_bridge_fixture(self):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _setUp(self):
|
|
super(PortFixture, self)._setUp()
|
|
if not self.bridge:
|
|
self.bridge = self.useFixture(self._create_bridge_fixture()).bridge
|
|
|
|
@classmethod
|
|
def get(cls, bridge, namespace=None, mac=None, port_id=None,
|
|
hybrid_plug=False):
|
|
"""Deduce PortFixture class from bridge type and instantiate it."""
|
|
if isinstance(bridge, ovs_lib.OVSBridge):
|
|
return OVSPortFixture(bridge, namespace, mac, port_id, hybrid_plug)
|
|
if isinstance(bridge, bridge_lib.BridgeDevice):
|
|
return LinuxBridgePortFixture(bridge, namespace, mac, port_id)
|
|
if isinstance(bridge, VethBridge):
|
|
return VethPortFixture(bridge, namespace)
|
|
tools.fail('Unexpected bridge type: %s' % type(bridge))
|
|
|
|
|
|
class OVSBridgeFixture(fixtures.Fixture):
|
|
"""Create an OVS bridge.
|
|
|
|
:ivar prefix: bridge name prefix
|
|
:type prefix: str
|
|
:ivar bridge: created bridge
|
|
:type bridge: OVSBridge
|
|
"""
|
|
|
|
def __init__(self, prefix=BR_PREFIX):
|
|
super(OVSBridgeFixture, self).__init__()
|
|
self.prefix = prefix
|
|
|
|
def _setUp(self):
|
|
ovs = ovs_lib.BaseOVS()
|
|
self.bridge = common_base.create_resource(self.prefix, ovs.add_bridge)
|
|
self.addCleanup(self.bridge.destroy)
|
|
|
|
|
|
class OVSTrunkBridgeFixture(OVSBridgeFixture):
|
|
"""This bridge doesn't generate the name."""
|
|
def _setUp(self):
|
|
ovs = ovs_lib.BaseOVS()
|
|
self.bridge = ovs.add_bridge(self.prefix)
|
|
self.addCleanup(self.bridge.destroy)
|
|
|
|
|
|
class OVSPortFixture(PortFixture):
|
|
NIC_NAME_LEN = 14
|
|
|
|
def __init__(self, bridge=None, namespace=None, mac=None, port_id=None,
|
|
hybrid_plug=False):
|
|
super(OVSPortFixture, self).__init__(bridge, namespace, mac, port_id)
|
|
self.hybrid_plug = hybrid_plug
|
|
self.vlan_tag = None
|
|
|
|
def _create_bridge_fixture(self):
|
|
return OVSBridgeFixture()
|
|
|
|
def _setUp(self):
|
|
super(OVSPortFixture, self)._setUp()
|
|
|
|
# because in some tests this port can be used to providing connection
|
|
# between linuxbridge agents and vlan_id can be also added to this
|
|
# device name it has to be max LB_DEVICE_NAME_MAX_LEN long
|
|
port_name = common_utils.get_rand_name(
|
|
LB_DEVICE_NAME_MAX_LEN,
|
|
PORT_PREFIX
|
|
)
|
|
|
|
if self.hybrid_plug:
|
|
self.hybrid_plug_port(port_name)
|
|
else:
|
|
self.plug_port(port_name)
|
|
|
|
def plug_port(self, port_name):
|
|
# TODO(jlibosva): Don't use interface driver for fullstack fake
|
|
# machines as the port should be treated by OVS agent and not by
|
|
# external party
|
|
interface_config = cfg.ConfigOpts()
|
|
config.register_interface_opts(interface_config)
|
|
ovs_interface = interface.OVSInterfaceDriver(interface_config)
|
|
ovs_interface.plug_new(
|
|
None,
|
|
self.port_id,
|
|
port_name,
|
|
self.mac,
|
|
bridge=self.bridge.br_name,
|
|
namespace=self.namespace)
|
|
# NOTE(mangelajo): for OVS implementations remove the DEAD VLAN tag
|
|
# on ports that we intend to use as fake vm interfaces, they
|
|
# need to be flat. This is related to lp#1767422
|
|
self.bridge.clear_db_attribute("Port", port_name, "tag")
|
|
self.addCleanup(self.bridge.delete_port, port_name)
|
|
self.port = ip_lib.IPDevice(port_name, self.namespace)
|
|
|
|
def hybrid_plug_port(self, port_name):
|
|
"""Plug port with linux bridge in the middle.
|
|
|
|
"""
|
|
ip_wrapper = ip_lib.IPWrapper(self.namespace)
|
|
qvb_name, qvo_name = self._get_veth_pair_names(self.port_id)
|
|
qvb, qvo = self.useFixture(NamedVethFixture(qvb_name, qvo_name)).ports
|
|
qvb.link.set_up()
|
|
qvo.link.set_up()
|
|
qbr_name = self._get_br_name(self.port_id)
|
|
self.qbr = self.useFixture(
|
|
LinuxBridgeFixture(qbr_name,
|
|
namespace=None,
|
|
prefix_is_full_name=True)).bridge
|
|
self.qbr.link.set_up()
|
|
self.qbr.setfd(0)
|
|
self.qbr.disable_stp()
|
|
self.qbr.addif(qvb_name)
|
|
qvo_attrs = ('external_ids', {'iface-id': self.port_id,
|
|
'iface-status': 'active',
|
|
'attached-mac': self.mac})
|
|
self.bridge.add_port(qvo_name, qvo_attrs)
|
|
|
|
# NOTE(jlibosva): Create fake vm port, instead of tap device, we use
|
|
# veth pair here in order to be able to attach it to linux bridge in
|
|
# root namespace. Name with tap is in root namespace and its peer is in
|
|
# the namespace
|
|
hybrid_port_name = iptables_firewall.get_hybrid_port_name(self.port_id)
|
|
bridge_port, self.port = self.useFixture(
|
|
NamedVethFixture(hybrid_port_name)).ports
|
|
self.addCleanup(self.port.link.delete)
|
|
ip_wrapper.add_device_to_namespace(self.port)
|
|
bridge_port.link.set_up()
|
|
self.qbr.addif(bridge_port)
|
|
|
|
self.port.link.set_address(self.mac)
|
|
self.port.link.set_up()
|
|
|
|
# NOTE(jlibosva): Methods below are taken from nova.virt.libvirt.vif
|
|
def _get_br_name(self, iface_id):
|
|
return ("qbr" + iface_id)[:self.NIC_NAME_LEN]
|
|
|
|
def _get_veth_pair_names(self, iface_id):
|
|
return (("qvb%s" % iface_id)[:self.NIC_NAME_LEN],
|
|
("qvo%s" % iface_id)[:self.NIC_NAME_LEN])
|
|
|
|
|
|
class LinuxBridgeFixture(fixtures.Fixture):
|
|
"""Create a linux bridge.
|
|
|
|
:ivar bridge: created bridge
|
|
:type bridge: BridgeDevice
|
|
:ivar namespace: created bridge namespace
|
|
:type namespace: str
|
|
"""
|
|
def __init__(self, prefix=BR_PREFIX, namespace=UNDEFINED,
|
|
prefix_is_full_name=False):
|
|
super(LinuxBridgeFixture, self).__init__()
|
|
self.prefix = prefix
|
|
self.prefix_is_full_name = prefix_is_full_name
|
|
self.namespace = namespace
|
|
|
|
def _setUp(self):
|
|
if self.namespace is UNDEFINED:
|
|
self.namespace = self.useFixture(NamespaceFixture()).name
|
|
self.bridge = self._create_bridge()
|
|
self.addCleanup(self.safe_delete)
|
|
self.bridge.link.set_up()
|
|
self.addCleanup(self.safe_set_down)
|
|
|
|
def safe_set_down(self):
|
|
try:
|
|
self.bridge.link.set_down()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
def safe_delete(self):
|
|
try:
|
|
self.bridge.delbr()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
def _create_bridge(self):
|
|
if self.prefix_is_full_name:
|
|
return bridge_lib.BridgeDevice.addbr(
|
|
name=self.prefix,
|
|
namespace=self.namespace
|
|
)
|
|
else:
|
|
return common_base.create_resource(
|
|
self.prefix,
|
|
bridge_lib.BridgeDevice.addbr,
|
|
namespace=self.namespace)
|
|
|
|
|
|
class LinuxBridgePortFixture(PortFixture):
|
|
"""Create a linux bridge port.
|
|
|
|
:ivar port: created port
|
|
:type port: IPDevice
|
|
:ivar br_port: bridge side veth peer port
|
|
:type br_port: IPDevice
|
|
"""
|
|
|
|
def __init__(self, bridge, namespace=None, mac=None, port_id=None):
|
|
super(LinuxBridgePortFixture, self).__init__(
|
|
bridge, namespace, mac, port_id)
|
|
# we need to override port_id value here because in Port() class it is
|
|
# always generated as random. In LinuxBridgePortFixture we need to have
|
|
# it empty if it was not give because then proper veth_pair will be
|
|
# created (for example in some functional tests)
|
|
self.port_id = port_id
|
|
|
|
def _create_bridge_fixture(self):
|
|
return LinuxBridgeFixture()
|
|
|
|
def _setUp(self):
|
|
super(LinuxBridgePortFixture, self)._setUp()
|
|
br_port_name = self._get_port_name()
|
|
if br_port_name:
|
|
self.veth_fixture = self.useFixture(
|
|
NamedVethFixture(veth0_prefix=br_port_name))
|
|
else:
|
|
self.veth_fixture = self.useFixture(VethFixture())
|
|
self.br_port, self.port = self.veth_fixture.ports
|
|
|
|
if self.mac:
|
|
self.port.link.set_address(self.mac)
|
|
|
|
# bridge side
|
|
br_ip_wrapper = ip_lib.IPWrapper(self.bridge.namespace)
|
|
br_ip_wrapper.add_device_to_namespace(self.br_port)
|
|
self.bridge.addif(self.br_port)
|
|
self.br_port.link.set_up()
|
|
|
|
# port side
|
|
ns_ip_wrapper = ip_lib.IPWrapper(self.namespace)
|
|
ns_ip_wrapper.add_device_to_namespace(self.port)
|
|
self.port.link.set_up()
|
|
|
|
def _get_port_name(self):
|
|
if self.port_id:
|
|
return linuxbridge_agent.LinuxBridgeManager.get_tap_device_name(
|
|
self.port_id)
|
|
return None
|
|
|
|
|
|
class VethBridge(object):
|
|
|
|
def __init__(self, ports):
|
|
self.ports = ports
|
|
self.unallocated_ports = list(self.ports)
|
|
|
|
def allocate_port(self):
|
|
try:
|
|
return self.unallocated_ports.pop()
|
|
except IndexError:
|
|
tools.fail('All FakeBridge ports (%s) are already allocated.' %
|
|
len(self.ports))
|
|
|
|
|
|
class VethBridgeFixture(fixtures.Fixture):
|
|
"""Simulate a bridge with a veth.
|
|
|
|
:ivar bridge: created bridge
|
|
:type bridge: FakeBridge
|
|
"""
|
|
|
|
def _setUp(self):
|
|
ports = self.useFixture(VethFixture()).ports
|
|
self.bridge = VethBridge(ports)
|
|
|
|
|
|
class VethPortFixture(PortFixture):
|
|
"""Create a veth bridge port.
|
|
|
|
:ivar port: created port
|
|
:type port: IPDevice
|
|
"""
|
|
|
|
def _create_bridge_fixture(self):
|
|
return VethBridgeFixture()
|
|
|
|
def _setUp(self):
|
|
super(VethPortFixture, self)._setUp()
|
|
self.port = self.bridge.allocate_port()
|
|
|
|
ns_ip_wrapper = ip_lib.IPWrapper(self.namespace)
|
|
ns_ip_wrapper.add_device_to_namespace(self.port)
|
|
self.port.link.set_up()
|