Merge "Copy images_client from tempest"
diff --git a/ceilometer/tests/tempest/exceptions.py b/ceilometer/tests/tempest/exceptions.py
new file mode 100644
index 0000000..92f335f
--- /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 a322ad1..179f8a1 100644
--- a/ceilometer/tests/tempest/service/client.py
+++ b/ceilometer/tests/tempest/service/client.py
@@ -23,12 +23,14 @@
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 0000000..e69de29
--- /dev/null
+++ b/ceilometer/tests/tempest/service/images/__init__.py
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 0000000..19a9d98
--- /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'] = "<Token omitted>"
+ 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 0000000..e69de29
--- /dev/null
+++ b/ceilometer/tests/tempest/service/images/v1/__init__.py
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 0000000..e9ca637
--- /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 0000000..e69de29
--- /dev/null
+++ b/ceilometer/tests/tempest/service/images/v2/__init__.py
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 0000000..d0cbadc
--- /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)