python-glanceclient-2.9.1/0000775000175100017510000000000013232162126015514 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/0000775000175100017510000000000013232162126020144 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/common/0000775000175100017510000000000013232162126021434 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/common/https.py0000666000175100017510000002313013232161651023153 0ustar zuulzuul00000000000000# Copyright 2014 Red Hat, 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 socket import ssl import struct import OpenSSL import six # NOTE(jokke): simplified transition to py3, behaves like py2 xrange from six.moves import range try: from eventlet import patcher # Handle case where we are running in a monkey patched environment if patcher.is_monkey_patched('socket'): from eventlet.green.httplib import HTTPSConnection from eventlet.green.OpenSSL.SSL import GreenConnection as Connection else: raise ImportError except ImportError: from OpenSSL import SSL from six.moves import http_client HTTPSConnection = http_client.HTTPSConnection Connection = SSL.Connection from glanceclient import exc def verify_callback(host=None): """Provide wrapper for do_verify_callback. We use a partial around the 'real' verify_callback function so that we can stash the host value without holding a reference on the VerifiedHTTPSConnection. """ def wrapper(connection, x509, errnum, depth, preverify_ok, host=host): return do_verify_callback(connection, x509, errnum, depth, preverify_ok, host=host) return wrapper def do_verify_callback(connection, x509, errnum, depth, preverify_ok, host=None): """Verify the server's SSL certificate. This is a standalone function rather than a method to avoid issues around closing sockets if a reference is held on a VerifiedHTTPSConnection by the callback function. """ if x509.has_expired(): msg = "SSL Certificate expired on '%s'" % x509.get_notAfter() raise exc.SSLCertificateError(msg) if depth == 0 and preverify_ok: # We verify that the host matches against the last # certificate in the chain return host_matches_cert(host, x509) else: # Pass through OpenSSL's default result return preverify_ok def host_matches_cert(host, x509): """Verify the certificate identifies the host. Verify that the x509 certificate we have received from 'host' correctly identifies the server we are connecting to, ie that the certificate's Common Name or a Subject Alternative Name matches 'host'. """ def check_match(name): # Directly match the name if name == host: return True # Support single wildcard matching if name.startswith('*.') and host.find('.') > 0: if name[2:] == host.split('.', 1)[1]: return True common_name = x509.get_subject().commonName # First see if we can match the CN if check_match(common_name): return True # Also try Subject Alternative Names for a match san_list = None for i in range(x509.get_extension_count()): ext = x509.get_extension(i) if ext.get_short_name() == b'subjectAltName': san_list = str(ext) for san in ''.join(san_list.split()).split(','): if san.startswith('DNS:'): if check_match(san.split(':', 1)[1]): return True # Server certificate does not match host msg = ('Host "%s" does not match x509 certificate contents: ' 'CommonName "%s"' % (host, common_name)) if san_list is not None: msg = msg + ', subjectAltName "%s"' % san_list raise exc.SSLCertificateError(msg) def to_bytes(s): if isinstance(s, six.string_types): return six.b(s) else: return s 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 = Connection(*args, **kwargs) def __getattr__(self, name): return getattr(self.connection, name) def makefile(self, *args, **kwargs): return socket._fileobject(self.connection, *args, **kwargs) class VerifiedHTTPSConnection(HTTPSConnection): """Extended OpenSSL HTTPSConnection for enhanced SSL support. Note: Much of this functionality can eventually be replaced with native Python 3.3 code. """ # Restrict the set of client supported cipher suites CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\ 'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\ 'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS' def __init__(self, host, port=None, key_file=None, cert_file=None, cacert=None, timeout=None, insecure=False, ssl_compression=True): # List of exceptions reported by Python3 instead of # SSLConfigurationError if six.PY3: excp_lst = (TypeError, FileNotFoundError, ssl.SSLError) else: # NOTE(jamespage) # Accommodate changes in behaviour for pep-0467, introduced # in python 2.7.9. # https://github.com/python/peps/blob/master/pep-0476.txt excp_lst = (TypeError, IOError, ssl.SSLError) try: 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 # NOTE(flaper87): `is_verified` is needed for # requests' urllib3. If insecure is True then # the request is not `verified`, hence `not insecure` self.is_verified = not insecure self.ssl_compression = ssl_compression self.cacert = None if cacert is None else str(cacert) self.set_context() # ssl exceptions are reported in various form in Python 3 # so to be compatible, we report the same kind as under # Python2 except excp_lst as e: raise exc.SSLConfigurationError(str(e)) def set_context(self): """Set up the OpenSSL context.""" self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) self.context.set_cipher_list(self.CIPHERS) 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, verify_callback(host=self.host)) else: self.context.set_verify(OpenSSL.SSL.VERIFY_NONE, lambda *args: True) 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.cacert: try: self.context.load_verify_locations(to_bytes(self.cacert)) except Exception as e: msg = 'Unable to load CA from "%s" %s' % (self.cacert, e) raise exc.SSLConfigurationError(msg) else: self.context.set_default_verify_paths() def connect(self): """Connect to an SSL port using the OpenSSL library. This method also applies per-connection parameters to the connection. """ result = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM) if result: socket_family = result[0][0] if socket_family == socket.AF_INET6: sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) else: # If due to some reason the address lookup fails - we still connect # to IPv4 socket. This retains the older behavior. sock = socket.socket(socket.AF_INET, 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) self.sock.connect((self.host, self.port)) python-glanceclient-2.9.1/glanceclient/common/exceptions.py0000666000175100017510000000134113232161651024172 0ustar zuulzuul00000000000000# 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. # This is here for compatibility purposes. Once all known OpenStack clients # are updated to use glanceclient.exc, this file should be removed from glanceclient.exc import * # noqa python-glanceclient-2.9.1/glanceclient/common/http.py0000666000175100017510000003225713232161651023002 0ustar zuulzuul00000000000000# 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 copy import logging import socket from keystoneauth1 import adapter from keystoneauth1 import exceptions as ksa_exc import OpenSSL from oslo_utils import importutils from oslo_utils import netutils import requests import six try: import json except ImportError: import simplejson as json from oslo_utils import encodeutils from glanceclient.common import utils from glanceclient import exc osprofiler_web = importutils.try_import("osprofiler.web") LOG = logging.getLogger(__name__) USER_AGENT = 'python-glanceclient' CHUNKSIZE = 1024 * 64 # 64kB REQ_ID_HEADER = 'X-OpenStack-Request-ID' def encode_headers(headers): """Encodes headers. Note: This should be used right before sending anything out. :param headers: Headers to encode :returns: Dictionary with encoded headers' names and values """ return dict((encodeutils.safe_encode(h), encodeutils.safe_encode(v)) for h, v in headers.items() if v is not None) class _BaseHTTPClient(object): @staticmethod def _chunk_body(body): chunk = body while chunk: chunk = body.read(CHUNKSIZE) if not chunk: break yield chunk def _set_common_request_kwargs(self, headers, kwargs): """Handle the common parameters used to send the request.""" # Default Content-Type is octet-stream content_type = headers.get('Content-Type', 'application/octet-stream') # NOTE(jamielennox): remove this later. Managers should pass json= if # they want to send json data. data = kwargs.pop("data", None) if data is not None and not isinstance(data, six.string_types): try: data = json.dumps(data) content_type = 'application/json' except TypeError: # Here we assume it's # a file-like object # and we'll chunk it data = self._chunk_body(data) headers['Content-Type'] = content_type kwargs['stream'] = content_type == 'application/octet-stream' return data def _handle_response(self, resp): if not resp.ok: LOG.debug("Request returned failure status %s.", resp.status_code) raise exc.from_response(resp, resp.content) elif (resp.status_code == requests.codes.MULTIPLE_CHOICES and resp.request.path_url != '/versions'): # NOTE(flaper87): Eventually, we'll remove the check on `versions` # which is a bug (1491350) on the server. raise exc.from_response(resp) content_type = resp.headers.get('Content-Type') # Read body into string if it isn't obviously image data if content_type == 'application/octet-stream': # Do not read all response in memory when downloading an image. body_iter = _close_after_stream(resp, CHUNKSIZE) else: content = resp.text if content_type and content_type.startswith('application/json'): # Let's use requests json method, it should take care of # response encoding body_iter = resp.json() else: body_iter = six.StringIO(content) try: body_iter = json.loads(''.join([c for c in body_iter])) except ValueError: body_iter = None return resp, body_iter class HTTPClient(_BaseHTTPClient): def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.identity_headers = kwargs.get('identity_headers') self.auth_token = kwargs.get('token') self.language_header = kwargs.get('language_header') self.global_request_id = kwargs.get('global_request_id') if self.identity_headers: self.auth_token = self.identity_headers.pop('X-Auth-Token', self.auth_token) self.session = requests.Session() self.session.headers["User-Agent"] = USER_AGENT if self.language_header: self.session.headers["Accept-Language"] = self.language_header self.timeout = float(kwargs.get('timeout', 600)) if self.endpoint.startswith("https"): if kwargs.get('insecure', False) is True: self.session.verify = False else: if kwargs.get('cacert', None) is not '': self.session.verify = kwargs.get('cacert', True) self.session.cert = (kwargs.get('cert_file'), kwargs.get('key_file')) @staticmethod def parse_endpoint(endpoint): return netutils.urlsplit(endpoint) def log_curl_request(self, method, url, headers, data, kwargs): curl = ['curl -g -i -X %s' % method] headers = copy.deepcopy(headers) headers.update(self.session.headers) for (key, value) in headers.items(): header = '-H \'%s: %s\'' % utils.safe_header(key, value) curl.append(header) if not self.session.verify: curl.append('-k') else: if isinstance(self.session.verify, six.string_types): curl.append(' --cacert %s' % self.session.verify) if self.session.cert: curl.append(' --cert %s --key %s' % self.session.cert) if data and isinstance(data, six.string_types): curl.append('-d \'%s\'' % data) curl.append(url) msg = ' '.join([encodeutils.safe_decode(item, errors='ignore') for item in curl]) LOG.debug(msg) @staticmethod def log_http_response(resp): status = (resp.raw.version / 10.0, resp.status_code, resp.reason) dump = ['\nHTTP/%.1f %s %s' % status] headers = resp.headers.items() dump.extend(['%s: %s' % utils.safe_header(k, v) for k, v in headers]) dump.append('') content_type = resp.headers.get('Content-Type') if content_type != 'application/octet-stream': dump.extend([resp.text, '']) LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore') for x in dump])) def _request(self, method, url, **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 headers = copy.deepcopy(kwargs.pop('headers', {})) if self.identity_headers: for k, v in self.identity_headers.items(): headers.setdefault(k, v) data = self._set_common_request_kwargs(headers, kwargs) # add identity header to the request if not headers.get('X-Auth-Token'): headers['X-Auth-Token'] = self.auth_token if self.global_request_id: headers.setdefault(REQ_ID_HEADER, self.global_request_id) if osprofiler_web: headers.update(osprofiler_web.get_trace_id_headers()) # Note(flaper87): Before letting headers / url fly, # they should be encoded otherwise httplib will # complain. headers = encode_headers(headers) if self.endpoint.endswith("/") or url.startswith("/"): conn_url = "%s%s" % (self.endpoint, url) else: conn_url = "%s/%s" % (self.endpoint, url) self.log_curl_request(method, conn_url, headers, data, kwargs) try: resp = self.session.request(method, conn_url, data=data, headers=headers, **kwargs) except requests.exceptions.Timeout as e: message = ("Error communicating with %(url)s: %(e)s" % dict(url=conn_url, e=e)) raise exc.InvalidEndpoint(message=message) except requests.exceptions.ConnectionError as e: message = ("Error finding address for %(url)s: %(e)s" % dict(url=conn_url, e=e)) raise exc.CommunicationError(message=message) except socket.gaierror as e: message = "Error finding address for %s: %s" % ( self.endpoint_hostname, e) raise exc.InvalidEndpoint(message=message) except (socket.error, socket.timeout, IOError) as e: endpoint = self.endpoint message = ("Error communicating with %(endpoint)s %(e)s" % {'endpoint': endpoint, 'e': e}) raise exc.CommunicationError(message=message) except OpenSSL.SSL.Error as e: message = ("SSL Error communicating with %(url)s: %(e)s" % {'url': conn_url, 'e': e}) raise exc.CommunicationError(message=message) # log request-id for each api call request_id = resp.headers.get('x-openstack-request-id') if request_id: LOG.debug('%(method)s call to image for ' '%(url)s used request id ' '%(response_request_id)s', {'method': resp.request.method, 'url': resp.url, 'response_request_id': request_id}) resp, body_iter = self._handle_response(resp) self.log_http_response(resp) return resp, body_iter def head(self, url, **kwargs): return self._request('HEAD', url, **kwargs) def get(self, url, **kwargs): return self._request('GET', url, **kwargs) def post(self, url, **kwargs): return self._request('POST', url, **kwargs) def put(self, url, **kwargs): return self._request('PUT', url, **kwargs) def patch(self, url, **kwargs): return self._request('PATCH', url, **kwargs) def delete(self, url, **kwargs): return self._request('DELETE', url, **kwargs) def _close_after_stream(response, chunk_size): """Iterate over the content and ensure the response is closed after.""" # Yield each chunk in the response body for chunk in response.iter_content(chunk_size=chunk_size): yield chunk # Once we're done streaming the body, ensure everything is closed. # This will return the connection to the HTTPConnectionPool in urllib3 # and ideally reduce the number of HTTPConnectionPool full warnings. response.close() class SessionClient(adapter.Adapter, _BaseHTTPClient): def __init__(self, session, **kwargs): kwargs.setdefault('user_agent', USER_AGENT) kwargs.setdefault('service_type', 'image') self.global_request_id = kwargs.pop('global_request_id', None) super(SessionClient, self).__init__(session, **kwargs) def request(self, url, method, **kwargs): headers = kwargs.pop('headers', {}) if self.global_request_id: headers.setdefault(REQ_ID_HEADER, self.global_request_id) kwargs['raise_exc'] = False data = self._set_common_request_kwargs(headers, kwargs) try: # NOTE(pumaranikar): To avoid bug #1641239, no modification of # headers should be allowed after encode_headers() is called. resp = super(SessionClient, self).request(url, method, headers=encode_headers(headers), data=data, **kwargs) except ksa_exc.ConnectTimeout as e: conn_url = self.get_endpoint(auth=kwargs.get('auth')) conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/')) message = ("Error communicating with %(url)s %(e)s" % dict(url=conn_url, e=e)) raise exc.InvalidEndpoint(message=message) except ksa_exc.ConnectFailure as e: conn_url = self.get_endpoint(auth=kwargs.get('auth')) conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/')) message = ("Error finding address for %(url)s: %(e)s" % dict(url=conn_url, e=e)) raise exc.CommunicationError(message=message) return self._handle_response(resp) def get_http_client(endpoint=None, session=None, **kwargs): if session: return SessionClient(session, **kwargs) elif endpoint: return HTTPClient(endpoint, **kwargs) else: raise AttributeError('Constructing a client must contain either an ' 'endpoint or a session') python-glanceclient-2.9.1/glanceclient/common/__init__.py0000666000175100017510000000000013232161651023537 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/common/utils.py0000666000175100017510000004144313232161651023160 0ustar zuulzuul00000000000000# 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. from __future__ import print_function import errno import functools import hashlib import json import os import re import six.moves.urllib.parse as urlparse import sys import threading import uuid import six if os.name == 'nt': import msvcrt else: msvcrt = None from oslo_utils import encodeutils from oslo_utils import strutils import prettytable import wrapt from glanceclient._i18n import _ from glanceclient import exc _memoized_property_lock = threading.Lock() SENSITIVE_HEADERS = ('X-Auth-Token', ) REQUIRED_FIELDS_ON_DATA = ('disk_format', 'container_format') # Decorator for cli-args def arg(*args, **kwargs): def _decorator(func): # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) return func return _decorator def on_data_require_fields(data_fields, required=REQUIRED_FIELDS_ON_DATA): """Decorator to check commands' validity This decorator checks that required fields are present when image data has been supplied via command line arguments or via stdin On error throws CommandError exception with meaningful message. :param data_fields: Which fields' presence imply image data :type data_fields: iter :param required: Required fields :type required: iter :return: function decorator """ def args_decorator(func): def prepare_fields(fields): args = ('--' + x.replace('_', '-') for x in fields) return ', '.join(args) @functools.wraps(func) def func_wrapper(gc, args): # Set of arguments with data fields = set(a[0] for a in vars(args).items() if a[1]) # Fields the conditional requirements depend on present = fields.intersection(data_fields) # How many conditional requirements are missing missing = set(required) - fields # We use get_data_file to check if data is provided in stdin if (present or get_data_file(args)) and missing: msg = (_("error: Must provide %(req)s when using %(opt)s.") % {'req': prepare_fields(missing), 'opt': prepare_fields(present) or 'stdin'}) raise exc.CommandError(msg) return func(gc, args) return func_wrapper return args_decorator def schema_args(schema_getter, omit=None): omit = omit or [] typemap = { 'string': encodeutils.safe_decode, 'integer': int, 'boolean': strutils.bool_from_string, 'array': list } def _decorator(func): schema = schema_getter() if schema is None: param = '' kwargs = { 'help': ("Please run with connection parameters set to " "retrieve the schema for generating help for this " "command") } func.__dict__.setdefault('arguments', []).insert(0, ((param, ), kwargs)) else: properties = schema.get('properties', {}) for name, property in properties.items(): if name in omit: continue param = '--' + name.replace('_', '-') kwargs = {} type_str = property.get('type', 'string') if isinstance(type_str, list): # NOTE(flaper87): This means the server has # returned something like `['null', 'string']`, # therefore we use the first non-`null` type as # the valid type. for t in type_str: if t != 'null': type_str = t break if type_str == 'array': items = property.get('items') kwargs['type'] = typemap.get(items.get('type')) kwargs['nargs'] = '+' else: kwargs['type'] = typemap.get(type_str) if type_str == 'boolean': kwargs['metavar'] = '[True|False]' else: kwargs['metavar'] = '<%s>' % name.upper() description = property.get('description', "") if 'enum' in property: if len(description): description += " " # NOTE(flaper87): Make sure all values are `str/unicode` # for the `join` to succeed. Enum types can also be `None` # therefore, join's call would fail without the following # list comprehension vals = [six.text_type(val) for val in property.get('enum')] description += ('Valid values: ' + ', '.join(vals)) kwargs['help'] = description func.__dict__.setdefault('arguments', []).insert(0, ((param, ), kwargs)) return func return _decorator def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) def print_list(objs, fields, formatters=None, field_settings=None): formatters = formatters or {} field_settings = field_settings or {} pt = prettytable.PrettyTable([f for f in fields], caching=False) pt.align = 'l' for o in objs: row = [] for field in fields: if field in field_settings: for setting, value in field_settings[field].items(): setting_dict = getattr(pt, setting) setting_dict[field] = value if field in formatters: row.append(formatters[field](o)) else: field_name = field.lower().replace(' ', '_') data = getattr(o, field_name, None) or '' row.append(data) pt.add_row(row) print(encodeutils.safe_decode(pt.get_string())) def print_dict(d, max_column_width=80): pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) pt.align = 'l' pt.max_width = max_column_width for k, v in d.items(): if isinstance(v, (dict, list)): v = json.dumps(v) pt.add_row([k, v]) print(encodeutils.safe_decode(pt.get_string(sortby='Property'))) def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" # first try to get entity as integer id try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) except exc.NotFound: pass # now try to get entity as uuid try: # This must be unicode for Python 3 compatibility. # If you pass a bytestring to uuid.UUID, you will get a TypeError uuid.UUID(encodeutils.safe_decode(name_or_id)) return manager.get(name_or_id) except (ValueError, exc.NotFound): pass # finally try to find entity by name matches = list(manager.list(filters={'name': name_or_id})) num_matches = len(matches) if num_matches == 0: msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exc.CommandError(msg) elif num_matches > 1: msg = ("Multiple %s matches found for '%s', use an ID to be more" " specific." % (manager.resource_class.__name__.lower(), name_or_id)) raise exc.CommandError(msg) else: return matches[0] def env(*vars, **kwargs): """Search for the first defined of possibly many env vars. Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def exit(msg='', exit_code=1): if msg: print_err(msg) sys.exit(exit_code) def print_err(msg): print(encodeutils.safe_decode(msg), file=sys.stderr) def save_image(data, path): """Save an image to the specified path. :param data: binary data of the image :param path: path to save the image to """ if path is None: # NOTE(kragniz): for py3 compatibility: sys.stdout.buffer is only # present on py3, otherwise fall back to sys.stdout image = getattr(sys.stdout, 'buffer', sys.stdout) else: image = open(path, 'wb') try: for chunk in data: image.write(chunk) finally: if path is not None: image.close() def make_size_human_readable(size): suffix = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'] base = 1024.0 index = 0 if size is None: size = 0 while size >= base: index = index + 1 size = size / base padded = '%.1f' % size stripped = padded.rstrip('0').rstrip('.') return '%s%s' % (stripped, suffix[index]) def get_file_size(file_obj): """Analyze file-like object and attempt to determine its size. :param file_obj: file-like object. :retval: The file's size or None if it cannot be determined. """ if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and (six.PY2 or six.PY3 and file_obj.seekable())): try: curr = file_obj.tell() file_obj.seek(0, os.SEEK_END) size = file_obj.tell() file_obj.seek(curr) return size except IOError as e: if e.errno == errno.ESPIPE: # Illegal seek. This means the file object # is a pipe (e.g. the user is trying # to pipe image data to the client, # echo testdata | bin/glance add blah...), or # that file object is empty, or that a file-like # object which doesn't support 'seek/tell' has # been supplied. return else: raise def get_data_file(args): if args.file: return open(args.file, 'rb') else: # distinguish cases where: # (1) stdin is not valid (as in cron jobs): # glance ... <&- # (2) image data is provided through standard input: # glance ... < /tmp/file or cat /tmp/file | glance ... # (3) no image data provided: # glance ... try: os.fstat(0) except OSError: # (1) stdin is not valid (closed...) return None if not sys.stdin.isatty(): # (2) image data is provided through standard input image = sys.stdin if hasattr(sys.stdin, 'buffer'): image = sys.stdin.buffer if msvcrt: msvcrt.setmode(image.fileno(), os.O_BINARY) return image else: # (3) no image data provided return None def strip_version(endpoint): """Strip version from the last component of endpoint if present.""" # NOTE(flaper87): This shouldn't be necessary if # we make endpoint the first argument. However, we # can't do that just yet because we need to keep # backwards compatibility. if not isinstance(endpoint, six.string_types): raise ValueError("Expected endpoint") version = None # Get rid of trailing '/' if present endpoint = endpoint.rstrip('/') url_parts = urlparse.urlparse(endpoint) (scheme, netloc, path, __, __, __) = url_parts path = path.lstrip('/') # regex to match 'v1' or 'v2.0' etc if re.match('v\d+\.?\d*', path): version = float(path.lstrip('v')) endpoint = scheme + '://' + netloc return endpoint, version def print_image(image_obj, human_readable=False, max_col_width=None): ignore = ['self', 'access', 'file', 'schema'] image = dict([item for item in image_obj.items() if item[0] not in ignore]) if human_readable: image['size'] = make_size_human_readable(image['size']) if str(max_col_width).isdigit(): print_dict(image, max_column_width=max_col_width) else: print_dict(image) def integrity_iter(iter, checksum): """Check image data integrity. :raises: IOError """ md5sum = hashlib.md5() for chunk in iter: yield chunk if isinstance(chunk, six.string_types): chunk = six.b(chunk) md5sum.update(chunk) md5sum = md5sum.hexdigest() if md5sum != checksum: raise IOError(errno.EPIPE, 'Corrupt image download. Checksum was %s expected %s' % (md5sum, checksum)) def memoized_property(fn): attr_name = '_lazy_once_' + fn.__name__ @property def _memoized_property(self): if hasattr(self, attr_name): return getattr(self, attr_name) else: with _memoized_property_lock: if not hasattr(self, attr_name): setattr(self, attr_name, fn(self)) return getattr(self, attr_name) return _memoized_property def safe_header(name, value): if value is not None and name in SENSITIVE_HEADERS: h = hashlib.sha1(value) d = h.hexdigest() return name, "{SHA1}%s" % d else: return name, value def endpoint_version_from_url(endpoint, default_version=None): if endpoint: endpoint, version = strip_version(endpoint) return endpoint, version or default_version else: return None, default_version def debug_enabled(argv): if bool(env('GLANCECLIENT_DEBUG')) is True: return True if '--debug' in argv or '-d' in argv: return True return False class IterableWithLength(object): def __init__(self, iterable, length): self.iterable = iterable self.length = length def __iter__(self): try: for chunk in self.iterable: yield chunk finally: self.iterable.close() def next(self): return next(self.iterable) # In Python 3, __next__() has replaced next(). __next__ = next def __len__(self): return self.length class RequestIdProxy(wrapt.ObjectProxy): def __init__(self, wrapped): # `wrapped` is a tuple: (original_obj, response_obj) super(RequestIdProxy, self).__init__(wrapped[0]) self._self_wrapped = wrapped[0] req_id = _extract_request_id(wrapped[1]) self._self_request_ids = [req_id] @property def request_ids(self): return self._self_request_ids @property def wrapped(self): return self._self_wrapped # Overriden next method to act as iterator def next(self): return next(self._self_wrapped) # In Python 3, __next__() has replaced next(). __next__ = next class GeneratorProxy(wrapt.ObjectProxy): def __init__(self, wrapped): super(GeneratorProxy, self).__init__(wrapped) self._self_wrapped = wrapped self._self_request_ids = [] def _set_request_ids(self, resp): if self._self_request_ids == []: req_id = _extract_request_id(resp) self._self_request_ids = [req_id] def _next(self): obj, resp = next(self._self_wrapped) self._set_request_ids(resp) return obj # Override generator's next method to add # request id on each iteration def next(self): return self._next() # For Python 3 compatibility def __next__(self): return self._next() def __iter__(self): return self @property def request_ids(self): return self._self_request_ids @property def wrapped(self): return self._self_wrapped def add_req_id_to_object(): @wrapt.decorator def inner(wrapped, instance, args, kwargs): return RequestIdProxy(wrapped(*args, **kwargs)) return inner def add_req_id_to_generator(): @wrapt.decorator def inner(wrapped, instance, args, kwargs): return GeneratorProxy(wrapped(*args, **kwargs)) return inner def _extract_request_id(resp): # TODO(rsjethani): Do we need more checks here? return resp.headers.get('x-openstack-request-id') python-glanceclient-2.9.1/glanceclient/common/progressbar.py0000666000175100017510000000636213232161651024352 0ustar zuulzuul00000000000000# Copyright 2013 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 sys import six class _ProgressBarBase(object): """A progress bar provider for a wrapped obect. Base abstract class used by specific class wrapper to show a progress bar when the wrapped object are consumed. :param wrapped: Object to wrap that hold data to be consumed. :param totalsize: The total size of the data in the wrapped object. :note: The progress will be displayed only if sys.stdout is a tty. """ def __init__(self, wrapped, totalsize): self._wrapped = wrapped self._totalsize = float(totalsize) self._show_progress = sys.stdout.isatty() and self._totalsize != 0 self._percent = 0 def _display_progress_bar(self, size_read): if self._show_progress: self._percent += size_read / self._totalsize # Output something like this: [==========> ] 49% sys.stdout.write('\r[{0:<30}] {1:.0%}'.format( '=' * int(round(self._percent * 29)) + '>', self._percent )) sys.stdout.flush() def __getattr__(self, attr): # Forward other attribute access to the wrapped object. return getattr(self._wrapped, attr) class VerboseFileWrapper(_ProgressBarBase): """A file wrapper with a progress bar. The file wrapper shows and advances a progress bar whenever the wrapped file's read method is called. """ def read(self, *args, **kwargs): data = self._wrapped.read(*args, **kwargs) if data: self._display_progress_bar(len(data)) else: if self._show_progress: # Break to a new line from the progress bar for incoming # output. sys.stdout.write('\n') return data class VerboseIteratorWrapper(_ProgressBarBase): """An iterator wrapper with a progress bar. The iterator wrapper shows and advances a progress bar whenever the wrapped data is consumed from the iterator. :note: Use only with iterator that yield strings. """ def __iter__(self): return self def next(self): try: data = six.next(self._wrapped) # NOTE(mouad): Assuming that data is a string b/c otherwise calling # len function will not make any sense. self._display_progress_bar(len(data)) return data except StopIteration: if self._show_progress: # Break to a new line from the progress bar for incoming # output. sys.stdout.write('\n') raise # In Python 3, __next__() has replaced next(). __next__ = next python-glanceclient-2.9.1/glanceclient/_i18n.py0000666000175100017510000000145613232161651021446 0ustar zuulzuul00000000000000# 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 oslo_i18n as i18n _translators = i18n.TranslatorFactory(domain='glanceclient') # The primary translation function using the well-known name "_" _ = _translators.primary python-glanceclient-2.9.1/glanceclient/__init__.py0000666000175100017510000000215513232161651022264 0ustar zuulzuul00000000000000# Copyright 2012 OpenStack Foundation # # 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. # NOTE(bcwaldon): this try/except block is needed to run setup.py due to # its need to import local code before installing required dependencies try: import glanceclient.client Client = glanceclient.client.Client except ImportError: import warnings warnings.warn("Could not import glanceclient.client", ImportWarning) import pbr.version version_info = pbr.version.VersionInfo('python-glanceclient') try: __version__ = version_info.version_string() except AttributeError: __version__ = None python-glanceclient-2.9.1/glanceclient/shell.py0000666000175100017510000007237313232161665021652 0ustar zuulzuul00000000000000# 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. """ Command-line interface to the OpenStack Images API. """ from __future__ import print_function import argparse import copy import getpass import hashlib import json import logging import os import sys import traceback from oslo_utils import encodeutils from oslo_utils import importutils import six import six.moves.urllib.parse as urlparse import glanceclient from glanceclient._i18n import _ from glanceclient.common import utils from glanceclient import exc from keystoneauth1 import discover from keystoneauth1 import exceptions as ks_exc from keystoneauth1.identity import v2 as v2_auth from keystoneauth1.identity import v3 as v3_auth from keystoneauth1 import loading osprofiler_profiler = importutils.try_import("osprofiler.profiler") SUPPORTED_VERSIONS = [1, 2] class OpenStackImagesShell(object): def _append_global_identity_args(self, parser, argv): # register common identity args parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL')) parser.set_defaults(os_project_name=utils.env( 'OS_PROJECT_NAME', 'OS_TENANT_NAME')) parser.set_defaults(os_project_id=utils.env( 'OS_PROJECT_ID', 'OS_TENANT_ID')) parser.add_argument('--key-file', dest='os_key', help='DEPRECATED! Use --os-key.') parser.add_argument('--ca-file', dest='os_cacert', help='DEPRECATED! Use --os-cacert.') parser.add_argument('--cert-file', dest='os_cert', help='DEPRECATED! Use --os-cert.') parser.add_argument('--os_tenant_id', help=argparse.SUPPRESS) parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) parser.add_argument('--os-region-name', default=utils.env('OS_REGION_NAME'), help='Defaults to env[OS_REGION_NAME].') parser.add_argument('--os_region_name', help=argparse.SUPPRESS) parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN].') parser.add_argument('--os_auth_token', help=argparse.SUPPRESS) parser.add_argument('--os-service-type', default=utils.env('OS_SERVICE_TYPE'), help='Defaults to env[OS_SERVICE_TYPE].') parser.add_argument('--os_service_type', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint-type', default=utils.env('OS_ENDPOINT_TYPE'), help='Defaults to env[OS_ENDPOINT_TYPE].') parser.add_argument('--os_endpoint_type', help=argparse.SUPPRESS) loading.register_session_argparse_arguments(parser) # Peek into argv to see if os-auth-token (or the deprecated # os_auth_token) or the new os-token or the environment variable # OS_AUTH_TOKEN were given. In which case, the token auth plugin is # what the user wants. Else, we'll default to password. default_auth_plugin = 'password' token_opts = ['os-token', 'os-auth-token', 'os_auth-token'] if argv and any(i in token_opts for i in argv): default_auth_plugin = 'token' loading.register_auth_argparse_arguments( parser, argv, default=default_auth_plugin) def get_base_parser(self, argv): parser = argparse.ArgumentParser( prog='glance', description=__doc__.strip(), epilog='See "glance help COMMAND" ' 'for help on a specific command.', add_help=False, formatter_class=HelpFormatter, ) # Global arguments parser.add_argument('-h', '--help', action='store_true', help=argparse.SUPPRESS, ) parser.add_argument('--version', action='version', version=glanceclient.__version__) parser.add_argument('-d', '--debug', default=bool(utils.env('GLANCECLIENT_DEBUG')), action='store_true', help='Defaults to env[GLANCECLIENT_DEBUG].') parser.add_argument('-v', '--verbose', default=False, action="store_true", help="Print more verbose output.") parser.add_argument('--get-schema', default=False, action="store_true", dest='get_schema', help='Ignores cached copy and forces retrieval ' 'of schema that generates portions of the ' 'help text. Ignored with API version 1.') parser.add_argument('-f', '--force', dest='force', default=False, action='store_true', help='Prevent select actions from requesting ' 'user confirmation.') parser.add_argument('--os-image-url', default=utils.env('OS_IMAGE_URL'), help=('Defaults to env[OS_IMAGE_URL]. ' 'If the provided image url contains ' 'a version number and ' '`--os-image-api-version` is omitted ' 'the version of the URL will be picked as ' 'the image api version to use.')) parser.add_argument('--os_image_url', help=argparse.SUPPRESS) parser.add_argument('--os-image-api-version', default=utils.env('OS_IMAGE_API_VERSION', default=None), help='Defaults to env[OS_IMAGE_API_VERSION] or 2.') parser.add_argument('--os_image_api_version', help=argparse.SUPPRESS) if osprofiler_profiler: parser.add_argument('--profile', metavar='HMAC_KEY', default=utils.env('OS_PROFILE'), help='HMAC key to use for encrypting context ' 'data for performance profiling of operation. ' 'This key should be the value of HMAC key ' 'configured in osprofiler middleware in ' 'glance, it is specified in glance ' 'configuration file at ' '/etc/glance/glance-api.conf and ' '/etc/glance/glance-registry.conf. Without ' 'key the profiling will not be triggered even ' 'if osprofiler is enabled on server side. ' 'Defaults to env[OS_PROFILE].') self._append_global_identity_args(parser, argv) return parser def get_subcommand_parser(self, version, argv=None): parser = self.get_base_parser(argv) self.subcommands = {} subparsers = parser.add_subparsers(metavar='') submodule = importutils.import_versioned_module('glanceclient', version, 'shell') self._find_actions(subparsers, submodule) self._find_actions(subparsers, self) self._add_bash_completion_subparser(subparsers) return parser def _find_actions(self, subparsers, actions_module): for attr in (a for a in dir(actions_module) if a.startswith('do_')): # Replace underscores with hyphens in the commands # displayed to the user command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' help = desc.strip().split('\n')[0] arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, help=help, description=desc, add_help=False, formatter_class=HelpFormatter ) subparser.add_argument('-h', '--help', action='help', help=argparse.SUPPRESS, ) self.subcommands[command] = subparser for (args, kwargs) in arguments: subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def _add_bash_completion_subparser(self, subparsers): subparser = subparsers.add_parser('bash_completion', add_help=False, formatter_class=HelpFormatter) self.subcommands['bash_completion'] = subparser subparser.set_defaults(func=self.do_bash_completion) def _get_image_url(self, args): """Translate the available url-related options into a single string. Return the endpoint that should be used to talk to Glance if a clear decision can be made. Otherwise, return None. """ if args.os_image_url: return args.os_image_url else: return None def _discover_auth_versions(self, session, auth_url): # discover the API versions the server is supporting base on the # given URL v2_auth_url = None v3_auth_url = None try: ks_discover = discover.Discover(session=session, url=auth_url) v2_auth_url = ks_discover.url_for('2.0') v3_auth_url = ks_discover.url_for('3.0') except ks_exc.ClientException as e: # Identity service may not support discover API version. # Lets trying to figure out the API version from the original URL. url_parts = urlparse.urlparse(auth_url) (scheme, netloc, path, params, query, fragment) = url_parts path = path.lower() if path.startswith('/v3'): v3_auth_url = auth_url elif path.startswith('/v2'): v2_auth_url = auth_url else: # not enough information to determine the auth version msg = ('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url. Identity service may not support API ' 'version discovery. Please provide a versioned ' 'auth_url instead. error=%s') % (e) raise exc.CommandError(msg) return (v2_auth_url, v3_auth_url) def _get_keystone_auth_plugin(self, ks_session, **kwargs): # discover the supported keystone versions using the given auth url auth_url = kwargs.pop('auth_url', None) (v2_auth_url, v3_auth_url) = self._discover_auth_versions( session=ks_session, auth_url=auth_url) # Determine which authentication plugin to use. First inspect the # auth_url to see the supported version. If both v3 and v2 are # supported, then use the highest version if possible. user_id = kwargs.pop('user_id', None) username = kwargs.pop('username', None) password = kwargs.pop('password', None) user_domain_name = kwargs.pop('user_domain_name', None) user_domain_id = kwargs.pop('user_domain_id', None) # project and tenant can be used interchangeably project_id = (kwargs.pop('project_id', None) or kwargs.pop('tenant_id', None)) project_name = (kwargs.pop('project_name', None) or kwargs.pop('tenant_name', None)) project_domain_id = kwargs.pop('project_domain_id', None) project_domain_name = kwargs.pop('project_domain_name', None) auth = None use_domain = (user_domain_id or user_domain_name or project_domain_id or project_domain_name) use_v3 = v3_auth_url and (use_domain or (not v2_auth_url)) use_v2 = v2_auth_url and not use_domain if use_v3: auth = v3_auth.Password( v3_auth_url, user_id=user_id, username=username, password=password, user_domain_id=user_domain_id, user_domain_name=user_domain_name, project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) elif use_v2: auth = v2_auth.Password( v2_auth_url, username, password, tenant_id=project_id, tenant_name=project_name) else: # if we get here it means domain information is provided # (caller meant to use Keystone V3) but the auth url is # actually Keystone V2. Obviously we can't authenticate a V3 # user using V2. exc.CommandError("Credential and auth_url mismatch. The given " "auth_url is using Keystone V2 endpoint, which " "may not able to handle Keystone V3 credentials. " "Please provide a correct Keystone V3 auth_url.") return auth def _get_kwargs_to_create_auth_plugin(self, args): if not args.os_username: raise exc.CommandError( _("You must provide a username via" " either --os-username or " "env[OS_USERNAME]")) if not args.os_password: # No password, If we've got a tty, try prompting for it if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): # Check for Ctl-D try: args.os_password = getpass.getpass('OS Password: ') except EOFError: pass # No password because we didn't have a tty or the # user Ctl-D when prompted. if not args.os_password: raise exc.CommandError( _("You must provide a password via " "either --os-password, " "env[OS_PASSWORD], " "or prompted response")) # Validate password flow auth os_project_name = getattr( args, 'os_project_name', getattr(args, 'os_tenant_name', None)) os_project_id = getattr( args, 'os_project_id', getattr(args, 'os_tenant_id', None)) if not any([os_project_name, os_project_id]): # tenant is deprecated in Keystone v3. Use the latest # terminology instead. raise exc.CommandError( _("You must provide a project_id or project_name (" "with project_domain_name or project_domain_id) " "via " " --os-project-id (env[OS_PROJECT_ID])" " --os-project-name (env[OS_PROJECT_NAME])," " --os-project-domain-id " "(env[OS_PROJECT_DOMAIN_ID])" " --os-project-domain-name " "(env[OS_PROJECT_DOMAIN_NAME])")) if not args.os_auth_url: raise exc.CommandError( _("You must provide an auth url via" " either --os-auth-url or " "via env[OS_AUTH_URL]")) kwargs = { 'auth_url': args.os_auth_url, 'username': args.os_username, 'user_id': args.os_user_id, 'user_domain_id': args.os_user_domain_id, 'user_domain_name': args.os_user_domain_name, 'password': args.os_password, 'tenant_name': args.os_tenant_name, 'tenant_id': args.os_tenant_id, 'project_name': args.os_project_name, 'project_id': args.os_project_id, 'project_domain_name': args.os_project_domain_name, 'project_domain_id': args.os_project_domain_id, } return kwargs def _get_versioned_client(self, api_version, args): endpoint = self._get_image_url(args) auth_token = args.os_auth_token if endpoint and auth_token: kwargs = { 'token': auth_token, 'insecure': args.insecure, 'timeout': args.timeout, 'cacert': args.os_cacert, 'cert': args.os_cert, 'key': args.os_key, } else: ks_session = loading.load_session_from_argparse_arguments(args) auth_plugin_kwargs = self._get_kwargs_to_create_auth_plugin(args) ks_session.auth = self._get_keystone_auth_plugin( ks_session=ks_session, **auth_plugin_kwargs) kwargs = {'session': ks_session} if endpoint is None: endpoint_type = args.os_endpoint_type or 'public' service_type = args.os_service_type or 'image' endpoint = ks_session.get_endpoint( service_type=service_type, interface=endpoint_type, region_name=args.os_region_name) return glanceclient.Client(api_version, endpoint, **kwargs) def _cache_schemas(self, options, client, home_dir='~/.glanceclient'): homedir = os.path.expanduser(home_dir) path_prefix = homedir if options.os_auth_url: hash_host = hashlib.sha1(options.os_auth_url.encode('utf-8')) path_prefix = os.path.join(path_prefix, hash_host.hexdigest()) if not os.path.exists(path_prefix): try: os.makedirs(path_prefix) except OSError as e: # This avoids glanceclient to crash if it can't write to # ~/.glanceclient, which may happen on some env (for me, # it happens in Jenkins, as glanceclient can't write to # /var/lib/jenkins). msg = '%s' % e print(encodeutils.safe_decode(msg), file=sys.stderr) resources = ['image', 'metadefs/namespace', 'metadefs/resource_type'] schema_file_paths = [os.path.join(path_prefix, x + '_schema.json') for x in ['image', 'namespace', 'resource_type']] failed_download_schema = 0 for resource, schema_file_path in zip(resources, schema_file_paths): if (not os.path.exists(schema_file_path)) or options.get_schema: try: schema = client.schemas.get(resource) with open(schema_file_path, 'w') as f: f.write(json.dumps(schema.raw())) except exc.Unauthorized: raise exc.CommandError( "Invalid OpenStack Identity credentials.") except Exception: # NOTE(esheffield) do nothing here, we'll get a message # later if the schema is missing failed_download_schema += 1 pass return failed_download_schema >= len(resources) def main(self, argv): def _get_subparser(api_version): try: return self.get_subcommand_parser(api_version, argv) except ImportError as e: if not str(e): # Add a generic import error message if the raised # ImportError has none. raise ImportError('Unable to import module. Re-run ' 'with --debug for more info.') raise # Parse args once to find version # NOTE(flepied) Under Python3, parsed arguments are removed # from the list so make a copy for the first parsing base_argv = copy.deepcopy(argv) parser = self.get_base_parser(argv) (options, args) = parser.parse_known_args(base_argv) try: # NOTE(flaper87): Try to get the version from the # image-url first. If no version was specified, fallback # to the api-image-version arg. If both of these fail then # fallback to the minimum supported one and let keystone # do the magic. endpoint = self._get_image_url(options) endpoint, url_version = utils.strip_version(endpoint) except ValueError: # NOTE(flaper87): ValueError is raised if no endpoint is provided url_version = None # build available subcommands based on version try: api_version = int(options.os_image_api_version or url_version or 2) if api_version not in SUPPORTED_VERSIONS: raise ValueError except ValueError: msg = ("Invalid API version parameter. " "Supported values are %s" % SUPPORTED_VERSIONS) utils.exit(msg=msg) # Handle top-level --help/-h before attempting to parse # a command off the command line if options.help or not argv: parser = _get_subparser(api_version) self.do_help(options, parser=parser) return 0 # NOTE(sigmavirus24): Above, args is defined as the left over # arguments from parser.parse_known_args(). This allows us to # skip any parameters to command-line flags that may have been passed # to glanceclient, e.g., --os-auth-token. self._fixup_subcommand(args, argv) # short-circuit and deal with help command right away. sub_parser = _get_subparser(api_version) args = sub_parser.parse_args(argv) if args.func == self.do_help: self.do_help(args, parser=sub_parser) return 0 elif args.func == self.do_bash_completion: self.do_bash_completion(args) return 0 if not options.os_image_api_version and api_version == 2: switch_version = True client = self._get_versioned_client('2', args) resp, body = client.http_client.get('/versions') for version in body['versions']: if version['id'].startswith('v2'): # NOTE(flaper87): We know v2 is enabled in the server, # which means we should be able to get the schemas and # move on. switch_version = self._cache_schemas(options, client) break if switch_version: print('WARNING: The client is falling back to v1 because' ' the accessing to v2 failed. This behavior will' ' be removed in future versions', file=sys.stderr) api_version = 1 sub_parser = _get_subparser(api_version) # Parse args again and call whatever callback was selected args = sub_parser.parse_args(argv) # NOTE(flaper87): Make sure we re-use the password input if we # have one. This may happen if the schemas were downloaded in # this same command. Password will be asked to download the # schemas and then for the operations below. if not args.os_password and options.os_password: args.os_password = options.os_password if args.debug: # Set up the root logger to debug so that the submodules can # print debug messages logging.basicConfig(level=logging.DEBUG) # for iso8601 < 0.1.11 logging.getLogger('iso8601').setLevel(logging.WARNING) LOG = logging.getLogger('glanceclient') LOG.addHandler(logging.StreamHandler()) LOG.setLevel(logging.DEBUG if args.debug else logging.INFO) profile = osprofiler_profiler and options.profile if profile: osprofiler_profiler.init(options.profile) client = self._get_versioned_client(api_version, args) try: args.func(client, args) except exc.Unauthorized: raise exc.CommandError("Invalid OpenStack Identity credentials.") finally: if profile: trace_id = osprofiler_profiler.get().get_base_id() print("Profiling trace ID: %s" % trace_id) print("To display trace use next command:\n" "osprofiler trace show --html %s " % trace_id) @staticmethod def _fixup_subcommand(unknown_args, argv): # NOTE(sigmavirus24): Sometimes users pass the wrong subcommand name # to glanceclient. If they're using Python 2 they will see an error: # > invalid choice: u'imgae-list' (choose from ...) # To avoid this, we look at the extra args already parsed from above # and try to predict what the subcommand will be based on it being the # first non - or -- prefixed argument in args. We then find that in # argv and encode it from unicode so users don't see the pesky `u'` # prefix. for arg in unknown_args: if not arg.startswith('-'): # This will cover both - and -- subcommand_name = arg break else: subcommand_name = '' if (subcommand_name and six.PY2 and isinstance(subcommand_name, six.text_type)): # NOTE(sigmavirus24): if we found a subcommand name, then let's # find it in the argv list and replace it with a bytes object # instead. Note, that if we encode the argument on Python 3, the # user will instead see a pesky `b'` string instead of the `u'` # string we mention above. subcommand_index = argv.index(subcommand_name) argv[subcommand_index] = encodeutils.safe_encode(subcommand_name) @utils.arg('command', metavar='', nargs='?', help='Display help for .') def do_help(self, args, parser): """Display help about this program or one of its subcommands.""" command = getattr(args, 'command', '') if command: if args.command in self.subcommands: self.subcommands[args.command].print_help() else: raise exc.CommandError("'%s' is not a valid subcommand" % args.command) else: parser.print_help() if not args.os_image_api_version or args.os_image_api_version == '2': # NOTE(NiallBunting) This currently assumes that the only versions # are one and two. try: if command is None: print("\nRun `glance --os-image-api-version 1 help`" " for v1 help") else: self.get_subcommand_parser(1) if command in self.subcommands: command = ' ' + command print(("\nRun `glance --os-image-api-version 1 help%s`" " for v1 help") % (command or '')) except ImportError: pass def do_bash_completion(self, _args): """Prints arguments for bash_completion. Prints all of the commands and options to stdout so that the glance.bash_completion script doesn't have to hard code them. """ commands = set() options = set() for sc_str, sc in self.subcommands.items(): commands.add(sc_str) for option in sc._optionals._option_string_actions.keys(): options.add(option) commands.remove('bash_completion') commands.remove('bash-completion') print(' '.join(commands | options)) class HelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(HelpFormatter, self).start_section(heading) def main(): try: argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] OpenStackImagesShell().main(argv) except KeyboardInterrupt: utils.exit('... terminating glance client', exit_code=130) except Exception as e: if utils.debug_enabled(argv) is True: traceback.print_exc() utils.exit(encodeutils.exception_to_unicode(e)) python-glanceclient-2.9.1/glanceclient/exc.py0000666000175100017510000001133613232161651021305 0ustar zuulzuul00000000000000# 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 re import sys import six class BaseException(Exception): """An error occurred.""" def __init__(self, message=None): self.message = message def __str__(self): return self.message or self.__class__.__doc__ class CommandError(BaseException): """Invalid usage of CLI.""" class InvalidEndpoint(BaseException): """The provided endpoint is invalid.""" class CommunicationError(BaseException): """Unable to communicate with server.""" class ClientException(Exception): """DEPRECATED!""" class HTTPException(ClientException): """Base exception for all HTTP-derived exceptions.""" code = 'N/A' def __init__(self, details=None): self.details = details or self.__class__.__name__ def __str__(self): return "%s (HTTP %s)" % (self.details, self.code) class HTTPMultipleChoices(HTTPException): code = 300 def __str__(self): self.details = ("Requested version of OpenStack Images API is not " "available.") return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code, self.details) class BadRequest(HTTPException): """DEPRECATED!""" code = 400 class HTTPBadRequest(BadRequest): pass class Unauthorized(HTTPException): """DEPRECATED!""" code = 401 class HTTPUnauthorized(Unauthorized): pass class Forbidden(HTTPException): """DEPRECATED!""" code = 403 class HTTPForbidden(Forbidden): pass class NotFound(HTTPException): """DEPRECATED!""" code = 404 class HTTPNotFound(NotFound): pass class HTTPMethodNotAllowed(HTTPException): code = 405 class Conflict(HTTPException): """DEPRECATED!""" code = 409 class HTTPConflict(Conflict): pass class OverLimit(HTTPException): """DEPRECATED!""" code = 413 class HTTPOverLimit(OverLimit): pass class HTTPInternalServerError(HTTPException): code = 500 class HTTPNotImplemented(HTTPException): code = 501 class HTTPBadGateway(HTTPException): code = 502 class ServiceUnavailable(HTTPException): """DEPRECATED!""" code = 503 class HTTPServiceUnavailable(ServiceUnavailable): pass # NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception # classes _code_map = {} for obj_name in dir(sys.modules[__name__]): if obj_name.startswith('HTTP'): obj = getattr(sys.modules[__name__], obj_name) _code_map[obj.code] = obj def from_response(response, body=None): """Return an instance of an HTTPException based on httplib response.""" cls = _code_map.get(response.status_code, HTTPException) if body and 'json' in response.headers['content-type']: # Iterate over the nested objects and retrieve the "message" attribute. messages = [obj.get('message') for obj in response.json().values()] # Join all of the messages together nicely and filter out any objects # that don't have a "message" attr. details = '\n'.join(i for i in messages if i is not None) return cls(details=details) elif body and 'html' in response.headers['content-type']: # Split the lines, strip whitespace and inline HTML from the response. details = [re.sub(r'<.+?>', '', i.strip()) for i in response.text.splitlines()] details = [i for i in details if i] # Remove duplicates from the list. details_seen = set() details_temp = [] for i in details: if i not in details_seen: details_temp.append(i) details_seen.add(i) # Return joined string separated by colons. details = ': '.join(details_temp) return cls(details=details) elif body: if six.PY3: body = body.decode('utf-8') details = body.replace('\n\n', '\n') return cls(details=details) return cls() class NoTokenLookupException(Exception): """DEPRECATED!""" pass class EndpointNotFound(Exception): """DEPRECATED!""" pass class SSLConfigurationError(BaseException): pass class SSLCertificateError(BaseException): pass python-glanceclient-2.9.1/glanceclient/v2/0000775000175100017510000000000013232162126020473 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/v2/schemas.py0000666000175100017510000001020013232161652022466 0ustar zuulzuul00000000000000# 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 copy import json import jsonpatch import warlock.model as warlock class SchemaBasedModel(warlock.Model): """Glance specific subclass of the warlock Model. This implementation alters the function of the patch property to take into account the schema's core properties. With this version undefined properties which are core will generated 'replace' operations rather than 'add' since this is what the Glance API expects. """ def _make_custom_patch(self, new, original): if not self.get('tags'): tags_patch = [] else: tags_patch = [{"path": "/tags", "value": self.get('tags'), "op": "replace"}] patch_string = jsonpatch.make_patch(original, new).to_string() patch = json.loads(patch_string) if not patch: return json.dumps(tags_patch) else: return json.dumps(patch + tags_patch) @warlock.Model.patch.getter def patch(self): """Return a jsonpatch object representing the delta.""" original = copy.deepcopy(self.__dict__['__original__']) new = dict(self) if self.schema: for (name, prop) in self.schema['properties'].items(): if (name not in original and name in new and prop.get('is_base', True)): original[name] = None original['tags'] = None new['tags'] = None return self._make_custom_patch(new, original) class SchemaProperty(object): def __init__(self, name, **kwargs): self.name = name self.description = kwargs.get('description') self.is_base = kwargs.get('is_base', True) def translate_schema_properties(schema_properties): """Parse the properties dictionary of a schema document. :returns: list of SchemaProperty objects """ properties = [] for (name, prop) in schema_properties.items(): properties.append(SchemaProperty(name, **prop)) return properties class Schema(object): def __init__(self, raw_schema): self._raw_schema = raw_schema self.name = raw_schema['name'] raw_properties = raw_schema['properties'] self.properties = translate_schema_properties(raw_properties) def is_core_property(self, property_name): """Check if a property with a given name is known to the schema. Determines if it is either a base property or a custom one registered in schema-image.json file :param property_name: name of the property :returns: True if the property is known, False otherwise """ return self._check_property(property_name, True) def is_base_property(self, property_name): """Checks if a property with a given name is a base property. :param property_name: name of the property :returns: True if the property is base, False otherwise """ return self._check_property(property_name, False) def _check_property(self, property_name, allow_non_base): for prop in self.properties: if property_name == prop.name: return prop.is_base or allow_non_base return False def raw(self): return copy.deepcopy(self._raw_schema) class Controller(object): def __init__(self, http_client): self.http_client = http_client def get(self, schema_name): uri = '/v2/schemas/%s' % schema_name _, raw_schema = self.http_client.get(uri) return Schema(raw_schema) python-glanceclient-2.9.1/glanceclient/v2/image_members.py0000666000175100017510000000425713232161652023656 0ustar zuulzuul00000000000000# Copyright 2013 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 warlock from glanceclient.common import utils from glanceclient.v2 import schemas MEMBER_STATUS_VALUES = ('accepted', 'rejected', 'pending') class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('member') return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel) @utils.add_req_id_to_generator() def list(self, image_id): url = '/v2/images/%s/members' % image_id resp, body = self.http_client.get(url) for member in body['members']: yield self.model(member), resp @utils.add_req_id_to_object() def delete(self, image_id, member_id): resp, body = self.http_client.delete('/v2/images/%s/members/%s' % (image_id, member_id)) return (resp, body), resp @utils.add_req_id_to_object() def update(self, image_id, member_id, member_status): url = '/v2/images/%s/members/%s' % (image_id, member_id) body = {'status': member_status} resp, updated_member = self.http_client.put(url, data=body) return self.model(updated_member), resp @utils.add_req_id_to_object() def create(self, image_id, member_id): url = '/v2/images/%s/members' % image_id body = {'member': member_id} resp, created_member = self.http_client.post(url, data=body) return self.model(created_member), resp python-glanceclient-2.9.1/glanceclient/v2/versions.py0000666000175100017510000000166113232161652022726 0ustar zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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. class VersionController(object): def __init__(self, http_client): self.http_client = http_client def list(self): """List all versions.""" url = '/versions' resp, body = self.http_client.get(url) return body.get('versions', None) python-glanceclient-2.9.1/glanceclient/v2/__init__.py0000666000175100017510000000117113232161652022611 0ustar zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # 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 glanceclient.v2.client import Client # noqa python-glanceclient-2.9.1/glanceclient/v2/shell.py0000666000175100017510000013164413232161652022172 0ustar zuulzuul00000000000000# 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 sys from glanceclient._i18n import _ from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient import exc from glanceclient.v2 import image_members from glanceclient.v2 import image_schema from glanceclient.v2 import images from glanceclient.v2 import namespace_schema from glanceclient.v2 import resource_type_schema from glanceclient.v2 import tasks import json import os MEMBER_STATUS_VALUES = image_members.MEMBER_STATUS_VALUES IMAGE_SCHEMA = None DATA_FIELDS = ('location', 'copy_from', 'file') def get_image_schema(): global IMAGE_SCHEMA if IMAGE_SCHEMA is None: schema_path = os.path.expanduser("~/.glanceclient/image_schema.json") if os.path.isfile(schema_path): with open(schema_path, "r") as f: schema_raw = f.read() IMAGE_SCHEMA = json.loads(schema_raw) else: return image_schema._BASE_SCHEMA return IMAGE_SCHEMA @utils.schema_args(get_image_schema, omit=['created_at', 'updated_at', 'file', 'checksum', 'virtual_size', 'size', 'status', 'schema', 'direct_url', 'locations', 'self']) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded ' 'during creation. Alternatively, the image data can be ' 'passed to the client via stdin.')) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.on_data_require_fields(DATA_FIELDS) def do_image_create(gc, args): """Create a new image.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (x[0] == 'property' or schema.is_core_property(x[0])), _args)) raw_properties = fields.pop('property', []) for datum in raw_properties: key, value = datum.split('=', 1) fields[key] = value file_name = fields.pop('file', None) if file_name is not None and os.access(file_name, os.R_OK) is False: utils.exit("File %s does not exist or user does not have read " "privileges to it" % file_name) image = gc.images.create(**fields) try: if utils.get_data_file(args) is not None: args.id = image['id'] args.size = None do_image_upload(gc, args) image = gc.images.get(args.id) finally: utils.print_image(image) @utils.schema_args(get_image_schema, omit=['created_at', 'updated_at', 'file', 'checksum', 'virtual_size', 'size', 'status', 'schema', 'direct_url', 'locations', 'self']) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded ' 'during creation. Alternatively, the image data can be ' 'passed to the client via stdin.')) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('--import-method', metavar='', default='glance-direct', help=_('Import method used for Image Import workflow. ' 'Valid values can be retrieved with import-info command.')) @utils.on_data_require_fields(DATA_FIELDS) def do_image_create_via_import(gc, args): """EXPERIMENTAL: Create a new image via image import.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (x[0] == 'property' or schema.is_core_property(x[0])), _args)) raw_properties = fields.pop('property', []) for datum in raw_properties: key, value = datum.split('=', 1) fields[key] = value file_name = fields.pop('file', None) if file_name is not None and os.access(file_name, os.R_OK) is False: utils.exit("File %s does not exist or user does not have read " "privileges to it" % file_name) import_methods = gc.images.get_import_info().get('import-methods') if file_name and (not import_methods or 'glance-direct' not in import_methods.get('value')): utils.exit("No suitable import method available for direct upload, " "please use image-create instead.") image = gc.images.create(**fields) try: if utils.get_data_file(args) is not None: args.id = image['id'] args.size = None do_image_stage(gc, args) args.from_create = True do_image_import(gc, args) image = gc.images.get(args.id) finally: utils.print_image(image) @utils.arg('id', metavar='', help=_('ID of image to update.')) @utils.schema_args(get_image_schema, omit=['id', 'locations', 'created_at', 'updated_at', 'file', 'checksum', 'virtual_size', 'size', 'status', 'schema', 'direct_url', 'tags', 'self']) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @utils.arg('--remove-property', metavar="key", action='append', default=[], help=_("Name of arbitrary property to remove from the image.")) def do_image_update(gc, args): """Update an existing image.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (x[0] in ['property', 'remove_property'] or schema.is_core_property(x[0])), _args)) raw_properties = fields.pop('property', []) for datum in raw_properties: key, value = datum.split('=', 1) fields[key] = value remove_properties = fields.pop('remove_property', None) image_id = fields.pop('id') image = gc.images.update(image_id, remove_properties, **fields) utils.print_image(image) @utils.arg('--limit', metavar='', default=None, type=int, help=_('Maximum number of images to get.')) @utils.arg('--page-size', metavar='', default=None, type=int, help=_('Number of images to request in each paginated request.')) @utils.arg('--visibility', metavar='', help=_('The visibility of the images to display.')) @utils.arg('--member-status', metavar='', help=_('The status of images to display.')) @utils.arg('--owner', metavar='', help=_('Display images owned by .')) @utils.arg('--property-filter', metavar='', help=_("Filter images by a user-defined image property."), action='append', dest='properties', default=[]) @utils.arg('--checksum', metavar='', help=_('Displays images that match the checksum.')) @utils.arg('--tag', metavar='', action='append', help=_("Filter images by a user-defined tag.")) @utils.arg('--sort-key', default=[], action='append', choices=images.SORT_KEY_VALUES, help=_('Sort image list by specified fields.' ' May be used multiple times.')) @utils.arg('--sort-dir', default=[], action='append', choices=images.SORT_DIR_VALUES, help=_('Sort image list in specified directions.')) @utils.arg('--sort', metavar='[:]', default=None, help=(_("Comma-separated list of sort keys and directions in the " "form of [:]. Valid keys: %s. OPTIONAL." ) % ', '.join(images.SORT_KEY_VALUES))) def do_image_list(gc, args): """List images you can access.""" filter_keys = ['visibility', 'member_status', 'owner', 'checksum', 'tag'] filter_items = [(key, getattr(args, key)) for key in filter_keys] if args.properties: filter_properties = [prop.split('=', 1) for prop in args.properties] if any(len(pair) != 2 for pair in filter_properties): utils.exit('Argument --property-filter expected properties in the' ' format KEY=VALUE') filter_items += filter_properties filters = dict([item for item in filter_items if item[1] is not None]) kwargs = {'filters': filters} if args.limit is not None: kwargs['limit'] = args.limit if args.page_size is not None: kwargs['page_size'] = args.page_size if args.sort_key: kwargs['sort_key'] = args.sort_key if args.sort_dir: kwargs['sort_dir'] = args.sort_dir if args.sort is not None: kwargs['sort'] = args.sort elif not args.sort_dir and not args.sort_key: kwargs['sort_key'] = 'name' kwargs['sort_dir'] = 'asc' columns = ['ID', 'Name'] if args.verbose: columns += ['Disk_format', 'Container_format', 'Size', 'Status', 'Owner'] images = gc.images.list(**kwargs) utils.print_list(images, columns) @utils.arg('id', metavar='', help=_('ID of image to describe.')) @utils.arg('--human-readable', action='store_true', default=False, help=_('Print image size in a human-friendly format.')) @utils.arg('--max-column-width', metavar='', default=80, help=_('The max column width of the printed table.')) def do_image_show(gc, args): """Describe a specific image.""" image = gc.images.get(args.id) utils.print_image(image, args.human_readable, int(args.max_column_width)) @utils.arg('--image-id', metavar='', required=True, help=_('Image to display members of.')) def do_member_list(gc, args): """Describe sharing permissions by image.""" members = gc.image_members.list(args.image_id) columns = ['Image ID', 'Member ID', 'Status'] utils.print_list(members, columns) @utils.arg('image_id', metavar='', help=_('Image from which to remove member.')) @utils.arg('member_id', metavar='', help=_('Tenant to remove as member.')) def do_member_delete(gc, args): """Delete image member.""" if not (args.image_id and args.member_id): utils.exit('Unable to delete member. Specify image_id and member_id') else: gc.image_members.delete(args.image_id, args.member_id) @utils.arg('image_id', metavar='', help=_('Image from which to update member.')) @utils.arg('member_id', metavar='', help=_('Tenant to update.')) @utils.arg('member_status', metavar='', choices=MEMBER_STATUS_VALUES, help=(_('Updated status of member. Valid Values: %s') % ', '.join(str(val) for val in MEMBER_STATUS_VALUES))) def do_member_update(gc, args): """Update the status of a member for a given image.""" if not (args.image_id and args.member_id and args.member_status): utils.exit('Unable to update member. Specify image_id, member_id and' ' member_status') else: member = gc.image_members.update(args.image_id, args.member_id, args.member_status) member = [member] columns = ['Image ID', 'Member ID', 'Status'] utils.print_list(member, columns) @utils.arg('image_id', metavar='', help=_('Image with which to create member.')) @utils.arg('member_id', metavar='', help=_('Tenant to add as member.')) def do_member_create(gc, args): """Create member for a given image.""" if not (args.image_id and args.member_id): utils.exit('Unable to create member. Specify image_id and member_id') else: member = gc.image_members.create(args.image_id, args.member_id) member = [member] columns = ['Image ID', 'Member ID', 'Status'] utils.print_list(member, columns) @utils.arg('model', metavar='', help=_('Name of model to describe.')) def do_explain(gc, args): """Describe a specific model.""" try: schema = gc.schemas.get(args.model) except exc.HTTPNotFound: utils.exit('Unable to find requested model \'%s\'' % args.model) else: formatters = {'Attribute': lambda m: m.name} columns = ['Attribute', 'Description'] utils.print_list(schema.properties, columns, formatters) def do_import_info(gc, args): """Print import methods available from Glance.""" try: import_info = gc.images.get_import_info() except exc.HTTPNotFound: utils.exit('Target Glance does not support Image Import workflow') else: utils.print_dict(import_info) @utils.arg('--file', metavar='', help=_('Local file to save downloaded image data to. ' 'If this is not specified and there is no redirection ' 'the image data will not be saved.')) @utils.arg('id', metavar='', help=_('ID of image to download.')) @utils.arg('--progress', action='store_true', default=False, help=_('Show download progress bar.')) def do_image_download(gc, args): """Download a specific image.""" if sys.stdout.isatty() and (args.file is None): msg = ('No redirection or local file specified for downloaded image ' 'data. Please specify a local file with --file to save ' 'downloaded image or redirect output to another source.') utils.exit(msg) try: body = gc.images.data(args.id) except (exc.HTTPForbidden, exc.HTTPException) as e: msg = "Unable to download image '%s'. (%s)" % (args.id, e) utils.exit(msg) if body.wrapped is None: msg = ('Image %s has no data.' % args.id) utils.exit(msg) if args.progress: body = progressbar.VerboseIteratorWrapper(body, len(body)) utils.save_image(body, args.file) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded.' ' Alternatively, images can be passed' ' to the client via stdin.')) @utils.arg('--size', metavar='', type=int, help=_('Size in bytes of image to be uploaded. Default is to get ' 'size from provided data object but this is supported in ' 'case where size cannot be inferred.'), default=None) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('id', metavar='', help=_('ID of image to upload data to.')) def do_image_upload(gc, args): """Upload data for a specific image.""" image_data = utils.get_data_file(args) if args.progress: filesize = utils.get_file_size(image_data) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) image_data = progressbar.VerboseFileWrapper(image_data, filesize) gc.images.upload(args.id, image_data, args.size) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded.' ' Alternatively, images can be passed' ' to the client via stdin.')) @utils.arg('--size', metavar='', type=int, help=_('Size in bytes of image to be uploaded. Default is to get ' 'size from provided data object but this is supported in ' 'case where size cannot be inferred.'), default=None) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('id', metavar='', help=_('ID of image to upload data to.')) def do_image_stage(gc, args): """Upload data for a specific image to staging.""" image_data = utils.get_data_file(args) if args.progress: filesize = utils.get_file_size(image_data) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) image_data = progressbar.VerboseFileWrapper(image_data, filesize) gc.images.stage(args.id, image_data, args.size) @utils.arg('--import-method', metavar='', default='glance-direct', help=_('Import method used for Image Import workflow. ' 'Valid values can be retrieved with import-info command ' 'and the default "glance-direct" is used with ' '"image-stage".')) @utils.arg('id', metavar='', help=_('ID of image to import.')) def do_image_import(gc, args): """Initiate the image import taskflow.""" try: gc.images.image_import(args.id, args.import_method) except exc.HTTPNotFound: utils.exit('Target Glance does not support Image Import workflow') else: if not getattr(args, 'from_create', False): image = gc.images.get(args.id) utils.print_image(image) @utils.arg('id', metavar='', nargs='+', help=_('ID of image(s) to delete.')) def do_image_delete(gc, args): """Delete specified image.""" failure_flag = False for args_id in args.id: try: gc.images.delete(args_id) except exc.HTTPForbidden: msg = "You are not permitted to delete the image '%s'." % args_id utils.print_err(msg) failure_flag = True except exc.HTTPNotFound: msg = "No image with an ID of '%s' exists." % args_id utils.print_err(msg) failure_flag = True except exc.HTTPConflict: msg = "Unable to delete image '%s' because it is in use." % args_id utils.print_err(msg) failure_flag = True except exc.HTTPException as e: msg = "'%s': Unable to delete image '%s'" % (e, args_id) utils.print_err(msg) failure_flag = True if failure_flag: utils.exit() @utils.arg('id', metavar='', help=_('ID of image to deactivate.')) def do_image_deactivate(gc, args): """Deactivate specified image.""" gc.images.deactivate(args.id) @utils.arg('id', metavar='', help=_('ID of image to reactivate.')) def do_image_reactivate(gc, args): """Reactivate specified image.""" gc.images.reactivate(args.id) @utils.arg('image_id', metavar='', help=_('Image to be updated with the given tag.')) @utils.arg('tag_value', metavar='', help=_('Value of the tag.')) def do_image_tag_update(gc, args): """Update an image with the given tag.""" if not (args.image_id and args.tag_value): utils.exit('Unable to update tag. Specify image_id and tag_value') else: gc.image_tags.update(args.image_id, args.tag_value) image = gc.images.get(args.image_id) image = [image] columns = ['ID', 'Tags'] utils.print_list(image, columns) @utils.arg('image_id', metavar='', help=_('ID of the image from which to delete tag.')) @utils.arg('tag_value', metavar='', help=_('Value of the tag.')) def do_image_tag_delete(gc, args): """Delete the tag associated with the given image.""" if not (args.image_id and args.tag_value): utils.exit('Unable to delete tag. Specify image_id and tag_value') else: gc.image_tags.delete(args.image_id, args.tag_value) @utils.arg('--url', metavar='', required=True, help=_('URL of location to add.')) @utils.arg('--metadata', metavar='', default='{}', help=_('Metadata associated with the location. ' 'Must be a valid JSON object (default: %(default)s)')) @utils.arg('id', metavar='', help=_('ID of image to which the location is to be added.')) def do_location_add(gc, args): """Add a location (and related metadata) to an image.""" try: metadata = json.loads(args.metadata) except ValueError: utils.exit('Metadata is not a valid JSON object.') else: image = gc.images.add_location(args.id, args.url, metadata) utils.print_dict(image) @utils.arg('--url', metavar='', action='append', required=True, help=_('URL of location to remove. May be used multiple times.')) @utils.arg('id', metavar='', help=_('ID of image whose locations are to be removed.')) def do_location_delete(gc, args): """Remove locations (and related metadata) from an image.""" gc.images.delete_locations(args.id, set(args.url)) @utils.arg('--url', metavar='', required=True, help=_('URL of location to update.')) @utils.arg('--metadata', metavar='', default='{}', help=_('Metadata associated with the location. ' 'Must be a valid JSON object (default: %(default)s)')) @utils.arg('id', metavar='', help=_('ID of image whose location is to be updated.')) def do_location_update(gc, args): """Update metadata of an image's location.""" try: metadata = json.loads(args.metadata) if metadata == {}: print("WARNING -- The location's metadata will be updated to " "an empty JSON object.") except ValueError: utils.exit('Metadata is not a valid JSON object.') else: image = gc.images.update_location(args.id, args.url, metadata) utils.print_dict(image) # Metadata - catalog NAMESPACE_SCHEMA = None def get_namespace_schema(): global NAMESPACE_SCHEMA if NAMESPACE_SCHEMA is None: schema_path = os.path.expanduser("~/.glanceclient/" "namespace_schema.json") if os.path.isfile(schema_path): with open(schema_path, "r") as f: schema_raw = f.read() NAMESPACE_SCHEMA = json.loads(schema_raw) else: return namespace_schema.BASE_SCHEMA return NAMESPACE_SCHEMA def _namespace_show(namespace, max_column_width=None): namespace = dict(namespace) # Warlock objects are compatible with dicts # Flatten dicts for display if 'properties' in namespace: props = [k for k in namespace['properties']] namespace['properties'] = props if 'resource_type_associations' in namespace: assocs = [assoc['name'] for assoc in namespace['resource_type_associations']] namespace['resource_type_associations'] = assocs if 'objects' in namespace: objects = [obj['name'] for obj in namespace['objects']] namespace['objects'] = objects if 'tags' in namespace: tags = [tag['name'] for tag in namespace['tags']] namespace['tags'] = tags if max_column_width: utils.print_dict(namespace, max_column_width) else: utils.print_dict(namespace) @utils.arg('namespace', metavar='', help=_('Name of the namespace.')) @utils.schema_args(get_namespace_schema, omit=['namespace', 'property_count', 'properties', 'tag_count', 'tags', 'object_count', 'objects', 'resource_types']) def do_md_namespace_create(gc, args): """Create a new metadata definitions namespace.""" schema = gc.schemas.get('metadefs/namespace') _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (schema.is_core_property(x[0])), _args)) namespace = gc.metadefs_namespace.create(**fields) _namespace_show(namespace) @utils.arg('--file', metavar='', help=_('Path to file with namespace schema to import. ' 'Alternatively, namespaces schema can be passed to the ' 'client via stdin.')) def do_md_namespace_import(gc, args): """Import a metadata definitions namespace from file or standard input.""" namespace_data = utils.get_data_file(args) if not namespace_data: utils.exit('No metadata definition namespace passed via stdin or ' '--file argument.') try: namespace_json = json.load(namespace_data) except ValueError: utils.exit('Schema is not a valid JSON object.') else: namespace = gc.metadefs_namespace.create(**namespace_json) _namespace_show(namespace) @utils.arg('id', metavar='', help=_('Name of namespace to update.')) @utils.schema_args(get_namespace_schema, omit=['property_count', 'properties', 'tag_count', 'tags', 'object_count', 'objects', 'resource_type_associations', 'schema']) def do_md_namespace_update(gc, args): """Update an existing metadata definitions namespace.""" schema = gc.schemas.get('metadefs/namespace') _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (schema.is_core_property(x[0])), _args)) namespace = gc.metadefs_namespace.update(args.id, **fields) _namespace_show(namespace) @utils.arg('namespace', metavar='', help=_('Name of namespace to describe.')) @utils.arg('--resource-type', metavar='', help=_('Applies prefix of given resource type associated to a ' 'namespace to all properties of a namespace.'), default=None) @utils.arg('--max-column-width', metavar='', default=80, help=_('The max column width of the printed table.')) def do_md_namespace_show(gc, args): """Describe a specific metadata definitions namespace. Lists also the namespace properties, objects and resource type associations. """ kwargs = {} if args.resource_type: kwargs['resource_type'] = args.resource_type namespace = gc.metadefs_namespace.get(args.namespace, **kwargs) _namespace_show(namespace, int(args.max_column_width)) @utils.arg('--resource-types', metavar='', action='append', help=_('Resource type to filter namespaces.')) @utils.arg('--visibility', metavar='', help=_('Visibility parameter to filter namespaces.')) @utils.arg('--page-size', metavar='', default=None, type=int, help=_('Number of namespaces to request ' 'in each paginated request.')) def do_md_namespace_list(gc, args): """List metadata definitions namespaces.""" filter_keys = ['resource_types', 'visibility'] filter_items = [(key, getattr(args, key, None)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size namespaces = gc.metadefs_namespace.list(**kwargs) columns = ['namespace'] utils.print_list(namespaces, columns) @utils.arg('namespace', metavar='', help=_('Name of namespace to delete.')) def do_md_namespace_delete(gc, args): """Delete specified metadata definitions namespace with its contents.""" gc.metadefs_namespace.delete(args.namespace) # Metadata - catalog RESOURCE_TYPE_SCHEMA = None def get_resource_type_schema(): global RESOURCE_TYPE_SCHEMA if RESOURCE_TYPE_SCHEMA is None: schema_path = os.path.expanduser("~/.glanceclient/" "resource_type_schema.json") if os.path.isfile(schema_path): with open(schema_path, "r") as f: schema_raw = f.read() RESOURCE_TYPE_SCHEMA = json.loads(schema_raw) else: return resource_type_schema.BASE_SCHEMA return RESOURCE_TYPE_SCHEMA @utils.arg('namespace', metavar='', help=_('Name of namespace.')) @utils.schema_args(get_resource_type_schema) def do_md_resource_type_associate(gc, args): """Associate resource type with a metadata definitions namespace.""" schema = gc.schemas.get('metadefs/resource_type') _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (schema.is_core_property(x[0])), _args)) resource_type = gc.metadefs_resource_type.associate(args.namespace, **fields) utils.print_dict(resource_type) @utils.arg('namespace', metavar='', help=_('Name of namespace.')) @utils.arg('resource_type', metavar='', help=_('Name of resource type.')) def do_md_resource_type_deassociate(gc, args): """Deassociate resource type with a metadata definitions namespace.""" gc.metadefs_resource_type.deassociate(args.namespace, args.resource_type) def do_md_resource_type_list(gc, args): """List available resource type names.""" resource_types = gc.metadefs_resource_type.list() utils.print_list(resource_types, ['name']) @utils.arg('namespace', metavar='', help=_('Name of namespace.')) def do_md_namespace_resource_type_list(gc, args): """List resource types associated to specific namespace.""" resource_types = gc.metadefs_resource_type.get(args.namespace) utils.print_list(resource_types, ['name', 'prefix', 'properties_target']) @utils.arg('namespace', metavar='', help=_('Name of namespace the property will belong.')) @utils.arg('--name', metavar='', required=True, help=_('Internal name of a property.')) @utils.arg('--title', metavar='', required=True, help=_('Property name displayed to the user.')) @utils.arg('--schema', metavar='<SCHEMA>', required=True, help=_('Valid JSON schema of a property.')) def do_md_property_create(gc, args): """Create a new metadata definitions property inside a namespace.""" try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields = {'name': args.name, 'title': args.title} fields.update(schema) new_property = gc.metadefs_property.create(args.namespace, **fields) utils.print_dict(new_property) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the property belongs.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) @utils.arg('--name', metavar='<NAME>', default=None, help=_('New name of a property.')) @utils.arg('--title', metavar='<TITLE>', default=None, help=_('Property name displayed to the user.')) @utils.arg('--schema', metavar='<SCHEMA>', default=None, help=_('Valid JSON schema of a property.')) def do_md_property_update(gc, args): """Update metadata definitions property inside a namespace.""" fields = {} if args.name: fields['name'] = args.name if args.title: fields['title'] = args.title if args.schema: try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields.update(schema) new_property = gc.metadefs_property.update(args.namespace, args.property, **fields) utils.print_dict(new_property) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the property belongs.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) @utils.arg('--max-column-width', metavar='<integer>', default=80, help=_('The max column width of the printed table.')) def do_md_property_show(gc, args): """Describe a specific metadata definitions property inside a namespace.""" prop = gc.metadefs_property.get(args.namespace, args.property) utils.print_dict(prop, int(args.max_column_width)) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the property belongs.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) def do_md_property_delete(gc, args): """Delete a specific metadata definitions property inside a namespace.""" gc.metadefs_property.delete(args.namespace, args.property) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_namespace_properties_delete(gc, args): """Delete all metadata definitions property inside a specific namespace.""" gc.metadefs_property.delete_all(args.namespace) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_property_list(gc, args): """List metadata definitions properties inside a specific namespace.""" properties = gc.metadefs_property.list(args.namespace) columns = ['name', 'title', 'type'] utils.print_list(properties, columns) def _object_show(obj, max_column_width=None): obj = dict(obj) # Warlock objects are compatible with dicts # Flatten dicts for display if 'properties' in obj: objects = [k for k in obj['properties']] obj['properties'] = objects if max_column_width: utils.print_dict(obj, max_column_width) else: utils.print_dict(obj) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object will belong.')) @utils.arg('--name', metavar='<NAME>', required=True, help=_('Internal name of an object.')) @utils.arg('--schema', metavar='<SCHEMA>', required=True, help=_('Valid JSON schema of an object.')) def do_md_object_create(gc, args): """Create a new metadata definitions object inside a namespace.""" try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields = {'name': args.name} fields.update(schema) new_object = gc.metadefs_object.create(args.namespace, **fields) _object_show(new_object) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) @utils.arg('--name', metavar='<NAME>', default=None, help=_('New name of an object.')) @utils.arg('--schema', metavar='<SCHEMA>', default=None, help=_('Valid JSON schema of an object.')) def do_md_object_update(gc, args): """Update metadata definitions object inside a namespace.""" fields = {} if args.name: fields['name'] = args.name if args.schema: try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields.update(schema) new_object = gc.metadefs_object.update(args.namespace, args.object, **fields) _object_show(new_object) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) @utils.arg('--max-column-width', metavar='<integer>', default=80, help=_('The max column width of the printed table.')) def do_md_object_show(gc, args): """Describe a specific metadata definitions object inside a namespace.""" obj = gc.metadefs_object.get(args.namespace, args.object) _object_show(obj, int(args.max_column_width)) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) @utils.arg('--max-column-width', metavar='<integer>', default=80, help=_('The max column width of the printed table.')) def do_md_object_property_show(gc, args): """Describe a specific metadata definitions property inside an object.""" obj = gc.metadefs_object.get(args.namespace, args.object) try: prop = obj['properties'][args.property] prop['name'] = args.property except KeyError: utils.exit('Property %s not found in object %s.' % (args.property, args.object)) utils.print_dict(prop, int(args.max_column_width)) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) def do_md_object_delete(gc, args): """Delete a specific metadata definitions object inside a namespace.""" gc.metadefs_object.delete(args.namespace, args.object) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_namespace_objects_delete(gc, args): """Delete all metadata definitions objects inside a specific namespace.""" gc.metadefs_object.delete_all(args.namespace) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_object_list(gc, args): """List metadata definitions objects inside a specific namespace.""" objects = gc.metadefs_object.list(args.namespace) columns = ['name', 'description'] column_settings = { "description": { "max_width": 50, "align": "l" } } utils.print_list(objects, columns, field_settings=column_settings) def _tag_show(tag, max_column_width=None): tag = dict(tag) # Warlock objects are compatible with dicts if max_column_width: utils.print_dict(tag, max_column_width) else: utils.print_dict(tag) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace the tag will belong to.')) @utils.arg('--name', metavar='<NAME>', required=True, help=_('The name of the new tag to add.')) def do_md_tag_create(gc, args): """Add a new metadata definitions tag inside a namespace.""" name = args.name.strip() if name: new_tag = gc.metadefs_tag.create(args.namespace, name) _tag_show(new_tag) else: utils.exit('Please supply at least one non-blank tag name.') @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace the tags will belong to.')) @utils.arg('--names', metavar='<NAMES>', required=True, help=_('A comma separated list of tag names.')) @utils.arg('--delim', metavar='<DELIM>', required=False, help=_('The delimiter used to separate the names' ' (if none is provided then the default is a comma).')) def do_md_tag_create_multiple(gc, args): """Create new metadata definitions tags inside a namespace.""" delim = args.delim or ',' tags = [] names_list = args.names.split(delim) for name in names_list: name = name.strip() if name: tags.append(name) if not tags: utils.exit('Please supply at least one tag name. For example: ' '--names Tag1') fields = {'tags': tags} new_tags = gc.metadefs_tag.create_multiple(args.namespace, **fields) columns = ['name'] column_settings = { "description": { "max_width": 50, "align": "l" } } utils.print_list(new_tags, columns, field_settings=column_settings) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace to which the tag belongs.')) @utils.arg('tag', metavar='<TAG>', help=_('Name of the old tag.')) @utils.arg('--name', metavar='<NAME>', default=None, required=True, help=_('New name of the new tag.')) def do_md_tag_update(gc, args): """Rename a metadata definitions tag inside a namespace.""" name = args.name.strip() if name: fields = {'name': name} new_tag = gc.metadefs_tag.update(args.namespace, args.tag, **fields) _tag_show(new_tag) else: utils.exit('Please supply at least one non-blank tag name.') @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace to which the tag belongs.')) @utils.arg('tag', metavar='<TAG>', help=_('Name of the tag.')) def do_md_tag_show(gc, args): """Describe a specific metadata definitions tag inside a namespace.""" tag = gc.metadefs_tag.get(args.namespace, args.tag) _tag_show(tag) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace to which the tag belongs.')) @utils.arg('tag', metavar='<TAG>', help=_('Name of the tag.')) def do_md_tag_delete(gc, args): """Delete a specific metadata definitions tag inside a namespace.""" gc.metadefs_tag.delete(args.namespace, args.tag) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_namespace_tags_delete(gc, args): """Delete all metadata definitions tags inside a specific namespace.""" gc.metadefs_tag.delete_all(args.namespace) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_tag_list(gc, args): """List metadata definitions tags inside a specific namespace.""" tags = gc.metadefs_tag.list(args.namespace) columns = ['name'] column_settings = { "description": { "max_width": 50, "align": "l" } } utils.print_list(tags, columns, field_settings=column_settings) @utils.arg('--sort-key', default='status', choices=tasks.SORT_KEY_VALUES, help=_('Sort task list by specified field.')) @utils.arg('--sort-dir', default='desc', choices=tasks.SORT_DIR_VALUES, help=_('Sort task list in specified direction.')) @utils.arg('--page-size', metavar='<SIZE>', default=None, type=int, help=_('Number of tasks to request in each paginated request.')) @utils.arg('--type', metavar='<TYPE>', help=_('Filter tasks to those that have this type.')) @utils.arg('--status', metavar='<STATUS>', help=_('Filter tasks to those that have this status.')) def do_task_list(gc, args): """List tasks you can access.""" filter_keys = ['type', 'status'] filter_items = [(key, getattr(args, key)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size kwargs['sort_key'] = args.sort_key kwargs['sort_dir'] = args.sort_dir tasks = gc.tasks.list(**kwargs) columns = ['ID', 'Type', 'Status', 'Owner'] utils.print_list(tasks, columns) @utils.arg('id', metavar='<TASK_ID>', help=_('ID of task to describe.')) def do_task_show(gc, args): """Describe a specific task.""" task = gc.tasks.get(args.id) ignore = ['self', 'schema'] task = dict([item for item in task.items() if item[0] not in ignore]) utils.print_dict(task) @utils.arg('--type', metavar='<TYPE>', help=_('Type of Task. Please refer to Glance schema or ' 'documentation to see which tasks are supported.')) @utils.arg('--input', metavar='<STRING>', default='{}', help=_('Parameters of the task to be launched')) def do_task_create(gc, args): """Create a new task.""" if not (args.type and args.input): utils.exit('Unable to create task. Specify task type and input.') else: try: input = json.loads(args.input) except ValueError: utils.exit('Failed to parse the "input" parameter. Must be a ' 'valid JSON object.') task_values = {'type': args.type, 'input': input} task = gc.tasks.create(**task_values) ignore = ['self', 'schema'] task = dict([item for item in task.items() if item[0] not in ignore]) utils.print_dict(task) ��������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/metadefs.py�����������������������������������������������0000666�0001751�0001751�00000052556�13232161652�022657� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2014 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. from oslo_utils import encodeutils import six from six.moves.urllib import parse import warlock from glanceclient.common import utils from glanceclient.v2 import schemas DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('created_at', 'namespace') class NamespaceController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/namespace') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, **kwargs): """Create a namespace. :param kwargs: Unpacked namespace object. """ url = '/v2/metadefs/namespaces' try: namespace = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) resp, body = self.http_client.post(url, data=namespace) body.pop('self', None) return self.model(**body), resp def update(self, namespace_name, **kwargs): """Update a namespace. :param namespace_name: Name of a namespace (old one). :param kwargs: Unpacked namespace object. """ namespace = self.get(namespace_name) for (key, value) in kwargs.items(): try: setattr(namespace, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) # Remove read-only parameters. read_only = ['schema', 'updated_at', 'created_at'] for elem in read_only: if elem in namespace: del namespace[elem] url = '/v2/metadefs/namespaces/%(namespace)s' % { 'namespace': namespace_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=namespace.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp) } return self._get(namespace.namespace, header=req_id_hdr) def get(self, namespace, **kwargs): return self._get(namespace, **kwargs) @utils.add_req_id_to_object() def _get(self, namespace, header=None, **kwargs): """Get one namespace.""" query_params = parse.urlencode(kwargs) if kwargs: query_params = '?%s' % query_params url = '/v2/metadefs/namespaces/%(namespace)s%(query_params)s' % { 'namespace': namespace, 'query_params': query_params} header = header or {} resp, body = self.http_client.get(url, headers=header) # NOTE(bcwaldon): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, **kwargs): """Retrieve a listing of Namespace objects. :param page_size: Number of items to request in each paginated request :param limit: Use to request a specific page size. Expect a response to a limited request to return between zero and limit items. :param marker: Specifies the namespace of the last-seen namespace. The typical pattern of limit and marker is to make an initial limited request and then to use the last namespace from the response as the marker parameter in a subsequent limited request. :param sort_key: The field to sort on (for example, 'created_at') :param sort_dir: The direction to sort ('asc' or 'desc') :returns: generator over list of Namespaces """ ori_validate_fun = self.model.validate empty_fun = lambda *args, **kwargs: None def paginate(url): resp, body = self.http_client.get(url) for namespace in body['namespaces']: # NOTE(bcwaldon): remove 'self' for now until we have # an elegant way to pass it into the model constructor # without conflict. namespace.pop('self', None) yield self.model(**namespace), resp # NOTE(zhiyan): In order to resolve the performance issue # of JSON schema validation for image listing case, we # don't validate each image entry but do it only on first # image entry for each page. self.model.validate = empty_fun # NOTE(zhiyan); Reset validation function. self.model.validate = ori_validate_fun try: next_url = body['next'] except KeyError: return else: for namespace, resp in paginate(next_url): yield namespace, resp filters = kwargs.get('filters', {}) filters = {} if filters is None else filters if not kwargs.get('page_size'): filters['limit'] = DEFAULT_PAGE_SIZE else: filters['limit'] = kwargs['page_size'] if 'marker' in kwargs: filters['marker'] = kwargs['marker'] sort_key = kwargs.get('sort_key') if sort_key is not None: if sort_key in SORT_KEY_VALUES: filters['sort_key'] = sort_key else: raise ValueError('sort_key must be one of the following: %s.' % ', '.join(SORT_KEY_VALUES)) sort_dir = kwargs.get('sort_dir') if sort_dir is not None: if sort_dir in SORT_DIR_VALUES: filters['sort_dir'] = sort_dir else: raise ValueError('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) for param, value in filters.items(): if isinstance(value, list): filters[param] = encodeutils.safe_encode(','.join(value)) elif isinstance(value, six.string_types): filters[param] = encodeutils.safe_encode(value) url = '/v2/metadefs/namespaces?%s' % parse.urlencode(filters) for namespace, resp in paginate(url): yield namespace, resp @utils.add_req_id_to_object() def delete(self, namespace): """Delete a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp class ResourceTypeController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/resource_type') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def associate(self, namespace, **kwargs): """Associate a resource type with a namespace.""" try: res_type = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = '/v2/metadefs/namespaces/%(namespace)s/resource_types' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=res_type) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_object() def deassociate(self, namespace, resource): """Deassociate a resource type with a namespace.""" url = ('/v2/metadefs/namespaces/%(namespace)s/' 'resource_types/%(resource)s') % { 'namespace': namespace, 'resource': resource} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_generator() def list(self): """Retrieve a listing of available resource types. :returns: generator over list of resource_types """ url = '/v2/metadefs/resource_types' resp, body = self.http_client.get(url) for resource_type in body['resource_types']: yield self.model(**resource_type), resp @utils.add_req_id_to_generator() def get(self, namespace): url = '/v2/metadefs/namespaces/%(namespace)s/resource_types' % { 'namespace': namespace} resp, body = self.http_client.get(url) body.pop('self', None) for resource_type in body['resource_type_associations']: yield self.model(**resource_type), resp class PropertyController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/property') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, namespace, **kwargs): """Create a property. :param namespace: Name of a namespace the property will belong. :param kwargs: Unpacked property object. """ try: prop = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = '/v2/metadefs/namespaces/%(namespace)s/properties' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=prop) body.pop('self', None) return self.model(**body), resp def update(self, namespace, prop_name, **kwargs): """Update a property. :param namespace: Name of a namespace the property belongs. :param prop_name: Name of a property (old one). :param kwargs: Unpacked property object. """ prop = self.get(namespace, prop_name) for (key, value) in kwargs.items(): try: setattr(prop, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = ('/v2/metadefs/namespaces/%(namespace)s/' 'properties/%(prop_name)s') % { 'namespace': namespace, 'prop_name': prop_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=prop.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} return self._get(namespace, prop.name, req_id_hdr) def get(self, namespace, prop_name): return self._get(namespace, prop_name) @utils.add_req_id_to_object() def _get(self, namespace, prop_name, header=None): url = ('/v2/metadefs/namespaces/%(namespace)s/' 'properties/%(prop_name)s') % { 'namespace': namespace, 'prop_name': prop_name} header = header or {} resp, body = self.http_client.get(url, headers=header) body.pop('self', None) body['name'] = prop_name return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, namespace, **kwargs): """Retrieve a listing of metadata properties. :returns: generator over list of objects """ url = '/v2/metadefs/namespaces/%(namespace)s/properties' % { 'namespace': namespace} resp, body = self.http_client.get(url) for key, value in body['properties'].items(): value['name'] = key yield self.model(value), resp @utils.add_req_id_to_object() def delete(self, namespace, prop_name): """Delete a property.""" url = ('/v2/metadefs/namespaces/%(namespace)s/' 'properties/%(prop_name)s') % { 'namespace': namespace, 'prop_name': prop_name} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def delete_all(self, namespace): """Delete all properties in a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s/properties' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp class ObjectController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/object') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, namespace, **kwargs): """Create an object. :param namespace: Name of a namespace the object belongs. :param kwargs: Unpacked object. """ try: obj = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = '/v2/metadefs/namespaces/%(namespace)s/objects' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=obj) body.pop('self', None) return self.model(**body), resp def update(self, namespace, object_name, **kwargs): """Update an object. :param namespace: Name of a namespace the object belongs. :param object_name: Name of an object (old one). :param kwargs: Unpacked object. """ obj = self.get(namespace, object_name) for (key, value) in kwargs.items(): try: setattr(obj, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) # Remove read-only parameters. read_only = ['schema', 'updated_at', 'created_at'] for elem in read_only: if elem in obj: del obj[elem] url = ('/v2/metadefs/namespaces/%(namespace)s/' 'objects/%(object_name)s') % { 'namespace': namespace, 'object_name': object_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=obj.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} return self._get(namespace, obj.name, req_id_hdr) def get(self, namespace, object_name): return self._get(namespace, object_name) @utils.add_req_id_to_object() def _get(self, namespace, object_name, header=None): url = ('/v2/metadefs/namespaces/%(namespace)s/' 'objects/%(object_name)s') % { 'namespace': namespace, 'object_name': object_name} header = header or {} resp, body = self.http_client.get(url, headers=header) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, namespace, **kwargs): """Retrieve a listing of metadata objects. :returns: generator over list of objects """ url = '/v2/metadefs/namespaces/%(namespace)s/objects' % { 'namespace': namespace} resp, body = self.http_client.get(url) for obj in body['objects']: yield self.model(obj), resp @utils.add_req_id_to_object() def delete(self, namespace, object_name): """Delete an object.""" url = ('/v2/metadefs/namespaces/%(namespace)s/' 'objects/%(object_name)s') % { 'namespace': namespace, 'object_name': object_name} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def delete_all(self, namespace): """Delete all objects in a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s/objects' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp class TagController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/tag') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, namespace, tag_name): """Create a tag. :param namespace: Name of a namespace the Tag belongs. :param tag_name: The name of the new tag to create. """ url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} resp, body = self.http_client.post(url) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def create_multiple(self, namespace, **kwargs): """Create the list of tags. :param namespace: Name of a namespace to which the Tags belong. :param kwargs: list of tags. """ tag_names = kwargs.pop('tags', []) md_tag_list = [] for tag_name in tag_names: try: md_tag_list.append(self.model(name=tag_name)) except (warlock.InvalidOperation) as e: raise TypeError(encodeutils.exception_to_unicode(e)) tags = {'tags': md_tag_list} url = '/v2/metadefs/namespaces/%(namespace)s/tags' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=tags) body.pop('self', None) for tag in body['tags']: yield self.model(tag), resp def update(self, namespace, tag_name, **kwargs): """Update a tag. :param namespace: Name of a namespace the Tag belongs. :param tag_name: Name of the Tag (old one). :param kwargs: Unpacked tag. """ tag = self.get(namespace, tag_name) for (key, value) in kwargs.items(): try: setattr(tag, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) # Remove read-only parameters. read_only = ['updated_at', 'created_at'] for elem in read_only: if elem in tag: del tag[elem] url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=tag.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} return self._get(namespace, tag.name, req_id_hdr) def get(self, namespace, tag_name): return self._get(namespace, tag_name) @utils.add_req_id_to_object() def _get(self, namespace, tag_name, header=None): url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} header = header or {} resp, body = self.http_client.get(url, headers=header) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, namespace, **kwargs): """Retrieve a listing of metadata tags. :returns: generator over list of tags. """ url = '/v2/metadefs/namespaces/%(namespace)s/tags' % { 'namespace': namespace} resp, body = self.http_client.get(url) for tag in body['tags']: yield self.model(tag), resp @utils.add_req_id_to_object() def delete(self, namespace, tag_name): """Delete a tag.""" url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def delete_all(self, namespace): """Delete all tags in a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s/tags' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp ��������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/image_schema.py�������������������������������������������0000666�0001751�0001751�00000016342�13232161652�023462� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2015 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. _doc_url = "http://docs.openstack.org/user-guide/common/cli-manage-images.html" # noqa # NOTE(flaper87): Keep a copy of the current default schema so that # we can react on cases where there's no connection to an OpenStack # deployment. See #1481729 _BASE_SCHEMA = { "additionalProperties": { "type": "string" }, "name": "image", "links": [{ "href": "{self}", "rel": "self" }, { "href": "{file}", "rel": "enclosure" }, { "href": "{schema}", "rel": "describedby" }], "properties": { "container_format": { "enum": [None, "ami", "ari", "aki", "bare", "ovf", "ova", "docker"], "type": ["null", "string"], "description": "Format of the container" }, "min_ram": { "type": "integer", "description": "Amount of ram (in MB) required to boot image." }, "ramdisk_id": { "pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){12}$"), "type": ["null", "string"], "description": ("ID of image stored in Glance that should be " "used as the ramdisk when booting an AMI-style " "image."), "is_base": False }, "locations": { "items": { "required": ["url", "metadata"], "type": "object", "properties": { "url": { "type": "string", "maxLength": 255 }, "metadata": { "type": "object" } } }, "type": "array", "description": ("A set of URLs to access the image " "file kept in external store") }, "file": { "readOnly": True, "type": "string", "description": "An image file url" }, "owner": { "type": ["null", "string"], "description": "Owner of the image", "maxLength": 255 }, "id": { "pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){12}$"), "type": "string", "description": "An identifier for the image" }, "size": { "readOnly": True, "type": ["null", "integer"], "description": "Size of image file in bytes" }, "os_distro": { "type": "string", "description": ("Common name of operating system distribution " "as specified in %s" % _doc_url), "is_base": False }, "self": { "readOnly": True, "type": "string", "description": "An image self url" }, "disk_format": { "enum": [None, "ami", "ari", "aki", "vhd", "vhdx", "vmdk", "raw", "qcow2", "vdi", "iso", "ploop"], "type": ["null", "string"], "description": "Format of the disk" }, "os_version": { "type": "string", "description": "Operating system version as specified by the" " distributor", "is_base": False }, "direct_url": { "readOnly": True, "type": "string", "description": "URL to access the image file kept in external" " store" }, "schema": { "readOnly": True, "type": "string", "description": "An image schema url" }, "status": { "readOnly": True, "enum": ["queued", "saving", "active", "killed", "deleted", "pending_delete", "deactivated"], "type": "string", "description": "Status of the image" }, "tags": { "items": { "type": "string", "maxLength": 255 }, "type": "array", "description": "List of strings related to the image" }, "kernel_id": { "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": ["null", "string"], "description": "ID of image stored in Glance that should be " "used as the kernel when booting an " "AMI-style image.", "is_base": False }, "visibility": { "enum": ["public", "private", "community", "shared"], "type": "string", "description": "Scope of image accessibility" }, "updated_at": { "readOnly": True, "type": "string", "description": "Date and time of the last image modification" }, "min_disk": { "type": "integer", "description": "Amount of disk space (in GB) required to boot " "image." }, "virtual_size": { "readOnly": True, "type": ["null", "integer"], "description": "Virtual size of image in bytes" }, "instance_uuid": { "type": "string", "description": "Metadata which can be used to record which " "instance this image is associated with. " "(Informational only, does not create an " "instance snapshot.)", "is_base": False }, "name": { "type": ["null", "string"], "description": "Descriptive name for the image", "maxLength": 255 }, "checksum": { "readOnly": True, "type": ["null", "string"], "description": "md5 hash of image contents.", "maxLength": 32 }, "created_at": { "readOnly": True, "type": "string", "description": "Date and time of image registration" }, "protected": { "type": "boolean", "description": "If true, image will not be deletable." }, "architecture": { "type": "string", "description": ("Operating system architecture as specified " "in %s" % _doc_url), "is_base": False } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/tasks.py��������������������������������������������������0000666�0001751�0001751�00000010463�13232161652�022203� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2013 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. from oslo_utils import encodeutils import six import warlock from glanceclient.common import utils from glanceclient.v2 import schemas DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('id', 'type', 'status') class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('task') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_generator() def list(self, **kwargs): """Retrieve a listing of Task objects. :param page_size: Number of tasks to request in each paginated request :returns: generator over list of Tasks """ def paginate(url): resp, body = self.http_client.get(url) for task in body['tasks']: yield task, resp try: next_url = body['next'] except KeyError: return else: for task, resp in paginate(next_url): yield task, resp filters = kwargs.get('filters', {}) if not kwargs.get('page_size'): filters['limit'] = DEFAULT_PAGE_SIZE else: filters['limit'] = kwargs['page_size'] if 'marker' in kwargs: filters['marker'] = kwargs['marker'] sort_key = kwargs.get('sort_key') if sort_key is not None: if sort_key in SORT_KEY_VALUES: filters['sort_key'] = sort_key else: raise ValueError('sort_key must be one of the following: %s.' % ', '.join(SORT_KEY_VALUES)) sort_dir = kwargs.get('sort_dir') if sort_dir is not None: if sort_dir in SORT_DIR_VALUES: filters['sort_dir'] = sort_dir else: raise ValueError('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) for param, value in filters.items(): if isinstance(value, six.string_types): filters[param] = encodeutils.safe_encode(value) url = '/v2/tasks?%s' % six.moves.urllib.parse.urlencode(filters) for task, resp in paginate(url): # NOTE(flwang): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict task.pop('self', None) yield self.model(**task), resp @utils.add_req_id_to_object() def get(self, task_id): """Get a task based on given task id.""" url = '/v2/tasks/%s' % task_id resp, body = self.http_client.get(url) # NOTE(flwang): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_object() def create(self, **kwargs): """Create a new task.""" url = '/v2/tasks' task = self.model() for (key, value) in kwargs.items(): try: setattr(task, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) resp, body = self.http_client.post(url, data=task) # NOTE(flwang): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/resource_type_schema.py�����������������������������������0000666�0001751�0001751�00000005344�13232161652�025270� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2015 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. # NOTE(flaper87): Keep a copy of the current default schema so that # we can react on cases where there's no connection to an OpenStack # deployment. See #1481729 BASE_SCHEMA = { "additionalProperties": False, "required": ["name"], "name": "resource_type_association", "properties": { "name": { "type": "string", "description": "Resource type names should be aligned with Heat " "resource types whenever possible: http://docs." "openstack.org/developer/heat/template_guide/" "openstack.html", "maxLength": 80 }, "prefix": { "type": "string", "description": "Specifies the prefix to use for the given resource" " type. Any properties in the namespace should be" " prefixed with this prefix when being applied to" " the specified resource type. Must include prefix" " separator (e.g. a colon :).", "maxLength": 80 }, "properties_target": { "type": "string", "description": "Some resource types allow more than one key / " "value pair per instance. For example, Cinder " "allows user and image metadata on volumes. Only " "the image properties metadata is evaluated by Nova" " (scheduling or drivers). This property allows a " "namespace target to remove the ambiguity.", "maxLength": 80 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of resource type association.", "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last resource type " "association modification.", "format": "date-time" } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/namespace_schema.py���������������������������������������0000666�0001751�0001751�00000017274�13232161652�024341� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2015 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. # NOTE(flaper87): Keep a copy of the current default schema so that # we can react on cases where there's no connection to an OpenStack # deployment. See #1481729 BASE_SCHEMA = { "additionalProperties": False, "definitions": { "positiveInteger": { "minimum": 0, "type": "integer" }, "positiveIntegerDefault0": { "allOf": [ {"$ref": "#/definitions/positiveInteger"}, {"default": 0} ] }, "stringArray": { "type": "array", "items": {"type": "string"}, "uniqueItems": True }, "property": { "type": "object", "additionalProperties": { "type": "object", "required": ["title", "type"], "properties": { "name": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "operators": { "type": "array", "items": { "type": "string" } }, "type": { "type": "string", "enum": [ "array", "boolean", "integer", "number", "object", "string", None ] }, "required": { "$ref": "#/definitions/stringArray" }, "minimum": { "type": "number" }, "maximum": { "type": "number" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "enum": { "type": "array" }, "readonly": { "type": "boolean" }, "default": {}, "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "array", "boolean", "integer", "number", "object", "string", None ] }, "enum": { "type": "array" } } }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": False }, "additionalItems": { "type": "boolean" }, } } } }, "required": ["namespace"], "name": "namespace", "properties": { "namespace": { "type": "string", "description": "The unique namespace text.", "maxLength": 80 }, "display_name": { "type": "string", "description": "The user friendly name for the namespace. Used by " "UI if available.", "maxLength": 80 }, "description": { "type": "string", "description": "Provides a user friendly description of the " "namespace.", "maxLength": 500 }, "visibility": { "enum": [ "public", "private" ], "type": "string", "description": "Scope of namespace accessibility." }, "protected": { "type": "boolean", "description": "If true, namespace will not be deletable." }, "owner": { "type": "string", "description": "Owner of the namespace.", "maxLength": 255 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of namespace creation.", "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last namespace modification.", "format": "date-time" }, "schema": { "readOnly": True, "type": "string" }, "self": { "readOnly": True, "type": "string" }, "resource_type_associations": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "prefix": { "type": "string" }, "properties_target": { "type": "string" } } } }, "properties": { "$ref": "#/definitions/property" }, "objects": { "items": { "type": "object", "properties": { "required": { "$ref": "#/definitions/stringArray" }, "description": { "type": "string" }, "name": { "type": "string" }, "properties": { "$ref": "#/definitions/property" } } }, "type": "array" }, "tags": { "items": { "type": "object", "properties": { "name": { "type": "string" } } }, "type": "array" }, } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/image_tags.py���������������������������������������������0000666�0001751�0001751�00000003574�13232161652�023163� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2013 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 warlock from glanceclient.common import utils from glanceclient.v2 import schemas class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('image') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def update(self, image_id, tag_value): """Update an image with the given tag. :param image_id: image to be updated with the given tag. :param tag_value: value of the tag. """ url = '/v2/images/%s/tags/%s' % (image_id, tag_value) resp, body = self.http_client.put(url) return (resp, body), resp @utils.add_req_id_to_object() def delete(self, image_id, tag_value): """Delete the tag associated with the given image. :param image_id: Image whose tag to be deleted. :param tag_value: tag value to be deleted. """ url = '/v2/images/%s/tags/%s' % (image_id, tag_value) resp, body = self.http_client.delete(url) return (resp, body), resp ������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/client.py�������������������������������������������������0000666�0001751�0001751�00000005313�13232161652�022332� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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. from glanceclient.common import http from glanceclient.common import utils from glanceclient.v2 import image_members from glanceclient.v2 import image_tags from glanceclient.v2 import images from glanceclient.v2 import metadefs from glanceclient.v2 import schemas from glanceclient.v2 import tasks from glanceclient.v2 import versions class Client(object): """Client for the OpenStack Images v2 API. :param string endpoint: A user-supplied endpoint URL for the glance service. :param string token: Token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) :param string language_header: Set Accept-Language header to be sent in requests to glance. """ def __init__(self, endpoint=None, **kwargs): endpoint, self.version = utils.endpoint_version_from_url(endpoint, 2.0) self.http_client = http.get_http_client(endpoint=endpoint, **kwargs) self.schemas = schemas.Controller(self.http_client) self.images = images.Controller(self.http_client, self.schemas) self.image_tags = image_tags.Controller(self.http_client, self.schemas) self.image_members = image_members.Controller(self.http_client, self.schemas) self.tasks = tasks.Controller(self.http_client, self.schemas) self.metadefs_resource_type = ( metadefs.ResourceTypeController(self.http_client, self.schemas)) self.metadefs_property = ( metadefs.PropertyController(self.http_client, self.schemas)) self.metadefs_object = ( metadefs.ObjectController(self.http_client, self.schemas)) self.metadefs_tag = ( metadefs.TagController(self.http_client, self.schemas)) self.metadefs_namespace = ( metadefs.NamespaceController(self.http_client, self.schemas)) self.versions = versions.VersionController(self.http_client) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/v2/images.py�������������������������������������������������0000666�0001751�0001751�00000041706�13232161652�022327� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 json from oslo_utils import encodeutils from requests import codes import six from six.moves.urllib import parse import warlock from glanceclient.common import utils from glanceclient import exc from glanceclient.v2 import schemas DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('image') warlock_model = warlock.model_factory( schema.raw(), base_class=schemas.SchemaBasedModel) return warlock_model @utils.memoized_property def unvalidated_model(self): """A model which does not validate the image against the v2 schema.""" schema = self.schema_client.get('image') warlock_model = warlock.model_factory( schema.raw(), base_class=schemas.SchemaBasedModel) warlock_model.validate = lambda *args, **kwargs: None return warlock_model @staticmethod def _wrap(value): if isinstance(value, six.string_types): return [value] return value @staticmethod def _validate_sort_param(sort): """Validates sorting argument for invalid keys and directions values. :param sort: comma-separated list of sort keys with optional <:dir> after each key """ for sort_param in sort.strip().split(','): key, _sep, dir = sort_param.partition(':') if dir and dir not in SORT_DIR_VALUES: msg = ('Invalid sort direction: %(sort_dir)s.' ' It must be one of the following: %(available)s.' ) % {'sort_dir': dir, 'available': ', '.join(SORT_DIR_VALUES)} raise exc.HTTPBadRequest(msg) if key not in SORT_KEY_VALUES: msg = ('Invalid sort key: %(sort_key)s.' ' It must be one of the following: %(available)s.' ) % {'sort_key': key, 'available': ', '.join(SORT_KEY_VALUES)} raise exc.HTTPBadRequest(msg) return sort @utils.add_req_id_to_generator() def list(self, **kwargs): """Retrieve a listing of Image objects. :param page_size: Number of images to request in each paginated request. :returns: generator over list of Images. """ limit = kwargs.get('limit') # NOTE(flaper87): Don't use `get('page_size', DEFAULT_SIZE)` otherwise, # it could be possible to send invalid data to the server by passing # page_size=None. page_size = kwargs.get('page_size') or DEFAULT_PAGE_SIZE def paginate(url, page_size, limit=None): next_url = url req_id_hdr = {} while True: if limit and page_size > limit: # NOTE(flaper87): Avoid requesting 2000 images when limit # is 1 next_url = next_url.replace("limit=%s" % page_size, "limit=%s" % limit) resp, body = self.http_client.get(next_url, headers=req_id_hdr) # NOTE(rsjethani): Store curent request id so that it can be # used in subsequent requests. Refer bug #1525259 req_id_hdr['x-openstack-request-id'] = \ utils._extract_request_id(resp) for image in body['images']: # NOTE(bcwaldon): remove 'self' for now until we have # an elegant way to pass it into the model constructor # without conflict. image.pop('self', None) # We do not validate the model when listing. # This prevents side-effects of injecting invalid # schema values via v1. yield self.unvalidated_model(**image), resp if limit: limit -= 1 if limit <= 0: raise StopIteration try: next_url = body['next'] except KeyError: return filters = kwargs.get('filters', {}) # NOTE(flaper87): We paginate in the client, hence we use # the page_size as Glance's limit. filters['limit'] = page_size tags = filters.pop('tag', []) tags_url_params = [] for tag in tags: if not isinstance(tag, six.string_types): raise exc.HTTPBadRequest("Invalid tag value %s" % tag) tags_url_params.append({'tag': encodeutils.safe_encode(tag)}) for param, value in filters.items(): if isinstance(value, six.string_types): filters[param] = encodeutils.safe_encode(value) url = '/v2/images?%s' % parse.urlencode(filters) for param in tags_url_params: url = '%s&%s' % (url, parse.urlencode(param)) if 'sort' in kwargs: if 'sort_key' in kwargs or 'sort_dir' in kwargs: raise exc.HTTPBadRequest("The 'sort' argument is not supported" " with 'sort_key' or 'sort_dir'.") url = '%s&sort=%s' % (url, self._validate_sort_param( kwargs['sort'])) else: sort_dir = self._wrap(kwargs.get('sort_dir', [])) sort_key = self._wrap(kwargs.get('sort_key', [])) if len(sort_key) != len(sort_dir) and len(sort_dir) > 1: raise exc.HTTPBadRequest( "Unexpected number of sort directions: " "either provide a single sort direction or an equal " "number of sort keys and sort directions.") for key in sort_key: url = '%s&sort_key=%s' % (url, key) for dir in sort_dir: url = '%s&sort_dir=%s' % (url, dir) if isinstance(kwargs.get('marker'), six.string_types): url = '%s&marker=%s' % (url, kwargs['marker']) for image, resp in paginate(url, page_size, limit): yield image, resp @utils.add_req_id_to_object() def _get(self, image_id, header=None): url = '/v2/images/%s' % image_id header = header or {} resp, body = self.http_client.get(url, headers=header) # NOTE(bcwaldon): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.unvalidated_model(**body), resp def get(self, image_id): return self._get(image_id) @utils.add_req_id_to_object() def data(self, image_id, do_checksum=True): """Retrieve data of an image. :param image_id: ID of the image to download. :param do_checksum: Enable/disable checksum validation. :returns: An iterable body or None """ url = '/v2/images/%s/file' % image_id resp, body = self.http_client.get(url) if resp.status_code == codes.no_content: return None, resp checksum = resp.headers.get('content-md5', None) content_length = int(resp.headers.get('content-length', 0)) if do_checksum and checksum is not None: body = utils.integrity_iter(body, checksum) return utils.IterableWithLength(body, content_length), resp @utils.add_req_id_to_object() def upload(self, image_id, image_data, image_size=None, u_url=None): """Upload the data for an image. :param image_id: ID of the image to upload data for. :param image_data: File-like object supplying the data to upload. :param image_size: Unused - present for backwards compatibility :param u_url: Upload url to upload the data to. """ url = u_url or '/v2/images/%s/file' % image_id hdrs = {'Content-Type': 'application/octet-stream'} body = image_data resp, body = self.http_client.put(url, headers=hdrs, data=body) return (resp, body), resp @utils.add_req_id_to_object() def get_import_info(self): """Get Import info from discovery endpoint.""" url = '/v2/info/import' resp, body = self.http_client.get(url) return body, resp @utils.add_req_id_to_object() def stage(self, image_id, image_data, image_size=None): """Upload the data to image staging. :param image_id: ID of the image to upload data for. :param image_data: File-like object supplying the data to upload. :param image_size: Unused - present for backwards compatibility """ url = '/v2/images/%s/stage' % image_id resp, body = self.upload(image_id, image_data, u_url=url) return body, resp @utils.add_req_id_to_object() def image_import(self, image_id, method='glance-direct'): """Import Image via method.""" url = '/v2/images/%s/import' % image_id data = {'method': {'name': method}} resp, body = self.http_client.post(url, data=data) return body, resp @utils.add_req_id_to_object() def delete(self, image_id): """Delete an image.""" url = '/v2/images/%s' % image_id resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def create(self, **kwargs): """Create an image.""" url = '/v2/images' image = self.model() for (key, value) in kwargs.items(): try: setattr(image, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) resp, body = self.http_client.post(url, data=image) # NOTE(esheffield): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_object() def deactivate(self, image_id): """Deactivate an image.""" url = '/v2/images/%s/actions/deactivate' % image_id resp, body = self.http_client.post(url) return (resp, body), resp @utils.add_req_id_to_object() def reactivate(self, image_id): """Reactivate an image.""" url = '/v2/images/%s/actions/reactivate' % image_id resp, body = self.http_client.post(url) return (resp, body), resp def update(self, image_id, remove_props=None, **kwargs): """Update attributes of an image. :param image_id: ID of the image to modify. :param remove_props: List of property names to remove :param kwargs: Image attribute names and their new values. """ unvalidated_image = self.get(image_id) image = self.model(**unvalidated_image) for (key, value) in kwargs.items(): try: setattr(image, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) if remove_props: cur_props = image.keys() new_props = kwargs.keys() # NOTE(esheffield): Only remove props that currently exist on the # image and are NOT in the properties being updated / added props_to_remove = set(cur_props).intersection( set(remove_props).difference(new_props)) for key in props_to_remove: delattr(image, key) url = '/v2/images/%s' % image_id hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'} resp, _ = self.http_client.patch(url, headers=hdrs, data=image.patch) # Get request id from `patch` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} # NOTE(bcwaldon): calling image.patch doesn't clear the changes, so # we need to fetch the image again to get a clean history. This is # an obvious optimization for warlock return self._get(image_id, req_id_hdr) def _get_image_with_locations_or_fail(self, image_id): image = self.get(image_id) if getattr(image, 'locations', None) is None: raise exc.HTTPBadRequest('The administrator has disabled ' 'API access to image locations') return image @utils.add_req_id_to_object() def _send_image_update_request(self, image_id, patch_body): url = '/v2/images/%s' % image_id hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'} resp, body = self.http_client.patch(url, headers=hdrs, data=json.dumps(patch_body)) return (resp, body), resp def add_location(self, image_id, url, metadata): """Add a new location entry to an image's list of locations. It is an error to add a URL that is already present in the list of locations. :param image_id: ID of image to which the location is to be added. :param url: URL of the location to add. :param metadata: Metadata associated with the location. :returns: The updated image """ add_patch = [{'op': 'add', 'path': '/locations/-', 'value': {'url': url, 'metadata': metadata}}] response = self._send_image_update_request(image_id, add_patch) # Get request id from the above update request and pass the same to # following get request req_id_hdr = {'x-openstack-request-id': response.request_ids[0]} return self._get(image_id, req_id_hdr) def delete_locations(self, image_id, url_set): """Remove one or more location entries of an image. :param image_id: ID of image from which locations are to be removed. :param url_set: set of URLs of location entries to remove. :returns: None """ image = self._get_image_with_locations_or_fail(image_id) current_urls = [l['url'] for l in image.locations] missing_locs = url_set.difference(set(current_urls)) if missing_locs: raise exc.HTTPNotFound('Unknown URL(s): %s' % list(missing_locs)) # NOTE: warlock doesn't generate the most efficient patch for remove # operations (it shifts everything up and deletes the tail elements) so # we do it ourselves. url_indices = [current_urls.index(url) for url in url_set] url_indices.sort(reverse=True) patches = [{'op': 'remove', 'path': '/locations/%s' % url_idx} for url_idx in url_indices] return self._send_image_update_request(image_id, patches) def update_location(self, image_id, url, metadata): """Update an existing location entry in an image's list of locations. The URL specified must be already present in the image's list of locations. :param image_id: ID of image whose location is to be updated. :param url: URL of the location to update. :param metadata: Metadata associated with the location. :returns: The updated image """ image = self._get_image_with_locations_or_fail(image_id) url_map = dict([(l['url'], l) for l in image.locations]) if url not in url_map: raise exc.HTTPNotFound('Unknown URL: %s, the URL must be one of' ' existing locations of current image' % url) if url_map[url]['metadata'] == metadata: return image url_map[url]['metadata'] = metadata patches = [{'op': 'replace', 'path': '/locations', 'value': list(url_map.values())}] response = self._send_image_update_request(image_id, patches) # Get request id from the above update request and pass the same to # following get request req_id_hdr = {'x-openstack-request-id': response.request_ids[0]} return self._get(image_id, req_id_hdr) ����������������������������������������������������������python-glanceclient-2.9.1/glanceclient/tests/�������������������������������������������������������0000775�0001751�0001751�00000000000�13232162126�021306� 5����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/tests/unit/��������������������������������������������������0000775�0001751�0001751�00000000000�13232162126�022265� 5����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/glanceclient/tests/unit/test_exc.py���������������������������������������0000666�0001751�0001751�00000005201�13232161651�024457� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 mock import testtools from glanceclient import exc HTML_MSG = """<html> <head> <title>404 Entity Not Found

404 Entity Not Found

Entity could not be found

""" class TestHTTPExceptions(testtools.TestCase): def test_from_response(self): """exc.from_response should return instance of an HTTP exception.""" mock_resp = mock.Mock() mock_resp.status_code = 400 out = exc.from_response(mock_resp) self.assertIsInstance(out, exc.HTTPBadRequest) def test_handles_json(self): """exc.from_response should not print JSON.""" mock_resp = mock.Mock() mock_resp.status_code = 413 mock_resp.json.return_value = { "overLimit": { "code": 413, "message": "OverLimit Retry...", "details": "Error Details...", "retryAt": "2014-12-03T13:33:06Z" } } mock_resp.headers = { "content-type": "application/json" } err = exc.from_response(mock_resp, "Non-empty body") self.assertIsInstance(err, exc.HTTPOverLimit) self.assertEqual("OverLimit Retry...", err.details) def test_handles_html(self): """exc.from_response should not print HTML.""" mock_resp = mock.Mock() mock_resp.status_code = 404 mock_resp.text = HTML_MSG mock_resp.headers = { "content-type": "text/html" } err = exc.from_response(mock_resp, HTML_MSG) self.assertIsInstance(err, exc.HTTPNotFound) self.assertEqual("404 Entity Not Found: Entity could not be found", err.details) def test_format_no_content_type(self): mock_resp = mock.Mock() mock_resp.status_code = 400 mock_resp.headers = {'content-type': 'application/octet-stream'} body = b'Error \n\n' err = exc.from_response(mock_resp, body) self.assertEqual('Error \n', err.details) python-glanceclient-2.9.1/glanceclient/tests/unit/test_base.py0000666000175100017510000000474013232161651024621 0ustar zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! 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 testtools from glanceclient.v1.apiclient import base class TestBase(testtools.TestCase): def test_resource_repr(self): r = base.Resource(None, dict(foo="bar", baz="spam")) self.assertEqual("", repr(r)) def test_getid(self): self.assertEqual(4, base.getid(4)) class TmpObject(object): id = 4 self.assertEqual(4, base.getid(TmpObject)) def test_two_resources_with_same_id_are_not_equal(self): # Two resources with same ID: never equal if their info is not equal r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertNotEqual(r1, r2) def test_two_resources_with_same_id_and_info_are_equal(self): # Two resources with same ID: equal if their info is equal r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertEqual(r1, r2) def test_two_resources_with_eq_info_are_equal(self): # Two resources with no ID: equal if their info is equal r1 = base.Resource(None, {'name': 'joe', 'age': 12}) r2 = base.Resource(None, {'name': 'joe', 'age': 12}) self.assertEqual(r1, r2) def test_two_resources_with_diff_id_are_not_equal(self): # Two resources with diff ID: not equal r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) r2 = base.Resource(None, {'id': 2, 'name': 'hello'}) self.assertNotEqual(r1, r2) def test_two_resources_with_not_eq_info_are_not_equal(self): # Two resources with no ID: not equal if their info is not equal r1 = base.Resource(None, {'name': 'bill', 'age': 21}) r2 = base.Resource(None, {'name': 'joe', 'age': 12}) self.assertNotEqual(r1, r2) python-glanceclient-2.9.1/glanceclient/tests/unit/test_progressbar.py0000666000175100017510000000570113232161651026236 0ustar zuulzuul00000000000000# Copyright 2013 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 sys import requests import six import testtools from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient.tests import utils as test_utils class TestProgressBarWrapper(testtools.TestCase): def test_iter_iterator_display_progress_bar(self): size = 100 # create fake response object to return request-id with iterator resp = requests.Response() resp.headers['x-openstack-request-id'] = 'req-1234' iterator_with_len = utils.IterableWithLength(iter('X' * 100), size) requestid_proxy = utils.RequestIdProxy((iterator_with_len, resp)) saved_stdout = sys.stdout try: sys.stdout = output = test_utils.FakeTTYStdout() # Consume iterator. data = list(progressbar.VerboseIteratorWrapper(requestid_proxy, size)) self.assertEqual(['X'] * 100, data) self.assertEqual( '[%s>] 100%%\n' % ('=' * 29), output.getvalue() ) finally: sys.stdout = saved_stdout def test_iter_file_display_progress_bar(self): size = 98304 file_obj = six.StringIO('X' * size) saved_stdout = sys.stdout try: sys.stdout = output = test_utils.FakeTTYStdout() file_obj = progressbar.VerboseFileWrapper(file_obj, size) chunksize = 1024 chunk = file_obj.read(chunksize) while chunk: chunk = file_obj.read(chunksize) self.assertEqual( '[%s>] 100%%\n' % ('=' * 29), output.getvalue() ) finally: sys.stdout = saved_stdout def test_iter_file_no_tty(self): size = 98304 file_obj = six.StringIO('X' * size) saved_stdout = sys.stdout try: sys.stdout = output = test_utils.FakeNoTTYStdout() file_obj = progressbar.VerboseFileWrapper(file_obj, size) chunksize = 1024 chunk = file_obj.read(chunksize) while chunk: chunk = file_obj.read(chunksize) # If stdout is not a tty progress bar should do nothing. self.assertEqual('', output.getvalue()) finally: sys.stdout = saved_stdout python-glanceclient-2.9.1/glanceclient/tests/unit/var/0000775000175100017510000000000013232162126023055 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/var/expired-cert.crt0000666000175100017510000000417313232161652026174 0ustar zuulzuul00000000000000-----BEGIN CERTIFICATE----- MIIGFTCCA/2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE1MTcwNjMzWhcNMTIx MTE2MTcwNjMzWjCBqDEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE BhMCVVMxHjAcBgNVBAMTFW9wZW5zdGFjay5leGFtcGxlLmNvbTCCAiIwDQYJKoZI hvcNAQEBBQADggIPADCCAgoCggIBANn9w82sGN+iALSlZ5/Odd5iJ3MAJ5BoalMG kfUECGMewd7lE5+6ok1+vqVbYjd+F56aSkIJFR/ck51EYG2diGM5E5zjdiLcyB9l dKB5PmaB2P9dHyomy+sMONqhw5uEsWKIfPbtjzGRhjJL0bIYwptGr4JPraZy8R3d HWbTO3SlnFkjHHtfoKuZtRJq5OD1hXM8J9IEsBC90zw7RWCTw1iKllLfKITPUi7O i8ITjUyTVKR2e56XRtmxGgGsGyZpcYrmhRuLo9jyL9m3VuNzsfwDvCqn7cnZIOQa VO4hNZdO+33PINCC+YVNOGYwqfBuKxYvHJSbMfOZ6JDK98v65pWLBN7PObYIjQFH uJyK5DuQMqvyRIcrtfLUalepD+PQaCn4ajgXjpqBz4t0pMte8jh0i4clLwvT0elT PtA+MMos3hIGjJgEHTvLdCff9qlkjHlW7lg45PYn7S0Z7dqtBWD7Ys2B+AWp/skt hRr7YZeegLfHVJVkMFL6Ojs98161W2FLmEA+5nejzjx7kWlJsg9aZPbBnN87m6iK RHI+VkqSpBHm10iMlp4Nn30RtOj0wQhxoZjtEouGeRobHN5ULwpAfNEpKMMZf5bt 604JjOP9Pn+WzsvzGDeXjgxUP55PIR+EpHkvS5h1YQ+9RV5J669e2J9T4gnc0Abg t3jJvtp1AgMBAAGjODA2MDQGA1UdEQQtMCuCEGFsdDEuZXhhbXBsZS5jb22BDm9z QGV4YW1wbGUuY29tggcwLjAuMC4wMA0GCSqGSIb3DQEBBQUAA4ICAQBkKUA4lhsS zjcuh77wtAIP9SN5Se4CheTRDXKDeuwWB6VQDzdJdtqSnWNF6sVEA97vhNTSjaBD hfrtX9FZ+ImADlOf01t4Dakhsmje/DEPiQHaCy9P5fGtGIGRlWUyTmyQoV1LDLM5 wgB1V5Oz2iDat2AdvUb0OFP0O1M887OgPpfUDQJEUTVAs5JS+6P/6RPyFh/dHWiX UGoM0nMvTwsLWT4CZ9NdIChecVwBFqXjNytPY53tKbCWp77d/oGUg5Pb6EBD3xSW AeMJ6PuafDRgm/He8nOtZnUd+53Ha59yzSGnSopu5WqrUa/xD+ZiK6dX7LsH/M8y Hz0rh7w22qNHUxNaC3hrhx1BxX4au6z4kpKXIlAWH7ViRzVZ8XkwqqrndqWPWOFk 1emLLJ1dfT8FXdgpHenkUiktAf5qZhUWbF6nr9at+c4T7ZrLHSekux2r29kD9BJw O2gSSclxKlMPwirUC0P4J/2WP72kCbf6AEfKU2siT12E6/xOmgen9lVYKckBiLbb rJ97L1ieJI8GZTGExjtE9Lo+XVsv28D2XLU8vNCODs0xPZCr2TLNS/6YcnVy6594 vpvU7fbNFAyxG4sjQC0wHoN6rn+kd1kzfprmBHKTx3W7y+hzjb+W7iS2EZn20k+N l3+dFHnWayuCdqcFwIl3m8i8FupFihz9+A== -----END CERTIFICATE----- python-glanceclient-2.9.1/glanceclient/tests/unit/var/ca.crt0000666000175100017510000000410613232161652024160 0ustar zuulzuul00000000000000-----BEGIN CERTIFICATE----- MIIF7jCCA9YCCQDbl9qx7iIeJDANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQ T3BlbnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAh BgkqhkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0 ZSBDQTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3Rh Y2sgVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE2MTI1MDE2WhcN NDAwNDAzMTI1MDE2WjCBuDEZMBcGA1UEChMQT3BlbnN0YWNrIENBIE9yZzEaMBgG A1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluQGNh LmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBDQTELMAkGA1UECBMCQ0ExCzAJ BgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sgVGVzdCBDZXJ0aWZpY2F0ZSBB dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC94cpBjwj2 MD0w5j1Jlcy8Ljmk3r7CRaoV5vhWUrAWpT7Thxr/Ti0qAfZZRSIVpvBM0RlseH0Q toUJixuYMoNRPUQ74r/TRoO8HfjQDJfnXtWg2L7DRP8p4Zgj3vByBUCU+rKsbI/H Nssl/AronADbZXCoL5hJRN8euMYZGrt/Gh1ZotKE5gQlEjylDFlA3s3pn+ABLgzf 7L7iufwV3zLdPRHCb6Ve8YvUmKfI6gy+WwTRhNhLz4Nj0uBthnj6QhnRXtxkNT7A aAStqKH6TtYRnk2Owh8ITFbtLQ0/MSV8jHAxMXx9AloBhEKxv3cIpgLH6lOCnj// Ql+H6/QWtmTUHzP1kBfMhTQnWTfR92QTcgEMiZ7a07VyVtLh+kp/G5IUqpM6Pyz/ O6QDs7FF69bTpws7Ce916PPrGFZ9Gqvo/P0jXge8kYqO+a8QnTRldAxdUzPJCK9+ Dyi2LWeHf8nPFYdwW9Ov6Jw1CKDYxjJg6KIwnrMPa2eUdPB6/OKkqr9/KemOoKQu 4KSaYadFZbaJwt7JPZaHy6TpkGxW7Af8RqGrW6a6nWEFcfO2POuHcAHWL5LiRmni unm60DBF3b3itDTqCvER3mZE9pN8dqtxdpB8SUX8eq0UJJK2K8mJQS+oE9crbqYb 1kQbYjhhPLlvOQru+/m/abqZrC04u2OtYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IC AQA8wGVBbzfpQ3eYpchiHyHF9N5LIhr6Bt4jYDKLz8DIbElLtoOlgH/v7hLGJ7wu R9OteonwQ1qr9umMmnp61bKXOEBJLBJbGKEt0MNLmmX89+M/h3rdMVZEz/Hht/xK Xm4di8pjkHfmdhqsbiFW81lAt9W1r74lnH7wQHr9ueALGKDx0hi8pAZ27itgQVHL eA1erhw0kjr9BqWpDIskVwePcD7pFoZ48GQlST0uIEq5U+1AWq7AbOABsqODygKi Ri5pmTasNFT7nEX3ti4VN214MNy0JnPzTRNWR2rD0I30AebM3KkzTprbLVfnGkm4 7hOPV+Wc8EjgbbrUAIp2YpOfO/9nbgljTOUsqfjqxzvHx/09XOo2M6NIE5UiHqIq TXN7CeGIhBoYbvBAH2QvtveFXv41IYL4zFFXo4wTBSzCCOUGeDDv0U4hhsNaCkDQ G2TcubNA4g/FAtqLvPj/6VbIIgFE/1/6acsT+W0O+kkVAb7ej2dpI7J+jKXDXuiA PDCMn9dVQ7oAcaQvVdvvRphLdIZ9wHgqKhxKsMwzIMExuDKL0lWe/3sueFyol6nv xRCSgzr5MqSObbO3EnWgcUocBvlPyYLnTM2T8C5wh3BGnJXqJSRETggNn8PXBVIm +c5o+Ic0mYu4v8P1ZSozFdgf+HLriVPwzJU5dHvvTEu7sw== -----END CERTIFICATE----- python-glanceclient-2.9.1/glanceclient/tests/unit/var/certificate.crt0000666000175100017510000000661413232161652026065 0ustar zuulzuul00000000000000# Certificate: # Data: # Version: 3 (0x2) # Serial Number: 1 (0x1) # Signature Algorithm: sha1WithRSAEncryption # Issuer: O=Openstack CA Org, OU=Openstack Test CA/emailAddress=admin@ca.example.com, # L=State CA, ST=CA, C=AU, CN=Openstack Test Certificate Authority # Validity # Not Before: Nov 16 12:50:19 2012 GMT # Not After : Apr 3 12:50:19 2040 GMT # Subject: O=Openstack Test Org, OU=Openstack Test Unit/emailAddress=admin@example.com, # L=State1, ST=CA, C=US, CN=0.0.0.0 # Subject Public Key Info: # Public Key Algorithm: rsaEncryption # RSA Public Key: (4096 bit) # Modulus (4096 bit): # 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be: # 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1: # . # . # . # Exponent: 65537 (0x10001) # X509v3 extensions: # X509v3 Subject Alternative Name: # DNS:alt1.example.com, DNS:alt2.example.com # Signature Algorithm: sha1WithRSAEncryption # 2c:fc:5c:87:24:bd:4a:fa:40:d2:2e:35:a4:2a:f3:1c:b3:67: # b0:e4:8a:cd:67:6b:55:50:d4:cb:dd:2d:26:a5:15:62:90:a3: # . # . # . -----BEGIN CERTIFICATE----- MIIGADCCA+igAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE2MTI1MDE5WhcNNDAw NDAzMTI1MDE5WjCBmjEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE BhMCVVMxEDAOBgNVBAMTBzAuMC4wLjAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0 T5Hf3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcj hZfbf9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl /6hnlBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLL ppCUvpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9 gt75iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsE ClxiIFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt 6Ji24VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARf xKxlBQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpU qM2MTETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmi qUUL2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABozEw LzAtBgNVHREEJjAkghBhbHQxLmV4YW1wbGUuY29tghBhbHQyLmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBBQUAA4ICAQAs/FyHJL1K+kDSLjWkKvMcs2ew5IrNZ2tVUNTL 3S0mpRVikKOQbNLh5B6Q7eQIvilCdkuit7o2HrpxQHsRor5b4+LyjSLoltyE7dgr ioP5nkKH+ujw6PtMxJCiKvvI+6cVHh6EV2ZkddvbJLVBVVZmB4H64xocS3rrQj19 SXFYVrEjqdLzdGPNIBR+XVnTCeofXg1rkMaU7JuY8nRztee8PRVcKYX6scPfZJb8 +Ea2dsTmtQP4H9mk+JiKGYhEeMLVmjiv3q7KIFownTKZ88K6QbpW2Nj66ItvphoT QqI3rs6E8N0BhftiCcxXtXg+o4utfcnp8jTXX5tVnv44FqtWx7Gzg8XTLPri+ZEB 5IbgU4Q3qFicenBfjwZhH3+GNe52/wLVZLYjal5RPVSRdu9UEDeDAwTCMZSLF4lC rc9giQCMnJ4ISi6C7xH+lDZGFqcJd4oXg/ue9aOJJAFTwhd83fdCHhUu431iPrts NubfrHLMeUjluFgIWmhEZg+XTjB1SQeQzNaZiMODaAv4/40ZVKxvNpDFwIIsPUDf +uC+fv1Q8+alqVMl2ouVyr8ut43HWNV6CJHXODvFp5irjxzVSgLtYDVUInkDFJEs tFpTY21/zVAHIvsj2n4F1231nILR6vBp/WbwBY7r7j0oRtbaO3B1Q6tsbCZQRkKU tdc5rw== -----END CERTIFICATE----- python-glanceclient-2.9.1/glanceclient/tests/unit/var/badcert.crt0000666000175100017510000000000013232161652025166 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/var/wildcard-san-certificate.crt0000666000175100017510000000507713232161652030435 0ustar zuulzuul00000000000000#Certificate: # Data: # Version: 3 (0x2) # Serial Number: 11990626514780340979 (0xa66743493fdcc2f3) # Signature Algorithm: sha1WithRSAEncryption # Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0 # Validity # Not Before: Dec 10 15:31:22 2013 GMT # Not After : Nov 16 15:31:22 2113 GMT # Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0 # Subject Public Key Info: # Public Key Algorithm: rsaEncryption # Public-Key: (2048 bit) # Modulus: # 00:ca:6b:07:73:53:24:45:74:05:a5:2a:27:bd:3e: # . # . # . # Exponent: 65537 (0x10001) # X509v3 extensions: # X509v3 Key Usage: # Key Encipherment, Data Encipherment # X509v3 Extended Key Usage: # TLS Web Server Authentication # X509v3 Subject Alternative Name: # DNS:foo.example.net, DNS:*.example.com # Signature Algorithm: sha1WithRSAEncryption # 7e:41:69:da:f4:3c:06:d6:83:c6:f2:db:df:37:f1:ac:fa:f5: # . # . # . -----BEGIN CERTIFICATE----- MIIDxDCCAqygAwIBAgIJAKZnQ0k/3MLzMA0GCSqGSIb3DQEBBQUAMHgxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUxMRswGQYDVQQKExJP cGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFjayBUZXN0IFVuaXQx EDAOBgNVBAMTBzAuMC4wLjAwIBcNMTMxMjEwMTUzMTIyWhgPMjExMzExMTYxNTMx MjJaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUx MRswGQYDVQQKExJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFj ayBUZXN0IFVuaXQxEDAOBgNVBAMTBzAuMC4wLjAwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQDKawdzUyRFdAWlKie9Pn10j7frffN+z1gEMluK2CtDEwv9 kbD4uS/Kz4dujfTx03mdyNfiMVlOM+YJm/qeLLSdJyFyvZ9Y3WmJ+vT2RGlMMhLd /wEnMRrTYLL39pwI6z+gyw+4D78Pyv/OXy02IA6WtVEefYSx1vmVngb3pL+iBzhO 8CZXNI6lqrFhh+Hr4iMkYMtY1vTnwezAL6p64E/ZAFNPYCEJlacESTLQ4VZYniHc QTgnE1czlI1vxlIk1KDXAzUGeeopZecRih9qlTxtOpklqEciQEE+sHtPcvyvdRE9 Bdyx5rNSALLIcXs0ViJE1RPlw3fjdBoDIOygqvX1AgMBAAGjTzBNMAsGA1UdDwQE AwIEMDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAggg9mb28uZXhhbXBs ZS5uZXSCDSouZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADggEBAH5Badr0PAbW g8by29838az69Raul5IkpZQ5V3O1NaNNWxvmF1q8zFFqqGK5ktXJAwGiwnYEBb30 Zfrr+eFIEERzBthSJkWlP8NG+2ooMyg50femp+asAvW+KYYefJW8KaXTsznMsAFy z1agcWVYVZ4H9PwunEYn/rM1krLEe4Cagsw5nmf8VqZg+hHtw930q8cRzgDsZdfA jVK6dWdmzmLCUTL1GKCeNriDw1jIeFvNufC+Q3orH7xBx4VL+NV5ORWdNY/B8q1b mFHdzbuZX6v39+2ww6aZqG2orfxUocc/5Ox6fXqenKPI3moeHS6Ktesq7sEQSJ6H QZFsTuT/124= -----END CERTIFICATE----- python-glanceclient-2.9.1/glanceclient/tests/unit/var/wildcard-certificate.crt0000666000175100017510000000645413232161652027656 0ustar zuulzuul00000000000000#Certificate: # Data: # Version: 1 (0x0) # Serial Number: 13493453254446411258 (0xbb42603e589dedfa) # Signature Algorithm: sha1WithRSAEncryption # Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com # Validity # Not Before: Aug 21 17:29:18 2013 GMT # Not After : Jul 28 17:29:18 2113 GMT # Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com # Subject Public Key Info: # Public Key Algorithm: rsaEncryption # Public-Key: (4096 bit) # Modulus: # 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be: # 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1: # 46:1e:85:a3:2a:9c:a4:e0:c2:14:34:4f:91:df:dc: # . # . # . # Exponent: 65537 (0x10001) # Signature Algorithm: sha1WithRSAEncryption # 9f:cc:08:5d:19:ee:54:31:a3:57:d7:3c:89:89:c0:69:41:dd: # 46:f8:73:68:ec:46:b9:fa:f5:df:f6:d9:58:35:d8:53:94:88: # bd:36:a6:23:9e:0c:0d:89:62:35:91:49:b6:14:f4:43:69:3c: # . # . # . -----BEGIN CERTIFICATE----- MIIFyjCCA7ICCQC7QmA+WJ3t+jANBgkqhkiG9w0BAQUFADCBpTELMAkGA1UEBhMC VVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQHDAZTdGF0ZTExGzAZBgNVBAoMEk9wZW5z dGFjayBUZXN0IE9yZzEcMBoGA1UECwwTT3BlbnN0YWNrIFRlc3QgVW5pdDEbMBkG A1UEAwwSKi5wb25nLmV4YW1wbGUuY29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl eGFtcGxlLmNvbTAgFw0xMzA4MjExNzI5MThaGA8yMTEzMDcyODE3MjkxOFowgaUx CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEPMA0GA1UEBwwGU3RhdGUxMRswGQYD VQQKDBJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsME09wZW5zdGFjayBUZXN0 IFVuaXQxGzAZBgNVBAMMEioucG9uZy5leGFtcGxlLmNvbTEgMB4GCSqGSIb3DQEJ ARYRYWRtaW5AZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK AoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0T5Hf 3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcjhZfb f9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl/6hn lBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLLppCU vpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9gt75 iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsEClxi IFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt6Ji2 4VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARfxKxl BQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpUqM2M TETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmiqUUL 2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABMA0GCSqG SIb3DQEBBQUAA4ICAQCfzAhdGe5UMaNX1zyJicBpQd1G+HNo7Ea5+vXf9tlYNdhT lIi9NqYjngwNiWI1kUm2FPRDaTwC0kLxk5zBPzF7bcf0SwJCeDjmlUpY7YenS0DA XmIbg8FvgOlp69Ikrqz98Y4pB9H4O81WdjxNBBbHjrufAXxZYnh5rXrVsXeSJ8jN MYGWlSv4xwFGfRX53b8VwXFjGjAkH8SQGtRV2w9d0jF8OzFwBA4bKk4EplY0yBPR 2d7Y3RVrDnOVfV13F8CZxJ5fu+6QamUwIaTjpyqflE1L52KTy+vWPYR47H2u2bhD IeZRufJ8adNIOtH32EcENkusQjLrb3cTXGW00TljhFXd22GqL5d740u+GEKHtWh+ 9OKPTMZK8yK7d5EyS2agTVWmXU6HfpAKz9+AEOnVYErpnggNZjkmJ9kD185rGlSZ Vvo429hXoUAHNbd+8zda3ufJnJf5q4ZEl8+hp8xsvraUy83XLroVZRsKceldmAM8 swt6n6w5gRKg4xTH7KFrd+KNptaoY3SsVrnJuaSOPenrUXbZzaI2Q35CId93+8NP mXVIWdPO1msdZNiCYInRIGycK+oifUZPtAaJdErg8rt8NSpHzYKQ0jfjAGiVHBjK s0J2TjoKB3jtlrw2DAmFWKeMGNp//1Rm6kfQCCXWftn+TA7XEJhcjyDBVciugA== -----END CERTIFICATE----- python-glanceclient-2.9.1/glanceclient/tests/unit/var/privatekey.key0000666000175100017510000000625313232161652025765 0ustar zuulzuul00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKQIBAAKCAgEA1Ls6xKAGVDEjXbB4Wr5FRK6hiYYR2MqoM7BP8+FGHoWjKpyk 4MIUNE+R39xppSAbeCPev93MY9Kki63UfNXzPuSpvJSJVR8FZ9cuiynDJWlAuCvr IAy3I4WX23/fnzJHLq91pTcyqQBGiKLFsLLumL2HPlv5zwaSsTGzmeH2YjzBKPD8 ZsO7Jf+oZ5QTiY+3aWTmaR4VT2YM2DVaMnygmYsIHcR+CRGGvhEKLPO/tjcIbO2w oK0yy6aQlL6aG8GVecODtA+iDaVHwZrK+CALl3hVmWutb2WKoWlWwG005VMzixoe 2drs/YLe+YkkdidHtsZ7VWlCGCyAFDTjtnnZPDTALiDhq9hCMgu4tjvwU7YtmIxj Tu0rBApcYiBSo2GocpR5ja0tUP6DAHCaaZoc/qDgpijjJgGV2AB9QN4LP7FqYSaY L/zFreiYtuFaRWifqOGxyC5aiKbry+BiyDpJsVT+t/ZIWrpoCrYDSFEyif1erfyd 9VgEX8SsZQUE081fwXvvOGCYonXcfHrp1PAKKz8gCRc7Az0yaRVAeEOQcAHgxgsk oMSaVKjNjExEwcnHYlU6DOElpEbDmYGcUPgFX5JKg3LdgWrdpYPXqXCHs/D67KIU I0G5oqlFC9nN3XuJC2MyFhfBDdz9jHv+29Oo25+HOxZcvwDerSThttwc4R0CAwEA AQKCAgEAqnwqSu4cZFjFCQ6mRcL67GIvn3FM2DsBtfr0+HRvp4JeE4ZaNK4VVx71 vzx7hhRHL28/0vBEHzPvHun+wtUMDjlfNnyr2wXzZRb0fB7KAC9r6K15z8Og+dzU qNrAMmsu1OFVHUUxWnOYE2Svnj6oLMynmHhJqXqREWTNlOOce3pJKzCGdy0hzQAo zGnFhpcg3Fw6s7+iQHF+lb+cO53Zb3QW2xRgFZBwNd6eEwx9deCA5htPVFW5wbAJ asud4eSwkFb6M9Hbg6gT67rMMzIrWAbeQwgihIYSJe2v0qMyox6czjvuwZVMHJdH byBTkkVEmdxTd03V5F21f3wrik/4oWqytjmjvMIY1gGTMo7aBnvPoKpgc2fqJub9 cdAfGiJnFqo4Ae55mL4sgJPUCP7UATaDNAOCgt0zStmHMH8ACwk0dh1pzjyjpSR3 OQfFs8QCAl9cvzxwux1tzG/uYxOrr+Rj2JlZKW/ljbWOeE0Gnjca73F40uGkEIbZ 5i6YEuiPE6XGH0TP62Sdu2t5OlaKnZT12Tf6E8xNDsdaLuvAIz5sXyhoxvOmVd9w V4+uN1bZ10c5k/4uGRsHiXjX6IyYZEj8rKz6ryNikCdi6OzxWE3pCXmfBlVaXtO6 EIubzk6dgjWcsPoqOsIl5Ywz4RWu0YUk4ZxRts54jCn14bPQpoECggEBAPiLTN8Z I0GQXMQaq9sN8kVsM/6AG/vWbc+IukPDYEC6Prk79jzkxMpDP8qK9C71bh39U1ky Kz4gSsLi9v3rM1gZwNshkZJ/zdQJ1NiCkzJVJX48DGeyYqUBjVt8Si37V2vzblBN RvM7U3rDN0xGiannyWnBC/jed+ZFCo97E9yOxIAs2ekwsl+ED3j1cARv8pBTGWnw Zhh4AD/Osk5U038oYcWHaIzUuNhEpv46bFLjVT11mGHfUY51Db3jBn0HYRlOPEV/ F0kE5F+6rRg2tt7n0PO3UbzSNFyDRwtknJ2Nh4EtZZe93domls8SMR/kEHXcPLiQ ytEFyIAzsxfUwrECggEBANsc54N/LPmX1XuC643ZsDobH5/ALKc8W7wE7e82oSTD 7cKBgdgB71DupJ7m81LHaDgT2RIzjl+lR3VVYLR/ukMcW+47JWrHyrsinu6itOdt ruhw0UPksoJGsB4KxUdRioFVT7m45GpnseJL0tjYaTCW01swae4QL4skNjjphPrb b/heMz9n79TK2ePlw1BvJKH0fnOJRuh/v63pD9SymB8EPsazjloKZ5qTrqVi3Obs F8WTSdl8KB1JSgeppdvHRcZQY1J+UfdCAlGD/pP7/zCKkRYcetre7fGMKVyPIDzO GAWz0xA2jnrgg7UqIh74oRHe0lZVMdMQ7FoJbRa7KC0CggEAJreEbQh8bn0vhjjl ZoVApUHaw51vPobDql2RLncj6lFY7gACNrAoW52oNUP6D8qZscBBmJZxGAdtvfgf I6Tc5a91VG1hQOH5zTsO1f9ZMLEE2yo9gHXQWgXo4ER3RbxufNl56LZxA/jM40W/ unkOftIllPzGgakeIlfE8l7o1CXFRHY4J9Q3JRvsURpirb5GmeboAZG6RbuDxmzL Z9pc6+T9fgi+55lHhiEDpnyxXSQepilIaI6iJL/lORxBaX6ZyJhgWS8YEH7bmHH6 /tefGxAfg6ed6v0PvQ2SJpswrnZakmvg9IdWJOJ4AZ/C2UXsrn91Ugb0ISV2e0oS bvbssQKCAQBjstc04h0YxJmCxaNgu/iPt9+/1LV8st4awzNwcS8Jh40bv8nQ+7Bk 5vFIzFVTCSDGw2E2Avd5Vb8aCGskNioOd0ztLURtPdNlKu+eLbKayzGW2h6eAeWn mXpxcP0q4lNfXe4U16g3Mk+iZFXgDThvv3EUQQcyJ3M6oJN7eeXkLwzXuiUfaK+b 52EVbWpdovTMLG+NKp11FQummjF12n2VP11BFFplZe6WSzRgVIenGy4F3Grx5qhq CvsAWZT6V8XL4rAOzSOGmiZr6N9hfnwzHhm+Md9Ez8L88YWwc/97K1uK3LPg4LIb /yRuvmkgJolDlFuopMMzArRIk5lrimVRAoIBAQDZmXk/VMA7fsI1/2sgSME0xt1A jkJZMZSnVD0UDWFkbyK6E5jDnwVUyqBDYe+HJyT4UnPDNCj++BchCQcG0Jih04RM jwGqxkfTF9K7kfouINSSXPRw/BtHkqMhV/g324mWcifCFVkDQghuslfmey8BKumo 2KPyGnF9Q8CvTSQ0VlK1ZAKRf/zish49PMm7vD1KGkjRPliS3tgAmXPEpwijPGse 4dSUeTfw5wCKAoq9DHjyHdO5fnfkOvA5PMQ4JZAzOCzJak8ET+tw4wB/dBeYiLVi l00GHLYAr5Nv/WqVnl/VLMd9rOCnLck+pxBNSa6dTrp3FuY00son6hneIvkv -----END RSA PRIVATE KEY----- python-glanceclient-2.9.1/glanceclient/tests/unit/__init__.py0000666000175100017510000000000013232161651024370 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/test_utils.py0000666000175100017510000002025113232161652025043 0ustar zuulzuul00000000000000# 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 sys import mock from oslo_utils import encodeutils from requests import Response import six # NOTE(jokke): simplified transition to py3, behaves like py2 xrange from six.moves import range import testtools from glanceclient.common import utils REQUEST_ID = 'req-1234' def create_response_obj_with_req_id(req_id): resp = Response() resp.headers['x-openstack-request-id'] = req_id return resp class TestUtils(testtools.TestCase): def test_make_size_human_readable(self): self.assertEqual("106B", utils.make_size_human_readable(106)) self.assertEqual("1000kB", utils.make_size_human_readable(1024000)) self.assertEqual("1MB", utils.make_size_human_readable(1048576)) self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008)) self.assertEqual("9.3MB", utils.make_size_human_readable(9761280)) self.assertEqual("0B", utils.make_size_human_readable(None)) def test_get_new_file_size(self): size = 98304 file_obj = six.StringIO('X' * size) try: self.assertEqual(size, utils.get_file_size(file_obj)) # Check that get_file_size didn't change original file position. self.assertEqual(0, file_obj.tell()) finally: file_obj.close() def test_get_consumed_file_size(self): size, consumed = 98304, 304 file_obj = six.StringIO('X' * size) file_obj.seek(consumed) try: self.assertEqual(size, utils.get_file_size(file_obj)) # Check that get_file_size didn't change original file position. self.assertEqual(consumed, file_obj.tell()) finally: file_obj.close() def test_prettytable(self): class Struct(object): def __init__(self, **entries): self.__dict__.update(entries) # test that the prettytable output is wellformatted (left-aligned) columns = ['ID', 'Name'] val = ['Name1', 'another', 'veeeery long'] images = [Struct(**{'id': i ** 16, 'name': val[i]}) for i in range(len(val))] saved_stdout = sys.stdout try: sys.stdout = output_list = six.StringIO() utils.print_list(images, columns) sys.stdout = output_dict = six.StringIO() utils.print_dict({'K': 'k', 'Key': 'veeeeeeeeeeeeeeeeeeeeeeee' 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' 'eeeeeeeeeeeery long value'}, max_column_width=60) finally: sys.stdout = saved_stdout self.assertEqual('''\ +-------+--------------+ | ID | Name | +-------+--------------+ | | Name1 | | 1 | another | | 65536 | veeeery long | +-------+--------------+ ''', output_list.getvalue()) self.assertEqual('''\ +----------+--------------------------------------------------------------+ | Property | Value | +----------+--------------------------------------------------------------+ | K | k | | Key | veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee | | | eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee | | | ery long value | +----------+--------------------------------------------------------------+ ''', output_dict.getvalue()) def test_schema_args_with_list_types(self): # NOTE(flaper87): Regression for bug # https://bugs.launchpad.net/python-glanceclient/+bug/1401032 def schema_getter(_type='string', enum=False): prop = { 'type': ['null', _type], 'readOnly': True, 'description': 'Test schema', } if enum: prop['enum'] = [None, 'opt-1', 'opt-2'] def actual_getter(): return { 'additionalProperties': False, 'required': ['name'], 'name': 'test_schema', 'properties': { 'test': prop, } } return actual_getter def dummy_func(): pass decorated = utils.schema_args(schema_getter())(dummy_func) arg, opts = decorated.__dict__['arguments'][0] self.assertIn('--test', arg) self.assertEqual(encodeutils.safe_decode, opts['type']) decorated = utils.schema_args(schema_getter('integer'))(dummy_func) arg, opts = decorated.__dict__['arguments'][0] self.assertIn('--test', arg) self.assertEqual(int, opts['type']) decorated = utils.schema_args(schema_getter(enum=True))(dummy_func) arg, opts = decorated.__dict__['arguments'][0] self.assertIn('--test', arg) self.assertEqual(encodeutils.safe_decode, opts['type']) self.assertIn('None, opt-1, opt-2', opts['help']) def test_iterable_closes(self): # Regression test for bug 1461678. def _iterate(i): for chunk in i: raise(IOError) data = six.moves.StringIO('somestring') data.close = mock.Mock() i = utils.IterableWithLength(data, 10) self.assertRaises(IOError, _iterate, i) data.close.assert_called_with() def test_safe_header(self): self.assertEqual(('somekey', 'somevalue'), utils.safe_header('somekey', 'somevalue')) self.assertEqual(('somekey', None), utils.safe_header('somekey', None)) for sensitive_header in utils.SENSITIVE_HEADERS: (name, value) = utils.safe_header( sensitive_header, encodeutils.safe_encode('somestring')) self.assertEqual(sensitive_header, name) self.assertTrue(value.startswith("{SHA1}")) (name, value) = utils.safe_header(sensitive_header, None) self.assertEqual(sensitive_header, name) self.assertIsNone(value) def test_generator_proxy(self): def _test_decorator(): i = 1 resp = create_response_obj_with_req_id(REQUEST_ID) while True: yield i, resp i += 1 gen_obj = _test_decorator() proxy = utils.GeneratorProxy(gen_obj) # Proxy object should succeed in behaving as the # wrapped object self.assertIsInstance(proxy, type(gen_obj)) # Initially request_ids should be empty self.assertEqual([], proxy.request_ids) # Only after we have started iterating we should # see non-empty `request_ids` property proxy.next() self.assertEqual([REQUEST_ID], proxy.request_ids) # Even after multiple iterations `request_ids` property # should only contain one request id proxy.next() proxy.next() self.assertEqual(1, len(proxy.request_ids)) def test_request_id_proxy(self): def test_data(val): resp = create_response_obj_with_req_id(REQUEST_ID) return val, resp # Object of any type except decorator can be passed to test_data proxy = utils.RequestIdProxy(test_data(11)) # Verify that proxy object has a property `request_ids` and it is # a list of one request id self.assertEqual([REQUEST_ID], proxy.request_ids) python-glanceclient-2.9.1/glanceclient/tests/unit/test_http.py0000666000175100017510000004643613232161651024676 0ustar zuulzuul00000000000000# 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 functools import json import logging import uuid import fixtures from keystoneauth1 import session from keystoneauth1 import token_endpoint import mock from oslo_utils import encodeutils import requests from requests_mock.contrib import fixture import six from six.moves.urllib import parse from testscenarios import load_tests_apply_scenarios as load_tests # noqa import testtools from testtools import matchers import types import glanceclient from glanceclient.common import http from glanceclient.tests import utils def original_only(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): if not hasattr(self.client, 'log_curl_request'): self.skipTest('Skip logging tests for session client') return f(self, *args, **kwargs) class TestClient(testtools.TestCase): scenarios = [ ('httpclient', {'create_client': '_create_http_client'}), ('session', {'create_client': '_create_session_client'}) ] def _create_http_client(self): return http.HTTPClient(self.endpoint, token=self.token) def _create_session_client(self): auth = token_endpoint.Token(self.endpoint, self.token) sess = session.Session(auth=auth) return http.SessionClient(sess) def setUp(self): super(TestClient, self).setUp() self.mock = self.useFixture(fixture.Fixture()) self.endpoint = 'http://example.com:9292' self.ssl_endpoint = 'https://example.com:9292' self.token = u'abc123' self.client = getattr(self, self.create_client)() def test_identity_headers_and_token(self): identity_headers = { 'X-Auth-Token': 'auth_token', 'X-User-Id': 'user', 'X-Tenant-Id': 'tenant', 'X-Roles': 'roles', 'X-Identity-Status': 'Confirmed', 'X-Service-Catalog': 'service_catalog', } # with token kwargs = {'token': u'fake-token', 'identity_headers': identity_headers} http_client_object = http.HTTPClient(self.endpoint, **kwargs) self.assertEqual('auth_token', http_client_object.auth_token) self.assertTrue(http_client_object.identity_headers. get('X-Auth-Token') is None) def test_identity_headers_and_no_token_in_header(self): identity_headers = { 'X-User-Id': 'user', 'X-Tenant-Id': 'tenant', 'X-Roles': 'roles', 'X-Identity-Status': 'Confirmed', 'X-Service-Catalog': 'service_catalog', } # without X-Auth-Token in identity headers kwargs = {'token': u'fake-token', 'identity_headers': identity_headers} http_client_object = http.HTTPClient(self.endpoint, **kwargs) self.assertEqual(u'fake-token', http_client_object.auth_token) self.assertTrue(http_client_object.identity_headers. get('X-Auth-Token') is None) def test_identity_headers_and_no_token_in_session_header(self): # Tests that if token or X-Auth-Token are not provided in the kwargs # when creating the http client, the session headers don't contain # the X-Auth-Token key. identity_headers = { 'X-User-Id': 'user', 'X-Tenant-Id': 'tenant', 'X-Roles': 'roles', 'X-Identity-Status': 'Confirmed', 'X-Service-Catalog': 'service_catalog', } kwargs = {'identity_headers': identity_headers} http_client_object = http.HTTPClient(self.endpoint, **kwargs) self.assertIsNone(http_client_object.auth_token) self.assertNotIn('X-Auth-Token', http_client_object.session.headers) def test_identity_headers_are_passed(self): # Tests that if token or X-Auth-Token are not provided in the kwargs # when creating the http client, the session headers don't contain # the X-Auth-Token key. identity_headers = { 'X-User-Id': b'user', 'X-Tenant-Id': b'tenant', 'X-Roles': b'roles', 'X-Identity-Status': b'Confirmed', 'X-Service-Catalog': b'service_catalog', } kwargs = {'identity_headers': identity_headers} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v1/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers for k, v in identity_headers.items(): self.assertEqual(v, headers[k]) def test_language_header_passed(self): kwargs = {'language_header': 'nb_NO'} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(kwargs['language_header'], headers['Accept-Language']) def test_request_id_header_passed(self): global_id = encodeutils.safe_encode("req-%s" % uuid.uuid4()) kwargs = {'global_request_id': global_id} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(global_id, headers['X-OpenStack-Request-ID']) def test_language_header_not_passed_no_language(self): kwargs = {} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertNotIn('Accept-Language', headers) def test_connection_timeout(self): """Verify a InvalidEndpoint is received if connection times out.""" def cb(request, context): raise requests.exceptions.Timeout path = '/v1/images' self.mock.get(self.endpoint + path, text=cb) comm_err = self.assertRaises(glanceclient.exc.InvalidEndpoint, self.client.get, '/v1/images') self.assertIn(self.endpoint, comm_err.message) def test_connection_refused(self): """Verify a CommunicationError is received if connection is refused. The error should list the host and port that refused the connection. """ def cb(request, context): raise requests.exceptions.ConnectionError() path = '/v1/images/detail?limit=20' self.mock.get(self.endpoint + path, text=cb) comm_err = self.assertRaises(glanceclient.exc.CommunicationError, self.client.get, '/v1/images/detail?limit=20') self.assertIn(self.endpoint, comm_err.message) def test_http_encoding(self): path = '/v1/images/detail' text = 'Ok' self.mock.get(self.endpoint + path, text=text, headers={"Content-Type": "text/plain"}) headers = {"test": u'ni\xf1o'} resp, body = self.client.get(path, headers=headers) self.assertEqual(text, resp.text) def test_headers_encoding(self): value = u'ni\xf1o' headers = {"test": value, "none-val": None} encoded = http.encode_headers(headers) self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"]) self.assertNotIn("none-val", encoded) @mock.patch('keystoneauth1.adapter.Adapter.request') def test_http_duplicate_content_type_headers(self, mock_ksarq): """Test proper handling of Content-Type headers. encode_headers() must be called as late as possible before a request is sent. If this principle is violated, and if any changes are made to the headers between encode_headers() and the actual request (for instance a call to _set_common_request_kwargs()), and if you're trying to set a Content-Type that is not equal to application/octet-stream (the default), it is entirely possible that you'll end up with two Content-Type headers defined (yours plus application/octet-stream). The request will go out the door with only one of them chosen seemingly at random. This situation only occurs in python3. This test will never fail in python2. """ path = "/v2/images/my-image" headers = { "Content-Type": "application/openstack-images-v2.1-json-patch" } data = '[{"value": "qcow2", "path": "/disk_format", "op": "replace"}]' self.mock.patch(self.endpoint + path) sess_http_client = self._create_session_client() sess_http_client.patch(path, headers=headers, data=data) # Pull out the headers with which Adapter.request was invoked ksarqh = mock_ksarq.call_args[1]['headers'] # Only one Content-Type header (of any text-type) self.assertEqual(1, [encodeutils.safe_decode(key) for key in ksarqh.keys()].count(u'Content-Type')) # And it's the one we set self.assertEqual(b"application/openstack-images-v2.1-json-patch", ksarqh[b"Content-Type"]) def test_raw_request(self): """Verify the path being used for HTTP requests reflects accurately.""" headers = {"Content-Type": "text/plain"} text = 'Ok' path = '/v1/images/detail' self.mock.get(self.endpoint + path, text=text, headers=headers) resp, body = self.client.get('/v1/images/detail', headers=headers) self.assertEqual(headers, resp.headers) self.assertEqual(text, resp.text) def test_parse_endpoint(self): endpoint = 'http://example.com:9292' test_client = http.HTTPClient(endpoint, token=u'adc123') actual = test_client.parse_endpoint(endpoint) expected = parse.SplitResult(scheme='http', netloc='example.com:9292', path='', query='', fragment='') self.assertEqual(expected, actual) def test_get_connections_kwargs_http(self): endpoint = 'http://example.com:9292' test_client = http.HTTPClient(endpoint, token=u'adc123') self.assertEqual(600.0, test_client.timeout) def test__chunk_body_exact_size_chunk(self): test_client = http._BaseHTTPClient() bytestring = b'x' * http.CHUNKSIZE data = six.BytesIO(bytestring) chunk = list(test_client._chunk_body(data)) self.assertEqual(1, len(chunk)) self.assertEqual([bytestring], chunk) def test_http_chunked_request(self): text = "Ok" data = six.StringIO(text) path = '/v1/images/' self.mock.post(self.endpoint + path, text=text) headers = {"test": u'chunked_request'} resp, body = self.client.post(path, headers=headers, data=data) self.assertIsInstance(self.mock.last_request.body, types.GeneratorType) self.assertEqual(text, resp.text) def test_http_json(self): data = {"test": "json_request"} path = '/v1/images' text = 'OK' self.mock.post(self.endpoint + path, text=text) headers = {"test": u'chunked_request'} resp, body = self.client.post(path, headers=headers, data=data) self.assertEqual(text, resp.text) self.assertIsInstance(self.mock.last_request.body, six.string_types) self.assertEqual(data, json.loads(self.mock.last_request.body)) def test_http_chunked_response(self): data = "TEST" path = '/v1/images/' self.mock.get(self.endpoint + path, body=six.StringIO(data), headers={"Content-Type": "application/octet-stream"}) resp, body = self.client.get(path) self.assertIsInstance(body, types.GeneratorType) self.assertEqual([data], list(body)) @original_only def test_log_http_response_with_non_ascii_char(self): try: response = 'Ok' headers = {"Content-Type": "text/plain", "test": "value1\xa5\xa6"} fake = utils.FakeResponse(headers, six.StringIO(response)) self.client.log_http_response(fake) except UnicodeDecodeError as e: self.fail("Unexpected UnicodeDecodeError exception '%s'" % e) @original_only def test_log_curl_request_with_non_ascii_char(self): try: headers = {'header1': 'value1\xa5\xa6'} body = 'examplebody\xa5\xa6' self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body, None) except UnicodeDecodeError as e: self.fail("Unexpected UnicodeDecodeError exception '%s'" % e) @original_only @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_body_and_header(self, mock_log): hd_name = 'header1' hd_val = 'value1' headers = {hd_name: hd_val} body = 'examplebody' self.client.log_curl_request('GET', '/api/v1/', headers, body, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') hd_regex = ".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val) self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(hd_regex), 'header not found in curl command') body_regex = ".*\s-d\s+'%s'\s.*" % body self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(body_regex), 'body not found in curl command') def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert): headers = {'header1': 'value1'} http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key, cert_file=cert, cacert=cacert, token='fake-token') http_client_object.log_curl_request('GET', '/api/v1/', headers, None, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') needles = {'key': key, 'cert': cert, 'cacert': cacert} for option, value in needles.items(): if value: regex = ".*\s--%s\s+('%s'|%s).*" % (option, value, value) self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(regex), 'no --%s option in curl command' % option) else: regex = ".*\s--%s\s+.*" % option self.assertThat(mock_log.call_args[0][0], matchers.Not(matchers.MatchesRegex(regex)), 'unexpected --%s option in curl command' % option) @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_all_certs(self, mock_log): self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', 'cacert2') @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_some_certs(self, mock_log): self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None) @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_insecure_param(self, mock_log): headers = {'header1': 'value1'} http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True, token='fake-token') http_client_object.log_curl_request('GET', '/api/v1/', headers, None, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex('.*\s-k\s.*'), 'no -k option in curl command') @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_token_header(self, mock_log): fake_token = 'fake-token' headers = {'X-Auth-Token': fake_token} http_client_object = http.HTTPClient(self.endpoint, identity_headers=headers) http_client_object.log_curl_request('GET', '/api/v1/', headers, None, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') token_regex = '.*%s.*' % fake_token self.assertThat(mock_log.call_args[0][0], matchers.Not(matchers.MatchesRegex(token_regex)), 'token found in LOG.debug parameter') def test_log_request_id_once(self): logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) data = "TEST" path = '/v1/images/' self.mock.get(self.endpoint + path, body=six.StringIO(data), headers={"Content-Type": "application/octet-stream", 'x-openstack-request-id': "1234"}) resp, body = self.client.get(path) self.assertIsInstance(body, types.GeneratorType) self.assertEqual([data], list(body)) expected_log = ("GET call to image " "for http://example.com:9292/v1/images/ " "used request id 1234") self.assertEqual(1, logger.output.count(expected_log)) def test_expired_token_has_changed(self): # instantiate client with some token fake_token = b'fake-token' http_client = http.HTTPClient(self.endpoint, token=fake_token) path = '/v1/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(fake_token, headers['X-Auth-Token']) # refresh the token refreshed_token = b'refreshed-token' http_client.auth_token = refreshed_token http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(refreshed_token, headers['X-Auth-Token']) # regression check for bug 1448080 unicode_token = u'ni\xf1o' http_client.auth_token = unicode_token http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(b'ni\xc3\xb1o', headers['X-Auth-Token']) python-glanceclient-2.9.1/glanceclient/tests/unit/test_shell.py0000666000175100017510000012251713232161665025026 0ustar zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! 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 argparse try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict import hashlib import logging import os import sys import traceback import uuid import fixtures from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import fixture as ks_fixture import mock from requests_mock.contrib import fixture as rm_fixture import six from glanceclient.common import utils from glanceclient import exc from glanceclient import shell as openstack_shell from glanceclient.tests.unit.v2.fixtures import image_show_fixture from glanceclient.tests.unit.v2.fixtures import image_versions_fixture from glanceclient.tests import utils as testutils # NOTE (esheffield) Used for the schema caching tests from glanceclient.v2 import schemas as schemas import json DEFAULT_IMAGE_URL = 'http://127.0.0.1:9292/' DEFAULT_IMAGE_URL_INTERNAL = 'http://127.0.0.1:9191/' DEFAULT_USERNAME = 'username' DEFAULT_PASSWORD = 'password' DEFAULT_TENANT_ID = 'tenant_id' DEFAULT_TENANT_NAME = 'tenant_name' DEFAULT_PROJECT_ID = '0123456789' DEFAULT_USER_DOMAIN_NAME = 'user_domain_name' DEFAULT_UNVERSIONED_AUTH_URL = 'http://127.0.0.1:5000/' DEFAULT_V2_AUTH_URL = '%sv2.0' % DEFAULT_UNVERSIONED_AUTH_URL DEFAULT_V3_AUTH_URL = '%sv3' % DEFAULT_UNVERSIONED_AUTH_URL DEFAULT_AUTH_TOKEN = ' 3bcc3d3a03f44e3d8377f9247b0ad155' TEST_SERVICE_URL = 'http://127.0.0.1:5000/' DEFAULT_SERVICE_TYPE = 'image' DEFAULT_ENDPOINT_TYPE = 'public' FAKE_V2_ENV = {'OS_USERNAME': DEFAULT_USERNAME, 'OS_PASSWORD': DEFAULT_PASSWORD, 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, 'OS_AUTH_URL': DEFAULT_V2_AUTH_URL, 'OS_IMAGE_URL': DEFAULT_IMAGE_URL} FAKE_V3_ENV = {'OS_USERNAME': DEFAULT_USERNAME, 'OS_PASSWORD': DEFAULT_PASSWORD, 'OS_PROJECT_ID': DEFAULT_PROJECT_ID, 'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME, 'OS_AUTH_URL': DEFAULT_V3_AUTH_URL, 'OS_IMAGE_URL': DEFAULT_IMAGE_URL} FAKE_V4_ENV = {'OS_USERNAME': DEFAULT_USERNAME, 'OS_PASSWORD': DEFAULT_PASSWORD, 'OS_PROJECT_ID': DEFAULT_PROJECT_ID, 'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME, 'OS_AUTH_URL': DEFAULT_V3_AUTH_URL, 'OS_SERVICE_TYPE': DEFAULT_SERVICE_TYPE, 'OS_ENDPOINT_TYPE': DEFAULT_ENDPOINT_TYPE, 'OS_AUTH_TOKEN': DEFAULT_AUTH_TOKEN} TOKEN_ID = uuid.uuid4().hex V2_TOKEN = ks_fixture.V2Token(token_id=TOKEN_ID) V2_TOKEN.set_scope() _s = V2_TOKEN.add_service('image', name='glance') _s.add_endpoint(DEFAULT_IMAGE_URL) V3_TOKEN = ks_fixture.V3Token() V3_TOKEN.set_project_scope() _s = V3_TOKEN.add_service('image', name='glance') _s.add_standard_endpoints(public=DEFAULT_IMAGE_URL, internal=DEFAULT_IMAGE_URL_INTERNAL) class ShellTest(testutils.TestCase): # auth environment to use auth_env = FAKE_V2_ENV.copy() # expected auth plugin to invoke token_url = DEFAULT_V2_AUTH_URL + '/tokens' # Patch os.environ to avoid required auth info def make_env(self, exclude=None): env = dict((k, v) for k, v in self.auth_env.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): super(ShellTest, self).setUp() global _old_env _old_env, os.environ = os.environ, self.auth_env self.requests = self.useFixture(rm_fixture.Fixture()) json_list = ks_fixture.DiscoveryList(DEFAULT_UNVERSIONED_AUTH_URL) self.requests.get(DEFAULT_UNVERSIONED_AUTH_URL, json=json_list, status_code=300) json_v2 = {'version': ks_fixture.V2Discovery(DEFAULT_V2_AUTH_URL)} self.requests.get(DEFAULT_V2_AUTH_URL, json=json_v2) json_v3 = {'version': ks_fixture.V3Discovery(DEFAULT_V3_AUTH_URL)} self.requests.get(DEFAULT_V3_AUTH_URL, json=json_v3) self.v2_auth = self.requests.post(DEFAULT_V2_AUTH_URL + '/tokens', json=V2_TOKEN) headers = {'X-Subject-Token': TOKEN_ID} self.v3_auth = self.requests.post(DEFAULT_V3_AUTH_URL + '/auth/tokens', headers=headers, json=V3_TOKEN) global shell, _shell, assert_called, assert_called_anytime _shell = openstack_shell.OpenStackImagesShell() shell = lambda cmd: _shell.main(cmd.split()) def tearDown(self): super(ShellTest, self).tearDown() global _old_env os.environ = _old_env def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: sys.stdout = six.StringIO() sys.stderr = six.StringIO() _shell = openstack_shell.OpenStackImagesShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertIn(exc_value.code, exitcodes) finally: stdout = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig stderr = sys.stderr.getvalue() sys.stderr.close() sys.stderr = orig_stderr return (stdout, stderr) def test_fixup_subcommand(self): arglist = [u'image-list', u'--help'] if six.PY2: expected_arglist = [b'image-list', u'--help'] elif six.PY3: expected_arglist = [u'image-list', u'--help'] openstack_shell.OpenStackImagesShell._fixup_subcommand( arglist, arglist ) self.assertEqual(expected_arglist, arglist) def test_fixup_subcommand_with_options_preceding(self): arglist = [u'--os-auth-token', u'abcdef', u'image-list', u'--help'] unknown = arglist[2:] if six.PY2: expected_arglist = [ u'--os-auth-token', u'abcdef', b'image-list', u'--help' ] elif six.PY3: expected_arglist = [ u'--os-auth-token', u'abcdef', u'image-list', u'--help' ] openstack_shell.OpenStackImagesShell._fixup_subcommand( unknown, arglist ) self.assertEqual(expected_arglist, arglist) def test_help_unknown_command(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 help foofoo' self.assertRaises(exc.CommandError, shell.main, argstr.split()) @mock.patch('sys.stdout', six.StringIO()) @mock.patch('sys.stderr', six.StringIO()) @mock.patch('sys.argv', ['glance', 'help', 'foofoo']) def test_no_stacktrace_when_debug_disabled(self): with mock.patch.object(traceback, 'print_exc') as mock_print_exc: try: openstack_shell.main() except SystemExit: pass self.assertFalse(mock_print_exc.called) @mock.patch('sys.stdout', six.StringIO()) @mock.patch('sys.stderr', six.StringIO()) @mock.patch('sys.argv', ['glance', 'help', 'foofoo']) def test_stacktrace_when_debug_enabled_by_env(self): old_environment = os.environ.copy() os.environ = {'GLANCECLIENT_DEBUG': '1'} try: with mock.patch.object(traceback, 'print_exc') as mock_print_exc: try: openstack_shell.main() except SystemExit: pass self.assertTrue(mock_print_exc.called) finally: os.environ = old_environment @mock.patch('sys.stdout', six.StringIO()) @mock.patch('sys.stderr', six.StringIO()) @mock.patch('sys.argv', ['glance', '--debug', 'help', 'foofoo']) def test_stacktrace_when_debug_enabled(self): with mock.patch.object(traceback, 'print_exc') as mock_print_exc: try: openstack_shell.main() except SystemExit: pass self.assertTrue(mock_print_exc.called) def test_help(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 help' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertFalse(et_mock.called) def test_blank_call(self): shell = openstack_shell.OpenStackImagesShell() with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main('') self.assertEqual(0, actual) self.assertFalse(et_mock.called) def test_help_on_subcommand_error(self): self.assertRaises(exc.CommandError, shell, '--os-image-api-version 2 help bad') def test_help_v2_no_schema(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 help image-create' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertNotIn('', actual) self.assertFalse(et_mock.called) argstr = '--os-image-api-version 2 help md-namespace-create' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertNotIn('', actual) self.assertFalse(et_mock.called) argstr = '--os-image-api-version 2 help md-resource-type-associate' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertNotIn('', actual) self.assertFalse(et_mock.called) def test_get_base_parser(self): test_shell = openstack_shell.OpenStackImagesShell() # NOTE(stevemar): Use the current sys.argv for base_parser since it # doesn't matter for this test, it just needs to initialize the CLI actual_parser = test_shell.get_base_parser(sys.argv) description = 'Command-line interface to the OpenStack Images API.' expected = argparse.ArgumentParser( prog='glance', usage=None, description=description, conflict_handler='error', add_help=False, formatter_class=openstack_shell.HelpFormatter,) # NOTE(guochbo): Can't compare ArgumentParser instances directly # Convert ArgumentPaser to string first. self.assertEqual(str(expected), str(actual_parser)) @mock.patch.object(openstack_shell.OpenStackImagesShell, '_get_versioned_client') def test_cert_and_key_args_interchangeable(self, mock_versioned_client): # make sure --os-cert and --os-key are passed correctly args = ('--os-image-api-version 2 ' '--os-cert mycert ' '--os-key mykey image-list') shell(args) assert mock_versioned_client.called ((api_version, args), kwargs) = mock_versioned_client.call_args self.assertEqual('mycert', args.os_cert) self.assertEqual('mykey', args.os_key) # make sure we get the same thing with --cert-file and --key-file args = ('--os-image-api-version 2 ' '--cert-file mycertfile ' '--key-file mykeyfile image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) assert mock_versioned_client.called ((api_version, args), kwargs) = mock_versioned_client.call_args self.assertEqual('mycertfile', args.os_cert) self.assertEqual('mykeyfile', args.os_key) @mock.patch('glanceclient.v1.client.Client') def test_no_auth_with_token_and_image_url_with_v1(self, v1_client): # test no authentication is required if both token and endpoint url # are specified args = ('--os-image-api-version 1 --os-auth-token mytoken' ' --os-image-url https://image:1234/v1 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) assert v1_client.called (args, kwargs) = v1_client.call_args self.assertEqual('mytoken', kwargs['token']) self.assertEqual('https://image:1234', args[0]) @mock.patch('glanceclient.v2.client.Client') def test_no_auth_with_token_and_image_url_with_v2(self, v2_client): # test no authentication is required if both token and endpoint url # are specified args = ('--os-image-api-version 2 --os-auth-token mytoken ' '--os-image-url https://image:1234 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertTrue(v2_client.called) (args, kwargs) = v2_client.call_args self.assertEqual('mytoken', kwargs['token']) self.assertEqual('https://image:1234', args[0]) def _assert_auth_plugin_args(self): # make sure our auth plugin is invoked with the correct args self.assertFalse(self.v3_auth.called) body = json.loads(self.v2_auth.last_request.body) self.assertEqual(self.auth_env['OS_TENANT_NAME'], body['auth']['tenantName']) self.assertEqual(self.auth_env['OS_USERNAME'], body['auth']['passwordCredentials']['username']) self.assertEqual(self.auth_env['OS_PASSWORD'], body['auth']['passwordCredentials']['password']) @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_without_version(self, v2_client, cache_schemas): cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': [{'id': 'v2'}]}) args = 'image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) # NOTE(flaper87): this currently calls auth twice since it'll # authenticate to get the version list *and* to execute the command. # This is not the ideal behavior and it should be fixed in a follow # up patch. @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v2_auth.call_count) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_with_v2(self, v2_client): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v2_auth.call_count) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_unversioned_auth_url_with_v1( self, v1_client): args = ('--os-image-api-version 1 --os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL) glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) @mock.patch('glanceclient.v2.client.Client') @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) def test_auth_plugin_invocation_with_unversioned_auth_url_with_v2( self, v2_client, cache_schemas): args = ('--os-auth-url %s --os-image-api-version 2 ' 'image-list') % DEFAULT_UNVERSIONED_AUTH_URL glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) @mock.patch('glanceclient.Client') def test_endpoint_token_no_auth_req(self, mock_client): def verify_input(version=None, endpoint=None, *args, **kwargs): self.assertIn('token', kwargs) self.assertEqual(TOKEN_ID, kwargs['token']) self.assertEqual(DEFAULT_IMAGE_URL, endpoint) return mock.MagicMock() mock_client.side_effect = verify_input glance_shell = openstack_shell.OpenStackImagesShell() args = ['--os-image-api-version', '2', '--os-auth-token', TOKEN_ID, '--os-image-url', DEFAULT_IMAGE_URL, 'image-list'] glance_shell.main(args) self.assertEqual(1, mock_client.call_count) @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', side_effect=EOFError) @mock.patch('glanceclient.v2.client.Client') def test_password_prompted_ctrlD_with_v2(self, v2_client, mock_getpass, mock_stdin): cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': []}) glance_shell = openstack_shell.OpenStackImagesShell() self.make_env(exclude='OS_PASSWORD') # We should get Command Error because we mock Ctl-D. self.assertRaises(exc.CommandError, glance_shell.main, ['image-list']) # Make sure we are actually prompted. mock_getpass.assert_called_with('OS Password: ') @mock.patch( 'glanceclient.shell.OpenStackImagesShell._get_keystone_auth_plugin') @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) def test_no_auth_with_proj_name(self, cache_schemas, session): with mock.patch('glanceclient.v2.client.Client'): args = ('--os-project-name myname ' '--os-project-domain-name mydomain ' '--os-project-domain-id myid ' '--os-image-api-version 2 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) ((args), kwargs) = session.call_args self.assertEqual('myname', kwargs['project_name']) self.assertEqual('mydomain', kwargs['project_domain_name']) self.assertEqual('myid', kwargs['project_domain_id']) @mock.patch.object(openstack_shell.OpenStackImagesShell, 'main') def test_shell_keyboard_interrupt(self, mock_glance_shell): # Ensure that exit code is 130 for KeyboardInterrupt try: mock_glance_shell.side_effect = KeyboardInterrupt() openstack_shell.main() except SystemExit as ex: self.assertEqual(130, ex.code) @mock.patch('glanceclient.common.utils.exit', side_effect=utils.exit) def test_shell_illegal_version(self, mock_exit): # Only int versions are allowed on cli shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 1.1 image-list' try: shell.main(argstr.split()) except SystemExit as ex: self.assertEqual(1, ex.code) msg = ("Invalid API version parameter. " "Supported values are %s" % openstack_shell.SUPPORTED_VERSIONS) mock_exit.assert_called_with(msg=msg) @mock.patch('glanceclient.common.utils.exit', side_effect=utils.exit) def test_shell_unsupported_version(self, mock_exit): # Test an integer version which is not supported (-1) shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version -1 image-list' try: shell.main(argstr.split()) except SystemExit as ex: self.assertEqual(1, ex.code) msg = ("Invalid API version parameter. " "Supported values are %s" % openstack_shell.SUPPORTED_VERSIONS) mock_exit.assert_called_with(msg=msg) @mock.patch.object(openstack_shell.OpenStackImagesShell, 'get_subcommand_parser') def test_shell_import_error_with_mesage(self, mock_parser): msg = 'Unable to import module xxx' mock_parser.side_effect = ImportError('%s' % msg) shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 image-list' try: shell.main(argstr.split()) self.fail('No import error returned') except ImportError as e: self.assertEqual(msg, str(e)) @mock.patch.object(openstack_shell.OpenStackImagesShell, 'get_subcommand_parser') def test_shell_import_error_default_message(self, mock_parser): mock_parser.side_effect = ImportError shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 image-list' try: shell.main(argstr.split()) self.fail('No import error returned') except ImportError as e: msg = 'Unable to import module. Re-run with --debug for more info.' self.assertEqual(msg, str(e)) @mock.patch('glanceclient.v2.client.Client') @mock.patch('glanceclient.v1.images.ImageManager.list') def test_shell_v1_fallback_from_v2(self, v1_imgs, v2_client): self.make_env() cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': []}) args = 'image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertFalse(cli2.schemas.get.called) self.assertTrue(v1_imgs.called) @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') @mock.patch('glanceclient.v2.client.Client') def test_shell_no_fallback_from_v2(self, v2_client, cache_schemas): self.make_env() cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': [{'id': 'v2'}]}) cache_schemas.return_value = False args = 'image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertTrue(cli2.images.list.called) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_without_username_with_v1(self, v1_client): self.make_env(exclude='OS_USERNAME') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_without_username_with_v2(self, v2_client): self.make_env(exclude='OS_USERNAME') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_without_auth_url_with_v1(self, v1_client): self.make_env(exclude='OS_AUTH_URL') args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_without_auth_url_with_v2(self, v2_client): self.make_env(exclude='OS_AUTH_URL') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_without_tenant_with_v1(self, v1_client): if 'OS_TENANT_NAME' in os.environ: self.make_env(exclude='OS_TENANT_NAME') if 'OS_PROJECT_ID' in os.environ: self.make_env(exclude='OS_PROJECT_ID') args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) def test_auth_plugin_invocation_without_tenant_with_v2(self, v2_client, cache_schemas): if 'OS_TENANT_NAME' in os.environ: self.make_env(exclude='OS_TENANT_NAME') if 'OS_PROJECT_ID' in os.environ: self.make_env(exclude='OS_PROJECT_ID') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('sys.argv', ['glance']) @mock.patch('sys.stdout', six.StringIO()) @mock.patch('sys.stderr', six.StringIO()) def test_main_noargs(self): # Ensure that main works with no command-line arguments try: openstack_shell.main() except SystemExit: self.fail('Unexpected SystemExit') # We expect the normal v2 usage as a result expected = ['Command-line interface to the OpenStack Images API', 'image-list', 'image-deactivate', 'location-add'] for output in expected: self.assertIn(output, sys.stdout.getvalue()) @mock.patch('glanceclient.v2.client.Client') @mock.patch('glanceclient.v1.shell.do_image_list') @mock.patch('glanceclient.shell.logging.basicConfig') def test_setup_debug(self, conf, func, v2_client): cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': []}) args = '--debug image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) glance_logger = logging.getLogger('glanceclient') self.assertEqual(glance_logger.getEffectiveLevel(), logging.DEBUG) conf.assert_called_with(level=logging.DEBUG) class ShellTestWithKeystoneV3Auth(ShellTest): # auth environment to use auth_env = FAKE_V3_ENV.copy() token_url = DEFAULT_V3_AUTH_URL + '/auth/tokens' def _assert_auth_plugin_args(self): self.assertFalse(self.v2_auth.called) body = json.loads(self.v3_auth.last_request.body) user = body['auth']['identity']['password']['user'] self.assertEqual(self.auth_env['OS_USERNAME'], user['name']) self.assertEqual(self.auth_env['OS_PASSWORD'], user['password']) self.assertEqual(self.auth_env['OS_USER_DOMAIN_NAME'], user['domain']['name']) self.assertEqual(self.auth_env['OS_PROJECT_ID'], body['auth']['scope']['project']['id']) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v3_auth.call_count) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_with_v2(self, v2_client): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v3_auth.call_count) @mock.patch('keystoneauth1.discover.Discover', side_effect=ks_exc.ClientException()) def test_api_discovery_failed_with_unversioned_auth_url(self, discover): args = ('--os-image-api-version 2 --os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL) glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) def test_bash_completion(self): stdout, stderr = self.shell('--os-image-api-version 2 bash_completion') # just check we have some output required = [ '--status', 'image-create', 'help', '--size'] for r in required: self.assertIn(r, stdout.split()) avoided = [ 'bash_completion', 'bash-completion'] for r in avoided: self.assertNotIn(r, stdout.split()) class ShellTestWithNoOSImageURLPublic(ShellTestWithKeystoneV3Auth): # auth environment to use # default uses public auth_env = FAKE_V4_ENV.copy() def setUp(self): super(ShellTestWithNoOSImageURLPublic, self).setUp() self.image_url = DEFAULT_IMAGE_URL self.requests.get(DEFAULT_IMAGE_URL + 'v2/images', text='{"images": []}') @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(1, self.v3_auth.call_count) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_with_v2(self, v2_client): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(1, self.v3_auth.call_count) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') def test_endpoint_from_interface(self, v2_client): args = ('--os-image-api-version 2 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) assert v2_client.called (args, kwargs) = v2_client.call_args self.assertEqual(self.image_url, kwargs['endpoint_override']) def test_endpoint_real_from_interface(self): args = ('--os-image-api-version 2 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(self.requests.request_history[2].url, self.image_url + "v2/images?limit=20&" "sort_key=name&sort_dir=asc") class ShellTestWithNoOSImageURLInternal(ShellTestWithNoOSImageURLPublic): # auth environment to use # this uses internal FAKE_V5_ENV = FAKE_V4_ENV.copy() FAKE_V5_ENV['OS_ENDPOINT_TYPE'] = 'internal' auth_env = FAKE_V5_ENV.copy() def setUp(self): super(ShellTestWithNoOSImageURLPublic, self).setUp() self.image_url = DEFAULT_IMAGE_URL_INTERNAL self.requests.get(DEFAULT_IMAGE_URL_INTERNAL + 'v2/images', text='{"images": []}') class ShellCacheSchemaTest(testutils.TestCase): def setUp(self): super(ShellCacheSchemaTest, self).setUp() self._mock_client_setup() self._mock_shell_setup() self.cache_dir = '/dir_for_cached_schema' self.os_auth_url = 'http://localhost:5000/v2' url_hex = hashlib.sha1(self.os_auth_url.encode('utf-8')).hexdigest() self.prefix_path = (self.cache_dir + '/' + url_hex) self.cache_files = [self.prefix_path + '/image_schema.json', self.prefix_path + '/namespace_schema.json', self.prefix_path + '/resource_type_schema.json'] def tearDown(self): super(ShellCacheSchemaTest, self).tearDown() def _mock_client_setup(self): self.schema_dict = { 'name': 'image', 'properties': { 'name': {'type': 'string', 'description': 'Name of image'}, }, } self.client = mock.Mock() schema_odict = OrderedDict(self.schema_dict) self.client.schemas.get.return_value = schemas.Schema(schema_odict) def _mock_shell_setup(self): self.shell = openstack_shell.OpenStackImagesShell() self.shell._get_versioned_client = mock.create_autospec( self.shell._get_versioned_client, return_value=self.client, spec_set=True ) def _make_args(self, args): class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_gets_when_forced(self, exists_mock): options = { 'get_schema': True, 'os_auth_url': self.os_auth_url } schema_odict = OrderedDict(self.schema_dict) args = self._make_args(options) client = self.shell._get_versioned_client('2', args) self.shell._cache_schemas(args, client, home_dir=self.cache_dir) self.assertEqual(12, open.mock_calls.__len__()) self.assertEqual(mock.call(self.cache_files[0], 'w'), open.mock_calls[0]) self.assertEqual(mock.call(self.cache_files[1], 'w'), open.mock_calls[4]) self.assertEqual(mock.call().write(json.dumps(schema_odict)), open.mock_calls[2]) self.assertEqual(mock.call().write(json.dumps(schema_odict)), open.mock_calls[6]) @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', side_effect=[True, False, False, False]) def test_cache_schemas_gets_when_not_exists(self, exists_mock): options = { 'get_schema': False, 'os_auth_url': self.os_auth_url } schema_odict = OrderedDict(self.schema_dict) args = self._make_args(options) client = self.shell._get_versioned_client('2', args) self.shell._cache_schemas(args, client, home_dir=self.cache_dir) self.assertEqual(12, open.mock_calls.__len__()) self.assertEqual(mock.call(self.cache_files[0], 'w'), open.mock_calls[0]) self.assertEqual(mock.call(self.cache_files[1], 'w'), open.mock_calls[4]) self.assertEqual(mock.call().write(json.dumps(schema_odict)), open.mock_calls[2]) self.assertEqual(mock.call().write(json.dumps(schema_odict)), open.mock_calls[6]) @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_leaves_when_present_not_forced(self, exists_mock): options = { 'get_schema': False, 'os_auth_url': self.os_auth_url } client = mock.MagicMock() self.shell._cache_schemas(self._make_args(options), client, home_dir=self.cache_dir) exists_mock.assert_has_calls([ mock.call(self.prefix_path), mock.call(self.cache_files[0]), mock.call(self.cache_files[1]), mock.call(self.cache_files[2]) ]) self.assertEqual(4, exists_mock.call_count) self.assertEqual(0, open.mock_calls.__len__()) @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_leaves_auto_switch(self, exists_mock): options = { 'get_schema': True, 'os_auth_url': self.os_auth_url } self.client.schemas.get.return_value = Exception() client = mock.MagicMock() switch_version = self.shell._cache_schemas(self._make_args(options), client, home_dir=self.cache_dir) self.assertEqual(True, switch_version) class ShellTestRequests(testutils.TestCase): """Shell tests using the requests mock library.""" def _make_args(self, args): # NOTE(venkatesh): this conversion from a dict to an object # is required because the test_shell.do_xxx(gc, args) methods # expects the args to be attributes of an object. If passed as # dict directly, it throws an AttributeError. class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) def setUp(self): super(ShellTestRequests, self).setUp() self._old_env = os.environ os.environ = {} def tearDown(self): super(ShellTestRequests, self).tearDown() os.environ = self._old_env def test_download_has_no_stray_output_to_stdout(self): """Regression test for bug 1488914""" saved_stdout = sys.stdout try: sys.stdout = output = testutils.FakeNoTTYStdout() id = image_show_fixture['id'] self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/versions', json=image_versions_fixture) headers = {'Content-Length': '4', 'Content-type': 'application/octet-stream'} fake = testutils.FakeResponse(headers, six.StringIO('DATA')) self.requests.get('http://example.com/v1/images/%s' % id, raw=fake) self.requests.get('http://example.com/v1/images/detail' '?sort_key=name&sort_dir=asc&limit=20') headers = {'X-Image-Meta-Id': id} self.requests.head('http://example.com/v1/images/%s' % id, headers=headers) with mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') as mocked_cache_schema: mocked_cache_schema.return_value = True shell = openstack_shell.OpenStackImagesShell() argstr = ('--os-auth-token faketoken ' '--os-image-url http://example.com ' 'image-download %s' % id) shell.main(argstr.split()) self.assertTrue(mocked_cache_schema.called) # Ensure we have *only* image data self.assertEqual('DATA', output.getvalue()) finally: sys.stdout = saved_stdout def test_v1_download_has_no_stray_output_to_stdout(self): """Ensure no stray print statements corrupt the image""" saved_stdout = sys.stdout try: sys.stdout = output = testutils.FakeNoTTYStdout() id = image_show_fixture['id'] self.requests = self.useFixture(rm_fixture.Fixture()) headers = {'X-Image-Meta-Id': id} self.requests.head('http://example.com/v1/images/%s' % id, headers=headers) headers = {'Content-Length': '4', 'Content-type': 'application/octet-stream'} fake = testutils.FakeResponse(headers, six.StringIO('DATA')) self.requests.get('http://example.com/v1/images/%s' % id, headers=headers, raw=fake) shell = openstack_shell.OpenStackImagesShell() argstr = ('--os-image-api-version 1 --os-auth-token faketoken ' '--os-image-url http://example.com ' 'image-download %s' % id) shell.main(argstr.split()) # Ensure we have *only* image data self.assertEqual('DATA', output.getvalue()) finally: sys.stdout = saved_stdout def test_v2_download_has_no_stray_output_to_stdout(self): """Ensure no stray print statements corrupt the image""" saved_stdout = sys.stdout try: sys.stdout = output = testutils.FakeNoTTYStdout() id = image_show_fixture['id'] headers = {'Content-Length': '4', 'Content-type': 'application/octet-stream'} fake = testutils.FakeResponse(headers, six.StringIO('DATA')) self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/images/%s/file' % id, headers=headers, raw=fake) shell = openstack_shell.OpenStackImagesShell() argstr = ('--os-image-api-version 2 --os-auth-token faketoken ' '--os-image-url http://example.com ' 'image-download %s' % id) shell.main(argstr.split()) # Ensure we have *only* image data self.assertEqual('DATA', output.getvalue()) finally: sys.stdout = saved_stdout python-glanceclient-2.9.1/glanceclient/tests/unit/test_ssl.py0000666000175100017510000002171113232161652024506 0ustar zuulzuul00000000000000# 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 os import mock import six import ssl import testtools import threading from glanceclient import Client from glanceclient import exc from glanceclient import v1 from glanceclient import v2 if six.PY3 is True: import socketserver else: import SocketServer as socketserver TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'var')) class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): self.request.recv(1024) response = b'somebytes' self.request.sendall(response) class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): def get_request(self): key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') (_sock, addr) = socketserver.TCPServer.get_request(self) sock = ssl.wrap_socket(_sock, certfile=cert_file, keyfile=key_file, ca_certs=cacert, server_side=True, cert_reqs=ssl.CERT_REQUIRED) return sock, addr class TestHTTPSVerifyCert(testtools.TestCase): """Check 'requests' based ssl verification occurs. The requests library performs SSL certificate validation, however there is still a need to check that the glance client is properly integrated with requests so that cert validation actually happens. """ def setUp(self): # Rather than spinning up a new process, we create # a thread to perform client/server interaction. # This should run more quickly. super(TestHTTPSVerifyCert, self).setUp() server = ThreadedTCPServer(('127.0.0.1', 0), ThreadedTCPRequestHandler) __, self.port = server.server_address server_thread = threading.Thread(target=server.serve_forever) server_thread.daemon = True server_thread.start() @mock.patch('sys.stderr') def test_v1_requests_cert_verification(self, __): """v1 regression test for bug 115260.""" port = self.port url = 'https://0.0.0.0:%d' % port try: client = v1.Client(url, insecure=False, ssl_compression=True) client.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v1_requests_cert_verification_no_compression(self, __): """v1 regression test for bug 115260.""" # Legacy test. Verify 'no compression' has no effect port = self.port url = 'https://0.0.0.0:%d' % port try: client = v1.Client(url, insecure=False, ssl_compression=False) client.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_cert_verification(self, __): """v2 regression test for bug 115260.""" port = self.port url = 'https://0.0.0.0:%d' % port try: gc = v2.Client(url, insecure=False, ssl_compression=True) gc.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_cert_verification_no_compression(self, __): """v2 regression test for bug 115260.""" # Legacy test. Verify 'no compression' has no effect port = self.port url = 'https://0.0.0.0:%d' % port try: gc = v2.Client(url, insecure=False, ssl_compression=False) gc.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_valid_cert_verification(self, __): """Test absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=True, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if 'certificate verify failed' in e.message: self.fail('Certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_valid_cert_verification_no_compression(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if 'certificate verify failed' in e.message: self.fail('Certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_valid_cert_no_key(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cert_file=cert_file, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if ('PEM lib' not in e.message): self.fail('No appropriate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_bad_cert(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cert_file=cert_file, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: # NOTE(dsariel) # starting from python 2.7.8 the way to handle loading private # keys into the SSL_CTX was changed and error message become # similar to the one in 3.X if (six.PY2 and 'PrivateKey' not in e.message and 'PEM lib' not in e.message or six.PY3 and 'PEM lib' not in e.message): self.fail('No appropriate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_bad_ca(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cacert = os.path.join(TEST_VAR_DIR, 'badca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if 'invalid path' not in e.message: raise python-glanceclient-2.9.1/glanceclient/tests/unit/v2/0000775000175100017510000000000013232162126022614 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/v2/fixtures.py0000666000175100017510000002737013232161652025055 0ustar zuulzuul00000000000000# Copyright (c) 2015 OpenStack Foundation # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # 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. UUID = "3fc2ba62-9a02-433e-b565-d493ffc69034" image_list_fixture = { "images": [ { "checksum": "9cb02fe7fcac26f8a25d6db3109063ae", "container_format": "bare", "created_at": "2015-07-23T16:58:50.000000", "deleted": "false", "deleted_at": "null", "disk_format": "raw", "id": UUID, "is_public": "false", "min_disk": 0, "min_ram": 0, "name": "test", "owner": "3447cea05d6947658d73791ed9e0ed9f", "properties": { "kernel_id": 1234, "ramdisk_id": 5678 }, "protected": "false", "size": 145, "status": "active", "updated_at": "2015-07-23T16:58:51.000000", "virtual_size": "null" } ] } image_show_fixture = { "checksum": "9cb02fe7fcac26f8a25d6db3109063ae", "container_format": "bare", "created_at": "2015-07-24T12:18:13Z", "disk_format": "raw", "file": "/v2/images/%s/file" % UUID, "id": UUID, "kernel_id": "1234", "min_disk": 0, "min_ram": 0, "name": "img1", "owner": "411423405e10431fb9c47ac5b2446557", "protected": "false", "ramdisk_id": "5678", "schema": "/v2/schemas/image", "self": "/v2/images/%s" % UUID, "size": 145, "status": "active", "tags": [], "updated_at": "2015-07-24T12:18:13Z", "virtual_size": "null", "visibility": "shared" } image_create_fixture = { "checksum": "9cb02fe7fcac26f8a25d6db3109063ae", "container_format": "bare", "created_at": "2015-07-24T12:18:13Z", "disk_format": "raw", "file": "/v2/images/%s/file" % UUID, "id": UUID, "kernel_id": "af81fccd-b2e8-4232-886c-aa98dda22882", "min_disk": 0, "min_ram": 0, "name": "img1", "owner": "411423405e10431fb9c47ac5b2446557", "protected": False, "ramdisk_id": "fdb3f864-9458-4185-bd26-5d27fe6b6adf", "schema": "/v2/schemas/image", "self": "/v2/images/%s" % UUID, "size": 145, "status": "active", "tags": [], "updated_at": "2015-07-24T12:18:13Z", "virtual_size": 123, "visibility": "private" } schema_fixture = { "additionalProperties": { "type": "string" }, "links": [ { "href": "{self}", "rel": "self" }, { "href": "{file}", "rel": "enclosure" }, { "href": "{schema}", "rel": "describedby" } ], "name": "image", "properties": { "architecture": { "description": "Operating system architecture as specified in " "http://docs.openstack.org/user-guide/common" "/cli_manage_images.html", "is_base": "false", "type": "string" }, "checksum": { "readOnly": True, "description": "md5 hash of image contents.", "maxLength": 32, "type": [ "null", "string" ] }, "container_format": { "description": "Format of the container", "enum": [ "null", "ami", "ari", "aki", "bare", "ovf", "ova", "docker" ], "type": [ "null", "string" ] }, "created_at": { "readOnly": True, "description": "Date and time of image registration", "type": "string" }, "direct_url": { "readOnly": True, "description": "URL to access the image file kept in external " "store", "type": "string" }, "disk_format": { "description": "Format of the disk", "enum": [ "null", "ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso", "ploop" ], "type": [ "null", "string" ] }, "file": { "readOnly": True, "description": "An image file url", "type": "string" }, "id": { "description": "An identifier for the image", "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": "string" }, "instance_uuid": { "description": ("Metadata which can be used to record which " "instance this image is associated with. " "(Informational only, does not create an instance " "snapshot.)"), "is_base": "false", "type": "string" }, "kernel_id": { "description": "ID of image stored in Glance that should be used " "as the kernel when booting an AMI-style image.", "is_base": "false", "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": [ "null", "string" ] }, "locations": { "description": "A set of URLs to access the image file kept " "in external store", "items": { "properties": { "metadata": { "type": "object" }, "url": { "maxLength": 255, "type": "string" } }, "required": [ "url", "metadata" ], "type": "object" }, "type": "array" }, "min_disk": { "description": "Amount of disk space (in GB) required to " "boot image.", "type": "integer" }, "min_ram": { "description": "Amount of ram (in MB) required to boot image.", "type": "integer" }, "name": { "description": "Descriptive name for the image", "maxLength": 255, "type": [ "null", "string" ] }, "os_distro": { "description": "Common name of operating system distribution as " "specified in http://docs.openstack.org/trunk/" "openstack-compute/admin/content/" "adding-images.html", "is_base": "false", "type": "string" }, "os_version": { "description": "Operating system version as specified " "by the distributor", "is_base": "false", "type": "string" }, "owner": { "description": "Owner of the image", "maxLength": 255, "type": [ "null", "string" ] }, "protected": { "description": "If true, image will not be deletable.", "type": "boolean" }, "ramdisk_id": { "description": "ID of image stored in Glance that should be used " "as the ramdisk when booting an AMI-style image.", "is_base": "false", "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": [ "null", "string" ] }, "schema": { "readOnly": True, "description": "An image schema url", "type": "string" }, "self": { "readOnly": True, "description": "An image self url", "type": "string" }, "size": { "readOnly": True, "description": "Size of image file in bytes", "type": [ "null", "integer" ] }, "status": { "readOnly": True, "description": "Status of the image", "enum": [ "queued", "saving", "active", "killed", "deleted", "pending_delete" ], "type": "string" }, "tags": { "description": "List of strings related to the image", "items": { "maxLength": 255, "type": "string" }, "type": "array" }, "updated_at": { "readOnly": True, "description": "Date and time of the last image " "modification", "type": "string" }, "virtual_size": { "readOnly": True, "description": "Virtual size of image in bytes", "type": [ "null", "integer" ] }, "visibility": { "description": "Scope of image accessibility", "enum": [ "public", "private", "community", "shared" ], "type": "string" } } } image_versions_fixture = { "versions": [ { "id": "v2.3", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "CURRENT" }, { "id": "v2.2", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v2.1", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v2.0", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v1.1", "links": [ { "href": "http://localhost:9292/v1/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v1.0", "links": [ { "href": "http://localhost:9292/v1/", "rel": "self" } ], "status": "SUPPORTED" } ] } python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_client_requests.py0000666000175100017510000000753213232161652027452 0ustar zuulzuul00000000000000# Copyright (c) 2015 OpenStack Foundation # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # 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 requests_mock.contrib import fixture as rm_fixture from glanceclient import client from glanceclient.tests.unit.v2.fixtures import image_create_fixture from glanceclient.tests.unit.v2.fixtures import image_list_fixture from glanceclient.tests.unit.v2.fixtures import image_show_fixture from glanceclient.tests.unit.v2.fixtures import schema_fixture from glanceclient.tests import utils as testutils from glanceclient.v2.image_schema import _BASE_SCHEMA class ClientTestRequests(testutils.TestCase): """Client tests using the requests mock library.""" def test_list_bad_image_schema(self): # if kernel_id or ramdisk_id are not uuids, verify we can # still perform an image listing. Regression test for bug # 1477910 self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=schema_fixture) self.requests.get('http://example.com/v2/images?limit=20', json=image_list_fixture) gc = client.Client(2.2, "http://example.com/v2.1") images = gc.images.list() for image in images: pass def test_show_bad_image_schema(self): # if kernel_id or ramdisk_id are not uuids, verify we # don't fail due to schema validation self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=schema_fixture) self.requests.get('http://example.com/v2/images/%s' % image_show_fixture['id'], json=image_show_fixture) gc = client.Client(2.2, "http://example.com/v2.1") img = gc.images.get(image_show_fixture['id']) self.assertEqual(image_show_fixture['checksum'], img['checksum']) def test_invalid_disk_format(self): self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=_BASE_SCHEMA) self.requests.post('http://example.com/v2/images', json=image_create_fixture) self.requests.get('http://example.com/v2/images/%s' % image_show_fixture['id'], json=image_show_fixture) gc = client.Client(2.2, "http://example.com/v2.1") fields = {"disk_format": "qbull2"} try: gc.images.create(**fields) self.fail("Failed to raise exception when using bad disk format") except TypeError: pass def test_valid_disk_format(self): self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=_BASE_SCHEMA) self.requests.post('http://example.com/v2/images', json=image_create_fixture) self.requests.get('http://example.com/v2/images/%s' % image_show_fixture['id'], json=image_show_fixture) gc = client.Client(2.2, "http://example.com/v2.1") fields = {"disk_format": "vhdx"} gc.images.create(**fields) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_metadefs_objects.py0000666000175100017510000003013713232161652027537 0ustar zuulzuul00000000000000# 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' OBJECT1 = 'Object1' OBJECT2 = 'Object2' OBJECTNEW = 'ObjectNew' PROPERTY1 = 'Property1' PROPERTY2 = 'Property2' PROPERTY3 = 'Property3' PROPERTY4 = 'Property4' def _get_object_fixture(ns_name, obj_name, **kwargs): obj = { "description": "DESCRIPTION", "name": obj_name, "self": "/v2/metadefs/namespaces/%s/objects/%s" % (ns_name, obj_name), "required": [], "properties": { PROPERTY1: { "type": "integer", "description": "DESCRIPTION", "title": "Quota: CPU Shares" }, PROPERTY2: { "minimum": 1000, "type": "integer", "description": "DESCRIPTION", "maximum": 1000000, "title": "Quota: CPU Period" }}, "schema": "/v2/schemas/metadefs/object", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } obj.update(kwargs) return obj data_fixtures = { "/v2/metadefs/namespaces/%s/objects" % NAMESPACE1: { "GET": ( {}, { "objects": [ _get_object_fixture(NAMESPACE1, OBJECT1), _get_object_fixture(NAMESPACE1, OBJECT2) ], "schema": "v2/schemas/metadefs/objects" } ), "POST": ( {}, _get_object_fixture(NAMESPACE1, OBJECTNEW) ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/objects/%s" % (NAMESPACE1, OBJECT1): { "GET": ( {}, _get_object_fixture(NAMESPACE1, OBJECT1) ), "PUT": ( {}, _get_object_fixture(NAMESPACE1, OBJECT1) ), "DELETE": ( {}, {} ) } } schema_fixtures = { "metadefs/object": { "GET": ( {}, { "additionalProperties": False, "definitions": { "property": { "additionalProperties": { "required": [ "title", "type" ], "type": "object", "properties": { "additionalItems": { "type": "boolean" }, "enum": { "type": "array" }, "description": { "type": "string" }, "title": { "type": "string" }, "default": {}, "minLength": { "$ref": "#/definitions/positiveInteger" "Default0" }, "required": { "$ref": "#/definitions/stringArray" }, "maximum": { "type": "number" }, "minItems": { "$ref": "#/definitions/positiveInteger" "Default0" }, "readonly": { "type": "boolean" }, "minimum": { "type": "number" }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "uniqueItems": { "default": False, "type": "boolean" }, "pattern": { "type": "string", "format": "regex" }, "items": { "type": "object", "properties": { "enum": { "type": "array" }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": "object" }, "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "stringArray": { "uniqueItems": True, "items": { "type": "string" }, "type": "array" }, "positiveInteger": { "minimum": 0, "type": "integer" } }, "required": [ "name" ], "name": "object", "properties": { "created_at": { "type": "string", "readOnly": True, "description": "Date and time of object creation ", "format": "date-time" }, "description": { "type": "string" }, "name": { "type": "string" }, "self": { "type": "string" }, "required": { "$ref": "#/definitions/stringArray" }, "properties": { "$ref": "#/definitions/property" }, "schema": { "type": "string" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last object " "modification", "format": "date-time" }, } } ) } } class TestObjectController(testtools.TestCase): def setUp(self): super(TestObjectController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.ObjectController) def test_list_object(self): objects = self.controller.list(NAMESPACE1) actual = [obj.name for obj in objects] self.assertEqual([OBJECT1, OBJECT2], actual) def test_get_object(self): obj = self.controller.get(NAMESPACE1, OBJECT1) self.assertEqual(OBJECT1, obj.name) self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(list(obj.properties.keys()))) def test_create_object(self): properties = { 'name': OBJECTNEW, 'description': 'DESCRIPTION' } obj = self.controller.create(NAMESPACE1, **properties) self.assertEqual(OBJECTNEW, obj.name) def test_create_object_invalid_property(self): properties = { 'namespace': NAMESPACE1 } self.assertRaises(TypeError, self.controller.create, **properties) def test_update_object(self): properties = { 'description': 'UPDATED_DESCRIPTION' } obj = self.controller.update(NAMESPACE1, OBJECT1, **properties) self.assertEqual(OBJECT1, obj.name) def test_update_object_invalid_property(self): properties = { 'required': 'INVALID' } self.assertRaises(TypeError, self.controller.update, NAMESPACE1, OBJECT1, **properties) def test_update_object_disallowed_fields(self): properties = { 'description': 'UPDATED_DESCRIPTION' } self.controller.update(NAMESPACE1, OBJECT1, **properties) actual = self.api.calls # API makes three calls(GET, PUT, GET) for object update. # PUT has the request body in the list '''('PUT', '/v2/metadefs/namespaces/Namespace1/objects/Object1', {}, [('description', 'UPDATED_DESCRIPTION'), ('name', 'Object1'), ('properties', ...), ('required', [])])''' _disallowed_fields = ['self', 'schema', 'created_at', 'updated_at'] for key in actual[1][3]: self.assertNotIn(key, _disallowed_fields) def test_delete_object(self): self.controller.delete(NAMESPACE1, OBJECT1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/objects/%s' % (NAMESPACE1, OBJECT1), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_all_objects(self): self.controller.delete_all(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/objects' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_schemas.py0000666000175100017510000001613113232161652025657 0ustar zuulzuul00000000000000# 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 jsonpatch import testtools import warlock from glanceclient.tests import utils from glanceclient.v2 import schemas fixtures = { '/v2/schemas': { 'GET': ( {}, { 'image': '/v2/schemas/image', 'access': '/v2/schemas/image/access', }, ), }, '/v2/schemas/image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'name': {'type': 'string', 'description': 'Name of image'}, 'tags': {'type': 'array'} }, }, ), }, } _SCHEMA = schemas.Schema({ 'name': 'image', 'properties': { 'name': {'type': 'string'}, 'color': {'type': 'string'}, 'shape': {'type': 'string', 'is_base': False}, 'tags': {'type': 'array'} }, }) def compare_json_patches(a, b): """Return 0 if a and b describe the same JSON patch.""" return(jsonpatch.JsonPatch.from_string(a) == jsonpatch.JsonPatch.from_string(b)) class TestSchemaProperty(testtools.TestCase): def test_property_minimum(self): prop = schemas.SchemaProperty('size') self.assertEqual('size', prop.name) def test_property_description(self): prop = schemas.SchemaProperty('size', description='some quantity') self.assertEqual('size', prop.name) self.assertEqual('some quantity', prop.description) def test_property_is_base(self): prop1 = schemas.SchemaProperty('name') prop2 = schemas.SchemaProperty('foo', is_base=False) prop3 = schemas.SchemaProperty('foo', is_base=True) self.assertTrue(prop1.is_base) self.assertFalse(prop2.is_base) self.assertTrue(prop3.is_base) class TestSchema(testtools.TestCase): def test_schema_minimum(self): raw_schema = {'name': 'Country', 'properties': {}} schema = schemas.Schema(raw_schema) self.assertEqual('Country', schema.name) self.assertEqual([], schema.properties) def test_schema_with_property(self): raw_schema = {'name': 'Country', 'properties': {'size': {}}} schema = schemas.Schema(raw_schema) self.assertEqual('Country', schema.name) self.assertEqual(['size'], [p.name for p in schema.properties]) def test_raw(self): raw_schema = {'name': 'Country', 'properties': {}} schema = schemas.Schema(raw_schema) self.assertEqual(raw_schema, schema.raw()) def test_property_is_base(self): raw_schema = {'name': 'Country', 'properties': { 'size': {}, 'population': {'is_base': False}}} schema = schemas.Schema(raw_schema) self.assertTrue(schema.is_base_property('size')) self.assertFalse(schema.is_base_property('population')) self.assertFalse(schema.is_base_property('foo')) class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(fixtures) self.controller = schemas.Controller(self.api) def test_get_schema(self): schema = self.controller.get('image') self.assertEqual('image', schema.name) self.assertEqual(set(['name', 'tags']), set([p.name for p in schema.properties])) class TestSchemaBasedModel(testtools.TestCase): def setUp(self): super(TestSchemaBasedModel, self).setUp() self.model = warlock.model_factory(_SCHEMA.raw(), base_class=schemas.SchemaBasedModel) def test_patch_should_replace_missing_core_properties(self): obj = { 'name': 'fred' } original = self.model(obj) original['color'] = 'red' patch = original.patch expected = '[{"path": "/color", "value": "red", "op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_add_extra_properties(self): obj = { 'name': 'fred', } original = self.model(obj) original['weight'] = '10' patch = original.patch expected = '[{"path": "/weight", "value": "10", "op": "add"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_replace_extra_properties(self): obj = { 'name': 'fred', 'weight': '10' } original = self.model(obj) original['weight'] = '22' patch = original.patch expected = '[{"path": "/weight", "value": "22", "op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_remove_extra_properties(self): obj = { 'name': 'fred', 'weight': '10' } original = self.model(obj) del original['weight'] patch = original.patch expected = '[{"path": "/weight", "op": "remove"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_remove_core_properties(self): obj = { 'name': 'fred', 'color': 'red' } original = self.model(obj) del original['color'] patch = original.patch expected = '[{"path": "/color", "op": "remove"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_add_missing_custom_properties(self): obj = { 'name': 'fred' } original = self.model(obj) original['shape'] = 'circle' patch = original.patch expected = '[{"path": "/shape", "value": "circle", "op": "add"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_replace_custom_properties(self): obj = { 'name': 'fred', 'shape': 'circle' } original = self.model(obj) original['shape'] = 'square' patch = original.patch expected = '[{"path": "/shape", "value": "square", "op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_replace_tags(self): obj = {'name': 'fred', } original = self.model(obj) original['tags'] = ['tag1', 'tag2'] patch = original.patch expected = '[{"path": "/tags", "value": ["tag1", "tag2"], ' \ '"op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/base.py0000666000175100017510000001012113232161652024100 0ustar zuulzuul00000000000000# Copyright 2016 NTT DATA # # 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 BaseController(testtools.TestCase): def __init__(self, api, schema_api, controller_class): self.controller = controller_class(api, schema_api) def _assertRequestId(self, obj): self.assertIsNotNone(getattr(obj, 'request_ids', None)) self.assertEqual(['req-1234'], obj.request_ids) def list(self, *args, **kwargs): gen_obj = self.controller.list(*args, **kwargs) # For generator cases the request_ids property will be an empty list # until the underlying generator is invoked at-least once. resources = list(gen_obj) if len(resources) > 0: self._assertRequestId(gen_obj) else: # If list is empty that means geneator object has raised # StopIteration for first iteration and will not contain the # request_id in it. self.assertEqual([], gen_obj.request_ids) return resources def get(self, *args, **kwargs): resource = self.controller.get(*args, **kwargs) self._assertRequestId(resource) return resource def create(self, *args, **kwargs): resource = self.controller.create(*args, **kwargs) self._assertRequestId(resource) return resource def create_multiple(self, *args, **kwargs): tags = self.controller.create_multiple(*args, **kwargs) actual = [tag.name for tag in tags] self._assertRequestId(tags) return actual def update(self, *args, **properties): resource = self.controller.update(*args, **properties) self._assertRequestId(resource) return resource def delete(self, *args): resp = self.controller.delete(*args) self._assertRequestId(resp) def delete_all(self, *args): resp = self.controller.delete_all(*args) self._assertRequestId(resp) def deactivate(self, *args): resp = self.controller.deactivate(*args) self._assertRequestId(resp) def reactivate(self, *args): resp = self.controller.reactivate(*args) self._assertRequestId(resp) def upload(self, *args, **kwargs): resp = self.controller.upload(*args, **kwargs) self._assertRequestId(resp) def data(self, *args, **kwargs): body = self.controller.data(*args, **kwargs) self._assertRequestId(body) return body def delete_locations(self, *args): resp = self.controller.delete_locations(*args) self._assertRequestId(resp) def add_location(self, *args, **kwargs): resp = self.controller.add_location(*args, **kwargs) self._assertRequestId(resp) def update_location(self, *args, **kwargs): resp = self.controller.update_location(*args, **kwargs) self._assertRequestId(resp) def associate(self, *args, **kwargs): resource_types = self.controller.associate(*args, **kwargs) self._assertRequestId(resource_types) return resource_types def deassociate(self, *args): resp = self.controller.deassociate(*args) self._assertRequestId(resp) class BaseResourceTypeController(BaseController): def __init__(self, api, schema_api, controller_class): super(BaseResourceTypeController, self).__init__(api, schema_api, controller_class) def get(self, *args, **kwargs): resource_types = self.controller.get(*args) names = [rt.name for rt in resource_types] self._assertRequestId(resource_types) return names python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_metadefs_tags.py0000666000175100017510000001173313232161652027045 0ustar zuulzuul00000000000000# Copyright 2015 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' TAG1 = 'Tag1' TAG2 = 'Tag2' TAGNEW1 = 'TagNew1' TAGNEW2 = 'TagNew2' TAGNEW3 = 'TagNew3' def _get_tag_fixture(tag_name, **kwargs): tag = { "name": tag_name } tag.update(kwargs) return tag data_fixtures = { "/v2/metadefs/namespaces/%s/tags" % NAMESPACE1: { "GET": ( {}, { "tags": [ _get_tag_fixture(TAG1), _get_tag_fixture(TAG2) ] } ), "POST": ( {}, { 'tags': [ _get_tag_fixture(TAGNEW2), _get_tag_fixture(TAGNEW3) ] } ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW1): { "POST": ( {}, _get_tag_fixture(TAGNEW1) ) }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG1): { "GET": ( {}, _get_tag_fixture(TAG1) ), "PUT": ( {}, _get_tag_fixture(TAG2) ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG2): { "GET": ( {}, _get_tag_fixture(TAG2) ), }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW2): { "GET": ( {}, _get_tag_fixture(TAGNEW2) ), }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW3): { "GET": ( {}, _get_tag_fixture(TAGNEW3) ), } } schema_fixtures = { "metadefs/tag": { "GET": ( {}, { "additionalProperties": True, "name": { "type": "string" }, "created_at": { "type": "string", "readOnly": True, "description": ("Date and time of tag creation"), "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": ("Date and time of the last tag" " modification"), "format": "date-time" }, 'properties': {} } ) } } class TestTagController(testtools.TestCase): def setUp(self): super(TestTagController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.TagController) def test_list_tag(self): tags = self.controller.list(NAMESPACE1) actual = [tag.name for tag in tags] self.assertEqual([TAG1, TAG2], actual) def test_get_tag(self): tag = self.controller.get(NAMESPACE1, TAG1) self.assertEqual(TAG1, tag.name) def test_create_tag(self): tag = self.controller.create(NAMESPACE1, TAGNEW1) self.assertEqual(TAGNEW1, tag.name) def test_create_multiple_tags(self): properties = { 'tags': [TAGNEW2, TAGNEW3] } tags = self.controller.create_multiple(NAMESPACE1, **properties) self.assertEqual([TAGNEW2, TAGNEW3], tags) def test_update_tag(self): properties = { 'name': TAG2 } tag = self.controller.update(NAMESPACE1, TAG1, **properties) self.assertEqual(TAG2, tag.name) def test_delete_tag(self): self.controller.delete(NAMESPACE1, TAG1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/tags/%s' % (NAMESPACE1, TAG1), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_all_tags(self): self.controller.delete_all(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/tags' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/__init__.py0000666000175100017510000000000013232161652024720 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_metadefs_properties.py0000666000175100017510000002361213232161652030302 0ustar zuulzuul00000000000000# 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' PROPERTY1 = 'Property1' PROPERTY2 = 'Property2' PROPERTYNEW = 'PropertyNew' data_fixtures = { "/v2/metadefs/namespaces/%s/properties" % NAMESPACE1: { "GET": ( {}, { "properties": { PROPERTY1: { "default": "1", "type": "integer", "description": "Number of cores.", "title": "cores" }, PROPERTY2: { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "Specifies the CPU manufacturer.", "title": "Vendor" }, } } ), "POST": ( {}, { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "UPDATED_DESCRIPTION", "title": "Vendor", "name": PROPERTYNEW } ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/properties/%s" % (NAMESPACE1, PROPERTY1): { "GET": ( {}, { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "Specifies the CPU manufacturer.", "title": "Vendor" } ), "PUT": ( {}, { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "UPDATED_DESCRIPTION", "title": "Vendor" } ), "DELETE": ( {}, {} ) } } schema_fixtures = { "metadefs/property": { "GET": ( {}, { "additionalProperties": False, "definitions": { "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "stringArray": { "minItems": 1, "items": { "type": "string" }, "uniqueItems": True, "type": "array" }, "positiveInteger": { "minimum": 0, "type": "integer" } }, "required": [ "name", "title", "type" ], "name": "property", "properties": { "description": { "type": "string" }, "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, "enum": { "type": "array" }, "minimum": { "type": "number" }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "uniqueItems": { "default": False, "type": "boolean" }, "additionalItems": { "type": "boolean" }, "name": { "type": "string" }, "title": { "type": "string" }, "default": {}, "pattern": { "type": "string", "format": "regex" }, "required": { "$ref": "#/definitions/stringArray" }, "maximum": { "type": "number" }, "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, "readonly": { "type": "boolean" }, "items": { "type": "object", "properties": { "enum": { "type": "array" }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } } ) } } class TestPropertyController(testtools.TestCase): def setUp(self): super(TestPropertyController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.PropertyController) def test_list_property(self): properties = self.controller.list(NAMESPACE1) actual = [prop.name for prop in properties] self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(actual)) def test_get_property(self): prop = self.controller.get(NAMESPACE1, PROPERTY1) self.assertEqual(PROPERTY1, prop.name) def test_create_property(self): properties = { 'name': PROPERTYNEW, 'title': 'TITLE', 'type': 'string' } obj = self.controller.create(NAMESPACE1, **properties) self.assertEqual(PROPERTYNEW, obj.name) def test_create_property_invalid_property(self): properties = { 'namespace': NAMESPACE1 } self.assertRaises(TypeError, self.controller.create, **properties) def test_update_property(self): properties = { 'description': 'UPDATED_DESCRIPTION' } prop = self.controller.update(NAMESPACE1, PROPERTY1, **properties) self.assertEqual(PROPERTY1, prop.name) def test_update_property_invalid_property(self): properties = { 'type': 'INVALID' } self.assertRaises(TypeError, self.controller.update, NAMESPACE1, PROPERTY1, **properties) def test_update_property_disallowed_fields(self): properties = { 'description': 'UPDATED_DESCRIPTION' } self.controller.update(NAMESPACE1, PROPERTY1, **properties) actual = self.api.calls _disallowed_fields = ['created_at', 'updated_at'] for key in actual[1][3]: self.assertNotIn(key, _disallowed_fields) def test_delete_property(self): self.controller.delete(NAMESPACE1, PROPERTY1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/properties/%s' % (NAMESPACE1, PROPERTY1), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_all_properties(self): self.controller.delete_all(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/properties' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_members.py0000666000175100017510000000713013232161652025665 0ustar zuulzuul00000000000000# Copyright 2013 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import image_members IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1' MEMBER = '11223344-5566-7788-9911-223344556677' data_fixtures = { '/v2/images/{image}/members'.format(image=IMAGE): { 'GET': ( {}, {'members': [ { 'image_id': IMAGE, 'member_id': MEMBER, }, ]}, ), 'POST': ( {}, { 'image_id': IMAGE, 'member_id': MEMBER, 'status': 'pending' } ) }, '/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER): { 'DELETE': ( {}, None, ), 'PUT': ( {}, { 'image_id': IMAGE, 'member_id': MEMBER, 'status': 'accepted' } ), } } schema_fixtures = { 'member': { 'GET': ( {}, { 'name': 'member', 'properties': { 'image_id': {}, 'member_id': {} } }, ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, image_members.Controller) def test_list_image_members(self): image_id = IMAGE image_members = self.controller.list(image_id) self.assertEqual(IMAGE, image_members[0].image_id) self.assertEqual(MEMBER, image_members[0].member_id) def test_delete_image_member(self): image_id = IMAGE member_id = MEMBER self.controller.delete(image_id, member_id) expect = [ ('DELETE', '/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER), {}, None)] self.assertEqual(expect, self.api.calls) def test_update_image_members(self): image_id = IMAGE member_id = MEMBER status = 'accepted' image_member = self.controller.update(image_id, member_id, status) self.assertEqual(IMAGE, image_member.image_id) self.assertEqual(MEMBER, image_member.member_id) self.assertEqual(status, image_member.status) def test_create_image_members(self): image_id = IMAGE member_id = MEMBER status = 'pending' image_member = self.controller.create(image_id, member_id) self.assertEqual(IMAGE, image_member.image_id) self.assertEqual(MEMBER, image_member.member_id) self.assertEqual(status, image_member.status) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_metadefs_resource_types.py0000666000175100017510000001613413232161652031162 0ustar zuulzuul00000000000000# 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' RESOURCE_TYPE1 = 'ResourceType1' RESOURCE_TYPE2 = 'ResourceType2' RESOURCE_TYPE3 = 'ResourceType3' RESOURCE_TYPE4 = 'ResourceType4' RESOURCE_TYPENEW = 'ResourceTypeNew' data_fixtures = { "/v2/metadefs/namespaces/%s/resource_types" % NAMESPACE1: { "GET": ( {}, { "resource_type_associations": [ { "name": RESOURCE_TYPE3, "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", }, { "name": RESOURCE_TYPE4, "prefix": "PREFIX:", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ] } ), "POST": ( {}, { "name": RESOURCE_TYPENEW, "prefix": "PREFIX:", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), }, "/v2/metadefs/namespaces/%s/resource_types/%s" % (NAMESPACE1, RESOURCE_TYPE1): { "DELETE": ( {}, {} ), }, "/v2/metadefs/resource_types": { "GET": ( {}, { "resource_types": [ { "name": RESOURCE_TYPE1, "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", }, { "name": RESOURCE_TYPE2, "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ] } ) } } schema_fixtures = { "metadefs/resource_type": { "GET": ( {}, { "name": "resource_type", "properties": { "prefix": { "type": "string", "description": "Specifies the prefix to use for the " "given resource type. Any properties " "in the namespace should be prefixed " "with this prefix when being applied " "to the specified resource type. Must " "include prefix separator (e.g. a " "colon :).", "maxLength": 80 }, "properties_target": { "type": "string", "description": "Some resource types allow more than " "one key / value pair per instance. " "For example, Cinder allows user and " "image metadata on volumes. Only the " "image properties metadata is " "evaluated by Nova (scheduling or " "drivers). This property allows a " "namespace target to remove the " "ambiguity.", "maxLength": 80 }, "name": { "type": "string", "description": "Resource type names should be " "aligned with Heat resource types " "whenever possible: http://docs." "openstack.org/developer/heat/" "template_guide/openstack.html", "maxLength": 80 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of resource type " "association", "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last resource " "type association modification ", "format": "date-time" }, } } ) } } class TestResoureTypeController(testtools.TestCase): def setUp(self): super(TestResoureTypeController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseResourceTypeController( self.api, self.schema_api, metadefs.ResourceTypeController) def test_list_resource_types(self): resource_types = self.controller.list() names = [rt.name for rt in resource_types] self.assertEqual([RESOURCE_TYPE1, RESOURCE_TYPE2], names) def test_get_resource_types(self): resource_types = self.controller.get(NAMESPACE1) self.assertEqual([RESOURCE_TYPE3, RESOURCE_TYPE4], resource_types) def test_associate_resource_types(self): resource_types = self.controller.associate(NAMESPACE1, name=RESOURCE_TYPENEW) self.assertEqual(RESOURCE_TYPENEW, resource_types['name']) def test_associate_resource_types_invalid_property(self): longer = '1234' * 50 properties = {'name': RESOURCE_TYPENEW, 'prefix': longer} self.assertRaises(TypeError, self.controller.associate, NAMESPACE1, **properties) def test_deassociate_resource_types(self): self.controller.deassociate(NAMESPACE1, RESOURCE_TYPE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/resource_types/%s' % (NAMESPACE1, RESOURCE_TYPE1), {}, None)] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_versions.py0000666000175100017510000000455613232161652026114 0ustar zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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 testtools from glanceclient.tests import utils from glanceclient.v2 import versions fixtures = { '/versions': { 'GET': ( {}, {"versions": [ { "status": "EXPERIMENTAL", "id": "v3.0", "links": [ { "href": "http://10.229.45.145:9292/v3/", "rel": "self" } ] }, { "status": "CURRENT", "id": "v2.3", "links": [ { "href": "http://10.229.45.145:9292/v2/", "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v1.0", "links": [ { "href": "http://10.229.45.145:9292/v1/", "rel": "self" } ] } ]} ) } } class TestVersions(testtools.TestCase): def setUp(self): super(TestVersions, self).setUp() self.api = utils.FakeAPI(fixtures) self.controller = versions.VersionController(self.api) def test_version_list(self): version = list(self.controller.list()) self.assertEqual('v3.0', version[0]['id']) self.assertEqual('EXPERIMENTAL', version[0]['status']) self.assertEqual([{"href": "http://10.229.45.145:9292/v3/", "rel": "self"}], version[0]['links']) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_tasks.py0000666000175100017510000002572213232161652025367 0ustar zuulzuul00000000000000# Copyright 2013 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 testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import tasks _OWNED_TASK_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf' _OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f' _FAKE_OWNER_ID = '63e7f218-29de-4477-abdc-8db7c9533188' _PENDING_ID = '3a4560a1-e585-443e-9b39-553b46ec92d1' _PROCESSING_ID = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' fixtures = { '/v2/tasks?limit=%d' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, ]}, ), }, '/v2/tasks?limit=1': { 'GET': ( {}, { 'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, ], 'next': ('/v2/tasks?limit=1&' 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'), }, ), }, ('/v2/tasks?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): { 'GET': ( {}, {'tasks': [ { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'pending', }, ]}, ), }, '/v2/tasks/3a4560a1-e585-443e-9b39-553b46ec92d1': { 'GET': ( {}, { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, ), 'PATCH': ( {}, '', ), }, '/v2/tasks/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': { 'GET': ( {}, { 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3', 'type': 'import', 'status': 'pending', }, ), 'PATCH': ( {}, '', ), }, '/v2/tasks': { 'POST': ( {}, { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', 'input': '{"import_from": "file:///", ' '"import_from_format": "qcow2"}' }, ), }, '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&status=processing' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&type=fake' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ ]}, ), }, '/v2/tasks?limit=%d&status=fake' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ ]}, ), }, '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _FAKE_OWNER_ID): { 'GET': ({}, {'tasks': []}, ), }, '/v2/tasks?limit=%d&sort_key=type' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, ]}, ), }, '/v2/tasks?limit=%d&sort_dir=asc&sort_key=id' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, ]}, ), }, '/v2/tasks?limit=%d&sort_dir=desc&sort_key=id' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, ]}, ), }, } schema_fixtures = { 'task': { 'GET': ( {}, { 'name': 'task', 'properties': { 'id': {}, 'type': {}, 'status': {}, 'input': {}, 'result': {}, 'message': {}, }, 'additionalProperties': False, } ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, tasks.Controller) def test_list_tasks(self): tasks = self.controller.list() self.assertEqual(_PENDING_ID, tasks[0].id) self.assertEqual('import', tasks[0].type) self.assertEqual('pending', tasks[0].status) self.assertEqual(_PROCESSING_ID, tasks[1].id) self.assertEqual('import', tasks[1].type) self.assertEqual('processing', tasks[1].status) def test_list_tasks_paginated(self): tasks = self.controller.list(page_size=1) self.assertEqual(_PENDING_ID, tasks[0].id) self.assertEqual('import', tasks[0].type) self.assertEqual(_PROCESSING_ID, tasks[1].id) self.assertEqual('import', tasks[1].type) def test_list_tasks_with_status(self): filters = {'filters': {'status': 'processing'}} tasks = self.controller.list(**filters) self.assertEqual(_OWNED_TASK_ID, tasks[0].id) def test_list_tasks_with_wrong_status(self): filters = {'filters': {'status': 'fake'}} tasks = self.controller.list(**filters) self.assertEqual(0, len(tasks)) def test_list_tasks_with_type(self): filters = {'filters': {'type': 'import'}} tasks = self.controller.list(**filters) self.assertEqual(_OWNED_TASK_ID, tasks[0].id) def test_list_tasks_with_wrong_type(self): filters = {'filters': {'type': 'fake'}} tasks = self.controller.list(**filters) self.assertEqual(0, len(tasks)) def test_list_tasks_for_owner(self): filters = {'filters': {'owner': _OWNER_ID}} tasks = self.controller.list(**filters) self.assertEqual(_OWNED_TASK_ID, tasks[0].id) def test_list_tasks_for_fake_owner(self): filters = {'filters': {'owner': _FAKE_OWNER_ID}} tasks = self.controller.list(**filters) self.assertEqual(tasks, []) def test_list_tasks_filters_encoding(self): filters = {"owner": u"ni\xf1o"} try: self.controller.list(filters=filters) except KeyError: # NOTE(flaper87): It raises KeyError because there's # no fixture supporting this query: # /v2/tasks?owner=ni%C3%B1o&limit=20 # We just want to make sure filters are correctly encoded. pass self.assertEqual(b"ni\xc3\xb1o", filters["owner"]) def test_list_tasks_with_marker(self): tasks = self.controller.list(marker=_PENDING_ID, page_size=1) self.assertEqual(1, len(tasks)) self.assertEqual(_PROCESSING_ID, tasks[0]['id']) def test_list_tasks_with_single_sort_key(self): tasks = self.controller.list(sort_key='type') self.assertEqual(2, len(tasks)) self.assertEqual(_PENDING_ID, tasks[0].id) def test_list_tasks_with_invalid_sort_key(self): self.assertRaises(ValueError, self.controller.list, sort_key='invalid') def test_list_tasks_with_desc_sort_dir(self): tasks = self.controller.list(sort_key='id', sort_dir='desc') self.assertEqual(2, len(tasks)) self.assertEqual(_PENDING_ID, tasks[1].id) def test_list_tasks_with_asc_sort_dir(self): tasks = self.controller.list(sort_key='id', sort_dir='asc') self.assertEqual(2, len(tasks)) self.assertEqual(_PENDING_ID, tasks[0].id) def test_list_tasks_with_invalid_sort_dir(self): self.assertRaises(ValueError, self.controller.list, sort_dir='invalid') def test_get_task(self): task = self.controller.get(_PENDING_ID) self.assertEqual(_PENDING_ID, task.id) self.assertEqual('import', task.type) def test_create_task(self): properties = { 'type': 'import', 'input': {'import_from_format': 'ovf', 'import_from': 'swift://cloud.foo/myaccount/mycontainer/path'}, } task = self.controller.create(**properties) self.assertEqual(_PENDING_ID, task.id) self.assertEqual('import', task.type) def test_create_task_invalid_property(self): properties = { 'type': 'import', 'bad_prop': 'value', } self.assertRaises(TypeError, self.controller.create, **properties) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_metadefs_namespaces.py0000666000175100017510000006165213232161652030233 0ustar zuulzuul00000000000000# 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' NAMESPACE2 = 'Namespace2' NAMESPACE3 = 'Namespace3' NAMESPACE4 = 'Namespace4' NAMESPACE5 = 'Namespace5' NAMESPACE6 = 'Namespace6' NAMESPACE7 = 'Namespace7' NAMESPACE8 = 'Namespace8' NAMESPACENEW = 'NamespaceNew' RESOURCE_TYPE1 = 'ResourceType1' RESOURCE_TYPE2 = 'ResourceType2' OBJECT1 = 'Object1' PROPERTY1 = 'Property1' PROPERTY2 = 'Property2' def _get_namespace_fixture(ns_name, rt_name=RESOURCE_TYPE1, **kwargs): ns = { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "self": "/v2/metadefs/namespaces/%s" % ns_name, "namespace": ns_name, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": rt_name } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ns.update(kwargs) return ns data_fixtures = { "/v2/metadefs/namespaces?limit=20": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE1), _get_namespace_fixture(NAMESPACE2), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=1": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=1", "namespaces": [ _get_namespace_fixture(NAMESPACE7), ], "schema": "/v2/schemas/metadefs/namespaces", "next": "/v2/metadefs/namespaces?marker=%s&limit=1" % NAMESPACE7, } ) }, "/v2/metadefs/namespaces?limit=1&marker=%s" % NAMESPACE7: { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=2", "namespaces": [ _get_namespace_fixture(NAMESPACE8), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=2&marker=%s" % NAMESPACE6: { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=2", "namespaces": [ _get_namespace_fixture(NAMESPACE7), _get_namespace_fixture(NAMESPACE8), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=20&sort_dir=asc": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=1", "namespaces": [ _get_namespace_fixture(NAMESPACE1), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=20&sort_key=created_at": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=1", "namespaces": [ _get_namespace_fixture(NAMESPACE1), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=20&resource_types=%s" % RESOURCE_TYPE1: { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE3), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=20&resource_types=" "%s%%2C%s" % (RESOURCE_TYPE1, RESOURCE_TYPE2): { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE4), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=20&visibility=private": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE5), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces": { "POST": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "self": "/v2/metadefs/namespaces/%s" % 'NamespaceNew', "namespace": 'NamespaceNew', "visibility": "public", "protected": True, "owner": "admin", "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ) }, "/v2/metadefs/namespaces/%s" % NAMESPACE1: { "GET": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "objects": [ { "description": "DESCRIPTION2", "name": "OBJECT1", "self": "/v2/metadefs/namespaces/%s/objects/" % OBJECT1, "required": [], "properties": { PROPERTY1: { "type": "integer", "description": "DESCRIPTION3", "title": "Quota: CPU Shares" }, PROPERTY2: { "minimum": 1000, "type": "integer", "description": "DESCRIPTION4", "maximum": 1000000, "title": "Quota: CPU Period" }, }, "schema": "/v2/schemas/metadefs/object" } ], "self": "/v2/metadefs/namespaces/%s" % NAMESPACE1, "namespace": NAMESPACE1, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": RESOURCE_TYPE1 } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), "PUT": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "objects": [ { "description": "DESCRIPTION2", "name": "OBJECT1", "self": "/v2/metadefs/namespaces/%s/objects/" % OBJECT1, "required": [], "properties": { PROPERTY1: { "type": "integer", "description": "DESCRIPTION3", "title": "Quota: CPU Shares" }, PROPERTY2: { "minimum": 1000, "type": "integer", "description": "DESCRIPTION4", "maximum": 1000000, "title": "Quota: CPU Period" }, }, "schema": "/v2/schemas/metadefs/object" } ], "self": "/v2/metadefs/namespaces/%s" % NAMESPACENEW, "namespace": NAMESPACENEW, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": RESOURCE_TYPE1 } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s?resource_type=%s" % (NAMESPACE6, RESOURCE_TYPE1): { "GET": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "objects": [], "self": "/v2/metadefs/namespaces/%s" % NAMESPACE1, "namespace": NAMESPACE6, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": RESOURCE_TYPE1 } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), }, } schema_fixtures = { "metadefs/namespace": { "GET": ( {}, { "additionalProperties": False, "definitions": { "property": { "additionalProperties": { "required": [ "title", "type" ], "type": "object", "properties": { "additionalItems": { "type": "boolean" }, "enum": { "type": "array" }, "description": { "type": "string" }, "title": { "type": "string" }, "default": {}, "minLength": { "$ref": "#/definitions/" "positiveIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "maximum": { "type": "number" }, "minItems": { "$ref": "#/definitions/" "positiveIntegerDefault0" }, "readonly": { "type": "boolean" }, "minimum": { "type": "number" }, "maxItems": { "$ref": "#/definitions/" "positiveInteger" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "uniqueItems": { "default": False, "type": "boolean" }, "pattern": { "type": "string", "format": "regex" }, "items": { "type": "object", "properties": { "enum": { "type": "array" }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": "object" }, "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "stringArray": { "uniqueItems": True, "items": { "type": "string" }, "type": "array" }, "positiveInteger": { "minimum": 0, "type": "integer" } }, "required": [ "namespace" ], "name": "namespace", "properties": { "description": { "type": "string", "description": "Provides a user friendly description " "of the namespace.", "maxLength": 500 }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last namespace " "modification", "format": "date-time" }, "visibility": { "enum": [ "public", "private" ], "type": "string", "description": "Scope of namespace accessibility." }, "self": { "type": "string" }, "objects": { "items": { "type": "object", "properties": { "properties": { "$ref": "#/definitions/property" }, "required": { "$ref": "#/definitions/stringArray" }, "name": { "type": "string" }, "description": { "type": "string" } } }, "type": "array" }, "owner": { "type": "string", "description": "Owner of the namespace.", "maxLength": 255 }, "resource_types": { "items": { "type": "object", "properties": { "prefix": { "type": "string" }, "name": { "type": "string" }, "metadata_type": { "type": "string" } } }, "type": "array" }, "properties": { "$ref": "#/definitions/property" }, "display_name": { "type": "string", "description": "The user friendly name for the " "namespace. Used by UI if available.", "maxLength": 80 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of namespace creation ", "format": "date-time" }, "namespace": { "type": "string", "description": "The unique namespace text.", "maxLength": 80 }, "protected": { "type": "boolean", "description": "If true, namespace will not be " "deletable." }, "schema": { "type": "string" } } } ), } } class TestNamespaceController(testtools.TestCase): def setUp(self): super(TestNamespaceController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.NamespaceController) def test_list_namespaces(self): namespaces = self.controller.list() self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE1, namespaces[0]['namespace']) self.assertEqual(NAMESPACE2, namespaces[1]['namespace']) def test_list_namespaces_paginate(self): namespaces = self.controller.list(page_size=1) self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE7, namespaces[0]['namespace']) self.assertEqual(NAMESPACE8, namespaces[1]['namespace']) def test_list_with_limit_greater_than_page_size(self): namespaces = self.controller.list(page_size=1, limit=2) self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE7, namespaces[0]['namespace']) self.assertEqual(NAMESPACE8, namespaces[1]['namespace']) def test_list_with_marker(self): namespaces = self.controller.list(marker=NAMESPACE6, page_size=2) self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE7, namespaces[0]['namespace']) self.assertEqual(NAMESPACE8, namespaces[1]['namespace']) def test_list_with_sort_dir(self): namespaces = self.controller.list(sort_dir='asc', limit=1) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE1, namespaces[0]['namespace']) def test_list_with_sort_dir_invalid(self): # NOTE(TravT): The clients work by returning an iterator. # Invoking the iterator is what actually executes the logic. self.assertRaises(ValueError, self.controller.list, sort_dir='foo') def test_list_with_sort_key(self): namespaces = self.controller.list(sort_key='created_at', limit=1) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE1, namespaces[0]['namespace']) def test_list_with_sort_key_invalid(self): # NOTE(TravT): The clients work by returning an iterator. # Invoking the iterator is what actually executes the logic. self.assertRaises(ValueError, self.controller.list, sort_key='foo') def test_list_namespaces_with_one_resource_type_filter(self): namespaces = self.controller.list( filters={ 'resource_types': [RESOURCE_TYPE1] } ) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE3, namespaces[0]['namespace']) def test_list_namespaces_with_multiple_resource_types_filter(self): namespaces = self.controller.list( filters={ 'resource_types': [RESOURCE_TYPE1, RESOURCE_TYPE2] } ) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE4, namespaces[0]['namespace']) def test_list_namespaces_with_visibility_filter(self): namespaces = self.controller.list( filters={ 'visibility': 'private' } ) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE5, namespaces[0]['namespace']) def test_get_namespace(self): namespace = self.controller.get(NAMESPACE1) self.assertEqual(NAMESPACE1, namespace.namespace) self.assertTrue(namespace.protected) def test_get_namespace_with_resource_type(self): namespace = self.controller.get(NAMESPACE6, resource_type=RESOURCE_TYPE1) self.assertEqual(NAMESPACE6, namespace.namespace) self.assertTrue(namespace.protected) def test_create_namespace(self): properties = { 'namespace': NAMESPACENEW } namespace = self.controller.create(**properties) self.assertEqual(NAMESPACENEW, namespace.namespace) self.assertTrue(namespace.protected) def test_create_namespace_invalid_data(self): properties = {} self.assertRaises(TypeError, self.controller.create, **properties) def test_create_namespace_invalid_property(self): properties = {'namespace': 'NewNamespace', 'protected': '123'} self.assertRaises(TypeError, self.controller.create, **properties) def test_update_namespace(self): properties = {'display_name': 'My Updated Name'} namespace = self.controller.update(NAMESPACE1, **properties) self.assertEqual(NAMESPACE1, namespace.namespace) def test_update_namespace_invalid_property(self): properties = {'protected': '123'} self.assertRaises(TypeError, self.controller.update, NAMESPACE1, **properties) def test_update_namespace_disallowed_fields(self): properties = {'display_name': 'My Updated Name'} self.controller.update(NAMESPACE1, **properties) actual = self.api.calls _disallowed_fields = ['self', 'schema', 'created_at', 'updated_at'] for key in actual[1][3]: self.assertNotIn(key, _disallowed_fields) def test_delete_namespace(self): self.controller.delete(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_tags.py0000666000175100017510000000467413232161652025203 0ustar zuulzuul00000000000000# Copyright 2013 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 from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import image_tags IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1' TAG = 'tag01' data_fixtures = { '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG): { 'DELETE': ( {}, None, ), 'PUT': ( {}, { 'image_id': IMAGE, 'tag_value': TAG } ), } } schema_fixtures = { 'tag': { 'GET': ( {}, {'name': 'image', 'properties': {'image_id': {}, 'tags': {}}} ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, image_tags.Controller) def test_update_image_tag(self): image_id = IMAGE tag_value = TAG self.controller.update(image_id, tag_value) expect = [ ('PUT', '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_image_tag(self): image_id = IMAGE tag_value = TAG self.controller.delete(image_id, tag_value) expect = [ ('DELETE', '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG), {}, None)] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_images.py0000666000175100017510000012165013232161652025504 0ustar zuulzuul00000000000000# 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 errno import mock import testtools from glanceclient import exc from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import images _CHKSUM = '93264c3edf5972c9f1cb309543d38a5c' _CHKSUM1 = '54264c3edf5972c9f1cb309453d38a46' _TAG1 = 'power' _TAG2 = '64bit' _BOGUS_ID = '63e7f218-29de-4477-abdc-8db7c9533188' _EVERYTHING_ID = '802cbbb7-0379-4c38-853f-37302b5e3d29' _OWNED_IMAGE_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf' _OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f' _PRIVATE_ID = 'e33560a7-3964-4de5-8339-5a24559f99ab' _PUBLIC_ID = '857806e7-05b6-48e0-9d40-cb0e6fb727b9' _SHARED_ID = '331ac905-2a38-44c5-a83d-653db8f08313' _COMMUNITY_ID = '609ec9fc-0ee4-44c4-854d-0480af576929' _STATUS_REJECTED_ID = 'f3ea56ff-d7e4-4451-998c-1e3d33539c8e' data_fixtures = { '/v2/schemas/image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'id': {}, 'name': {}, 'locations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'metadata': {'type': 'object'}, 'url': {'type': 'string'}, }, 'required': ['url', 'metadata'], }, }, 'color': {'type': 'string', 'is_base': False}, }, 'additionalProperties': {'type': 'string'}, }, ), }, '/v2/images?limit=%d' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=2': { 'GET': ( {}, { 'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ], 'next': ('/v2/images?limit=2&' 'marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'), }, ), }, '/v2/images?limit=1': { 'GET': ( {}, { 'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ], 'next': ('/v2/images?limit=1&' 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'), }, ), }, ('/v2/images?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, ('/v2/images?limit=1&marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'): { 'GET': ( {}, {'images': [ { 'id': '3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', 'name': 'image-3', }, ]}, ), }, '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1': { 'GET': ( {}, { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ), 'PATCH': ( {}, '', ), }, '/v2/images/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': { 'GET': ( {}, { 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3', 'name': 'image-3', 'barney': 'rubble', 'george': 'jetson', 'color': 'red', }, ), 'PATCH': ( {}, '', ), }, '/v2/images': { 'POST': ( {}, { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ), }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a': { 'DELETE': ( {}, { 'id': '87b634c1-f893-33c9-28a9-e5673c99239a', }, ), }, '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/file': { 'PUT': ( {}, '', ), }, '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( {}, 'A', ), }, '/v2/images/66fb18d6-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/1b1c6366-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/reactivate': { 'POST': ({}, None) }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/deactivate': { 'POST': ({}, None) }, '/v2/images?limit=%d&visibility=public' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _PUBLIC_ID, 'harvey': 'lipshitz', }, ]}, ), }, '/v2/images?limit=%d&visibility=private' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _PRIVATE_ID, }, ]}, ), }, '/v2/images?limit=%d&visibility=shared' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _SHARED_ID, }, ]}, ), }, '/v2/images?limit=%d&visibility=community' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _COMMUNITY_ID, }, ]}, ), }, '/v2/images?limit=%d&member_status=rejected' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _STATUS_REJECTED_ID, }, ]}, ), }, '/v2/images?limit=%d&member_status=pending' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'images': [ { 'id': _OWNED_IMAGE_ID, }, ]}, ), }, '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&member_status=pending&owner=%s&visibility=shared' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': [ { 'id': _EVERYTHING_ID, }, ]}, ), }, '/v2/images?checksum=%s&limit=%d' % (_CHKSUM, images.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?checksum=%s&limit=%d' % (_CHKSUM1, images.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?checksum=wrong&limit=%d' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG2): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&tag=%s&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1, _TAG2): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&tag=fake' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images/a2b83adc-888e-11e3-8872-78acc0b951d8': { 'GET': ( {}, { 'id': 'a2b83adc-888e-11e3-8872-78acc0b951d8', 'name': 'image-location-tests', 'locations': [{u'url': u'http://foo.com/', u'metadata': {u'foo': u'foometa'}}, {u'url': u'http://bar.com/', u'metadata': {u'bar': u'barmeta'}}], }, ), 'PATCH': ( {}, '', ) }, '/v2/images?limit=%d&os_distro=NixOS' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '8b052954-c76c-4e02-8e90-be89a70183a8', 'name': 'image-5', 'os_distro': 'NixOS', }, ]}, ), }, '/v2/images?limit=%d&my_little_property=cant_be_this_cute' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&sort_key=name' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_dir=asc&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort=name%%3Adesc%%2Csize%%3Aasc' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, } schema_fixtures = { 'image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'id': {}, 'name': {}, 'locations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'metadata': {'type': 'object'}, 'url': {'type': 'string'}, }, 'required': ['url', 'metadata'], } }, 'color': {'type': 'string', 'is_base': False}, 'tags': {'type': 'array'}, }, 'additionalProperties': {'type': 'string'}, } ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, images.Controller) def test_list_images(self): images = self.controller.list() self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) def test_list_images_paginated(self): images = self.controller.list(page_size=1) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) def test_list_images_paginated_with_limit(self): images = self.controller.list(limit=3, page_size=2) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) self.assertEqual('3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', images[2].id) self.assertEqual('image-3', images[2].name) self.assertEqual(3, len(images)) def test_list_images_with_marker(self): images = self.controller.list( limit=1, marker='3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[0].id) self.assertEqual('image-2', images[0].name) def test_list_images_visibility_public(self): filters = {'filters': {'visibility': 'public'}} images = self.controller.list(**filters) self.assertEqual(_PUBLIC_ID, images[0].id) def test_list_images_visibility_private(self): filters = {'filters': {'visibility': 'private'}} images = self.controller.list(**filters) self.assertEqual(_PRIVATE_ID, images[0].id) def test_list_images_visibility_shared(self): filters = {'filters': {'visibility': 'shared'}} images = self.controller.list(**filters) self.assertEqual(_SHARED_ID, images[0].id) def test_list_images_visibility_community(self): filters = {'filters': {'visibility': 'community'}} images = list(self.controller.list(**filters)) self.assertEqual(_COMMUNITY_ID, images[0].id) def test_list_images_member_status_rejected(self): filters = {'filters': {'member_status': 'rejected'}} images = self.controller.list(**filters) self.assertEqual(_STATUS_REJECTED_ID, images[0].id) def test_list_images_for_owner(self): filters = {'filters': {'owner': _OWNER_ID}} images = self.controller.list(**filters) self.assertEqual(_OWNED_IMAGE_ID, images[0].id) def test_list_images_for_checksum_single_image(self): fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'checksum': _CHKSUM}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % fake_id, images[0].id) def test_list_images_for_checksum_multiple_images(self): fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'checksum': _CHKSUM1}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % fake_id1, images[0].id) self.assertEqual('%s' % fake_id2, images[1].id) def test_list_images_for_wrong_checksum(self): filters = {'filters': {'checksum': 'wrong'}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_bogus_owner(self): filters = {'filters': {'owner': _BOGUS_ID}} images = self.controller.list(**filters) self.assertEqual([], images) def test_list_images_for_bunch_of_filters(self): filters = {'filters': {'owner': _BOGUS_ID, 'visibility': 'shared', 'member_status': 'pending'}} images = self.controller.list(**filters) self.assertEqual(_EVERYTHING_ID, images[0].id) def test_list_images_filters_encoding(self): filters = {"owner": u"ni\xf1o"} try: self.controller.list(filters=filters) except KeyError: # NOTE(flaper87): It raises KeyError because there's # no fixture supporting this query: # /v2/images?owner=ni%C3%B1o&limit=20 # We just want to make sure filters are correctly encoded. pass self.assertEqual(b"ni\xc3\xb1o", filters["owner"]) def test_list_images_for_tag_single_image(self): img_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'tag': [_TAG1]}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % img_id, images[0].id) def test_list_images_for_tag_multiple_images(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' img_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'tag': [_TAG2]}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) self.assertEqual('%s' % img_id2, images[1].id) def test_list_images_for_multi_tags(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'tag': [_TAG1, _TAG2]}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_images_for_non_existent_tag(self): filters = {'filters': {'tag': ['fake']}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_invalid_tag(self): filters = {'filters': {'tag': [[]]}} self.assertRaises(exc.HTTPBadRequest, self.controller.list, **filters) def test_list_images_with_single_sort_key(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = 'name' images = self.controller.list(sort_key=sort_key) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_with_multiple_sort_keys(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] images = self.controller.list(sort_key=sort_key) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_images_with_desc_sort_dir(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = 'id' sort_dir = 'desc' images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_multiple_sort_keys_and_one_sort_dir(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] sort_dir = 'desc' images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_multiple_sort_dirs(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] sort_dir = ['desc', 'asc'] images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_new_sorting_syntax(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort = 'name:desc,size:asc' images = self.controller.list(sort=sort) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_sort_dirs_fewer_than_keys(self): sort_key = ['name', 'id', 'created_at'] sort_dir = ['desc', 'asc'] self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort_key=sort_key, sort_dir=sort_dir) def test_list_images_combined_syntax(self): sort_key = ['name', 'id'] sort_dir = ['desc', 'asc'] sort = 'name:asc' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort, sort_key=sort_key, sort_dir=sort_dir) def test_list_images_new_sorting_syntax_invalid_key(self): sort = 'INVALID:asc' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort) def test_list_images_new_sorting_syntax_invalid_direction(self): sort = 'name:INVALID' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort) def test_list_images_for_property(self): filters = {'filters': dict([('os_distro', 'NixOS')])} images = self.controller.list(**filters) self.assertEqual(1, len(images)) def test_list_images_for_non_existent_property(self): filters = {'filters': dict([('my_little_property', 'cant_be_this_cute')])} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_get_image(self): image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id) self.assertEqual('image-1', image.name) def test_create_image(self): properties = { 'name': 'image-1' } image = self.controller.create(**properties) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id) self.assertEqual('image-1', image.name) def test_create_bad_additionalProperty_type(self): properties = { 'name': 'image-1', 'bad_prop': True, } with testtools.ExpectedException(TypeError): self.controller.create(**properties) def test_delete_image(self): self.controller.delete('87b634c1-f893-33c9-28a9-e5673c99239a') expect = [ ('DELETE', '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a', {}, None)] self.assertEqual(expect, self.api.calls) def test_deactivate_image(self): id_image = '87b634c1-f893-33c9-28a9-e5673c99239a' self.controller.deactivate(id_image) expect = [('POST', '/v2/images/%s/actions/deactivate' % id_image, {}, None)] self.assertEqual(expect, self.api.calls) def test_reactivate_image(self): id_image = '87b634c1-f893-33c9-28a9-e5673c99239a' self.controller.reactivate(id_image) expect = [('POST', '/v2/images/%s/actions/reactivate' % id_image, {}, None)] self.assertEqual(expect, self.api.calls) def test_data_upload(self): image_data = 'CCC' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.upload(image_id, image_data) expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, image_data)] self.assertEqual(expect, self.api.calls) def test_data_upload_w_size(self): image_data = 'CCC' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.upload(image_id, image_data, image_size=3) expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, image_data)] self.assertEqual(expect, self.api.calls) def test_data_without_checksum(self): body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('A', body) body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205') body = ''.join([b for b in body]) self.assertEqual('A', body) def test_data_with_wrong_checksum(self): body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong' self.assertIn(msg, str(e)) def test_data_with_checksum(self): body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('CCC', body) body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8') body = ''.join([b for b in body]) self.assertEqual('CCC', body) def test_download_no_data(self): resp = utils.FakeResponse(headers={}, status_code=204) self.controller.controller.http_client.get = mock.Mock( return_value=(resp, None)) self.controller.data('image_id') def test_download_forbidden(self): self.controller.controller.http_client.get = mock.Mock( side_effect=exc.HTTPForbidden()) try: self.controller.data('image_id') self.fail('No forbidden exception raised.') except exc.HTTPForbidden: pass def test_update_replace_prop(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'name': 'pong'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'replace'), ('path', '/name'), ('value', 'pong')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-1', image.name) def test_update_add_prop(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'finn': 'human'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-1', image.name) def test_update_remove_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' remove_props = ['barney'] image = self.controller.update(image_id, remove_props) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'remove'), ('path', '/barney')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_replace_remove_same_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' # Updating a property takes precedence over removing a property params = {'barney': 'miller'} remove_props = ['barney'] image = self.controller.update(image_id, remove_props, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = ([[('op', 'replace'), ('path', '/barney'), ('value', 'miller')]]) expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_add_remove_same_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' # Adding a property takes precedence over removing a property params = {'finn': 'human'} remove_props = ['finn'] image = self.controller.update(image_id, remove_props, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_bad_additionalProperty_type(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' params = {'name': 'pong', 'bad_prop': False} with testtools.ExpectedException(TypeError): self.controller.update(image_id, **params) def test_update_add_custom_property(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'color': 'red'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/color'), ('value', 'red')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) def test_update_replace_custom_property(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' params = {'color': 'blue'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'replace'), ('path', '/color'), ('value', 'blue')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) def test_location_ops_when_server_disabled_location_ops(self): # Location operations should not be allowed if server has not # enabled location related operations. There is no need to check it # when do location add, because the check would be done in server side. image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' estr = 'The administrator has disabled API access to image locations' url = 'http://bar.com/' meta = {'bar': 'barmeta'} e = self.assertRaises(exc.HTTPBadRequest, self.controller.delete_locations, image_id, set([url])) self.assertIn(estr, str(e)) e = self.assertRaises(exc.HTTPBadRequest, self.controller.update_location, image_id, url, meta) self.assertIn(estr, str(e)) def _empty_get(self, image_id, headers=None): return ('GET', '/v2/images/%s' % image_id, headers or {}, None) def _patch_req(self, image_id, patch_body): c_type = 'application/openstack-images-v2.1-json-patch' data = [sorted(d.items()) for d in patch_body] return ('PATCH', '/v2/images/%s' % image_id, {'Content-Type': c_type}, data) def test_add_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}} add_patch = {'path': '/locations/-', 'value': new_loc, 'op': 'add'} headers = {'x-openstack-request-id': 'req-1234'} self.controller.add_location(image_id, **new_loc) self.assertEqual([self._patch_req(image_id, [add_patch]), self._empty_get(image_id, headers=headers)], self.api.calls) @mock.patch.object(images.Controller, '_send_image_update_request', side_effect=exc.HTTPBadRequest) def test_add_duplicate_location(self, mock_request): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'newfoo'}} self.assertRaises(exc.HTTPBadRequest, self.controller.add_location, image_id, **new_loc) def test_remove_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' url_set = set(['http://foo.com/', 'http://bar.com/']) del_patches = [{'path': '/locations/1', 'op': 'remove'}, {'path': '/locations/0', 'op': 'remove'}] self.controller.delete_locations(image_id, url_set) self.assertEqual([self._empty_get(image_id), self._patch_req(image_id, del_patches)], self.api.calls) def test_remove_missing_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' url_set = set(['http://spam.ham/']) err_str = 'Unknown URL(s): %s' % list(url_set) err = self.assertRaises(exc.HTTPNotFound, self.controller.delete_locations, image_id, url_set) self.assertIn(err_str, str(err)) def test_update_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://foo.com/', 'metadata': {'spam': 'ham'}} headers = {'x-openstack-request-id': 'req-1234'} fixture_idx = '/v2/images/%s' % (image_id) orig_locations = data_fixtures[fixture_idx]['GET'][1]['locations'] loc_map = dict([(l['url'], l) for l in orig_locations]) loc_map[new_loc['url']] = new_loc mod_patch = [{'path': '/locations', 'op': 'replace', 'value': list(loc_map.values())}] self.controller.update_location(image_id, **new_loc) self.assertEqual([self._empty_get(image_id), self._patch_req(image_id, mod_patch), self._empty_get(image_id, headers=headers)], self.api.calls) def test_update_tags(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' tag_map = {'tags': ['tag01', 'tag02', 'tag03']} headers = {'x-openstack-request-id': 'req-1234'} image = self.controller.update(image_id, **tag_map) expected_body = [{'path': '/tags', 'op': 'replace', 'value': tag_map['tags']}] expected = [ self._empty_get(image_id), self._patch_req(image_id, expected_body), self._empty_get(image_id, headers=headers) ] self.assertEqual(expected, self.api.calls) self.assertEqual(image_id, image.id) def test_update_missing_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}} err_str = 'Unknown URL: %s' % new_loc['url'] err = self.assertRaises(exc.HTTPNotFound, self.controller.update_location, image_id, **new_loc) self.assertIn(err_str, str(err)) python-glanceclient-2.9.1/glanceclient/tests/unit/v2/test_shell_v2.py0000666000175100017510000017060013232161652025754 0ustar zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! 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 argparse import json import mock import os import six import tempfile import testtools from glanceclient.common import utils from glanceclient import exc from glanceclient import shell # NOTE(geguileo): This is very nasty, but I can't find a better way to set # command line arguments in glanceclient.v2.shell.do_image_create that are # set by decorator utils.schema_args while preserving the spirits of the test # Backup original decorator original_schema_args = utils.schema_args # Set our own decorator that calls the original but with simulated schema def schema_args(schema_getter, omit=None): global original_schema_args # We only add the 2 arguments that are required by image-create my_schema_getter = lambda: { 'properties': { 'container_format': { 'enum': [None, 'ami', 'ari', 'aki', 'bare', 'ovf', 'ova', 'docker'], 'type': 'string', 'description': 'Format of the container'}, 'disk_format': { 'enum': [None, 'ami', 'ari', 'aki', 'vhd', 'vhdx', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso', 'ploop'], 'type': 'string', 'description': 'Format of the disk'}, 'location': {'type': 'string'}, 'locations': {'type': 'string'}, 'copy_from': {'type': 'string'}}} return original_schema_args(my_schema_getter, omit) utils.schema_args = schema_args from glanceclient.v2 import shell as test_shell # Return original decorator. utils.schema_args = original_schema_args class ShellV2Test(testtools.TestCase): def setUp(self): super(ShellV2Test, self).setUp() self._mock_utils() self.gc = self._mock_glance_client() self.shell = shell.OpenStackImagesShell() os.environ = { 'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_TOKEN_ID': 'test', 'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/', 'OS_AUTH_TOKEN': 'pass', 'OS_IMAGE_API_VERSION': '1', 'OS_REGION_NAME': 'test', 'OS_IMAGE_URL': 'http://is.invalid'} self.shell = shell.OpenStackImagesShell() self.patched = mock.patch('glanceclient.common.utils.get_data_file', autospec=True, return_value=None) self.mock_get_data_file = self.patched.start() def tearDown(self): super(ShellV2Test, self).tearDown() self.patched.stop() def _make_args(self, args): # NOTE(venkatesh): this conversion from a dict to an object # is required because the test_shell.do_xxx(gc, args) methods # expects the args to be attributes of an object. If passed as # dict directly, it throws an AttributeError. class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) def _mock_glance_client(self): my_mocked_gc = mock.Mock() my_mocked_gc.schemas.return_value = 'test' my_mocked_gc.get.return_value = {} return my_mocked_gc def _mock_utils(self): utils.print_list = mock.Mock() utils.print_dict = mock.Mock() utils.save_image = mock.Mock() def assert_exits_with_msg(self, func, func_args, err_msg=None): with mock.patch.object(utils, 'exit') as mocked_utils_exit: mocked_utils_exit.return_value = '%s' % err_msg func(self.gc, func_args) if err_msg: mocked_utils_exit.assert_called_once_with(err_msg) else: mocked_utils_exit.assert_called_once_with() def _run_command(self, cmd): self.shell.main(cmd.split()) @mock.patch('sys.stderr') def test_image_create_missing_disk_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create ' + '--file fake_src --container-format bare') self.assertEqual('error: Must provide --disk-format when using ' '--file.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create ' + '--file fake_src --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using --file.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = six.StringIO() e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create' ' --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using stdin.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_disk_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = six.StringIO() e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create' ' --container-format bare') self.assertEqual('error: Must provide --disk-format when using stdin.', e.message) def test_do_image_list(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, 'verbose': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag' } mocked_list.assert_called_once_with(page_size=18, sort_key=['name', 'id'], sort_dir=['desc', 'asc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_with_single_sort_key(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name'], 'sort_dir': ['desc'], 'sort': None, 'verbose': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag' } mocked_list.assert_called_once_with(page_size=18, sort_key=['name'], sort_dir=['desc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_new_sorting_syntax(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort': 'name:desc,size:asc', 'sort_key': [], 'sort_dir': [], 'verbose': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag' } mocked_list.assert_called_once_with( page_size=18, sort='name:desc,size:asc', filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_with_property_filter(self): input = { 'limit': None, 'page_size': 1, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': ['os_distro=NixOS', 'architecture=x86_64'], 'sort_key': ['name'], 'sort_dir': ['desc'], 'sort': None, 'verbose': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_distro': 'NixOS', 'architecture': 'x86_64' } mocked_list.assert_called_once_with(page_size=1, sort_key=['name'], sort_dir=['desc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_show_human_readable(self): args = self._make_args({'id': 'pass', 'page_size': 18, 'human_readable': True, 'max_column_width': 120}) with mock.patch.object(self.gc.images, 'get') as mocked_list: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['size'] = 1024 mocked_list.return_value = expect_image test_shell.do_image_show(self.gc, args) mocked_list.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({'id': 'pass', 'size': '1kB'}, max_column_width=120) def test_do_image_show(self): args = self._make_args({'id': 'pass', 'page_size': 18, 'human_readable': False, 'max_column_width': 120}) with mock.patch.object(self.gc.images, 'get') as mocked_list: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['size'] = 1024 mocked_list.return_value = expect_image test_shell.do_image_show(self.gc, args) mocked_list.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({'id': 'pass', 'size': 1024}, max_column_width=120) @mock.patch('sys.stdin', autospec=True) def test_do_image_create_no_user_props(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': None}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' mocked_create.return_value = expect_image # Ensure that the test stdin is not considered # to be supplying image data mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name='IMG-01', disk_format='vhd', container_format='bare') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) def test_do_image_create_with_file(self): self.mock_get_data_file.return_value = six.StringIO() try: file_name = None with open(tempfile.mktemp(), 'w+') as f: f.write('Some data here') f.flush() f.seek(0) file_name = f.name temp_args = {'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': file_name, 'progress': False} args = self._make_args(temp_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' mocked_create.return_value = expect_image mocked_get.return_value = expect_image test_shell.do_image_create(self.gc, args) temp_args.pop('file', None) mocked_create.assert_called_once_with(**temp_args) mocked_get.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) finally: try: os.remove(f.name) except Exception: pass @mock.patch('sys.stdin', autospec=True) def test_do_image_create_with_unicode(self, mock_stdin): name = u'\u041f\u0420\u0418\u0412\u0415\u0422\u0418\u041a' args = self._make_args({'name': name, 'file': None}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict((field, field) for field in ignore_fields) expect_image['id'] = 'pass' expect_image['name'] = name mocked_create.return_value = expect_image mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name=name) utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': name}) @mock.patch('sys.stdin', autospec=True) def test_do_image_create_with_user_props(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'property': ['myprop=myval'], 'file': None, 'container_format': 'bare', 'disk_format': 'qcow2'}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['myprop'] = 'myval' mocked_create.return_value = expect_image # Ensure that the test stdin is not considered # to be supplying image data mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name='IMG-01', myprop='myval', container_format='bare', disk_format='qcow2') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'}) def test_do_image_update_no_user_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', None, name='IMG-01', disk_format='vhd', container_format='bare') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) def test_do_image_update_with_user_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'property': ['myprop=myval']}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['myprop'] = 'myval' mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', None, name='IMG-01', myprop='myval') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'}) def test_do_image_update_with_remove_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'remove-property': ['container_format']}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', ['container_format'], name='IMG-01', disk_format='vhd') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd'}) def test_do_explain(self): input = { 'page_size': 18, 'id': 'pass', 'schemas': 'test', 'model': 'test', } args = self._make_args(input) with mock.patch.object(utils, 'print_list'): test_shell.do_explain(self.gc, args) self.gc.schemas.get.assert_called_once_with('test') def test_do_location_add(self): gc = self.gc loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'bar'}} args = self._make_args({'id': 'pass', 'url': loc['url'], 'metadata': json.dumps(loc['metadata'])}) with mock.patch.object(gc.images, 'add_location') as mocked_addloc: expect_image = {'id': 'pass', 'locations': [loc]} mocked_addloc.return_value = expect_image test_shell.do_location_add(self.gc, args) mocked_addloc.assert_called_once_with('pass', loc['url'], loc['metadata']) utils.print_dict.assert_called_once_with(expect_image) def test_do_location_delete(self): gc = self.gc loc_set = set(['http://foo/bar', 'http://spam/ham']) args = self._make_args({'id': 'pass', 'url': loc_set}) with mock.patch.object(gc.images, 'delete_locations') as mocked_rmloc: test_shell.do_location_delete(self.gc, args) mocked_rmloc.assert_called_once_with('pass', loc_set) def test_do_location_update(self): gc = self.gc loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'bar'}} args = self._make_args({'id': 'pass', 'url': loc['url'], 'metadata': json.dumps(loc['metadata'])}) with mock.patch.object(gc.images, 'update_location') as mocked_modloc: expect_image = {'id': 'pass', 'locations': [loc]} mocked_modloc.return_value = expect_image test_shell.do_location_update(self.gc, args) mocked_modloc.assert_called_once_with('pass', loc['url'], loc['metadata']) utils.print_dict.assert_called_once_with(expect_image) def test_image_upload(self): args = self._make_args( {'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False}) with mock.patch.object(self.gc.images, 'upload') as mocked_upload: utils.get_data_file = mock.Mock(return_value='testfile') mocked_upload.return_value = None test_shell.do_image_upload(self.gc, args) mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024) def test_image_download(self): args = self._make_args( {'id': 'IMG-01', 'file': 'test', 'progress': True}) with mock.patch.object(self.gc.images, 'data') as mocked_data, \ mock.patch.object(utils, '_extract_request_id'): mocked_data.return_value = utils.RequestIdProxy( [c for c in 'abcdef']) test_shell.do_image_download(self.gc, args) mocked_data.assert_called_once_with('IMG-01') @mock.patch.object(utils, 'exit') @mock.patch('sys.stdout', autospec=True) def test_image_download_no_file_arg(self, mocked_stdout, mocked_utils_exit): # Indicate that no file name was given as command line argument args = self._make_args({'id': '1234', 'file': None, 'progress': False}) # Indicate that no file is specified for output redirection mocked_stdout.isatty = lambda: True test_shell.do_image_download(self.gc, args) mocked_utils_exit.assert_called_once_with( 'No redirection or local file specified for downloaded image' ' data. Please specify a local file with --file to save' ' downloaded image or redirect output to another source.') def test_do_image_delete(self): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.return_value = 0 test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) def test_do_image_deactivate(self): args = argparse.Namespace(id='image1') with mock.patch.object(self.gc.images, 'deactivate') as mocked_deactivate: mocked_deactivate.return_value = 0 test_shell.do_image_deactivate(self.gc, args) self.assertEqual(1, mocked_deactivate.call_count) def test_do_image_reactivate(self): args = argparse.Namespace(id='image1') with mock.patch.object(self.gc.images, 'reactivate') as mocked_reactivate: mocked_reactivate.return_value = 0 test_shell.do_image_reactivate(self.gc, args) self.assertEqual(1, mocked_reactivate.call_count) @mock.patch.object(utils, 'exit') @mock.patch.object(utils, 'print_err') def test_do_image_delete_with_invalid_ids(self, mocked_print_err, mocked_utils_exit): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPNotFound test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) self.assertEqual(2, mocked_print_err.call_count) mocked_utils_exit.assert_called_once_with() @mock.patch.object(utils, 'exit') @mock.patch.object(utils, 'print_err') def test_do_image_delete_with_forbidden_ids(self, mocked_print_err, mocked_utils_exit): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPForbidden test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) self.assertEqual(2, mocked_print_err.call_count) mocked_utils_exit.assert_called_once_with() @mock.patch.object(utils, 'exit') @mock.patch.object(utils, 'print_err') def test_do_image_delete_with_image_in_use(self, mocked_print_err, mocked_utils_exit): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPConflict test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) self.assertEqual(2, mocked_print_err.call_count) mocked_utils_exit.assert_called_once_with() def test_do_image_delete_deleted(self): image_id = 'deleted-img' args = argparse.Namespace(id=[image_id]) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPNotFound self.assert_exits_with_msg(func=test_shell.do_image_delete, func_args=args) @mock.patch.object(utils, 'print_err') def test_do_image_download_with_forbidden_id(self, mocked_print_err): args = self._make_args({'id': 'IMG-01', 'file': None, 'progress': False}) with mock.patch.object(self.gc.images, 'data') as mocked_data: mocked_data.side_effect = exc.HTTPForbidden try: test_shell.do_image_download(self.gc, args) self.fail('Exit not called') except SystemExit: pass self.assertEqual(1, mocked_data.call_count) self.assertEqual(1, mocked_print_err.call_count) @mock.patch.object(utils, 'print_err') def test_do_image_download_with_500(self, mocked_print_err): args = self._make_args({'id': 'IMG-01', 'file': None, 'progress': False}) with mock.patch.object(self.gc.images, 'data') as mocked_data: mocked_data.side_effect = exc.HTTPInternalServerError try: test_shell.do_image_download(self.gc, args) self.fail('Exit not called') except SystemExit: pass self.assertEqual(1, mocked_data.call_count) self.assertEqual(1, mocked_print_err.call_count) def test_do_member_list(self): args = self._make_args({'image_id': 'IMG-01'}) with mock.patch.object(self.gc.image_members, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_member_list(self.gc, args) mocked_list.assert_called_once_with('IMG-01') columns = ['Image ID', 'Member ID', 'Status'] utils.print_list.assert_called_once_with({}, columns) def test_do_member_create(self): args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'}) with mock.patch.object(self.gc.image_members, 'create') as mock_create: mock_create.return_value = {} test_shell.do_member_create(self.gc, args) mock_create.assert_called_once_with('IMG-01', 'MEM-01') columns = ['Image ID', 'Member ID', 'Status'] utils.print_list.assert_called_once_with([{}], columns) def test_do_member_create_with_few_arguments(self): args = self._make_args({'image_id': None, 'member_id': 'MEM-01'}) msg = 'Unable to create member. Specify image_id and member_id' self.assert_exits_with_msg(func=test_shell.do_member_create, func_args=args, err_msg=msg) def test_do_member_update(self): input = { 'image_id': 'IMG-01', 'member_id': 'MEM-01', 'member_status': 'status', } args = self._make_args(input) with mock.patch.object(self.gc.image_members, 'update') as mock_update: mock_update.return_value = {} test_shell.do_member_update(self.gc, args) mock_update.assert_called_once_with('IMG-01', 'MEM-01', 'status') columns = ['Image ID', 'Member ID', 'Status'] utils.print_list.assert_called_once_with([{}], columns) def test_do_member_update_with_few_arguments(self): input = { 'image_id': 'IMG-01', 'member_id': 'MEM-01', 'member_status': None, } args = self._make_args(input) msg = 'Unable to update member. Specify image_id, member_id' \ ' and member_status' self.assert_exits_with_msg(func=test_shell.do_member_update, func_args=args, err_msg=msg) def test_do_member_delete(self): args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'}) with mock.patch.object(self.gc.image_members, 'delete') as mock_delete: test_shell.do_member_delete(self.gc, args) mock_delete.assert_called_once_with('IMG-01', 'MEM-01') def test_do_member_delete_with_few_arguments(self): args = self._make_args({'image_id': None, 'member_id': 'MEM-01'}) msg = 'Unable to delete member. Specify image_id and member_id' self.assert_exits_with_msg(func=test_shell.do_member_delete, func_args=args, err_msg=msg) def test_image_tag_update(self): args = self._make_args({'image_id': 'IMG-01', 'tag_value': 'tag01'}) with mock.patch.object(self.gc.image_tags, 'update') as mocked_update: self.gc.images.get = mock.Mock(return_value={}) mocked_update.return_value = None test_shell.do_image_tag_update(self.gc, args) mocked_update.assert_called_once_with('IMG-01', 'tag01') def test_image_tag_update_with_few_arguments(self): args = self._make_args({'image_id': None, 'tag_value': 'tag01'}) msg = 'Unable to update tag. Specify image_id and tag_value' self.assert_exits_with_msg(func=test_shell.do_image_tag_update, func_args=args, err_msg=msg) def test_image_tag_delete(self): args = self._make_args({'image_id': 'IMG-01', 'tag_value': 'tag01'}) with mock.patch.object(self.gc.image_tags, 'delete') as mocked_delete: mocked_delete.return_value = None test_shell.do_image_tag_delete(self.gc, args) mocked_delete.assert_called_once_with('IMG-01', 'tag01') def test_image_tag_delete_with_few_arguments(self): args = self._make_args({'image_id': 'IMG-01', 'tag_value': None}) msg = 'Unable to delete tag. Specify image_id and tag_value' self.assert_exits_with_msg(func=test_shell.do_image_tag_delete, func_args=args, err_msg=msg) def test_do_md_namespace_create(self): args = self._make_args({'namespace': 'MyNamespace', 'protected': True}) with mock.patch.object(self.gc.metadefs_namespace, 'create') as mocked_create: expect_namespace = { 'namespace': 'MyNamespace', 'protected': True } mocked_create.return_value = expect_namespace test_shell.do_md_namespace_create(self.gc, args) mocked_create.assert_called_once_with(namespace='MyNamespace', protected=True) utils.print_dict.assert_called_once_with(expect_namespace) def test_do_md_namespace_import(self): args = self._make_args({'file': 'test'}) expect_namespace = { 'namespace': 'MyNamespace', 'protected': True } with mock.patch.object(self.gc.metadefs_namespace, 'create') as mocked_create: mock_read = mock.Mock(return_value=json.dumps(expect_namespace)) mock_file = mock.Mock(read=mock_read) utils.get_data_file = mock.Mock(return_value=mock_file) mocked_create.return_value = expect_namespace test_shell.do_md_namespace_import(self.gc, args) mocked_create.assert_called_once_with(**expect_namespace) utils.print_dict.assert_called_once_with(expect_namespace) def test_do_md_namespace_import_invalid_json(self): args = self._make_args({'file': 'test'}) mock_read = mock.Mock(return_value='Invalid') mock_file = mock.Mock(read=mock_read) utils.get_data_file = mock.Mock(return_value=mock_file) self.assertRaises(SystemExit, test_shell.do_md_namespace_import, self.gc, args) def test_do_md_namespace_import_no_input(self): args = self._make_args({'file': None}) utils.get_data_file = mock.Mock(return_value=None) self.assertRaises(SystemExit, test_shell.do_md_namespace_import, self.gc, args) def test_do_md_namespace_update(self): args = self._make_args({'id': 'MyNamespace', 'protected': True}) with mock.patch.object(self.gc.metadefs_namespace, 'update') as mocked_update: expect_namespace = { 'namespace': 'MyNamespace', 'protected': True } mocked_update.return_value = expect_namespace test_shell.do_md_namespace_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', id='MyNamespace', protected=True) utils.print_dict.assert_called_once_with(expect_namespace) def test_do_md_namespace_show(self): args = self._make_args({'namespace': 'MyNamespace', 'max_column_width': 80, 'resource_type': None}) with mock.patch.object(self.gc.metadefs_namespace, 'get') as mocked_get: expect_namespace = {'namespace': 'MyNamespace'} mocked_get.return_value = expect_namespace test_shell.do_md_namespace_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace') utils.print_dict.assert_called_once_with(expect_namespace, 80) def test_do_md_namespace_show_resource_type(self): args = self._make_args({'namespace': 'MyNamespace', 'max_column_width': 80, 'resource_type': 'RESOURCE'}) with mock.patch.object(self.gc.metadefs_namespace, 'get') as mocked_get: expect_namespace = {'namespace': 'MyNamespace'} mocked_get.return_value = expect_namespace test_shell.do_md_namespace_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', resource_type='RESOURCE') utils.print_dict.assert_called_once_with(expect_namespace, 80) def test_do_md_namespace_list(self): args = self._make_args({'resource_type': None, 'visibility': None, 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_page_size(self): args = self._make_args({'resource_type': None, 'visibility': None, 'page_size': 2}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={}, page_size=2) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_one_filter(self): args = self._make_args({'resource_types': ['OS::Compute::Aggregate'], 'visibility': None, 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as \ mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={ 'resource_types': ['OS::Compute::Aggregate']}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_all_filters(self): args = self._make_args({'resource_types': ['OS::Compute::Aggregate'], 'visibility': 'public', 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={ 'resource_types': ['OS::Compute::Aggregate'], 'visibility': 'public'}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_unknown_filter(self): args = self._make_args({'resource_type': None, 'visibility': None, 'some_arg': 'some_value', 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'content': False}) with mock.patch.object(self.gc.metadefs_namespace, 'delete') as \ mocked_delete: test_shell.do_md_namespace_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace') def test_do_md_resource_type_associate(self): args = self._make_args({'namespace': 'MyNamespace', 'name': 'MyResourceType', 'prefix': 'PREFIX:'}) with mock.patch.object(self.gc.metadefs_resource_type, 'associate') as mocked_associate: expect_rt = { 'namespace': 'MyNamespace', 'name': 'MyResourceType', 'prefix': 'PREFIX:' } mocked_associate.return_value = expect_rt test_shell.do_md_resource_type_associate(self.gc, args) mocked_associate.assert_called_once_with('MyNamespace', **expect_rt) utils.print_dict.assert_called_once_with(expect_rt) def test_do_md_resource_type_deassociate(self): args = self._make_args({'namespace': 'MyNamespace', 'resource_type': 'MyResourceType'}) with mock.patch.object(self.gc.metadefs_resource_type, 'deassociate') as mocked_deassociate: test_shell.do_md_resource_type_deassociate(self.gc, args) mocked_deassociate.assert_called_once_with('MyNamespace', 'MyResourceType') def test_do_md_resource_type_list(self): args = self._make_args({}) with mock.patch.object(self.gc.metadefs_resource_type, 'list') as mocked_list: expect_objects = ['MyResourceType1', 'MyResourceType2'] mocked_list.return_value = expect_objects test_shell.do_md_resource_type_list(self.gc, args) self.assertEqual(1, mocked_list.call_count) def test_do_md_namespace_resource_type_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_resource_type, 'get') as mocked_get: expect_objects = [{'namespace': 'MyNamespace', 'object': 'MyObject'}] mocked_get.return_value = expect_objects test_shell.do_md_namespace_resource_type_list(self.gc, args) mocked_get.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with(expect_objects, ['name', 'prefix', 'properties_target']) def test_do_md_property_create(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyProperty", 'title': "Title", 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_property, 'create') as mocked_create: expect_property = { 'namespace': 'MyNamespace', 'name': 'MyProperty', 'title': 'Title' } mocked_create.return_value = expect_property test_shell.do_md_property_create(self.gc, args) mocked_create.assert_called_once_with('MyNamespace', name='MyProperty', title='Title') utils.print_dict.assert_called_once_with(expect_property) def test_do_md_property_create_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyProperty", 'title': "Title", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_property_create, self.gc, args) def test_do_md_property_update(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty', 'name': 'NewName', 'title': "Title", 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_property, 'update') as mocked_update: expect_property = { 'namespace': 'MyNamespace', 'name': 'MyProperty', 'title': 'Title' } mocked_update.return_value = expect_property test_shell.do_md_property_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', 'MyProperty', name='NewName', title='Title') utils.print_dict.assert_called_once_with(expect_property) def test_do_md_property_update_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty', 'name': "MyObject", 'title': "Title", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_property_update, self.gc, args) def test_do_md_property_show(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_property, 'get') as mocked_get: expect_property = { 'namespace': 'MyNamespace', 'property': 'MyProperty', 'title': 'Title' } mocked_get.return_value = expect_property test_shell.do_md_property_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyProperty') utils.print_dict.assert_called_once_with(expect_property, 80) def test_do_md_property_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty'}) with mock.patch.object(self.gc.metadefs_property, 'delete') as mocked_delete: test_shell.do_md_property_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace', 'MyProperty') def test_do_md_namespace_property_delete(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_property, 'delete_all') as mocked_delete_all: test_shell.do_md_namespace_properties_delete(self.gc, args) mocked_delete_all.assert_called_once_with('MyNamespace') def test_do_md_property_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_property, 'list') as mocked_list: expect_objects = [{'namespace': 'MyNamespace', 'property': 'MyProperty', 'title': 'MyTitle'}] mocked_list.return_value = expect_objects test_shell.do_md_property_list(self.gc, args) mocked_list.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with(expect_objects, ['name', 'title', 'type']) def test_do_md_object_create(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyObject", 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_object, 'create') as mocked_create: expect_object = { 'namespace': 'MyNamespace', 'name': 'MyObject' } mocked_create.return_value = expect_object test_shell.do_md_object_create(self.gc, args) mocked_create.assert_called_once_with('MyNamespace', name='MyObject') utils.print_dict.assert_called_once_with(expect_object) def test_do_md_object_create_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyObject", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_object_create, self.gc, args) def test_do_md_object_update(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'name': 'NewName', 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_object, 'update') as mocked_update: expect_object = { 'namespace': 'MyNamespace', 'name': 'MyObject' } mocked_update.return_value = expect_object test_shell.do_md_object_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', 'MyObject', name='NewName') utils.print_dict.assert_called_once_with(expect_object) def test_do_md_object_update_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'name': "MyObject", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_object_update, self.gc, args) def test_do_md_object_show(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get: expect_object = { 'namespace': 'MyNamespace', 'object': 'MyObject' } mocked_get.return_value = expect_object test_shell.do_md_object_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyObject') utils.print_dict.assert_called_once_with(expect_object, 80) def test_do_md_object_property_show(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'property': 'MyProperty', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get: expect_object = {'name': 'MyObject', 'properties': { 'MyProperty': {'type': 'string'} }} mocked_get.return_value = expect_object test_shell.do_md_object_property_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyObject') utils.print_dict.assert_called_once_with({'type': 'string', 'name': 'MyProperty'}, 80) def test_do_md_object_property_show_non_existing(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'property': 'MyProperty', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get: expect_object = {'name': 'MyObject', 'properties': {}} mocked_get.return_value = expect_object self.assertRaises(SystemExit, test_shell.do_md_object_property_show, self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyObject') def test_do_md_object_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject'}) with mock.patch.object(self.gc.metadefs_object, 'delete') as mocked_delete: test_shell.do_md_object_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace', 'MyObject') def test_do_md_namespace_objects_delete(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_object, 'delete_all') as mocked_delete_all: test_shell.do_md_namespace_objects_delete(self.gc, args) mocked_delete_all.assert_called_once_with('MyNamespace') def test_do_md_object_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_object, 'list') as mocked_list: expect_objects = [{'namespace': 'MyNamespace', 'object': 'MyObject'}] mocked_list.return_value = expect_objects test_shell.do_md_object_list(self.gc, args) mocked_list.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with( expect_objects, ['name', 'description'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) def test_do_md_tag_create(self): args = self._make_args({'namespace': 'MyNamespace', 'name': 'MyTag'}) with mock.patch.object(self.gc.metadefs_tag, 'create') as mocked_create: expect_tag = { 'namespace': 'MyNamespace', 'name': 'MyTag' } mocked_create.return_value = expect_tag test_shell.do_md_tag_create(self.gc, args) mocked_create.assert_called_once_with('MyNamespace', 'MyTag') utils.print_dict.assert_called_once_with(expect_tag) def test_do_md_tag_update(self): args = self._make_args({'namespace': 'MyNamespace', 'tag': 'MyTag', 'name': 'NewTag'}) with mock.patch.object(self.gc.metadefs_tag, 'update') as mocked_update: expect_tag = { 'namespace': 'MyNamespace', 'name': 'NewTag' } mocked_update.return_value = expect_tag test_shell.do_md_tag_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', 'MyTag', name='NewTag') utils.print_dict.assert_called_once_with(expect_tag) def test_do_md_tag_show(self): args = self._make_args({'namespace': 'MyNamespace', 'tag': 'MyTag', 'sort_dir': 'desc'}) with mock.patch.object(self.gc.metadefs_tag, 'get') as mocked_get: expect_tag = { 'namespace': 'MyNamespace', 'tag': 'MyTag' } mocked_get.return_value = expect_tag test_shell.do_md_tag_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyTag') utils.print_dict.assert_called_once_with(expect_tag) def test_do_md_tag_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'tag': 'MyTag'}) with mock.patch.object(self.gc.metadefs_tag, 'delete') as mocked_delete: test_shell.do_md_tag_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace', 'MyTag') def test_do_md_namespace_tags_delete(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_tag, 'delete_all') as mocked_delete_all: test_shell.do_md_namespace_tags_delete(self.gc, args) mocked_delete_all.assert_called_once_with('MyNamespace') def test_do_md_tag_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_tag, 'list') as mocked_list: expect_tags = [{'namespace': 'MyNamespace', 'tag': 'MyTag'}] mocked_list.return_value = expect_tags test_shell.do_md_tag_list(self.gc, args) mocked_list.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with( expect_tags, ['name'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) def test_do_md_tag_create_multiple(self): args = self._make_args({'namespace': 'MyNamespace', 'delim': ',', 'names': 'MyTag1, MyTag2'}) with mock.patch.object( self.gc.metadefs_tag, 'create_multiple') as mocked_create_tags: expect_tags = [{'tags': [{'name': 'MyTag1'}, {'name': 'MyTag2'}]}] mocked_create_tags.return_value = expect_tags test_shell.do_md_tag_create_multiple(self.gc, args) mocked_create_tags.assert_called_once_with( 'MyNamespace', tags=['MyTag1', 'MyTag2']) utils.print_list.assert_called_once_with( expect_tags, ['name'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) python-glanceclient-2.9.1/glanceclient/tests/unit/v1/0000775000175100017510000000000013232162126022613 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/v1/test_image_members.py0000666000175100017510000001031313232161652027023 0ustar zuulzuul00000000000000# 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 from glanceclient.tests import utils import glanceclient.v1.image_members import glanceclient.v1.images fixtures = { '/v1/images/1/members': { 'GET': ( {}, {'members': [ {'member_id': '1', 'can_share': False}, ]}, ), 'PUT': ({}, None), }, '/v1/images/1/members/1': { 'GET': ( {}, {'member': { 'member_id': '1', 'can_share': False, }}, ), 'PUT': ({}, None), 'DELETE': ({}, None), }, '/v1/shared-images/1': { 'GET': ( {}, {'shared_images': [ {'image_id': '1', 'can_share': False}, ]}, ), }, } class ImageMemberManagerTest(testtools.TestCase): def setUp(self): super(ImageMemberManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api) self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True) def test_list_by_image(self): members = self.mgr.list(image=self.image) expect = [('GET', '/v1/images/1/members', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual(1, len(members)) self.assertEqual('1', members[0].member_id) self.assertEqual('1', members[0].image_id) self.assertEqual(False, members[0].can_share) def test_list_by_member(self): resource_class = glanceclient.v1.image_members.ImageMember member = resource_class(self.api, {'member_id': '1'}, True) self.mgr.list(member=member) expect = [('GET', '/v1/shared-images/1', {}, None)] self.assertEqual(expect, self.api.calls) def test_get(self): member = self.mgr.get(self.image, '1') expect = [('GET', '/v1/images/1/members/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', member.member_id) self.assertEqual('1', member.image_id) self.assertEqual(False, member.can_share) def test_delete(self): self.mgr.delete('1', '1') expect = [('DELETE', '/v1/images/1/members/1', {}, None)] self.assertEqual(expect, self.api.calls) def test_create(self): self.mgr.create(self.image, '1', can_share=True) expect_body = {'member': {'can_share': True}} expect = [('PUT', '/v1/images/1/members/1', {}, sorted(expect_body.items()))] self.assertEqual(expect, self.api.calls) def test_replace(self): body = [ {'member_id': '2', 'can_share': False}, {'member_id': '3'}, ] self.mgr.replace(self.image, body) expect = [('PUT', '/v1/images/1/members', {}, sorted({'memberships': body}.items()))] self.assertEqual(expect, self.api.calls) def test_replace_objects(self): body = [ glanceclient.v1.image_members.ImageMember( self.mgr, {'member_id': '2', 'can_share': False}, True), glanceclient.v1.image_members.ImageMember( self.mgr, {'member_id': '3', 'can_share': True}, True), ] self.mgr.replace(self.image, body) expect_body = { 'memberships': [ {'member_id': '2', 'can_share': False}, {'member_id': '3', 'can_share': True}, ], } expect = [('PUT', '/v1/images/1/members', {}, sorted(expect_body.items()))] self.assertEqual(expect, self.api.calls) python-glanceclient-2.9.1/glanceclient/tests/unit/v1/__init__.py0000666000175100017510000000000013232161652024717 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/unit/v1/test_shell.py0000666000175100017510000005403713232161652025351 0ustar zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! 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 argparse import json import os import six import subprocess import tempfile import testtools import mock from glanceclient import exc from glanceclient import shell import glanceclient.v1.client as client import glanceclient.v1.images import glanceclient.v1.shell as v1shell from glanceclient.tests import utils if six.PY3: import io file_type = io.IOBase else: file_type = file fixtures = { '/v1/images/96d2c7e1-de4e-4612-8aa2-ba26610c804e': { 'PUT': ( { 'Location': 'http://fakeaddress.com:9292/v1/images/' '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'X-Openstack-Request-Id': 'req-b645039d-e1c7-43e5-b27b-2d18a173c42b', 'Date': 'Mon, 29 Apr 2013 10:24:32 GMT' }, json.dumps({ 'image': { 'status': 'active', 'name': 'testimagerename', 'deleted': False, 'container_format': 'ami', 'created_at': '2013-04-25T15:47:43', 'disk_format': 'ami', 'updated_at': '2013-04-29T10:24:32', 'id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'min_disk': 0, 'protected': False, 'min_ram': 0, 'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'owner': '1310db0cce8f40b0987a5acbe139765a', 'is_public': True, 'deleted_at': None, 'properties': { 'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114' }, 'size': 25165824 } }) ), 'HEAD': ( { 'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'x-image-meta-status': 'active' }, None ), 'GET': ( { 'x-image-meta-status': 'active', 'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a', 'x-image-meta-name': 'cirros-0.3.1-x86_64-uec', 'x-image-meta-container_format': 'ami', 'x-image-meta-created_at': '2013-04-25T15:47:43', 'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'location': 'http://fakeaddress.com:9292/v1/images/' '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'x-image-meta-min_ram': '0', 'x-image-meta-updated_at': '2013-04-25T15:47:43', 'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'x-image-meta-property-ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114', 'date': 'Mon, 29 Apr 2013 09:25:17 GMT', 'x-image-meta-property-kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'x-openstack-request-id': 'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f', 'x-image-meta-deleted': 'False', 'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'x-image-meta-protected': 'False', 'x-image-meta-min_disk': '0', 'x-image-meta-size': '25165824', 'x-image-meta-is_public': 'True', 'content-type': 'text/html; charset=UTF-8', 'x-image-meta-disk_format': 'ami', }, None ) }, '/v1/images/44d2c7e1-de4e-4612-8aa2-ba26610c444f': { 'PUT': ( { 'Location': 'http://fakeaddress.com:9292/v1/images/' '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'X-Openstack-Request-Id': 'req-b645039d-e1c7-43e5-b27b-2d18a173c42b', 'Date': 'Mon, 29 Apr 2013 10:24:32 GMT' }, json.dumps({ 'image': { 'status': 'queued', 'name': 'testimagerename', 'deleted': False, 'container_format': 'ami', 'created_at': '2013-04-25T15:47:43', 'disk_format': 'ami', 'updated_at': '2013-04-29T10:24:32', 'id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'min_disk': 0, 'protected': False, 'min_ram': 0, 'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'owner': '1310db0cce8f40b0987a5acbe139765a', 'is_public': True, 'deleted_at': None, 'properties': { 'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114' }, 'size': 25165824 } }) ), 'HEAD': ( { 'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'x-image-meta-status': 'queued' }, None ), 'GET': ( { 'x-image-meta-status': 'queued', 'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a', 'x-image-meta-name': 'cirros-0.3.1-x86_64-uec', 'x-image-meta-container_format': 'ami', 'x-image-meta-created_at': '2013-04-25T15:47:43', 'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'location': 'http://fakeaddress.com:9292/v1/images/' '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'x-image-meta-min_ram': '0', 'x-image-meta-updated_at': '2013-04-25T15:47:43', 'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'x-image-meta-property-ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114', 'date': 'Mon, 29 Apr 2013 09:25:17 GMT', 'x-image-meta-property-kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'x-openstack-request-id': 'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f', 'x-image-meta-deleted': 'False', 'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'x-image-meta-protected': 'False', 'x-image-meta-min_disk': '0', 'x-image-meta-size': '25165824', 'x-image-meta-is_public': 'True', 'content-type': 'text/html; charset=UTF-8', 'x-image-meta-disk_format': 'ami', }, None ) }, '/v1/images/detail?limit=20&name=70aa106f-3750-4d7c-a5ce-0a535ac08d0a': { 'GET': ( {}, {'images': [ { 'id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a', 'name': 'imagedeleted', 'deleted': True, 'status': 'deleted', }, ]}, ), }, '/v1/images/70aa106f-3750-4d7c-a5ce-0a535ac08d0a': { 'HEAD': ( { 'x-image-meta-id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a', 'x-image-meta-status': 'deleted' }, None ) } } class ShellInvalidEndpointandParameterTest(utils.TestCase): # Patch os.environ to avoid required auth info. def setUp(self): """Run before each test.""" super(ShellInvalidEndpointandParameterTest, self).setUp() self.old_environment = os.environ.copy() os.environ = { 'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_TOKEN_ID': 'test', 'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/', 'OS_AUTH_TOKEN': 'pass', 'OS_IMAGE_API_VERSION': '1', 'OS_REGION_NAME': 'test', 'OS_IMAGE_URL': 'http://is.invalid'} self.shell = shell.OpenStackImagesShell() self.patched = mock.patch('glanceclient.common.utils.get_data_file', autospec=True, return_value=None) self.mock_get_data_file = self.patched.start() self.gc = self._mock_glance_client() def _make_args(self, args): # NOTE(venkatesh): this conversion from a dict to an object # is required because the test_shell.do_xxx(gc, args) methods # expects the args to be attributes of an object. If passed as # dict directly, it throws an AttributeError. class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) def _mock_glance_client(self): my_mocked_gc = mock.Mock() my_mocked_gc.get.return_value = {} return my_mocked_gc def tearDown(self): super(ShellInvalidEndpointandParameterTest, self).tearDown() os.environ = self.old_environment self.patched.stop() def run_command(self, cmd): self.shell.main(cmd.split()) def assert_called(self, method, url, body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, **kwargs) def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) def test_image_list_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-list') def test_image_create_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-create') def test_image_delete_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-delete ') def test_image_download_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-download ') def test_members_list_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'member-list --image-id fake') def test_image_show_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-show --human-readable ') def test_member_create_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'member-create --can-share ') def test_member_delete_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'member-delete ') @mock.patch('sys.stderr') def test_image_create_invalid_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-create --size 10gb') @mock.patch('sys.stderr') def test_image_create_invalid_ram_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-create --min-ram 10gb') @mock.patch('sys.stderr') def test_image_create_invalid_min_disk_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-create --min-disk 10gb') @mock.patch('sys.stderr') def test_image_create_missing_disk_format(self, __): # We test for all possible sources for origin in ('--file', '--location', '--copy-from'): e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create ' + origin + ' fake_src --container-format bare') self.assertEqual('error: Must provide --disk-format when using ' + origin + '.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format(self, __): # We test for all possible sources for origin in ('--file', '--location', '--copy-from'): e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create ' + origin + ' fake_src --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using ' + origin + '.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = six.StringIO() e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create' ' --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using stdin.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_disk_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = six.StringIO() e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create' ' --container-format bare') self.assertEqual('error: Must provide --disk-format when using stdin.', e.message) @mock.patch('sys.stderr') def test_image_update_invalid_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-update --size 10gb') @mock.patch('sys.stderr') def test_image_update_invalid_min_disk_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-update --min-disk 10gb') @mock.patch('sys.stderr') def test_image_update_invalid_ram_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-update --min-ram 10gb') @mock.patch('sys.stderr') def test_image_list_invalid_min_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-list --size-min 10gb') @mock.patch('sys.stderr') def test_image_list_invalid_max_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-list --size-max 10gb') def test_do_image_list_with_changes_since(self): input = { 'name': None, 'limit': None, 'status': None, 'container_format': 'bare', 'size_min': None, 'size_max': None, 'is_public': True, 'disk_format': 'raw', 'page_size': 20, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': None, 'sort_dir': None, 'all_tenants': False, 'human_readable': True, 'changes_since': '2011-1-1' } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} v1shell.do_image_list(self.gc, args) exp_img_filters = {'container_format': 'bare', 'changes-since': '2011-1-1', 'disk_format': 'raw', 'is_public': True} mocked_list.assert_called_once_with(sort_dir=None, sort_key=None, owner='test', page_size=20, filters=exp_img_filters) class ShellStdinHandlingTests(testtools.TestCase): def _fake_update_func(self, *args, **kwargs): """Replace glanceclient.images.update with a fake. To determine the parameters that would be supplied with the update request. """ # Store passed in args self.collected_args = (args, kwargs) # Return the first arg, which is an image, # as do_image_update expects this. return args[0] def setUp(self): super(ShellStdinHandlingTests, self).setUp() self.api = utils.FakeAPI(fixtures) self.gc = client.Client("http://fakeaddress.com") self.gc.images = glanceclient.v1.images.ImageManager(self.api) # Store real stdin, so it can be restored in tearDown. self.real_sys_stdin_fd = os.dup(0) # Replace stdin with a FD that points to /dev/null. dev_null = open('/dev/null') self.dev_null_fd = dev_null.fileno() os.dup2(dev_null.fileno(), 0) # Replace the image update function with a fake, # so that we can tell if the data field was set correctly. self.real_update_func = self.gc.images.update self.collected_args = [] self.gc.images.update = self._fake_update_func def tearDown(self): """Restore stdin and gc.images.update to their pretest states.""" super(ShellStdinHandlingTests, self).tearDown() def try_close(fd): try: os.close(fd) except OSError: # Already closed pass # Restore stdin os.dup2(self.real_sys_stdin_fd, 0) # Close duplicate stdin handle try_close(self.real_sys_stdin_fd) # Close /dev/null handle try_close(self.dev_null_fd) # Restore the real image update function self.gc.images.update = self.real_update_func def _do_update(self, image='96d2c7e1-de4e-4612-8aa2-ba26610c804e'): """call v1/shell's do_image_update function.""" v1shell.do_image_update( self.gc, argparse.Namespace( image=image, name='testimagerename', property={}, purge_props=False, human_readable=False, file=None, progress=False ) ) def test_image_delete_deleted(self): self.assertRaises( exc.CommandError, v1shell.do_image_delete, self.gc, argparse.Namespace( images=['70aa106f-3750-4d7c-a5ce-0a535ac08d0a'] ) ) def test_image_update_closed_stdin(self): """Test image update with a closed stdin. Supply glanceclient with a closed stdin, and perform an image update to an active image. Glanceclient should not attempt to read stdin. """ # NOTE(hughsaunders) Close stdin, which is repointed to /dev/null by # setUp() os.close(0) self._do_update() self.assertTrue( 'data' not in self.collected_args[1] or self.collected_args[1]['data'] is None ) def test_image_update_opened_stdin(self): """Test image update with an opened stdin. Supply glanceclient with a stdin, and perform an image update to an active image. Glanceclient should not allow it. """ self.assertRaises( SystemExit, v1shell.do_image_update, self.gc, argparse.Namespace( image='96d2c7e1-de4e-4612-8aa2-ba26610c804e', property={}, ) ) def test_image_update_data_is_read_from_file(self): """Ensure that data is read from a file.""" try: # NOTE(hughsaunders) Create a tmpfile, write some data to it and # set it as stdin f = open(tempfile.mktemp(), 'w+') f.write('Some Data') f.flush() f.seek(0) os.dup2(f.fileno(), 0) self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f') self.assertIn('data', self.collected_args[1]) self.assertIsInstance(self.collected_args[1]['data'], file_type) self.assertEqual(b'Some Data', self.collected_args[1]['data'].read()) finally: try: f.close() os.remove(f.name) except Exception: pass def test_image_update_data_is_read_from_pipe(self): """Ensure that data is read from a pipe.""" try: # NOTE(hughsaunders): Setup a pipe, duplicate it to stdin # ensure it is read. process = subprocess.Popen(['/bin/echo', 'Some Data'], stdout=subprocess.PIPE) os.dup2(process.stdout.fileno(), 0) self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f') self.assertIn('data', self.collected_args[1]) self.assertIsInstance(self.collected_args[1]['data'], file_type) self.assertEqual(b'Some Data\n', self.collected_args[1]['data'].read()) finally: try: process.stdout.close() except OSError: pass python-glanceclient-2.9.1/glanceclient/tests/unit/v1/test_versions.py0000666000175100017510000000476513232161652026115 0ustar zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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 testtools from glanceclient.tests import utils import glanceclient.v1.versions fixtures = { '/versions': { 'GET': ( {}, {"versions": [ { "status": "EXPERIMENTAL", "id": "v3.0", "links": [ { "href": "http://10.229.45.145:9292/v3/", "rel": "self" } ] }, { "status": "CURRENT", "id": "v2.3", "links": [ { "href": "http://10.229.45.145:9292/v2/", "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v1.0", "links": [ { "href": "http://10.229.45.145:9292/v1/", "rel": "self" } ] } ]} ) } } class TestVersions(testtools.TestCase): def setUp(self): super(TestVersions, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.versions.VersionManager(self.api) def test_version_list(self): versions = self.mgr.list() expect = [('GET', '/versions', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual(3, len(versions)) self.assertEqual('v3.0', versions[0]['id']) self.assertEqual('EXPERIMENTAL', versions[0]['status']) self.assertEqual([{"href": "http://10.229.45.145:9292/v3/", "rel": "self"}], versions[0]['links']) python-glanceclient-2.9.1/glanceclient/tests/unit/v1/test_images.py0000666000175100017510000007711413232161652025510 0ustar zuulzuul00000000000000# 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 errno import json import testtools import six from six.moves.urllib import parse from glanceclient.tests import utils from glanceclient.v1 import client from glanceclient.v1 import images from glanceclient.v1 import shell fixtures = { '/v1/images': { 'POST': ( { 'location': '/v1/images/1', 'x-openstack-request-id': 'req-1234', }, json.dumps( {'image': { 'id': '1', 'name': 'image-1', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': '1024', 'min_ram': '512', 'min_disk': '10', 'properties': {'a': 'b', 'c': 'd'}, 'is_public': False, 'protected': False, 'deleted': False, }}, ), ), }, '/v1/images/detail?limit=20': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?is_public=None&limit=20': { 'GET': ( {'x-openstack-request-id': 'req-1234'}, {'images': [ { 'id': 'a', 'owner': 'A', 'is_public': 'True', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'owner': 'B', 'is_public': 'False', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'is_public': 'False', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?is_public=None&limit=5': { 'GET': ( {}, {'images': [ { 'id': 'a', 'owner': 'A', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'owner': 'B', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b2', 'owner': 'B', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=5': { 'GET': ( {}, {'images': [ { 'id': 'a', 'owner': 'A', 'is_public': 'False', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'owner': 'A', 'is_public': 'False', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b2', 'owner': 'B', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'is_public': 'True', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=20&marker=a': { 'GET': ( {}, {'images': [ { 'id': 'b', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=1': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-0', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=1&marker=a': { 'GET': ( {}, {'images': [ { 'id': 'b', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=2': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=2&marker=b': { 'GET': ( {}, {'images': [ { 'id': 'c', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=20&name=foo': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=20&property-ping=pong': { 'GET': ( {}, {'images': [ { 'id': '1', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=20&sort_dir=desc': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=20&sort_key=name': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/1': { 'HEAD': ( { 'x-image-meta-id': '1', 'x-image-meta-name': 'image-1', 'x-image-meta-property-arch': 'x86_64', 'x-image-meta-is_public': 'false', 'x-image-meta-protected': 'false', 'x-image-meta-deleted': 'false', }, None), 'GET': ( {}, 'XXX', ), 'PUT': ( {}, json.dumps( {'image': { 'id': '1', 'name': 'image-2', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': '1024', 'min_ram': '512', 'min_disk': '10', 'properties': {'a': 'b', 'c': 'd'}, 'is_public': False, 'protected': False, }}, ), ), 'DELETE': ({}, None), }, '/v1/images/2': { 'HEAD': ( { 'x-image-meta-id': '2' }, None, ), 'GET': ( { 'x-image-meta-checksum': 'wrong' }, 'YYY', ), }, '/v1/images/3': { 'HEAD': ( { 'x-image-meta-id': '3', 'x-image-meta-name': u"ni\xf1o" }, None, ), 'GET': ( { 'x-image-meta-checksum': '0745064918b49693cca64d6b6a13d28a' }, 'ZZZ', ), }, '/v1/images/4': { 'HEAD': ( { 'x-image-meta-id': '4', 'x-image-meta-name': 'image-4', 'x-image-meta-property-arch': 'x86_64', 'x-image-meta-is_public': 'false', 'x-image-meta-protected': 'false', 'x-image-meta-deleted': 'false', 'x-openstack-request-id': 'req-1234', }, None), 'GET': ( { 'x-openstack-request-id': 'req-1234', }, 'XXX', ), 'PUT': ( { 'x-openstack-request-id': 'req-1234', }, json.dumps( {'image': { 'id': '4', 'name': 'image-4', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': '1024', 'min_ram': '512', 'min_disk': '10', 'properties': {'a': 'b', 'c': 'd'}, 'is_public': False, 'protected': False, }}, ), ), 'DELETE': ( { 'x-openstack-request-id': 'req-1234', }, None), }, '/v1/images/v2_created_img': { 'PUT': ( {}, json.dumps({ "image": { "status": "queued", "deleted": False, "container_format": "bare", "min_ram": 0, "updated_at": "2013-12-20T01:51:45", "owner": "foo", "min_disk": 0, "is_public": False, "deleted_at": None, "id": "v2_created_img", "size": None, "name": "bar", "checksum": None, "created_at": "2013-12-20T01:50:38", "disk_format": "qcow2", "properties": {}, "protected": False } }) ), }, } class ImageManagerTest(testtools.TestCase): def setUp(self): super(ImageManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = images.ImageManager(self.api) def test_paginated_list(self): images = list(self.mgr.list(page_size=2)) expect = [ ('GET', '/v1/images/detail?limit=2', {}, None), ('GET', '/v1/images/detail?limit=2&marker=b', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(3, len(images)) self.assertEqual('a', images[0].id) self.assertEqual('b', images[1].id) self.assertEqual('c', images[2].id) def test_list_with_limit_less_than_page_size(self): results = list(self.mgr.list(page_size=2, limit=1)) expect = [('GET', '/v1/images/detail?limit=2', {}, None)] self.assertEqual(1, len(results)) self.assertEqual(expect, self.api.calls) def test_list_with_limit_greater_than_page_size(self): images = list(self.mgr.list(page_size=1, limit=2)) expect = [ ('GET', '/v1/images/detail?limit=1', {}, None), ('GET', '/v1/images/detail?limit=1&marker=a', {}, None), ] self.assertEqual(2, len(images)) self.assertEqual('a', images[0].id) self.assertEqual('b', images[1].id) self.assertEqual(expect, self.api.calls) def test_list_with_marker(self): list(self.mgr.list(marker='a')) url = '/v1/images/detail?limit=20&marker=a' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_filter(self): list(self.mgr.list(filters={'name': "foo"})) url = '/v1/images/detail?limit=20&name=foo' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_property_filters(self): list(self.mgr.list(filters={'properties': {'ping': 'pong'}})) url = '/v1/images/detail?limit=20&property-ping=pong' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_sort_dir(self): list(self.mgr.list(sort_dir='desc')) url = '/v1/images/detail?limit=20&sort_dir=desc' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_sort_key(self): list(self.mgr.list(sort_key='name')) url = '/v1/images/detail?limit=20&sort_key=name' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_get(self): image = self.mgr.get('1') expect = [('HEAD', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-1', image.name) self.assertEqual(False, image.is_public) self.assertEqual(False, image.protected) self.assertEqual(False, image.deleted) self.assertEqual({u'arch': u'x86_64'}, image.properties) def test_get_int(self): image = self.mgr.get(1) expect = [('HEAD', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-1', image.name) self.assertEqual(False, image.is_public) self.assertEqual(False, image.protected) self.assertEqual(False, image.deleted) self.assertEqual({u'arch': u'x86_64'}, image.properties) def test_get_encoding(self): image = self.mgr.get('3') self.assertEqual(u"ni\xf1o", image.name) def test_get_req_id(self): params = {'return_req_id': []} self.mgr.get('4', **params) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_data(self): data = ''.join([b for b in self.mgr.data('1', do_checksum=False)]) expect = [('GET', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) expect += [('GET', '/v1/images/1', {}, None)] data = ''.join([b for b in self.mgr.data('1')]) self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) def test_data_with_wrong_checksum(self): data = ''.join([b for b in self.mgr.data('2', do_checksum=False)]) expect = [('GET', '/v1/images/2', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('YYY', data) expect += [('GET', '/v1/images/2', {}, None)] data = self.mgr.data('2') self.assertEqual(expect, self.api.calls) try: data = ''.join([b for b in data]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong' self.assertIn(msg, str(e)) def test_data_req_id(self): params = { 'do_checksum': False, 'return_req_id': [], } ''.join([b for b in self.mgr.data('4', **params)]) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_data_with_checksum(self): data = ''.join([b for b in self.mgr.data('3', do_checksum=False)]) expect = [('GET', '/v1/images/3', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) expect += [('GET', '/v1/images/3', {}, None)] data = ''.join([b for b in self.mgr.data('3')]) self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) def test_delete(self): self.mgr.delete('1') expect = [('DELETE', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_req_id(self): params = { 'return_req_id': [] } self.mgr.delete('4', **params) expect = [('DELETE', '/v1/images/4', {}, None)] self.assertEqual(self.api.calls, expect) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_create_without_data(self): params = { 'id': '1', 'name': 'image-1', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': 1024, 'min_ram': 512, 'min_disk': 10, 'copy_from': 'http://example.com', 'properties': {'a': 'b', 'c': 'd'}, } image = self.mgr.create(**params) expect_headers = { 'x-image-meta-id': '1', 'x-image-meta-name': 'image-1', 'x-image-meta-container_format': 'ovf', 'x-image-meta-disk_format': 'vhd', 'x-image-meta-owner': 'asdf', 'x-image-meta-size': '1024', 'x-image-meta-min_ram': '512', 'x-image-meta-min_disk': '10', 'x-glance-api-copy-from': 'http://example.com', 'x-image-meta-property-a': 'b', 'x-image-meta-property-c': 'd', } expect = [('POST', '/v1/images', expect_headers, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-1', image.name) self.assertEqual('ovf', image.container_format) self.assertEqual('vhd', image.disk_format) self.assertEqual('asdf', image.owner) self.assertEqual(1024, image.size) self.assertEqual(512, image.min_ram) self.assertEqual(10, image.min_disk) self.assertEqual(False, image.is_public) self.assertEqual(False, image.protected) self.assertEqual(False, image.deleted) self.assertEqual({'a': 'b', 'c': 'd'}, image.properties) def test_create_with_data(self): image_data = six.StringIO('XXX') self.mgr.create(data=image_data) expect_headers = {'x-image-meta-size': '3'} expect = [('POST', '/v1/images', expect_headers, image_data)] self.assertEqual(expect, self.api.calls) def test_create_req_id(self): params = { 'id': '4', 'name': 'image-4', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': 1024, 'min_ram': 512, 'min_disk': 10, 'copy_from': 'http://example.com', 'properties': {'a': 'b', 'c': 'd'}, 'return_req_id': [], } image = self.mgr.create(**params) expect_headers = { 'x-image-meta-id': '4', 'x-image-meta-name': 'image-4', 'x-image-meta-container_format': 'ovf', 'x-image-meta-disk_format': 'vhd', 'x-image-meta-owner': 'asdf', 'x-image-meta-size': '1024', 'x-image-meta-min_ram': '512', 'x-image-meta-min_disk': '10', 'x-glance-api-copy-from': 'http://example.com', 'x-image-meta-property-a': 'b', 'x-image-meta-property-c': 'd', } expect = [('POST', '/v1/images', expect_headers, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_update(self): fields = { 'name': 'image-2', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': 1024, 'min_ram': 512, 'min_disk': 10, 'copy_from': 'http://example.com', 'properties': {'a': 'b', 'c': 'd'}, 'deleted': False, } image = self.mgr.update('1', **fields) expect_hdrs = { 'x-image-meta-name': 'image-2', 'x-image-meta-container_format': 'ovf', 'x-image-meta-disk_format': 'vhd', 'x-image-meta-owner': 'asdf', 'x-image-meta-size': '1024', 'x-image-meta-min_ram': '512', 'x-image-meta-min_disk': '10', 'x-glance-api-copy-from': 'http://example.com', 'x-image-meta-property-a': 'b', 'x-image-meta-property-c': 'd', 'x-image-meta-deleted': 'False', 'x-glance-registry-purge-props': 'false', } expect = [('PUT', '/v1/images/1', expect_hdrs, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-2', image.name) self.assertEqual(1024, image.size) self.assertEqual(512, image.min_ram) self.assertEqual(10, image.min_disk) def test_update_with_data(self): image_data = six.StringIO('XXX') self.mgr.update('1', data=image_data) expect_headers = {'x-image-meta-size': '3', 'x-glance-registry-purge-props': 'false'} expect = [('PUT', '/v1/images/1', expect_headers, image_data)] self.assertEqual(expect, self.api.calls) def test_update_with_purge_props(self): self.mgr.update('1', purge_props=True) expect_headers = {'x-glance-registry-purge-props': 'true'} expect = [('PUT', '/v1/images/1', expect_headers, None)] self.assertEqual(expect, self.api.calls) def test_update_with_purge_props_false(self): self.mgr.update('1', purge_props=False) expect_headers = {'x-glance-registry-purge-props': 'false'} expect = [('PUT', '/v1/images/1', expect_headers, None)] self.assertEqual(expect, self.api.calls) def test_update_req_id(self): fields = { 'purge_props': True, 'return_req_id': [], } self.mgr.update('4', **fields) expect_headers = {'x-glance-registry-purge-props': 'true'} expect = [('PUT', '/v1/images/4', expect_headers, None)] self.assertEqual(expect, self.api.calls) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, fields['return_req_id']) def test_image_meta_from_headers_encoding(self): value = u"ni\xf1o" if six.PY2: fields = {"x-image-meta-name": "ni\xc3\xb1o"} else: fields = {"x-image-meta-name": value} headers = self.mgr._image_meta_from_headers(fields) self.assertEqual(value, headers["name"]) def test_image_list_with_owner(self): images = self.mgr.list(owner='A', page_size=20) image_list = list(images) self.assertEqual('A', image_list[0].owner) self.assertEqual('a', image_list[0].id) self.assertEqual(1, len(image_list)) def test_image_list_with_owner_req_id(self): fields = { 'owner': 'A', 'return_req_id': [], } images = self.mgr.list(**fields) next(images) self.assertEqual(['req-1234'], fields['return_req_id']) def test_image_list_with_notfound_owner(self): images = self.mgr.list(owner='X', page_size=20) self.assertEqual(0, len(list(images))) def test_image_list_with_empty_string_owner(self): images = self.mgr.list(owner='', page_size=20) image_list = list(images) self.assertRaises(AttributeError, lambda: image_list[0].owner) self.assertEqual('c', image_list[0].id) self.assertEqual(1, len(image_list)) def test_image_list_with_unspecified_owner(self): images = self.mgr.list(owner=None, page_size=5) image_list = list(images) self.assertEqual('A', image_list[0].owner) self.assertEqual('a', image_list[0].id) self.assertEqual('A', image_list[1].owner) self.assertEqual('b', image_list[1].id) self.assertEqual('B', image_list[2].owner) self.assertEqual('b2', image_list[2].id) self.assertRaises(AttributeError, lambda: image_list[3].owner) self.assertEqual('c', image_list[3].id) self.assertEqual(4, len(image_list)) def test_image_list_with_owner_and_limit(self): images = self.mgr.list(owner='B', page_size=5, limit=1) image_list = list(images) self.assertEqual('B', image_list[0].owner) self.assertEqual('b', image_list[0].id) self.assertEqual(1, len(image_list)) def test_image_list_all_tenants(self): images = self.mgr.list(is_public=None, page_size=5) image_list = list(images) self.assertEqual('A', image_list[0].owner) self.assertEqual('a', image_list[0].id) self.assertEqual('B', image_list[1].owner) self.assertEqual('b', image_list[1].id) self.assertEqual('B', image_list[2].owner) self.assertEqual('b2', image_list[2].id) self.assertRaises(AttributeError, lambda: image_list[3].owner) self.assertEqual('c', image_list[3].id) self.assertEqual(4, len(image_list)) def test_update_v2_created_image_using_v1(self): fields_to_update = { 'name': 'bar', 'container_format': 'bare', 'disk_format': 'qcow2', } image = self.mgr.update('v2_created_img', **fields_to_update) expect_hdrs = { 'x-image-meta-name': 'bar', 'x-image-meta-container_format': 'bare', 'x-image-meta-disk_format': 'qcow2', 'x-glance-registry-purge-props': 'false', } expect = [('PUT', '/v1/images/v2_created_img', expect_hdrs, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('v2_created_img', image.id) self.assertEqual('bar', image.name) self.assertEqual(0, image.size) self.assertEqual('bare', image.container_format) self.assertEqual('qcow2', image.disk_format) class ImageTest(testtools.TestCase): def setUp(self): super(ImageTest, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = images.ImageManager(self.api) def test_delete(self): image = self.mgr.get('1') image.delete() expect = [ ('HEAD', '/v1/images/1', {}, None), ('HEAD', '/v1/images/1', {}, None), ('DELETE', '/v1/images/1', {}, None), ] self.assertEqual(expect, self.api.calls) def test_update(self): image = self.mgr.get('1') image.update(name='image-5') expect = [ ('HEAD', '/v1/images/1', {}, None), ('HEAD', '/v1/images/1', {}, None), ('PUT', '/v1/images/1', {'x-image-meta-name': 'image-5', 'x-glance-registry-purge-props': 'false'}, None), ] self.assertEqual(expect, self.api.calls) def test_data(self): image = self.mgr.get('1') data = ''.join([b for b in image.data()]) expect = [ ('HEAD', '/v1/images/1', {}, None), ('HEAD', '/v1/images/1', {}, None), ('GET', '/v1/images/1', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) data = ''.join([b for b in image.data(do_checksum=False)]) expect += [('GET', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) def test_data_with_wrong_checksum(self): image = self.mgr.get('2') data = ''.join([b for b in image.data(do_checksum=False)]) expect = [ ('HEAD', '/v1/images/2', {}, None), ('HEAD', '/v1/images/2', {}, None), ('GET', '/v1/images/2', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual('YYY', data) data = image.data() expect += [('GET', '/v1/images/2', {}, None)] self.assertEqual(expect, self.api.calls) try: data = ''.join([b for b in image.data()]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong' self.assertIn(msg, str(e)) def test_data_with_checksum(self): image = self.mgr.get('3') data = ''.join([b for b in image.data(do_checksum=False)]) expect = [ ('HEAD', '/v1/images/3', {}, None), ('HEAD', '/v1/images/3', {}, None), ('GET', '/v1/images/3', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) data = ''.join([b for b in image.data()]) expect += [('GET', '/v1/images/3', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) class ParameterFakeAPI(utils.FakeAPI): image_list = {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]} def get(self, url, **kwargs): self.url = url return utils.FakeResponse({}), ParameterFakeAPI.image_list class FakeArg(object): def __init__(self, arg_dict): self.arg_dict = arg_dict self.fields = arg_dict.keys() def __getattr__(self, name): if name in self.arg_dict: return self.arg_dict[name] else: return None class UrlParameterTest(testtools.TestCase): def setUp(self): super(UrlParameterTest, self).setUp() self.api = ParameterFakeAPI({}) self.gc = client.Client("http://fakeaddress.com") self.gc.images = images.ImageManager(self.api) def test_is_public_list(self): shell.do_image_list(self.gc, FakeArg({"is_public": "True"})) parts = parse.urlparse(self.api.url) qs_dict = parse.parse_qs(parts.query) self.assertIn('is_public', qs_dict) self.assertTrue(qs_dict['is_public'][0].lower() == "true") python-glanceclient-2.9.1/glanceclient/tests/unit/test_client.py0000666000175100017510000000535513232161651025170 0ustar zuulzuul00000000000000# Copyright 2014 Red Hat, 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 testtools from glanceclient import client from glanceclient import v1 from glanceclient import v2 class ClientTest(testtools.TestCase): def test_no_endpoint_error(self): self.assertRaises(ValueError, client.Client, None) def test_endpoint(self): gc = client.Client(1, "http://example.com") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v1.client.Client) def test_versioned_endpoint(self): gc = client.Client(1, "http://example.com/v2") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v1.client.Client) def test_versioned_endpoint_no_version(self): gc = client.Client(endpoint="http://example.com/v2") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_versioned_endpoint_with_minor_revision(self): gc = client.Client(2.2, "http://example.com/v2.1") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_endpoint_with_version_hostname(self): gc = client.Client(2, "http://v1.example.com") self.assertEqual("http://v1.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_versioned_endpoint_with_version_hostname_v2(self): gc = client.Client(endpoint="http://v1.example.com/v2") self.assertEqual("http://v1.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_versioned_endpoint_with_version_hostname_v1(self): gc = client.Client(endpoint="http://v2.example.com/v1") self.assertEqual("http://v2.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v1.client.Client) def test_versioned_endpoint_with_minor_revision_and_version_hostname(self): gc = client.Client(endpoint="http://v1.example.com/v2.1") self.assertEqual("http://v1.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) python-glanceclient-2.9.1/glanceclient/tests/functional/0000775000175100017510000000000013232162126023450 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/functional/test_readonly_glance.py0000666000175100017510000001144113232161651030214 0ustar zuulzuul00000000000000# 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 re from tempest.lib import exceptions from glanceclient.tests.functional import base class SimpleReadOnlyGlanceClientTest(base.ClientTestBase): """Read only functional python-glanceclient tests. This only exercises client commands that are read only. """ def test_list_v1(self): out = self.glance('--os-image-api-version 1 image-list') endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, [ 'ID', 'Name', 'Disk Format', 'Container Format', 'Size', 'Status']) def test_list_v2(self): out = self.glance('--os-image-api-version 2 image-list') endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['ID', 'Name']) def test_fake_action(self): self.assertRaises(exceptions.CommandFailed, self.glance, 'this-does-not-exist') def test_member_list_v1(self): tenant_name = '--tenant-id %s' % self.creds['project_name'] out = self.glance('--os-image-api-version 1 member-list', params=tenant_name) endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['Image ID', 'Member ID', 'Can Share']) def test_member_list_v2(self): try: # NOTE(flwang): If set disk-format and container-format, Jenkins # will raise an error said can't recognize the params, though it # works fine at local. Without the two params, Glance will # complain. So we just catch the exception can skip it. self.glance('--os-image-api-version 2 image-create --name temp') except Exception: pass out = self.glance('--os-image-api-version 2 image-list' ' --visibility private') image_list = self.parser.listing(out) # NOTE(flwang): Because the member-list command of v2 is using # image-id as required parameter, so we have to get a valid image id # based on current environment. If there is no valid image id, we will # pass in a fake one and expect a 404 error. if len(image_list) > 0: param_image_id = '--image-id %s' % image_list[0]['ID'] out = self.glance('--os-image-api-version 2 member-list', params=param_image_id) endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['Image ID', 'Member ID', 'Status']) else: param_image_id = '--image-id fake_image_id' self.assertRaises(exceptions.CommandFailed, self.glance, '--os-image-api-version 2 member-list', params=param_image_id) def test_help(self): help_text = self.glance('--os-image-api-version 2 help') lines = help_text.split('\n') self.assertFirstLineStartsWith(lines, 'usage: glance') commands = [] cmds_start = lines.index('Positional arguments:') cmds_end = lines.index('Optional arguments:') command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)') for line in lines[cmds_start:cmds_end]: match = command_pattern.match(line) if match: commands.append(match.group(1)) commands = set(commands) wanted_commands = {'bash-completion', 'help', 'image-create', 'image-deactivate', 'image-delete', 'image-download', 'image-list', 'image-reactivate', 'image-show', 'image-tag-delete', 'image-tag-update', 'image-update', 'image-upload', 'location-add', 'location-delete', 'location-update', 'member-create', 'member-delete', 'member-list', 'member-update', 'task-create', 'task-list', 'task-show'} self.assertFalse(wanted_commands - commands) def test_version(self): self.glance('', flags='--version') def test_debug_list(self): self.glance('--os-image-api-version 2 image-list', flags='--debug') python-glanceclient-2.9.1/glanceclient/tests/functional/test_http_headers.py0000666000175100017510000000521513232161651027542 0ustar zuulzuul00000000000000# 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 glanceclient.tests.functional import base import time IMAGE = {"protected": False, "disk_format": "qcow2", "name": "glance_functional_test_image.img", "visibility": "private", "container_format": "bare"} class HttpHeadersTest(base.ClientTestBase): def test_encode_headers_python(self): """Test proper handling of Content-Type headers. encode_headers() must be called as late as possible before a request is sent. If this principle is violated, and if any changes are made to the headers between encode_headers() and the actual request (for instance a call to _set_common_request_kwargs()), and if you're trying to set a Content-Type that is not equal to application/octet-stream (the default), it is entirely possible that you'll end up with two Content-Type headers defined (yours plus application/octet-stream). The request will go out the door with only one of them chosen seemingly at random. This test uses a call to update() because it sets a header such as the following (this example may be subject to change): Content-Type: application/openstack-images-v2.1-json-patch This situation only occurs in python3. This test will never fail in python2. There is no test against the CLI because it swallows the error. """ # the failure is intermittent - try up to 6 times for attempt in range(0, 6): glanceclient = self.glance_pyclient() image = glanceclient.find(IMAGE["name"]) if image: glanceclient.glance.images.delete(image.id) image = glanceclient.glance.images.create(name=IMAGE["name"]) self.assertTrue(image.status == "queued") try: image = glanceclient.glance.images.update(image.id, disk_format="qcow2") except Exception as e: self.assertFalse("415 Unsupported Media Type" in e.details) time.sleep(5) python-glanceclient-2.9.1/glanceclient/tests/functional/base.py0000666000175100017510000000672013232161651024745 0ustar zuulzuul00000000000000# 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 glanceclient from keystoneauth1 import loading from keystoneauth1 import session import os import os_client_config from tempest.lib.cli import base def credentials(cloud='devstack-admin'): """Retrieves credentials to run functional tests Credentials are either read via os-client-config from the environment or from a config file ('clouds.yaml'). Environment variables override those from the config file. devstack produces a clouds.yaml with two named clouds - one named 'devstack' which has user privs and one named 'devstack-admin' which has admin privs. This function will default to getting the devstack-admin cloud as that is the current expected behavior. """ return os_client_config.OpenStackConfig().get_one_cloud(cloud=cloud) class ClientTestBase(base.ClientTestBase): """This is a first pass at a simple read only python-glanceclient test. This only exercises client commands that are read only. This should test commands: * as a regular user * as an admin user * with and without optional parameters * initially just check return codes, and later test command outputs """ def _get_clients(self): self.creds = credentials().get_auth_args() cli_dir = os.environ.get( 'OS_GLANCECLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin')) return base.CLIClient( username=self.creds['username'], password=self.creds['password'], tenant_name=self.creds['project_name'], user_domain_id=self.creds['user_domain_id'], project_domain_id=self.creds['project_domain_id'], uri=self.creds['auth_url'], cli_dir=cli_dir) def glance(self, *args, **kwargs): return self.clients.glance(*args, **kwargs) def glance_pyclient(self): ks_creds = dict( auth_url=self.creds["auth_url"], username=self.creds["username"], password=self.creds["password"], project_name=self.creds["project_name"], user_domain_id=self.creds["user_domain_id"], project_domain_id=self.creds["project_domain_id"]) keystoneclient = self.Keystone(**ks_creds) return self.Glance(keystoneclient) class Keystone(object): def __init__(self, **kwargs): loader = loading.get_plugin_loader("password") auth = loader.load_from_options(**kwargs) self.session = session.Session(auth=auth) class Glance(object): def __init__(self, keystone, version="2"): self.glance = glanceclient.Client( version, session=keystone.session) def find(self, image_name): for image in self.glance.images.list(): if image.name == image_name: return image return None python-glanceclient-2.9.1/glanceclient/tests/functional/__init__.py0000666000175100017510000000000013232161651025553 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/functional/README.rst0000666000175100017510000000344013232161651025144 0ustar zuulzuul00000000000000====================================== python-glanceclient functional testing ====================================== Idea ---- Run real client/server requests in the gate to catch issues which are difficult to catch with a purely unit test approach. Many projects (nova, keystone...) already have this form of testing in the gate. Testing Theory -------------- Since python-glanceclient has two uses, CLI and python API, we should have two sets of functional tests. CLI and python API. The python API tests should never use the CLI. But the CLI tests can use the python API where adding native support to the CLI for the required functionality would involve a non trivial amount of work. Functional Test Guidelines -------------------------- The functional tests require: 1) A working Glance/Keystone installation (for example, devstack) 2) A yaml file containing valid credentials If you are using devstack, a yaml file will have been created for you with the name /etc/openstack/clouds.yaml. The test code knows where to find it, so if you're using devstack, you don't need to do anything else. If you are not using devstack you should create a yaml file with the following format: clouds: devstack-admin: auth: auth_url: http://10.0.0.1:35357/v2.0 password: example project_domain_id: default project_name: admin user_domain_id: default username: admin identity_api_version: '2.0' region_name: RegionOne The tests will look for a file named 'clouds.yaml' in the following locations (in this order, first found wins): * current directory * ~/.config/openstack * /etc/openstack You may also set the environment variable OS_CLIENT_CONFIG_FILE to the absolute pathname of a file and that location will be inserted at the front of the search list. python-glanceclient-2.9.1/glanceclient/tests/__init__.py0000666000175100017510000000000013232161651023411 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/tests/utils.py0000666000175100017510000001544713232161652023040 0ustar zuulzuul00000000000000# 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 copy import json import six import six.moves.urllib.parse as urlparse import testtools from glanceclient.v2 import schemas class FakeAPI(object): def __init__(self, fixtures): self.fixtures = fixtures self.calls = [] def _request(self, method, url, headers=None, data=None, content_length=None): call = build_call_record(method, sort_url_by_query_keys(url), headers or {}, data) if content_length is not None: call = tuple(list(call) + [content_length]) self.calls.append(call) fixture = self.fixtures[sort_url_by_query_keys(url)][method] data = fixture[1] if isinstance(fixture[1], six.string_types): try: data = json.loads(fixture[1]) except ValueError: data = six.StringIO(fixture[1]) return FakeResponse(fixture[0], fixture[1]), data def get(self, *args, **kwargs): return self._request('GET', *args, **kwargs) def post(self, *args, **kwargs): return self._request('POST', *args, **kwargs) def put(self, *args, **kwargs): return self._request('PUT', *args, **kwargs) def patch(self, *args, **kwargs): return self._request('PATCH', *args, **kwargs) def delete(self, *args, **kwargs): return self._request('DELETE', *args, **kwargs) def head(self, *args, **kwargs): return self._request('HEAD', *args, **kwargs) class FakeSchemaAPI(FakeAPI): def get(self, *args, **kwargs): _, raw_schema = self._request('GET', *args, **kwargs) return schemas.Schema(raw_schema) class RawRequest(object): def __init__(self, headers, body=None, version=1.0, status=200, reason="Ok"): """A crafted request object used for testing. :param headers: dict representing HTTP response headers :param body: file-like object :param version: HTTP Version :param status: Response status code :param reason: Status code related message. """ self.body = body self.status = status self.reason = reason self.version = version self.headers = headers def getheaders(self): return copy.deepcopy(self.headers).items() def getheader(self, key, default): return self.headers.get(key, default) def read(self, amt): return self.body.read(amt) class FakeResponse(object): def __init__(self, headers=None, body=None, version=1.0, status_code=200, reason="Ok"): """A crafted response object used for testing. :param headers: dict representing HTTP response headers :param body: file-like object :param version: HTTP Version :param status: Response status code :param reason: Status code related message. """ self.body = body self.reason = reason self.version = version self.headers = headers self.headers['x-openstack-request-id'] = 'req-1234' self.status_code = status_code self.raw = RawRequest(headers, body=body, reason=reason, version=version, status=status_code) @property def status(self): return self.status_code @property def ok(self): return (self.status_code < 400 or self.status_code >= 600) def read(self, amt): return self.body.read(amt) def close(self): pass @property def content(self): if hasattr(self.body, "read"): return self.body.read() return self.body @property def text(self): if isinstance(self.content, six.binary_type): return self.content.decode('utf-8') return self.content def json(self, **kwargs): return self.body and json.loads(self.text) or "" def iter_content(self, chunk_size=1, decode_unicode=False): while True: chunk = self.raw.read(chunk_size) if not chunk: break yield chunk def release_conn(self, **kwargs): pass class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { 'config': {'danger_mode': False}, 'verify': True} class FakeTTYStdout(six.StringIO): """A Fake stdout that try to emulate a TTY device as much as possible.""" def isatty(self): return True def write(self, data): # When a CR (carriage return) is found reset file. if data.startswith('\r'): self.seek(0) data = data[1:] return six.StringIO.write(self, data) class FakeNoTTYStdout(FakeTTYStdout): """A Fake stdout that is not a TTY device.""" def isatty(self): return False def sort_url_by_query_keys(url): """A helper function which sorts the keys of the query string of a url. For example, an input of '/v2/tasks?sort_key=id&sort_dir=asc&limit=10' returns '/v2/tasks?limit=10&sort_dir=asc&sort_key=id'. This is to prevent non-deterministic ordering of the query string causing problems with unit tests. :param url: url which will be ordered by query keys :returns url: url with ordered query keys """ parsed = urlparse.urlparse(url) queries = urlparse.parse_qsl(parsed.query, True) sorted_query = sorted(queries, key=lambda x: x[0]) encoded_sorted_query = urlparse.urlencode(sorted_query, True) url_parts = (parsed.scheme, parsed.netloc, parsed.path, parsed.params, encoded_sorted_query, parsed.fragment) return urlparse.urlunparse(url_parts) def build_call_record(method, url, headers, data): """Key the request body be ordered if it's a dict type.""" if isinstance(data, dict): data = sorted(data.items()) if isinstance(data, six.string_types): # NOTE(flwang): For image update, the data will be a 'list' which # contains operation dict, such as: [{"op": "remove", "path": "/a"}] try: data = json.loads(data) except ValueError: return (method, url, headers or {}, data) data = [sorted(d.items()) for d in data] return (method, url, headers or {}, data) python-glanceclient-2.9.1/glanceclient/v1/0000775000175100017510000000000013232162126020472 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/v1/image_members.py0000666000175100017510000000677213232161652023661 0ustar zuulzuul00000000000000# 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. from glanceclient.v1.apiclient import base class ImageMember(base.Resource): def __repr__(self): return "" % self._info @property def id(self): return self.member_id def delete(self): self.manager.delete(self) class ImageMemberManager(base.ManagerWithFind): resource_class = ImageMember def get(self, image, member_id): image_id = base.getid(image) url = '/v1/images/%s/members/%s' % (image_id, member_id) resp, body = self.client.get(url) member = body['member'] member['image_id'] = image_id return ImageMember(self, member, loaded=True) def list(self, image=None, member=None): out = [] if image and member: try: out.append(self.get(image, member)) # TODO(bcwaldon): narrow this down to 404 except Exception: pass elif image: out.extend(self._list_by_image(image)) elif member: out.extend(self._list_by_member(member)) else: # TODO(bcwaldon): figure out what is appropriate to do here as we # are unable to provide the requested response pass return out def _list_by_image(self, image): image_id = base.getid(image) url = '/v1/images/%s/members' % image_id resp, body = self.client.get(url) out = [] for member in body['members']: member['image_id'] = image_id out.append(ImageMember(self, member, loaded=True)) return out def _list_by_member(self, member): member_id = base.getid(member) url = '/v1/shared-images/%s' % member_id resp, body = self.client.get(url) out = [] for member in body['shared_images']: member['member_id'] = member_id out.append(ImageMember(self, member, loaded=True)) return out def delete(self, image_id, member_id): self._delete("/v1/images/%s/members/%s" % (image_id, member_id)) def create(self, image, member_id, can_share=False): """Creates an image.""" url = '/v1/images/%s/members/%s' % (base.getid(image), member_id) body = {'member': {'can_share': can_share}} self.client.put(url, data=body) def replace(self, image, members): memberships = [] for member in members: try: obj = { 'member_id': member.member_id, 'can_share': member.can_share, } except AttributeError: obj = {'member_id': member['member_id']} if 'can_share' in member: obj['can_share'] = member['can_share'] memberships.append(obj) url = '/v1/images/%s/members' % base.getid(image) self.client.put(url, data={'memberships': memberships}) python-glanceclient-2.9.1/glanceclient/v1/versions.py0000666000175100017510000000162713232161652022727 0ustar zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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 glanceclient.v1.apiclient import base class VersionManager(base.ManagerWithFind): def list(self): """List all versions.""" url = '/versions' resp, body = self.client.get(url) return body.get('versions', None) python-glanceclient-2.9.1/glanceclient/v1/__init__.py0000666000175100017510000000126213232161652022611 0ustar zuulzuul00000000000000# 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. from glanceclient.v1.client import Client # noqa python-glanceclient-2.9.1/glanceclient/v1/apiclient/0000775000175100017510000000000013232162126022442 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/v1/apiclient/base.py0000666000175100017510000004163513232161652023744 0ustar zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2012 Grid Dynamics # Copyright 2013 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. """ Base utilities to build API operation managers and objects on top of. """ ######################################################################## # # THIS MODULE IS DEPRECATED # # Please refer to # https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for # the discussion leading to this deprecation. # # We recommend checking out the python-openstacksdk project # (https://launchpad.net/python-openstacksdk) instead. # ######################################################################## # E1102: %s is not callable # pylint: disable=E1102 import abc import copy from oslo_utils import strutils import six from six.moves.urllib import parse from glanceclient._i18n import _ from glanceclient.v1.apiclient import exceptions def getid(obj): """Return id if argument is a Resource. Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ try: if obj.uuid: return obj.uuid except AttributeError: pass try: return obj.id except AttributeError: return obj # TODO(aababilov): call run_hooks() in HookableMixin's child classes class HookableMixin(object): """Mixin so classes can register and run hooks.""" _hooks_map = {} @classmethod def add_hook(cls, hook_type, hook_func): """Add a new hook of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param hook_func: hook function """ if hook_type not in cls._hooks_map: cls._hooks_map[hook_type] = [] cls._hooks_map[hook_type].append(hook_func) @classmethod def run_hooks(cls, hook_type, *args, **kwargs): """Run all hooks of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param args: args to be passed to every hook function :param kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: hook_func(*args, **kwargs) class BaseManager(HookableMixin): """Basic manager type providing common operations. Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, client): """Initializes BaseManager with `client`. :param client: instance of BaseClient descendant for HTTP requests """ super(BaseManager, self).__init__() self.client = client def _list(self, url, response_key=None, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'servers'. If response_key is None - all response body will be used. :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) """ if json: body = self.client.post(url, json=json).json() else: body = self.client.get(url).json() if obj_class is None: obj_class = self.resource_class data = body[response_key] if response_key is not None else body # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: data = data['values'] except (KeyError, TypeError): pass return [obj_class(self, res, loaded=True) for res in data if res] def _get(self, url, response_key=None): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'server'. If response_key is None - all response body will be used. """ body = self.client.get(url).json() data = body[response_key] if response_key is not None else body return self.resource_class(self, data, loaded=True) def _head(self, url): """Retrieve request headers for an object. :param url: a partial URL, e.g., '/servers' """ resp = self.client.head(url) return resp.status_code == 204 def _post(self, url, json, response_key=None, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'server'. If response_key is None - all response body will be used. :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() data = body[response_key] if response_key is not None else body if return_raw: return data return self.resource_class(self, data) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers'. If response_key is None - all response body will be used. """ resp = self.client.put(url, json=json) # PUT requests may not return a body if resp.content: body = resp.json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _patch(self, url, json=None, response_key=None): """Update an object with PATCH method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers'. If response_key is None - all response body will be used. """ body = self.client.patch(url, json=json).json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _delete(self, url): """Delete an object. :param url: a partial URL, e.g., '/servers/my-server' """ return self.client.delete(url) @six.add_metaclass(abc.ABCMeta) class ManagerWithFind(BaseManager): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): pass def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() else: return matches[0] def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ found = [] searches = kwargs.items() for obj in self.list(): try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found class CrudManager(BaseManager): """Base manager class for manipulating entities. Children of this class are expected to define a `collection_key` and `key`. - `collection_key`: Usually a plural noun by convention (e.g. `entities`); used to refer collections in both URL's (e.g. `/v3/entities`) and JSON objects containing a list of member resources (e.g. `{'entities': [{}, {}, {}]}`). - `key`: Usually a singular noun by convention (e.g. `entity`); used to refer to an individual member of the collection. """ collection_key = None key = None def build_url(self, base_url=None, **kwargs): """Builds a resource URL for the given kwargs. Given an example collection where `collection_key = 'entities'` and `key = 'entity'`, the following URL's could be generated. By default, the URL will represent a collection of entities, e.g.:: /entities If kwargs contains an `entity_id`, then the URL will represent a specific member, e.g.:: /entities/{entity_id} :param base_url: if provided, the generated URL will be appended to it """ url = base_url if base_url is not None else '' url += '/%s' % self.collection_key # do we have a specific entity? entity_id = kwargs.get('%s_id' % self.key) if entity_id is not None: url += '/%s' % entity_id return url def _filter_kwargs(self, kwargs): """Drop null values and handle ids.""" for key, ref in kwargs.copy().items(): if ref is None: kwargs.pop(key) else: if isinstance(ref, Resource): kwargs.pop(key) kwargs['%s_id' % key] = getid(ref) return kwargs def create(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._post( self.build_url(**kwargs), {self.key: kwargs}, self.key) def get(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._get( self.build_url(**kwargs), self.key) def head(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._head(self.build_url(**kwargs)) def list(self, base_url=None, **kwargs): """List the collection. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', }, self.collection_key) def put(self, base_url=None, **kwargs): """Update an element. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._put(self.build_url(base_url=base_url, **kwargs)) def update(self, **kwargs): kwargs = self._filter_kwargs(kwargs) params = kwargs.copy() params.pop('%s_id' % self.key) return self._patch( self.build_url(**kwargs), {self.key: params}, self.key) def delete(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._delete( self.build_url(**kwargs)) def find(self, base_url=None, **kwargs): """Find a single item with attributes matching ``**kwargs``. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', }, self.collection_key) num = len(rl) if num == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(msg) elif num > 1: raise exceptions.NoUniqueMatch else: return rl[0] class Extension(HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') manager_class = None def __init__(self, name, module): super(Extension, self).__init__() self.name = name self.module = module self._parse_extension_module() def _parse_extension_module(self): self.manager_class = None for attr_name, attr_value in self.module.__dict__.items(): if attr_name in self.SUPPORTED_HOOKS: self.add_hook(attr_name, attr_value) else: try: if issubclass(attr_value, BaseManager): self.manager_class = attr_value except TypeError: pass def __repr__(self): return "" % self.name class Resource(object): """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. """ HUMAN_ID = False NAME_ATTR = 'name' def __init__(self, manager, info, loaded=False): """Populate and bind to a manager. :param manager: BaseManager object :param info: dictionary representing resource attributes :param loaded: prevent lazy-loading if set to True """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) @property def human_id(self): """Human-readable ID which can be used for bash completion.""" if self.HUMAN_ID: name = getattr(self, self.NAME_ATTR, None) if name is not None: return strutils.to_slug(name) return None def _add_details(self, info): for (k, v) in info.items(): try: setattr(self, k, v) self._info[k] = v except AttributeError: # In this case we already defined the attribute on the class pass def __getattr__(self, k): if k not in self.__dict__: # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: return self.__dict__[k] def get(self): """Support for lazy loading details. Some clients, such as novaclient have the option to lazy load the details, details which can be loaded with this function. """ # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): return new = self.manager.get(self.id) if new: self._add_details(new._info) self._add_details( {'x_request_id': self.manager.client.last_request_id}) def __eq__(self, other): if not isinstance(other, Resource): return NotImplemented # two resources of different types are not equal if not isinstance(other, self.__class__): return False return self._info == other._info def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val def to_dict(self): return copy.deepcopy(self._info) python-glanceclient-2.9.1/glanceclient/v1/apiclient/exceptions.py0000666000175100017510000003125113232161652025204 0ustar zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 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. """ Exception definitions. """ ######################################################################## # # THIS MODULE IS DEPRECATED # # Please refer to # https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for # the discussion leading to this deprecation. # # We recommend checking out the python-openstacksdk project # (https://launchpad.net/python-openstacksdk) instead. # ######################################################################## import inspect import sys import six from glanceclient._i18n import _ class ClientException(Exception): """The base exception class for all exceptions this library raises.""" pass class ValidationError(ClientException): """Error in validation on API client side.""" pass class UnsupportedVersion(ClientException): """User is trying to use an unsupported version of the API.""" pass class CommandError(ClientException): """Error in CLI tool.""" pass class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass class ConnectionError(ClientException): """Cannot connect to API service.""" pass class ConnectionRefused(ConnectionError): """Connection refused while trying to connect to API service.""" pass class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( _("Authentication failed. Missing options: %s") % ", ".join(opt_names)) self.opt_names = opt_names class AuthSystemNotFound(AuthorizationFailure): """User has specified an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( _("AuthSystemNotFound: %r") % auth_system) self.auth_system = auth_system class NoUniqueMatch(ClientException): """Multiple entities found instead of one.""" pass class EndpointException(ClientException): """Something is rotten in Service Catalog.""" pass class EndpointNotFound(EndpointException): """Could not find requested endpoint in Service Catalog.""" pass class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( _("AmbiguousEndpoints: %r") % endpoints) self.endpoints = endpoints class HttpError(ClientException): """The base exception class for all HTTP exceptions.""" http_status = 0 message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string) class HTTPRedirection(HttpError): """HTTP Redirection.""" message = _("HTTP Redirection") class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = _("HTTP Client Error") class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = _("HTTP Server Error") class MultipleChoices(HTTPRedirection): """HTTP 300 - Multiple Choices. Indicates multiple options for the resource that the client may follow. """ http_status = 300 message = _("Multiple Choices") class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = _("Bad Request") class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = _("Unauthorized") class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = _("Payment Required") class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = _("Forbidden") class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = _("Not Found") class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = _("Not Acceptable") class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = _("Proxy Authentication Required") class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = _("Request Timeout") class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = _("Conflict") class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = _("Gone") class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = _("Length Required") class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = _("Request Entity Too Large") def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RequestEntityTooLarge, self).__init__(*args, **kwargs) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = _("Request-URI Too Long") class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = _("Requested Range Not Satisfiable") class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = _("Expectation Failed") class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = _("Unprocessable Entity") class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = _("Internal Server Error") # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = _("Not Implemented") class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = _("Service Unavailable") class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = _("HTTP Version Not Supported") # _code_map contains all the classes that have http_status attribute. _code_map = dict( (getattr(obj, 'http_status', None), obj) for name, obj in vars(sys.modules[__name__]).items() if inspect.isclass(obj) and getattr(obj, 'http_status', False) ) def from_response(response, method, url): """Returns an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ req_id = response.headers.get("x-openstack-request-id") # NOTE(hdd) true for older versions of nova and cinder if not req_id: req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if isinstance(body, dict): error = body.get(list(body)[0]) if isinstance(error, dict): kwargs["message"] = (error.get("message") or error.get("faultstring")) kwargs["details"] = (error.get("details") or six.text_type(body)) elif content_type.startswith("text/"): kwargs["details"] = getattr(response, 'text', '') try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls(**kwargs) python-glanceclient-2.9.1/glanceclient/v1/apiclient/__init__.py0000666000175100017510000000000013232161652024546 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/glanceclient/v1/apiclient/utils.py0000666000175100017510000000640313232161652024164 0ustar zuulzuul00000000000000# # 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. ######################################################################## # # THIS MODULE IS DEPRECATED # # Please refer to # https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for # the discussion leading to this deprecation. # # We recommend checking out the python-openstacksdk project # (https://launchpad.net/python-openstacksdk) instead. # ######################################################################## from oslo_utils import encodeutils from oslo_utils import uuidutils import six from glanceclient._i18n import _ from glanceclient.v1.apiclient import exceptions def find_resource(manager, name_or_id, **find_args): """Look for resource in a given manager. Used as a helper for the _find_* methods. Example: .. code-block:: python def _find_hypervisor(cs, hypervisor): #Get a hypervisor by name or ID. return cliutils.find_resource(cs.hypervisors, hypervisor) """ # first try to get entity as integer id try: return manager.get(int(name_or_id)) except (TypeError, ValueError, exceptions.NotFound): pass # now try to get entity as uuid try: if six.PY2: tmp_id = encodeutils.safe_encode(name_or_id) else: tmp_id = encodeutils.safe_decode(name_or_id) if uuidutils.is_uuid_like(tmp_id): return manager.get(tmp_id) except (TypeError, ValueError, exceptions.NotFound): pass # for str id which is not uuid if getattr(manager, 'is_alphanum_id_allowed', False): try: return manager.get(name_or_id) except exceptions.NotFound: pass try: try: return manager.find(human_id=name_or_id, **find_args) except exceptions.NotFound: pass # finally try to find entity by name try: resource = getattr(manager, 'resource_class', None) name_attr = resource.NAME_ATTR if resource else 'name' kwargs = {name_attr: name_or_id} kwargs.update(find_args) return manager.find(**kwargs) except exceptions.NotFound: msg = _("No %(name)s with a name or " "ID of '%(name_or_id)s' exists.") % { "name": manager.resource_class.__name__.lower(), "name_or_id": name_or_id } raise exceptions.CommandError(msg) except exceptions.NoUniqueMatch: msg = _("Multiple %(name)s matches found for " "'%(name_or_id)s', use an ID to be more specific.") % { "name": manager.resource_class.__name__.lower(), "name_or_id": name_or_id } raise exceptions.CommandError(msg) python-glanceclient-2.9.1/glanceclient/v1/shell.py0000666000175100017510000004473313232161652022173 0ustar zuulzuul00000000000000# 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. from __future__ import print_function import copy import functools import os import sys from oslo_utils import encodeutils from oslo_utils import strutils from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient import exc import glanceclient.v1.images CONTAINER_FORMATS = ('Acceptable formats: ami, ari, aki, bare, ovf, ova,' 'docker.') DISK_FORMATS = ('Acceptable formats: ami, ari, aki, vhd, vdhx, vmdk, raw, ' 'qcow2, vdi, iso, and ploop.') DATA_FIELDS = ('location', 'copy_from', 'file') _bool_strict = functools.partial(strutils.bool_from_string, strict=True) @utils.arg('--name', metavar='', help='Filter images to those that have this name.') @utils.arg('--status', metavar='', help='Filter images to those that have this status.') @utils.arg('--changes-since', metavar='', help='Filter images to those that changed since the given time' ', which will include the deleted images.') @utils.arg('--container-format', metavar='', help='Filter images to those that have this container format. ' + CONTAINER_FORMATS) @utils.arg('--disk-format', metavar='', help='Filter images to those that have this disk format. ' + DISK_FORMATS) @utils.arg('--size-min', metavar='', type=int, help='Filter images to those with a size greater than this.') @utils.arg('--size-max', metavar='', type=int, help='Filter images to those with a size less than this.') @utils.arg('--property-filter', metavar='', help="Filter images by a user-defined image property.", action='append', dest='properties', default=[]) @utils.arg('--page-size', metavar='', default=None, type=int, help='Number of images to request in each paginated request.') @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--sort-key', default='name', choices=glanceclient.v1.images.SORT_KEY_VALUES, help='Sort image list by specified field.') @utils.arg('--sort-dir', default='asc', choices=glanceclient.v1.images.SORT_DIR_VALUES, help='Sort image list in specified direction.') @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help=('Allows the user to select a listing of public or non ' 'public images.')) @utils.arg('--owner', default=None, metavar='', help='Display only images owned by this tenant id. Filtering ' 'occurs on the client side so may be inefficient. This option ' 'is mainly intended for admin use. Use an empty string (\'\') ' 'to list images with no owner. Note: This option overrides ' 'the --is-public argument if present. Note: the v2 API ' 'supports more efficient server-side owner based filtering.') @utils.arg('--all-tenants', action='store_true', default=False, help=('Allows the admin user to list all images ' 'irrespective of the image\'s owner or is_public value.')) def do_image_list(gc, args): """List images you can access.""" filter_keys = ['name', 'status', 'container_format', 'disk_format', 'size_min', 'size_max', 'is_public', 'changes_since'] filter_items = [(key, getattr(args, key)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) if 'changes_since' in filters: filters['changes-since'] = filters.pop('changes_since') if args.properties: property_filter_items = [p.split('=', 1) for p in args.properties] if any(len(pair) != 2 for pair in property_filter_items): utils.exit('Argument --property-filter requires properties in the' ' format KEY=VALUE') filters['properties'] = dict(property_filter_items) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size kwargs['sort_key'] = args.sort_key kwargs['sort_dir'] = args.sort_dir kwargs['owner'] = args.owner if args.all_tenants is True: kwargs['is_public'] = None images = gc.images.list(**kwargs) if args.human_readable: def convert_size(image): image.size = utils.make_size_human_readable(image.size) return image images = (convert_size(image) for image in images) columns = ['ID', 'Name', 'Disk Format', 'Container Format', 'Size', 'Status'] utils.print_list(images, columns) def _image_show(image, human_readable=False, max_column_width=80): # Flatten image properties dict for display info = copy.deepcopy(image._info) if human_readable: info['size'] = utils.make_size_human_readable(info['size']) for (k, v) in info.pop('properties').items(): info['Property \'%s\'' % k] = v utils.print_dict(info, max_column_width=max_column_width) def _set_data_field(fields, args): if 'location' not in fields and 'copy_from' not in fields: fields['data'] = utils.get_data_file(args) @utils.arg('image', metavar='', help='Name or ID of image to describe.') @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--max-column-width', metavar='', default=80, help='The max column width of the printed table.') def do_image_show(gc, args): """Describe a specific image.""" image_id = utils.find_resource(gc.images, args.image).id image = gc.images.get(image_id) _image_show(image, args.human_readable, max_column_width=int(args.max_column_width)) @utils.arg('--file', metavar='', help='Local file to save downloaded image data to. ' 'If this is not specified and there is no redirection ' 'the image data will not be saved.') @utils.arg('image', metavar='', help='Name or ID of image to download.') @utils.arg('--progress', action='store_true', default=False, help='Show download progress bar.') def do_image_download(gc, args): """Download a specific image.""" image = utils.find_resource(gc.images, args.image) body = image.data() if args.progress: body = progressbar.VerboseIteratorWrapper(body, len(body)) if not (sys.stdout.isatty() and args.file is None): utils.save_image(body, args.file) else: print('No redirection or local file specified for downloaded image ' 'data. Please specify a local file with --file to save ' 'downloaded image or redirect output to another source.') @utils.arg('--id', metavar='', help='ID of image to reserve.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--store', metavar='', help='Store to upload image to.') @utils.arg('--disk-format', metavar='', help='Disk format of image. ' + DISK_FORMATS) @utils.arg('--container-format', metavar='', help='Container format of image. ' + CONTAINER_FORMATS) @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', type=int, help=('Size of image data (in bytes). Only used with' ' \'--location\' and \'--copy_from\'.')) @utils.arg('--min-disk', metavar='', type=int, help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min-ram', metavar='', type=int, help='Minimum amount of ram needed to boot image (in megabytes).') @utils.arg('--location', metavar='', help=('URL where the data for this image already resides. For ' 'example, if the image data is stored in swift, you could ' 'specify \'swift+http://tenant%%3Aaccount:key@auth_url/' 'v2.0/container/obj\'. ' '(Note: \'%%3A\' is \':\' URL encoded.)')) @utils.arg('--file', metavar='', help=('Local file that contains disk image to be uploaded during' ' creation. Alternatively, images can be passed to the client' ' via stdin.')) @utils.arg('--checksum', metavar='', help=('Hash of image data used Glance can use for verification.' ' Provide a md5 checksum here.')) @utils.arg('--copy-from', metavar='', help=('Similar to \'--location\' in usage, but this indicates that' ' the Glance server should immediately copy the data and' ' store it in its configured image store.')) @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help='Make image accessible to the public.') @utils.arg('--is-protected', type=_bool_strict, metavar='{True,False}', help='Prevent image from being deleted.') @utils.arg('--property', metavar="", action='append', default=[], help=("Arbitrary property to associate with image. " "May be used multiple times.")) @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--progress', action='store_true', default=False, help='Show upload progress bar.') @utils.on_data_require_fields(DATA_FIELDS) def do_image_create(gc, args): """Create a new image.""" # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) fields['is_public'] = fields.get('is_public') if 'is_protected' in fields: fields['protected'] = fields.pop('is_protected') raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: key, value = datum.split('=', 1) fields['properties'][key] = value # Filter out values we can't use CREATE_PARAMS = glanceclient.v1.images.CREATE_PARAMS fields = dict(filter(lambda x: x[0] in CREATE_PARAMS, fields.items())) _set_data_field(fields, args) # Only show progress bar for local image files if fields.get('data') and args.progress: filesize = utils.get_file_size(fields['data']) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) fields['data'] = progressbar.VerboseFileWrapper( fields['data'], filesize ) image = gc.images.create(**fields) _image_show(image, args.human_readable) def _is_image_data_provided(args): """Return True if some image data has probably been provided by the user""" # NOTE(kragniz): Check stdin works, then check is there is any data # on stdin or a filename has been provided with --file try: os.fstat(0) except OSError: return False return not sys.stdin.isatty() or args.file or args.copy_from @utils.arg('image', metavar='', help='Name or ID of image to modify.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--disk-format', metavar='', help='Disk format of image. ' + DISK_FORMATS) @utils.arg('--container-format', metavar='', help='Container format of image. ' + CONTAINER_FORMATS) @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', type=int, help='Size of image data (in bytes).') @utils.arg('--min-disk', metavar='', type=int, help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min-ram', metavar='', type=int, help='Minimum amount of ram needed to boot image (in megabytes).') @utils.arg('--location', metavar='', help=('URL where the data for this image already resides. For ' 'example, if the image data is stored in swift, you could ' 'specify \'swift+http://tenant%%3Aaccount:key@auth_url/' 'v2.0/container/obj\'. ' '(Note: \'%%3A\' is \':\' URL encoded.) ' 'This option only works for images in \'queued\' status.')) @utils.arg('--file', metavar='', help=('Local file that contains disk image to be uploaded during' ' update. Alternatively, images can be passed to the client' ' via stdin.')) @utils.arg('--checksum', metavar='', help='Hash of image data used Glance can use for verification.') @utils.arg('--copy-from', metavar='', help=('Similar to \'--location\' in usage, but this indicates that' ' the Glance server should immediately copy the data and' ' store it in its configured image store.' ' This option only works for images in \'queued\' status.')) @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help='Make image accessible to the public.') @utils.arg('--is-protected', type=_bool_strict, metavar='{True,False}', help='Prevent image from being deleted.') @utils.arg('--property', metavar="", action='append', default=[], help=("Arbitrary property to associate with image. " "May be used multiple times.")) @utils.arg('--purge-props', action='store_true', default=False, help=("If this flag is present, delete all image properties " "not explicitly set in the update request. Otherwise, " "those properties not referenced are preserved.")) @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--progress', action='store_true', default=False, help='Show upload progress bar.') def do_image_update(gc, args): """Update a specific image.""" # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) image_arg = fields.pop('image') image = utils.find_resource(gc.images, image_arg) if 'is_protected' in fields: fields['protected'] = fields.pop('is_protected') raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: key, value = datum.split('=', 1) fields['properties'][key] = value # Filter out values we can't use UPDATE_PARAMS = glanceclient.v1.images.UPDATE_PARAMS fields = dict(filter(lambda x: x[0] in UPDATE_PARAMS, fields.items())) if image.status == 'queued': _set_data_field(fields, args) if args.progress: filesize = utils.get_file_size(fields['data']) fields['data'] = progressbar.VerboseFileWrapper( fields['data'], filesize ) elif _is_image_data_provided(args): # NOTE(kragniz): Exit with an error if the status is not queued # and image data was provided utils.exit('Unable to upload image data to an image which ' 'is %s.' % image.status) image = gc.images.update(image, purge_props=args.purge_props, **fields) _image_show(image, args.human_readable) @utils.arg('images', metavar='', nargs='+', help='Name or ID of image(s) to delete.') def do_image_delete(gc, args): """Delete specified image(s).""" for args_image in args.images: image = utils.find_resource(gc.images, args_image) if image and image.status == "deleted": msg = "No image with an ID of '%s' exists." % image.id raise exc.CommandError(msg) try: if args.verbose: print('Requesting image delete for %s ...' % encodeutils.safe_decode(args_image), end=' ') gc.images.delete(image) if args.verbose: print('[Done]') except exc.HTTPException as e: if args.verbose: print('[Fail]') print('%s: Unable to delete image %s' % (e, args_image)) @utils.arg('--image-id', metavar='', help='Filter results by an image ID.') @utils.arg('--tenant-id', metavar='', help='Filter results by a tenant ID.') def do_member_list(gc, args): """Describe sharing permissions by image or tenant.""" if args.image_id and args.tenant_id: utils.exit('Unable to filter members by both --image-id and' ' --tenant-id.') elif args.image_id: kwargs = {'image': args.image_id} elif args.tenant_id: kwargs = {'member': args.tenant_id} else: utils.exit('Unable to list all members. Specify --image-id or' ' --tenant-id') members = gc.image_members.list(**kwargs) columns = ['Image ID', 'Member ID', 'Can Share'] utils.print_list(members, columns) @utils.arg('image', metavar='', help='Image to add member to.') @utils.arg('tenant_id', metavar='', help='Tenant to add as member.') @utils.arg('--can-share', action='store_true', default=False, help='Allow the specified tenant to share this image.') def do_member_create(gc, args): """Share a specific image with a tenant.""" image = utils.find_resource(gc.images, args.image) gc.image_members.create(image, args.tenant_id, args.can_share) @utils.arg('image', metavar='', help='Image from which to remove member.') @utils.arg('tenant_id', metavar='', help='Tenant to remove as member.') def do_member_delete(gc, args): """Remove a shared image from a tenant.""" image_id = utils.find_resource(gc.images, args.image).id gc.image_members.delete(image_id, args.tenant_id) python-glanceclient-2.9.1/glanceclient/v1/client.py0000666000175100017510000000340713232161652022333 0ustar zuulzuul00000000000000# 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. from glanceclient.common import http from glanceclient.common import utils from glanceclient.v1 import image_members from glanceclient.v1 import images from glanceclient.v1 import versions class Client(object): """Client for the OpenStack Images v1 API. :param string endpoint: A user-supplied endpoint URL for the glance service. :param string token: Token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) :param string language_header: Set Accept-Language header to be sent in requests to glance. """ def __init__(self, endpoint=None, **kwargs): """Initialize a new client for the Images v1 API.""" endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0) self.http_client = http.get_http_client(endpoint=endpoint, **kwargs) self.images = images.ImageManager(self.http_client) self.image_members = image_members.ImageMemberManager(self.http_client) self.versions = versions.VersionManager(self.http_client) python-glanceclient-2.9.1/glanceclient/v1/images.py0000666000175100017510000003335513232161652022327 0ustar zuulzuul00000000000000# 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 copy from oslo_utils import encodeutils from oslo_utils import strutils import six import six.moves.urllib.parse as urlparse from glanceclient.common import utils from glanceclient.v1.apiclient import base UPDATE_PARAMS = ('name', 'disk_format', 'container_format', 'min_disk', 'min_ram', 'owner', 'size', 'is_public', 'protected', 'location', 'checksum', 'copy_from', 'properties', # NOTE(bcwaldon: an attempt to update 'deleted' will be # ignored, but we need to support it for backwards- # compatibility with the legacy client library 'deleted') CREATE_PARAMS = UPDATE_PARAMS + ('id', 'store') DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') OS_REQ_ID_HDR = 'x-openstack-request-id' class Image(base.Resource): def __repr__(self): return "" % self._info def update(self, **fields): self.manager.update(self, **fields) def delete(self, **kwargs): return self.manager.delete(self) def data(self, **kwargs): return self.manager.data(self, **kwargs) class ImageManager(base.ManagerWithFind): resource_class = Image def _list(self, url, response_key, obj_class=None, body=None): resp, body = self.client.get(url) if obj_class is None: obj_class = self.resource_class data = body[response_key] return ([obj_class(self, res, loaded=True) for res in data if res], resp) def _image_meta_from_headers(self, headers): meta = {'properties': {}} safe_decode = encodeutils.safe_decode for key, value in headers.items(): # NOTE(flaper87): this is a compatibility fix # for urllib3 >= 1.11. Please, refer to this # bug for more info: # https://bugs.launchpad.net/python-glanceclient/+bug/1487645 key = key.lower() value = safe_decode(value, incoming='utf-8') if key.startswith('x-image-meta-property-'): _key = safe_decode(key[22:], incoming='utf-8') meta['properties'][_key] = value elif key.startswith('x-image-meta-'): _key = safe_decode(key[13:], incoming='utf-8') meta[_key] = value for key in ['is_public', 'protected', 'deleted']: if key in meta: meta[key] = strutils.bool_from_string(meta[key]) return self._format_image_meta_for_user(meta) def _image_meta_to_headers(self, fields): headers = {} fields_copy = copy.deepcopy(fields) # NOTE(flaper87): Convert to str, headers # that are not instance of basestring. All # headers will be encoded later, before the # request is sent. def to_str(value): if not isinstance(value, six.string_types): return str(value) return value for key, value in fields_copy.pop('properties', {}).items(): headers['x-image-meta-property-%s' % key] = to_str(value) for key, value in fields_copy.items(): headers['x-image-meta-%s' % key] = to_str(value) return headers @staticmethod def _format_image_meta_for_user(meta): for key in ['size', 'min_ram', 'min_disk']: if key in meta: try: meta[key] = int(meta[key]) if meta[key] else 0 except ValueError: pass return meta def get(self, image, **kwargs): """Get the metadata for a specific image. :param image: image object or id to look up :rtype: :class:`Image` """ image_id = base.getid(image) resp, body = self.client.head('/v1/images/%s' % urlparse.quote(str(image_id))) meta = self._image_meta_from_headers(resp.headers) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return Image(self, meta) def data(self, image, do_checksum=True, **kwargs): """Get the raw data for a specific image. :param image: image object or id to look up :param do_checksum: Enable/disable checksum validation :rtype: iterable containing image data """ image_id = base.getid(image) resp, body = self.client.get('/v1/images/%s' % urlparse.quote(str(image_id))) content_length = int(resp.headers.get('content-length', 0)) checksum = resp.headers.get('x-image-meta-checksum', None) if do_checksum and checksum is not None: body = utils.integrity_iter(body, checksum) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return utils.IterableWithLength(body, content_length) def _build_params(self, parameters): params = {'limit': parameters.get('page_size', DEFAULT_PAGE_SIZE)} if 'marker' in parameters: params['marker'] = parameters['marker'] sort_key = parameters.get('sort_key') if sort_key is not None: if sort_key in SORT_KEY_VALUES: params['sort_key'] = sort_key else: raise ValueError('sort_key must be one of the following: %s.' % ', '.join(SORT_KEY_VALUES)) sort_dir = parameters.get('sort_dir') if sort_dir is not None: if sort_dir in SORT_DIR_VALUES: params['sort_dir'] = sort_dir else: raise ValueError('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) filters = parameters.get('filters', {}) properties = filters.pop('properties', {}) for key, value in properties.items(): params['property-%s' % key] = value params.update(filters) if parameters.get('owner') is not None: params['is_public'] = None if 'is_public' in parameters: params['is_public'] = parameters['is_public'] return params def list(self, **kwargs): """Get a list of images. :param page_size: number of items to request in each paginated request :param limit: maximum number of images to return :param marker: begin returning images that appear later in the image list than that represented by this image id :param filters: dict of direct comparison filters that mimics the structure of an image object :param owner: If provided, only images with this owner (tenant id) will be listed. An empty string ('') matches ownerless images. :param return_req_id: If an empty list is provided, populate this list with the request ID value from the header x-openstack-request-id :rtype: list of :class:`Image` """ absolute_limit = kwargs.get('limit') page_size = kwargs.get('page_size', DEFAULT_PAGE_SIZE) owner = kwargs.get('owner', None) def filter_owner(owner, image): # If client side owner 'filter' is specified # only return images that match 'owner'. if owner is None: # Do not filter based on owner return False if (not hasattr(image, 'owner')) or image.owner is None: # ownerless image return not (owner == '') else: return not (image.owner == owner) def paginate(qp, return_request_id=None): for param, value in qp.items(): if isinstance(value, six.string_types): # Note(flaper87) Url encoding should # be moved inside http utils, at least # shouldn't be here. # # Making sure all params are str before # trying to encode them qp[param] = encodeutils.safe_decode(value) url = '/v1/images/detail?%s' % urlparse.urlencode(qp) images, resp = self._list(url, "images") if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) for image in images: yield image return_request_id = kwargs.get('return_req_id', None) params = self._build_params(kwargs) seen = 0 while True: seen_last_page = 0 filtered = 0 for image in paginate(params, return_request_id): last_image = image.id if filter_owner(owner, image): # Note(kragniz): ignore this image filtered += 1 continue if (absolute_limit is not None and seen + seen_last_page >= absolute_limit): # Note(kragniz): we've seen enough images return else: seen_last_page += 1 yield image seen += seen_last_page if seen_last_page + filtered == 0: # Note(kragniz): we didn't get any images in the last page return if absolute_limit is not None and seen >= absolute_limit: # Note(kragniz): reached the limit of images to return return if page_size and seen_last_page + filtered < page_size: # Note(kragniz): we've reached the last page of the images return # Note(kragniz): there are more images to come params['marker'] = last_image seen_last_page = 0 def delete(self, image, **kwargs): """Delete an image.""" url = "/v1/images/%s" % base.getid(image) resp, body = self.client.delete(url) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) def create(self, **kwargs): """Create an image. TODO(bcwaldon): document accepted params """ image_data = kwargs.pop('data', None) if image_data is not None: image_size = utils.get_file_size(image_data) if image_size is not None: kwargs.setdefault('size', image_size) fields = {} for field in kwargs: if field in CREATE_PARAMS: fields[field] = kwargs[field] elif field == 'return_req_id': continue else: msg = 'create() got an unexpected keyword argument \'%s\'' raise TypeError(msg % field) copy_from = fields.pop('copy_from', None) hdrs = self._image_meta_to_headers(fields) if copy_from is not None: hdrs['x-glance-api-copy-from'] = copy_from resp, body = self.client.post('/v1/images', headers=hdrs, data=image_data) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return Image(self, self._format_image_meta_for_user(body['image'])) def update(self, image, **kwargs): """Update an image. TODO(bcwaldon): document accepted params """ image_data = kwargs.pop('data', None) if image_data is not None: image_size = utils.get_file_size(image_data) if image_size is not None: kwargs.setdefault('size', image_size) hdrs = {} purge_props = 'false' purge_props_bool = kwargs.pop('purge_props', None) if purge_props_bool: purge_props = 'true' hdrs['x-glance-registry-purge-props'] = purge_props fields = {} for field in kwargs: if field in UPDATE_PARAMS: fields[field] = kwargs[field] elif field == 'return_req_id': continue else: msg = 'update() got an unexpected keyword argument \'%s\'' raise TypeError(msg % field) copy_from = fields.pop('copy_from', None) hdrs.update(self._image_meta_to_headers(fields)) if copy_from is not None: hdrs['x-glance-api-copy-from'] = copy_from url = '/v1/images/%s' % base.getid(image) resp, body = self.client.put(url, headers=hdrs, data=image_data) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return Image(self, self._format_image_meta_for_user(body['image'])) python-glanceclient-2.9.1/glanceclient/client.py0000666000175100017510000000464213232161651022006 0ustar zuulzuul00000000000000# 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 warnings from oslo_utils import importutils from glanceclient.common import utils def Client(version=None, endpoint=None, session=None, *args, **kwargs): """Client for the OpenStack Images API. Generic client for the OpenStack Images API. See version classes for specific details. :param string version: The version of API to use. :param session: A keystoneauth1 session that should be used for transport. :type session: keystoneauth1.session.Session """ # FIXME(jamielennox): Add a deprecation warning if no session is passed. # Leaving it as an option until we can ensure nothing break when we switch. if session: if endpoint: kwargs.setdefault('endpoint_override', endpoint) if not version: __, version = utils.strip_version(endpoint) if not version: msg = ("You must provide a client version when using session") raise RuntimeError(msg) else: if version is not None: warnings.warn(("`version` keyword is being deprecated. Please pass" " the version as part of the URL. " "http://$HOST:$PORT/v$VERSION_NUMBER"), DeprecationWarning) endpoint, url_version = utils.strip_version(endpoint) version = version or url_version if not version: msg = ("Please provide either the version or an url with the form " "http://$HOST:$PORT/v$VERSION_NUMBER") raise RuntimeError(msg) module = importutils.import_versioned_module('glanceclient', int(version), 'client') client_class = getattr(module, 'Client') return client_class(endpoint, *args, session=session, **kwargs) python-glanceclient-2.9.1/releasenotes/0000775000175100017510000000000013232162126020205 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/source/0000775000175100017510000000000013232162126021505 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/source/index.rst0000666000175100017510000000025613232161652023356 0ustar zuulzuul00000000000000========================== glanceclient Release Notes ========================== .. toctree:: :maxdepth: 1 unreleased pike ocata newton mitaka earlier python-glanceclient-2.9.1/releasenotes/source/_static/0000775000175100017510000000000013232162126023133 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/source/_static/.placeholder0000666000175100017510000000000013232161652025411 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/source/_templates/0000775000175100017510000000000013232162126023642 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/source/_templates/.placeholder0000666000175100017510000000000013232161652026120 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/source/pike.rst0000666000175100017510000000021713232161652023174 0ustar zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike python-glanceclient-2.9.1/releasenotes/source/unreleased.rst0000666000175100017510000000015313232161652024372 0ustar zuulzuul00000000000000============================ Current Series Release Notes ============================ .. release-notes:: python-glanceclient-2.9.1/releasenotes/source/newton.rst0000666000175100017510000000023213232161652023553 0ustar zuulzuul00000000000000=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton python-glanceclient-2.9.1/releasenotes/source/ocata.rst0000666000175100017510000000023013232161652023326 0ustar zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata python-glanceclient-2.9.1/releasenotes/source/conf.py0000666000175100017510000002221213232161652023010 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # 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. # Glance Release Notes documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) import openstackdocstheme # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'glanceclient Release Notes' copyright = u'2016, Glance Developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. import pbr.version # The full version, including alpha/beta/rc tags. glance_version = pbr.version.VersionInfo('glanceclient') release = glance_version.version_string_with_vcs() # The short X.Y version. version = glance_version.canonical_version_string() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] html_theme_path = [openstackdocstheme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'GlanceClientReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'glanceclientReleaseNotes.tex', u'glanceclient Release Notes Documentation', u'Glance Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'glanceclientreleasenotes', u'glanceclient Release Notes Documentation', [u'Glance Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'glanceclientReleaseNotes', u'glanceclient Release Notes Documentation', u'Glance Developers', 'glanceclientReleaseNotes', 'Python bindings for the OpenStack Image service.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] python-glanceclient-2.9.1/releasenotes/source/earlier.rst0000666000175100017510000005602313232161652023675 0ustar zuulzuul00000000000000================== Earlier Releases ================== 1.2.0 ----- * This release consists mainly bugfixes since Liberty release. * Some functionality has been added, documentation improved. * Trivial & typo fixed and requirement changes not included below. * 1511180_: Add versions list function * 1508356_: Added reactivate/deactivate image using CLI * 1510340_: Fix the missing help descripiton of "image-create" * 8a4cd79 Add documentation for running the functional tests * 5a24705 Update docs to recommend KSA instead of KSC * 1507386_: Use clouds.yaml from devstack for functional tests * 4fb3092 Add translation to v2 shell * b51634a improve readme contents * 1480529_: Add support for setting Accept-Language header * 1504058_: Use the subcomand parsed args instead of the base * afd1810 Stop trying to send image_size to the server * 1485407_: Support image deletion in batches in v2 * 1295356_: print usage when no argument is specified for python3 * df0f664 Do not use openstack.common.i18n in glance client * 1f2fefb Use common identity parameters fro keystone client * 1499540_: No auth when token and endpoint are passed * 557acb1 Use dictionary literal for dictionary creation * c6addc7 Replace exception_to_str with oslo.utils function * 1496305_: Don't get the image before deleting it * 1495632_: Fix human readable when size is None * 1489727_: Add parsing the endpoint URL * 1467719_: Add check Identity validate when get schemas .. _1511180: https://bugs.launchpad.net/python-glanceclient/+bug/1511180 .. _1508356: https://bugs.launchpad.net/python-glanceclient/+bug/1508356 .. _1510340: https://bugs.launchpad.net/python-glanceclient/+bug/1510340 .. _1507386: https://bugs.launchpad.net/python-neutronclient/+bug/1507386 .. _1480529: https://bugs.launchpad.net/python-glanceclient/+bug/1480529 .. _1504058: https://bugs.launchpad.net/python-glanceclient/+bug/1504058 .. _1485407: https://bugs.launchpad.net/python-glanceclient/+bug/1485407 .. _1295356: https://bugs.launchpad.net/python-novaclient/+bug/1295356 .. _1499540: https://bugs.launchpad.net/python-glanceclient/+bug/1499540 .. _1496305: https://bugs.launchpad.net/python-glanceclient/+bug/1496305 .. _1495632: https://bugs.launchpad.net/python-glanceclient/+bug/1495632 .. _1489727: https://bugs.launchpad.net/python-glanceclient/+bug/1489727 .. _1467719: https://bugs.launchpad.net/glance/+bug/1467719 1.1.0 ----- * This release provides mainly bugfixes for the bugs discovered after defaulting to v2 API on CLI. If you're using 1.0.0 client, it is highly recommended to upgrade. * 1494259_: Fixes CLI client called without subcommands * 1488914_: Print the reverting back to v1 to stderr * 1487645_: Invalid output running the command 'glance image-show ' * 1490457_: Don't make `help` require auth parameters * 1491311_: check for None value in utils.safe_header * f0b30f4 Updated from global requirements * 1490462_: Consider `--os-token` when using v2 * 1489381_: Check if v2 is available and fallback * 1491646_: Update path to subunit2html in post_test_hook * 1488892_: Password should be prompted once .. _1494259: https://bugs.launchpad.net/python-glanceclient/+bug/1494259 .. _1488914: https://bugs.launchpad.net/python-glanceclient/+bug/1488914 .. _1487645: https://bugs.launchpad.net/python-glanceclient/+bug/1487645 .. _1490457: https://bugs.launchpad.net/python-glanceclient/+bug/1490457 .. _1491311: https://bugs.launchpad.net/python-glanceclient/+bug/1491311 .. _1490462: https://bugs.launchpad.net/python-glanceclient/+bug/1490462 .. _1489381: https://bugs.launchpad.net/python-glanceclient/+bug/1489381 .. _1491646: https://bugs.launchpad.net/python-glanceclient/+bug/1491646 .. _1488892: https://bugs.launchpad.net/python-glanceclient/+bug/1488892 1.0.0 ----- * This major release of python-glanceclient defaults to using the Images v2 API for the Command Line Interface. This is consistent with the current situation in the Glance project, where the Images v1 API is 'SUPPORTED' and the Images v2 API is 'CURRENT'. Further, it makes the CLI consistent with the client API, which has used the Images v2 API as the default since the Kilo release. A lot of effort has been invested to make the transition as smooth as possible, but we acknowledge that CLI users will encounter backwards incompatibility. * remcustssl_: Remove custom SSL compression handling * 14be607 Add more information show in v2 * 1309272_: Require disk and container format on image-create * 1481729_: Ship the default image schema in the client * 181131e Use API v2 as default * 1477910_: V2: Do not validate image schema when listing * 9284eb4 Updated from global requirements * 1475769_: Add unicode support for properties values in v2 shell * 1479020_: Fix failure to create glance https connection pool * ec0f2df Enable flake8 checks * 1433637_: Extend unittests coverage for v2 tasks module * metatags_: Support for Metadata Definition Catalog for Tags * b48ff98 Fix exception message in Http.py * 1472234_: Fix an issue with broken test on ci * 1473454_: Remove usage of assert_called_once on Mock objects * 9fdd4f1 Add .eggs/* to .gitignore * 0f9aa99 Updated from global requirements * 1468485_: Account for dictionary order in test_shell.py * bp-oslo-ns_: Do not fall back to namespaced oslo.i18n * b10e893 Updated from global requirements * 1465373_: Add v2 support for the marker attribute * 997c12d Import only modules and update tox.ini * 0810805 Updated from global requirements * 1461678_: Close iterables at the end of iteration * bp-session_: Make glanceclient accept a session object * 5e85d61 cleanup openstack-common.conf and sync updated files * 1432701_: Add parameter 'changes-since' for image-list of v1 .. _remcustssl: https://review.openstack.org/#/c/187674 .. _1309272: https://bugs.launchpad.net/python-glanceclient/+bug/1309272 .. _1481729: https://bugs.launchpad.net/python-glanceclient/+bug/1481729 .. _1477910: https://bugs.launchpad.net/python-glanceclient/+bug/1477910 .. _1475769: https://bugs.launchpad.net/python-glanceclient/+bug/1475769 .. _1479020: https://bugs.launchpad.net/python-glanceclient/+bug/1479020 .. _1433637: https://bugs.launchpad.net/python-glanceclient/+bug/1433637 .. _metatags: https://review.openstack.org/#/c/179674/ .. _1472234: https://bugs.launchpad.net/python-glanceclient/+bug/1472234 .. _1473454: https://bugs.launchpad.net/python-cinderclient/+bug/1473454 .. _1468485: https://bugs.launchpad.net/python-glanceclient/+bug/1468485 .. _bp-oslo-ns: https://blueprints.launchpad.net/oslo-incubator/+spec/remove-namespace-packages .. _1465373: https://bugs.launchpad.net/python-glanceclient/+bug/1465373 .. _1461678: https://bugs.launchpad.net/nova/+bug/1461678 .. _bp-session: https://blueprints.launchpad.net/python-glanceclient/+spec/session-objects .. _1432701: https://bugs.launchpad.net/glance/+bug/1432701 0.19.0 ------ * 1381514_: Include ``owner`` in v2 image list * 1433884_: Fix ``md-object-update`` issue * 1446096_: Stop crashing if ``$HOME`` is not writable * 1402632_: Improve import related error handling .. _1381514: https://bugs.launchpad.net/python-glanceclient/+bug/1381514 .. _1433884: https://bugs.launchpad.net/python-glanceclient/+bug/1433884 .. _1455102: https://bugs.launchpad.net/python-glanceclient/+bug/1455102 .. _1446096: https://bugs.launchpad.net/python-glanceclient/+bug/1446096 .. _1402632: https://bugs.launchpad.net/python-glanceclient/+bug/1402632 0.18.0 ------ * 1442664_, 1442883_, 1357430_: Fix errors when SSL compression is disabled * 1399778_: Remove ``locations`` from image-create arguments * 1439513_: Fix error on python 3 when creating a task with and invalid property * Stop accepting ``*args`` in the main client interface * Expose ``is_base`` schema property attribute, allowing the client to differentiate between base and custom image properties * 1433962_: Validate whether a tag is valid when filtering for images. Invalid tags now raise an error rather than being ignored * 1434381_: Add ``--human-readable`` option to ``image-show`` .. _1442664: https://bugs.launchpad.net/python-glanceclient/+bug/1442664 .. _1442883: https://bugs.launchpad.net/python-glanceclient/+bug/1442883 .. _1357430: https://bugs.launchpad.net/python-glanceclient/+bug/1357430 .. _1399778: https://bugs.launchpad.net/python-glanceclient/+bug/1399778 .. _1439513: https://bugs.launchpad.net/python-glanceclient/+bug/1439513 .. _1433962: https://bugs.launchpad.net/python-glanceclient/+bug/1433962 .. _1434381: https://bugs.launchpad.net/python-glanceclient/+bug/1434381 0.17.0 ------ * 1420707_: Updated help for v2 member-update api * glance-sorting-enhancements_: Extend images CLI v2 with new sorting syntax * glance-sorting-enhancements_: Add the ability to specify the sort dir for each key * glance-sorting-enhancements_: Adds the ability to sort images with multiple keys * 1306774_: Apply expected patch format when updating tags in v2.images * 1429088_: v2: read limit for list from --limit in shell * 1428797_: Fix leaking sockets after v2 list operation * 1423939_: Fix leaking sockets after v1 list operation * 1408033_: v2: Allow upload from stdin on image-create .. _1420707: https://bugs.launchpad.net/python-glanceclient/+bug/1420707 .. _glance-sorting-enhancements: https://blueprints.launchpad.net/glance/+spec/glance-sorting-enhancements .. _1306774: https://bugs.launchpad.net/python-glanceclient/+bug/1306774 .. _1429088: https://bugs.launchpad.net/python-glanceclient/+bug/1429088 .. _1428797: https://bugs.launchpad.net/python-glanceclient/+bug/1428797 .. _1423939: https://bugs.launchpad.net/python-glanceclient/+bug/1423939 .. _1408033: https://bugs.launchpad.net/python-glanceclient/+bug/1408033 0.16.1 ------ * 1423165_: Fix sockets leaking for a subset of operations (show, delete and update) * 1395084_: Show error when trying to upload image data to non-queued image * 1398838_: Stop showing JSON and HTML in error messages returned from the glance service * 1396550_: More reliably register connection pools in cases where urllib3 is both vendored and installed system-wide .. _1423165: https://bugs.launchpad.net/python-glanceclient/+bug/1423165 .. _1395084: https://bugs.launchpad.net/python-glanceclient/+bug/1395084 .. _1398838: https://bugs.launchpad.net/python-glanceclient/+bug/1398838 .. _1396550: https://bugs.launchpad.net/python-glanceclient/+bug/1396550 0.16.0 ------ * Add --limit option to the v2 list operation. This allows a user to limit the number of images requested from the glance server. Previously the client would always go through the entire list of images * The CLI exit code on keyboard interrupt is now ``130``, changed from ``1``. * 1370283_: The set of supported SSL ciphers is now reduced to a smaller and more secure subset * 1384664_, 1402746_: Fix enabling the progress bar on download and upload when image data is piped into the client causing the client to crash * 1415935_: NoneType header values are now ignored when encoding headers * 1341777_: Requests which are streamed are now explicitly closed when the end of the stream has been reached * 1394236_: The CLI is now strict about what it counts as a boolean, and exits with an error if a non-boolean value is used as input to a boolean option * 1401197_: The CLI is now strict about valid inputs to ``--os-image-api-version`` * 1333119_: The CLI now raises a more useful error message when a user requests the deletion of an image which is already deleted * 1384759_: Fix client erroring if ``--os-tenant-id`` and ``--os-tenant-name`` are not defined * 1228744_: Add globoff option to debug curl statements. This allows it to work with IPv6 addresses .. _1370283: https://bugs.launchpad.net/python-glanceclient/+bug/1370283 .. _1384664: https://bugs.launchpad.net/python-glanceclient/+bug/1384664 .. _1402746: https://bugs.launchpad.net/python-glanceclient/+bug/1402746 .. _1415935: https://bugs.launchpad.net/python-glanceclient/+bug/1415935 .. _1394236: https://bugs.launchpad.net/python-glanceclient/+bug/1394236 .. _1401197: https://bugs.launchpad.net/python-glanceclient/+bug/1401197 .. _1384759: https://bugs.launchpad.net/python-glanceclient/+bug/1384759 .. _1228744: https://bugs.launchpad.net/python-glanceclient/+bug/1228744 .. _1333119: https://bugs.launchpad.net/python-glanceclient/+bug/1333119 0.15.0 ------ * Stop requiring a version to create a Client instance. The ``version`` argument is now a keyword. If no ``version`` is specified and a versioned endpoint is supplied, glanceclient will use the endpoint's version. If the endpoint is unversioned and a value for ``version`` is not supplied, glanceclient falls back to v1. This change is backwards-compatible. Examples:: >>> glanceclient.Client(version=1, endpoint='http://localhost:9292') # returns a v1 client >>> glanceclient.Client(endpoint='http://localhost:9292/v2') # returns a v2 client >>> glanceclient.Client(endpoint='http://localhost:9292') # returns a v1 client >>> glanceclient.Client(2, 'http://localhost:9292/v2') # old behavior is preserved * Add bash completion to glance client. The new bash completion files are stored in ``tools/glance.bash_completion`` * Add tty password entry. This prompts for a password if neither ``--os-password`` nor ``OS_PASSWORD`` have been set * Add the ``--property-filter`` option from the v1 client to v2 image-list. This allows you to do something similar to:: $ glance --os-image-api-version 2 image-list --property-filter os_distro=NixOS * 1324067_: Allow --file flag in v2 image-create. This selects a local disk image to upload during the creation of the image * 1395841_: Output a useful error on an invalid ``--os-image-api-version`` argument * 1394965_: Add ``identity_headers`` back into the request headers * 1350802_: Remove read only options from v2 shell commands. The options omitted are - ``created_at`` - ``updated_at`` - ``file`` - ``checksum`` - ``virtual_size`` - ``size`` - ``status`` - ``schema`` - ``direct_url`` * 1381295_: Stop setting X-Auth-Token key in http session header if there is no token provided * 1378844_: Fix ``--public`` being ignored on image-create * 1367782_: Fix to ensure ``endpoint_type`` is used by ``_get_endpoint()`` * 1381816_: Support Pagination for namespace list * 1401032_: Add support for enum types in the schema that accept ``None`` .. _1324067: https://bugs.launchpad.net/python-glanceclient/+bug/1324067 .. _1395841: https://bugs.launchpad.net/python-glanceclient/+bug/1395841 .. _1394965: https://bugs.launchpad.net/python-glanceclient/+bug/1394965 .. _1350802: https://bugs.launchpad.net/python-glanceclient/+bug/1350802 .. _1381295: https://bugs.launchpad.net/python-glanceclient/+bug/1381295 .. _1378844: https://bugs.launchpad.net/python-glanceclient/+bug/1378844 .. _1367782: https://bugs.launchpad.net/python-glanceclient/+bug/1367782 .. _1381816: https://bugs.launchpad.net/python-glanceclient/+bug/1381816 .. _1401032: https://bugs.launchpad.net/python-glanceclient/+bug/1401032 0.14.2 ------ * Add support for Glance Tasks calls (task create, list all and show) * 1362179_: Default to system CA bundle if no CA certificate is provided * 1350251_, 1347150_, 1362766_: Don't replace the https handler in the poolmanager * 1371559_: Skip non-base properties in patch method .. _1362179: https://bugs.launchpad.net/python-glanceclient/+bug/1362179 .. _1350251: https://bugs.launchpad.net/python-glanceclient/+bug/1350251 .. _1347150: https://bugs.launchpad.net/python-glanceclient/+bug/1347150 .. _1362766: https://bugs.launchpad.net/python-glanceclient/+bug/1362766 .. _1371559: https://bugs.launchpad.net/python-glanceclient/+bug/1371559 0.14.1 ------ * Print traceback to stderr if ``--debug`` is set * Downgrade log message for http request failures * Fix CLI image-update giving the wrong help on '--tags' parameter * 1367326_: Fix requests to non-bleeding edge servers using the v2 API * 1329301_: Update how tokens are redacted * 1369756_: Fix decoding errors when logging response headers .. _1367326: https://bugs.launchpad.net/python-glanceclient/+bug/1367326 .. _1329301: https://bugs.launchpad.net/python-glanceclient/+bug/1329301 .. _1369756: https://bugs.launchpad.net/python-glanceclient/+bug/1369756 0.14.0 ------ * Add support for metadata definitions catalog API * Enable osprofiler profiling support to glanceclient and its shell. This adds the ``--profile `` argument. * Add support for Keystone v3 * Replace old httpclient with requests * Fix performance issue for image listing of v2 API * 1364893_: Catch a new urllib3 exception: ProtocolError * 1359880_: Fix error when logging http response with python 3 * 1357430_: Ensure server's SSL cert is validated to help guard against man-in-the-middle attack * 1314218_: Remove deprecated commands from shell * 1348030_: Fix glance-client on IPv6 controllers * 1341777_: Don't stream non-binary requests .. _1364893: https://bugs.launchpad.net/python-glanceclient/+bug/1364893 .. _1359880: https://bugs.launchpad.net/python-glanceclient/+bug/1359880 .. _1357430: https://bugs.launchpad.net/python-glanceclient/+bug/1357430 .. _1314218: https://bugs.launchpad.net/python-glanceclient/+bug/1314218 .. _1348030: https://bugs.launchpad.net/python-glanceclient/+bug/1348030 .. _1341777: https://bugs.launchpad.net/python-glanceclient/+bug/1341777 0.13.0 ------ * Add command line support for image multi-locations * Py3K support completed * Fixed several issues related to UX * Progress bar support for V2 0.12.0 ------ * Add command line support for V2 image create, update, and upload * Enable querying for images by tag * 1230032_, 1231524_: Fix several issues with handling redirects * 1206095_: Use openstack-images-v2.1-json-patch for update method .. _1230032: http://bugs.launchpad.net/python-glanceclient/+bug/1230032 .. _1231524: http://bugs.launchpad.net/python-glanceclient/+bug/1231524 .. _1206095: http://bugs.launchpad.net/python-glanceclient/+bug/1206095 0.11.0 ------ * 1212463_: Allow single-wildcard SSL common name matching * 1208618_: Support absolute redirects for endpoint urls * 1190606_: Properly handle integer-like image ids * Support removing properties from images in the v2 library .. _1212463: http://bugs.launchpad.net/python-glanceclient/+bug/1212463 .. _1208618: http://bugs.launchpad.net/python-glanceclient/+bug/1208618 .. _1190606: http://bugs.launchpad.net/python-glanceclient/+bug/1190606 0.10.0 ------ * 1192229_: Security Update! Fix SSL certificate CNAME checking to handle ip addresses correctly * Add an optional progress bar for image downloads * Additional v2 api functionality, including image creation and uploads * Allow v1 admin clients to list all users' images, and to list the images of specific tenants. * Add a --checksum option to the v2 CLI for selecting images by checksum * Added support for image creation and uploads to the v2 library * Added support for updating and deleting v2 image tags to the v2 library and CLI * Added support for managing image memberships to the v2 library and CLI * Added a cli man page. * 1184566_: Fix support for unix pipes when uploading images in the v1 CLI * 1157864_: Fix an issue where glanceclient would fail with eventlet. .. _1192229: http://bugs.launchpad.net/python-glanceclient/+bug/1192229 .. _1184566: http://bugs.launchpad.net/python-glanceclient/+bug/1184566 .. _1157864: http://bugs.launchpad.net/python-glanceclient/+bug/1157864 0.9.0 ----- * Implement 'visibility', 'owner' and 'member_status' filters for v2 CLI and library * Relax prettytable dependency to v0.7.X * 1118799_: Implement filter on 'is_public' attribute in v1 API * 1157905_, 1130390_: Improve handling of SIGINT (CTRL-C) .. _1118799: http://bugs.launchpad.net/python-glanceclient/+bug/1118799 .. _1157905: http://bugs.launchpad.net/python-glanceclient/+bug/1157905 .. _1130390: http://bugs.launchpad.net/python-glanceclient/+bug/1130390 0.8.0 ----- * Implement image-delete for Image API v2 * Update warlock dependency to >= 0.7.0 and < 1 * 1061150_: Support non-ASCII characters * 1102944_: The port option is configurable when using HTTPS * 1093380_: Support image names in place of IDs for CLI commands * 1094917_: Better representation of errors through CLI .. _1061150: http://bugs.launchpad.net/python-glanceclient/+bug/1061150 .. _1102944: http://bugs.launchpad.net/python-glanceclient/+bug/1102944 .. _1093380: http://bugs.launchpad.net/python-glanceclient/+bug/1093380 .. _1094917: http://bugs.launchpad.net/python-glanceclient/+bug/1094917 0.7.0 ----- * Add ``--store`` option to ``image-create`` command * Deprecate ``--ca-file`` in favor of ``--os-cacert`` * 1082957_: Add ``--sort-key`` and ``--sort-dir`` CLI options to ``image-list`` command * 1081542_: Change default ``image-list`` CLI sort to order by image name ascending * 1079692_: Verify SSL certification hostnames when using HTTPS * 1080739_: Use ``--os-region-name`` in service catalog lookup .. _1082957: http://bugs.launchpad.net/python-glanceclient/+bug/1082957 .. _1081542: http://bugs.launchpad.net/python-glanceclient/+bug/1081542 .. _1079692: http://bugs.launchpad.net/python-glanceclient/+bug/1079692 .. _1080739: http://bugs.launchpad.net/python-glanceclient/+bug/1080739 0.6.0 ----- * Multiple image ID can be passed to ``glance image-delete`` * ``glance --version`` and glanceclient.__version__ expose the current library version * Use ``--human-readable`` with ``image-list`` and ``image-show`` to display image sizes in human-friendly formats * Use OpenSSL for HTTPS connections * 1056220_: Always use 'Transfer-Encoding: chunked' when transferring image data * 1052846_: Padded endpoints enabled (e.g. glance.example.com/padding/v1) * 1050345_: ``glance image-create`` and ``glance image-update`` now work on Windows .. _1056220: http://bugs.launchpad.net/python-glanceclient/+bug/1056220 .. _1052846: http://bugs.launchpad.net/python-glanceclient/+bug/1052846 .. _1050345: http://bugs.launchpad.net/python-glanceclient/+bug/1050345 0.5.1 ----- * 1045824_: Always send Content-Length when updating image with image data * 1046607_: Handle 300 Multiple Choices nicely in the CLI * 1035931_: Properly display URI in legacy 'show' command * 1048698_: Catch proper httplib InvalidURL exception .. _1045824: http://bugs.launchpad.net/python-glanceclient/+bug/1045824 .. _1046607: http://bugs.launchpad.net/python-glanceclient/+bug/1046607 .. _1035931: http://bugs.launchpad.net/python-glanceclient/+bug/1035931 .. _1048698: http://bugs.launchpad.net/python-glanceclient/+bug/1048698 0.5.0 ----- * Add 'image-download' command to CLI * Relax dependency on warlock to anything less than v2 0.4.2 ----- * 1037233_: Fix v1 image list where limit kwarg is less than page_size .. _1037233: https://bugs.launchpad.net/python-glanceclient/+bug/1037233 0.4.1 ----- * Default to system CA cert if one is not provided while using SSL * 1036315_: Allow 'deleted' to be provided in v1 API image update * 1036299_: Fix case where boolean values were treated as strings in v1 API * 1036297_: Fix case where int values were treated as strings in v1 API .. _1036315: https://bugs.launchpad.net/python-glanceclient/+bug/1036315 .. _1036299: https://bugs.launchpad.net/python-glanceclient/+bug/1036299 .. _1036297: https://bugs.launchpad.net/python-glanceclient/+bug/1036297 0.4.0 ----- * Send client SSL certificate to server for self-identification * Properly validate server SSL certificates * Images API v2 image data download python-glanceclient-2.9.1/releasenotes/source/mitaka.rst0000666000175100017510000000023213232161652023507 0ustar zuulzuul00000000000000=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka python-glanceclient-2.9.1/releasenotes/notes/0000775000175100017510000000000013232162126021335 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/notes/.placeholder0000666000175100017510000000000013232161652023613 0ustar zuulzuul00000000000000python-glanceclient-2.9.1/releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml0000666000175100017510000000077613232161652030225 0ustar zuulzuul00000000000000--- prelude: > Switch to using keystoneauth for session and auth plugins. other: - > [`bp use-keystoneauth `_] As of keystoneclient 2.2.0, the session and auth plugins code has been deprecated. These modules have been moved to the keystoneauth library. Consumers of the session and plugin modules are encouraged to move to keystoneauth. Note that there should be no change to end users of glanceclient. python-glanceclient-2.9.1/releasenotes/notes/return-request-id-to-caller-47f4c0a684b1d88e.yaml0000666000175100017510000000052213232161652031730 0ustar zuulzuul00000000000000--- features: - Added support to return "x-openstack-request-id" header in request_ids attribute for better tracing. | For ex. | >>> from glanceclient import Client | >>> glance = Client('2', endpoint='OS_IMAGE_ENDPOINT', token='OS_AUTH_TOKEN') | >>> res = glance.images.get('') | >>> res.request_ids python-glanceclient-2.9.1/releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml0000666000175100017510000000031113232161652027437 0ustar zuulzuul00000000000000--- features: - Added support to log 'x-openstack-request-id' for each api call. Please refer, https://blueprints.launchpad.net/python-glanceclient/+spec/log-request-id for more details. python-glanceclient-2.9.1/releasenotes/notes/pike-relnote-2c77b01aa8799f35.yaml0000666000175100017510000001003613232161652026770 0ustar zuulzuul00000000000000--- prelude: > This was a quiet development cycle for the python-glanceclient. There were several improvements made in the testing code, and the documentation was reorganized in accord with the `new standard layout `_ for OpenStack projects. The main feature in this release is the addition of support for the new image import functionality introduced into Glance during this cycle. features: - | Client support has been added for the new image import functionality introduced into Glance in this cycle. This is a feature of the Images API version 2 only, and it is disabled by default in Glance. The following commands have been added to the command line interface: * ``import-info`` - gets information about the import configuration of the target Glance * ``image-stage`` - uploads image data to the staging area * ``image-import`` - initiates the import process for a previously created image record whose image data is currently in the staging area * ``image-create-via-import`` - this is an EXPERIMENTAL command that compresses the three-step import process of the Images API version 2 into a single call from the command line, just as the current client ``image-create`` command compresses a two-step process into one step. *It is EXPERIMENTAL because the name of the command may change or it may be removed entirely in future releases.* The intent is that as Glance image import is adopted by deployers, this command may be renamed to ``image-create`` as it behaves exactly the same from the user's point of view. It is included in this release so that the Glance team can get feedback from deployers and end users. fixes: - | The following are some highlights of the bug fixes included in this release. * Bug 1659010_: Help text inaccurate for container_format, disk_format * Bug 1570766_: Fix 'UnicodeEncodeError' for unicode values in url * Bug 1583919_: --no-ssl-compression is deprecated .. _1659010: https://code.launchpad.net/bugs/1659010 .. _1570766: https://code.launchpad.net/bugs/1570766 .. _1583919: https://code.launchpad.net/bugs/1583919 other: - | The deprecated ``--no-ssl-compression`` option to the python-glanceclient command line interface has been removed_. The option has been inoperative_ since the Liberty release. - | An optimization_ was added in the case where an image download is requested from the command line interface without specifying either a filename destination for the data or output redirection. The optimization properly delays opening a connection to the server until *after* the CLI has verified that the user has specified a location for the downloaded data. In the pre-optimized code, if a user did not have permission to download the requested image or if the image had no data associated with it, the CLI would fail with an appropriate message when the client attempted to create the connection but before it had determined that there was no place to put the data. With this optimization, a user will not be able to "probe" the server to see whether image data is available without specifying either the ``--file`` option or command line redirection. - | The argument to the ``--profile`` option of the command line interface may now be specified_ by setting the value of the ``OS_PROFILE`` environment variable. .. _removed: https://git.openstack.org/cgit/openstack/python-glanceclient/commit/?id=28c003dc1179ddb3124fd30c6f525dd341ae9213 .. _inoperative: https://specs.openstack.org/openstack/glance-specs/specs/liberty/approved/remove-special-client-ssl-handling.html .. _optimization: https://git.openstack.org/cgit/openstack/python-glanceclient/commit/?id=1df55dd952fe52c1c1fc2583326d017275b01ddc .. _specified: https://git.openstack.org/cgit/openstack/python-glanceclient/commit/?id=c0f88d5fc0fd947319e022cfeba21bcb15635316 python-glanceclient-2.9.1/HACKING.rst0000666000175100017510000000035513232161651017321 0ustar zuulzuul00000000000000Glance Style Commandments ========================= - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ - Step 2: Read on Glance Specific Commandments ---------------------------- None so far python-glanceclient-2.9.1/run_tests.sh0000777000175100017510000000211513232161652020105 0ustar zuulzuul00000000000000#!/bin/bash function usage { echo "Usage: $0 [OPTION]..." echo "Run python-glanceclient's test suite(s)" echo "" echo " -p, --pep8 Just run flake8" echo " -h, --help Print this usage message" echo "" echo "This script is deprecated and currently retained for compatibility." echo 'You can run the full test suite for multiple environments by running "tox".' echo 'You can run tests for only python 2.7 by running "tox -e py27", or run only' echo 'the flake8 tests with "tox -e pep8".' exit } command -v tox > /dev/null 2>&1 if [ $? -ne 0 ]; then echo 'This script requires "tox" to run.' echo 'You can install it with "pip install tox".' exit 1; fi just_pep8=0 function process_option { case "$1" in -h|--help) usage;; -p|--pep8) let just_pep8=1;; esac } for arg in "$@"; do process_option $arg done if [ $just_pep8 -eq 1 ]; then tox -e pep8 exit fi tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit if [ ${PIPESTATUS[0]} -ne 0 ]; then exit ${PIPESTATUS[0]} fi if [ -z "$toxargs" ]; then tox -e pep8 fi python-glanceclient-2.9.1/setup.py0000666000175100017510000000200613232161652017231 0ustar zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) python-glanceclient-2.9.1/.zuul.yaml0000666000175100017510000000266113232161651017466 0ustar zuulzuul00000000000000- job: name: glanceclient-dsvm-functional parent: devstack-tox-functional description: | devstack-based functional tests for glanceclient required-projects: - openstack/python-glanceclient timeout: 4200 vars: devstack_localrc: # TODO(rosmaita): remove when glanceclient tests no longer # use the Images v1 API GLANCE_V1_ENABLED: True LIBS_FROM_GIT: python-glanceclient devstack_services: # turn off ceilometer ceilometer-acentral: false ceilometer-acompute: false ceilometer-alarm-evaluator: false ceilometer-alarm-notifier: false ceilometer-anotification: false ceilometer-api: false ceilometer-collector: false # turn on swift s-account: true s-container: true s-object: true s-proxy: true # Hardcode glanceclient path so the job can be run on glance patches zuul_work_dir: src/git.openstack.org/openstack/python-glanceclient - job: name: glanceclient-dsvm-functional-identity-v3-only parent: glanceclient-dsvm-functional vars: devstack_localrc: ENABLE_IDENTITY_V2: False - project: name: openstack/python-glanceclient check: jobs: - glanceclient-dsvm-functional - glanceclient-dsvm-functional-identity-v3-only: voting: false gate: jobs: - glanceclient-dsvm-functional python-glanceclient-2.9.1/setup.cfg0000666000175100017510000000233613232162126017343 0ustar zuulzuul00000000000000[metadata] name = python-glanceclient summary = OpenStack Image API Client Library description-file = README.rst license = Apache License, Version 2.0 author = OpenStack author-email = openstack-dev@lists.openstack.org home-page = https://docs.openstack.org/python-glanceclient/latest/ classifier = Development Status :: 5 - Production/Stable Environment :: Console Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 [files] packages = glanceclient [global] setup-hooks = pbr.hooks.setup_hook [entry_points] console_scripts = glance = glanceclient.shell:main [build_sphinx] builders = html,man all-files = 1 warning-is-error = 1 source-dir = doc/source build-dir = doc/build [upload_sphinx] upload-dir = doc/build/html [wheel] universal = 1 [pbr] autodoc_index_modules = True autodoc_exclude_modules = glanceclient.tests.* api_doc_dir = reference/api [egg_info] tag_build = tag_date = 0 python-glanceclient-2.9.1/tox.ini0000666000175100017510000000262713232161652017043 0ustar zuulzuul00000000000000[tox] envlist = py35,py27,pep8 minversion = 1.6 skipsdist = True [testenv] usedevelop = True install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False PYTHONHASHSEED=0 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] commands = flake8 [testenv:venv] commands = {posargs} [pbr] warnerror = True [testenv:functional] # See glanceclient/tests/functional/README.rst # for information on running the functional tests. setenv = OS_TEST_PATH = ./glanceclient/tests/functional whitelist_externals = bash commands = bash tools/fix_ca_bundle.sh python setup.py testr --testr-args='{posargs}' [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' coverage report [testenv:docs] commands= python setup.py build_sphinx [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] ignore = F403,F812,F821 show-source = True exclude = .venv*,.tox,dist,*egg,build,.git,doc,*lib/python*,.update-venv [hacking] import_exceptions = six.moves,glanceclient._i18n python-glanceclient-2.9.1/CONTRIBUTING.rst0000666000175100017510000000107613232161651020165 0ustar zuulzuul00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/python-glanceclient python-glanceclient-2.9.1/python_glanceclient.egg-info/0000775000175100017510000000000013232162126023237 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/python_glanceclient.egg-info/requires.txt0000664000175100017510000000026313232162125025637 0ustar zuulzuul00000000000000pbr!=2.1.0,>=2.0.0 PrettyTable<0.8,>=0.7.1 keystoneauth1>=3.3.0 requests>=2.14.2 warlock<2,>=1.2.0 six>=1.10.0 oslo.utils>=3.33.0 oslo.i18n>=3.15.3 wrapt>=1.7.0 pyOpenSSL>=16.2.0 python-glanceclient-2.9.1/python_glanceclient.egg-info/not-zip-safe0000664000175100017510000000000113232162102025457 0ustar zuulzuul00000000000000 python-glanceclient-2.9.1/python_glanceclient.egg-info/dependency_links.txt0000664000175100017510000000000113232162125027304 0ustar zuulzuul00000000000000 python-glanceclient-2.9.1/python_glanceclient.egg-info/top_level.txt0000664000175100017510000000001513232162125025764 0ustar zuulzuul00000000000000glanceclient python-glanceclient-2.9.1/python_glanceclient.egg-info/PKG-INFO0000664000175100017510000000753013232162125024340 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: python-glanceclient Version: 2.9.1 Summary: OpenStack Image API Client Library Home-page: https://docs.openstack.org/python-glanceclient/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: Apache License, Version 2.0 Description-Content-Type: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html :alt: The following tags have been asserted for Python bindings to the OpenStack Images API: "project:official", "stable:follows-policy", "vulnerability:managed". Follow the link for an explanation of these tags. .. NOTE(rosmaita): the alt text above will have to be updated when additional tags are asserted for python-glanceclient. (The SVG in the governance repo is updated automatically.) .. Change things from this point on =========================================== Python bindings to the OpenStack Images API =========================================== .. image:: https://img.shields.io/pypi/v/python-glanceclient.svg :target: https://pypi.python.org/pypi/python-glanceclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-glanceclient.svg :target: https://pypi.python.org/pypi/python-glanceclient/ :alt: Downloads This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress. Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. The master repository is in `Git `_. See release notes and more at ``_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.python.org/pypi/python-glanceclient .. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/ .. _Launchpad project: https://launchpad.net/python-glanceclient .. _Blueprints: https://blueprints.launchpad.net/python-glanceclient .. _Bugs: https://bugs.launchpad.net/python-glanceclient .. _Source: https://git.openstack.org/cgit/openstack/python-glanceclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/glance-specs/ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 python-glanceclient-2.9.1/python_glanceclient.egg-info/entry_points.txt0000664000175100017510000000006413232162125026534 0ustar zuulzuul00000000000000[console_scripts] glance = glanceclient.shell:main python-glanceclient-2.9.1/python_glanceclient.egg-info/pbr.json0000664000175100017510000000005613232162125024715 0ustar zuulzuul00000000000000{"git_version": "b982516", "is_release": true}python-glanceclient-2.9.1/python_glanceclient.egg-info/SOURCES.txt0000664000175100017510000001035713232162126025131 0ustar zuulzuul00000000000000.coveragerc .mailmap .testr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst requirements.txt run_tests.sh setup.cfg setup.py test-requirements.txt tox.ini doc/source/conf.py doc/source/index.rst doc/source/cli/details.rst doc/source/cli/glance.rst doc/source/cli/index.rst doc/source/cli/property-keys.rst doc/source/reference/apiv2.rst doc/source/reference/index.rst doc/source/reference/api/index.rst glanceclient/__init__.py glanceclient/_i18n.py glanceclient/client.py glanceclient/exc.py glanceclient/shell.py glanceclient/common/__init__.py glanceclient/common/exceptions.py glanceclient/common/http.py glanceclient/common/https.py glanceclient/common/progressbar.py glanceclient/common/utils.py glanceclient/tests/__init__.py glanceclient/tests/utils.py glanceclient/tests/functional/README.rst glanceclient/tests/functional/__init__.py glanceclient/tests/functional/base.py glanceclient/tests/functional/test_http_headers.py glanceclient/tests/functional/test_readonly_glance.py glanceclient/tests/unit/__init__.py glanceclient/tests/unit/test_base.py glanceclient/tests/unit/test_client.py glanceclient/tests/unit/test_exc.py glanceclient/tests/unit/test_http.py glanceclient/tests/unit/test_progressbar.py glanceclient/tests/unit/test_shell.py glanceclient/tests/unit/test_ssl.py glanceclient/tests/unit/test_utils.py glanceclient/tests/unit/v1/__init__.py glanceclient/tests/unit/v1/test_image_members.py glanceclient/tests/unit/v1/test_images.py glanceclient/tests/unit/v1/test_shell.py glanceclient/tests/unit/v1/test_versions.py glanceclient/tests/unit/v2/__init__.py glanceclient/tests/unit/v2/base.py glanceclient/tests/unit/v2/fixtures.py glanceclient/tests/unit/v2/test_client_requests.py glanceclient/tests/unit/v2/test_images.py glanceclient/tests/unit/v2/test_members.py glanceclient/tests/unit/v2/test_metadefs_namespaces.py glanceclient/tests/unit/v2/test_metadefs_objects.py glanceclient/tests/unit/v2/test_metadefs_properties.py glanceclient/tests/unit/v2/test_metadefs_resource_types.py glanceclient/tests/unit/v2/test_metadefs_tags.py glanceclient/tests/unit/v2/test_schemas.py glanceclient/tests/unit/v2/test_shell_v2.py glanceclient/tests/unit/v2/test_tags.py glanceclient/tests/unit/v2/test_tasks.py glanceclient/tests/unit/v2/test_versions.py glanceclient/tests/unit/var/badcert.crt glanceclient/tests/unit/var/ca.crt glanceclient/tests/unit/var/certificate.crt glanceclient/tests/unit/var/expired-cert.crt glanceclient/tests/unit/var/privatekey.key glanceclient/tests/unit/var/wildcard-certificate.crt glanceclient/tests/unit/var/wildcard-san-certificate.crt glanceclient/v1/__init__.py glanceclient/v1/client.py glanceclient/v1/image_members.py glanceclient/v1/images.py glanceclient/v1/shell.py glanceclient/v1/versions.py glanceclient/v1/apiclient/__init__.py glanceclient/v1/apiclient/base.py glanceclient/v1/apiclient/exceptions.py glanceclient/v1/apiclient/utils.py glanceclient/v2/__init__.py glanceclient/v2/client.py glanceclient/v2/image_members.py glanceclient/v2/image_schema.py glanceclient/v2/image_tags.py glanceclient/v2/images.py glanceclient/v2/metadefs.py glanceclient/v2/namespace_schema.py glanceclient/v2/resource_type_schema.py glanceclient/v2/schemas.py glanceclient/v2/shell.py glanceclient/v2/tasks.py glanceclient/v2/versions.py python_glanceclient.egg-info/PKG-INFO python_glanceclient.egg-info/SOURCES.txt python_glanceclient.egg-info/dependency_links.txt python_glanceclient.egg-info/entry_points.txt python_glanceclient.egg-info/not-zip-safe python_glanceclient.egg-info/pbr.json python_glanceclient.egg-info/requires.txt python_glanceclient.egg-info/top_level.txt releasenotes/notes/.placeholder releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml releasenotes/notes/pike-relnote-2c77b01aa8799f35.yaml releasenotes/notes/return-request-id-to-caller-47f4c0a684b1d88e.yaml releasenotes/source/conf.py releasenotes/source/earlier.rst releasenotes/source/index.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/unreleased.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder tools/fix_ca_bundle.sh tools/glance.bash_completion tools/with_venv.shpython-glanceclient-2.9.1/PKG-INFO0000664000175100017510000000753013232162126016616 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: python-glanceclient Version: 2.9.1 Summary: OpenStack Image API Client Library Home-page: https://docs.openstack.org/python-glanceclient/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: Apache License, Version 2.0 Description-Content-Type: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html :alt: The following tags have been asserted for Python bindings to the OpenStack Images API: "project:official", "stable:follows-policy", "vulnerability:managed". Follow the link for an explanation of these tags. .. NOTE(rosmaita): the alt text above will have to be updated when additional tags are asserted for python-glanceclient. (The SVG in the governance repo is updated automatically.) .. Change things from this point on =========================================== Python bindings to the OpenStack Images API =========================================== .. image:: https://img.shields.io/pypi/v/python-glanceclient.svg :target: https://pypi.python.org/pypi/python-glanceclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-glanceclient.svg :target: https://pypi.python.org/pypi/python-glanceclient/ :alt: Downloads This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress. Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. The master repository is in `Git `_. See release notes and more at ``_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.python.org/pypi/python-glanceclient .. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/ .. _Launchpad project: https://launchpad.net/python-glanceclient .. _Blueprints: https://blueprints.launchpad.net/python-glanceclient .. _Bugs: https://bugs.launchpad.net/python-glanceclient .. _Source: https://git.openstack.org/cgit/openstack/python-glanceclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/glance-specs/ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 python-glanceclient-2.9.1/.mailmap0000666000175100017510000000030113232161651017133 0ustar zuulzuul00000000000000# "man git-shortlog" for reference David Koo python-glanceclient-2.9.1/ChangeLog0000664000175100017510000007631213232162124017275 0ustar zuulzuul00000000000000CHANGES ======= 2.9.1 ----- * Updated from global requirements * Avoid tox\_install.sh for constraints support 2.9.0 ----- * Compare against 'RequestIdProxy.wrapped' * Update Image service property keys doc * Update Image service property keys docs * Updated from global requirements * Revise functional testing README file * Restore functional testing under ssl * Migrate dsvm functional test jobs to project repo * Add domain info to functional test clients * Updated from global requirements * Updated from global requirements * Updated from global requirements * Remove Babel as a runtime requirement * image-create-via-import fails with ValueError * Fix image-import call * Updated from global requirements * stage call fails with TypeError * Update reno for stable/pike * Updated from global requirements 2.8.0 ----- * Add missing docstring * Add image import features to client * Add documentation for image import commands * Add release note for Pike * Update glanceclient version ref * Removed the --no-ssl-compression parameter which is deprecated * Validate input args before trying image download * Make --profile load from environment variables * Updated from global requirements * Remove team:diverse-affiliation from tags * Updated from global requirements * Update and optimize documentation links * Updated from global requirements * help text for container\_format, disk\_format * Updated from global requirements * Fix man page build * turn on warning-is-error in sphinx build * use openstackdocstheme html context * update the doc URLs in the readme * switch to openstackdocstheme * import content from cli-reference in openstack-manuals * move existing content into the new standard structure * add explicit dependency on pyopenssl * Updated from global requirements * allow unhandled exceptions to cause test errors * move old release notes into the releasenotes doc tree * Enable code coverage report in console output * Replace assertTrue(isinstance()) with assertIsInstance() * Updated from global requirements * Updated from global requirements 2.7.0 ----- * Allow global\_request\_id in Client constructor * Downloading image with --progress fails * Updated from global requirements * v2: Content-Type: application/octet-stream header always added * Convert IOError from requests * Downloading image with --progress fails for python3 * Updated from global requirements * Updated from global requirements * gitignore: Ignore auto-generated docs * doc: Remove cruft from conf.py * Use Sphinx 1.5 warning-is-error * Explicitly set 'builders' option * Updated from global requirements * Updated from global requirements * Remove references to Python 3.4 * Remove log translations * Updated from global requirements * Updated from global requirements * Update test requirement * Updated from global requirements * x-openstack-request-id logged twice in logs * Updated from global requirements * Updated from global requirements * Replace six.iteritems() with .items() * Replace functions 'dict.get' and 'del' with 'dict.pop' * Update reno for stable/ocata * Fix 'UnicodeEncodeError' for unicode values in url 2.6.0 ----- * Updated from global requirements * Add request id to returned objects * Add ploop in disk\_format * Updated from global requirements * Handle formatting of subcommand name in error output * Add vhdx in disk\_format * Replace dict.iteritems() with dict.items() * Updated from global requirements * Use import\_versioned\_module from oslo.utils * Updated from global requirements * Add alt text for badges * Show team and repo badges on README * Add support for community images * Updated from global requirements * Move old oslo-incubator code out of openstack/common * Updated from global requirements * Enable release notes translation * Add Apache 2.0 license to source file * Updated from global requirements * Remove unused \_i18n.py shim * Updated from global requirements * Replace 'assertTrue(a not in b)' with 'assertNotIn(a, b)' * Updated from global requirements * Updated from global requirements * standardize release note page ordering * Updated from global requirements * switch from keystoneclient to keystoneauth * Improve tools/tox\_install.sh * Use constraints everywhere * Update reno for stable/newton 2.5.0 ----- * Updated from global requirements * Revert "Don't update tags every time" * Update doc URL 2.4.0 ----- * Updated from global requirements * Updated from global requirements 2.3.0 ----- * Updated from global requirements * Remove unused openstack/common/apiclient/client * Replace OpenStack LLC with OpenStack Foundation * py3: Fix encoding and use sys.stdin.buffer * Remove discover from test-requirements * Updated from global requirements * Log request-id before exceptions raised * Fix string interpolation to delayed to be handled by the logging code 2.2.0 ----- * Add comment about workaround for py3 * Properly build releasenotes * Update docs URL * Add Python 3.5 classifier and venv * Fix warlock model creation * Updated from global requirements * image-download: tests to catch stray output * Use correct order of arguments to assertEqual * Updated from global requirements * Update outdated image shema * Replace tempest\_lib with tempest.lib * Log request-id for each api call * Updated from global requirements * Updated from global requirements 2.1.0 ----- * Updated from global requirements * Remove deprecated construct method from session init * Add upper constraints to glanceclient * Remove unused skip\_authentication decorator * Don't update tags every time * Updated from global requirements * Fixed grammar in image-download command description * Updated from global requirements * Updated from global requirements * Updated from global requirements * Revert "Add last\_request\_id member to HTTPClient and SessionClient" * [Trivial] Remove unnecessary executable privilege * Fix "Codec can't encode characters" * Updated from global requirements * Update the home-page with developer documentation * Get endpoint if os\_image\_url is not set * Updated from global requirements * Corrected wrong parameter in docstring * Updated from global requirements * Enable hacking checks * Fix typos in docstrings and comments * Updated from global requirements * Re-enable stacktracing when --debug is used * Add last\_request\_id member to HTTPClient and SessionClient * Fix v2 so that you can see the default help info * Update auth\_token before sending request * Fix missing of debug info after we use session * Ship the default metadata schema in the client * Docs are generated incorrectly * Fix location update * Catch InUseByStore case in do\_image\_delete * Update reno for stable/mitaka 2.0.0 ----- * Handle 403 forbidden on download * Test: use assert\_has\_calls() instead * Updated from global requirements * trival: fix a typo in comment * Auto-generated squash commit * Add reno to glanceclient * Updated from global requirements * v2 - "readOnly" key should be used in schemas * Fix client initialization in shell tests * Remove code needed for python2.5 * Fix warnings in glanceclient README * Updated from global requirements * Remove argparse from requirements * Change metavar for location commands in V2 * Updated from global requirements * Updated from global requirements * Enhance description of instance-uuid option for image-create * Updated from global requirements * Remove monkey-patching for getsockopt * Fixed TestHTTPSVerifyCert failure messages * Remove location check from V2 client * Use session when not specified token or endpoint * Updated from global requirements * Skip schema validation on GET /v2/images/%s * Trival: Remove 'MANIFEST.in' * Remove openstack-common.conf * Updated from global requirements * Drop py33 support * Change assertTrue(isinstance()) by optimal assert * Add help the ability to sort images with multiple keys * use keystoneclient exceptions instead of oslo-incubator code * Add docker to image\_schema on glance v2 cli * remove python 2.6 trove classifier * Fix image-download to stdout on Python 3.x * Updated from global requirements * replace the deprecated keystoneclient...apiclient * Replace assertEqual(None, \*) with assertIsNone in tests * Removes MANIFEST.in as it is not needed explicitely by PBR * Updated from global requirements * Remove broken try/except workaround for old requests * Remove py26 support * Deprecated tox -downloadcache option removed * Updated from global requirements * Disable suggestion of v1 help for v2 commands * Remove pypy env from tox * Fix the download error when the image locations are blank * Updated from global requirements * Run py34 env first when launching tests * Updated from global requirements * Fix help for image-create * Fix tests for image-create * Fix Resource.\_\_eq\_\_ mismatch semantics of object equal * Update set of wanted commands in read-only test * Fix typo in 'help' in python-glanceclient * Add ordereddict requirement for py26 tests * Updated from global requirements * Ensure that identity token in header is not an unicode string * Change man page examples to correlate default v2 1.2.0 ----- * Release Notes for version 1.2.0 * Fix 'test\_help' for shell client * Add versions list function * Added reactivate/deactivate image using CLI * Fix the missing help descripiton of "image-create" * Add documentation for running the functional tests * Updated from global requirements * Remove unused sphinx Makefile * Update docs to recommend KSA instead of KSC * Use clouds.yaml from devstack for functional tests * Add translation to v2 shell * Import i18n functions directly * Updated from global requirements * improve readme contents * Add support for setting Accept-Language header * Updated from global requirements * Use the subcomand parsed args instead of the base * Stop trying to send image\_size to the server * Support image deletion in batches in v2 * print usage when no argument is specified for python3 * Updated from global requirements * Remove self from image-create/image-update * Updated from global requirements * Do not use openstack.common.i18n in glance client * Added unit tests for 'Unicode support shell client' * Use common identity parameters fro keystone client * No auth when token and endpoint are passed * Use dictionary literal for dictionary creation * Replace exception\_to\_str with oslo.utils function * Change ignore-errors to ignore\_errors * Add period in help message * Don't get the image before deleting it * Fix human readable when size is None 1.1.0 ----- * Change next version in docs * 1.0.1 Release notes * Fixes CLI client called without subcommands * Print the reverting back to v1 to stderr * Updates default --sort behaviour * Invalid output running the command 'glance image-show ' * Don't make \`help\` require auth parameters * Add parsing the endpoint URL * check for None value in utils.safe\_header * Updated from global requirements * Consider \`--os-token\` when using v2 * Check if v2 is available and fallback * Update path to subunit2html in post\_test\_hook * Fix the remove property logic in V2 * Password should be prompted once 1.0.0 ----- * 1.0.0 release notes * Fix Typos in comments * Remove custom SSL compression handling * Add more information show in v2 * Require disk and container format on image-create * Ship the default image schema in the client * Use API v2 as default * V2: Do not validate image schema when listing * Updated from global requirements * Add unicode support for properties values in v2 shell * Fix failure to create glance https connection pool * Add check Identity validate when get schemas * Enable flake8 checks * Extend unittests coverage for v2 tasks module * Support for Metadata Definition Catalog for Tags * Fix exception message in Http.py * Fix an issue with broken test on ci * Remove usage of assert\_called\_once on Mock objects * Add .eggs/\* to .gitignore * Updated from global requirements * Account for dictionary order in test\_shell.py * Do not fall back to namespaced oslo.i18n * Updated from global requirements * Add v2 support for the marker attribute * Import only modules and update tox.ini * Updated from global requirements * Close iterables at the end of iteration * Make glanceclient accept a session object * cleanup openstack-common.conf and sync updated files 0.19.0 ------ * Add release notes for 0.19.0 * Updated from global requirements * Include owner and status option in v2 image list * Fix Metadef Object update issue with python-glanceclient * Fix functional tests in gate * Do not crash on homedir mkdir * Improve import related error handling * Add parameter 'changes-since' for image-list of v1 * Check image-download for redirection * Add some basic CLI functional tests * Use assertIn instead of assertTrue in tests * Unorder compare in tests * Add release notes for 0.18.0 * Update README to work with release tools * Create functional test base * Move unit tests to standard directory 0.18.0 ------ * Uncap library requirements for liberty * Add unit tests for log\_curl\_request * Fix https stack trace on python 3.4 client * Fix client when using no ssl compression * Add SSL cert verification regression tests * Omit 'locations' as image-create parameter * Creating task with invalid property crashes in py3 * Don't accept \*args for client * Stub authentication requests rather than plugins * Remove keystoneclient mocks * Replace mox in tests with requests-mock * Expose 'is\_base' schema property attribute * Validate tag name when filtering for images * Remove redundant FakeSchemaAPI \_\_init\_\_ method * glance image-show now have --human-readable option * Test unit for checking update active images * Correct help messages for image-update command 0.17.0 ------ * Add release notes for 0.17.0 * Updated help for v2 member-update api * Extend images CLI v2 with new sorting syntax * Add the ability to specify the sort dir for each key * Import sys module * Adds the ability to sort images with multiple keys * add examples for properties and doc build script * Apply expected patch format when updating tags in v2.images * v2: read limit for list from --limit in shell * Fix leaking sockets after v2 list operation * Fix leaking sockets after v1 list operation 0.16.1 ------ * Add release notes for 0.16.1 * removed excessive call to os.path.exists * Fix tests failing if keystone is running locally * Unify using six.moves.range rename everywhere 0.16.0 ------ * Add release notes for 0.16.0 * Show error on trying to upload to non-queued image * https: Prevent leaking sockets for some operations * Glance image delete output * Strip json and html from error messages * Generate API documentation * Unit tests covering missing username or password * Register our own ConnectionPool without globals * Updated from global requirements * Change oslo.utils to oslo\_utils * Return 130 for keyboard interrupt * Ignore NoneType when encoding headers * Remove graduated gettextutils from openstack/common * Use utils.exit rather than print+sys.exit * Remove uuidutils from openstack-common * Add a \`--limit\` parameter to list operations * Fixed CLI help for bash-completion * Remove openstack.common.importutils * Remove openstack.common.strutils * Adds basic examples of v2 API usage * Sync latest apiclient from oslo-inc * Remove duplicate 'a' in the help string of --os-image-url * Close streamed requests explicitly * Handle HTTP byte returns in python 3 * Updated from global requirements * Add validation to --property-filter in v1 shell * v2: Allow upload from stdin on image-create * Fix v2 image create --file documentation * Make non-boolean check strict * Disable progress bar if image is piped into client * Fix Requests breaking download progress bar * Fix broken-pipe seen in glance-api * Update HTTPS certificate handling for pep-0476 0.15.0 ------ * Add release notes for 0.15.0 * Support Pagination for namespace list * Output clear error message on invalid api version * Support schema types with non-str value * Don't require version to create Client instance * Add os\_ prefix to project\_domain\_name/id * Workflow documentation is now in infra-manual * Use any instead of False in generator * Allow --file in image-create with v2 Image API * Add useful error on invalid --os-image-api-version * Add release notes for 0.14.0 - 0.14.2 * Fix minor typo in version error message * Send \`identity\_headers\` through the wire * Curl statements to include globoff for IPv6 URLs * Remove readonly options from v2 shell commands * Add --property-filter option to v2 image-list * Fix py34 failure for glance client 0.14.2 ------ * Don't set X-Auth-Token key in http session header if no token provided * Don't replace the https handler in the poolmanager * Refactor method of constructing dicts in some tests * Adds tty password entry for glanceclient * Fixed doc example * '--public' ignored on image create * Remove network\_utils * Skip non-base properties in patch method * Adds support for Glance Tasks calls * Reduce the set of supported client SSL ciphers * Fix the ordering of assertEqual arguments 0.14.1 ------ * Update how tokens are redacted * Handle UnicodeDecodeError in log\_http\_response * Print traceback to stderr if --debug is set * Stop using intersphinx * Updated from global requirements * Fix v2 requests to non-bleeding edge servers * Fix to ensure endpoint\_type is used by \_get\_endpoint() * Work toward Python 3.4 support and testing 0.14.0 ------ * Support for Metadata Definitions Catalog API * Catch new urllib3 exception: ProtocolError * Default to system CA bundle if no CA certificate is provided * Import missing gettextutils.\_ in shell.py * Fix error when logging http response with python 3 * Fix indentation in tox.ini * Add bash completion to glance client * Ensure server's SSL cert is validated * Enable osprofiler interface in glanceclient shell * Hide stderr noise in test output * Remove deprecated commands from shell * Normalize glanceclient requested service url * Fix glance-client to work with IPv6 controllers * Add support for Keystone v3 * Downgrade log message for http request failures * Update theme for docs * Add a tox job for generating docs * Don't stream non-binary requests * Use a correctly formatted example location in help * Replace old httpclient with requests * CLI image-update gives a wrong help on '--tags' param * Enable F841 * Resolving the performance issue for image listing of v2 API * Add profiling support to glanceclinet * Use immutable arg rather mutable arg * Add CONTRIBUTING.rst 0.13.1 ------ * Added release notes for 0.13.0 * Add wheels section to the setup.cfg * Add missing classifiers * Add license to setup.cfg * Fix CA certificate handling * Add the six module dependency * Prepend '/' to the delete url for the v2 client * Set purge-props header correctly in image update * Updated from global requirements * Change a debug line to prevent UnicodeDecodeError issue * Add support for location parameters in v2 commands * Convert passed integer values into int in v1 shell * Reuse class Manager from common code * Fix help text in image-create * Python 3: use next(foo) instead of foo.next() * Remove auth token from http logging * Finalize Python3 support * fixed typos found by RETF rules * Updated from global requirements * Remove py3k module * Return request ID to callers * progress flag not supported in v2 API * Fix for invalid literal ValueError parsing ipv6 url(s) * Adding network\_utils module from oslo-incubator * Sync with oslo-incubator * Fix the parameter order of assertEqual in glanceclient v1 test * Sync with Oslo * Python 3: do not use \_\_builtin\_\_ * Change assertTrue(isinstance()) by optimal assert * Updated from global requirements * Python3: do not use the 'file' type * Python 3: do not use the unicode() function * Fix the parameter order of assertEqual in glanceclient v2 test * Improve help strings * Fix the parameter order of assertEqual in glanceclient test * Python3: define a \_\_next\_\_() method for VerboseIteratorWrapper * test\_shell: remove a deprecated keyword argument * Python 3: Fix JsonPatch-related issues * Pass bytes to tempfile.NamedTemporaryFile().write() * Replace file with open, which is Python 3 compatible * Remove tox locale overrides * Fix misspellings in python-glanceclient * Update my mailmap * Add support for image size in v2 api upload * Only show progress bar for local image files * Using common method 'bool\_from\_string' from oslo strutils * Handle endpoints with versions consistently * Allow updating empty created v2 images from v1 * server 500 should not be a client error * It was removed urllib, urllib2 & urlparse modules * python3: Switch to mox3 instead of mox * Remove vim header * Python 3: use six.iteritems and six.string\_types * Python3: use six.StringIO rather than StringIO.StringIO * Python3: use six.StringIO rather than StringIO.StringIO * Replace file.write and os.path.exists by mock * Python 3: use six.iteritems() instead of iteritems() * Python 3: use six.iteritems() instead of iteritems() * Fix glanceclient http.py string formatting error * Reuse Resource from oslo * Get better format for long lines with PrettyTable * Remove unused imports * Sync apiclient and py3kcompat from oslo * Fix and enable gating on H306 * SSL: Handle wildcards in Subject Alternative Names * Updated from global requirements * Replace inheritance hierarchy with composition * Updates tox.ini to use new features * Updates .gitignore * Readd missing Babel dependency after merge from Oslo(master/bdda833) * Fix extra new line that break from progress bar 0.12.0 ------ * Add release notes for 0.12.0 * Make HACKING.rst DRYer * change assertEquals to assertEqual * Fix Pep8 errors found by Pep8 1.4.6 * python3: use six.moves for httplib imports * Sync from oslo-incubator * python3: xrange no longer exists * Fix misused assertTrue in unit tests * Add CLI for V2 image create, update, and upload * Fix regression bug after removing posixpath in http.py * Fix getting header in redirect processing * Fix default value for a header * Replace OpenStack LLC with OpenStack Foundation * Support glance client can get ipv6 image url correctly * Added support for running the tests under PyPy with tox * Enable query image by tag * Fix python 3.x related Hacking warnings * Fix glanceclient usage inconsistences for options * Add 0.11.0 doc notes * Use openstack-images-v2.1-json-patch for update method * Allow single-wildcard SSL common name matching * Revert "removed deprecated parameter --public" * \Allow removal of properties using glance v2 api * Updated from global requirements 0.10.0 ------ * Revert 02116565d358a4fa254217779fef82b14b38d8ca * Add 0.10.0 docs update * Show a pretty progressbar when uploading and downloading an image * Raise warlock requirement * Cast image\_id to string before calling urllib.quote * Don't use posixpath for URLs * Changes to allow image upload with V2 api * removed deprecated parameter --public * Encode error messages before sending them to stdout * Allow v1 client to list all users' images * Add v1 client side owner based filtering * Enable client library V2 to create an image * Provide glance CLI man page * Fix test assertions & test cases for V2 Shell Unit test * HTTPS response issues * Increase default page\_size value * Pass all identity headers received to glance * Fix SSL certificate CNAME checking * uncap python-keystoneclient version requirement * Expose checksum index image property in client * Flake8 should ignore build folder * Enable client V2 to update/delete tags for a given image * Rename invalid domain name to be RFC compliant * Start using Pyflakes and Hacking * Removes extra slash on endpoints without a path * Remove explicit distribute depend * Replace utils.ensure\_(str|unicode) with strutils.safe(decode|encode) * Do not decode headers in v1/images.py * Fix problem where image data is not read from a pipe * Add tests for encodings * python3: Introduce py33 to tox.ini * Rename requires files to standard names * Don't attempt to read stdin if it is empty * Update importutils and openstack-common.conf format * Convert non-ascii characters within image property to unicode * Migrate to pbr * Migrate to flake8 * Add test for glanceclient shells * Improve unit tests for python-glanceclient.glanceclient.common.base * Image Members for glance v2 api * Fix inconsistent --debug messages on image-update * Expand HACKING with commit message guidelines * Prevent WantReadError when using https * Improve Python 3.x compatibility * Sync with oslo-incubator copy of setup.py and version.py * bug 1166263 image-update handling for closed stdin * Test that copy\_from is used properly in old API * Fix "glance add" parsing of "copy\_from" option * Fix problem running glance --version * Improve unit tests for python-glanceclient.glanceclient.common.http 0.9.0 ----- * Add docs for 0.9.0 * Filter images list by public=True|False * Trapping KeyboardInterrupt sooner * Allow for prettytable 0.7.x as well * Implements filters: visibility, owner, member\_status. Includes tests * Add missing spaces in help msg * Control C does not cancel the CLI cleanly * Replace SchemaNotFound with HTTPNotFound * Use getattr properly in legacy shell 0.8.0 ----- * Add docs for v0.8.0 * Report name resolution errors properly * Decode input and encode output * Add library support for v2 image update * Expect minumum warlock version of 0.7.0 * Update to latest oslo-version * Update .coveragerc * Make effective ssl callback behaviour more obvious * Quote image ids before passing them to glance * Fix typo in image-update help page * Adds image-delete functionality * Change https port to be an optional parameter * Migrate to testr * Add image names to glance command arguments * Use testtools instead of unittest * Add details to stdout error message 0.7.0 ----- * Document v0.7.0 release * Support --os-cacert * Update --location help to reference swift store * Change default image sort to use name * Add --sort-key and --sort-dir to image-list * Pin pep8 to 1.3.3 * Allow setting x-image-meta-store through shell on image creation * Verify that host matches certificate 0.6.0 ----- * Document bugs/features for v0.6.0 * Hook up region\_name argument * Simplify human-readable size output * Make image sizes more readable for humans * Set useful boolean flag metavars * Unpin keystoneclient dependency * Fixes bug on Windows related to a wrong API url * Enhance --checksum help with algorithm * added --version as new parameter * Fixes setup compatibility issue on Windows * Allow deletion of multiple images through CLI * Fixes shell command for member-delete * Add OpenStack trove classifier for PyPI * Implement blueprint ssl-connect-rework * Handle create/update of images with unknown size * Display acceptable disk/container formats in help text * Simplify http(s) connection instantiation * Add happy path tests for ResponseBodyIterator * Use full URI path from Glance endpoint in HTTP requests * Typo in image-create help page * Fixes glance add / update / image-create / image-update on Windows * Fix weird "None" displayed on some errors * Make ConnectionRefused error more informative 0.5.1 ----- * Document remaining bug for v0.5.1 * Update docs for v0.5.1 release * Corrects URI to display hostname, port properly * Catches HTTP 300 while printing responses * get\_connection should raise httplib.InvalidURL * Fix PEP8 issues * Specified Content-Length in update request header * Sync importutils changes from openstack-common 0.5.0 ----- * Update release notes for v0.5.0 * Add nosehtmloutput as a test dependency * Update command descriptions * Update pip-requires with warlock<2 * Enable client V1 to download images * Simplify docs and provide 'News' on index.rst 0.4.2 ----- * Ensure v1 'limit' query parameter works correctly 0.4.1 ----- * Allow 'deleted' to be passed through image update * Cast is\_public, protected, deleted to bool * Return known int values as int, not str * Use system CA certificate file 0.4.0 ----- * socket errors and timeouts should be CommunicationErrors * Handle communication failures cleanly * Enable client V2 to download images * Refactor HTTP-related exceptions * Simplify v2 schema lookup * legacy\_shell.py shouldn't be executable * Client-side SSL Connection * SSL Certificate Validation 0.3.0 ----- * Add missing copyright headers * Add legacy compat layer to v1 shell * Allow CLI opts to override auth token and endpoint * Update python-keystoneclient version dependency * Stop looking for v2 image in container 0.2.0 ----- * Add exceptions for 500 and 503 HTTP status codes * Refactor http request/response logging * Fix --debug CLI option * Fix coverage reporting test * Honor '--insecure' commandline flag also for keystone authentication * Replace httplib2 with httplib as http driver * Clarify usage of --insecure flag * Add pagination to v1 image-list * Update README usage examples * Relax prettytable dependency to v0.6.X from v0.6 * Add pagination to v2 image-list * Prevent links from being printed in v2 CLI * Align print\_dict to the left * Convert v2 images list method to generator * Replace static v2 Image model with warlock model * Add support for viewing a single image through v2 * Rewrite link parsing for finding v2 schemas * Establish the supported importable interface * Add --is-public to image-create * Wrap image data in iterator * Translate is\_protected to protected * Change --protected to --is-protected in create * Properly map boolean-like arguments to True/False * Add ability to get version information in python * Latest setup goodness * Remove AuthorizationFailure exception * Preserve image properties on update * Add --file to image-update and correct bad name * Allow image filtering by custom properties * Expand v1 image-list filters * Add --timeout option to cli * Add size filtering to image-list action * Allow image upload from local file to v1 API * Use PyPI for keystoneclient * Switch CLI to support underscores and dashes 0.1.1 ----- * Split reading of versioninfo out into a method * Add support for tag-based version numbers * Support --os-endpoint-type in glanceclient * Hook up GET /v1/images/ 0.1.0 ----- * Add initial docs * Edit build\_sphinx options * Minimize tox.ini * Add 'explain' command to v2 that describes schemas * Stick prettytable at v0.6 * Add tests dir to pep8 command * Set pep8 dependency at v1.2 * Add minimal support for the v2 API * Auto generate AUTHORS file for glanceclient component * Include ChangeLog in tarball * Properly install from zipball * Adds support for --insecure * Fix the zipball change * Replace git url with github zipball * Refactor HTTPClient to use two request methods * Add missing files to MANIFEST.in * Add importutils from openstack-common * Adding service type as configurable shell option * Remove printt * Added condition requirement to simplejson * Use tox for running tests locally * Adds filter support to images.list() * Add '.tox' to .gitignore * Add fields to image-list * Strip version from service catalog endpoint * Fix image-create using pipelines * Allow tenant name to be used in authentication * Make tox cover output coverage.xml * Add Sphinx to test-requires * Updated depend processing to norms * Fixing pep8 errors * Add AUTHORS test case * Added gitreview file * Adding id for image members * image membership management works * Adding support for passing image data through cli * Image update works * More complete image creation * Correct keystoneclient egg name in pip-requires * Adding image-create action * Adding shared-images support * Image members bones * Basic testing * Update version to 2012.2 * Further cleanup * Basic get/list operations work * All the latest OpenStack hotness * Initial checkin for new CLI and client package python-glanceclient-2.9.1/README.rst0000666000175100017510000000477613232161651017225 0ustar zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html :alt: The following tags have been asserted for Python bindings to the OpenStack Images API: "project:official", "stable:follows-policy", "vulnerability:managed". Follow the link for an explanation of these tags. .. NOTE(rosmaita): the alt text above will have to be updated when additional tags are asserted for python-glanceclient. (The SVG in the governance repo is updated automatically.) .. Change things from this point on =========================================== Python bindings to the OpenStack Images API =========================================== .. image:: https://img.shields.io/pypi/v/python-glanceclient.svg :target: https://pypi.python.org/pypi/python-glanceclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-glanceclient.svg :target: https://pypi.python.org/pypi/python-glanceclient/ :alt: Downloads This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress. Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. The master repository is in `Git `_. See release notes and more at ``_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.python.org/pypi/python-glanceclient .. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/ .. _Launchpad project: https://launchpad.net/python-glanceclient .. _Blueprints: https://blueprints.launchpad.net/python-glanceclient .. _Bugs: https://bugs.launchpad.net/python-glanceclient .. _Source: https://git.openstack.org/cgit/openstack/python-glanceclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/glance-specs/ python-glanceclient-2.9.1/requirements.txt0000666000175100017510000000077213232161652021013 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD keystoneauth1>=3.3.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 warlock<2,>=1.2.0 # Apache-2.0 six>=1.10.0 # MIT oslo.utils>=3.33.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 wrapt>=1.7.0 # BSD License pyOpenSSL>=16.2.0 # Apache-2.0 python-glanceclient-2.9.1/.coveragerc0000666000175100017510000000011113232161651017632 0ustar zuulzuul00000000000000[run] branch = True source = glanceclient [report] ignore_errors = True python-glanceclient-2.9.1/.testr.conf0000666000175100017510000000030013232161651017577 0ustar zuulzuul00000000000000[DEFAULT] test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./glanceclient/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list python-glanceclient-2.9.1/AUTHORS0000664000175100017510000002170213232162125016565 0ustar zuulzuul00000000000000Abhishek Kekane Abhishek Talwar Adam Gandelman Alan Meadows Alessandro Pilotti Alessio Ababilov Alex Gaynor Alex Meade Alexander Bashmakov Alexander Tivelkov Alexey Galkin AmalaBasha Andre Naehring Andreas Jaeger Andreas Jaeger Andrew Laski Andrey Kurilin Andy Botting Andy McCrae Anita Kuno Ankit Agrawal Atsushi SAKAI Bhuvan Arumugam Bob Thyne Boris Pavlovic Brad Pokorny Brian Lamar Brian Rosmaita Brian Rosmaita Brian Waldon Cao ShuFeng Cao Xuan Hoang Chang Bo Guo ChangBo Guo(gcb) Chaozhe.Chen Chris Behrens Chris Buccella Chris Yeoh Christian Berendt Chuck Short Cindy Pallares Clark Boylan Corey Bryant Cyril Roelandt Dan Prince Danny Al-Gaaf Dao Cong Tien Darja Shakhray Davanum Srinivas Davanum Srinivas David Edery David Koo David Peraza David Sariel David Wittman Dazhao Dean Troyer Diego Parrilla Dirk Mueller Dominik Heidler Doug Hellmann Doug Hellmann Edward Hope-Morley Eiichi Aikawa Eric Fried Erno Kuvaja Erno Kuvaja Evgeny Antyshev Fei Long Wang Fei Long Wang Flaper Fesp Flavio Percoco Flavio Percoco Flavio Percoco Florian Haas Frederic Lepied Frode Nordahl Gabe Westmaas Gabriel Hurley Georges Dubus Ghe Rivero Gorka Eguileor Haikel Guemar Hangdong Zhang Hugh Saunders Ian Cordasco Ian Cordasco Ian Wienand Ihar Hrachyshka Itisha Dewan Jake Yip Jakub Ruzicka James E. Blair James Li James Page Jamie Lennox Jared Culp Javeme Javier Pena Jay Pipes Jeremy Stanley Jimmy McCrory Joe Gordon John Bresnahan John Trowbridge Jon Bernard Juan Manuel Olle Justin Santa Barbara KATO Tomoyuki Kamil Rykowski Ken'ichi Ohmichi Kevin McDonald Kirill Kyrylo Romanenko Lakshmi N Sampath Lars Gellrich Le Tian Ren Li Wei LiuNanke Long Quan Sha Longgeek Louis Taylor Louis Taylor Luong Anh Tuan M V P Nitesh Maithem Manuel Desbonnet Mark J. Washenberger Mark McLoughlin Markus Zoeller Matt Riedemann Matthew Booth MattieuPuel Michael Basnight Michael Still Michal Dulko Mike Fedosin Monty Taylor Niall Bunting NiallBunting NiallBunting Nicolas Simonds Nikhil Komawar Nikhil Komawar Noboru arai Oleksii Chuprykov Ondřej Nový OpenStack Release Bot Pawel Koniszewski PranaliD Rakesh H S Ravi Jethani Ravi Shekhar Jethani Rob Crittenden Robert Collins Rui Chen Russell Bryant Sabari Kumar Murugesan Sascha Peilicke Sean Dague Sean Dague Sean Dague Shane Wang Shu Muto Shuquan Huang Sirushti Murugesan Stanislaw Pitucha Stephen Finucane Steve Lewis Steve Martinelli Steve Martinelli Stuart McLaren Sudipta Biswas Sulochan Acharya Sushil Kumar Swapnil Kulkarni (coolsvap) Takashi NATSUME Takeaki Matsumoto Tatyana Leontovich Thierry Carrez Thomas Goirand Thomas Leaman Tin Lam Tom Cocozzello Tom Leaman Tovin Seven Travis Tripp Unmesh Gurjar Van Hung Pham Venkatesh Sampath Victor Morales Vincent Untz Vishvananda Ishaya Wayne Okuma Wu Wenxiang Yamini Sardana Yang Yu Yassine Lamgarchal Yvonne Stachowski Zhenguo Niu Zhi Yan Liu ZhiQiang Fan Zhiqiang Fan Zuul amalaba bhagyashris ckonstanski d34dh0r53 dineshbhor eddie-sheffield haobing1 iccha-sethi iccha.sethi isethi jaypipes ji-xuepeng kairat_kushaev lijunbo liuqing llg8212 lrqrun m.benchchaoui@cloudbau.de mouad benchchaoui pawnesh.kumar rahulram rico.lin ricolin ricolin shu-mutou sonu.kumar sridhargaddam venkatamahesh wanghong wangxiyuan wangzhenyu yangds yatin yatin karel zheng yin zhengyao1 zwei python-glanceclient-2.9.1/doc/0000775000175100017510000000000013232162126016261 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/doc/source/0000775000175100017510000000000013232162126017561 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/doc/source/index.rst0000666000175100017510000000063713232161651021434 0ustar zuulzuul00000000000000============================================== Python Bindings for the OpenStack Images API ============================================== This is a client for the OpenStack Images API. There's :doc:`a Python API ` (the :mod:`glanceclient` module) and a :doc:`command-line script ` (installed as :program:`glance`). .. toctree:: :maxdepth: 2 reference/index cli/index python-glanceclient-2.9.1/doc/source/cli/0000775000175100017510000000000013232162126020330 5ustar zuulzuul00000000000000python-glanceclient-2.9.1/doc/source/cli/index.rst0000666000175100017510000000212513232161651022175 0ustar zuulzuul00000000000000============================= Command-line Tool Reference ============================= In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-tenant-id``, and ``--os-auth-url``) or set them in environment variables:: export OS_USERNAME=user export OS_PASSWORD=pass export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b export OS_AUTH_URL=http://auth.example.com:5000/v2.0 The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-image-url`` and ``--os-auth-token``. You can alternatively set these environment variables:: export OS_IMAGE_URL=http://glance.example.org:9292/ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 Once you've configured your authentication parameters, you can run ``glance help`` to see a complete listing of available commands. .. toctree:: details property-keys glance python-glanceclient-2.9.1/doc/source/cli/glance.rst0000666000175100017510000000410313232161651022315 0ustar zuulzuul00000000000000============================== :program:`glance` CLI man page ============================== .. program:: glance .. highlight:: bash SYNOPSIS ======== :program:`glance` [options] [command-options] :program:`glance help` :program:`glance help` DESCRIPTION =========== The :program:`glance` command line utility interacts with OpenStack Images Service (Glance). In order to use the CLI, you must provide your OpenStack username, password, project (historically called tenant), and auth endpoint. You can use configuration options ``--os-username``, ``--os-password``, ``--os-tenant-id``, and ``--os-auth-url`` or set corresponding environment variables:: export OS_USERNAME=user export OS_PASSWORD=pass export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b export OS_AUTH_URL=http://auth.example.com:5000/v2.0 The command line tool will attempt to reauthenticate using provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-image-url`` and ``--os-auth-token`` or by setting corresponding environment variables:: export OS_IMAGE_URL=http://glance.example.org:9292/ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 You can select an API version to use by ``--os-image-api-version`` option or by setting corresponding environment variable:: export OS_IMAGE_API_VERSION=1 Default Images API used is v2. OPTIONS ======= To get a list of available commands and options run:: glance help To get usage and options of a command:: glance help EXAMPLES ======== Get information about image-create command:: glance help image-create See available images:: glance image-list Create new image:: glance image-create --name foo --disk-format=qcow2 \ --container-format=bare --visibility=public \ --file /tmp/foo.img Describe a specific image:: glance image-show BUGS ==== Glance client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-glanceclient/. python-glanceclient-2.9.1/doc/source/cli/details.rst0000666000175100017510000011551313232161665022526 0ustar zuulzuul00000000000000========================================== Image service (glance) command-line client ========================================== The glance client is the command-line interface (CLI) for the Image service (glance) API and its extensions. This chapter documents :command:`glance` version ``2.8.0``. For help on a specific :command:`glance` command, enter: .. code-block:: console $ glance help COMMAND .. _glance_command_usage: glance usage ~~~~~~~~~~~~ .. code-block:: console usage: glance [--version] [-d] [-v] [--get-schema] [-f] [--os-image-url OS_IMAGE_URL] [--os-image-api-version OS_IMAGE_API_VERSION] [--profile HMAC_KEY] [--key-file OS_KEY] [--ca-file OS_CACERT] [--cert-file OS_CERT] [--os-region-name OS_REGION_NAME] [--os-auth-token OS_AUTH_TOKEN] [--os-service-type OS_SERVICE_TYPE] [--os-endpoint-type OS_ENDPOINT_TYPE] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] [--os-auth-type ] [--os-auth-url OS_AUTH_URL] [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] [--os-project-id OS_PROJECT_ID] [--os-project-name OS_PROJECT_NAME] [--os-project-domain-id OS_PROJECT_DOMAIN_ID] [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] [--os-trust-id OS_TRUST_ID] [--os-default-domain-id OS_DEFAULT_DOMAIN_ID] [--os-default-domain-name OS_DEFAULT_DOMAIN_NAME] [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] [--os-user-domain-id OS_USER_DOMAIN_ID] [--os-user-domain-name OS_USER_DOMAIN_NAME] [--os-password OS_PASSWORD] ... .. _glance_command_options: glance optional arguments ~~~~~~~~~~~~~~~~~~~~~~~~~ ``--version`` show program's version number and exit ``-d, --debug`` Defaults to ``env[GLANCECLIENT_DEBUG]``. ``-v, --verbose`` Print more verbose output. ``--get-schema`` Ignores cached copy and forces retrieval of schema that generates portions of the help text. Ignored with API version 1. ``-f, --force`` Prevent select actions from requesting user confirmation. ``--os-image-url OS_IMAGE_URL`` Defaults to ``env[OS_IMAGE_URL]``. If the provided image url contains a version number and \`--os-image-api-version\` is omitted the version of the URL will be picked as the image api version to use. ``--os-image-api-version OS_IMAGE_API_VERSION`` Defaults to ``env[OS_IMAGE_API_VERSION]`` or 2. ``--profile HMAC_KEY`` HMAC key to use for encrypting context data for performance profiling of operation. This key should be the value of HMAC key configured in osprofiler middleware in glance, it is specified in glance configuration file at /etc/glance/glance-api.conf and /etc/glance/glance-registry.conf. Without key the profiling will not be triggered even if osprofiler is enabled on server side. Defaults to ``env[OS_PROFILE]``. ``--key-file OS_KEY`` **DEPRECATED!** Use --os-key. ``--ca-file OS_CACERT`` **DEPRECATED!** Use --os-cacert. ``--cert-file OS_CERT`` **DEPRECATED!** Use --os-cert. ``--os-region-name OS_REGION_NAME`` Defaults to ``env[OS_REGION_NAME]``. ``--os-auth-token OS_AUTH_TOKEN`` Defaults to ``env[OS_AUTH_TOKEN]``. ``--os-service-type OS_SERVICE_TYPE`` Defaults to ``env[OS_SERVICE_TYPE]``. ``--os-endpoint-type OS_ENDPOINT_TYPE`` Defaults to ``env[OS_ENDPOINT_TYPE]``. ``--os-auth-type , --os-auth-plugin `` Authentication type to use .. _glance_explain: glance explain -------------- .. code-block:: console usage: glance explain Describe a specific model. **Positional arguments:** ```` Name of model to describe. .. _glance_image-create: glance image-create ------------------- .. code-block:: console usage: glance image-create [--architecture ] [--protected [True|False]] [--name ] [--instance-uuid ] [--min-disk ] [--visibility ] [--kernel-id ] [--tags [ ...]] [--os-version ] [--disk-format ] [--os-distro ] [--id ] [--owner ] [--ramdisk-id ] [--min-ram ] [--container-format ] [--property ] [--file ] [--progress] Create a new image. **Optional arguments:** ``--architecture `` Operating system architecture as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#architecture ``--protected [True|False]`` If true, image will not be deletable. ``--name `` Descriptive name for the image ``--instance-uuid `` Metadata which can be used to record which instance this image is associated with. (Informational only, does not create an instance snapshot.) ``--min-disk `` Amount of disk space (in GB) required to boot image. ``--visibility `` Scope of image accessibility Valid values: public, private, community, shared ``--kernel-id `` ID of image stored in Glance that should be used as the kernel when booting an AMI-style image. ``--tags [ ...]`` List of strings related to the image ``--os-version `` Operating system version as specified by the distributor ``--disk-format `` Format of the disk Valid values: None, ami, ari, aki, vhd, vhdx, vmdk, raw, qcow2, vdi, iso, ploop ``--os-distro `` Common name of operating system distribution as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#os-distro ``--id `` An identifier for the image ``--owner `` Owner of the image ``--ramdisk-id `` ID of image stored in Glance that should be used as the ramdisk when booting an AMI-style image. ``--min-ram `` Amount of ram (in MB) required to boot image. ``--container-format `` Format of the container Valid values: None, ami, ari, aki, bare, ovf, ova, docker ``--property `` Arbitrary property to associate with image. May be used multiple times. ``--file `` Local file that contains disk image to be uploaded during creation. Alternatively, the image data can be passed to the client via stdin. ``--progress`` Show upload progress bar. .. _glance_image-deactivate: glance image-deactivate ----------------------- .. code-block:: console usage: glance image-deactivate Deactivate specified image. **Positional arguments:** ```` ID of image to deactivate. .. _glance_image-delete: glance image-delete ------------------- .. code-block:: console usage: glance image-delete [ ...] Delete specified image. **Positional arguments:** ```` ID of image(s) to delete. .. _glance_image-download: glance image-download --------------------- .. code-block:: console usage: glance image-download [--file ] [--progress] Download a specific image. **Positional arguments:** ```` ID of image to download. **Optional arguments:** ``--file `` Local file to save downloaded image data to. If this is not specified and there is no redirection the image data will not be saved. ``--progress`` Show download progress bar. .. _glance_image-list: glance image-list ----------------- .. code-block:: console usage: glance image-list [--limit ] [--page-size ] [--visibility ] [--member-status ] [--owner ] [--property-filter ] [--checksum ] [--tag ] [--sort-key {name,status,container_format,disk_format,size,id,created_at,updated_at}] [--sort-dir {asc,desc}] [--sort [:]] List images you can access. **Optional arguments:** ``--limit `` Maximum number of images to get. ``--page-size `` Number of images to request in each paginated request. ``--visibility `` The visibility of the images to display. ``--member-status `` The status of images to display. ``--owner `` Display images owned by . ``--property-filter `` Filter images by a user-defined image property. ``--checksum `` Displays images that match the checksum. ``--tag `` Filter images by a user-defined tag. ``--sort-key {name,status,container_format,disk_format,size,id,created_at,updated_at}`` Sort image list by specified fields. May be used multiple times. ``--sort-dir {asc,desc}`` Sort image list in specified directions. ``--sort [:]`` Comma-separated list of sort keys and directions in the form of [:]. Valid keys: name, status, container_format, disk_format, size, id, created_at, updated_at. OPTIONAL. .. _glance_image-reactivate: glance image-reactivate ----------------------- .. code-block:: console usage: glance image-reactivate Reactivate specified image. **Positional arguments:** ```` ID of image to reactivate. .. _glance_image-show: glance image-show ----------------- .. code-block:: console usage: glance image-show [--human-readable] [--max-column-width ] Describe a specific image. **Positional arguments:** ```` ID of image to describe. **Optional arguments:** ``--human-readable`` Print image size in a human-friendly format. ``--max-column-width `` The max column width of the printed table. .. _glance_image-tag-delete: glance image-tag-delete ----------------------- .. code-block:: console usage: glance image-tag-delete Delete the tag associated with the given image. **Positional arguments:** ```` ID of the image from which to delete tag. ```` Value of the tag. .. _glance_image-tag-update: glance image-tag-update ----------------------- .. code-block:: console usage: glance image-tag-update Update an image with the given tag. **Positional arguments:** ```` Image to be updated with the given tag. ```` Value of the tag. .. _glance_image-update: glance image-update ------------------- .. code-block:: console usage: glance image-update [--architecture ] [--protected [True|False]] [--name ] [--instance-uuid ] [--min-disk ] [--visibility ] [--kernel-id ] [--os-version ] [--disk-format ] [--os-distro ] [--owner ] [--ramdisk-id ] [--min-ram ] [--container-format ] [--property ] [--remove-property key] Update an existing image. **Positional arguments:** ```` ID of image to update. **Optional arguments:** ``--architecture `` Operating system architecture as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#architecture ``--protected [True|False]`` If true, image will not be deletable. ``--name `` Descriptive name for the image ``--instance-uuid `` Metadata which can be used to record which instance this image is associated with. (Informational only, does not create an instance snapshot.) ``--min-disk `` Amount of disk space (in GB) required to boot image. ``--visibility `` Scope of image accessibility Valid values: public, private, community, shared ``--kernel-id `` ID of image stored in Glance that should be used as the kernel when booting an AMI-style image. ``--os-version `` Operating system version as specified by the distributor ``--disk-format `` Format of the disk Valid values: None, ami, ari, aki, vhd, vhdx, vmdk, raw, qcow2, vdi, iso, ploop ``--os-distro `` Common name of operating system distribution as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#os-distro ``--owner `` Owner of the image ``--ramdisk-id `` ID of image stored in Glance that should be used as the ramdisk when booting an AMI-style image. ``--min-ram `` Amount of ram (in MB) required to boot image. ``--container-format `` Format of the container Valid values: None, ami, ari, aki, bare, ovf, ova, docker ``--property `` Arbitrary property to associate with image. May be used multiple times. ``--remove-property`` key Name of arbitrary property to remove from the image. .. _glance_image-upload: glance image-upload ------------------- .. code-block:: console usage: glance image-upload [--file ] [--size ] [--progress] Upload data for a specific image. **Positional arguments:** ```` ID of image to upload data to. **Optional arguments:** ``--file `` Local file that contains disk image to be uploaded. Alternatively, images can be passed to the client via stdin. ``--size `` Size in bytes of image to be uploaded. Default is to get size from provided data object but this is supported in case where size cannot be inferred. ``--progress`` Show upload progress bar. .. _glance_import-info: glance import-info ------------------ .. code-block:: console usage: glance import-info Prints the import methods available from Glance, or a message if the target Glance does not support image import. .. _glance_image-stage: glance image-stage ------------------ .. code-block:: console usage: glance image-stage [--file ] [--size ] [--progress] Upload data for a specific image to staging. **Positional arguments:** ```` ID of image to upload data to. **Optional arguments:** ``--file `` Local file that contains disk image to be uploaded. Alternatively, images can be passed to the client via stdin. ``--size `` Size in bytes of image to be uploaded. Default is to get size from provided data object but this is supported in case where size cannot be inferred. ``--progress`` Show upload progress bar. .. _glance_image-import: glance image-import ------------------- .. code-block:: console usage: glance image-import [--import-method ] Initiate the image import taskflow. **Positional arguments:** ```` ID of image to import. **Optional arguments:** ``--import-method `` Import method used for Image Import workflow. Valid values can be retrieved with import-info command and the default "glance-direct" is used with "image-stage". .. _glance_image-create-via-import: glance image-create-via-import ------------------------------ This is an **EXPERIMENTAL** command. It may be renamed or removed in future releases. .. code-block:: console usage: glance image-create-via import [--architecture ] [--protected [True|False]] [--name ] [--instance-uuid ] [--min-disk ] [--visibility ] [--kernel-id ] [--tags [ ...]] [--os-version ] [--disk-format ] [--os-distro ] [--id ] [--owner ] [--ramdisk-id ] [--min-ram ] [--container-format ] [--property ] [--file ] [--progress] Create a new image using the image import process. **NOTE** This is an EXPERIMENTAL command. It may be renamed or removed in future releases. **Optional arguments:** ``--architecture `` Operating system architecture as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#architecture ``--protected [True|False]`` If true, image will not be deletable. ``--name `` Descriptive name for the image ``--instance-uuid `` Metadata which can be used to record which instance this image is associated with. (Informational only, does not create an instance snapshot.) ``--min-disk `` Amount of disk space (in GB) required to boot image. ``--visibility `` Scope of image accessibility Valid values: public, private, community, shared ``--kernel-id `` ID of image stored in Glance that should be used as the kernel when booting an AMI-style image. ``--tags [ ...]`` List of strings related to the image ``--os-version `` Operating system version as specified by the distributor ``--disk-format `` Format of the disk Valid values: None, ami, ari, aki, vhd, vhdx, vmdk, raw, qcow2, vdi, iso, ploop ``--os-distro `` Common name of operating system distribution as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#os-distro ``--id `` An identifier for the image ``--owner `` Owner of the image ``--ramdisk-id `` ID of image stored in Glance that should be used as the ramdisk when booting an AMI-style image. ``--min-ram `` Amount of ram (in MB) required to boot image. ``--container-format `` Format of the container Valid values: None, ami, ari, aki, bare, ovf, ova, docker ``--property `` Arbitrary property to associate with image. May be used multiple times. ``--file `` Local file that contains disk image to be uploaded during creation. Alternatively, the image data can be passed to the client via stdin. ``--progress`` Show upload progress bar. .. _glance_location-add: glance location-add ------------------- .. code-block:: console usage: glance location-add --url [--metadata ] Add a location (and related metadata) to an image. **Positional arguments:** ```` ID of image to which the location is to be added. **Optional arguments:** ``--url `` URL of location to add. ``--metadata `` Metadata associated with the location. Must be a valid JSON object (default: {}) .. _glance_location-delete: glance location-delete ---------------------- .. code-block:: console usage: glance location-delete --url Remove locations (and related metadata) from an image. **Positional arguments:** ```` ID of image whose locations are to be removed. **Optional arguments:** ``--url `` URL of location to remove. May be used multiple times. .. _glance_location-update: glance location-update ---------------------- .. code-block:: console usage: glance location-update --url [--metadata ] Update metadata of an image's location. **Positional arguments:** ```` ID of image whose location is to be updated. **Optional arguments:** ``--url `` URL of location to update. ``--metadata `` Metadata associated with the location. Must be a valid JSON object (default: {}) .. _glance_md-namespace-create: glance md-namespace-create -------------------------- .. code-block:: console usage: glance md-namespace-create [--schema ] [--created-at ] [--resource-type-associations [ ...]] [--protected [True|False]] [--self ] [--display-name ] [--owner ] [--visibility ] [--updated-at ] [--description ] Create a new metadata definitions namespace. **Positional arguments:** ```` Name of the namespace. **Optional arguments:** ``--schema `` ``--created-at `` Date and time of namespace creation. ``--resource-type-associations [...]`` ``--protected [True|False]`` If true, namespace will not be deletable. ``--self `` ``--display-name `` The user friendly name for the namespace. Used by UI if available. ``--owner `` Owner of the namespace. ``--visibility `` Scope of namespace accessibility. Valid values: public, private ``--updated-at `` Date and time of the last namespace modification. ``--description `` Provides a user friendly description of the namespace. .. _glance_md-namespace-delete: glance md-namespace-delete -------------------------- .. code-block:: console usage: glance md-namespace-delete Delete specified metadata definitions namespace with its contents. **Positional arguments:** ```` Name of namespace to delete. .. _glance_md-namespace-import: glance md-namespace-import -------------------------- .. code-block:: console usage: glance md-namespace-import [--file ] Import a metadata definitions namespace from file or standard input. **Optional arguments:** ``--file `` Path to file with namespace schema to import. Alternatively, namespaces schema can be passed to the client via stdin. .. _glance_md-namespace-list: glance md-namespace-list ------------------------ .. code-block:: console usage: glance md-namespace-list [--resource-types ] [--visibility ] [--page-size ] List metadata definitions namespaces. **Optional arguments:** ``--resource-types `` Resource type to filter namespaces. ``--visibility `` Visibility parameter to filter namespaces. ``--page-size `` Number of namespaces to request in each paginated request. .. _glance_md-namespace-objects-delete: glance md-namespace-objects-delete ---------------------------------- .. code-block:: console usage: glance md-namespace-objects-delete Delete all metadata definitions objects inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-properties-delete: glance md-namespace-properties-delete ------------------------------------- .. code-block:: console usage: glance md-namespace-properties-delete Delete all metadata definitions property inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-resource-type-list: glance md-namespace-resource-type-list -------------------------------------- .. code-block:: console usage: glance md-namespace-resource-type-list List resource types associated to specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-show: glance md-namespace-show ------------------------ .. code-block:: console usage: glance md-namespace-show [--resource-type ] [--max-column-width ] Describe a specific metadata definitions namespace. Lists also the namespace properties, objects and resource type associations. **Positional arguments:** ```` Name of namespace to describe. **Optional arguments:** ``--resource-type `` Applies prefix of given resource type associated to a namespace to all properties of a namespace. ``--max-column-width `` The max column width of the printed table. .. _glance_md-namespace-tags-delete: glance md-namespace-tags-delete ------------------------------- .. code-block:: console usage: glance md-namespace-tags-delete Delete all metadata definitions tags inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-update: glance md-namespace-update -------------------------- .. code-block:: console usage: glance md-namespace-update [--created-at ] [--protected [True|False]] [--namespace ] [--self ] [--display-name ] [--owner ] [--visibility ] [--updated-at ] [--description ] Update an existing metadata definitions namespace. **Positional arguments:** ```` Name of namespace to update. **Optional arguments:** ``--created-at `` Date and time of namespace creation. ``--protected [True|False]`` If true, namespace will not be deletable. ``--namespace `` The unique namespace text. ``--self `` ``--display-name `` The user friendly name for the namespace. Used by UI if available. ``--owner `` Owner of the namespace. ``--visibility `` Scope of namespace accessibility. Valid values: public, private ``--updated-at `` Date and time of the last namespace modification. ``--description `` Provides a user friendly description of the namespace. .. _glance_md-object-create: glance md-object-create ----------------------- .. code-block:: console usage: glance md-object-create --name --schema Create a new metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object will belong. **Optional arguments:** ``--name `` Internal name of an object. ``--schema `` Valid JSON schema of an object. .. _glance_md-object-delete: glance md-object-delete ----------------------- .. code-block:: console usage: glance md-object-delete Delete a specific metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. .. _glance_md-object-list: glance md-object-list --------------------- .. code-block:: console usage: glance md-object-list List metadata definitions objects inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-object-property-show: glance md-object-property-show ------------------------------ .. code-block:: console usage: glance md-object-property-show [--max-column-width ] Describe a specific metadata definitions property inside an object. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. ```` Name of a property. **Optional arguments:** ``--max-column-width `` The max column width of the printed table. .. _glance_md-object-show: glance md-object-show --------------------- .. code-block:: console usage: glance md-object-show [--max-column-width ] Describe a specific metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. **Optional arguments:** ``--max-column-width `` The max column width of the printed table. .. _glance_md-object-update: glance md-object-update ----------------------- .. code-block:: console usage: glance md-object-update [--name ] [--schema ] Update metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. **Optional arguments:** ``--name `` New name of an object. ``--schema `` Valid JSON schema of an object. .. _glance_md-property-create: glance md-property-create ------------------------- .. code-block:: console usage: glance md-property-create --name --title --schema <SCHEMA> <NAMESPACE> Create a new metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property will belong. **Optional arguments:** ``--name <NAME>`` Internal name of a property. ``--title <TITLE>`` Property name displayed to the user. ``--schema <SCHEMA>`` Valid JSON schema of a property. .. _glance_md-property-delete: glance md-property-delete ------------------------- .. code-block:: console usage: glance md-property-delete <NAMESPACE> <PROPERTY> Delete a specific metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property belongs. ``<PROPERTY>`` Name of a property. .. _glance_md-property-list: glance md-property-list ----------------------- .. code-block:: console usage: glance md-property-list <NAMESPACE> List metadata definitions properties inside a specific namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. .. _glance_md-property-show: glance md-property-show ----------------------- .. code-block:: console usage: glance md-property-show [--max-column-width <integer>] <NAMESPACE> <PROPERTY> Describe a specific metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property belongs. ``<PROPERTY>`` Name of a property. **Optional arguments:** ``--max-column-width <integer>`` The max column width of the printed table. .. _glance_md-property-update: glance md-property-update ------------------------- .. code-block:: console usage: glance md-property-update [--name <NAME>] [--title <TITLE>] [--schema <SCHEMA>] <NAMESPACE> <PROPERTY> Update metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property belongs. ``<PROPERTY>`` Name of a property. **Optional arguments:** ``--name <NAME>`` New name of a property. ``--title <TITLE>`` Property name displayed to the user. ``--schema <SCHEMA>`` Valid JSON schema of a property. .. _glance_md-resource-type-associate: glance md-resource-type-associate --------------------------------- .. code-block:: console usage: glance md-resource-type-associate [--updated-at <UPDATED_AT>] [--name <NAME>] [--properties-target <PROPERTIES_TARGET>] [--prefix <PREFIX>] [--created-at <CREATED_AT>] <NAMESPACE> Associate resource type with a metadata definitions namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. **Optional arguments:** ``--updated-at <UPDATED_AT>`` Date and time of the last resource type association modification. ``--name <NAME>`` Resource type names should be aligned with Heat resource types whenever possible: https://docs.openstack.org/heat/latest/template_guide/openstack.html ``--properties-target <PROPERTIES_TARGET>`` Some resource types allow more than one key / value pair per instance. For example, Cinder allows user and image metadata on volumes. Only the image properties metadata is evaluated by Nova (scheduling or drivers). This property allows a namespace target to remove the ambiguity. ``--prefix <PREFIX>`` Specifies the prefix to use for the given resource type. Any properties in the namespace should be prefixed with this prefix when being applied to the specified resource type. Must include prefix separator (e.g. a colon :). ``--created-at <CREATED_AT>`` Date and time of resource type association. .. _glance_md-resource-type-deassociate: glance md-resource-type-deassociate ----------------------------------- .. code-block:: console usage: glance md-resource-type-deassociate <NAMESPACE> <RESOURCE_TYPE> Deassociate resource type with a metadata definitions namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. ``<RESOURCE_TYPE>`` Name of resource type. .. _glance_md-resource-type-list: glance md-resource-type-list ---------------------------- .. code-block:: console usage: glance md-resource-type-list List available resource type names. .. _glance_md-tag-create: glance md-tag-create -------------------- .. code-block:: console usage: glance md-tag-create --name <NAME> <NAMESPACE> Add a new metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace the tag will belong to. **Optional arguments:** ``--name <NAME>`` The name of the new tag to add. .. _glance_md-tag-create-multiple: glance md-tag-create-multiple ----------------------------- .. code-block:: console usage: glance md-tag-create-multiple --names <NAMES> [--delim <DELIM>] <NAMESPACE> Create new metadata definitions tags inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace the tags will belong to. **Optional arguments:** ``--names <NAMES>`` A comma separated list of tag names. ``--delim <DELIM>`` The delimiter used to separate the names (if none is provided then the default is a comma). .. _glance_md-tag-delete: glance md-tag-delete -------------------- .. code-block:: console usage: glance md-tag-delete <NAMESPACE> <TAG> Delete a specific metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace to which the tag belongs. ``<TAG>`` Name of the tag. .. _glance_md-tag-list: glance md-tag-list ------------------ .. code-block:: console usage: glance md-tag-list <NAMESPACE> List metadata definitions tags inside a specific namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. .. _glance_md-tag-show: glance md-tag-show ------------------ .. code-block:: console usage: glance md-tag-show <NAMESPACE> <TAG> Describe a specific metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace to which the tag belongs. ``<TAG>`` Name of the tag. .. _glance_md-tag-update: glance md-tag-update -------------------- .. code-block:: console usage: glance md-tag-update --name <NAME> <NAMESPACE> <TAG> Rename a metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace to which the tag belongs. ``<TAG>`` Name of the old tag. **Optional arguments:** ``--name <NAME>`` New name of the new tag. .. _glance_member-create: glance member-create -------------------- .. code-block:: console usage: glance member-create <IMAGE_ID> <MEMBER_ID> Create member for a given image. **Positional arguments:** ``<IMAGE_ID>`` Image with which to create member. ``<MEMBER_ID>`` Tenant to add as member. .. _glance_member-delete: glance member-delete -------------------- .. code-block:: console usage: glance member-delete <IMAGE_ID> <MEMBER_ID> Delete image member. **Positional arguments:** ``<IMAGE_ID>`` Image from which to remove member. ``<MEMBER_ID>`` Tenant to remove as member. .. _glance_member-list: glance member-list ------------------ .. code-block:: console usage: glance member-list --image-id <IMAGE_ID> Describe sharing permissions by image. **Optional arguments:** ``--image-id <IMAGE_ID>`` Image to display members of. .. _glance_member-update: glance member-update -------------------- .. code-block:: console usage: glance member-update <IMAGE_ID> <MEMBER_ID> <MEMBER_STATUS> Update the status of a member for a given image. **Positional arguments:** ``<IMAGE_ID>`` Image from which to update member. ``<MEMBER_ID>`` Tenant to update. ``<MEMBER_STATUS>`` Updated status of member. Valid Values: accepted, rejected, pending .. _glance_task-create: glance task-create ------------------ .. code-block:: console usage: glance task-create [--type <TYPE>] [--input <STRING>] Create a new task. **Optional arguments:** ``--type <TYPE>`` Type of Task. Please refer to Glance schema or documentation to see which tasks are supported. ``--input <STRING>`` Parameters of the task to be launched .. _glance_task-list: glance task-list ---------------- .. code-block:: console usage: glance task-list [--sort-key {id,type,status}] [--sort-dir {asc,desc}] [--page-size <SIZE>] [--type <TYPE>] [--status <STATUS>] List tasks you can access. **Optional arguments:** ``--sort-key {id,type,status}`` Sort task list by specified field. ``--sort-dir {asc,desc}`` Sort task list in specified direction. ``--page-size <SIZE>`` Number of tasks to request in each paginated request. ``--type <TYPE>`` Filter tasks to those that have this type. ``--status <STATUS>`` Filter tasks to those that have this status. .. _glance_task-show: glance task-show ---------------- .. code-block:: console usage: glance task-show <TASK_ID> Describe a specific task. **Positional arguments:** ``<TASK_ID>`` ID of task to describe. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/cli/property-keys.rst������������������������������������������0000666�0001751�0001751�00000041443�13232161651�023731� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=========================== Image service property keys =========================== The following keys, together with the components to which they are specific, can be used with the property option for both the :command:`openstack image set` and :command:`openstack image create` commands. For example: .. code-block:: console $ openstack image set IMG-UUID --property architecture=x86_64 .. note:: Behavior set using image properties overrides behavior set using flavors. For more information, refer to the `Manage images <https://docs.openstack.org/glance/latest/admin/manage-images.html>`_ in the OpenStack Administrator Guide. .. list-table:: Image service property keys :widths: 15 35 50 90 :header-rows: 1 * - Specific to - Key - Description - Supported values * - All - ``architecture`` - The CPU architecture that must be supported by the hypervisor. For example, ``x86_64``, ``arm``, or ``ppc64``. Run :command:`uname -m` to get the architecture of a machine. We strongly recommend using the architecture data vocabulary defined by the `libosinfo project <http://libosinfo.org/>`_ for this purpose. - * ``alpha`` - `DEC 64-bit RISC <https://en.wikipedia.org/wiki/DEC_Alpha>`_ * ``armv7l`` - `ARM Cortex-A7 MPCore <https://en.wikipedia.org/wiki/ARM_architecture>`_ * ``cris`` - `Ethernet, Token Ring, AXis—Code Reduced Instruction Set <https://en.wikipedia.org/wiki/ETRAX_CRIS>`_ * ``i686`` - `Intel sixth-generation x86 (P6 micro architecture) <https://en.wikipedia.org/wiki/X86>`_ * ``ia64`` - `Itanium <https://en.wikipedia.org/wiki/Itanium>`_ * ``lm32`` - `Lattice Micro32 <https://en.wikipedia.org/wiki/Milkymist>`_ * ``m68k`` - `Motorola 68000 <https://en.wikipedia.org/wiki/Motorola_68000_family>`_ * ``microblaze`` - `Xilinx 32-bit FPGA (Big Endian) <https://en.wikipedia.org/wiki/MicroBlaze>`_ * ``microblazeel`` - `Xilinx 32-bit FPGA (Little Endian) <https://en.wikipedia.org/wiki/MicroBlaze>`_ * ``mips`` - `MIPS 32-bit RISC (Big Endian) <https://en.wikipedia.org/wiki/MIPS_architecture>`_ * ``mipsel`` - `MIPS 32-bit RISC (Little Endian) <https://en.wikipedia.org/wiki/MIPS_architecture>`_ * ``mips64`` - `MIPS 64-bit RISC (Big Endian) <https://en.wikipedia.org/wiki/MIPS_architecture>`_ * ``mips64el`` - `MIPS 64-bit RISC (Little Endian) <https://en.wikipedia.org/wiki/MIPS_architecture>`_ * ``openrisc`` - `OpenCores RISC <https://en.wikipedia.org/wiki/OpenRISC#QEMU_support>`_ * ``parisc`` - `HP Precision Architecture RISC <https://en.wikipedia.org/wiki/PA-RISC>`_ * parisc64 - `HP Precision Architecture 64-bit RISC <https://en.wikipedia.org/wiki/PA-RISC>`_ * ppc - `PowerPC 32-bit <https://en.wikipedia.org/wiki/PowerPC>`_ * ppc64 - `PowerPC 64-bit <https://en.wikipedia.org/wiki/PowerPC>`_ * ppcemb - `PowerPC (Embedded 32-bit) <https://en.wikipedia.org/wiki/PowerPC>`_ * s390 - `IBM Enterprise Systems Architecture/390 <https://en.wikipedia.org/wiki/S390>`_ * s390x - `S/390 64-bit <https://en.wikipedia.org/wiki/S390x>`_ * sh4 - `SuperH SH-4 (Little Endian) <https://en.wikipedia.org/wiki/SuperH>`_ * sh4eb - `SuperH SH-4 (Big Endian) <https://en.wikipedia.org/wiki/SuperH>`_ * sparc - `Scalable Processor Architecture, 32-bit <https://en.wikipedia.org/wiki/Sparc>`_ * sparc64 - `Scalable Processor Architecture, 64-bit <https://en.wikipedia.org/wiki/Sparc>`_ * unicore32 - `Microprocessor Research and Development Center RISC Unicore32 <https://en.wikipedia.org/wiki/Unicore>`_ * x86_64 - `64-bit extension of IA-32 <https://en.wikipedia.org/wiki/X86>`_ * xtensa - `Tensilica Xtensa configurable microprocessor core <https://en.wikipedia.org/wiki/Xtensa#Processor_Cores>`_ * xtensaeb - `Tensilica Xtensa configurable microprocessor core <https://en.wikipedia.org/wiki/Xtensa#Processor_Cores>`_ (Big Endian) * - All - ``hypervisor_type`` - The hypervisor type. Note that ``qemu`` is used for both QEMU and KVM hypervisor types. - ``hyperv``, ``ironic``, ``lxc``, ``qemu``, ``uml``, ``vmware``, or ``xen``. * - All - ``instance_type_rxtx_factor`` - Optional property allows created servers to have a different bandwidth cap than that defined in the network they are attached to. This factor is multiplied by the ``rxtx_base`` property of the network. The ``rxtx_base`` property defaults to ``1.0``, which is the same as the attached network. This parameter is only available for Xen or NSX based systems. - Float (default value is ``1.0``) * - All - ``instance_uuid`` - For snapshot images, this is the UUID of the server used to create this image. - Valid server UUID * - All - ``img_config_drive`` - Specifies whether the image needs a config drive. - ``mandatory`` or ``optional`` (default if property is not used). * - All - ``kernel_id`` - The ID of an image stored in the Image service that should be used as the kernel when booting an AMI-style image. - Valid image ID * - All - ``os_distro`` - The common name of the operating system distribution in lowercase (uses the same data vocabulary as the `libosinfo project`_). Specify only a recognized value for this field. Deprecated values are listed to assist you in searching for the recognized value. - * ``arch`` - Arch Linux. Do not use ``archlinux`` or ``org.archlinux``. * ``centos`` - Community Enterprise Operating System. Do not use ``org.centos`` or ``CentOS``. * ``debian`` - Debian. Do not use ``Debian` or ``org.debian``. * ``fedora`` - Fedora. Do not use ``Fedora``, ``org.fedora``, or ``org.fedoraproject``. * ``freebsd`` - FreeBSD. Do not use ``org.freebsd``, ``freeBSD``, or ``FreeBSD``. * ``gentoo`` - Gentoo Linux. Do not use ``Gentoo`` or ``org.gentoo``. * ``mandrake`` - Mandrakelinux (MandrakeSoft) distribution. Do not use ``mandrakelinux`` or ``MandrakeLinux``. * ``mandriva`` - Mandriva Linux. Do not use ``mandrivalinux``. * ``mes`` - Mandriva Enterprise Server. Do not use ``mandrivaent`` or ``mandrivaES``. * ``msdos`` - Microsoft Disc Operating System. Do not use ``ms-dos``. * ``netbsd`` - NetBSD. Do not use ``NetBSD`` or ``org.netbsd``. * ``netware`` - Novell NetWare. Do not use ``novell`` or ``NetWare``. * ``openbsd`` - OpenBSD. Do not use ``OpenBSD`` or ``org.openbsd``. * ``opensolaris`` - OpenSolaris. Do not use ``OpenSolaris`` or ``org.opensolaris``. * ``opensuse`` - openSUSE. Do not use ``suse``, ``SuSE``, or `` org.opensuse``. * ``rhel`` - Red Hat Enterprise Linux. Do not use ``redhat``, ``RedHat``, or ``com.redhat``. * ``sled`` - SUSE Linux Enterprise Desktop. Do not use ``com.suse``. * ``ubuntu`` - Ubuntu. Do not use ``Ubuntu``, ``com.ubuntu``, ``org.ubuntu``, or ``canonical``. * ``windows`` - Microsoft Windows. Do not use ``com.microsoft.server`` or ``windoze``. * - All - ``os_version`` - The operating system version as specified by the distributor. - Valid version number (for example, ``11.10``). * - All - ``os_secure_boot`` - Secure Boot is a security standard. When the instance starts, Secure Boot first examines software such as firmware and OS by their signature and only allows them to run if the signatures are valid. For Hyper-V: Images must be prepared as Generation 2 VMs. Instance must also contain ``hw_machine_type=hyperv-gen2`` image property. Linux guests will also require bootloader's digital signature provided as ``os_secure_boot_signature`` and ``hypervisor_version_requires'>=10.0'`` image properties. - * ``required`` - Enable the Secure Boot feature. * ``disabled`` or ``optional`` - (default) Disable the Secure Boot feature. * - All - ``ramdisk_id`` - The ID of image stored in the Image service that should be used as the ramdisk when booting an AMI-style image. - Valid image ID. * - All - ``vm_mode`` - The virtual machine mode. This represents the host/guest ABI (application binary interface) used for the virtual machine. - * ``hvm`` - Fully virtualized. This is the mode used by QEMU and KVM. * ``xen`` - Xen 3.0 paravirtualized. * ``uml`` - User Mode Linux paravirtualized. * ``exe`` - Executables in containers. This is the mode used by LXC. * - libvirt API driver - ``hw_cpu_sockets`` - The preferred number of sockets to expose to the guest. - Integer. * - libvirt API driver - ``hw_cpu_cores`` - The preferred number of cores to expose to the guest. - Integer. * - libvirt API driver - ``hw_cpu_threads`` - The preferred number of threads to expose to the guest. - Integer. * - libvirt API driver - ``hw_disk_bus`` - Specifies the type of disk controller to attach disk devices to. - One of ``scsi``, ``virtio``, ``uml``, ``xen``, ``ide``, or ``usb``. * - libvirt API driver - ``hw_pointer_model`` - Input devices that allow interaction with a graphical framebuffer, for example to provide a graphic tablet for absolute cursor movement. Currently only supported by the KVM/QEMU hypervisor configuration and VNC or SPICE consoles must be enabled. - ``usbtablet`` * - libvirt API driver - ``hw_rng_model`` - Adds a random-number generator device to the image's instances. The cloud administrator can enable and control device behavior by configuring the instance's flavor. By default: * The generator device is disabled. * ``/dev/random`` is used as the default entropy source. To specify a physical HW RNG device, use the following option in the nova.conf file: .. code-block:: ini rng_dev_path=/dev/hwrng - ``virtio``, or other supported device. * - libvirt API driver, Hyper-V driver - ``hw_machine_type`` - For libvirt: Enables booting an ARM system using the specified machine type. By default, if an ARM image is used and its type is not specified, Compute uses ``vexpress-a15`` (for ARMv7) or ``virt`` (for AArch64) machine types. For Hyper-V: Specifies whether the Hyper-V instance will be a generation 1 or generation 2 VM. By default, if the property is not provided, the instances will be generation 1 VMs. If the image is specific for generation 2 VMs but the property is not provided accordingly, the instance will fail to boot. - For libvirt: Valid types can be viewed by using the :command:`virsh capabilities` command (machine types are displayed in the ``machine`` tag). For hyper-V: Acceptable values are either ``hyperv-gen1`` or ``hyperv-gen2``. * - libvirt API driver, XenAPI driver - ``os_type`` - The operating system installed on the image. The ``libvirt`` API driver and ``XenAPI`` driver contains logic that takes different actions depending on the value of the ``os_type`` parameter of the image. For example, for ``os_type=windows`` images, it creates a FAT32-based swap partition instead of a Linux swap partition, and it limits the injected host name to less than 16 characters. - ``linux`` or ``windows``. * - libvirt API driver - ``hw_scsi_model`` - Enables the use of VirtIO SCSI (``virtio-scsi``) to provide block device access for compute instances; by default, instances use VirtIO Block (``virtio-blk``). VirtIO SCSI is a para-virtualized SCSI controller device that provides improved scalability and performance, and supports advanced SCSI hardware. - ``virtio-scsi`` * - libvirt API driver - ``hw_serial_port_count`` - Specifies the count of serial ports that should be provided. If ``hw:serial_port_count`` is not set in the flavor's extra_specs, then any count is permitted. If ``hw:serial_port_count`` is set, then this provides the default serial port count. It is permitted to override the default serial port count, but only with a lower value. - Integer * - libvirt API driver - ``hw_video_model`` - The video image driver used. - ``vga``, ``cirrus``, ``vmvga``, ``xen``, or ``qxl``. * - libvirt API driver - ``hw_video_ram`` - Maximum RAM for the video image. Used only if a ``hw_video:ram_max_mb`` value has been set in the flavor's extra_specs and that value is higher than the value set in ``hw_video_ram``. - Integer in MB (for example, ``64``). * - libvirt API driver - ``hw_watchdog_action`` - Enables a virtual hardware watchdog device that carries out the specified action if the server hangs. The watchdog uses the ``i6300esb`` device (emulating a PCI Intel 6300ESB). If ``hw_watchdog_action`` is not specified, the watchdog is disabled. - * ``disabled`` - (default) The device is not attached. Allows the user to disable the watchdog for the image, even if it has been enabled using the image's flavor. * ``reset`` - Forcefully reset the guest. * ``poweroff`` - Forcefully power off the guest. * ``pause`` - Pause the guest. * ``none`` - Only enable the watchdog; do nothing if the server hangs. * - libvirt API driver - ``os_command_line`` - The kernel command line to be used by the ``libvirt`` driver, instead of the default. For Linux Containers (LXC), the value is used as arguments for initialization. This key is valid only for Amazon kernel, ``ramdisk``, or machine images (``aki``, ``ari``, or ``ami``). - * - libvirt API driver and VMware API driver - ``hw_vif_model`` - Specifies the model of virtual network interface device to use. - The valid options depend on the configured hypervisor. * ``KVM`` and ``QEMU``: ``e1000``, ``ne2k_pci``, ``pcnet``, ``rtl8139``, and ``virtio``. * VMware: ``e1000``, ``e1000e``, ``VirtualE1000``, ``VirtualE1000e``, ``VirtualPCNet32``, ``VirtualSriovEthernetCard``, and ``VirtualVmxnet``. * Xen: ``e1000``, ``netfront``, ``ne2k_pci``, ``pcnet``, and ``rtl8139``. * - libvirt API driver - ``hw_vif_multiqueue_enabled`` - If ``true``, this enables the ``virtio-net multiqueue`` feature. In this case, the driver sets the number of queues equal to the number of guest vCPUs. This makes the network performance scale across a number of vCPUs. - ``true`` | ``false`` * - libvirt API driver - ``hw_boot_menu`` - If ``true``, enables the BIOS bootmenu. In cases where both the image metadata and Extra Spec are set, the Extra Spec setting is used. This allows for flexibility in setting/overriding the default behavior as needed. - ``true`` or ``false`` * - libvirt API driver - ``img_hide_hypervisor_id`` - Some hypervisors add a signature to their guests. While the presence of the signature can enable some paravirtualization features on the guest, it can also have the effect of preventing some drivers from loading. Hiding the signature by setting this property to ``true`` may allow such drivers to load and work. - ``true`` or ``false`` * - VMware API driver - ``vmware_adaptertype`` - The virtual SCSI or IDE controller used by the hypervisor. - ``lsiLogic``, ``lsiLogicsas``, ``busLogic``, ``ide``, or ``paraVirtual``. * - VMware API driver - ``vmware_ostype`` - A VMware GuestID which describes the operating system installed in the image. This value is passed to the hypervisor when creating a virtual machine. If not specified, the key defaults to ``otherGuest``. - See `thinkvirt.com <http://www.thinkvirt.com/?q=node/181>`_. * - VMware API driver - ``vmware_image_version`` - Currently unused. - ``1`` * - XenAPI driver - ``auto_disk_config`` - If ``true``, the root partition on the disk is automatically resized before the instance boots. This value is only taken into account by the Compute service when using a Xen-based hypervisor with the ``XenAPI`` driver. The Compute service will only attempt to resize if there is a single partition on the image, and only if the partition is in ``ext3`` or ``ext4`` format. - ``true`` or ``false`` �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/conf.py��������������������������������������������������������0000666�0001751�0001751�00000005533�13232161651�021072� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2015 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 os import sys import openstackdocstheme sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'openstackdocstheme', ] # openstackdocstheme options repository_name = 'openstack/python-glanceclient' bug_project = 'python-glanceclient' bug_tag = '' # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'python-glanceclient' copyright = u'OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. #html_theme = 'nature' html_theme = 'openstackdocs' # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = ['_theme'] html_theme_path = [openstackdocstheme.get_html_theme_path()] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project html_last_updated_fmt = '%Y-%m-%d %H:%M' # -- Options for man page output ---------------------------------------------- # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' man_pages = [ ('cli/glance', 'glance', u'Client for OpenStack Images API', [u'OpenStack Foundation'], 1), ] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/reference/�����������������������������������������������������0000775�0001751�0001751�00000000000�13232162126�021517� 5����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/reference/index.rst��������������������������������������������0000666�0001751�0001751�00000001511�13232161651�023362� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������========================== Python Library Reference ========================== In order to use the python api directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: >>> from glanceclient import Client >>> glance = Client('1', endpoint=OS_IMAGE_ENDPOINT, token=OS_AUTH_TOKEN) >>> image = glance.images.create(name="My Test Image") >>> print image.status 'queued' >>> image.update(data=open('/tmp/myimage.iso', 'rb')) >>> print image.status 'active' >>> image.update(properties=dict(my_custom_property='value')) >>> with open('/tmp/copyimage.iso', 'wb') as f: for chunk in image.data(): f.write(chunk) >>> image.delete() .. toctree:: :maxdepth: 2 api/index apiv2 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/reference/apiv2.rst��������������������������������������������0000666�0001751�0001751�00000003412�13232161651�023276� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Python API v2 ============= To create a client:: from keystoneauth1 import loading from keystoneauth1 import session from glanceclient import Client loader = loading.get_plugin_loader('password') auth = loader.load_from_options( auth_url=AUTH_URL, username=USERNAME, password=PASSWORD, project_id=PROJECT_ID) session = session.Session(auth=auth) glance = Client('2', session=session) Create ------ Create a new image:: image = glance.images.create(name="myNewImage") glance.images.upload(image.id, open('/tmp/myimage.iso', 'rb')) Show ---- Describe a specific image:: glance.images.get(image.id) Update ------ Update a specific image:: # update with a list of image attribute names and their new values glance.images.update(image.id, name="myNewImageName") Custom Properties ----------------- Set a custom property on an image:: # set an arbitrary property on an image glance.images.update(image.id, my_custom_property='value') Remove a custom property from an image:: # remove the custom property 'my_custom_property' glance.images.update(image.id, remove_props=['my_custom_property']) Delete ------ Delete specified image(s):: glance.images.delete(image.id) List ---- List images you can access:: for image in glance.images.list(): print image Download -------- Download a specific image:: d = glance.images.data(image.id) Share an Image -------------- Share a specific image with a tenant:: glance.image_members.create(image_id, member_id) Remove a Share -------------- Remove a shared image from a tenant:: glance.image_members.delete(image_id, member_id) List Sharings ------------- Describe sharing permissions by image or tenant:: glance.image_members.list(image_id) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/reference/api/�������������������������������������������������0000775�0001751�0001751�00000000000�13232162126�022270� 5����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/doc/source/reference/api/index.rst����������������������������������������0000666�0001751�0001751�00000000160�13232161651�024132� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������====================== Python API Reference ====================== .. toctree:: :maxdepth: 2 autoindex ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/LICENSE�������������������������������������������������������������������0000666�0001751�0001751�00000023636�13232161651�016537� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ��������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/test-requirements.txt�����������������������������������������������������0000666�0001751�0001751�00000001217�13232161652�021763� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 mock>=2.0.0 # BSD ordereddict>=1.1 # MIT os-client-config>=1.28.0 # Apache-2.0 openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinx!=1.6.6,>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testtools>=2.2.0 # MIT testscenarios>=0.4 # Apache-2.0/BSD fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1.0 # Apache-2.0 tempest>=17.1.0 # Apache-2.0 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/tools/��������������������������������������������������������������������0000775�0001751�0001751�00000000000�13232162126�016654� 5����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/tools/with_venv.sh��������������������������������������������������������0000777�0001751�0001751�00000000303�13232161652�021225� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash command -v tox > /dev/null 2>&1 if [ $? -ne 0 ]; then echo 'This script requires "tox" to run.' echo 'You can install it with "pip install tox".' exit 1; fi tox -evenv -- $@ �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/tools/fix_ca_bundle.sh����������������������������������������������������0000777�0001751�0001751�00000002546�13232161652�022011� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# When the functional tests are run in a devstack environment, we # need to make sure that the python-requests module installed by # tox in the test environment can find the distro-specific CA store # where the devstack certs have been installed. # # assumptions: # - devstack is running # - the devstack tls-proxy service is running # # This code based on a function in devstack lib/tls function set_ca_bundle { local python_cmd='.tox/functional/bin/python' local capath=$($python_cmd -c $'try:\n from requests import certs\n print (certs.where())\nexcept ImportError: pass') # of course, each distro keeps the CA store in a different location local fedora_CA='/etc/pki/tls/certs/ca-bundle.crt' local ubuntu_CA='/etc/ssl/certs/ca-certificates.crt' local suse_CA='/etc/ssl/ca-bundle.pem' # the distro CA is rooted in /etc, so if ours isn't, we need to # change it if [[ ! $capath == "" && ! $capath =~ ^/etc/.* && ! -L $capath ]]; then if [[ -e $fedora_CA ]]; then rm -f $capath ln -s $fedora_CA $capath elif [[ -e $ubuntu_CA ]]; then rm -f $capath ln -s $ubuntu_CA $capath elif [[ -e $suse_CA ]]; then rm -f $capath ln -s $suse_CA $capath else echo "can't set CA bundle, expect tests to fail" fi fi } set_ca_bundle ����������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-2.9.1/tools/glance.bash_completion����������������������������������������������0000666�0001751�0001751�00000001562�13232161652�023206� 0����������������������������������������������������������������������������������������������������ustar �zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������_glance_opts="" # lazy init _glance_flags="" # lazy init _glance_opts_exp="" # lazy init _glance() { local cur prev nbc cflags COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_glance_opts" == "x" ] ; then nbc="`glance bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" _glance_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" _glance_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" _glance_opts_exp="`echo "$_glance_opts" | sed 's/^ *//' | tr ' ' '|'`" fi if [[ " ${COMP_WORDS[@]} " =~ " "($_glance_opts_exp)" " && "$prev" != "help" ]] ; then COMPREPLY=($(compgen -W "${_glance_flags}" -- ${cur})) else COMPREPLY=($(compgen -W "${_glance_opts}" -- ${cur})) fi return 0 } complete -F _glance glance������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������