# Copyright 2018 GoDaddy # # 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 oslo_serialization import jsonutils from tempest import config from tempest.lib.common import rest_client from tempest.lib.common.utils import test_utils from tempest.lib import exceptions from octavia_tempest_plugin.common import constants as const from octavia_tempest_plugin.tests import waiters CONF = config.CONF LOG = logging.getLogger(__name__) class Unset(object): def __bool__(self): return False __nonzero__ = __bool__ def __repr__(self): return 'Unset' class BaseLBaaSClient(rest_client.RestClient): root_tag = None list_root_tag = None base_uri = '/v2.0/lbaas/{object}' def __init__(self, auth_provider, service, region, **kwargs): super(BaseLBaaSClient, self).__init__(auth_provider, service, region, **kwargs) self.timeout = CONF.load_balancer.build_timeout self.build_interval = CONF.load_balancer.build_interval self.uri = self.base_uri.format(object=self.list_root_tag) # Create a method for each object's cleanup # This method should be used (rather than delete) for tempest cleanups. cleanup_func_name = 'cleanup_{}'.format(self.root_tag) if not hasattr(self, cleanup_func_name): setattr(self, cleanup_func_name, self._cleanup_obj) def _create_object(self, parent_id=None, return_object_only=True, **kwargs): """Create an object. :param return_object_only: If True, the response returns the object inside the root tag. False returns the full response from the API. :param **kwargs: All attributes of the object should be passed as keyword arguments to this function. :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 appropriate object. """ obj_dict = {self.root_tag: kwargs} if parent_id: request_uri = self.uri.format(parent=parent_id) else: request_uri = self.uri response, body = self.post(request_uri, jsonutils.dumps(obj_dict)) self.expected_success(201, response.status) if return_object_only: return jsonutils.loads(body.decode('utf-8'))[self.root_tag] else: return jsonutils.loads(body.decode('utf-8')) def _show_object(self, obj_id, parent_id=None, query_params=None, return_object_only=True): """Get object details. :param obj_id: The object 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 appropriate object. """ if parent_id: uri = self.uri.format(parent=parent_id) else: uri = self.uri if query_params: request_uri = '{0}/{1}?{2}'.format(uri, obj_id, query_params) else: request_uri = '{0}/{1}'.format(uri, obj_id) response, body = self.get(request_uri) self.expected_success(200, response.status) if return_object_only: return jsonutils.loads(body.decode('utf-8'))[self.root_tag] else: return jsonutils.loads(body.decode('utf-8')) def _list_objects(self, parent_id=None, query_params=None, return_object_only=True): """Get a list of the appropriate 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 appropriate objects. """ if parent_id: uri = self.uri.format(parent=parent_id) else: uri = self.uri if query_params: request_uri = '{0}?{1}'.format(uri, query_params) else: request_uri = uri response, body = self.get(request_uri) self.expected_success(200, response.status) if return_object_only: return jsonutils.loads(body.decode('utf-8'))[self.list_root_tag] else: return jsonutils.loads(body.decode('utf-8')) def _update_object(self, obj_id, parent_id=None, return_object_only=True, **kwargs): """Update an object. :param obj_id: The object ID to update. :param parent_id: The parent object ID, if applicable. :param return_object_only: If True, the response returns the object inside the root tag. False returns the full response from the API. :param **kwargs: All attributes of the object should be passed as keyword arguments to this function. :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 appropriate object. """ obj_dict = {self.root_tag: kwargs} if parent_id: uri = self.uri.format(parent=parent_id) else: uri = self.uri request_uri = '{0}/{1}'.format(uri, obj_id) response, body = self.put(request_uri, jsonutils.dumps(obj_dict)) self.expected_success(200, response.status) if return_object_only: return jsonutils.loads(body.decode('utf-8'))[self.root_tag] else: return jsonutils.loads(body.decode('utf-8')) def _delete_obj(self, obj_id, parent_id=None, ignore_errors=False, cascade=False): """Delete an object. :param obj_id: The object ID to delete. :param ignore_errors: True if errors should be ignored. :param cascade: If true will delete all child objects of an object, if that object supports it. :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. """ if parent_id: uri = self.uri.format(parent=parent_id) else: uri = self.uri if cascade: request_uri = '{0}/{1}?cascade=true'.format(uri, obj_id) else: request_uri = '{0}/{1}'.format(uri, obj_id) if ignore_errors: try: response, body = self.delete(request_uri) except Exception: return else: response, body = self.delete(request_uri) self.expected_success(204, response.status) return response.status def _cleanup_obj(self, obj_id, lb_client=None, lb_id=None, parent_id=None): """Clean up an object (for use in tempest addClassResourceCleanup). We always need to wait for the parent LB to be in a mutable state before deleting the child object, and the cleanups will not guarantee this if we just pass the delete function to tempest cleanup. For example, if we add multiple listeners on the same LB to cleanup, tempest will delete the first one and then immediately try to delete the second one, which will fail because the LB will be immutable. We also need to wait to return until the parent LB is back in a mutable state so future tests don't break right at the start. This function: * Waits until the parent LB is ACTIVE * Deletes the object * Waits until the parent LB is ACTIVE :param obj_id: The object ID to clean up. :param lb_client: (Optional) The loadbalancer client, if this isn't the loadbalancer client already. :param lb_id: (Optional) The ID of the parent loadbalancer, if the main obj_id is for a sub-object and not a loadbalancer. :return: """ if parent_id: uri = self.uri.format(parent=parent_id) else: uri = self.uri if lb_client and lb_id: wait_id = lb_id wait_client = lb_client wait_func = lb_client.show_loadbalancer else: wait_id = obj_id wait_client = self wait_func = self._show_object LOG.info("Starting cleanup for %s %s...", self.root_tag, obj_id) try: request_uri = '{0}/{1}'.format(uri, obj_id) response, body = self.get(request_uri) resp_obj = jsonutils.loads(body.decode('utf-8'))[self.root_tag] if (response.status == 404 or resp_obj['provisioning_status'] == const.DELETED): raise exceptions.NotFound() except exceptions.NotFound: # Already gone, cleanup complete LOG.info("%s %s is already gone. Cleanup considered complete.", self.root_tag, obj_id) return LOG.info("Waiting for %s %s to be ACTIVE...", wait_client.root_tag, wait_id) try: waiters.wait_for_status(wait_func, wait_id, const.PROVISIONING_STATUS, const.ACTIVE, self.build_interval, self.timeout) except exceptions.UnexpectedResponseCode: # Status is ERROR, go ahead with deletion LOG.debug("Found %s %s in ERROR status, proceeding with cleanup.", wait_client.root_tag, wait_id) except exceptions.TimeoutException: # Timed out, nothing to be done, let errors happen LOG.error("Timeout exceeded waiting to clean up %s %s.", self.root_tag, obj_id) except exceptions.NotFound: # Already gone, cleanup complete LOG.info("%s %s is already gone. Cleanup considered complete.", wait_client.root_tag.capitalize(), wait_id) return except Exception as e: # Log that something weird happens, then let the chips fall LOG.error("Cleanup encountered an unknown exception while waiting " "for %s %s: %s", wait_client.root_tag, wait_id, e) uri = '{0}/{1}'.format(uri, obj_id) LOG.info("Cleaning up %s %s...", self.root_tag, obj_id) return_status = test_utils.call_and_ignore_notfound_exc( self.delete, uri) if lb_id and lb_client: LOG.info("Waiting for %s %s to be ACTIVE...", wait_client.root_tag, wait_id) waiters.wait_for_status(wait_func, wait_id, const.PROVISIONING_STATUS, const.ACTIVE, self.build_interval, self.timeout) else: LOG.info("Waiting for %s %s to be DELETED...", wait_client.root_tag, wait_id) waiters.wait_for_deleted_status_or_not_found( wait_func, wait_id, const.PROVISIONING_STATUS, CONF.load_balancer.check_interval, CONF.load_balancer.check_timeout) LOG.info("Cleanup complete for %s %s...", self.root_tag, obj_id) return return_status def is_resource_deleted(self, id): """Check if the object is deleted. :param id: The object ID to check. :return: boolean state representing the object's deleted state """ try: obj = self._show_object(id) if obj.get(const.PROVISIONING_STATUS) == const.DELETED: return True except exceptions.NotFound: return True return False def get_max_api_version(self): """Get the maximum version available on the API endpoint. :return: Maximum version string available on the endpoint. """ response, body = self.get('/') self.expected_success(200, response.status) versions_list = jsonutils.loads(body.decode('utf-8'))['versions'] current_versions = (version for version in versions_list if version['status'] == 'CURRENT') max_version = '0.0' for version in current_versions: ver_string = version['id'] if ver_string.startswith("v"): ver_string = ver_string[1:] ver_split = list(map(int, ver_string.split('.'))) max_split = list(map(int, max_version.split('.'))) if len(ver_split) > 2: raise exceptions.InvalidAPIVersionString(version=ver_string) if ver_split[0] > max_split[0] or ( ver_split[0] == max_split[0] and ver_split[1] >= max_split[1]): max_version = ver_string if max_version == '0.0': raise exceptions.InvalidAPIVersionString(version=max_version) return max_version def is_version_supported(self, api_version, version): """Check if a version is supported by the API. :param api_version: Reference endpoint API version. :param version: Version to check against API version. :return: boolean if the version is supported. """ api_split = list(map(int, api_version.split('.'))) ver_split = list(map(int, version.split('.'))) if api_split[0] > ver_split[0] or ( api_split[0] == ver_split[0] and api_split[1] >= ver_split[1]): return True return False