diff --git a/octavia_tempest_plugin/clients.py b/octavia_tempest_plugin/clients.py index f2a77671..7fe36067 100644 --- a/octavia_tempest_plugin/clients.py +++ b/octavia_tempest_plugin/clients.py @@ -17,6 +17,12 @@ from tempest import config from octavia_tempest_plugin.services.load_balancer.v2 import ( amphora_client) +from octavia_tempest_plugin.services.load_balancer.v2 import ( + availability_zone_capabilities_client) +from octavia_tempest_plugin.services.load_balancer.v2 import ( + availability_zone_client) +from octavia_tempest_plugin.services.load_balancer.v2 import ( + availability_zone_profile_client) from octavia_tempest_plugin.services.load_balancer.v2 import ( flavor_capabilities_client) from octavia_tempest_plugin.services.load_balancer.v2 import ( @@ -74,3 +80,11 @@ class ManagerV2(clients.Manager): self.provider_client = provider_client.ProviderClient(**params) self.flavor_capabilities_client = ( flavor_capabilities_client.FlavorCapabilitiesClient(**params)) + self.availability_zone_capabilities_client = ( + availability_zone_capabilities_client + .AvailabilityZoneCapabilitiesClient(**params)) + self.availability_zone_profile_client = ( + availability_zone_profile_client.AvailabilityZoneProfileClient( + **params)) + self.availability_zone_client = ( + availability_zone_client.AvailabilityZoneClient(**params)) diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py index 44bf3e50..d4e1755f 100644 --- a/octavia_tempest_plugin/common/constants.py +++ b/octavia_tempest_plugin/common/constants.py @@ -14,12 +14,17 @@ # API field names ACTIVE_CONNECTIONS = 'active_connections' +AVAILABILITY_ZONE = 'availability_zone' +AVAILABILITY_ZONE_DATA = 'availability_zone_data' +AVAILABILITY_ZONE_PROFILE_ID = 'availability_zone_profile_id' ADMIN_STATE_UP = 'admin_state_up' BYTES_IN = 'bytes_in' BYTES_OUT = 'bytes_out' CREATED_AT = 'created_at' DESCRIPTION = 'description' +FLAVOR_DATA = 'flavor_data' FLAVOR_ID = 'flavor_id' +FLAVOR_PROFILE_ID = 'flavor_profile_id' ID = 'id' LISTENERS = 'listeners' LOADBALANCER = 'loadbalancer' @@ -78,9 +83,7 @@ HTTP_METHOD = 'http_method' URL_PATH = 'url_path' EXPECTED_CODES = 'expected_codes' -FLAVOR_DATA = 'flavor_data' ENABLED = 'enabled' -FLAVOR_PROFILE_ID = 'flavor_profile_id' # Other constants ACTIVE = 'ACTIVE' @@ -202,6 +205,10 @@ AMPHORA_PROVIDERS = ['amphora', 'amphorav2', 'octavia'] # Flavor capabilities LOADBALANCER_TOPOLOGY = 'loadbalancer_topology' +# Availability zone capabilities +COMPUTE_ZONE = 'compute_zone' +MANAGEMENT_NETWORK = 'management_network' + # API valid fields SHOW_LOAD_BALANCER_RESPONSE_FIELDS = ( ADMIN_STATE_UP, CREATED_AT, DESCRIPTION, FLAVOR_ID, ID, LISTENERS, NAME, @@ -253,3 +260,9 @@ SHOW_AMPHORA_RESPONSE_FIELDS = [ SHOW_FLAVOR_PROFILE_FIELDS = [ID, NAME, PROVIDER_NAME, FLAVOR_DATA] SHOW_FLAVOR_FIELDS = [ID, NAME, DESCRIPTION, ENABLED, FLAVOR_PROFILE_ID] + +SHOW_AVAILABILITY_ZONE_PROFILE_FIELDS = [ + ID, NAME, PROVIDER_NAME, AVAILABILITY_ZONE_DATA] + +SHOW_AVAILABILITY_ZONE_FIELDS = [ + NAME, DESCRIPTION, ENABLED, AVAILABILITY_ZONE_PROFILE_ID] diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py index 29dc1da8..8573d892 100644 --- a/octavia_tempest_plugin/config.py +++ b/octavia_tempest_plugin/config.py @@ -132,6 +132,13 @@ OctaviaGroup = [ 'topology. One of: SINGLE - One amphora per load ' 'balancer. ACTIVE_STANDBY - Two amphora per load ' 'balancer.'}), + cfg.DictOpt('expected_availability_zone_capability', + help=('Defines a provider availability zone capability that ' + 'is expected to be present in the selected provider ' + 'under test. It is specified in a "name": "description" ' + 'dict. Example: {"compute_zone": "The compute ' + 'availability zone."}'), + default={'compute_zone': 'The compute availability zone.'}), # Networking cfg.BoolOpt('test_with_ipv6', default=True, diff --git a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py new file mode 100644 index 00000000..92696a77 --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py @@ -0,0 +1,79 @@ +# Copyright 2019 Rackspace US Inc. All rights reserved. +# Copyright 2019 Verizon Media +# +# 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 octavia_tempest_plugin.services.load_balancer.v2 import base_client +from octavia_tempest_plugin.services.load_balancer.v2 import provider_client + +Unset = base_client.Unset + + +class AvailabilityZoneCapabilitiesClient(base_client.BaseLBaaSClient): + + list_root_tag = 'availability_zone_capabilities' + + def __init__(self, *args, **kwargs): + super(AvailabilityZoneCapabilitiesClient, self).__init__( + *args, **kwargs) + providers_list_root_tag = provider_client.ProviderClient.list_root_tag + # /v2.0/lbaas/providers//availability_zone_capabilities + self.uri = "{provider_base_uri}/{parent}/{object}".format( + provider_base_uri=self.base_uri.format( + object=providers_list_root_tag), + parent="{parent}", + object=self.list_root_tag + ) + + def list_availability_zone_capabilities(self, provider, query_params=None, + return_object_only=True): + """Get a list of provider availability zone capability objects. + + :param provider: The provider to query for availability zone + capabilities. + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :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 NotFound: If a 404 response code is received + :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: A list of availability zone capability objects. + """ + return self._list_objects(parent_id=provider, + query_params=query_params, + return_object_only=return_object_only) diff --git a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py new file mode 100644 index 00000000..c729f21a --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py @@ -0,0 +1,274 @@ +# 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. +# + +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 + + +class AvailabilityZoneClient(base_client.BaseLBaaSClient): + + root_tag = 'availability_zone' + list_root_tag = 'availability_zones' + + resource_path = 'availabilityzones' + + def __init__(self, *args, **kwargs): + super(AvailabilityZoneClient, self).__init__(*args, **kwargs) + self.uri = self.base_uri.format(object=self.resource_path) + + def create_availability_zone(self, name, availability_zone_profile_id, + description=Unset, enabled=Unset, + return_object_only=True): + """Create an availability zone. + + :param name: Human-readable name of the resource. + :param availability_zone_profile_id: The ID of the associated + availability zone profile. + :param description: A human-readable description for the resource. + :param enabled: If the resource is available for use. + The default is True. + :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 NotFound: If a 404 response code is received + :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: An availability zone object. + """ + kwargs = {arg: value for arg, value in locals().items() + if arg != 'self' and value is not Unset} + return self._create_object(**kwargs) + + def show_availability_zone(self, availability_zone_name, query_params=None, + return_object_only=True): + """Get the availability zone details. + + :param availability_zone_name: The availability zone name to query. + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :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 NotFound: If a 404 response code is received + :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: An availability zone object. + """ + return self._show_object(obj_id=availability_zone_name, + query_params=query_params, + return_object_only=return_object_only) + + def list_availability_zones(self, query_params=None, + return_object_only=True): + """Get a list of availability zone objects. + + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :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 NotFound: If a 404 response code is received + :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: A list of availability zone objects. + """ + return self._list_objects(query_params=query_params, + return_object_only=return_object_only) + + def update_availability_zone(self, availability_zone_name, + description=Unset, enabled=Unset, + return_object_only=True): + """Update an availability zone. + + :param availability_zone_name: The availability zone name to update. + :param description: A human-readable description for the resource. + :param enabled: If the resource is available for use. + :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 NotFound: If a 404 response code is received + :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: An availability zone object. + """ + kwargs = {arg: value for arg, value in locals().items() + if arg != 'self' and value is not Unset} + kwargs['obj_id'] = kwargs.pop('availability_zone_name') + return self._update_object(**kwargs) + + def delete_availability_zone(self, availability_zone_name, + ignore_errors=False): + """Delete an availability zone. + + :param availability_zone_name: The availability zone name to delete. + :param ignore_errors: True if errors should be ignored. + :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 NotFound: If a 404 response code is received + :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. + """ + return self._delete_obj(obj_id=availability_zone_name, + ignore_errors=ignore_errors) + + def cleanup_an_availability_zone(self, availability_zone_name): + """Delete an availability zone for tempest cleanup. + + We cannot use the cleanup_availability_zone method as availability + zones do not have a provisioning_status. + + :param availability_zone_name: The availability zone name 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=availability_zone_name) + except exceptions.NotFound: + # Already gone, cleanup complete + LOG.info("Availability zone %s is already gone. " + "Cleanup considered complete.", availability_zone_name) diff --git a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py new file mode 100644 index 00000000..631162d1 --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py @@ -0,0 +1,280 @@ +# 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. +# + +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 + + +class AvailabilityZoneProfileClient(base_client.BaseLBaaSClient): + + root_tag = 'availability_zone_profile' + list_root_tag = 'availability_zone_profiles' + + resource_path = 'availabilityzoneprofiles' + + def __init__(self, *args, **kwargs): + super(AvailabilityZoneProfileClient, self).__init__(*args, **kwargs) + self.uri = self.base_uri.format(object=self.resource_path) + + def create_availability_zone_profile(self, name, provider_name, + availability_zone_data, + return_object_only=True): + """Create an availability zone profile. + + :param name: Human-readable name of the resource. + :param provider_name: The octavia provider name. + :param availability_zone_data: The JSON string containing the + availability zone metadata. + :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 NotFound: If a 404 response code is received + :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: An availability zone profile object. + """ + kwargs = {arg: value for arg, value in locals().items() + if arg != 'self' and value is not Unset} + return self._create_object(**kwargs) + + def show_availability_zone_profile(self, availability_zone_profile_id, + query_params=None, + return_object_only=True): + """Get the availability zone profile details. + + :param availability_zone_profile_id: The availability zone profile ID + to query. + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :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 NotFound: If a 404 response code is received + :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: An availability zone profile object. + """ + return self._show_object(obj_id=availability_zone_profile_id, + query_params=query_params, + return_object_only=return_object_only) + + def list_availability_zone_profiles(self, query_params=None, + return_object_only=True): + """Get a list of availability zone profile objects. + + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :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 NotFound: If a 404 response code is received + :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: A list of availability zone profile objects. + """ + return self._list_objects(query_params=query_params, + return_object_only=return_object_only) + + def update_availability_zone_profile( + self, availability_zone_profile_id, name=Unset, provider_name=Unset, + availability_zone_data=Unset, return_object_only=True): + """Update an availability zone profile. + + :param availability_zone_profile_id: The availability zone profile ID + to update. + :param name: Human-readable name of the resource. + :param provider_name: The octavia provider name. + :param availability_zone_data: The JSON string containing the + availability zone metadata. + :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 NotFound: If a 404 response code is received + :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: An availability zone profile object. + """ + kwargs = {arg: value for arg, value in locals().items() + if arg != 'self' and value is not Unset} + kwargs['obj_id'] = kwargs.pop('availability_zone_profile_id') + return self._update_object(**kwargs) + + def delete_availability_zone_profile(self, availability_zone_profile_id, + ignore_errors=False): + """Delete an availability zone profile. + + :param availability_zone_profile_id: The availability zone profile ID + to delete. + :param ignore_errors: True if errors should be ignored. + :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 NotFound: If a 404 response code is received + :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. + """ + return self._delete_obj(obj_id=availability_zone_profile_id, + ignore_errors=ignore_errors) + + def cleanup_availability_zone_profile(self, availability_zone_profile_id): + """Delete an availability zone profile for tempest cleanup. + + We cannot use the cleanup_availability_zone_profile method as + availability zone profiles do not have a provisioning_status. + + :param availability_zone_profile_id: The availability zone 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=availability_zone_profile_id) + except exceptions.NotFound: + # Already gone, cleanup complete + LOG.info("Availability zone profile %s is already gone. " + "Cleanup considered complete.", + availability_zone_profile_id) diff --git a/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py b/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py new file mode 100644 index 00000000..023d5f55 --- /dev/null +++ b/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py @@ -0,0 +1,492 @@ +# 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 oslo_serialization import jsonutils +from oslo_utils import uuidutils +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 AvailabilityZoneAPITest(test_base.LoadBalancerBaseTest): + """Test the availability zone object API.""" + + @classmethod + def resource_setup(cls): + """Setup resources needed by the tests.""" + super(AvailabilityZoneAPITest, cls).resource_setup() + + # 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 (cls.lb_admin_availability_zone_profile_client + .is_version_supported(cls.api_version, '2.14')): + return + + # Create a shared availability zone profile + availability_zone_profile_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile-setup") + availability_zone_data = { + const.COMPUTE_ZONE: 'my_compute_zone', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data_json = jsonutils.dumps(availability_zone_data) + + availability_zone_profile_kwargs = { + const.NAME: availability_zone_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data_json + } + + cls.availability_zone_profile = ( + cls.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile_kwargs)) + cls.addClassResourceCleanup( + cls.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + cls.availability_zone_profile[const.ID]) + cls.availability_zone_profile_id = ( + cls.availability_zone_profile[const.ID]) + + @decorators.idempotent_id('3899ef15-37c3-48a3-807f-8bb10bd295f0') + def test_availability_zone_create(self): + """Tests availability zone create and basic show APIs. + + * Tests that users without the loadbalancer admin role cannot + create an availability zone. + * Create a fully populated availability zone. + * 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_availability_zone_client.is_version_supported( + self.api_version, '2.14'): + raise self.skipException('Availability zones are only available ' + 'on Octavia API version 2.14 or newer.') + availability_zone_name = data_utils.rand_name( + "lb_admin_availability_zone-create") + availability_zone_description = data_utils.arbitrary_string(size=255) + + availability_zone_kwargs = { + const.NAME: availability_zone_name, + const.DESCRIPTION: availability_zone_description, + const.ENABLED: True, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + # Test that a user without the load balancer admin role cannot + # create an availability zone + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises(exceptions.Forbidden, + self.os_primary.availability_zone_client + .create_availability_zone, + **availability_zone_kwargs) + + # Happy path + availability_zone = ( + self.lb_admin_availability_zone_client + .create_availability_zone(**availability_zone_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + availability_zone[const.NAME]) + + self.assertEqual(availability_zone_name, availability_zone[const.NAME]) + self.assertEqual(availability_zone_description, + availability_zone[const.DESCRIPTION]) + self.assertTrue(availability_zone[const.ENABLED]) + self.assertEqual(self.availability_zone_profile_id, + availability_zone[const.AVAILABILITY_ZONE_PROFILE_ID]) + + @decorators.idempotent_id('bba84c0c-2832-4c4c-90ff-d28acfe4ae36') + def test_availability_zone_list(self): + """Tests availability zone list API and field filtering. + + * Create three availability zones. + * Validates that non-admin accounts cannot list the availability zones. + * List the availability zones using the default sort order. + * List the availability zones using descending sort order. + * List the availability zones using ascending sort order. + * List the availability zones returning one field at a time. + * List the availability zones returning two fields. + * List the availability zones filtering to one of the three. + * List the availability zones 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_availability_zone_client.is_version_supported( + self.api_version, '2.14'): + raise self.skipException('Availability zones are only available ' + 'on Octavia API version 2.14 or newer.') + + # Create availability zone 1 + az1_name = data_utils.rand_name("lb_admin_availability_zone-list-1") + az1_description = 'A' + + az1_kwargs = { + const.NAME: az1_name, + const.DESCRIPTION: az1_description, + const.ENABLED: True, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + az1 = (self.lb_admin_availability_zone_client + .create_availability_zone(**az1_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + az1[const.NAME]) + + # Create availability zone 2 + az2_name = data_utils.rand_name("lb_admin_availability_zone-list-2") + az2_description = 'B' + + az2_kwargs = { + const.NAME: az2_name, + const.DESCRIPTION: az2_description, + const.ENABLED: False, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + az2 = (self.lb_admin_availability_zone_client + .create_availability_zone(**az2_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + az2[const.NAME]) + + # Create availability zone 3 + az3_name = data_utils.rand_name("lb_admin_availability_zone-list-3") + az3_description = 'C' + + az3_kwargs = { + const.NAME: az3_name, + const.DESCRIPTION: az3_description, + const.ENABLED: True, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + az3 = (self.lb_admin_availability_zone_client + .create_availability_zone(**az3_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + az3[const.NAME]) + + # default sort order (by Name) reference list + ref_id_list_asc = [az1[const.NAME], az2[const.NAME], + az3[const.NAME]] + 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 role cannot + # list availability zones. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_client + .list_availability_zones) + + # Check the default sort order (by ID) + availability_zones = ( + self.mem_availability_zone_client.list_availability_zones()) + # Remove availability zones not used in this test + availability_zones = [ + az for az in availability_zones + if 'lb_admin_availability_zone-list' in az[const.NAME]] + self.assertEqual(3, len(availability_zones)) + self.assertEqual(ref_id_list_asc[0], availability_zones[0][const.NAME]) + self.assertEqual(ref_id_list_asc[1], availability_zones[1][const.NAME]) + self.assertEqual(ref_id_list_asc[2], availability_zones[2][const.NAME]) + + # Check the descending sort order by name + availability_zones = ( + self.lb_admin_availability_zone_client.list_availability_zones( + query_params='{sort}={name}:{order}'.format( + sort=const.SORT, name=const.NAME, order=const.DESC))) + # Remove availability zones not used in this test + availability_zones = [ + az for az in availability_zones + if 'lb_admin_availability_zone-list' in az[const.NAME]] + self.assertEqual(3, len(availability_zones)) + self.assertEqual(az3_name, availability_zones[0][const.NAME]) + self.assertEqual(az2_name, availability_zones[1][const.NAME]) + self.assertEqual(az1_name, availability_zones[2][const.NAME]) + + # Check the ascending sort order by name + availability_zones = ( + self.mem_availability_zone_client.list_availability_zones( + query_params='{sort}={name}:{order}'.format( + sort=const.SORT, name=const.NAME, order=const.ASC))) + # Remove availability zones not used in this test + availability_zones = [ + az for az in availability_zones + if 'lb_admin_availability_zone-list' in az[const.NAME]] + self.assertEqual(3, len(availability_zones)) + self.assertEqual(az1_name, availability_zones[0][const.NAME]) + self.assertEqual(az2_name, availability_zones[1][const.NAME]) + self.assertEqual(az3_name, availability_zones[2][const.NAME]) + + ref_availability_zones = [az1, az2, az3] + sorted_availability_zones = sorted(ref_availability_zones, + key=itemgetter(const.NAME)) + sorted_enabled_availability_zones = [ + az for az in sorted_availability_zones + if az[const.ENABLED]] + + # Test fields + for field in const.SHOW_AVAILABILITY_ZONE_FIELDS: + availability_zones = ( + self.mem_availability_zone_client + .list_availability_zones( + query_params='{fields}={field}&{fields}={name}'.format( + fields=const.FIELDS, field=field, name=const.NAME)) + ) + # Remove availability zones not used in this test + availability_zones = [ + az for az in availability_zones + if 'lb_admin_availability_zone-list' in az[const.NAME]] + self.assertEqual(3, len(availability_zones)) + self.assertEqual(sorted_availability_zones[0][field], + availability_zones[0][field]) + self.assertEqual(sorted_availability_zones[1][field], + availability_zones[1][field]) + self.assertEqual(sorted_availability_zones[2][field], + availability_zones[2][field]) + + # Test filtering + availability_zone = ( + self.mem_availability_zone_client.list_availability_zones( + query_params='{name}={az_name}'.format( + name=const.NAME, az_name=az2[const.NAME]))) + self.assertEqual(1, len(availability_zone)) + self.assertEqual(az2[const.NAME], availability_zone[0][const.NAME]) + + # Test combined params + availability_zones = ( + self.mem_availability_zone_client.list_availability_zones( + query_params='{enabled}={enable}&{fields}={name}&' + '{sort}={ID}:{desc}'.format( + enabled=const.ENABLED, + enable=True, + fields=const.FIELDS, name=const.NAME, + sort=const.SORT, ID=const.NAME, + desc=const.DESC))) + # Remove availability zones not used in this test + availability_zones = [ + az for az in availability_zones + if 'lb_admin_availability_zone-list' in az[const.NAME]] + self.assertEqual(2, len(availability_zones)) + self.assertEqual(1, len(availability_zones[0])) + self.assertEqual(sorted_enabled_availability_zones[1][const.NAME], + availability_zones[0][const.NAME]) + self.assertEqual(sorted_enabled_availability_zones[0][const.NAME], + availability_zones[1][const.NAME]) + + @decorators.idempotent_id('4fa77f96-ba75-4255-bef8-6710cd7cb762') + def test_availability_zone_show(self): + """Tests availability zone show API. + + * Create a fully populated availability zone. + * Validate that non-lb-admin accounts cannot see the availability zone. + * Show availability zone details. + * Validate the show 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_availability_zone_client.is_version_supported( + self.api_version, '2.14'): + raise self.skipException('Availability zones are only available ' + 'on Octavia API version 2.14 or newer.') + availability_zone_name = data_utils.rand_name( + "lb_admin_availability_zone-show") + availability_zone_description = data_utils.arbitrary_string(size=255) + + availability_zone_kwargs = { + const.NAME: availability_zone_name, + const.DESCRIPTION: availability_zone_description, + const.ENABLED: True, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + # Happy path + availability_zone = ( + self.lb_admin_availability_zone_client + .create_availability_zone(**availability_zone_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + availability_zone[const.NAME]) + + # Test that a user without the load balancer role cannot + # show availability zone details. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_client + .show_availability_zone, + availability_zone[const.NAME]) + + result = self.mem_availability_zone_client.show_availability_zone( + availability_zone[const.NAME]) + + self.assertEqual(availability_zone_name, result[const.NAME]) + self.assertEqual(availability_zone_description, + result[const.DESCRIPTION]) + self.assertTrue(result[const.ENABLED]) + self.assertEqual(self.availability_zone_profile_id, + result[const.AVAILABILITY_ZONE_PROFILE_ID]) + + @decorators.idempotent_id('9c466b9f-b70a-456d-9172-eb79b7820c7f') + def test_availability_zone_update(self): + """Tests availability zone update API. + + * Create a fully populated availability zone. + * Show availability zone details. + * Validate the show reflects the initial values. + * Validate that non-admin accounts cannot update the availability zone. + * Update the availability zone details. + * Show availability zone 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_availability_zone_client.is_version_supported( + self.api_version, '2.14'): + raise self.skipException('Availability zones are only available ' + 'on Octavia API version 2.14 or newer.') + availability_zone_name = data_utils.rand_name( + "lb_admin_availability_zone-update") + availability_zone_description = data_utils.arbitrary_string(size=255) + + availability_zone_kwargs = { + const.NAME: availability_zone_name, + const.DESCRIPTION: availability_zone_description, + const.ENABLED: True, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + # Happy path + availability_zone = ( + self.lb_admin_availability_zone_client + .create_availability_zone(**availability_zone_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + availability_zone[const.NAME]) + + availability_zone_description2 = data_utils.arbitrary_string(size=255) + availability_zone_updated_kwargs = { + const.DESCRIPTION: availability_zone_description2, + const.ENABLED: False} + + # Test that a user without the load balancer role cannot + # show availability zone details. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_client + .update_availability_zone, + availability_zone[const.NAME], + **availability_zone_updated_kwargs) + + updated_availability_zone = ( + self.lb_admin_availability_zone_client.update_availability_zone( + availability_zone[const.NAME], + **availability_zone_updated_kwargs)) + + self.assertEqual( + availability_zone[const.NAME], + updated_availability_zone[const.NAME]) + self.assertEqual( + availability_zone_description2, + updated_availability_zone[const.DESCRIPTION]) + self.assertEqual( + availability_zone[const.AVAILABILITY_ZONE_PROFILE_ID], + updated_availability_zone[const.AVAILABILITY_ZONE_PROFILE_ID]) + self.assertFalse(updated_availability_zone[const.ENABLED]) + + result = ( + self.mem_availability_zone_client + .show_availability_zone(availability_zone[const.NAME])) + + self.assertEqual(availability_zone[const.NAME], result[const.NAME]) + self.assertEqual(availability_zone_description2, + result[const.DESCRIPTION]) + self.assertEqual(availability_zone[const.AVAILABILITY_ZONE_PROFILE_ID], + result[const.AVAILABILITY_ZONE_PROFILE_ID]) + self.assertFalse(result[const.ENABLED]) + + @decorators.idempotent_id('11585b33-2689-4693-be3b-26b210bb7fc5') + def test_availability_zone_delete(self): + """Tests availability zone create and delete APIs. + + * Creates an availability zone. + * Validates that other accounts cannot delete the availability zone. + * Deletes the availability zone. + * Validates the availability zone no longer exists. + """ + # 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_availability_zone_client.is_version_supported( + self.api_version, '2.14'): + raise self.skipException('Availability zones are only available ' + 'on Octavia API version 2.14 or newer.') + availability_zone_name = data_utils.rand_name( + "lb_admin_availability_zone-delete") + availability_zone_description = data_utils.arbitrary_string(size=255) + + availability_zone_kwargs = { + const.NAME: availability_zone_name, + const.DESCRIPTION: availability_zone_description, + const.ENABLED: True, + const.AVAILABILITY_ZONE_PROFILE_ID: + self.availability_zone_profile_id} + + # Happy path + availability_zone = ( + self.lb_admin_availability_zone_client + .create_availability_zone(**availability_zone_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_client + .cleanup_an_availability_zone, + availability_zone[const.NAME]) + + # Test that a user without the load balancer admin role cannot + # delete an availability zone. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_client + .delete_availability_zone, + availability_zone[const.NAME]) + + # Happy path + self.lb_admin_availability_zone_client.delete_availability_zone( + availability_zone[const.NAME]) + + self.assertRaises( + exceptions.NotFound, + self.lb_admin_availability_zone_client.show_availability_zone, + availability_zone[const.NAME]) diff --git a/octavia_tempest_plugin/tests/api/v2/test_availability_zone_capabilities.py b/octavia_tempest_plugin/tests/api/v2/test_availability_zone_capabilities.py new file mode 100644 index 00000000..4a12057a --- /dev/null +++ b/octavia_tempest_plugin/tests/api/v2/test_availability_zone_capabilities.py @@ -0,0 +1,93 @@ +# Copyright 2019 Rackspace US Inc. All rights reserved. +# Copyright 2019 Verizon Media +# +# 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 tempest import config +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 AvailabilityZoneCapabilitiesAPITest(test_base.LoadBalancerBaseTest): + """Test the provider availability zone capabilities API.""" + + @decorators.idempotent_id('cb3e4c59-4114-420b-9837-2666d4d5fef4') + def test_availability_zone_capabilities_list(self): + """Tests provider availability zone capabilities list API/filtering. + + * Validates that non-lb admin accounts cannot list the capabilities. + * List the availability zone capablilities. + * Validate that the "loadbalancer_topology" capablility is present. + * List the providers returning one field at a time. + """ + # 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.mem_provider_client.is_version_supported( + self.api_version, '2.14'): + raise self.skipException( + 'Availability zone capabilities are only available ' + 'on Octavia API version 2.14 or newer.') + + # Test that a user without the load balancer admin role cannot + # list provider availability zone capabilities. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + os_primary_capabilities_client = ( + self.os_primary.availability_zone_capabilities_client) + self.assertRaises( + exceptions.Forbidden, + (os_primary_capabilities_client + .list_availability_zone_capabilities), + CONF.load_balancer.provider) + + # Check for an expected availability zone capability for the + # configured provider + admin_capabilities_client = ( + self.lb_admin_availability_zone_capabilities_client) + capabilities = ( + admin_capabilities_client.list_availability_zone_capabilities( + CONF.load_balancer.provider)) + + expected_name = list( + CONF.load_balancer.expected_availability_zone_capability)[0] + expected_description = ( + CONF.load_balancer.expected_availability_zone_capability[ + expected_name]) + for capability in capabilities: + if capability[const.NAME] == expected_name: + self.assertEqual(expected_description, + capability[const.DESCRIPTION]) + + # Test fields + capabilities = ( + admin_capabilities_client.list_availability_zone_capabilities( + CONF.load_balancer.provider, + query_params='{fields}={field}&{field}={exp_name}'.format( + fields=const.FIELDS, field=const.NAME, + exp_name=expected_name))) + self.assertEqual(1, len(capabilities[0])) + self.assertEqual(expected_name, capabilities[0][const.NAME]) + + capabilities = ( + admin_capabilities_client.list_availability_zone_capabilities( + CONF.load_balancer.provider, + query_params='{fields}={field}&{name}={exp_name}'.format( + fields=const.FIELDS, field=const.DESCRIPTION, + name=const.NAME, exp_name=expected_name))) + self.assertEqual(1, len(capabilities[0])) + self.assertEqual(expected_description, + capabilities[0][const.DESCRIPTION]) diff --git a/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py b/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py new file mode 100644 index 00000000..86ae0663 --- /dev/null +++ b/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py @@ -0,0 +1,536 @@ +# 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 oslo_utils import uuidutils +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 AvailabilityZoneProfileAPITest(test_base.LoadBalancerBaseTest): + """Test the availability zone profile object API.""" + + @decorators.idempotent_id('e512b580-ef32-44c3-bbd2-efdc27ba2ea6') + def test_availability_zone_profile_create(self): + """Tests availability zone profile create and basic show APIs. + + * Tests that users without the loadbalancer admin role cannot + create availability zone profiles. + * Create a fully populated availability zone 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_availability_zone_profile_client + .is_version_supported(self.api_version, '2.14')): + raise self.skipException( + 'Availability zone profiles are only available on ' + 'Octavia API version 2.14 or newer.') + + availability_zone_profile_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile1-create") + availability_zone_data = { + const.COMPUTE_ZONE: 'my_compute_zone', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data_json = jsonutils.dumps(availability_zone_data) + + availability_zone_profile_kwargs = { + const.NAME: availability_zone_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data_json + } + + # Test that a user without the load balancer admin role cannot + # create an availability zone profile profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_profile_client + .create_availability_zone_profile, + **availability_zone_profile_kwargs) + + # Happy path + availability_zone_profile = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile[const.ID]) + + UUID(availability_zone_profile[const.ID]) + self.assertEqual( + availability_zone_profile_name, + availability_zone_profile[const.NAME]) + self.assertEqual( + CONF.load_balancer.provider, + availability_zone_profile[const.PROVIDER_NAME]) + self.assertEqual( + availability_zone_data_json, + availability_zone_profile[const.AVAILABILITY_ZONE_DATA]) + + @decorators.idempotent_id('ef7d1c45-e312-46ce-8dcb-f2fe26295658') + def test_availability_zone_profile_list(self): + """Tests availability zone profile list API and field filtering. + + * Create three availability zone profiles. + * Validates that non-admin accounts cannot list the availability zone + profiles. + * List the availability zone profiles using the default sort order. + * List the availability zone profiles using descending sort order. + * List the availability zone profiles using ascending sort order. + * List the availability zone profiles returning one field at a time. + * List the availability zone profiles returning two fields. + * List the availability zone profiles filtering to one of the three. + * List the availability zone 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_availability_zone_profile_client + .is_version_supported(self.api_version, '2.14')): + raise self.skipException( + 'Availability zone profiles are only available on ' + 'Octavia API version 2.14 or newer.') + + # Create availability zone profile 1 + availability_zone_profile1_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile-list-1") + availability_zone_data1 = { + const.COMPUTE_ZONE: 'my_compute_zone1', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data1_json = jsonutils.dumps(availability_zone_data1) + + availability_zone_profile1_kwargs = { + const.NAME: availability_zone_profile1_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data1_json + } + availability_zone_profile1 = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile1_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile1[const.ID]) + + # Create availability zone profile 2 + availability_zone_profile2_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile-list-2") + availability_zone_data2 = { + const.COMPUTE_ZONE: 'my_compute_zone2', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data2_json = jsonutils.dumps(availability_zone_data2) + + availability_zone_profile2_kwargs = { + const.NAME: availability_zone_profile2_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data2_json + } + availability_zone_profile2 = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile2_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile2[const.ID]) + + # Create availability zone profile 3 + availability_zone_profile3_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile-list-3") + availability_zone_data3 = { + const.COMPUTE_ZONE: 'my_compute_zone3', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data3_json = jsonutils.dumps(availability_zone_data3) + + availability_zone_profile3_kwargs = { + const.NAME: availability_zone_profile3_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data3_json + } + availability_zone_profile3 = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile3_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile3[const.ID]) + + # default sort order (by ID) reference list + ref_id_list_asc = [availability_zone_profile1[const.ID], + availability_zone_profile2[const.ID], + availability_zone_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 availability zone profiles. + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_profile_client + .list_availability_zone_profiles) + + # Check the default sort order (by ID) + profiles = (self.lb_admin_availability_zone_profile_client + .list_availability_zone_profiles()) + # Remove availability zone profiles not used in this test + profiles = [ + prof for prof in profiles + if 'lb_admin_availabilityzoneprofile-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_availability_zone_profile_client + .list_availability_zone_profiles( + query_params='{sort}={name}:{order}'.format( + sort=const.SORT, name=const.NAME, order=const.DESC))) + # Remove availability zone profiles not used in this test + profiles = [ + prof for prof in profiles + if 'lb_admin_availabilityzoneprofile-list' in prof[const.NAME]] + self.assertEqual(3, len(profiles)) + self.assertEqual(availability_zone_profile3_name, + profiles[0][const.NAME]) + self.assertEqual(availability_zone_profile2_name, + profiles[1][const.NAME]) + self.assertEqual(availability_zone_profile1_name, + profiles[2][const.NAME]) + + # Check the ascending sort order by name + profiles = ( + self.lb_admin_availability_zone_profile_client + .list_availability_zone_profiles( + query_params='{sort}={name}:{order}'.format( + sort=const.SORT, name=const.NAME, order=const.ASC))) + # Remove availability zone profiles not used in this test + profiles = [ + prof for prof in profiles + if 'lb_admin_availabilityzoneprofile-list' in prof[const.NAME]] + self.assertEqual(3, len(profiles)) + self.assertEqual(availability_zone_profile1_name, + profiles[0][const.NAME]) + self.assertEqual(availability_zone_profile2_name, + profiles[1][const.NAME]) + self.assertEqual(availability_zone_profile3_name, + profiles[2][const.NAME]) + + ref_profiles = [availability_zone_profile1, availability_zone_profile2, + availability_zone_profile3] + sorted_profiles = sorted(ref_profiles, key=itemgetter(const.ID)) + + # Test fields + availability_zone_profile_client = ( + self.lb_admin_availability_zone_profile_client) + for field in const.SHOW_AVAILABILITY_ZONE_PROFILE_FIELDS: + profiles = ( + availability_zone_profile_client + .list_availability_zone_profiles( + query_params='{fields}={field}&{fields}={name}'.format( + fields=const.FIELDS, field=field, name=const.NAME))) + # Remove availability zone profiles not used in this test + profiles = [ + prof for prof in profiles + if 'lb_admin_availabilityzoneprofile-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_availability_zone_profile_client + .list_availability_zone_profiles( + query_params='{name}={prof_name}'.format( + name=const.NAME, + prof_name=availability_zone_profile2[const.NAME]))) + self.assertEqual(1, len(profile)) + self.assertEqual(availability_zone_profile2[const.ID], + profile[0][const.ID]) + + # Test combined params + profiles = ( + self.lb_admin_availability_zone_profile_client + .list_availability_zone_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 availability zone profiles not used in this test + profiles = [ + prof for prof in profiles + if 'lb_admin_availabilityzoneprofile-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('379d92dc-7f6d-4674-ae6f-b3aa2120c677') + def test_availability_zone_profile_show(self): + """Tests availability zone profile show API. + + * Create a fully populated availability zone profile. + * Show availability zone profile details. + * Validate the show reflects the requested values. + * Validates that non-lb-admin accounts cannot see the availability zone + 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_availability_zone_profile_client + .is_version_supported(self.api_version, '2.14')): + raise self.skipException( + 'Availability zone profiles are only available on ' + 'Octavia API version 2.14 or newer.') + + availability_zone_profile_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile1-show") + availability_zone_data = { + const.COMPUTE_ZONE: 'my_compute_zone', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data_json = jsonutils.dumps(availability_zone_data) + + availability_zone_profile_kwargs = { + const.NAME: availability_zone_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data_json + } + + availability_zone_profile = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile[const.ID]) + + # Test that a user without the load balancer admin role cannot + # show an availability zone profile profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_profile_client + .show_availability_zone_profile, + availability_zone_profile[const.ID]) + + result = ( + self.lb_admin_availability_zone_profile_client + .show_availability_zone_profile( + availability_zone_profile[const.ID])) + + self.assertEqual(availability_zone_profile_name, result[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + result[const.PROVIDER_NAME]) + self.assertEqual(availability_zone_data_json, + result[const.AVAILABILITY_ZONE_DATA]) + + @decorators.idempotent_id('7121d4c0-f751-4b4e-a4c1-ab06c27a54a4') + def test_availability_zone_profile_update(self): + """Tests availability zone profile update API. + + * Create a fully populated availability zone profile. + * Show availability zone profile details. + * Validate the show reflects the initial values. + * Validates that non-admin accounts cannot update the availability zone + profile. + * Update the availability zone profile details. + * Show availability zone 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_availability_zone_profile_client + .is_version_supported(self.api_version, '2.14')): + raise self.skipException( + 'Availability zone profiles are only available on ' + 'Octavia API version 2.14 or newer.') + + availability_zone_profile_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile1-update") + availability_zone_data = { + const.COMPUTE_ZONE: 'my_compute_zone1', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data_json = jsonutils.dumps(availability_zone_data) + + availability_zone_profile_kwargs = { + const.NAME: availability_zone_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data_json + } + + availability_zone_profile = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile[const.ID]) + + self.assertEqual( + availability_zone_profile_name, + availability_zone_profile[const.NAME]) + self.assertEqual( + CONF.load_balancer.provider, + availability_zone_profile[const.PROVIDER_NAME]) + self.assertEqual( + availability_zone_data_json, + availability_zone_profile[const.AVAILABILITY_ZONE_DATA]) + + availability_zone_profile_name2 = data_utils.rand_name( + "lb_admin_availabilityzoneprofile1-update2") + availability_zone_data2 = { + const.COMPUTE_ZONE: 'my_compute_zone2', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data2_json = jsonutils.dumps(availability_zone_data2) + + # TODO(johnsom) Figure out a reliable second provider + availability_zone_profile_updated_kwargs = { + const.NAME: availability_zone_profile_name2, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data2_json + } + + # Test that a user without the load balancer admin role cannot + # create an availability zone profile profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_profile_client + .update_availability_zone_profile, + availability_zone_profile[const.ID], + **availability_zone_profile_updated_kwargs) + + result = ( + self.lb_admin_availability_zone_profile_client + .update_availability_zone_profile( + availability_zone_profile[const.ID], + **availability_zone_profile_updated_kwargs)) + + self.assertEqual(availability_zone_profile_name2, result[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + result[const.PROVIDER_NAME]) + self.assertEqual(availability_zone_data2_json, + result[const.AVAILABILITY_ZONE_DATA]) + + # Check that a show reflects the new values + get_result = ( + self.lb_admin_availability_zone_profile_client + .show_availability_zone_profile( + availability_zone_profile[const.ID])) + + self.assertEqual(availability_zone_profile_name2, + get_result[const.NAME]) + self.assertEqual(CONF.load_balancer.provider, + get_result[const.PROVIDER_NAME]) + self.assertEqual(availability_zone_data2_json, + get_result[const.AVAILABILITY_ZONE_DATA]) + + @decorators.idempotent_id('371cee1d-3404-4744-b5c5-8a3d37aa8425') + def test_availability_zone_profile_delete(self): + """Tests availability zone profile create and delete APIs. + + * Creates an availability zone profile profile. + * Validates that other accounts cannot delete the availability zone + profile. + * Deletes the availability zone profile. + * Validates the availability zone 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_availability_zone_profile_client + .is_version_supported(self.api_version, '2.14')): + raise self.skipException( + 'Availability zone profiles are only available on ' + 'Octavia API version 2.14 or newer.') + + availability_zone_profile_name = data_utils.rand_name( + "lb_admin_availabilityzoneprofile1-delete") + availability_zone_data = { + const.COMPUTE_ZONE: 'my_compute_zone', + const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(), + } + availability_zone_data_json = jsonutils.dumps(availability_zone_data) + + availability_zone_profile_kwargs = { + const.NAME: availability_zone_profile_name, + const.PROVIDER_NAME: CONF.load_balancer.provider, + const.AVAILABILITY_ZONE_DATA: availability_zone_data_json + } + + availability_zone_profile = ( + self.lb_admin_availability_zone_profile_client + .create_availability_zone_profile( + **availability_zone_profile_kwargs)) + self.addCleanup( + self.lb_admin_availability_zone_profile_client + .cleanup_availability_zone_profile, + availability_zone_profile[const.ID]) + + # Test that a user without the load balancer admin role cannot + # delete an availability zone profile profile + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.availability_zone_profile_client + .delete_availability_zone_profile, + availability_zone_profile[const.ID]) + + # Happy path + (self.lb_admin_availability_zone_profile_client + .delete_availability_zone_profile( + availability_zone_profile[const.ID])) + + self.assertRaises( + exceptions.NotFound, + self.lb_admin_availability_zone_profile_client + .show_availability_zone_profile, + availability_zone_profile[const.ID]) diff --git a/octavia_tempest_plugin/tests/api/v2/test_flavor_capabilities.py b/octavia_tempest_plugin/tests/api/v2/test_flavor_capabilities.py index 924c0444..7f9da51a 100644 --- a/octavia_tempest_plugin/tests/api/v2/test_flavor_capabilities.py +++ b/octavia_tempest_plugin/tests/api/v2/test_flavor_capabilities.py @@ -52,7 +52,7 @@ class FlavorCapabilitiesAPITest(test_base.LoadBalancerBaseTest): CONF.load_balancer.provider) # Check for an expected flavor capability for the configured provider - admin_capabilities_client = self.lb_admin_capabilities_client + admin_capabilities_client = self.lb_admin_flavor_capabilities_client capabilities = admin_capabilities_client.list_flavor_capabilities( CONF.load_balancer.provider) diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py index 5033ade6..4ef4a917 100644 --- a/octavia_tempest_plugin/tests/test_base.py +++ b/octavia_tempest_plugin/tests/test_base.py @@ -128,8 +128,16 @@ class LoadBalancerBaseTest(test.BaseTestCase): cls.mem_flavor_client = cls.os_roles_lb_member.flavor_client cls.mem_provider_client = cls.os_roles_lb_member.provider_client cls.os_admin_servers_client = cls.os_admin.servers_client - cls.lb_admin_capabilities_client = ( + cls.lb_admin_flavor_capabilities_client = ( cls.os_roles_lb_admin.flavor_capabilities_client) + cls.lb_admin_availability_zone_capabilities_client = ( + cls.os_roles_lb_admin.availability_zone_capabilities_client) + cls.lb_admin_availability_zone_profile_client = ( + cls.os_roles_lb_admin.availability_zone_profile_client) + cls.lb_admin_availability_zone_client = ( + cls.os_roles_lb_admin.availability_zone_client) + cls.mem_availability_zone_client = ( + cls.os_roles_lb_member.availability_zone_client) @classmethod def resource_setup(cls):