diff --git a/whitebox_neutron_tempest_plugin/config.py b/whitebox_neutron_tempest_plugin/config.py index 9fa045d..019ccb0 100644 --- a/whitebox_neutron_tempest_plugin/config.py +++ b/whitebox_neutron_tempest_plugin/config.py @@ -83,5 +83,8 @@ WhiteboxNeutronPluginOptions = [ help='Common user to access openstack nodes via ssh.'), cfg.StrOpt('overcloud_key_file', default='/home/tempest/.ssh/id_rsa', - help='ssh private key file path for overcloud nodes access.') + 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.') ] diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/base.py b/whitebox_neutron_tempest_plugin/tests/scenario/base.py index b9e4d40..c0776d9 100644 --- a/whitebox_neutron_tempest_plugin/tests/scenario/base.py +++ b/whitebox_neutron_tempest_plugin/tests/scenario/base.py @@ -66,7 +66,7 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase): output, errors = local_utils.run_local_cmd(cmd) LOG.debug("Stderr: {}".format(errors.decode())) output = output.decode() - LOG.debug("Output: {}".format(output)) + LOG.debug("Output: {}".format(output)) return output.strip() def get_host_for_server(self, server_id): 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 new file mode 100644 index 0000000..a863b21 --- /dev/null +++ b/whitebox_neutron_tempest_plugin/tests/scenario/test_neutron_availability_zones_ml2ovs.py @@ -0,0 +1,418 @@ +# 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.common import utils +from neutron_tempest_plugin import config +from tempest.lib import decorators +from tempest.lib import exceptions + +from whitebox_neutron_tempest_plugin.tests.scenario import base + +AZ_SUPPORTED_AGENTS = [constants.AGENT_TYPE_DHCP, constants.AGENT_TYPE_L3] +CONF = config.CONF +WB_CONF = config.CONF.whitebox_neutron_plugin_options + + +class NeutronAvaliabilityzonesTest(base.BaseTempestWhiteboxTestCase): + + credentials = ['primary', 'admin'] + required_extensions = ['network_availability_zone', + 'availability_zone', 'router_availability_zone'] + + @classmethod + def resource_setup(cls): + super(NeutronAvaliabilityzonesTest, cls).resource_setup() + cls.client = cls.os_adm.network_client + cls.get_neutron_agent_availability_zones() + if not cls.AZs_list: + raise cls.skipException("No availability zones configured") + if cls.has_ovn_support: + raise cls.skipException( + "These availability zone tests are supported on OVS only.") + + @classmethod + def get_neutron_agent_availability_zones(cls): + """Obtain list agents availability_zones for an agent type + Only L3 and DHCP agent support availability_zone, default + availability_zone is "nova". + """ + body = cls.client.list_agents() + agents = body['agents'] + cls.AZs_list = [] + cls.l3_agent_list = {} + cls.dhcp_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) + if agent.get('agent_type') == constants.AGENT_TYPE_L3: + 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 + + def integer(int_var): + try: + return int(int_var) + except ValueError: + return 0 + + def array(list_var): + try: + return_list = list_var.strip().split(',') + if return_list == ['']: + raise AttributeError('Empty string can not be splitted') + return return_list + except AttributeError: + return [] + + agent_params = {'max_l3_agents_per_router': integer, + 'min_l3_agents_per_router': integer, + 'dhcp_agents_per_network': integer, + 'default_availability_zones': array} + for param, func in agent_params.items(): + cls.agent_conf[param] = func( + cmd_executor(cmd.format(param))) + + def _check_az_router(self, az, router): + """Check if a router was deployed over the agents + in the configured Azs + """ + self._check_az_resource(az, router=router) + + def _check_az_network(self, az, network): + """Check if a network was deployed over the agents + in the configured Azs + """ + self._check_az_resource(az, network=network) + + def _check_az_resource(self, az, network=None, router=None): + """Check that resource is correctly spawned in required AZs + + Function checks that router or network (resource) is assigned to the + relevant agents plus verifies that there are network namespaces + configured on the nodes. + There are several possible scenarios: + Scenario 1 (not enough agents): + Resource is expected to be deployed over several (lets say 3) + agents, 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 available agents + Scenario 2 (not enough AZs): + Resource is expected to be deployed over several (lets say 3) agents, + but there are less or equal amount of availability zones are provided + as a hint. In this case there should be at least one agent from each + availability zone to host the resource and the all other agents will + be chosen randomly + Scenario 3 (too many AZs): + Resource is expected to be deployed over several (lets say 3) + agents, 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 agent per zone). + """ + if not network and not router: + raise AttributeError('Network or router object required') + elif network and router: + raise AttributeError('Only one object should be provided') + elif network: + resource = network + resource_list = self.dhcp_agent_list + netspace_name = 'qdhcp-' + network['id'] + conf_param = 'dhcp_agents_per_network' + list_func = self.client.list_dhcp_agent_hosting_network + else: + resource = router + resource_list = self.l3_agent_list + netspace_name = 'qrouter-' + router['id'] + conf_param = 'max_l3_agents_per_router' + list_func = self.client.list_l3_agents_hosting_router + body = list_func(resource['id']) + resource_hosts = list(agent['host'] for agent in body['agents']) + az_hosts = [] + for agent_host, agent_zone in resource_list.items(): + if agent_zone in az: + az_hosts.append(agent_host) + + def test_resource_namespace(): + node_name = host.split('.')[0] + ( + '.ctlplane' if WB_CONF.openstack_type == 'tripleo' else '') + netns = self.get_node_client(node_name).exec_command('ip netns') + netspaces = [] + for ns in netns.split('\n'): + netspaces.append(ns.split(' ')[0]) + if netspace_name in netspaces: + return True + else: + return False + + # Scenario 1 from docstring + if self.agent_conf[conf_param] >= len(az_hosts): + for host in az_hosts: + self.assertIn(host, resource_hosts) + utils.wait_until_true(test_resource_namespace) + # Scenario 2 from docstring + elif self.agent_conf[conf_param] >= len(az): + for host in resource_hosts: + self.assertIn(host, az_hosts) + utils.wait_until_true(test_resource_namespace) + tmp_zones = [] + for agent_details in body['agents']: + self.assertIn(agent_details['availability_zone'], az) + tmp_zones.append(agent_details['availability_zone']) + self.assertEqual(set(az).symmetric_difference(set(tmp_zones)), + set()) + # Scenario 3 from docstring + else: + for host in resource_hosts: + self.assertIn(host, az_hosts) + utils.wait_until_true(test_resource_namespace) + tmp_zones = [] + for agent_details in body['agents']: + self.assertIn(agent_details['availability_zone'], az) + tmp_zones.append(agent_details['availability_zone']) + self.assertTrue(set(az).issuperset(set(tmp_zones))) + self.assertEqual(len(tmp_zones), len(set(tmp_zones))) + + @decorators.idempotent_id('7e677f27-097e-4331-a26c-47f02546d295') + def test_neutron_AZs_router_network_1az(self): + """Check that a router and network configured with only one + AZ is deployed over nodes in this AZ + """ + for az in self.AZs_list: + network = self.create_network(availability_zone_hints=[az]) + subnet = self.create_subnet(network) + router = self.create_router_by_client(availability_zone_hints=[az]) + self.create_router_interface(router['id'], subnet['id']) + self._check_az_router([az], router) + self._check_az_network([az], network) + + @decorators.idempotent_id('fe77b6df-5de0-4876-9a97-8e6b3e7617e6') + def test_network_created_in_single_az_with_not_enough_agents(self): + """Verify that network is deployed over one AZ with NOT enough agents + + Network should be deployed over all the agents in the availability zone + """ + + az_host_counter = {} + for tmp_az in self.dhcp_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.agent_conf['dhcp_agents_per_network']: + az = tmp_az + break + else: + raise self.skipException('No availability zone with not enough ' + 'resources available') + network = self.create_network(availability_zone_hints=[az]) + self.create_subnet(network) + self._check_az_network([az], network) + + @decorators.idempotent_id('51bca87e-fc8d-41b0-9a03-d8f2e16de322') + def test_network_created_in_single_az_with_enough_agents(self): + """Verify that network is deployed over one AZ with enough agents + + Network should be deployed over the required amount of the agents + in the specified availability zone + """ + + az_host_counter = {} + for tmp_az in self.dhcp_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.agent_conf['dhcp_agents_per_network']: + az = tmp_az + break + else: + raise self.skipException('No availability zone with enough ' + 'resources available') + network = self.create_network(availability_zone_hints=[az]) + self.create_subnet(network) + self._check_az_network([az], network) + + @decorators.idempotent_id('f9244141-d176-4d54-b21b-305c81f3aca0') + def test_network_created_in_all_azs(self): + """Verify that network is created in all available AZs + + If the amount of availability zones is more then the + `dhcp_agents_per_network` the test will be scipped as the network + 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") + network = self.create_network( + availability_zone_hints=self.AZs_list) + self.create_subnet(network) + self._check_az_network(self.AZs_list, network) + + @decorators.idempotent_id('95465489-ef2c-4c25-84d1-650b9b1395b6') + def test_network_created_in_several_azs(self): + """Verify that network is created in several but not all the AZs + + Test will be executed in the environments with more than 1 DHCP agent + per network and with more than 2 availability zones + """ + + if len(self.AZs_list) <= 2: + raise self.skipException("More than 2 AZs required") + if self.agent_conf['dhcp_agents_per_network'] <= 1: + raise self.skipException("More than 1 DHCP agents required") + amount = min(self.agent_conf['dhcp_agents_per_network'], + len(self.AZs_list) - 1) + network = self.create_network( + availability_zone_hints=self.AZs_list[:amount]) + self.create_subnet(network) + self._check_az_network(self.AZs_list[:amount], network) + + @decorators.idempotent_id('c559f79b-3941-4978-9337-ab0e97a7b6f5') + def test_network_created_in_default_azs(self): + """Verify that network is correctly spawened in default AZs + + If there is no parameter specified for default AZs, all the AZs + are recognized as default. + """ + + if len(self.agent_conf['default_availability_zones']) > \ + self.agent_conf['dhcp_agents_per_network']: + raise self.skipException('something') + network = self.create_network() + self.create_subnet(network) + azs = self.agent_conf['default_availability_zones'] or self.AZs_list + self._check_az_network(azs, network) + + @decorators.idempotent_id('2673b58d-6875-4232-bf47-8cc5e9aa4479') + def test_network_creation_failed_for_not_existing_az(self): + """Verify that network 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_network, + availability_zone_hints=tmp_az_list) + + @decorators.idempotent_id('22be1c6d-9bf9-4c79-a9ce-1225fef885ad') + def test_router_created_in_single_az_with_not_enough_agents(self): + """Verify that router is deployed over one AZ with NOT enough agents + + Router should be deployed over all the agents in the availability zone + """ + + az_host_counter = {} + for tmp_az in self.l3_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.agent_conf['max_l3_agents_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('a679b0fb-9f25-4faa-8132-405a5afb722a') + def test_router_created_in_single_az_with_enough_agents(self): + """Verify that router is deployed over one AZ with enough agents + + Router should be deployed over the required amount of the agents + in the specified availability zone + """ + + az_host_counter = {} + for tmp_az in self.l3_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.agent_conf['max_l3_agents_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('d22c3ee0-0241-4a86-8ded-0a0b9d0fefb9') + 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 then the + `max_l3_agents_per_router` the test will be scipped 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('eeb04734-0c78-4fa5-81cb-569bfdc4daeb') + 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 L3 agent + 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.agent_conf['max_l3_agents_per_router'] <= 1: + raise self.skipException("More than 1 L3 agents required") + amount = min(self.agent_conf['max_l3_agents_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('26d05e2e-c491-43f2-8368-f396e994b14f') + 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. + """ + + if len(self.agent_conf['default_availability_zones']) > \ + self.agent_conf['max_l3_agents_per_router']: + raise self.skipException('something') + 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('8ab54f63-2b3c-48fb-87b4-e4785b72c543') + 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)