summaryrefslogtreecommitdiff
path: root/tempest_lib/common
diff options
context:
space:
mode:
authorGhanshyam Mann <gmann@ghanshyammann.com>2022-04-05 17:38:00 -0500
committerGhanshyam <gmann@ghanshyammann.com>2022-04-05 23:41:14 +0000
commitfc1791f8589cb8dba9d8d9ef7856be17b2bc77f3 (patch)
tree034f0b3ccd9559c3227abac0e54abbd3f4f67920 /tempest_lib/common
parent023426894a4f72d906ed6f79c55ed7152a732b44 (diff)
downloadtempest-lib-master.tar.gz
Retire tempest-lib in favor of tempest.libHEADmaster
As of the 1.0.0 release tempest-lib as a separate repository and project is deprecated. We moved all the code to tempest.lib temepst version 10. Hoping everyone is moved to tempest.lib now, We can retire the tempest-lib. We discussed the same in PTG and agreed to proceed on retirement - https://etherpad.opendev.org/p/qa-zed-ptg Needed-By: https://review.opendev.org/c/openstack/governance/+/836704 Change-Id: I37ceb96e084f569ea59e4849ca8770742ce17846
Diffstat (limited to 'tempest_lib/common')
-rw-r--r--tempest_lib/common/__init__.py0
-rw-r--r--tempest_lib/common/http.py25
-rw-r--r--tempest_lib/common/rest_client.py894
-rw-r--r--tempest_lib/common/ssh.py174
-rw-r--r--tempest_lib/common/utils/__init__.py0
-rw-r--r--tempest_lib/common/utils/data_utils.py186
-rw-r--r--tempest_lib/common/utils/misc.py87
7 files changed, 0 insertions, 1366 deletions
diff --git a/tempest_lib/common/__init__.py b/tempest_lib/common/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest_lib/common/__init__.py
+++ /dev/null
diff --git a/tempest_lib/common/http.py b/tempest_lib/common/http.py
deleted file mode 100644
index b3793bc..0000000
--- a/tempest_lib/common/http.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright 2013 OpenStack Foundation
-# Copyright 2013 Citrix Systems, 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 httplib2
-
-
-class ClosingHttp(httplib2.Http):
- def request(self, *args, **kwargs):
- original_headers = kwargs.get('headers', {})
- new_headers = dict(original_headers, connection='close')
- new_kwargs = dict(kwargs, headers=new_headers)
- return super(ClosingHttp, self).request(*args, **new_kwargs)
diff --git a/tempest_lib/common/rest_client.py b/tempest_lib/common/rest_client.py
deleted file mode 100644
index c5886e6..0000000
--- a/tempest_lib/common/rest_client.py
+++ /dev/null
@@ -1,894 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# Copyright 2013 IBM Corp.
-# 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 collections
-import logging as real_logging
-import re
-import time
-
-import jsonschema
-from oslo_log import log as logging
-from oslo_serialization import jsonutils as json
-import six
-
-from tempest_lib.common import http
-from tempest_lib.common.utils import misc as misc_utils
-from tempest_lib import exceptions
-
-# redrive rate limited calls at most twice
-MAX_RECURSION_DEPTH = 2
-
-# All the successful HTTP status codes from RFC 7231 & 4918
-HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
-
-# All the redirection HTTP status codes from RFC 7231 & 4918
-HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
-
-# JSON Schema validator and format checker used for JSON Schema validation
-JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
-FORMAT_CHECKER = jsonschema.draft4_format_checker
-
-
-class RestClient(object):
- """Unified OpenStack RestClient class
-
- This class is used for building openstack api clients on top of. It is
- intended to provide a base layer for wrapping outgoing http requests in
- keystone auth as well as providing response code checking and error
- handling.
-
- :param auth_provider: an auth provider object used to wrap requests in auth
- :param str service: The service name to use for the catalog lookup
- :param str region: The region to use for the catalog lookup
- :param str endpoint_type: The endpoint type to use for the catalog lookup
- :param int build_interval: Time in seconds between to status checks in
- wait loops
- :param int build_timeout: Timeout in seconds to wait for a wait operation.
- :param bool disable_ssl_certificate_validation: Set to true to disable ssl
- certificate validation
- :param str ca_certs: File containing the CA Bundle to use in verifying a
- TLS server cert
- :param str trace_request: Regex to use for specifying logging the entirety
- of the request and response payload
- """
- TYPE = "json"
-
- # The version of the API this client implements
- api_version = None
-
- LOG = logging.getLogger(__name__)
-
- def __init__(self, auth_provider, service, region,
- endpoint_type='publicURL',
- build_interval=1, build_timeout=60,
- disable_ssl_certificate_validation=False, ca_certs=None,
- trace_requests=''):
- self.auth_provider = auth_provider
- self.service = service
- self.region = region
- self.endpoint_type = endpoint_type
- self.build_interval = build_interval
- self.build_timeout = build_timeout
- self.trace_requests = trace_requests
-
- self._skip_path = False
- self.general_header_lc = set(('cache-control', 'connection',
- 'date', 'pragma', 'trailer',
- 'transfer-encoding', 'via',
- 'warning'))
- self.response_header_lc = set(('accept-ranges', 'age', 'etag',
- 'location', 'proxy-authenticate',
- 'retry-after', 'server',
- 'vary', 'www-authenticate'))
- dscv = disable_ssl_certificate_validation
- self.http_obj = http.ClosingHttp(
- disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
-
- def _get_type(self):
- return self.TYPE
-
- def get_headers(self, accept_type=None, send_type=None):
- """Return the default headers which will be used with outgoing requests
-
- :param str accept_type: The media type to use for the Accept header, if
- one isn't provided the object var TYPE will be
- used
- :param str send_type: The media-type to use for the Content-Type
- header, if one isn't provided the object var
- TYPE will be used
- :rtype: dict
- :return: The dictionary of headers which can be used in the headers
- dict for outgoing request
- """
- if accept_type is None:
- accept_type = self._get_type()
- if send_type is None:
- send_type = self._get_type()
- return {'Content-Type': 'application/%s' % send_type,
- 'Accept': 'application/%s' % accept_type}
-
- def __str__(self):
- STRING_LIMIT = 80
- str_format = ("service:%s, base_url:%s, "
- "filters: %s, build_interval:%s, build_timeout:%s"
- "\ntoken:%s..., \nheaders:%s...")
- return str_format % (self.service, self.base_url,
- self.filters, self.build_interval,
- self.build_timeout,
- str(self.token)[0:STRING_LIMIT],
- str(self.get_headers())[0:STRING_LIMIT])
-
- @property
- def user(self):
- """The username used for requests
-
- :rtype: string
- :return: The username being used for requests
- """
-
- return self.auth_provider.credentials.username
-
- @property
- def user_id(self):
- """The user_id used for requests
-
- :rtype: string
- :return: The user id being used for requests
- """
- return self.auth_provider.credentials.user_id
-
- @property
- def tenant_name(self):
- """The tenant/project being used for requests
-
- :rtype: string
- :return: The tenant/project name being used for requests
- """
- return self.auth_provider.credentials.tenant_name
-
- @property
- def tenant_id(self):
- """The tenant/project id being used for requests
-
- :rtype: string
- :return: The tenant/project id being used for requests
- """
- return self.auth_provider.credentials.tenant_id
-
- @property
- def password(self):
- """The password being used for requests
-
- :rtype: string
- :return: The password being used for requests
- """
- return self.auth_provider.credentials.password
-
- @property
- def base_url(self):
- return self.auth_provider.base_url(filters=self.filters)
-
- @property
- def token(self):
- return self.auth_provider.get_token()
-
- @property
- def filters(self):
- _filters = dict(
- service=self.service,
- endpoint_type=self.endpoint_type,
- region=self.region
- )
- if self.api_version is not None:
- _filters['api_version'] = self.api_version
- if self._skip_path:
- _filters['skip_path'] = self._skip_path
- return _filters
-
- def skip_path(self):
- """When set, ignore the path part of the base URL from the catalog"""
- self._skip_path = True
-
- def reset_path(self):
- """When reset, use the base URL from the catalog as-is"""
- self._skip_path = False
-
- @classmethod
- def expected_success(cls, expected_code, read_code):
- """Check expected success response code against the http response
-
- :param int expected_code: The response code that is expected.
- Optionally a list of integers can be used
- to specify multiple valid success codes
- :param int read_code: The response code which was returned in the
- response
- :raises AssertionError: if the expected_code isn't a valid http success
- response code
- :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
- expected http success code
- """
- assert_msg = ("This function only allowed to use for HTTP status"
- "codes which explicitly defined in the RFC 7231 & 4918."
- "{0} is not a defined Success Code!"
- ).format(expected_code)
- if isinstance(expected_code, list):
- for code in expected_code:
- assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
- else:
- assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
-
- # NOTE(afazekas): the http status code above 400 is processed by
- # the _error_checker method
- if read_code < 400:
- pattern = """Unexpected http success status code {0},
- The expected status code is {1}"""
- if ((not isinstance(expected_code, list) and
- (read_code != expected_code)) or
- (isinstance(expected_code, list) and
- (read_code not in expected_code))):
- details = pattern.format(read_code, expected_code)
- raise exceptions.InvalidHttpSuccessCode(details)
-
- def post(self, url, body, headers=None, extra_headers=False):
- """Send a HTTP POST request using keystone auth
-
- :param str url: the relative url to send the post request to
- :param dict body: the request body
- :param dict headers: The headers to use for the request
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('POST', url, extra_headers, headers, body)
-
- def get(self, url, headers=None, extra_headers=False):
- """Send a HTTP GET request using keystone service catalog and auth
-
- :param str url: the relative url to send the post request to
- :param dict headers: The headers to use for the request
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('GET', url, extra_headers, headers)
-
- def delete(self, url, headers=None, body=None, extra_headers=False):
- """Send a HTTP DELETE request using keystone service catalog and auth
-
- :param str url: the relative url to send the post request to
- :param dict headers: The headers to use for the request
- :param dict body: the request body
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('DELETE', url, extra_headers, headers, body)
-
- def patch(self, url, body, headers=None, extra_headers=False):
- """Send a HTTP PATCH request using keystone service catalog and auth
-
- :param str url: the relative url to send the post request to
- :param dict body: the request body
- :param dict headers: The headers to use for the request
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('PATCH', url, extra_headers, headers, body)
-
- def put(self, url, body, headers=None, extra_headers=False):
- """Send a HTTP PUT request using keystone service catalog and auth
-
- :param str url: the relative url to send the post request to
- :param dict body: the request body
- :param dict headers: The headers to use for the request
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('PUT', url, extra_headers, headers, body)
-
- def head(self, url, headers=None, extra_headers=False):
- """Send a HTTP HEAD request using keystone service catalog and auth
-
- :param str url: the relative url to send the post request to
- :param dict headers: The headers to use for the request
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('HEAD', url, extra_headers, headers)
-
- def copy(self, url, headers=None, extra_headers=False):
- """Send a HTTP COPY request using keystone service catalog and auth
-
- :param str url: the relative url to send the post request to
- :param dict headers: The headers to use for the request
- :param dict extra_headers: If the headers returned by the get_headers()
- method are to be used but additional headers
- are needed in the request pass them in as a
- dict
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :rtype: tuple
- """
- return self.request('COPY', url, extra_headers, headers)
-
- def get_versions(self):
- """Get the versions on a endpoint from the keystone catalog
-
- This method will make a GET request on the baseurl from the keystone
- catalog to return a list of API versions. It is expected that a GET
- on the endpoint in the catalog will return a list of supported API
- versions.
-
- :return tuple with response headers and list of version numbers
- :rtype: tuple
- """
- resp, body = self.get('')
- body = self._parse_resp(body)
- versions = map(lambda x: x['id'], body)
- return resp, versions
-
- def _get_request_id(self, resp):
- for i in ('x-openstack-request-id', 'x-compute-request-id'):
- if i in resp:
- return resp[i]
- return ""
-
- def _safe_body(self, body, maxlen=4096):
- # convert a structure into a string safely
- try:
- text = six.text_type(body)
- except UnicodeDecodeError:
- # if this isn't actually text, return marker that
- return "<BinaryData: removed>"
- if len(text) > maxlen:
- return text[:maxlen]
- else:
- return text
-
- def _log_request_start(self, method, req_url, req_headers=None,
- req_body=None):
- if req_headers is None:
- req_headers = {}
- caller_name = misc_utils.find_test_caller()
- if self.trace_requests and re.search(self.trace_requests, caller_name):
- self.LOG.debug('Starting Request (%s): %s %s' %
- (caller_name, method, req_url))
-
- def _log_request_full(self, method, req_url, resp,
- secs="", req_headers=None,
- req_body=None, resp_body=None,
- caller_name=None, extra=None):
- if 'X-Auth-Token' in req_headers:
- req_headers['X-Auth-Token'] = '<omitted>'
- log_fmt = """Request - Headers: %s
- Body: %s
- Response - Headers: %s
- Body: %s"""
-
- self.LOG.debug(
- log_fmt % (
- str(req_headers),
- self._safe_body(req_body),
- str(resp),
- self._safe_body(resp_body)),
- extra=extra)
-
- def _log_request(self, method, req_url, resp,
- secs="", req_headers=None,
- req_body=None, resp_body=None):
- if req_headers is None:
- req_headers = {}
- # if we have the request id, put it in the right part of the log
- extra = dict(request_id=self._get_request_id(resp))
- # NOTE(sdague): while we still have 6 callers to this function
- # we're going to just provide work around on who is actually
- # providing timings by gracefully adding no content if they don't.
- # Once we're down to 1 caller, clean this up.
- caller_name = misc_utils.find_test_caller()
- if secs:
- secs = " %.3fs" % secs
- self.LOG.info(
- 'Request (%s): %s %s %s%s' % (
- caller_name,
- resp['status'],
- method,
- req_url,
- secs),
- extra=extra)
-
- # Also look everything at DEBUG if you want to filter this
- # out, don't run at debug.
- if self.LOG.isEnabledFor(real_logging.DEBUG):
- self._log_request_full(method, req_url, resp, secs, req_headers,
- req_body, resp_body, caller_name, extra)
-
- def _parse_resp(self, body):
- try:
- body = json.loads(body)
- except ValueError:
- return body
-
- # We assume, that if the first value of the deserialized body's
- # item set is a dict or a list, that we just return the first value
- # of deserialized body.
- # Essentially "cutting out" the first placeholder element in a body
- # that looks like this:
- #
- # {
- # "users": [
- # ...
- # ]
- # }
- try:
- # Ensure there are not more than one top-level keys
- # NOTE(freerunner): Ensure, that JSON is not nullable to
- # to prevent StopIteration Exception
- if len(body.keys()) != 1:
- return body
- # Just return the "wrapped" element
- first_key, first_item = six.next(six.iteritems(body))
- if isinstance(first_item, (dict, list)):
- return first_item
- except (ValueError, IndexError):
- pass
- return body
-
- def response_checker(self, method, resp, resp_body):
- """A sanity check on the response from a HTTP request
-
- This method does a sanity check on whether the response from an HTTP
- request conforms the HTTP RFC.
-
- :param str method: The HTTP verb of the request associated with the
- response being passed in.
- :param resp: The response headers
- :param resp_body: The body of the response
- :raises ResponseWithNonEmptyBody: If the response with the status code
- is not supposed to have a body
- :raises ResponseWithEntity: If the response code is 205 but has an
- entity
- """
- if (resp.status in set((204, 205, 304)) or resp.status < 200 or
- method.upper() == 'HEAD') and resp_body:
- raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
- # NOTE(afazekas):
- # If the HTTP Status Code is 205
- # 'The response MUST NOT include an entity.'
- # A HTTP entity has an entity-body and an 'entity-header'.
- # In the HTTP response specification (Section 6) the 'entity-header'
- # 'generic-header' and 'response-header' are in OR relation.
- # All headers not in the above two group are considered as entity
- # header in every interpretation.
-
- if (resp.status == 205 and
- 0 != len(set(resp.keys()) - set(('status',)) -
- self.response_header_lc - self.general_header_lc)):
- raise exceptions.ResponseWithEntity()
- # NOTE(afazekas)
- # Now the swift sometimes (delete not empty container)
- # returns with non json error response, we can create new rest class
- # for swift.
- # Usually RFC2616 says error responses SHOULD contain an explanation.
- # The warning is normal for SHOULD/SHOULD NOT case
-
- # Likely it will cause an error
- if method != 'HEAD' and not resp_body and resp.status >= 400:
- self.LOG.warning("status >= 400 response with empty body")
-
- def _request(self, method, url, headers=None, body=None):
- """A simple HTTP request interface."""
- # Authenticate the request with the auth provider
- req_url, req_headers, req_body = self.auth_provider.auth_request(
- method, url, headers, body, self.filters)
-
- # Do the actual request, and time it
- start = time.time()
- self._log_request_start(method, req_url)
- resp, resp_body = self.raw_request(
- req_url, method, headers=req_headers, body=req_body)
- end = time.time()
- self._log_request(method, req_url, resp, secs=(end - start),
- req_headers=req_headers, req_body=req_body,
- resp_body=resp_body)
-
- # Verify HTTP response codes
- self.response_checker(method, resp, resp_body)
-
- return resp, resp_body
-
- def raw_request(self, url, method, headers=None, body=None):
- """Send a raw HTTP request without the keystone catalog or auth
-
- This method sends a HTTP request in the same manner as the request()
- method, however it does so without using keystone auth or the catalog
- to determine the base url. Additionally no response handling is done
- the results from the request are just returned.
-
- :param str url: Full url to send the request
- :param str method: The HTTP verb to use for the request
- :param str headers: Headers to use for the request if none are specifed
- the headers
- :param str body: Body to to send with the request
- :rtype: tuple
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- """
- if headers is None:
- headers = self.get_headers()
- return self.http_obj.request(url, method,
- headers=headers, body=body)
-
- def request(self, method, url, extra_headers=False, headers=None,
- body=None):
- """Send a HTTP request with keystone auth and using the catalog
-
- This method will send an HTTP request using keystone auth in the
- headers and the catalog to determine the endpoint to use for the
- baseurl to send the request to. Additionally
-
- When a response is received it will check it to see if an error
- response was received. If it was an exception will be raised to enable
- it to be handled quickly.
-
- This method will also handle rate-limiting, if a 413 response code is
- received it will retry the request after waiting the 'retry-after'
- duration from the header.
-
- :param str method: The HTTP verb to use for the request
- :param str url: Relative url to send the request to
- :param dict extra_headers: If specified without the headers kwarg the
- headers sent with the request will be the
- combination from the get_headers() method
- and this kwarg
- :param dict headers: Headers to use for the request if none are
- specifed the headers returned from the
- get_headers() method are used. If the request
- explicitly requires no headers use an empty dict.
- :param str body: Body to to send with the request
- :rtype: tuple
- :return: a tuple with the first entry containing the response headers
- and the second the response body
- :raises UnexpectedContentType: If the content-type of the response
- isn't an expect type
- :raises Unauthorized: If a 401 response code is received
- :raises Forbidden: If a 403 response code is received
- :raises NotFound: If a 404 response code is received
- :raises BadRequest: If a 400 response code is received
- :raises Gone: If a 410 response code is received
- :raises Conflict: If a 409 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 InvalidContentType: If a 415 response code is received
- :raises UnprocessableEntity: If a 422 response code is received
- :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
- and couldn't be parsed
- :raises NotImplemented: If a 501 response code is received
- :raises ServerFault: If a 500 response code is received
- :raises UnexpectedResponseCode: If a response code above 400 is
- received and it doesn't fall into any
- of the handled checks
- """
- # if extra_headers is True
- # default headers would be added to headers
- retry = 0
-
- if headers is None:
- # NOTE(vponomaryov): if some client do not need headers,
- # it should explicitly pass empty dict
- headers = self.get_headers()
- elif extra_headers:
- try:
- headers = headers.copy()
- headers.update(self.get_headers())
- except (ValueError, TypeError):
- headers = self.get_headers()
-
- resp, resp_body = self._request(method, url,
- headers=headers, body=body)
-
- while (resp.status == 413 and
- 'retry-after' in resp and
- not self.is_absolute_limit(
- resp, self._parse_resp(resp_body)) and
- retry < MAX_RECURSION_DEPTH):
- retry += 1
- delay = int(resp['retry-after'])
- time.sleep(delay)
- resp, resp_body = self._request(method, url,
- headers=headers, body=body)
- self._error_checker(method, url, headers, body,
- resp, resp_body)
- return resp, resp_body
-
- def _error_checker(self, method, url,
- headers, body, resp, resp_body):
-
- # NOTE(mtreinish): Check for httplib response from glance_http. The
- # object can't be used here because importing httplib breaks httplib2.
- # If another object from a class not imported were passed here as
- # resp this could possibly fail
- if str(type(resp)) == "<type 'instance'>":
- ctype = resp.getheader('content-type')
- else:
- try:
- ctype = resp['content-type']
- # NOTE(mtreinish): Keystone delete user responses doesn't have a
- # content-type header. (They don't have a body) So just pretend it
- # is set.
- except KeyError:
- ctype = 'application/json'
-
- # It is not an error response
- if resp.status < 400:
- return
-
- JSON_ENC = ['application/json', 'application/json; charset=utf-8']
- # NOTE(mtreinish): This is for compatibility with Glance and swift
- # APIs. These are the return content types that Glance api v1
- # (and occasionally swift) are using.
- TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
- 'text/plain; charset=utf-8']
-
- if ctype.lower() in JSON_ENC:
- parse_resp = True
- elif ctype.lower() in TXT_ENC:
- parse_resp = False
- else:
- raise exceptions.UnexpectedContentType(str(resp.status),
- resp=resp)
-
- if resp.status == 401:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.Unauthorized(resp_body, resp=resp)
-
- if resp.status == 403:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.Forbidden(resp_body, resp=resp)
-
- if resp.status == 404:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.NotFound(resp_body, resp=resp)
-
- if resp.status == 400:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.BadRequest(resp_body, resp=resp)
-
- if resp.status == 410:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.Gone(resp_body, resp=resp)
-
- if resp.status == 409:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.Conflict(resp_body, resp=resp)
-
- if resp.status == 413:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- if self.is_absolute_limit(resp, resp_body):
- raise exceptions.OverLimit(resp_body, resp=resp)
- else:
- raise exceptions.RateLimitExceeded(resp_body, resp=resp)
-
- if resp.status == 415:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.InvalidContentType(resp_body, resp=resp)
-
- if resp.status == 422:
- if parse_resp:
- resp_body = self._parse_resp(resp_body)
- raise exceptions.UnprocessableEntity(resp_body, resp=resp)
-
- if resp.status in (500, 501):
- message = resp_body
- if parse_resp:
- try:
- resp_body = self._parse_resp(resp_body)
- except ValueError:
- # If response body is a non-json string message.
- # Use resp_body as is and raise InvalidResponseBody
- # exception.
- raise exceptions.InvalidHTTPResponseBody(message)
- else:
- if isinstance(resp_body, dict):
- # I'm seeing both computeFault
- # and cloudServersFault come back.
- # Will file a bug to fix, but leave as is for now.
- if 'cloudServersFault' in resp_body:
- message = resp_body['cloudServersFault']['message']
- elif 'computeFault' in resp_body:
- message = resp_body['computeFault']['message']
- elif 'error' in resp_body:
- message = resp_body['error']['message']
- elif 'message' in resp_body:
- message = resp_body['message']
- else:
- message = resp_body
-
- if resp.status == 501:
- raise exceptions.NotImplemented(resp_body, resp=resp,
- message=message)
- else:
- raise exceptions.ServerFault(resp_body, resp=resp,
- message=message)
-
- if resp.status >= 400:
- raise exceptions.UnexpectedResponseCode(str(resp.status),
- resp=resp)
-
- def is_absolute_limit(self, resp, resp_body):
- if (not isinstance(resp_body, collections.Mapping) or
- 'retry-after' not in resp):
- return True
- over_limit = resp_body.get('overLimit', None)
- if not over_limit:
- return True
- return 'exceed' in over_limit.get('message', 'blabla')
-
- def wait_for_resource_deletion(self, id):
- """Waits for a resource to be deleted
-
- This method will loop over is_resource_deleted until either
- is_resource_deleted returns True or the build timeout is reached. This
- depends on is_resource_deleted being implemented
-
- :param str id: The id of the resource to check
- :raises TimeoutException: If the build_timeout has elapsed and the
- resource still hasn't been deleted
- """
- start_time = int(time.time())
- while True:
- if self.is_resource_deleted(id):
- return
- if int(time.time()) - start_time >= self.build_timeout:
- message = ('Failed to delete %(resource_type)s %(id)s within '
- 'the required time (%(timeout)s s).' %
- {'resource_type': self.resource_type, 'id': id,
- 'timeout': self.build_timeout})
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
- time.sleep(self.build_interval)
-
- def is_resource_deleted(self, id):
- """Subclasses override with specific deletion detection."""
- message = ('"%s" does not implement is_resource_deleted'
- % self.__class__.__name__)
- raise NotImplementedError(message)
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'resource'
-
- @classmethod
- def validate_response(cls, schema, resp, body):
- # Only check the response if the status code is a success code
- # TODO(cyeoh): Eventually we should be able to verify that a failure
- # code if it exists is something that we expect. This is explicitly
- # declared in the V3 API and so we should be able to export this in
- # the response schema. For now we'll ignore it.
- if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
- cls.expected_success(schema['status_code'], resp.status)
-
- # Check the body of a response
- body_schema = schema.get('response_body')
- if body_schema:
- try:
- jsonschema.validate(body, body_schema,
- cls=JSONSCHEMA_VALIDATOR,
- format_checker=FORMAT_CHECKER)
- except jsonschema.ValidationError as ex:
- msg = ("HTTP response body is invalid (%s)") % ex
- raise exceptions.InvalidHTTPResponseBody(msg)
- else:
- if body:
- msg = ("HTTP response body should not exist (%s)") % body
- raise exceptions.InvalidHTTPResponseBody(msg)
-
- # Check the header of a response
- header_schema = schema.get('response_header')
- if header_schema:
- try:
- jsonschema.validate(resp, header_schema,
- cls=JSONSCHEMA_VALIDATOR,
- format_checker=FORMAT_CHECKER)
- except jsonschema.ValidationError as ex:
- msg = ("HTTP response header is invalid (%s)") % ex
- raise exceptions.InvalidHTTPResponseHeader(msg)
-
-
-class ResponseBody(dict):
- """Class that wraps an http response and dict body into a single value.
-
- Callers that receive this object will normally use it as a dict but
- can extract the response if needed.
- """
-
- def __init__(self, response, body=None):
- body_data = body or {}
- self.update(body_data)
- self.response = response
-
- def __str__(self):
- body = super(ResponseBody, self).__str__()
- return "response: %s\nBody: %s" % (self.response, body)
-
-
-class ResponseBodyData(object):
- """Class that wraps an http response and string data into a single value.
-
- """
-
- def __init__(self, response, data):
- self.response = response
- self.data = data
-
- def __str__(self):
- return "response: %s\nBody: %s" % (self.response, self.data)
-
-
-class ResponseBodyList(list):
- """Class that wraps an http response and list body into a single value.
-
- Callers that receive this object will normally use it as a list but
- can extract the response if needed.
- """
-
- def __init__(self, response, body=None):
- body_data = body or []
- self.extend(body_data)
- self.response = response
-
- def __str__(self):
- body = super(ResponseBodyList, self).__str__()
- return "response: %s\nBody: %s" % (self.response, body)
diff --git a/tempest_lib/common/ssh.py b/tempest_lib/common/ssh.py
deleted file mode 100644
index e55d359..0000000
--- a/tempest_lib/common/ssh.py
+++ /dev/null
@@ -1,174 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# 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 select
-import socket
-import time
-import warnings
-
-from oslo_log import log as logging
-import six
-
-from tempest_lib import exceptions
-
-
-with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- import paramiko
-
-
-LOG = logging.getLogger(__name__)
-
-
-class Client(object):
-
- def __init__(self, host, username, password=None, timeout=300, pkey=None,
- channel_timeout=10, look_for_keys=False, key_filename=None):
- self.host = host
- self.username = username
- self.password = password
- if isinstance(pkey, six.string_types):
- pkey = paramiko.RSAKey.from_private_key(
- six.StringIO(str(pkey)))
- self.pkey = pkey
- self.look_for_keys = look_for_keys
- self.key_filename = key_filename
- self.timeout = int(timeout)
- self.channel_timeout = float(channel_timeout)
- self.buf_size = 1024
-
- def _get_ssh_connection(self, sleep=1.5, backoff=1):
- """Returns an ssh connection to the specified host."""
- bsleep = sleep
- ssh = paramiko.SSHClient()
- ssh.set_missing_host_key_policy(
- paramiko.AutoAddPolicy())
- _start_time = time.time()
- if self.pkey is not None:
- LOG.info("Creating ssh connection to '%s' as '%s'"
- " with public key authentication",
- self.host, self.username)
- else:
- LOG.info("Creating ssh connection to '%s' as '%s'"
- " with password %s",
- self.host, self.username, str(self.password))
- attempts = 0
- while True:
- try:
- ssh.connect(self.host, username=self.username,
- password=self.password,
- look_for_keys=self.look_for_keys,
- key_filename=self.key_filename,
- timeout=self.channel_timeout, pkey=self.pkey)
- LOG.info("ssh connection to %s@%s successfully created",
- self.username, self.host)
- return ssh
- except (EOFError,
- socket.error,
- paramiko.SSHException) as e:
- if self._is_timed_out(_start_time):
- LOG.exception("Failed to establish authenticated ssh"
- " connection to %s@%s after %d attempts",
- self.username, self.host, attempts)
- raise exceptions.SSHTimeout(host=self.host,
- user=self.username,
- password=self.password)
- bsleep += backoff
- attempts += 1
- LOG.warning("Failed to establish authenticated ssh"
- " connection to %s@%s (%s). Number attempts: %s."
- " Retry after %d seconds.",
- self.username, self.host, e, attempts, bsleep)
- time.sleep(bsleep)
-
- def _is_timed_out(self, start_time):
- return (time.time() - self.timeout) > start_time
-
- @staticmethod
- def _can_system_poll():
- return hasattr(select, 'poll')
-
- def exec_command(self, cmd, encoding="utf-8"):
- """Execute the specified command on the server
-
- Note that this method is reading whole command outputs to memory, thus
- shouldn't be used for large outputs.
-
- :param str cmd: Command to run at remote server.
- :param str encoding: Encoding for result from paramiko.
- Result will not be decoded if None.
- :returns: data read from standard output of the command.
- :raises: SSHExecCommandFailed if command returns nonzero
- status. The exception contains command status stderr content.
- :raises: TimeoutException if cmd doesn't end when timeout expires.
- """
- ssh = self._get_ssh_connection()
- transport = ssh.get_transport()
- channel = transport.open_session()
- channel.fileno() # Register event pipe
- channel.exec_command(cmd)
- channel.shutdown_write()
- exit_status = channel.recv_exit_status()
-
- # If the executing host is linux-based, poll the channel
- if self._can_system_poll():
- out_data_chunks = []
- err_data_chunks = []
- poll = select.poll()
- poll.register(channel, select.POLLIN)
- start_time = time.time()
-
- while True:
- ready = poll.poll(self.channel_timeout)
- if not any(ready):
- if not self._is_timed_out(start_time):
- continue
- raise exceptions.TimeoutException(
- "Command: '{0}' executed on host '{1}'.".format(
- cmd, self.host))
- if not ready[0]: # If there is nothing to read.
- continue
- out_chunk = err_chunk = None
- if channel.recv_ready():
- out_chunk = channel.recv(self.buf_size)
- out_data_chunks += out_chunk,
- if channel.recv_stderr_ready():
- err_chunk = channel.recv_stderr(self.buf_size)
- err_data_chunks += err_chunk,
- if channel.closed and not err_chunk and not out_chunk:
- break
- out_data = b''.join(out_data_chunks)
- err_data = b''.join(err_data_chunks)
- # Just read from the channels
- else:
- out_file = channel.makefile('rb', self.buf_size)
- err_file = channel.makefile_stderr('rb', self.buf_size)
- out_data = out_file.read()
- err_data = err_file.read()
- if encoding:
- out_data = out_data.decode(encoding)
- err_data = err_data.decode(encoding)
-
- if 0 != exit_status:
- raise exceptions.SSHExecCommandFailed(
- command=cmd, exit_status=exit_status,
- stderr=err_data, stdout=out_data)
- return out_data
-
- def test_connection_auth(self):
- """Raises an exception when we can not connect to server via ssh."""
- connection = self._get_ssh_connection()
- connection.close()
diff --git a/tempest_lib/common/utils/__init__.py b/tempest_lib/common/utils/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest_lib/common/utils/__init__.py
+++ /dev/null
diff --git a/tempest_lib/common/utils/data_utils.py b/tempest_lib/common/utils/data_utils.py
deleted file mode 100644
index 01b6477..0000000
--- a/tempest_lib/common/utils/data_utils.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# 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 itertools
-import netaddr
-import random
-import string
-import uuid
-
-
-def rand_uuid():
- """Generate a random UUID string
-
- :return: a random UUID (e.g. '1dc12c7d-60eb-4b61-a7a2-17cf210155b6')
- :rtype: string
- """
- return str(uuid.uuid4())
-
-
-def rand_uuid_hex():
- """Generate a random UUID hex string
-
- :return: a random UUID (e.g. '0b98cf96d90447bda4b46f31aeb1508c')
- :rtype: string
- """
- return uuid.uuid4().hex
-
-
-def rand_name(name='', prefix=None):
- """Generate a random name that inclues a random number
-
- :param str name: The name that you want to include
- :param str prefix: The prefix that you want to include
- :return: a random name. The format is
- '<prefix>-<random number>-<name>-<random number>'.
- (e.g. 'prefixfoo-1308607012-namebar-154876201')
- :rtype: string
- """
- randbits = str(random.randint(1, 0x7fffffff))
- rand_name = randbits
- if name:
- rand_name = name + '-' + rand_name
- if prefix:
- rand_name = prefix + '-' + rand_name
- return rand_name
-
-
-def rand_password(length=15):
- """Generate a random password
-
- :param int length: The length of password that you expect to set
- (If it's smaller than 3, it's same as 3.)
- :return: a random password. The format is
- '<random upper letter>-<random number>-<random special character>
- -<random ascii letters or digit characters or special symbols>'
- (e.g. 'G2*ac8&lKFFgh%2')
- :rtype: string
- """
- upper = random.choice(string.ascii_uppercase)
- ascii_char = string.ascii_letters
- digits = string.digits
- digit = random.choice(string.digits)
- puncs = '~!@#$%^&*_=+'
- punc = random.choice(puncs)
- seed = ascii_char + digits + puncs
- pre = upper + digit + punc
- password = pre + ''.join(random.choice(seed) for x in range(length - 3))
- return password
-
-
-def rand_url():
- """Generate a random url that inclues a random number
-
- :return: a random url. The format is 'https://url-<random number>.com'.
- (e.g. 'https://url-154876201.com')
- :rtype: string
- """
- randbits = str(random.randint(1, 0x7fffffff))
- return 'https://url-' + randbits + '.com'
-
-
-def rand_int_id(start=0, end=0x7fffffff):
- """Generate a random integer value
-
- :param int start: The value that you expect to start here
- :param int end: The value that you expect to end here
- :return: a random integer value
- :rtype: int
- """
- return random.randint(start, end)
-
-
-def rand_mac_address():
- """Generate an Ethernet MAC address
-
- :return: an random Ethernet MAC address
- :rtype: string
- """
- # NOTE(vish): We would prefer to use 0xfe here to ensure that linux
- # bridge mac addresses don't change, but it appears to
- # conflict with libvirt, so we use the next highest octet
- # that has the unicast and locally administered bits set
- # properly: 0xfa.
- # Discussion: https://bugs.launchpad.net/nova/+bug/921838
- mac = [0xfa, 0x16, 0x3e,
- random.randint(0x00, 0xff),
- random.randint(0x00, 0xff),
- random.randint(0x00, 0xff)]
- return ':'.join(["%02x" % x for x in mac])
-
-
-def parse_image_id(image_ref):
- """Return the image id from a given image ref
-
- This function just returns the last word of the given image ref string
- splitting with '/'.
- :param str image_ref: a string that includes the image id
- :return: the image id string
- :rtype: string
- """
- return image_ref.rsplit('/')[-1]
-
-
-def arbitrary_string(size=4, base_text=None):
- """Return size characters from base_text
-
- This generates a string with an arbitrary number of characters, generated
- by looping the base_text string. If the size is smaller than the size of
- base_text, returning string is shrinked to the size.
- :param int size: a returning charactors size
- :param str base_text: a string you want to repeat
- :return: size string
- :rtype: string
- """
- if not base_text:
- base_text = 'test'
- return ''.join(itertools.islice(itertools.cycle(base_text), size))
-
-
-def random_bytes(size=1024):
- """Return size randomly selected bytes as a string
-
- :param int size: a returning bytes size
- :return: size randomly bytes
- :rtype: string
- """
- return ''.join([chr(random.randint(0, 255))
- for i in range(size)])
-
-
-def get_ipv6_addr_by_EUI64(cidr, mac):
- """Generate a IPv6 addr by EUI-64 with CIDR and MAC
-
- :param str cidr: a IPv6 CIDR
- :param str mac: a MAC address
- :return: an IPv6 Address
- :rtype: netaddr.IPAddress
- """
- # Check if the prefix is IPv4 address
- is_ipv4 = netaddr.valid_ipv4(cidr)
- if is_ipv4:
- msg = "Unable to generate IP address by EUI64 for IPv4 prefix"
- raise TypeError(msg)
- try:
- eui64 = int(netaddr.EUI(mac).eui64())
- prefix = netaddr.IPNetwork(cidr)
- return netaddr.IPAddress(prefix.first + eui64 ^ (1 << 57))
- except (ValueError, netaddr.AddrFormatError):
- raise TypeError('Bad prefix or mac format for generating IPv6 '
- 'address by EUI-64: %(prefix)s, %(mac)s:'
- % {'prefix': cidr, 'mac': mac})
- except TypeError:
- raise TypeError('Bad prefix type for generate IPv6 address by '
- 'EUI-64: %s' % cidr)
diff --git a/tempest_lib/common/utils/misc.py b/tempest_lib/common/utils/misc.py
deleted file mode 100644
index b97dd86..0000000
--- a/tempest_lib/common/utils/misc.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# 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 inspect
-import re
-
-from oslo_log import log as logging
-
-LOG = logging.getLogger(__name__)
-
-
-def singleton(cls):
- """Simple wrapper for classes that should only have a single instance."""
- instances = {}
-
- def getinstance():
- if cls not in instances:
- instances[cls] = cls()
- return instances[cls]
- return getinstance
-
-
-def find_test_caller():
- """Find the caller class and test name.
-
- Because we know that the interesting things that call us are
- test_* methods, and various kinds of setUp / tearDown, we
- can look through the call stack to find appropriate methods,
- and the class we were in when those were called.
- """
- caller_name = None
- names = []
- frame = inspect.currentframe()
- is_cleanup = False
- # Start climbing the ladder until we hit a good method
- while True:
- try:
- frame = frame.f_back
- name = frame.f_code.co_name
- names.append(name)
- if re.search("^(test_|setUp|tearDown)", name):
- cname = ""
- if 'self' in frame.f_locals:
- cname = frame.f_locals['self'].__class__.__name__
- if 'cls' in frame.f_locals:
- cname = frame.f_locals['cls'].__name__
- caller_name = cname + ":" + name
- break
- elif re.search("^_run_cleanup", name):
- is_cleanup = True
- elif name == 'main':
- caller_name = 'main'
- break
- else:
- cname = ""
- if 'self' in frame.f_locals:
- cname = frame.f_locals['self'].__class__.__name__
- if 'cls' in frame.f_locals:
- cname = frame.f_locals['cls'].__name__
-
- # the fact that we are running cleanups is indicated pretty
- # deep in the stack, so if we see that we want to just
- # start looking for a real class name, and declare victory
- # once we do.
- if is_cleanup and cname:
- if not re.search("^RunTest", cname):
- caller_name = cname + ":_run_cleanups"
- break
- except Exception:
- break
- # prevents frame leaks
- del frame
- if caller_name is None:
- LOG.debug("Sane call name not found in %s" % names)
- return caller_name