neutron/neutron/tests/fullstack/test_trunk.py
Sławek Kapłoński 806cf71eb5 Fullstack: init trunk agent's driver only when necessary
Trunk driver is not needed to be initialized when "trunk"
service plugin is not enabled.
On production environments it's not possible to base on
"service_plugins" config option on L2 agent's side so this
driver is initialized always.
It cause problems on fullstack tests becasue there is race
condition between different ovs agents which consumes events
from Openvswitch monitor.
On fullstack tests however we can assume that agent's and server's
config are in sync so trunk driver can be initialized only if
"trunk" service plugin is enabled on server side.

Change-Id: I3ad8d6e7b8f103867ee277078d03f3a01c20ac0d
Closes-Bug: #1687709
2017-11-03 16:49:12 +00:00

314 lines
12 KiB
Python

# Copyright 2016 Red Hat, 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 functools
import netaddr
from neutron_lib import constants
from oslo_utils import uuidutils
from neutron.common import utils
from neutron.services.trunk.drivers.openvswitch.agent import ovsdb_handler
from neutron.services.trunk.drivers.openvswitch.agent import trunk_manager
from neutron.services.trunk.drivers.openvswitch import utils as trunk_ovs_utils
from neutron.tests.fullstack import base
from neutron.tests.fullstack.resources import environment
from neutron.tests.fullstack.resources import machine
def trunk_bridge_does_not_exist(trunk_id):
"""Return true if trunk bridge for given ID does not exists."""
bridge = trunk_manager.TrunkBridge(trunk_id)
return not bridge.exists()
def make_ip_network(port, network):
"""Make an IPNetwork object from port and network.
Function returns IPNetwork object containing fixed IP address from port
dictionary with prefixlen from network object.
:param port: Port dictionary returned by Neutron API
:param network: IPNetwork object in which the port's IP will be assigned.
"""
ip_address = netaddr.IPAddress(
port['fixed_ips'][0]['ip_address'])
return netaddr.IPNetwork(
(ip_address.value, network.prefixlen))
class TrunkTestException(Exception):
pass
class Network(object):
"""A helper class to keep persistent info about assigned addresses."""
def __init__(self, prefix, network_cidr, tag=None):
self.prefix = prefix
self.network = netaddr.IPNetwork(network_cidr)
self.neutron_network = None
self.neutron_subnet = None
self.tag = tag
# Currently, only vlan is supported. Pass via __init__ once more are
# supported.
self.segmentation_type = 'vlan'
@property
def cidr(self):
return str(self.network.cidr)
@property
def gateway(self):
"""Return lowest possible IP in the given subnet."""
return str(netaddr.IPAddress(self.network.first + 1))
@property
def id(self):
return self.neutron_network['id']
@property
def name(self):
return "%s-network" % self.prefix
@property
def subnet_name(self):
return "%s-subnet" % self.prefix
class TestTrunkPlugin(base.BaseFullStackTestCase):
def setUp(self):
host_desc = [environment.HostDescription(
l3_agent=False,
l2_agent_type=constants.AGENT_TYPE_OVS)]
env_desc = environment.EnvironmentDescription(service_plugins='trunk')
env = environment.Environment(env_desc, host_desc)
super(TestTrunkPlugin, self).setUp(env)
self.tenant_id = uuidutils.generate_uuid()
self.trunk_network = Network('trunk', '10.0.0.0/24')
self.vlan1_network = Network('vlan1', '192.168.0.0/24', tag=10)
self.vlan2_network = Network('vlan2', '192.168.1.0/24', tag=20)
self.host = self.environment.hosts[0]
for network in (
self.trunk_network, self.vlan1_network, self.vlan2_network):
self.create_network_and_subnet(network)
def create_network_and_subnet(self, network):
"""Create network and subnet resources in Neutron based on network
object.
The resource names will be <prefix>-network and <prefix>-subnet, where
prefix is taken from network object.
:param network: Network object from this module.
"""
network.neutron_network = self.safe_client.create_network(
self.tenant_id, network.name)
network.neutron_subnet = self.safe_client.create_subnet(
self.tenant_id,
network.id,
cidr=network.cidr,
gateway_ip=network.gateway,
name=network.subnet_name,
enable_dhcp=False)
def create_vlan_aware_vm(self, trunk_network, vlan_networks):
"""Create a fake machine with one untagged port and subports
according vlan_networks parameter.
:param trunk_network: Instance of Network where trunk port should be
created.
:param vlan_networks: List of Network instances where subports should
be created.
"""
trunk_parent_port = self.safe_client.create_port(
self.tenant_id, trunk_network.id)
vlan_subports = [
self.safe_client.create_port(self.tenant_id, vlan_network.id,
mac_address=trunk_parent_port['mac_address'])
for vlan_network in vlan_networks]
trunk = self.safe_client.create_trunk(
self.tenant_id,
name='mytrunk',
port_id=trunk_parent_port['id'],
sub_ports=[
{'port_id': vlan_subport['id'],
'segmentation_type': 'vlan',
'segmentation_id': vlan_network.tag}
for vlan_subport, vlan_network in zip(vlan_subports,
vlan_networks)
],
)
vm = self.useFixture(
machine.FakeFullstackTrunkMachine(
trunk,
self.host,
trunk_network.id,
self.tenant_id,
self.safe_client,
neutron_port=trunk_parent_port,
bridge_name=trunk_ovs_utils.gen_trunk_br_name(trunk['id'])))
for port, vlan_network in zip(vlan_subports, vlan_networks):
ip_network = make_ip_network(port, vlan_network.network)
vm.add_vlan_interface(
port['mac_address'], ip_network, vlan_network.tag)
vm.block_until_boot()
return vm
def create_vm_in_network(self, network):
"""Create a fake machine in given network."""
return self.useFixture(
machine.FakeFullstackMachine(
self.host,
network.id,
self.tenant_id,
self.safe_client
)
)
def add_subport_to_vm(self, vm, subport_network):
"""Add subport from subport_network to given vm.
:param vm: FakeFullstackMachine instance to with subport should be
added.
:param subport_network: Network object representing network containing
port for subport.
"""
subport = self.safe_client.create_port(
self.tenant_id, subport_network.id,
mac_address=vm.neutron_port['mac_address'])
subport_spec = {
'port_id': subport['id'],
'segmentation_type': subport_network.segmentation_type,
'segmentation_id': subport_network.tag
}
self.safe_client.trunk_add_subports(
self.tenant_id, vm.trunk['id'], [subport_spec])
ip_network = make_ip_network(subport, subport_network.network)
vm.add_vlan_interface(
subport['mac_address'], ip_network, subport_network.tag)
# NOTE(slaweq): As is described in bug
# https://bugs.launchpad.net/neutron/+bug/1687709 when more than one
# different ovs-agent with enabled trunk driver is running at a time it
# might lead to race contitions between them.
# Because of that ovs_agent used for fullstack tests is monkeypatched and
# loads trunk driver only if trunk service plugin is enabled.
# That makes restriction that only a single set of tests with trunk-enabled
# services will run at the same time.
def test_trunk_lifecycle(self):
"""Test life-cycle of a fake VM with trunk port.
This test uses 4 fake machines:
- vlan_aware_vm (A) that is at the beginning connected to a trunk
network and a vlan1 network.
- trunk_network_vm (B) that is connected to the trunk network.
- vlan1_network_vm (C) that is connected to the vlan1 network.
- vlan2_network_vm (D) that is connected to a vlan2 network.
Scenario steps:
- all the vms from above are created
- A can talk with B (over the trunk network)
- A can talk with C (over the vlan1 network)
- A can not talk with D (no leg on the vlan2 network)
- subport from the vlan2 network is added to A
- A can now talk with D (over the vlan2 network)
- subport from the vlan1 network is removed from A
- A can talk with B (over the trunk network)
- A can not talk with C (no leg on the vlan1 network)
- A can talk with D (over the vlan2 network)
- A is deleted which leads to removal of trunk bridge
- no leftovers like patch ports to the trunk bridge should remain on
an integration bridge
"""
vlan_aware_vm = self.create_vlan_aware_vm(
self.trunk_network,
[self.vlan1_network]
)
trunk_id = vlan_aware_vm.trunk['id']
# Create helper vms with different networks
trunk_network_vm = self.create_vm_in_network(self.trunk_network)
vlan1_network_vm = self.create_vm_in_network(self.vlan1_network)
vlan2_network_vm = self.create_vm_in_network(self.vlan2_network)
for vm in trunk_network_vm, vlan1_network_vm, vlan2_network_vm:
vm.block_until_boot()
# Test connectivity to trunk and subport
vlan_aware_vm.block_until_ping(trunk_network_vm.ip)
vlan_aware_vm.block_until_ping(vlan1_network_vm.ip)
# Subport for vlan2 hasn't been added yet
vlan_aware_vm.block_until_no_ping(vlan2_network_vm.ip)
# Add another subport and test
self.add_subport_to_vm(vlan_aware_vm, self.vlan2_network)
vlan_aware_vm.block_until_ping(vlan2_network_vm.ip)
# Remove the first subport
self.safe_client.trunk_remove_subports(
self.tenant_id,
trunk_id,
[vlan_aware_vm.trunk['sub_ports'][0]])
# vlan1_network_vm now shouldn't be able to talk to vlan_aware_vm
vlan_aware_vm.block_until_no_ping(vlan1_network_vm.ip)
# but trunk and vlan2 should be able to ping
vlan_aware_vm.block_until_ping(trunk_network_vm.ip)
vlan_aware_vm.block_until_ping(vlan2_network_vm.ip)
# Delete vm and check that patch ports and trunk bridge are gone
vlan_aware_vm.destroy()
bridge_doesnt_exist_predicate = functools.partial(
trunk_bridge_does_not_exist, trunk_id)
utils.wait_until_true(
bridge_doesnt_exist_predicate,
exception=TrunkTestException(
'Trunk bridge with ID %s has not been removed' %
trunk_id)
)
integration_bridge = self.host.get_bridge(None)
no_patch_ports_predicate = functools.partial(
lambda bridge: not ovsdb_handler.bridge_has_service_port(bridge),
integration_bridge,
)
try:
utils.wait_until_true(no_patch_ports_predicate)
except utils.WaitTimeout:
# Create exception object after timeout to provide up-to-date list
# of interfaces
raise TrunkTestException(
"Integration bridge %s still has following ports while some of"
" them are patch ports for trunk that were supposed to be "
"removed: %s" % (
integration_bridge.br_name,
integration_bridge.get_iface_name_list()
)
)