diff --git a/ceilometer/tests/tempest/exceptions.py b/ceilometer/tests/tempest/exceptions.py new file mode 100644 index 00000000..92f335f9 --- /dev/null +++ b/ceilometer/tests/tempest/exceptions.py @@ -0,0 +1,169 @@ +# 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 testtools + + +class TempestException(Exception): + """Base Tempest Exception + + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + message = "An unknown exception occurred" + + def __init__(self, *args, **kwargs): + super(TempestException, self).__init__() + try: + self._error_string = self.message % kwargs + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + if len(args) > 0: + # If there is a non-kwarg parameter, assume it's the error + # message or reason description and tack it on to the end + # of the exception message + # Convert all arguments into their string representations... + args = ["%s" % arg for arg in args] + self._error_string = (self._error_string + + "\nDetails: %s" % '\n'.join(args)) + + def __str__(self): + return self._error_string + + +class RestClientException(TempestException, + testtools.TestCase.failureException): + pass + + +class InvalidConfiguration(TempestException): + message = "Invalid Configuration" + + +class InvalidCredentials(TempestException): + message = "Invalid Credentials" + + +class InvalidServiceTag(TempestException): + message = "Invalid service tag" + + +class InvalidIdentityVersion(TempestException): + message = "Invalid version %(identity_version)s of the identity service" + + +class TimeoutException(TempestException): + message = "Request timed out" + + +class BuildErrorException(TempestException): + message = "Server %(server_id)s failed to build and is in ERROR status" + + +class ImageKilledException(TempestException): + message = "Image %(image_id)s 'killed' while waiting for '%(status)s'" + + +class AddImageException(TempestException): + message = "Image %(image_id)s failed to become ACTIVE in the allotted time" + + +class VolumeBuildErrorException(TempestException): + message = "Volume %(volume_id)s failed to build and is in ERROR status" + + +class VolumeRestoreErrorException(TempestException): + message = "Volume %(volume_id)s failed to restore and is in ERROR status" + + +class SnapshotBuildErrorException(TempestException): + message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status" + + +class VolumeBackupException(TempestException): + message = "Volume backup %(backup_id)s failed and is in ERROR status" + + +class StackBuildErrorException(TempestException): + message = ("Stack %(stack_identifier)s is in %(stack_status)s status " + "due to '%(stack_status_reason)s'") + + +class EndpointNotFound(TempestException): + message = "Endpoint not found" + + +class IdentityError(TempestException): + message = "Got identity error" + + +class ServerUnreachable(TempestException): + message = "The server is not reachable via the configured network" + + +# NOTE(andreaf) This exception is added here to facilitate the migration +# of get_network_from_name and preprov_creds to tempest.lib, and it should +# be migrated along with them +class InvalidTestResource(TempestException): + message = "%(name) is not a valid %(type), or the name is ambiguous" + + +class RFCViolation(RestClientException): + message = "RFC Violation" + + +class InvalidHttpSuccessCode(RestClientException): + message = "The success code is different than the expected one" + + +class BadRequest(RestClientException): + message = "Bad request" + + +class ResponseWithNonEmptyBody(RFCViolation): + message = ("RFC Violation! Response with %(status)d HTTP Status Code " + "MUST NOT have a body") + + +class ResponseWithEntity(RFCViolation): + message = ("RFC Violation! Response with 205 HTTP Status Code " + "MUST NOT have an entity") + + +class InvalidHTTPResponseHeader(RestClientException): + message = "HTTP response header is invalid" + + +class InvalidStructure(TempestException): + message = "Invalid structure of table with details" + + +class CommandFailed(Exception): + def __init__(self, returncode, cmd, output, stderr): + super(CommandFailed, self).__init__() + self.returncode = returncode + self.cmd = cmd + self.stdout = output + self.stderr = stderr + + def __str__(self): + return ("Command '%s' returned non-zero exit status %d.\n" + "stdout:\n%s\n" + "stderr:\n%s" % (self.cmd, + self.returncode, + self.stdout, + self.stderr)) diff --git a/ceilometer/tests/tempest/service/client.py b/ceilometer/tests/tempest/service/client.py index a322ad18..179f8a1a 100644 --- a/ceilometer/tests/tempest/service/client.py +++ b/ceilometer/tests/tempest/service/client.py @@ -23,12 +23,14 @@ from tempest.lib.services.compute.floating_ips_client import FloatingIPsClient from tempest.lib.services.compute.networks_client import NetworksClient from tempest.lib.services.compute.servers_client import ServersClient from tempest import manager -from tempest.services.image.v1.json.images_client import ImagesClient -from tempest.services.image.v2.json.images_client import \ - ImagesClient as ImagesClientV2 from tempest.services.object_storage.container_client import ContainerClient from tempest.services.object_storage.object_client import ObjectClient +from ceilometer.tests.tempest.service.images.v1.images_client import \ + ImagesClient +from ceilometer.tests.tempest.service.images.v2.images_client import \ + ImagesClient as ImagesClientV2 + CONF = config.CONF diff --git a/ceilometer/tests/tempest/service/images/__init__.py b/ceilometer/tests/tempest/service/images/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ceilometer/tests/tempest/service/images/glance_http.py b/ceilometer/tests/tempest/service/images/glance_http.py new file mode 100644 index 00000000..19a9d981 --- /dev/null +++ b/ceilometer/tests/tempest/service/images/glance_http.py @@ -0,0 +1,361 @@ +# 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. + +# Originally copied from python-glanceclient + +import copy +import hashlib +import posixpath +import re +import socket +import struct + +import OpenSSL +from oslo_log import log as logging +import six +from six import moves +from six.moves import http_client as httplib +from six.moves.urllib import parse as urlparse + +from ceilometer.tests.tempest import exceptions as exc + +LOG = logging.getLogger(__name__) +USER_AGENT = 'tempest' +CHUNKSIZE = 1024 * 64 # 64kB +TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$') + + +class HTTPClient(object): + + def __init__(self, auth_provider, filters, **kwargs): + self.auth_provider = auth_provider + self.filters = filters + self.endpoint = auth_provider.base_url(filters) + endpoint_parts = urlparse.urlparse(self.endpoint) + self.endpoint_scheme = endpoint_parts.scheme + self.endpoint_hostname = endpoint_parts.hostname + self.endpoint_port = endpoint_parts.port + + self.connection_class = self._get_connection_class( + self.endpoint_scheme) + self.connection_kwargs = self._get_connection_kwargs( + self.endpoint_scheme, **kwargs) + + @staticmethod + def _get_connection_class(scheme): + if scheme == 'https': + return VerifiedHTTPSConnection + else: + return httplib.HTTPConnection + + @staticmethod + def _get_connection_kwargs(scheme, **kwargs): + _kwargs = {'timeout': float(kwargs.get('timeout', 600))} + + if scheme == 'https': + _kwargs['ca_certs'] = kwargs.get('ca_certs', None) + _kwargs['cert_file'] = kwargs.get('cert_file', None) + _kwargs['key_file'] = kwargs.get('key_file', None) + _kwargs['insecure'] = kwargs.get('insecure', False) + _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True) + + return _kwargs + + def _get_connection(self): + _class = self.connection_class + try: + return _class(self.endpoint_hostname, self.endpoint_port, + **self.connection_kwargs) + except httplib.InvalidURL: + raise exc.EndpointNotFound + + def _http_request(self, url, method, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around httplib.HTTP(S)Connection.request to handle tasks such + as setting headers and error handling. + """ + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', USER_AGENT) + + self._log_request(method, url, kwargs['headers']) + + conn = self._get_connection() + + try: + url_parts = urlparse.urlparse(url) + conn_url = posixpath.normpath(url_parts.path) + LOG.debug('Actual Path: {path}'.format(path=conn_url)) + if kwargs['headers'].get('Transfer-Encoding') == 'chunked': + conn.putrequest(method, conn_url) + for header, value in kwargs['headers'].items(): + conn.putheader(header, value) + conn.endheaders() + chunk = kwargs['body'].read(CHUNKSIZE) + # Chunk it, baby... + while chunk: + conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) + chunk = kwargs['body'].read(CHUNKSIZE) + conn.send('0\r\n\r\n') + else: + conn.request(method, conn_url, **kwargs) + resp = conn.getresponse() + except socket.gaierror as e: + message = ("Error finding address for %(url)s: %(e)s" % + {'url': url, 'e': e}) + raise exc.EndpointNotFound(message) + except (socket.error, socket.timeout) as e: + message = ("Error communicating with %(endpoint)s %(e)s" % + {'endpoint': self.endpoint, 'e': e}) + raise exc.TimeoutException(message) + + body_iter = ResponseBodyIterator(resp) + # Read body into string if it isn't obviously image data + if resp.getheader('content-type', None) != 'application/octet-stream': + body_str = ''.join([body_chunk for body_chunk in body_iter]) + body_iter = six.StringIO(body_str) + self._log_response(resp, None) + else: + self._log_response(resp, body_iter) + + return resp, body_iter + + def _log_request(self, method, url, headers): + LOG.info('Request: ' + method + ' ' + url) + if headers: + headers_out = headers + if 'X-Auth-Token' in headers and headers['X-Auth-Token']: + token = headers['X-Auth-Token'] + if len(token) > 64 and TOKEN_CHARS_RE.match(token): + headers_out = headers.copy() + headers_out['X-Auth-Token'] = "" + LOG.info('Request Headers: ' + str(headers_out)) + + def _log_response(self, resp, body): + status = str(resp.status) + LOG.info("Response Status: " + status) + if resp.getheaders(): + LOG.info('Response Headers: ' + str(resp.getheaders())) + if body: + str_body = str(body) + length = len(body) + LOG.info('Response Body: ' + str_body[:2048]) + if length >= 2048: + self.LOG.debug("Large body (%d) md5 summary: %s", length, + hashlib.md5(str_body).hexdigest()) + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + if 'body' in kwargs: + if (hasattr(kwargs['body'], 'read') + and method.lower() in ('post', 'put')): + # We use 'Transfer-Encoding: chunked' because + # body size may not always be known in advance. + kwargs['headers']['Transfer-Encoding'] = 'chunked' + + # Decorate the request with auth + req_url, kwargs['headers'], kwargs['body'] = \ + self.auth_provider.auth_request( + method=method, url=url, headers=kwargs['headers'], + body=kwargs.get('body', None), filters=self.filters) + return self._http_request(req_url, method, **kwargs) + + +class OpenSSLConnectionDelegator(object): + """An OpenSSL.SSL.Connection delegator. + + Supplies an additional 'makefile' method which httplib requires + and is not present in OpenSSL.SSL.Connection. + + Note: Since it is not possible to inherit from OpenSSL.SSL.Connection + a delegator must be used. + """ + def __init__(self, *args, **kwargs): + self.connection = OpenSSL.SSL.Connection(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self.connection, name) + + def makefile(self, *args, **kwargs): + # Ensure the socket is closed when this file is closed + kwargs['close'] = True + return socket._fileobject(self.connection, *args, **kwargs) + + +class VerifiedHTTPSConnection(httplib.HTTPSConnection): + """Extended HTTPSConnection which uses OpenSSL library for enhanced SSL + + Note: Much of this functionality can eventually be replaced + with native Python 3.3 code. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + ca_certs=None, timeout=None, insecure=False, + ssl_compression=True): + httplib.HTTPSConnection.__init__(self, host, port, + key_file=key_file, + cert_file=cert_file) + self.key_file = key_file + self.cert_file = cert_file + self.timeout = timeout + self.insecure = insecure + self.ssl_compression = ssl_compression + self.ca_certs = ca_certs + self.setcontext() + + @staticmethod + def host_matches_cert(host, x509): + """Verify that the x509 certificate we have received from 'host' + + Identifies the server we are connecting to, ie that the certificate's + Common Name or a Subject Alternative Name matches 'host'. + """ + # First see if we can match the CN + if x509.get_subject().commonName == host: + return True + + # Also try Subject Alternative Names for a match + san_list = None + for i in moves.xrange(x509.get_extension_count()): + ext = x509.get_extension(i) + if ext.get_short_name() == 'subjectAltName': + san_list = str(ext) + for san in ''.join(san_list.split()).split(','): + if san == "DNS:%s" % host: + return True + + # Server certificate does not match host + msg = ('Host "%s" does not match x509 certificate contents: ' + 'CommonName "%s"' % (host, x509.get_subject().commonName)) + if san_list is not None: + msg = msg + ', subjectAltName "%s"' % san_list + raise exc.SSLCertificateError(msg) + + def verify_callback(self, connection, x509, errnum, + depth, preverify_ok): + if x509.has_expired(): + msg = "SSL Certificate expired on '%s'" % x509.get_notAfter() + raise exc.SSLCertificateError(msg) + + if depth == 0 and preverify_ok is True: + # We verify that the host matches against the last + # certificate in the chain + return self.host_matches_cert(self.host, x509) + else: + # Pass through OpenSSL's default result + return preverify_ok + + def setcontext(self): + """Set up the OpenSSL context.""" + self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + + if self.ssl_compression is False: + self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION + + if self.insecure is not True: + self.context.set_verify(OpenSSL.SSL.VERIFY_PEER, + self.verify_callback) + else: + self.context.set_verify(OpenSSL.SSL.VERIFY_NONE, + self.verify_callback) + + if self.cert_file: + try: + self.context.use_certificate_file(self.cert_file) + except Exception as e: + msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e) + raise exc.SSLConfigurationError(msg) + if self.key_file is None: + # We support having key and cert in same file + try: + self.context.use_privatekey_file(self.cert_file) + except Exception as e: + msg = ('No key file specified and unable to load key ' + 'from "%s" %s' % (self.cert_file, e)) + raise exc.SSLConfigurationError(msg) + + if self.key_file: + try: + self.context.use_privatekey_file(self.key_file) + except Exception as e: + msg = 'Unable to load key from "%s" %s' % (self.key_file, e) + raise exc.SSLConfigurationError(msg) + + if self.ca_certs: + try: + self.context.load_verify_locations(self.ca_certs) + except Exception as e: + msg = 'Unable to load CA from "%s" %s' % (self.ca_certs, e) + raise exc.SSLConfigurationError(msg) + else: + self.context.set_default_verify_paths() + + def connect(self): + """Connect to SSL port and apply per-connection parameters.""" + try: + addresses = socket.getaddrinfo(self.host, + self.port, + socket.AF_UNSPEC, + socket.SOCK_STREAM) + except OSError as msg: + raise exc.RestClientException(msg) + for res in addresses: + af, socktype, proto, canonname, sa = res + sock = socket.socket(af, socket.SOCK_STREAM) + + if self.timeout is not None: + # '0' microseconds + sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, + struct.pack('LL', self.timeout, 0)) + self.sock = OpenSSLConnectionDelegator(self.context, sock) + try: + self.sock.connect(sa) + except OSError as msg: + if self.sock: + self.sock = None + continue + break + if self.sock is None: + # Happen only when all results have failed. + raise exc.RestClientException('Cannot connect to %s' % self.host) + + def close(self): + if self.sock: + # Remove the reference to the socket but don't close it yet. + # Response close will close both socket and associated + # file. Closing socket too soon will cause response + # reads to fail with socket IO error 'Bad file descriptor'. + self.sock = None + httplib.HTTPSConnection.close(self) + + +class ResponseBodyIterator(object): + """A class that acts as an iterator over an HTTP response.""" + + def __init__(self, resp): + self.resp = resp + + def __iter__(self): + while True: + yield self.next() + + def next(self): + chunk = self.resp.read(CHUNKSIZE) + if chunk: + return chunk + else: + raise StopIteration() diff --git a/ceilometer/tests/tempest/service/images/v1/__init__.py b/ceilometer/tests/tempest/service/images/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ceilometer/tests/tempest/service/images/v1/images_client.py b/ceilometer/tests/tempest/service/images/v1/images_client.py new file mode 100644 index 00000000..e9ca637d --- /dev/null +++ b/ceilometer/tests/tempest/service/images/v1/images_client.py @@ -0,0 +1,257 @@ +# 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 copy +import errno +import os + +from oslo_log import log as logging +from oslo_serialization import jsonutils as json +import six +from six.moves.urllib import parse as urllib +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + +from ceilometer.tests.tempest.service.images import glance_http + +LOG = logging.getLogger(__name__) + + +class ImagesClient(rest_client.RestClient): + + def __init__(self, auth_provider, catalog_type, region, **kwargs): + super(ImagesClient, self).__init__( + auth_provider, catalog_type, region, **kwargs) + self._http = None + self.dscv = kwargs.get("disable_ssl_certificate_validation") + self.ca_certs = kwargs.get("ca_certs") + + def _image_meta_from_headers(self, headers): + meta = {'properties': {}} + for key, value in six.iteritems(headers): + if key.startswith('x-image-meta-property-'): + _key = key[22:] + meta['properties'][_key] = value + elif key.startswith('x-image-meta-'): + _key = key[13:] + meta[_key] = value + + for key in ['is_public', 'protected', 'deleted']: + if key in meta: + meta[key] = meta[key].strip().lower() in ('t', 'true', 'yes', + '1') + for key in ['size', 'min_ram', 'min_disk']: + if key in meta: + try: + meta[key] = int(meta[key]) + except ValueError: + pass + return meta + + def _image_meta_to_headers(self, fields): + headers = {} + fields_copy = copy.deepcopy(fields) + copy_from = fields_copy.pop('copy_from', None) + if copy_from is not None: + headers['x-glance-api-copy-from'] = copy_from + for key, value in six.iteritems(fields_copy.pop('properties', {})): + headers['x-image-meta-property-%s' % key] = str(value) + for key, value in six.iteritems(fields_copy.pop('api', {})): + headers['x-glance-api-property-%s' % key] = str(value) + for key, value in six.iteritems(fields_copy): + headers['x-image-meta-%s' % key] = str(value) + return headers + + def _get_file_size(self, obj): + """Analyze file-like object and attempt to determine its size. + + :param obj: file-like object, typically redirected from stdin. + :retval The file's size or None if it cannot be determined. + """ + # For large images, we need to supply the size of the + # image file. See LP Bugs #827660 and #845788. + if hasattr(obj, 'seek') and hasattr(obj, 'tell'): + try: + obj.seek(0, os.SEEK_END) + obj_size = obj.tell() + obj.seek(0) + return obj_size + except IOError as e: + if e.errno == errno.ESPIPE: + # Illegal seek. This means the user is trying + # to pipe image data to the client, e.g. + # echo testdata | bin/glance add blah..., or + # that stdin is empty, or that a file-like + # object which doesn't support 'seek/tell' has + # been supplied. + return None + else: + raise + else: + # Cannot determine size of input image + return None + + def _get_http(self): + return glance_http.HTTPClient(auth_provider=self.auth_provider, + filters=self.filters, + insecure=self.dscv, + ca_certs=self.ca_certs) + + def _create_with_data(self, headers, data): + resp, body_iter = self.http.raw_request('POST', '/v1/images', + headers=headers, body=data) + self._error_checker('POST', '/v1/images', headers, data, resp, + body_iter) + body = json.loads(''.join([c for c in body_iter])) + return rest_client.ResponseBody(resp, body) + + def _update_with_data(self, image_id, headers, data): + url = '/v1/images/%s' % image_id + resp, body_iter = self.http.raw_request('PUT', url, headers=headers, + body=data) + self._error_checker('PUT', url, headers, data, + resp, body_iter) + body = json.loads(''.join([c for c in body_iter])) + return rest_client.ResponseBody(resp, body) + + @property + def http(self): + if self._http is None: + self._http = self._get_http() + return self._http + + def create_image(self, **kwargs): + headers = {} + data = kwargs.pop('data', None) + headers.update(self._image_meta_to_headers(kwargs)) + + if data is not None: + return self._create_with_data(headers, data) + + resp, body = self.post('v1/images', None, headers) + self.expected_success(201, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def update_image(self, image_id, **kwargs): + headers = {} + data = kwargs.pop('data', None) + headers.update(self._image_meta_to_headers(kwargs)) + + if data is not None: + return self._update_with_data(image_id, headers, data) + + url = 'v1/images/%s' % image_id + resp, body = self.put(url, None, headers) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def delete_image(self, image_id): + url = 'v1/images/%s' % image_id + resp, body = self.delete(url) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def list_images(self, detail=False, **kwargs): + """Return a list of all images filtered by input parameters. + + Available params: see http://developer.openstack.org/ + api-ref-image-v1.html#listImage-v1 + + Most parameters except the following are passed to the API without + any changes. + :param changes_since: The name is changed to changes-since + """ + url = 'v1/images' + + if detail: + url += '/detail' + + properties = kwargs.pop('properties', {}) + for key, value in six.iteritems(properties): + kwargs['property-%s' % key] = value + + if kwargs.get('changes_since'): + kwargs['changes-since'] = kwargs.pop('changes_since') + + if len(kwargs) > 0: + url += '?%s' % urllib.urlencode(kwargs) + + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def check_image(self, image_id): + """Check image metadata.""" + url = 'v1/images/%s' % image_id + resp, __ = self.head(url) + self.expected_success(200, resp.status) + body = self._image_meta_from_headers(resp) + return rest_client.ResponseBody(resp, body) + + def show_image(self, image_id): + """Get image details plus the image itself.""" + url = 'v1/images/%s' % image_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + return rest_client.ResponseBodyData(resp, body) + + def is_resource_deleted(self, id): + try: + if self.check_image(id)['status'] == 'deleted': + return True + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Returns the primary type of resource this client works with.""" + return 'image_meta' + + def list_image_members(self, image_id): + url = 'v1/images/%s/members' % image_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def list_shared_images(self, tenant_id): + """List shared images with the specified tenant""" + url = 'v1/shared-images/%s' % tenant_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def add_member(self, member_id, image_id, **kwargs): + """Add a member to an image. + + Available params: see http://developer.openstack.org/ + api-ref-image-v1.html#addMember-v1 + """ + url = 'v1/images/%s/members/%s' % (image_id, member_id) + body = json.dumps({'member': kwargs}) + resp, __ = self.put(url, body) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp) + + def delete_member(self, member_id, image_id): + url = 'v1/images/%s/members/%s' % (image_id, member_id) + resp, __ = self.delete(url) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp) diff --git a/ceilometer/tests/tempest/service/images/v2/__init__.py b/ceilometer/tests/tempest/service/images/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ceilometer/tests/tempest/service/images/v2/images_client.py b/ceilometer/tests/tempest/service/images/v2/images_client.py new file mode 100644 index 00000000..d0cbadc6 --- /dev/null +++ b/ceilometer/tests/tempest/service/images/v2/images_client.py @@ -0,0 +1,245 @@ +# 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. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + +from ceilometer.tests.tempest.service.images import glance_http + + +class ImagesClient(rest_client.RestClient): + + def __init__(self, auth_provider, catalog_type, region, **kwargs): + super(ImagesClient, self).__init__( + auth_provider, catalog_type, region, **kwargs) + self._http = None + self.dscv = kwargs.get("disable_ssl_certificate_validation") + self.ca_certs = kwargs.get("ca_certs") + + def _get_http(self): + return glance_http.HTTPClient(auth_provider=self.auth_provider, + filters=self.filters, + insecure=self.dscv, + ca_certs=self.ca_certs) + + @property + def http(self): + if self._http is None: + self._http = self._get_http() + return self._http + + def update_image(self, image_id, patch): + """Update an image. + + Available params: see http://developer.openstack.org/ + api-ref-image-v2.html#updateImage-v2 + """ + data = json.dumps(patch) + headers = {"Content-Type": "application/openstack-images-v2.0" + "-json-patch"} + resp, body = self.patch('v2/images/%s' % image_id, data, headers) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_image(self, **kwargs): + """Create an image. + + Available params: see http://developer.openstack.org/ + api-ref-image-v2.html#createImage-v2 + """ + data = json.dumps(kwargs) + resp, body = self.post('v2/images', data) + self.expected_success(201, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def deactivate_image(self, image_id): + url = 'v2/images/%s/actions/deactivate' % image_id + resp, body = self.post(url, None) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) + + def reactivate_image(self, image_id): + url = 'v2/images/%s/actions/reactivate' % image_id + resp, body = self.post(url, None) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) + + def delete_image(self, image_id): + url = 'v2/images/%s' % image_id + resp, _ = self.delete(url) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp) + + def list_images(self, params=None): + url = 'v2/images' + + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_image(self, image_id): + url = 'v2/images/%s' % image_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_image(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Returns the primary type of resource this client works with.""" + return 'image' + + def store_image_file(self, image_id, data): + url = 'v2/images/%s/file' % image_id + headers = {'Content-Type': 'application/octet-stream'} + resp, body = self.http.raw_request('PUT', url, headers=headers, + body=data) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) + + def show_image_file(self, image_id): + url = 'v2/images/%s/file' % image_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + return rest_client.ResponseBodyData(resp, body) + + def add_image_tag(self, image_id, tag): + url = 'v2/images/%s/tags/%s' % (image_id, tag) + resp, body = self.put(url, body=None) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) + + def delete_image_tag(self, image_id, tag): + url = 'v2/images/%s/tags/%s' % (image_id, tag) + resp, _ = self.delete(url) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp) + + def list_image_members(self, image_id): + url = 'v2/images/%s/members' % image_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_image_member(self, image_id, **kwargs): + """Create an image member. + + Available params: see http://developer.openstack.org/ + api-ref-image-v2.html#createImageMember-v2 + """ + url = 'v2/images/%s/members' % image_id + data = json.dumps(kwargs) + resp, body = self.post(url, data) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def update_image_member(self, image_id, member_id, **kwargs): + """Update an image member. + + Available params: see http://developer.openstack.org/ + api-ref-image-v2.html#updateImageMember-v2 + """ + url = 'v2/images/%s/members/%s' % (image_id, member_id) + data = json.dumps(kwargs) + resp, body = self.put(url, data) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_image_member(self, image_id, member_id): + url = 'v2/images/%s/members/%s' % (image_id, member_id) + resp, body = self.get(url) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, json.loads(body)) + + def delete_image_member(self, image_id, member_id): + url = 'v2/images/%s/members/%s' % (image_id, member_id) + resp, _ = self.delete(url) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp) + + def show_schema(self, schema): + url = 'v2/schemas/%s' % schema + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def list_resource_types(self): + url = '/v2/metadefs/resource_types' + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_namespace(self, **kwargs): + """Create a namespace. + + Available params: see http://developer.openstack.org/ + api-ref-image-v2.html#createNamespace-v2 + """ + data = json.dumps(kwargs) + resp, body = self.post('/v2/metadefs/namespaces', data) + self.expected_success(201, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_namespace(self, namespace): + url = '/v2/metadefs/namespaces/%s' % namespace + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def update_namespace(self, namespace, **kwargs): + """Update a namespace. + + Available params: see http://developer.openstack.org/ + api-ref-image-v2.html#updateNamespace-v2 + """ + # NOTE: On Glance API, we need to pass namespace on both URI + # and a request body. + params = {'namespace': namespace} + params.update(kwargs) + data = json.dumps(params) + url = '/v2/metadefs/namespaces/%s' % namespace + resp, body = self.put(url, body=data) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def delete_namespace(self, namespace): + url = '/v2/metadefs/namespaces/%s' % namespace + resp, _ = self.delete(url) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp)