diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/base.py b/whitebox_neutron_tempest_plugin/tests/scenario/base.py index 433787f..c8443a6 100644 --- a/whitebox_neutron_tempest_plugin/tests/scenario/base.py +++ b/whitebox_neutron_tempest_plugin/tests/scenario/base.py @@ -767,6 +767,70 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase): 'Command failure "{}" on "{}" nodes.'.format( cmd, group_name)) + @classmethod + def cli_create_resource(cls, cmd, + cleanup_method=None, + cleanup_args=None, + env_prefix=None, + no_id_cmd=False, + cleanup=True): + """Wrapper for OSP resource creation using commands. + Includes sourcing commonly used credentials and common + cleanup command call after test class is done/failed. + + :param cmd: Creation command to execute. + Example: 'openstack ... create ...' + :type cmd: str + + :param cleanup_method: Cleanup function to handle resource + after test class is finished/failed. + Default method is validate_command from base class in combination + with a default delete command deduced from given create command. + :type cleanup_method: function, optional + + :param cleanup_args: Arguments passed to cleanup method. + Default arguments work in combination with default cleanup_method, + which is a single argument, a figured delete appropriate command. + :type cleanup_args: tuple, optional + + :param env_prefix: Prefix added to create command. + Default prefix sources test user rc file, usually ~/openrc . + :type env_prefix: str, optional + + :param no_id_cmd: When set to True omits command suffix to return + uuid of created resource, then returns all output. + (Useful for non ordinary creation commands, or extra output parsing). + Default is False. + :type no_id_cmd: bool, optional + + :param cleanup: When set to False, skips adding cleanup in stack + (therefore not using cleanup_method and cleanup_args). + Default is True. + :type cleanup: bool, optional + + :returns: uuid of resource, or all output according to + no_id_cmd boolean. + Default is uuid. + """ + # default to overcloudrc credentials + _env_prefix = env_prefix or cls.get_osp_cmd_prefix() + _get_id_suffix = '' if no_id_cmd else ' -f value -c id' + _cmd = _env_prefix + cmd + _get_id_suffix + _id = cls.validate_command(_cmd).strip() + # default to delete using CLI, and common arguments figured from cmd + if cleanup: + _cleanup_method = cleanup_method or cls.validate_command + _cleanup_args = cleanup_args or ( + _cmd.partition('create ')[0] + 'delete {}'.format(_id),) + cls.addClassResourceCleanup(_cleanup_method, *_cleanup_args) + # length of uuid with dashes, as seen in CLI output + if not no_id_cmd and len(_id) != 36: + raise AssertionError( + "Command for resource creation failed: '{}'".format(_cmd)) + LOG.debug('Command for resource creation succeeded') + return _id + + @classmethod def find_host_virsh_name(cls, host): cmd = ("timeout 10 ssh {} sudo virsh list --name | grep -w {}").format( WB_CONF.hypervisor_host, host) diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/test_router_flavors.py b/whitebox_neutron_tempest_plugin/tests/scenario/test_router_flavors.py new file mode 100644 index 0000000..c07c714 --- /dev/null +++ b/whitebox_neutron_tempest_plugin/tests/scenario/test_router_flavors.py @@ -0,0 +1,341 @@ +# 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 uuid import uuid4 as rand_uuid + +from neutron_tempest_plugin import config +from oslo_log import log +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators + +from whitebox_neutron_tempest_plugin.tests.scenario import base as wb_base + + +CONF = config.CONF +WB_CONF = CONF.whitebox_neutron_plugin_options +LOG = log.getLogger(__name__) + + +class RouterFlavorsTestOvn(wb_base.BaseTempestTestCaseOvn): + credentials = ['primary', 'admin'] + required_extensions = ['router', 'flavors'] + + USER_DRIVER_DOT_PATH = \ + 'neutron.services.ovn_l3.service_providers.user_defined.UserDefined' + + @classmethod + def _test_router_flavors_cli_crud(cls): + """Test CRUD operations for router flavor using openstack CLI. + + Many different API requests are affected by feature when configured + + Therefore this scenario aims to realistically use many of the + affected API requests with openstack commands, + focus is on the commands which relate to user flavor. + ovn flavor is the default and will be tested by other tests. + """ + # 1) validate new drivers (service providers): ovn and user-defined + # are loaded and presented, ovn is shown as the only default + cls.validate_command( + cls.osp_cmd_prefix + 'openstack network service provider list', + pattern='|.*user-defined.*|.*False.*|.*ovn.*|.*True.*|') + # 2) create a user service profile for the router flavor + cls.profile_create_cmd = ( + 'openstack network flavor profile create ' + '--description "User defined router flavor profile" ' + '--enable ' + '--driver {} ' + '-f value -c id') + cls.user_profile_id = cls.cli_create_resource( + cmd=cls.profile_create_cmd.format(cls.USER_DRIVER_DOT_PATH)) + # 3) create user flavor + cls.flavor_create_cmd = ( + 'openstack network flavor create ' + '--service-type L3_ROUTER_NAT ' + '--description "User defined flavor for routers" ' + '{} ' + '-f value -c id') + user_flav_name = data_utils.rand_name('user-flavor') + cls.user_flavor_id = cls.cli_create_resource( + cmd=cls.flavor_create_cmd.format(user_flav_name)) + # verify flavor id is in list command output + cls.validate_command( + '{}openstack network flavor list'.format(cls.osp_cmd_prefix), + pattern=cls.user_flavor_id) + # 4) add service profile to router flavor + profile_fmt = \ + 'openstack network flavor {{}} profile {} {}'.format( + cls.user_flavor_id, + cls.user_profile_id) + cls.cli_create_resource( + profile_fmt.format('add'), + cleanup_args=(cls.osp_cmd_prefix + profile_fmt.format('remove'),), + no_id_cmd=True) + # 5) create routers with user-defined/ovn flavors, set external GW + user_router_name = data_utils.rand_name('user-router') + ovn_router_name = data_utils.rand_name('ovn-router') + cls.user_router_id = cls.cli_create_resource( + cmd='openstack router create --flavor-id {} {}'.format( + cls.user_flavor_id, + user_router_name)) + cls.validate_command( + '{}openstack router set --external-gateway {} {}'.format( + cls.osp_cmd_prefix, + CONF.network.public_network_id, + user_router_name)) + cls.ovn_router_id = cls.cli_create_resource( + cmd='openstack router create {}'.format(ovn_router_name)) + # 6) verify user router is listed + cls.validate_command( + '{}openstack router list'.format(cls.osp_cmd_prefix), + pattern=cls.user_router_id) + # 7) verify resource creation related to user flavor router (ipv4/6): + # network, subnet, ports (ovn router resources covered as default) + # networks + user_net_name = data_utils.rand_name('user-network') + cls.user_network_id = cls.cli_create_resource( + 'openstack network create {}'.format(user_net_name)) + ovn_net_name = data_utils.rand_name('ovn-network') + cls.ovn_network_id = cls.cli_create_resource( + 'openstack network create {}'.format(ovn_net_name)) + # subnet + user_subnet_name = data_utils.rand_name('user-subnet') + cls.user_subnet_id = cls.cli_create_resource( + ('openstack subnet create --subnet-range 10.1.1.0/24 ' + '--network {} {}').format( + user_net_name, + user_subnet_name)) + # add subnet to router + router_subnet_fmt = \ + 'openstack router {{}} subnet {} {}'.format( + cls.user_router_id, + cls.user_subnet_id) + cls.cli_create_resource( + router_subnet_fmt.format('add'), + cleanup_args=( + cls.osp_cmd_prefix + router_subnet_fmt.format('remove'),), + no_id_cmd=True) + # port + user_port_name = data_utils.rand_name('user-port') + cls.user_port_id = cls.cli_create_resource( + ('openstack port create --network {} --fixed-ip ' + 'subnet={} {}').format( + cls.user_network_id, + user_subnet_name, + user_port_name)) + if cls.ipv6: + # ipv6 network + user_net_ipv6_name = data_utils.rand_name('user-network-ipv6') + cls.user_network_ipv6_id = cls.cli_create_resource( + 'openstack network create {}'.format(user_net_ipv6_name)) + # ipv6 subnet + cidr_ipv6 = 'fd5a:3e75:a5d::/64' + ra_address_mode = 'slaac' + user_subnet_ipv6_name = data_utils.rand_name('user-subnet-ipv6') + cls.user_subnet_ipv6_id = cls.cli_create_resource( + ('openstack subnet create --ip-version 6 --subnet-range {0} ' + '--ipv6-ra-mode {1} --ipv6-address-mode {1} ' + '--network {2} {3}').format( + cidr_ipv6, + ra_address_mode, + user_net_ipv6_name, + user_subnet_ipv6_name)) + # ipv6 add subnet to router + router_subnet_ipv6_fmt = \ + 'openstack router {{}} subnet {} {}'.format( + cls.user_router_id, + cls.user_subnet_ipv6_id) + cls.cli_create_resource( + router_subnet_ipv6_fmt.format('add'), + cleanup_args=(cls.osp_cmd_prefix + + router_subnet_ipv6_fmt.format('remove'),), + no_id_cmd=True) + user_port_ipv6_name = data_utils.rand_name('user-port') + # ipv6 port + cls.user_port_ipv6_id = cls.cli_create_resource( + ('openstack port create --network {} --fixed-ip ' + 'subnet={} {}').format( + user_net_ipv6_name, + user_subnet_ipv6_name, + user_port_ipv6_name)) + # 8) verify fip creation related to user flavor router + cls.user_fip_id = cls.cli_create_resource( + 'openstack floating ip create {} --port {}'.format( + CONF.network.public_network_id, + user_port_name)) + + @classmethod + def resource_setup(cls): + super(RouterFlavorsTestOvn, cls).resource_setup() + cls.discover_nodes() + # skip tests if OVN router flavors feature isn't enabled + for node in cls.nodes: + if not node['is_controller']: + continue + cls.check_service_setting( + host=node, + service='neutron', + config_files=(WB_CONF.neutron_config,), + param='service_plugins', + value='ovn-router-flavors', + msg='OVN router flavors feature not enabled, skipping tests') + + cls.ipv6 = CONF.network_feature_enabled.ipv6_subnet_attributes + cls.osp_cmd_prefix = cls.get_osp_cmd_prefix() + + # Testing of router flavors CLI and CRUD starts here + # (first basic test does essential resource setup for other tests) + cls._test_router_flavors_cli_crud() + + @decorators.attr(type='negative') + @decorators.idempotent_id('5ad9c4ac-c53f-45dc-8e5e-1207d4d9b05f') + def test_router_flavors_negatives(self): + """Test CRUD actions expected failures for router flavors when + using openstack commands. + """ + # 1) test add/remove of non existing service profile to network flavor + # using id/name + bad_id = rand_uuid() + # test add non existing profile name + cmd = '{}openstack network flavor add profile {} kamehameha'.format( + self.osp_cmd_prefix, + self.user_flavor_id) + self.assertFalse( + self.validate_command( + cmd, + pattern='No ServiceProfile found for kamehameha', + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # test remove non existing profile name + self.assertFalse( + self.validate_command( + cmd.replace(' add ', ' remove ', 1), + pattern='No ServiceProfile found for kamehameha', + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # test add non existing profile id + cmd = '{}openstack network flavor add profile {} {}'.format( + self.osp_cmd_prefix, + self.user_flavor_id, + bad_id) + self.assertFalse( + self.validate_command( + cmd, + pattern='No ServiceProfile found for {}'.format(bad_id), + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # test remove non existing profile id + self.assertFalse( + self.validate_command( + cmd.replace(' add ', ' remove ', 1), + pattern='No ServiceProfile found for {}'.format(bad_id), + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # # 2) test removal of service profile from network flavor while + # # there is existing router which uses service profile + + # # NOTE(mblue): TEST MAY FAIL, WAITING FOR FIX. + # # related bug: https://bugzilla.redhat.com/show_bug.cgi?id=2237290 + # # Pattern argument for failure message can be added when fixed, + # # for now only command failure with non zero exit status verified. + # cmd = '{}openstack network flavor remove profile {} {}'.format( + # self.osp_cmd_prefix, + # self.user_flavor_id, + # self.user_profile_id) + # try: + # self.assertFalse( + # self.validate_command( + # cmd, + # ret_bool_status=True), + # 'Command should fail -> "{}"'.format(cmd)) + # # re-add profile in case of failure, prevent chain of failures + # # for next test classes/methods + # except AssertionError: + # LOG.exception('Test failed') + # self.validate_command(cmd.replace(' remove ', ' add ', 1)) + # raise + # # 3) test disable of service profile from network flavor while + # # there is existing router which uses service profile + # cmd = '{}openstack network flavor profile set --disable {}'.format( + # self.osp_cmd_prefix, + # self.user_profile_id) + # try: + # self.assertFalse( + # self.validate_command( + # cmd, + # ret_bool_status=True), + # 'Command should fail -> "{}"'.format(cmd)) + # # enable profile in case of failure, prevent chain of failures + # # for next test classes/methods + # except AssertionError: + # LOG.exception('Test failed') + # self.validate_command(cmd.replace('--disable ', '--enable ', 1)) + # raise + + # 4) test show flavor details of already deleted flavor, and bad id + cmd_fmt = '{}openstack network flavor show {{}}'.format( + self.osp_cmd_prefix) + # create and delete temporary flavor + temp_flavor_name = data_utils.rand_name('temp-flavor') + temp_flavor_id = self.cli_create_resource( + cmd=self.flavor_create_cmd.format(temp_flavor_name), + cleanup=False) + self.validate_command( + '{}openstack network flavor delete {}'.format( + self.osp_cmd_prefix, + temp_flavor_id)) + # verify failure of flavor show request for deleted id/name + cmd = cmd_fmt.format(temp_flavor_name) + self.assertFalse( + self.validate_command( + cmd, + pattern='No Flavor found for {}'.format(temp_flavor_name), + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + cmd = cmd_fmt.format(temp_flavor_id) + self.assertFalse( + self.validate_command( + cmd, + pattern='No Flavor found for {}'.format(temp_flavor_id), + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # verify failure of flavor show request for non existing id + cmd = cmd_fmt.format(bad_id) + self.assertFalse( + self.validate_command( + cmd, + pattern='No Flavor found for {}'.format(bad_id), + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # 5) test deleting network flavor while floating ip routed using + # flavor exists + cmd = '{}openstack network flavor delete {}'.format( + self.osp_cmd_prefix, + self.user_flavor_id) + try: + self.assertFalse( + self.validate_command( + cmd, + pattern='ConflictException: 409', + ret_bool_status=True), + 'Command should fail -> "{}"'.format(cmd)) + # re-create user flavor in case of failure, prevent chain of failures + # for next test classes/methods + except AssertionError: + LOG.exception('Test failed') + user_flav_name = data_utils.rand_name('user-flavor') + self.user_flavor_id = self.cli_create_resource( + cmd=self.flavor_create_cmd.format(user_flav_name)) + raise diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml index 1e39221..e04767a 100644 --- a/zuul.d/master_jobs.yaml +++ b/zuul.d/master_jobs.yaml @@ -23,6 +23,7 @@ (^whitebox_neutron_tempest_plugin.tests.scenario)" tempest_exclude_regex: "\ (^whitebox_neutron_tempest_plugin.tests.scenario.test_metadata_rate_limiting)|\ + (^whitebox_neutron_tempest_plugin.tests.scenario.test_router_flavors)|\ (^whitebox_neutron_tempest_plugin.tests.scenario.test_security_group_logging)|\ (^whitebox_neutron_tempest_plugin.tests.scenario.test_l3ha_ovn)|\ (test_multicast.*restart)|\ @@ -416,9 +417,20 @@ name: whitebox-neutron-tempest-plugin-ovn-single-thread parent: whitebox-neutron-tempest-plugin-ovn vars: + network_api_extensions_ovn: + - vlan-transparent + - ovn-router-flavors + devstack_localrc: + NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_ovn) | join(',') }}" + devstack_local_conf: + post-config: + $NEUTRON_CONF: + service_providers: + service_provider: "L3_ROUTER_NAT:user-defined:neutron.services.ovn_l3.service_providers.user_defined.UserDefined" tempest_concurrency: 1 tempest_test_regex: "\ (^whitebox_neutron_tempest_plugin.tests.scenario.test_metadata_rate_limiting)|\ + (^whitebox_neutron_tempest_plugin.tests.scenario.test_router_flavors)|\ (^whitebox_neutron_tempest_plugin.tests.scenario.test_security_group_logging)|\ (^whitebox_neutron_tempest_plugin.tests.scenario.test_l3ha_ovn)|\ (test_multicast.*restart)|\