From aff2e86ff911f8717f07b0d2089e4c2ce3d81072 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 11 Jan 2019 16:38:00 -0800 Subject: [PATCH] Adds flavor profile API tests This patch adds flavor profile API tests to the Octavia tempest plugin. Depends-On: https://review.openstack.org/626819 Change-Id: I4e76b3717ddc577a912e39edbe701c71825361d2 --- octavia_tempest_plugin/common/constants.py | 10 + .../load_balancer/v2/flavor_profile_client.py | 45 ++ .../tests/api/v2/test_flavor_profile.py | 435 ++++++++++++++++++ octavia_tempest_plugin/tests/test_base.py | 2 + 4 files changed, 492 insertions(+) create mode 100644 octavia_tempest_plugin/tests/api/v2/test_flavor_profile.py diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py index 4154e7ba..0dd0156f 100644 --- a/octavia_tempest_plugin/common/constants.py +++ b/octavia_tempest_plugin/common/constants.py @@ -28,6 +28,7 @@ OPERATING_STATUS = 'operating_status' POOLS = 'pools' PROJECT_ID = 'project_id' PROVIDER = 'provider' +PROVIDER_NAME = 'provider_name' PROVISIONING_STATUS = 'provisioning_status' REQUEST_ERRORS = 'request_errors' TOTAL_CONNECTIONS = 'total_connections' @@ -77,6 +78,8 @@ HTTP_METHOD = 'http_method' URL_PATH = 'url_path' EXPECTED_CODES = 'expected_codes' +FLAVOR_DATA = 'flavor_data' + # Other constants ACTIVE = 'ACTIVE' ADMIN_STATE_UP_TRUE = 'true' @@ -89,6 +92,8 @@ ONLINE = 'ONLINE' NO_MONITOR = 'NO_MONITOR' ERROR = 'ERROR' SORT = 'sort' +SINGLE = 'SINGLE' +ACTIVE_STANDBY = 'ACTIVE_STANDBY' # Protocols HTTP = 'HTTP' @@ -185,6 +190,9 @@ AMPHORA_STATUSES = ( STATUS_PENDING_DELETE, STATUS_DELETED, STATUS_ERROR ) +# Flavor capabilities +LOADBALANCER_TOPOLOGY = 'loadbalancer_topology' + # API valid fields SHOW_LOAD_BALANCER_RESPONSE_FIELDS = ( ADMIN_STATE_UP, CREATED_AT, DESCRIPTION, FLAVOR_ID, ID, LISTENERS, NAME, @@ -232,3 +240,5 @@ SHOW_AMPHORA_RESPONSE_FIELDS = [ VRRP_PORT_ID, HA_PORT_ID, CERT_EXPIRATION, CERT_BUSY, ROLE, STATUS, VRRP_INTERFACE, VRRP_ID, VRRP_PRIORITY, CACHED_ZONE ] + +SHOW_FLAVOR_PROFILE_FIELDS = [ID, NAME, PROVIDER_NAME, FLAVOR_DATA] diff --git a/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py b/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py index d4fad928..7f359b22 100644 --- a/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py +++ b/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py @@ -13,8 +13,12 @@ # under the License. # +from oslo_log import log as logging +from tempest.lib import exceptions + from octavia_tempest_plugin.services.load_balancer.v2 import base_client +LOG = logging.getLogger(__name__) Unset = base_client.Unset @@ -216,3 +220,44 @@ class FlavorProfileClient(base_client.BaseLBaaSClient): """ return self._delete_obj(obj_id=flavorprofile_id, ignore_errors=ignore_errors) + + def cleanup_flavor_profile(self, flavorprofile_id): + """Delete a flavor profile for tempest cleanup. + + We cannot use the cleanup_flavorprofile method as flavor profiles + do not have a provisioning_status. + + :param flavorprofile_id: The flavor profile ID to delete. + :raises AssertionError: if the expected_code isn't a valid http success + response code + :raises BadRequest: If a 400 response code is received + :raises Conflict: If a 409 response code is received + :raises Forbidden: If a 403 response code is received + :raises Gone: If a 410 response code is received + :raises InvalidContentType: If a 415 response code is received + :raises InvalidHTTPResponseBody: The response body wasn't valid JSON + :raises InvalidHttpSuccessCode: if the read code isn't an expected + http success code + :raises NotImplemented: If a 501 response code is received + :raises OverLimit: If a 413 response code is received and over_limit is + not in the response body + :raises RateLimitExceeded: If a 413 response code is received and + over_limit is in the response body + :raises ServerFault: If a 500 response code is received + :raises Unauthorized: If a 401 response code is received + :raises UnexpectedContentType: If the content-type of the response + isn't an expect type + :raises UnexpectedResponseCode: If a response code above 400 is + received and it doesn't fall into any + of the handled checks + :raises UnprocessableEntity: If a 422 response code is received and + couldn't be parsed + :returns: None if ignore_errors is True, the response status code + if not. + """ + try: + self._delete_obj(obj_id=flavorprofile_id) + except exceptions.NotFound: + # Already gone, cleanup complete + LOG.info("Flavor profile %s is already gone. " + "Cleanup considered complete.", flavorprofile_id) diff --git a/octavia_tempest_plugin/tests/api/v2/test_flavor_profile.py b/octavia_tempest_plugin/tests/api/v2/test_flavor_profile.py new file mode 100644 index 00000000..3bd6e548 --- /dev/null +++ b/octavia_tempest_plugin/tests/api/v2/test_flavor_profile.py @@ -0,0 +1,435 @@ +# Copyright 2019 Rackspace US 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. + +import copy +from operator import itemgetter +from uuid import UUID + +from oslo_serialization import jsonutils +from tempest import config +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib import exceptions + +from octavia_tempest_plugin.common import constants as const +from octavia_tempest_plugin.tests import test_base + +CONF = config.CONF + + +class FlavorProfileAPITest(test_base.LoadBalancerBaseTest): + """Test the flavor profile object API.""" + + @decorators.idempotent_id('d0e3a08e-d58a-4460-83ed-34307ca04cde') + def test_flavor_profile_create(self): + """Tests flavor profile create and basic show APIs. + + * Tests that users without the loadbalancer admin role cannot + create flavor profiles. + * Create a fully populated flavor profile. + * Validate the response reflects the requested values. + """ + # We have to do this here as the api_version and clients are not + # setup in time to use a decorator or the skip_checks mixin + if not self.lb_admin_flavor_profile_client.is_version_supported( + self.api_version, '2.6'): + raise self.skipException('Flavor profiles are only available on ' + 'Octavia API version 2.6 or newer.') + + flavor_profile_name = data_utils.rand_name( + "lb_admin_flavorprofile1-create") + flavor_data = {const.LOADBALANCER_TOPOLOGY: const.SINGLE} + flavor_data_json = jsonutils.dumps(flavor_data) + + flavor_profile_kwargs = { + const.NAME: flavor_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data_json + } + + # Test that a user without the load balancer admin role cannot + # create a flavor profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.flavor_profile_client.create_flavor_profile, + **flavor_profile_kwargs) + + # Happy path + flavor_profile = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile[const.ID]) + + UUID(flavor_profile[const.ID]) + self.assertEqual(flavor_profile_name, flavor_profile[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + flavor_profile[const.PROVIDER_NAME]) + self.assertEqual(flavor_data_json, flavor_profile[const.FLAVOR_DATA]) + + @decorators.idempotent_id('c4e17fdf-849a-4132-93ae-dfca21ce4444') + def test_flavor_profile_list(self): + """Tests flavor profile list API and field filtering. + + * Create three flavor profiles. + * Validates that non-admin accounts cannot list the flavor profiles. + * List the flavor profiles using the default sort order. + * List the flavor profiles using descending sort order. + * List the flavor profiles using ascending sort order. + * List the flavor profiles returning one field at a time. + * List the flavor profiles returning two fields. + * List the flavor profiles filtering to one of the three. + * List the flavor profiles filtered, one field, and sorted. + """ + # We have to do this here as the api_version and clients are not + # setup in time to use a decorator or the skip_checks mixin + if not self.lb_admin_flavor_profile_client.is_version_supported( + self.api_version, '2.6'): + raise self.skipException('Flavor profiles are only available on ' + 'Octavia API version 2.6 or newer.') + + # Create flavor profile 1 + flavor_profile1_name = data_utils.rand_name( + "lb_admin_flavorprofile-list-1") + flavor_data1 = {const.LOADBALANCER_TOPOLOGY: const.SINGLE} + flavor_data1_json = jsonutils.dumps(flavor_data1) + + flavor_profile1_kwargs = { + const.NAME: flavor_profile1_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data1_json + } + flavor_profile1 = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile1_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile1[const.ID]) + + # Create flavor profile 2 + flavor_profile2_name = data_utils.rand_name( + "lb_admin_flavorprofile-list-2") + flavor_data2 = {const.LOADBALANCER_TOPOLOGY: const.ACTIVE_STANDBY} + flavor_data2_json = jsonutils.dumps(flavor_data2) + + flavor_profile2_kwargs = { + const.NAME: flavor_profile2_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data2_json + } + flavor_profile2 = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile2_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile2[const.ID]) + + # Create flavor profile 3 + flavor_profile3_name = data_utils.rand_name( + "lb_admin_flavorprofile-list-3") + flavor_data3 = {const.LOADBALANCER_TOPOLOGY: const.SINGLE} + flavor_data3_json = jsonutils.dumps(flavor_data3) + + flavor_profile3_kwargs = { + const.NAME: flavor_profile3_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data3_json + } + flavor_profile3 = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile3_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile3[const.ID]) + + # default sort order (by ID) reference list + ref_id_list_asc = [flavor_profile1[const.ID], + flavor_profile2[const.ID], + flavor_profile3[const.ID]] + ref_id_list_dsc = copy.deepcopy(ref_id_list_asc) + ref_id_list_asc.sort() + ref_id_list_dsc.sort(reverse=True) + + # Test that a user without the load balancer admin role cannot + # list flavor profiles. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.flavor_profile_client.list_flavor_profiles) + + # Check the default sort order (by ID) + profiles = self.lb_admin_flavor_profile_client.list_flavor_profiles() + # Remove flavor profiles not used in this test + profiles = [prof for prof in profiles + if 'lb_admin_flavorprofile-list' in prof[const.NAME]] + self.assertEqual(3, len(profiles)) + self.assertEqual(ref_id_list_asc[0], profiles[0][const.ID]) + self.assertEqual(ref_id_list_asc[1], profiles[1][const.ID]) + self.assertEqual(ref_id_list_asc[2], profiles[2][const.ID]) + + # Check the descending sort order by name + profiles = self.lb_admin_flavor_profile_client.list_flavor_profiles( + query_params='{sort}={name}:{order}'.format( + sort=const.SORT, name=const.NAME, order=const.DESC)) + # Remove flavor profiles not used in this test + profiles = [prof for prof in profiles + if 'lb_admin_flavorprofile-list' in prof[const.NAME]] + self.assertEqual(3, len(profiles)) + self.assertEqual(flavor_profile3_name, profiles[0][const.NAME]) + self.assertEqual(flavor_profile2_name, profiles[1][const.NAME]) + self.assertEqual(flavor_profile1_name, profiles[2][const.NAME]) + + # Check the ascending sort order by name + profiles = self.lb_admin_flavor_profile_client.list_flavor_profiles( + query_params='{sort}={name}:{order}'.format( + sort=const.SORT, name=const.NAME, order=const.ASC)) + # Remove flavor profiles not used in this test + profiles = [prof for prof in profiles + if 'lb_admin_flavorprofile-list' in prof[const.NAME]] + self.assertEqual(3, len(profiles)) + self.assertEqual(flavor_profile1_name, profiles[0][const.NAME]) + self.assertEqual(flavor_profile2_name, profiles[1][const.NAME]) + self.assertEqual(flavor_profile3_name, profiles[2][const.NAME]) + + ref_profiles = [flavor_profile1, flavor_profile2, flavor_profile3] + sorted_profiles = sorted(ref_profiles, key=itemgetter(const.ID)) + + # Test fields + flavor_profile_client = self.lb_admin_flavor_profile_client + for field in const.SHOW_FLAVOR_PROFILE_FIELDS: + profiles = flavor_profile_client.list_flavor_profiles( + query_params='{fields}={field}&{fields}={name}'.format( + fields=const.FIELDS, field=field, name=const.NAME)) + # Remove flavor profiles not used in this test + profiles = [prof for prof in profiles + if 'lb_admin_flavorprofile-list' in prof[const.NAME]] + + self.assertEqual(3, len(profiles)) + self.assertEqual(sorted_profiles[0][field], profiles[0][field]) + self.assertEqual(sorted_profiles[1][field], profiles[1][field]) + self.assertEqual(sorted_profiles[2][field], profiles[2][field]) + + # Test filtering + profile = self.lb_admin_flavor_profile_client.list_flavor_profiles( + query_params='{name}={prof_name}'.format( + name=const.NAME, + prof_name=flavor_profile2[const.NAME])) + self.assertEqual(1, len(profile)) + self.assertEqual(flavor_profile2[const.ID], profile[0][const.ID]) + + # Test combined params + profiles = self.lb_admin_flavor_profile_client.list_flavor_profiles( + query_params='{provider_name}={provider}&{fields}={name}&' + '{sort}={ID}:{desc}'.format( + provider_name=const.PROVIDER_NAME, + provider=CONF.load_balancer.provider, + fields=const.FIELDS, name=const.NAME, + sort=const.SORT, ID=const.ID, + desc=const.DESC)) + # Remove flavor profiles not used in this test + profiles = [prof for prof in profiles + if 'lb_admin_flavorprofile-list' in prof[const.NAME]] + self.assertEqual(3, len(profiles)) + self.assertEqual(1, len(profiles[0])) + self.assertEqual(sorted_profiles[2][const.NAME], + profiles[0][const.NAME]) + self.assertEqual(sorted_profiles[1][const.NAME], + profiles[1][const.NAME]) + self.assertEqual(sorted_profiles[0][const.NAME], + profiles[2][const.NAME]) + + @decorators.idempotent_id('a2c2ff9a-fce1-42fd-8cfd-56dea31610f6') + def test_flavor_profile_show(self): + """Tests flavor profile show API. + + * Create a fully populated flavor profile. + * Show flavor profile details. + * Validate the show reflects the requested values. + * Validates that non-lb-admin accounts cannot see the flavor profile. + """ + # We have to do this here as the api_version and clients are not + # setup in time to use a decorator or the skip_checks mixin + if not self.lb_admin_flavor_profile_client.is_version_supported( + self.api_version, '2.6'): + raise self.skipException('Flavor profiles are only available on ' + 'Octavia API version 2.6 or newer.') + + flavor_profile_name = data_utils.rand_name( + "lb_admin_flavorprofile1-show") + flavor_data = {const.LOADBALANCER_TOPOLOGY: const.ACTIVE_STANDBY} + flavor_data_json = jsonutils.dumps(flavor_data) + + flavor_profile_kwargs = { + const.NAME: flavor_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data_json + } + + flavor_profile = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile[const.ID]) + + # Test that a user without the load balancer admin role cannot + # show a flavor profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.flavor_profile_client.show_flavor_profile, + flavor_profile[const.ID]) + + result = ( + self.lb_admin_flavor_profile_client.show_flavor_profile( + flavor_profile[const.ID])) + + self.assertEqual(flavor_profile_name, result[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + result[const.PROVIDER_NAME]) + self.assertEqual(flavor_data_json, result[const.FLAVOR_DATA]) + + @decorators.idempotent_id('32a2e285-8dfc-485f-a450-a4d450d3c3ec') + def test_flavor_profile_update(self): + """Tests flavor profile update API. + + * Create a fully populated flavor profile. + * Show flavor profile details. + * Validate the show reflects the initial values. + * Validates that non-admin accounts cannot update the flavor profile. + * Update the flavor profile details. + * Show flavor profile details. + * Validate the show reflects the updated values. + """ + + # We have to do this here as the api_version and clients are not + # setup in time to use a decorator or the skip_checks mixin + if not self.lb_admin_flavor_profile_client.is_version_supported( + self.api_version, '2.6'): + raise self.skipException('Flavor profiles are only available on ' + 'Octavia API version 2.6 or newer.') + + flavor_profile_name = data_utils.rand_name( + "lb_admin_flavorprofile1-update") + flavor_data = {const.LOADBALANCER_TOPOLOGY: const.SINGLE} + flavor_data_json = jsonutils.dumps(flavor_data) + + flavor_profile_kwargs = { + const.NAME: flavor_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data_json + } + + flavor_profile = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile[const.ID]) + + self.assertEqual(flavor_profile_name, flavor_profile[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + flavor_profile[const.PROVIDER_NAME]) + self.assertEqual(flavor_data_json, flavor_profile[const.FLAVOR_DATA]) + + flavor_profile_name2 = data_utils.rand_name( + "lb_admin_flavorprofile1-update2") + flavor_data2 = {const.LOADBALANCER_TOPOLOGY: const.ACTIVE_STANDBY} + flavor_data2_json = jsonutils.dumps(flavor_data2) + + # TODO(johnsom) Figure out a reliable second provider + flavor_profile_updated_kwargs = { + const.NAME: flavor_profile_name2, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data2_json + } + + # Test that a user without the load balancer admin role cannot + # create a flavor profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.flavor_profile_client.update_flavor_profile, + flavor_profile[const.ID], **flavor_profile_updated_kwargs) + + result = self.lb_admin_flavor_profile_client.update_flavor_profile( + flavor_profile[const.ID], **flavor_profile_updated_kwargs) + + self.assertEqual(flavor_profile_name2, result[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + result[const.PROVIDER_NAME]) + self.assertEqual(flavor_data2_json, result[const.FLAVOR_DATA]) + + # Check that a show reflects the new values + get_result = ( + self.lb_admin_flavor_profile_client.show_flavor_profile( + flavor_profile[const.ID])) + + self.assertEqual(flavor_profile_name2, get_result[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + get_result[const.PROVIDER_NAME]) + self.assertEqual(flavor_data2_json, get_result[const.FLAVOR_DATA]) + + @decorators.idempotent_id('4c2eaacf-c2c8-422a-b7dc-a30ceba6bcd4') + def test_flavor_profile_delete(self): + """Tests flavor profile create and delete APIs. + + * Creates a flavor profile. + * Validates that other accounts cannot delete the flavor profile. + * Deletes the flavor profile. + * Validates the flavor profile is in the DELETED state. + """ + # We have to do this here as the api_version and clients are not + # setup in time to use a decorator or the skip_checks mixin + if not self.lb_admin_flavor_profile_client.is_version_supported( + self.api_version, '2.6'): + raise self.skipException('Flavor profiles are only available on ' + 'Octavia API version 2.6 or newer.') + + flavor_profile_name = data_utils.rand_name( + "lb_admin_flavorprofile1-delete") + flavor_data = {const.LOADBALANCER_TOPOLOGY: const.SINGLE} + flavor_data_json = jsonutils.dumps(flavor_data) + + flavor_profile_kwargs = { + const.NAME: flavor_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.FLAVOR_DATA: flavor_data_json + } + + flavor_profile = ( + self.lb_admin_flavor_profile_client.create_flavor_profile( + **flavor_profile_kwargs)) + self.addCleanup( + self.lb_admin_flavor_profile_client.cleanup_flavor_profile, + flavor_profile[const.ID]) + + # Test that a user without the load balancer admin role cannot + # delete a flavor profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.flavor_profile_client.delete_flavor_profile, + flavor_profile[const.ID]) + + # Happy path + self.lb_admin_flavor_profile_client.delete_flavor_profile( + flavor_profile[const.ID]) + + self.assertRaises( + exceptions.NotFound, + self.lb_admin_flavor_profile_client.show_flavor_profile, + flavor_profile[const.ID]) diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py index b9354097..092fdccc 100644 --- a/octavia_tempest_plugin/tests/test_base.py +++ b/octavia_tempest_plugin/tests/test_base.py @@ -122,6 +122,8 @@ class LoadBalancerBaseTest(test.BaseTestCase): cls.mem_l7policy_client = cls.os_roles_lb_member.l7policy_client cls.mem_l7rule_client = cls.os_roles_lb_member.l7rule_client cls.mem_amphora_client = cls.os_roles_lb_member.amphora_client + cls.lb_admin_flavor_profile_client = ( + cls.os_roles_lb_admin.flavor_profile_client) @classmethod def resource_setup(cls):