neutron/neutron/tests/fullstack/test_trunk.py
Jakub Libosvar f0439a04ad fullstack: VLAN aware VMs test
Patch tests basic life-cycle of a trunk associated with a port. Test
creates a trunk with one subport - this tests interaction between
agent and ovsdb handler that calls via RPC to server.
Later a new subport is added which tests RPC interaction between
server and agent. Then deletes the first created subport. Finally trunk
is removed and checked that no patch ports remain on the integration
bridge.

Future work:
 - Run this test with linuxbridge
 - Test re-using port associated with trunk.
 - Test re-using subports.
 - Test with OVS firewall.

Partially-implements: blueprint vlan-aware-vms
Change-Id: Ie79a010e6751c1f1c2be5b1bf52511b9e100ad20
2016-09-29 13:23:39 -04:00

305 lines
11 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 eventlet
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()
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)
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 eventlet.TimeoutError:
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()
)
)