diff --git a/whitebox_neutron_tempest_plugin/config.py b/whitebox_neutron_tempest_plugin/config.py index 019ccb0..35d168f 100644 --- a/whitebox_neutron_tempest_plugin/config.py +++ b/whitebox_neutron_tempest_plugin/config.py @@ -25,7 +25,7 @@ WhiteboxNeutronPluginOptions = [ cfg.StrOpt('openstack_type', default='devstack', help='Type of openstack deployment, ' - 'e.g. devstack, tripeo, podified'), + 'e.g. devstack, podified'), cfg.StrOpt('pki_private_key', default='/etc/pki/tls/private/ovn_controller.key', help='File with private key. Need for TLS-everywhere ' @@ -86,5 +86,9 @@ WhiteboxNeutronPluginOptions = [ help='ssh private key file path for overcloud nodes access.'), cfg.StrOpt('neutron_config', default='/etc/neutron/neutron.conf', - help='Path to neutron configuration file.') + help='Path to neutron configuration file.'), + cfg.IntOpt('ovn_max_controller_gw_ports_per_router', + default=1, + help='The number of network nodes used ' + 'for the OVN router HA.') ] diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/base.py b/whitebox_neutron_tempest_plugin/tests/scenario/base.py index 768cdf1..cd9dc9b 100644 --- a/whitebox_neutron_tempest_plugin/tests/scenario/base.py +++ b/whitebox_neutron_tempest_plugin/tests/scenario/base.py @@ -59,8 +59,19 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase): sriov_agents = [ agent for agent in agents if 'sriov' in agent['binary']] cls.has_sriov_support = True if sriov_agents else False + # deployer tool dependent variables if WB_CONF.openstack_type == 'devstack': cls.master_node_client = cls.get_node_client('localhost') + cls.master_cont_cmd_executor = cls.run_on_master_controller + cls.neutron_api_prefix = '' + cls.neutron_conf = WB_CONF.neutron_config + elif WB_CONF.openstack_type == 'podified': + # NOTE(mblue): add podified option + pass + else: + LOG.warning(("Unrecognized deployer tool '{}', plugin supports " + "openstack_type as devstack/podified." + .format(WB_CONF.openstack_type))) @classmethod def run_on_master_controller(cls, cmd): diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/test_neutron_availability_zones_ml2ovs.py b/whitebox_neutron_tempest_plugin/tests/scenario/test_neutron_availability_zones_ml2ovs.py index a863b21..86173c8 100644 --- a/whitebox_neutron_tempest_plugin/tests/scenario/test_neutron_availability_zones_ml2ovs.py +++ b/whitebox_neutron_tempest_plugin/tests/scenario/test_neutron_availability_zones_ml2ovs.py @@ -26,7 +26,7 @@ CONF = config.CONF WB_CONF = config.CONF.whitebox_neutron_plugin_options -class NeutronAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): +class OvsAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): credentials = ['primary', 'admin'] required_extensions = ['network_availability_zone', @@ -34,7 +34,7 @@ class NeutronAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): @classmethod def resource_setup(cls): - super(NeutronAvaliabilityzonesTest, cls).resource_setup() + super(OvsAvaliabilityzonesTest, cls).resource_setup() cls.client = cls.os_adm.network_client cls.get_neutron_agent_availability_zones() if not cls.AZs_list: @@ -64,10 +64,8 @@ class NeutronAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): cls.l3_agent_list[agent.get('host')] = az else: cls.dhcp_agent_list[agent.get('host')] = az - cmd = 'sudo podman exec neutron_api crudini --get {} DEFAULT {{}}' \ - '|| echo'.format(WB_CONF.neutron_config) - if WB_CONF.openstack_type == 'devstack': - cmd_executor = cls.run_on_master_controller + cmd = '{} crudini --get {} DEFAULT {{}} || echo'.format( + cls.neutron_api_prefix, cls.neutron_conf) def integer(int_var): try: @@ -90,7 +88,7 @@ class NeutronAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): 'default_availability_zones': array} for param, func in agent_params.items(): cls.agent_conf[param] = func( - cmd_executor(cmd.format(param))) + cls.master_cont_cmd_executor(cmd.format(param))) def _check_az_router(self, az, router): """Check if a router was deployed over the agents @@ -152,8 +150,7 @@ class NeutronAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): az_hosts.append(agent_host) def test_resource_namespace(): - node_name = host.split('.')[0] + ( - '.ctlplane' if WB_CONF.openstack_type == 'tripleo' else '') + node_name = host.split('.')[0] netns = self.get_node_client(node_name).exec_command('ip netns') netspaces = [] for ns in netns.split('\n'): diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/test_router_availability_zones_ovn.py b/whitebox_neutron_tempest_plugin/tests/scenario/test_router_availability_zones_ovn.py new file mode 100644 index 0000000..8808270 --- /dev/null +++ b/whitebox_neutron_tempest_plugin/tests/scenario/test_router_availability_zones_ovn.py @@ -0,0 +1,254 @@ +# Copyright 2024 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +from neutron_lib import constants +from neutron_tempest_plugin import config +from oslo_log import log +from tempest.lib import decorators +from tempest.lib import exceptions + +from whitebox_neutron_tempest_plugin.tests.scenario import base + +AZ_SUPPORTED_AGENTS = ['OVN Controller Gateway agent'] +CONF = config.CONF +WB_CONF = CONF.whitebox_neutron_plugin_options +LOG = log.getLogger(__name__) + + +class OvnAvaliabilityzonesTest( + base.TrafficFlowTest, base.BaseTempestTestCaseOvn): + credentials = ['primary', 'admin'] + required_extensions = ['network_availability_zone', + 'availability_zone', 'router_availability_zone'] + + @classmethod + def resource_setup(cls): + super(OvnAvaliabilityzonesTest, cls).resource_setup() + if not cls.has_ovn_support: + raise cls.skipException( + "These availability zone tests are meant for OVN only.") + cls.client = cls.os_adm.network_client + cls.get_ovn_controller_gw_agent_availability_zones() + if not cls.AZs_list: + raise cls.skipException("No availability zones configured") + + @classmethod + def get_ovn_controller_gw_agent_availability_zones(cls): + """Obtain availability_zones for OVN Controller Gateway + Only OVN Controller Gateway supports router availability_zone + """ + body = cls.client.list_agents() + agents = body['agents'] + cls.AZs_list = [] + cls.ovn_controller_gw_agent_list = {} + cls.agent_conf = {} + for agent in agents: + if agent.get('agent_type') in AZ_SUPPORTED_AGENTS: + az = agent.get('availability_zone') + if (az and (az not in cls.AZs_list)): + cls.AZs_list.append(az) + cls.ovn_controller_gw_agent_list[ + agent.get('host').replace('"', '').split('.')[0]] = az + cmd = '{} crudini --get {} DEFAULT {{}} || echo'.format( + cls.neutron_api_prefix, cls.neutron_conf) + + def array(list_var): + return_list = list_var.strip().split(',') + if return_list == ['']: + LOG.debug('Empty string can not be splitted') + return [] + return return_list + + agent_params = {'default_availability_zones': array} + for param, func in agent_params.items(): + cls.agent_conf[param] = func( + cls.master_cont_cmd_executor(cmd.format(param))) + # TODO(ccamposr): Hard-coded value, should be extracted from the env + # it could be extracted from ovn database + cls.max_controller_gw_ports_per_router = \ + WB_CONF.ovn_max_controller_gw_ports_per_router + + def _check_az_router(self, az, router): + """Check that resource is correctly spawned in required AZs + + Function checks that router is assigned to the relevant + network nodes. + There are several possible scenarios: + Scenario 1 (not enough network nodes in the AZs configured): + Resource is expected to be deployed over several (lets say 3) + network nodes, but there are less or equal amount of them available + in the provided list of availability zones. In this case the resource + should be spawned over all the network nodes, + Scenario 2 (not enough AZs): + Resource is expected to be deployed over several (lets say 3) network + nodes, but there are less or equal amount of availability zones are + provided as a hint. In this case there should be at least one network + node from each availability zone to host the resource and the all other + network nodes will be chosen randomly + Scenario 3 (too many AZs): + Resource is expected to be deployed over several (lets say 3) + network nodes, but more (lets say 4) availability zones are provided + as a hint. In this case the resource should be spawned over 3 random + availability zones (one network node per zone). + """ + resource_list = self.ovn_controller_gw_agent_list + router_port = self.os_admin.network_client.list_ports( + device_owner=constants.DEVICE_OWNER_ROUTER_GW, + device_id=router['id'])['ports'][0] + chassis_list = self.get_router_gateway_chassis_list( + router_port['id']) + resource_hosts = list(self.get_router_gateway_chassis_by_id(iden) + for iden in chassis_list) + az_hosts = [] + for agent_host, agent_zone in resource_list.items(): + if agent_zone in az: + az_hosts.append(agent_host) + + # Scenario 1 from docstring + if self.max_controller_gw_ports_per_router >= len(az_hosts): + # Check that the router is deployed over all the az_hosts + for host in az_hosts: + self.assertIn(host, resource_hosts) + # Check that router isn't deployed over other az hosts + self.assertEqual(len(az_hosts), len(resource_hosts)) + # Scenario 2 from docstring + elif self.max_controller_gw_ports_per_router >= len(az): + tmp_zones = [] + # Check that the router is only deployed over az hosts + for host in resource_hosts: + self.assertIn(host, az_hosts) + tmp_zones.append(resource_list[host]) + # Check that it is deployed over all the azs + self.assertEqual(set(az).symmetric_difference(set(tmp_zones)), + set()) + # Check that the max redundancy is reached + self.assertEqual(len(resource_hosts), + self.max_controller_gw_ports_per_router) + # Scenario 3 from docstring + else: + tmp_zones = [] + # Check that the router is only deployed over az hosts + for host in resource_hosts: + self.assertIn(host, az_hosts) + tmp_zones.append(resource_list[host]) + # Check that it is deployed only th one instance per az + self.assertEqual(len(tmp_zones), len(set(tmp_zones))) + # Check that the max redundancy is reached + self.assertEqual(len(resource_hosts), + self.max_controller_gw_ports_per_router) + + @decorators.idempotent_id('bc0439e3-f503-4ebc-9194-0babe155a406') + def test_router_created_in_single_az_with_not_enough_agents(self): + """Verify that router is deployed over one AZ with NOT enough network + nodes + + Router should be deployed over all the network nodes in + the availability zone + """ + + az_host_counter = {} + for tmp_az in self.ovn_controller_gw_agent_list.values(): + tmp_counter = az_host_counter.get(tmp_az, 0) + 1 + az_host_counter[tmp_az] = tmp_counter + for tmp_az, counter in az_host_counter.items(): + if counter < self.max_controller_gw_ports_per_router: + az = tmp_az + break + else: + raise self.skipException('No availability zone with not enough ' + 'resources available') + router = self.create_router_by_client(availability_zone_hints=[az]) + self._check_az_router([az], router) + + @decorators.idempotent_id('584c2d71-282f-4ea1-8360-77bde6cdcbf1') + def test_router_created_in_single_az_with_enough_agents(self): + """Verify that router is deployed over one AZ with enough + network nodes + + Router should be deployed over the required amount of the + network nodes in the specified availability zone + """ + + az_host_counter = {} + for tmp_az in self.ovn_controller_gw_agent_list.values(): + tmp_counter = az_host_counter.get(tmp_az, 0) + 1 + az_host_counter[tmp_az] = tmp_counter + for tmp_az, counter in az_host_counter.items(): + if counter >= self.max_controller_gw_ports_per_router: + az = tmp_az + break + else: + raise self.skipException('No availability zone with enough ' + 'resources available') + router = self.create_router_by_client(availability_zone_hints=[az]) + self._check_az_router([az], router) + + @decorators.idempotent_id('b3c42bd9-6888-4397-88f1-c624048da138') + def test_router_created_in_all_azs(self): + """Verify that router is created in all available AZs + + If the amount of availability zones is more than the + OVN gw max number the test will be skipped as the router + will not be created in all the available AZs + """ + + if len(self.AZs_list) == 1: + raise self.skipException("Only one availability zone configured " + "but multiple zones required") + router = self.create_router_by_client( + availability_zone_hints=self.AZs_list) + self._check_az_router(self.AZs_list, router) + + @decorators.idempotent_id('e9a9ae58-19aa-41e3-bfb6-5c6881db5a17') + def test_router_created_in_several_azs(self): + """Verify that router is created in several but not all the AZs + + Test will be executed in the environments with more than 1 OVN gw + per router and with more than 2 availability zones + """ + + if len(self.AZs_list) <= 2: + raise self.skipException("More than 2 AZs required") + if self.max_controller_gw_ports_per_router <= 1: + raise self.skipException("More than 1 OVN gw required") + amount = min(self.max_controller_gw_ports_per_router, + len(self.AZs_list) - 1) + router = self.create_router_by_client( + availability_zone_hints=self.AZs_list[:amount]) + self._check_az_router(self.AZs_list[:amount], router) + + @decorators.idempotent_id('b2ea83d8-7b2c-4c58-9c3f-d87cc04b4f1b') + def test_router_created_in_default_azs(self): + """Verify that router is correctly spawned in default AZs + + If there is no parameter specified for default AZs, all the AZs + are recognized as default. + """ + + router = self.create_router_by_client() + azs = self.agent_conf['default_availability_zones'] or self.AZs_list + self._check_az_router(azs, router) + + @decorators.idempotent_id('64bfe42a-3420-4922-8c06-1b87a3d10da2') + def test_router_creation_failed_for_not_existing_az(self): + """Verify that router creation failed if AZ doesn't exist""" + + tmp_az_list = [] + for az in self.AZs_list: + tmp_az_list.append(az) + tmp_az_list.append('not_existing_az') + self.assertRaises(exceptions.NotFound, + self.create_router_by_client, + availability_zone_hints=tmp_az_list)