Merge "Update OVS router namespace test cases"

This commit is contained in:
Zuul 2022-06-27 11:02:49 +00:00 committed by Gerrit Code Review
commit 48b4bade21
5 changed files with 294 additions and 81 deletions

View File

@ -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):

View File

@ -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

View File

@ -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}")

View File

@ -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()

View File

@ -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)