Update OVS router namespace test cases
- Split test cases in sequencial sub-tests - Treat dvr_no_external as dvr Depends-On: https://review.opendev.org/c/x/devstack-plugin-tobiko/+/846357 Change-Id: Ief941ae2583ce4bafedf59db00ec21ebb93ae521
This commit is contained in:
parent
164df84c22
commit
1fe9e0a550
@ -85,14 +85,21 @@ def get_l3_agent_mode(
|
||||
l3_agent_conf_path: str,
|
||||
default='legacy',
|
||||
connection: sh.ShellConnectionType = None) -> str:
|
||||
connection = sh.shell_connection(connection)
|
||||
LOG.debug(f"Read L3 agent mode from file '{l3_agent_conf_path}' on host "
|
||||
f" '{connection.hostname}'...")
|
||||
with sh.open_file(l3_agent_conf_path, 'rt',
|
||||
connection=connection) as fd:
|
||||
LOG.debug(f"Parse ini file '{l3_agent_conf_path}'...")
|
||||
content = tobiko.parse_ini_file(fd)
|
||||
try:
|
||||
return content['DEFAULT', 'agent_mode']
|
||||
agent_mode = content['DEFAULT', 'agent_mode']
|
||||
except KeyError:
|
||||
LOG.error(f"agent_mode not found in file {l3_agent_conf_path}")
|
||||
return default
|
||||
agent_mode = default
|
||||
LOG.debug(f"Got L3 agent mode from file '{l3_agent_conf_path}' "
|
||||
f"on host '{connection.hostname}': '{agent_mode}'")
|
||||
return agent_mode
|
||||
|
||||
|
||||
class NetworkingAgentFixture(tobiko.SharedFixture):
|
||||
|
@ -35,6 +35,7 @@ assert_ovn_unsupported_dhcp_option_messages = (
|
||||
get_hosts_namespaces = _namespace.get_hosts_namespaces
|
||||
assert_namespace_in_hosts = _namespace.assert_namespace_in_hosts
|
||||
assert_namespace_not_in_hosts = _namespace.assert_namespace_not_in_hosts
|
||||
wait_for_namespace_in_hosts = _namespace.wait_for_namespace_in_hosts
|
||||
|
||||
list_nodes_processes = _sh.list_nodes_processes
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import json
|
||||
import typing
|
||||
|
||||
import tobiko
|
||||
@ -36,23 +37,59 @@ def get_hosts_namespaces(hostnames: typing.Iterable[str] = None,
|
||||
return namespaces
|
||||
|
||||
|
||||
def assert_namespace_in_hosts(namespace: str,
|
||||
def wait_for_namespace_in_hosts(*namespaces: str,
|
||||
hostnames: typing.Iterable[str] = None,
|
||||
timeout: tobiko.Seconds = None,
|
||||
count: int = None,
|
||||
interval: tobiko.Seconds = None,
|
||||
present=True,
|
||||
**params):
|
||||
for attempt in tobiko.retry(timeout=timeout,
|
||||
count=count,
|
||||
interval=interval,
|
||||
default_timeout=60.,
|
||||
default_interval=5.):
|
||||
try:
|
||||
if present:
|
||||
assert_namespace_in_hosts(*namespaces,
|
||||
hostnames=hostnames,
|
||||
**params)
|
||||
else:
|
||||
assert_namespace_not_in_hosts(*namespaces,
|
||||
hostnames=hostnames,
|
||||
**params)
|
||||
except tobiko.FailureException: # type: ignore
|
||||
if attempt.is_last:
|
||||
raise
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
def assert_namespace_in_hosts(*namespaces: str,
|
||||
hostnames: typing.Iterable[str] = None,
|
||||
**params):
|
||||
namespaces = get_hosts_namespaces(hostnames=hostnames, **params)
|
||||
actual_hostnames = set(_hostname
|
||||
for _hostnames in namespaces.values()
|
||||
for _hostname in _hostnames)
|
||||
tobiko.get_test_case().assertIn(
|
||||
namespace, set(namespaces),
|
||||
f"Namespace {namespace!r} not in hosts {actual_hostnames!r}")
|
||||
actual_namespaces = get_hosts_namespaces(hostnames=hostnames,
|
||||
**params)
|
||||
missing = set(namespaces) - set(actual_namespaces)
|
||||
if missing:
|
||||
actual_hostnames = sorted(set(
|
||||
_hostname
|
||||
for _hostnames in actual_namespaces.values()
|
||||
for _hostname in _hostnames))
|
||||
tobiko.fail(f"Network namespace(s) {sorted(missing)} missing in "
|
||||
f"host(s) {actual_hostnames!r}")
|
||||
|
||||
|
||||
def assert_namespace_not_in_hosts(namespace: str,
|
||||
def assert_namespace_not_in_hosts(*namespaces: str,
|
||||
hostnames: typing.Iterable[str] = None,
|
||||
**params):
|
||||
namespaces = get_hosts_namespaces(hostnames=hostnames, **params)
|
||||
actual_hostnames = namespaces.get(namespace)
|
||||
tobiko.get_test_case().assertNotIn(
|
||||
namespace, set(namespaces),
|
||||
f"Namespace {namespace!r} in hosts: {actual_hostnames!r}")
|
||||
unexpected_namespaces = collections.defaultdict(list)
|
||||
actual_namespaces = get_hosts_namespaces(hostnames=hostnames, **params)
|
||||
for namespace, hostnames in actual_namespaces.items():
|
||||
if namespace in sorted(set(namespaces)):
|
||||
for hostname in hostnames:
|
||||
unexpected_namespaces[hostname].append(namespace)
|
||||
if unexpected_namespaces:
|
||||
dump = json.dumps(unexpected_namespaces, indent=4, sort_keys=True)
|
||||
tobiko.fail(f"Unexpected network namespace(s) found in "
|
||||
f"host(s):\n{dump}")
|
||||
|
@ -46,10 +46,19 @@ OpenstackGroupNamesType = typing.Union[OpenstackGroupNameType, typing.Iterable[
|
||||
OpenstackGroupNameType]]
|
||||
|
||||
|
||||
def list_openstack_nodes(topology: 'OpenStackTopology' = None,
|
||||
HostAddressType = typing.Union[str, netaddr.IPAddress]
|
||||
|
||||
|
||||
def list_openstack_nodes(addresses: typing.Iterable[netaddr.IPAddress] = None,
|
||||
group: OpenstackGroupNamesType = None,
|
||||
hostnames=None, **kwargs):
|
||||
topology = topology or get_openstack_topology()
|
||||
hostnames: typing.Iterable[str] = None,
|
||||
topology: 'OpenStackTopology' = None,
|
||||
**kwargs) \
|
||||
-> tobiko.Selection['OpenStackTopologyNode']:
|
||||
if topology is None:
|
||||
topology = get_openstack_topology()
|
||||
|
||||
nodes: tobiko.Selection[OpenStackTopologyNode]
|
||||
if group is None:
|
||||
nodes = topology.nodes
|
||||
elif isinstance(group, str):
|
||||
@ -60,7 +69,40 @@ def list_openstack_nodes(topology: 'OpenStackTopology' = None,
|
||||
assert isinstance(group, abc.Iterable)
|
||||
nodes = topology.get_groups(groups=group)
|
||||
|
||||
if hostnames:
|
||||
return select_openstack_nodes(nodes,
|
||||
addresses=addresses,
|
||||
hostnames=hostnames,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def split_addresses_and_names(*hosts: HostAddressType) \
|
||||
-> typing.Tuple[typing.Set[netaddr.IPAddress], typing.Set[str]]:
|
||||
addresses = set()
|
||||
hostnames = set()
|
||||
for host in hosts:
|
||||
try:
|
||||
addresses.add(netaddr.IPAddress(host))
|
||||
except netaddr.AddrFormatError:
|
||||
hostnames.add(host)
|
||||
return addresses, hostnames
|
||||
|
||||
|
||||
def select_openstack_nodes(
|
||||
nodes: tobiko.Selection['OpenStackTopologyNode'],
|
||||
addresses: typing.Iterable[netaddr.IPAddress] = None,
|
||||
hostnames: typing.Iterable[str] = None,
|
||||
**kwargs) \
|
||||
-> tobiko.Selection['OpenStackTopologyNode']:
|
||||
for selector in addresses, hostnames:
|
||||
if selector is not None and not selector:
|
||||
return tobiko.Selection()
|
||||
|
||||
if addresses is not None:
|
||||
_addresses = set(addresses)
|
||||
nodes = nodes.select(
|
||||
lambda node: bool(set(node.addresses) & _addresses))
|
||||
|
||||
if hostnames is not None:
|
||||
names = {node_name_from_hostname(hostname)
|
||||
for hostname in hostnames}
|
||||
nodes = nodes.select(lambda node: node.name in names)
|
||||
@ -70,8 +112,18 @@ def list_openstack_nodes(topology: 'OpenStackTopology' = None,
|
||||
return nodes
|
||||
|
||||
|
||||
def find_openstack_node(topology=None, unique=False, **kwargs):
|
||||
nodes = list_openstack_nodes(topology=topology, **kwargs)
|
||||
def find_openstack_node(addresses: typing.Iterable[netaddr.IPAddress] = None,
|
||||
group: OpenstackGroupNamesType = None,
|
||||
hostnames: typing.Iterable[str] = None,
|
||||
topology: 'OpenStackTopology' = None,
|
||||
unique=False,
|
||||
**kwargs) \
|
||||
-> 'OpenStackTopologyNode':
|
||||
nodes = list_openstack_nodes(topology=topology,
|
||||
addresses=addresses,
|
||||
group=group,
|
||||
hostnames=hostnames,
|
||||
**kwargs)
|
||||
if unique:
|
||||
return nodes.unique
|
||||
else:
|
||||
@ -156,12 +208,13 @@ class OpenStackTopologyNode(object):
|
||||
_podman_client = None
|
||||
|
||||
def __init__(self, topology, name: str, ssh_client: ssh.SSHClientFixture,
|
||||
addresses: typing.List[netaddr.IPAddress], hostname: str):
|
||||
addresses: typing.Iterable[netaddr.IPAddress], hostname: str):
|
||||
self._topology = weakref.ref(topology)
|
||||
self.name = name
|
||||
self.ssh_client = ssh_client
|
||||
self.groups: typing.Set[str] = set()
|
||||
self.addresses: typing.List[netaddr.IPAddress] = list(addresses)
|
||||
self.addresses: tobiko.Selection[netaddr.IPAddress] = tobiko.select(
|
||||
addresses)
|
||||
self.hostname: str = hostname
|
||||
|
||||
_connection: typing.Optional[sh.ShellConnection] = None
|
||||
@ -618,7 +671,7 @@ def get_openstack_version():
|
||||
DEFAULT_TOPOLOGY_CLASS = OpenStackTopology
|
||||
|
||||
|
||||
def node_name_from_hostname(hostname):
|
||||
def node_name_from_hostname(hostname: str):
|
||||
return hostname.split('.', 1)[0].lower()
|
||||
|
||||
|
||||
|
@ -14,6 +14,9 @@
|
||||
# under the License.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import json
|
||||
import re
|
||||
import typing
|
||||
|
||||
import pytest
|
||||
@ -24,8 +27,8 @@ import tobiko
|
||||
from tobiko import config
|
||||
from tobiko.shell import ping
|
||||
from tobiko.shell import ip
|
||||
from tobiko.shell import ssh
|
||||
from tobiko.openstack import neutron
|
||||
from tobiko.openstack import nova
|
||||
from tobiko.openstack import stacks
|
||||
from tobiko.openstack import topology
|
||||
|
||||
@ -150,73 +153,185 @@ class L3HARouterTest(RouterTest):
|
||||
backup_agent['host'], state="backup")
|
||||
|
||||
|
||||
class DistributedRouterStackFixture(stacks.RouterStackFixture):
|
||||
class RouterNamespaceTestBase:
|
||||
|
||||
server_stack = tobiko.required_fixture(stacks.CirrosServerStackFixture)
|
||||
|
||||
host_groups = ['overcloud', 'compute', 'controller']
|
||||
|
||||
@property
|
||||
def hostnames(self) -> typing.List[str]:
|
||||
return sorted(
|
||||
node.hostname
|
||||
for node in topology.list_openstack_nodes(group=self.host_groups))
|
||||
|
||||
@property
|
||||
def router_stack(self) -> stacks.RouterStackFixture:
|
||||
return self.network_stack.gateway_stack
|
||||
|
||||
@property
|
||||
def network_stack(self) -> stacks.NetworkStackFixture:
|
||||
return self.server_stack.network_stack
|
||||
|
||||
@property
|
||||
def router_id(self) -> str:
|
||||
return self.router_stack.router_id
|
||||
|
||||
@property
|
||||
def router_details(self) -> neutron.RouterType:
|
||||
return self.router_stack.router_details
|
||||
|
||||
@property
|
||||
def router_namespace(self) -> str:
|
||||
return neutron.get_ovs_router_namespace(self.router_id)
|
||||
|
||||
|
||||
@neutron.skip_unless_is_ovs()
|
||||
class OvsRouterNamespaceTest(RouterNamespaceTestBase, testtools.TestCase):
|
||||
|
||||
def test_router_namespace(self):
|
||||
"""Check router namespace is being created on cloud hosts
|
||||
|
||||
When A VM running in a compute node is expected to route
|
||||
packages to a router, a network namespace is expected to exist on
|
||||
compute name that is named after the router ID
|
||||
"""
|
||||
ping.assert_reachable_hosts([self.server_stack.floating_ip_address])
|
||||
topology.assert_namespace_in_hosts(self.router_namespace,
|
||||
hostnames=self.hostnames)
|
||||
|
||||
|
||||
@neutron.skip_if_missing_networking_extensions('dvr')
|
||||
class DvrRouterStackFixture(stacks.RouterStackFixture):
|
||||
distributed = True
|
||||
|
||||
|
||||
@pytest.mark.ovn_migration
|
||||
class RouterNamespaceTest(testtools.TestCase):
|
||||
class DvrNetworkStackFixture(stacks.NetworkStackFixture):
|
||||
gateway_stack = tobiko.required_fixture(DvrRouterStackFixture,
|
||||
setup=False)
|
||||
|
||||
server_stack = tobiko.required_fixture(stacks.CirrosServerStackFixture)
|
||||
distributed_router_stack = (
|
||||
tobiko.required_fixture(DistributedRouterStackFixture))
|
||||
|
||||
@neutron.skip_unless_is_ovn()
|
||||
def test_router_namespace_on_ovn(self):
|
||||
"""Check router namespace is being created on compute host
|
||||
class DvrServerStackFixture(stacks.CirrosServerStackFixture):
|
||||
network_stack = tobiko.required_fixture(DvrNetworkStackFixture,
|
||||
setup=False)
|
||||
|
||||
When A VM running in a compute node is expected to route
|
||||
packages to a router, a network namespace is expected to exist on
|
||||
compute name that is named after the router ID
|
||||
"""
|
||||
router = self.server_stack.network_stack.gateway_details
|
||||
self.assert_has_not_router_namespace(router=router)
|
||||
|
||||
@neutron.skip_unless_is_ovs()
|
||||
def test_router_namespace_on_ovs(self):
|
||||
"""Check router namespace is being created on compute host
|
||||
L3_AGENT_MODE_DVR = re.compile(r'^dvr_')
|
||||
|
||||
When A VM running in a compute node is expected to route
|
||||
packages to a router, a network namespace is expected to exist on
|
||||
compute name that is named after the router ID
|
||||
"""
|
||||
router = self.server_stack.network_stack.gateway_details
|
||||
self.assert_has_router_namespace(router=router)
|
||||
|
||||
@neutron.skip_unless_is_ovs()
|
||||
@neutron.skip_if_missing_networking_extensions('dvr')
|
||||
def test_distributed_router_namespace(self):
|
||||
"""Test that no router namespace is created for DVR on compute node
|
||||
def is_l3_agent_mode_dvr(agent_mode: str) -> bool:
|
||||
return L3_AGENT_MODE_DVR.match(agent_mode) is not None
|
||||
|
||||
When A VM running in a compute node is not expected to route
|
||||
packages to a given router, a network namespace is not expected to
|
||||
exist on compute name that is named after the router ID
|
||||
"""
|
||||
router = self.distributed_router_stack.router_details
|
||||
self.assertTrue(router['distributed'])
|
||||
self.assert_has_not_router_namespace(router=router)
|
||||
|
||||
def assert_has_router_namespace(self, router: neutron.RouterType):
|
||||
router_namespace = f"qrouter-{router['id']}"
|
||||
self.assertIn(router_namespace,
|
||||
self.list_network_namespaces(),
|
||||
"No such router network namespace on hypervisor host")
|
||||
@neutron.skip_if_missing_networking_extensions('dvr')
|
||||
class DvrRouterNamespaceTest(RouterNamespaceTestBase, testtools.TestCase):
|
||||
|
||||
def assert_has_not_router_namespace(self, router: neutron.RouterType):
|
||||
router_namespace = f"qrouter-{router['id']}"
|
||||
self.assertNotIn(router_namespace,
|
||||
self.list_network_namespaces(),
|
||||
"Router network namespace found on hypervisor host")
|
||||
server_stack = tobiko.required_fixture(DvrServerStackFixture, setup=False)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
if self.legacy_hostnames:
|
||||
self.skipTest(f'Host(s) {self.legacy_hostnames!r} with legacy '
|
||||
'L3 agent mode')
|
||||
|
||||
host_groups = ['compute']
|
||||
|
||||
@property
|
||||
def hypervisor_ssh_client(self) -> ssh.SSHClientFixture:
|
||||
# Check the VM can reach a working gateway
|
||||
ping.assert_reachable_hosts([self.server_stack.ip_address])
|
||||
# List namespaces on hypervisor node
|
||||
hypervisor = topology.get_openstack_node(
|
||||
hostname=self.server_stack.hypervisor_hostname)
|
||||
return hypervisor.ssh_client
|
||||
def snat_namespace(self) -> str:
|
||||
return f'snat-{self.router_id}'
|
||||
|
||||
def list_network_namespaces(self) -> typing.List[str]:
|
||||
return ip.list_network_namespaces(
|
||||
ssh_client=self.hypervisor_ssh_client)
|
||||
_agent_modes: typing.Optional[typing.Dict[str, typing.List[str]]] = None
|
||||
|
||||
@property
|
||||
def agent_modes(self) \
|
||||
-> typing.Dict[str, typing.List[str]]:
|
||||
if self._agent_modes is None:
|
||||
self._agent_modes = collections.defaultdict(list)
|
||||
for node in topology.list_openstack_nodes(
|
||||
hostnames=self.hostnames):
|
||||
self._agent_modes[node.l3_agent_mode].append(node.name)
|
||||
agent_modes_dump = json.dumps(self._agent_modes,
|
||||
indent=4, sort_keys=True)
|
||||
LOG.info(f"Got L3 agent modes:\n{agent_modes_dump}")
|
||||
return self._agent_modes
|
||||
|
||||
@property
|
||||
def legacy_hostnames(self) -> typing.List[str]:
|
||||
return self.agent_modes['legacy']
|
||||
|
||||
@property
|
||||
def dvr_hostnames(self) -> typing.List[str]:
|
||||
return self.agent_modes['dvr'] + self.agent_modes['dvr_no_external']
|
||||
|
||||
@property
|
||||
def dvr_snat_hostnames(self) -> typing.List[str]:
|
||||
return self.agent_modes['dvr_snat']
|
||||
|
||||
def test_1_dvr_router_without_server(self):
|
||||
if self.dvr_hostnames:
|
||||
self.cleanup_stacks()
|
||||
self.setup_router()
|
||||
topology.assert_namespace_not_in_hosts(
|
||||
self.router_namespace,
|
||||
self.snat_namespace,
|
||||
hostnames=self.dvr_hostnames)
|
||||
else:
|
||||
self.skipTest(f'All hosts {self.hostnames!r} have '
|
||||
'dvr_snat L3 agent mode')
|
||||
|
||||
def test_2_dvr_snat_router_namespaces(self):
|
||||
if self.dvr_snat_hostnames:
|
||||
self.setup_router()
|
||||
topology.wait_for_namespace_in_hosts(
|
||||
self.router_namespace,
|
||||
self.snat_namespace,
|
||||
hostnames=self.dvr_snat_hostnames)
|
||||
else:
|
||||
self.skipTest(f'Any host {self.hostnames!r} has '
|
||||
'dvr_snat L3 agent mode')
|
||||
|
||||
def test_3_dvr_router_namespace_with_server(self):
|
||||
self.setup_server()
|
||||
self.wait_for_namespace_in_hypervisor_host()
|
||||
|
||||
def wait_for_namespace_in_hypervisor_host(self):
|
||||
hypervisor_hostname = self.server_stack.hypervisor_hostname
|
||||
agent_mode = topology.get_l3_agent_mode(hypervisor_hostname)
|
||||
LOG.info(f"Hypervisor host '{hypervisor_hostname}' has DVR agent "
|
||||
f"mode: '{agent_mode}'")
|
||||
topology.wait_for_namespace_in_hosts(self.router_namespace,
|
||||
hostnames=[hypervisor_hostname])
|
||||
|
||||
def test_4_server_is_reachable(self):
|
||||
self.setup_server()
|
||||
try:
|
||||
self.server_stack.assert_is_reachable()
|
||||
except ping.PingFailed:
|
||||
server_id = self.server_stack.server_id
|
||||
server_log = nova.get_console_output(server_id=server_id)
|
||||
LOG.exception(f"Unable to reach server {server_id}...\n"
|
||||
f"{server_log}\n")
|
||||
self.wait_for_namespace_in_hypervisor_host()
|
||||
nova.reboot_server(server=server_id)
|
||||
self.server_stack.assert_is_reachable()
|
||||
|
||||
def cleanup_stacks(self):
|
||||
for stack in [self.server_stack,
|
||||
self.network_stack,
|
||||
self.router_stack]:
|
||||
tobiko.cleanup_fixture(stack)
|
||||
|
||||
def setup_router(self):
|
||||
router = tobiko.setup_fixture(self.router_stack).router_details
|
||||
router_dump = json.dumps(router, indent=4, sort_keys=True)
|
||||
LOG.debug(f"Testing DVR router namespace: {router['id']}:\n"
|
||||
f"{router_dump}\n")
|
||||
self.assertTrue(router['distributed'])
|
||||
|
||||
def setup_network(self):
|
||||
self.setup_router()
|
||||
tobiko.setup_fixture(self.network_stack)
|
||||
|
||||
def setup_server(self):
|
||||
self.setup_network()
|
||||
tobiko.setup_fixture(self.server_stack)
|
||||
|
Loading…
Reference in New Issue
Block a user