summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.coveragerc1
-rw-r--r--README.rst20
-rw-r--r--doc/source/index.rst12
-rw-r--r--glanceclient/client.py4
-rw-r--r--glanceclient/common/base.py35
-rw-r--r--glanceclient/common/exceptions.py14
-rw-r--r--glanceclient/common/http.py62
-rw-r--r--glanceclient/common/https.py15
-rw-r--r--glanceclient/common/progressbar.py15
-rw-r--r--glanceclient/common/utils.py44
-rw-r--r--glanceclient/exc.py4
-rw-r--r--glanceclient/openstack/common/__init__.py0
-rw-r--r--glanceclient/openstack/common/_i18n.py45
-rw-r--r--glanceclient/openstack/common/apiclient/__init__.py0
-rw-r--r--glanceclient/openstack/common/apiclient/auth.py234
-rw-r--r--glanceclient/openstack/common/apiclient/client.py388
-rw-r--r--glanceclient/openstack/common/apiclient/fake_client.py187
-rw-r--r--[-rwxr-xr-x]glanceclient/shell.py129
-rw-r--r--glanceclient/tests/functional/base.py2
-rw-r--r--glanceclient/tests/functional/test_readonly_glance.py4
-rw-r--r--glanceclient/tests/unit/test_base.py2
-rw-r--r--glanceclient/tests/unit/test_exc.py8
-rw-r--r--glanceclient/tests/unit/test_http.py60
-rw-r--r--glanceclient/tests/unit/test_shell.py306
-rw-r--r--glanceclient/tests/unit/v1/test_images.py8
-rw-r--r--glanceclient/tests/unit/v1/test_shell.py15
-rw-r--r--glanceclient/tests/unit/v2/fixtures.py69
-rw-r--r--glanceclient/tests/unit/v2/test_images.py24
-rw-r--r--glanceclient/tests/unit/v2/test_schemas.py2
-rw-r--r--glanceclient/tests/unit/v2/test_shell_v2.py14
-rw-r--r--glanceclient/tests/unit/v2/test_tasks.py38
-rw-r--r--glanceclient/tests/utils.py11
-rw-r--r--glanceclient/v1/apiclient/__init__.py (renamed from glanceclient/openstack/__init__.py)0
-rw-r--r--glanceclient/v1/apiclient/base.py (renamed from glanceclient/openstack/common/apiclient/base.py)2
-rw-r--r--glanceclient/v1/apiclient/exceptions.py (renamed from glanceclient/openstack/common/apiclient/exceptions.py)0
-rw-r--r--glanceclient/v1/apiclient/utils.py (renamed from glanceclient/openstack/common/apiclient/utils.py)20
-rw-r--r--glanceclient/v1/image_members.py2
-rw-r--r--glanceclient/v1/images.py4
-rw-r--r--glanceclient/v1/shell.py2
-rw-r--r--glanceclient/v1/versions.py2
-rw-r--r--glanceclient/v2/image_schema.py156
-rw-r--r--glanceclient/v2/image_tags.py3
-rw-r--r--glanceclient/v2/images.py21
-rw-r--r--glanceclient/v2/metadefs.py32
-rw-r--r--glanceclient/v2/namespace_schema.py243
-rw-r--r--glanceclient/v2/resource_type_schema.py67
-rw-r--r--glanceclient/v2/schemas.py9
-rw-r--r--glanceclient/v2/shell.py16
-rw-r--r--glanceclient/v2/tasks.py8
-rw-r--r--releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml11
-rw-r--r--releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml6
-rw-r--r--releasenotes/source/conf.py3
-rw-r--r--releasenotes/source/index.rst2
-rw-r--r--releasenotes/source/mitaka.rst6
-rw-r--r--releasenotes/source/newton.rst6
-rw-r--r--requirements.txt14
-rw-r--r--setup.cfg3
-rw-r--r--test-requirements.txt19
-rwxr-xr-xtools/tox_install.sh55
-rw-r--r--tox.ini16
60 files changed, 1203 insertions, 1297 deletions
diff --git a/.coveragerc b/.coveragerc
index 092ee58..457eb50 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,7 +1,6 @@
[run]
branch = True
source = glanceclient
-omit = glanceclient/openstack/*
[report]
ignore_errors = True
diff --git a/README.rst b/README.rst
index 4e08b6a..192b0e8 100644
--- a/README.rst
+++ b/README.rst
@@ -1,3 +1,23 @@
+========================
+Team and repository tags
+========================
+
+.. image:: http://governance.openstack.org/badges/python-glanceclient.svg
+ :target: http://governance.openstack.org/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",
+ "team:diverse-affiliation".
+ 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
===========================================
diff --git a/doc/source/index.rst b/doc/source/index.rst
index d13fcea..e933cf9 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -23,7 +23,6 @@ In order to use the python api directly, you must first obtain an auth token and
Python API Reference
~~~~~~~~~~~~~~~~~~~~
-
.. toctree::
:maxdepth: 2
@@ -31,6 +30,17 @@ Python API Reference
ref/v1/index
ref/v2/index
+.. toctree::
+ :maxdepth: 1
+
+ How to use the v2 API <apiv2>
+
+Command-line Tool Reference
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. toctree::
+ :maxdepth: 1
+
+ man/glance
Command-line Tool
-----------------
diff --git a/glanceclient/client.py b/glanceclient/client.py
index db2a4f7..714c96a 100644
--- a/glanceclient/client.py
+++ b/glanceclient/client.py
@@ -25,8 +25,8 @@ def Client(version=None, endpoint=None, session=None, *args, **kwargs):
for specific details.
:param string version: The version of API to use.
- :param session: A keystoneclient session that should be used for transport.
- :type session: keystoneclient.session.Session
+ :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.
diff --git a/glanceclient/common/base.py b/glanceclient/common/base.py
deleted file mode 100644
index 55f265e..0000000
--- a/glanceclient/common/base.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-Base utilities to build API operation managers and objects on top of.
-
-DEPRECATED post v.0.12.0. Use 'glanceclient.openstack.common.apiclient.base'
-instead of this module."
-"""
-
-import warnings
-
-from glanceclient.openstack.common.apiclient import base
-
-
-warnings.warn("The 'glanceclient.common.base' module is deprecated post "
- "v.0.12.0. Use 'glanceclient.openstack.common.apiclient.base' "
- "instead of this one.", DeprecationWarning)
-
-
-getid = base.getid
-Manager = base.ManagerWithFind
-Resource = base.Resource
diff --git a/glanceclient/common/exceptions.py b/glanceclient/common/exceptions.py
index 64cc01e..1512557 100644
--- a/glanceclient/common/exceptions.py
+++ b/glanceclient/common/exceptions.py
@@ -1,3 +1,15 @@
+# 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 *
+from glanceclient.exc import * # noqa
diff --git a/glanceclient/common/http.py b/glanceclient/common/http.py
index 1157381..67a8d06 100644
--- a/glanceclient/common/http.py
+++ b/glanceclient/common/http.py
@@ -17,8 +17,8 @@ import copy
import logging
import socket
-from keystoneclient import adapter
-from keystoneclient import exceptions as ksc_exc
+from keystoneauth1 import adapter
+from keystoneauth1 import exceptions as ksa_exc
from oslo_utils import importutils
from oslo_utils import netutils
import requests
@@ -42,6 +42,20 @@ USER_AGENT = 'python-glanceclient'
CHUNKSIZE = 1024 * 64 # 64kB
+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 six.iteritems(headers) if v is not None)
+
+
class _BaseHTTPClient(object):
@staticmethod
@@ -49,7 +63,7 @@ class _BaseHTTPClient(object):
chunk = body
while chunk:
chunk = body.read(CHUNKSIZE)
- if chunk == '':
+ if not chunk:
break
yield chunk
@@ -78,8 +92,18 @@ class _BaseHTTPClient(object):
return data
def _handle_response(self, resp):
+ # log request-id for each api cal
+ request_id = resp.headers.get('x-openstack-request-id')
+ if request_id:
+ LOG.debug('%(method)s call to glance-api for '
+ '%(url)s used request id '
+ '%(response_request_id)s',
+ {'method': resp.request.method,
+ 'url': resp.url,
+ 'response_request_id': request_id})
+
if not resp.ok:
- LOG.debug("Request returned failure status %s." % resp.status_code)
+ 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'):
@@ -124,10 +148,6 @@ class HTTPClient(_BaseHTTPClient):
self.session = requests.Session()
self.session.headers["User-Agent"] = USER_AGENT
- if self.auth_token:
- self.session.headers["X-Auth-Token"] = encodeutils.safe_encode(
- self.auth_token)
-
if self.language_header:
self.session.headers["Accept-Language"] = self.language_header
@@ -197,20 +217,6 @@ class HTTPClient(_BaseHTTPClient):
LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore')
for x in dump]))
- @staticmethod
- 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 six.iteritems(headers) if v is not None)
-
def _request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
@@ -226,13 +232,17 @@ class HTTPClient(_BaseHTTPClient):
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 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 = self.encode_headers(headers)
+ headers = encode_headers(headers)
if self.endpoint.endswith("/") or url.startswith("/"):
conn_url = "%s%s" % (self.endpoint, url)
@@ -306,7 +316,7 @@ class SessionClient(adapter.Adapter, _BaseHTTPClient):
super(SessionClient, self).__init__(session, **kwargs)
def request(self, url, method, **kwargs):
- headers = kwargs.pop('headers', {})
+ headers = encode_headers(kwargs.pop('headers', {}))
kwargs['raise_exc'] = False
data = self._set_common_request_kwargs(headers, kwargs)
@@ -316,13 +326,13 @@ class SessionClient(adapter.Adapter, _BaseHTTPClient):
headers=headers,
data=data,
**kwargs)
- except ksc_exc.RequestTimeout as e:
+ 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 ksc_exc.ConnectionRefused as e:
+ 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" %
diff --git a/glanceclient/common/https.py b/glanceclient/common/https.py
index d2991cd..36015e5 100644
--- a/glanceclient/common/https.py
+++ b/glanceclient/common/https.py
@@ -52,7 +52,7 @@ 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
@@ -87,7 +87,7 @@ def do_verify_callback(connection, x509, errnum,
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
@@ -187,7 +187,7 @@ class HTTPSAdapter(adapters.HTTPAdapter):
class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
- """
+ """A replacement for the default HTTPSConnectionPool.
HTTPSConnectionPool will be instantiated when a new
connection is requested to the HTTPSAdapter. This
@@ -232,10 +232,8 @@ class OpenSSLConnectionDelegator(object):
class VerifiedHTTPSConnection(HTTPSConnection):
- """
+ """Extended OpenSSL HTTPSConnection for enhanced SSL support.
- Extended HTTPSConnection which uses the OpenSSL library
- for enhanced SSL support.
Note: Much of this functionality can eventually be replaced
with native Python 3.3 code.
"""
@@ -325,10 +323,9 @@ class VerifiedHTTPSConnection(HTTPSConnection):
self.context.set_default_verify_paths()
def connect(self):
- """
+ """Connect to an SSL port using the OpenSSL library.
- Connect to an SSL port using the OpenSSL library
- and apply per-connection parameters.
+ This method also applies per-connection parameters to the connection.
"""
result = socket.getaddrinfo(self.host, self.port, 0,
socket.SOCK_STREAM)
diff --git a/glanceclient/common/progressbar.py b/glanceclient/common/progressbar.py
index d372e5c..6cf0df3 100644
--- a/glanceclient/common/progressbar.py
+++ b/glanceclient/common/progressbar.py
@@ -19,7 +19,7 @@ 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.
@@ -51,10 +51,10 @@ class _ProgressBarBase(object):
class VerboseFileWrapper(_ProgressBarBase):
- """
+ """A file wrapper with a progress bar.
- A file wrapper that show and advance a progress bar
- whenever file's read method is called.
+ The file wrapper shows and advances a progress bar whenever the
+ wrapped file's read method is called.
"""
def read(self, *args, **kwargs):
@@ -70,10 +70,11 @@ class VerboseFileWrapper(_ProgressBarBase):
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.
- An iterator wrapper that show and advance a progress bar whenever
- data is consumed from the iterator.
:note: Use only with iterator that yield strings.
"""
diff --git a/glanceclient/common/utils.py b/glanceclient/common/utils.py
index 878c1a5..9f3a1fe 100644
--- a/glanceclient/common/utils.py
+++ b/glanceclient/common/utils.py
@@ -51,7 +51,7 @@ REQUIRED_FIELDS_ON_DATA = ('disk_format', 'container_format')
# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
- # Because of the sematics of decorator composition if we just append
+ # 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
@@ -133,7 +133,7 @@ def schema_args(schema_getter, omit=None):
if isinstance(type_str, list):
# NOTE(flaper87): This means the server has
# returned something like `['null', 'string']`,
- # therfore we use the first non-`null` type as
+ # therefore we use the first non-`null` type as
# the valid type.
for t in type_str:
if t != 'null':
@@ -159,7 +159,7 @@ def schema_args(schema_getter, omit=None):
# NOTE(flaper87): Make sure all values are `str/unicode`
# for the `join` to succeed. Enum types can also be `None`
- # therfore, join's call would fail without the following
+ # 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))
@@ -246,21 +246,6 @@ def find_resource(manager, name_or_id):
return matches[0]
-def skip_authentication(f):
- """Function decorator used to indicate a caller may be unauthenticated."""
- f.require_authentication = False
- return f
-
-
-def is_authentication_required(f):
- """Checks to see if the function requires authentication.
-
- Use the skip_authentication decorator to indicate a caller may
- skip the authentication step.
- """
- return getattr(f, 'require_authentication', True)
-
-
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars.
@@ -298,10 +283,10 @@ def save_image(data, path):
:param path: path to save the image to
"""
if path is None:
- if six.PY3:
- image = sys.stdout.buffer
- else:
- image = sys.stdout
+ # 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:
@@ -375,9 +360,12 @@ def get_data_file(args):
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(sys.stdin.fileno(), os.O_BINARY)
- return sys.stdin
+ msvcrt.setmode(image.fileno(), os.O_BINARY)
+ return image
else:
# (3) no image data provided
return None
@@ -467,6 +455,14 @@ def endpoint_version_from_url(endpoint, default_version=None):
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
diff --git a/glanceclient/exc.py b/glanceclient/exc.py
index 29189e4..c8616c3 100644
--- a/glanceclient/exc.py
+++ b/glanceclient/exc.py
@@ -16,6 +16,8 @@
import re
import sys
+import six
+
class BaseException(Exception):
"""An error occurred."""
@@ -177,6 +179,8 @@ def from_response(response, body=None):
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)
diff --git a/glanceclient/openstack/common/__init__.py b/glanceclient/openstack/common/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/glanceclient/openstack/common/__init__.py
+++ /dev/null
diff --git a/glanceclient/openstack/common/_i18n.py b/glanceclient/openstack/common/_i18n.py
deleted file mode 100644
index d1339ad..0000000
--- a/glanceclient/openstack/common/_i18n.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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.
-
-"""oslo.i18n integration module.
-
-See http://docs.openstack.org/developer/oslo.i18n/usage.html
-
-"""
-
-try:
- import oslo_i18n
-
- # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
- # application name when this module is synced into the separate
- # repository. It is OK to have more than one translation function
- # using the same domain, since there will still only be one message
- # catalog.
- _translators = oslo_i18n.TranslatorFactory(domain='glanceclient')
-
- # The primary translation function using the well-known name "_"
- _ = _translators.primary
-
- # Translators for log levels.
- #
- # The abbreviated names are meant to reflect the usual use of a short
- # name like '_'. The "L" is for "log" and the other letter comes from
- # the level.
- _LI = _translators.log_info
- _LW = _translators.log_warning
- _LE = _translators.log_error
- _LC = _translators.log_critical
-except ImportError:
- # NOTE(dims): Support for cases where a project wants to use
- # code from oslo-incubator, but is not ready to be internationalized
- # (like tempest)
- _ = _LI = _LW = _LE = _LC = lambda x: x
diff --git a/glanceclient/openstack/common/apiclient/__init__.py b/glanceclient/openstack/common/apiclient/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/glanceclient/openstack/common/apiclient/__init__.py
+++ /dev/null
diff --git a/glanceclient/openstack/common/apiclient/auth.py b/glanceclient/openstack/common/apiclient/auth.py
deleted file mode 100644
index 771df04..0000000
--- a/glanceclient/openstack/common/apiclient/auth.py
+++ /dev/null
@@ -1,234 +0,0 @@
-# Copyright 2013 OpenStack Foundation
-# Copyright 2013 Spanish National Research Council.
-# 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.
-
-# E0202: An attribute inherited from %s hide this method
-# pylint: disable=E0202
-
-########################################################################
-#
-# 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 abc
-import argparse
-import os
-
-import six
-from stevedore import extension
-
-from glanceclient.openstack.common.apiclient import exceptions
-
-
-_discovered_plugins = {}
-
-
-def discover_auth_systems():
- """Discover the available auth-systems.
-
- This won't take into account the old style auth-systems.
- """
- global _discovered_plugins
- _discovered_plugins = {}
-
- def add_plugin(ext):
- _discovered_plugins[ext.name] = ext.plugin
-
- ep_namespace = "glanceclient.openstack.common.apiclient.auth"
- mgr = extension.ExtensionManager(ep_namespace)
- mgr.map(add_plugin)
-
-
-def load_auth_system_opts(parser):
- """Load options needed by the available auth-systems into a parser.
-
- This function will try to populate the parser with options from the
- available plugins.
- """
- group = parser.add_argument_group("Common auth options")
- BaseAuthPlugin.add_common_opts(group)
- for name, auth_plugin in six.iteritems(_discovered_plugins):
- group = parser.add_argument_group(
- "Auth-system '%s' options" % name,
- conflict_handler="resolve")
- auth_plugin.add_opts(group)
-
-
-def load_plugin(auth_system):
- try:
- plugin_class = _discovered_plugins[auth_system]
- except KeyError:
- raise exceptions.AuthSystemNotFound(auth_system)
- return plugin_class(auth_system=auth_system)
-
-
-def load_plugin_from_args(args):
- """Load required plugin and populate it with options.
-
- Try to guess auth system if it is not specified. Systems are tried in
- alphabetical order.
-
- :type args: argparse.Namespace
- :raises: AuthPluginOptionsMissing
- """
- auth_system = args.os_auth_system
- if auth_system:
- plugin = load_plugin(auth_system)
- plugin.parse_opts(args)
- plugin.sufficient_options()
- return plugin
-
- for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
- plugin_class = _discovered_plugins[plugin_auth_system]
- plugin = plugin_class()
- plugin.parse_opts(args)
- try:
- plugin.sufficient_options()
- except exceptions.AuthPluginOptionsMissing:
- continue
- return plugin
- raise exceptions.AuthPluginOptionsMissing(["auth_system"])
-
-
-@six.add_metaclass(abc.ABCMeta)
-class BaseAuthPlugin(object):
- """Base class for authentication plugins.
-
- An authentication plugin needs to override at least the authenticate
- method to be a valid plugin.
- """
-
- auth_system = None
- opt_names = []
- common_opt_names = [
- "auth_system",
- "username",
- "password",
- "tenant_name",
- "token",
- "auth_url",
- ]
-
- def __init__(self, auth_system=None, **kwargs):
- self.auth_system = auth_system or self.auth_system
- self.opts = dict((name, kwargs.get(name))
- for name in self.opt_names)
-
- @staticmethod
- def _parser_add_opt(parser, opt):
- """Add an option to parser in two variants.
-
- :param opt: option name (with underscores)
- """
- dashed_opt = opt.replace("_", "-")
- env_var = "OS_%s" % opt.upper()
- arg_default = os.environ.get(env_var, "")
- arg_help = "Defaults to env[%s]." % env_var
- parser.add_argument(
- "--os-%s" % dashed_opt,
- metavar="<%s>" % dashed_opt,
- default=arg_default,
- help=arg_help)
- parser.add_argument(
- "--os_%s" % opt,
- metavar="<%s>" % dashed_opt,
- help=argparse.SUPPRESS)
-
- @classmethod
- def add_opts(cls, parser):
- """Populate the parser with the options for this plugin.
- """
- for opt in cls.opt_names:
- # use `BaseAuthPlugin.common_opt_names` since it is never
- # changed in child classes
- if opt not in BaseAuthPlugin.common_opt_names:
- cls._parser_add_opt(parser, opt)
-
- @classmethod
- def add_common_opts(cls, parser):
- """Add options that are common for several plugins.
- """
- for opt in cls.common_opt_names:
- cls._parser_add_opt(parser, opt)
-
- @staticmethod
- def get_opt(opt_name, args):
- """Return option name and value.
-
- :param opt_name: name of the option, e.g., "username"
- :param args: parsed arguments
- """
- return (opt_name, getattr(args, "os_%s" % opt_name, None))
-
- def parse_opts(self, args):
- """Parse the actual auth-system options if any.
-
- This method is expected to populate the attribute `self.opts` with a
- dict containing the options and values needed to make authentication.
- """
- self.opts.update(dict(self.get_opt(opt_name, args)
- for opt_name in self.opt_names))
-
- def authenticate(self, http_client):
- """Authenticate using plugin defined method.
-
- The method usually analyses `self.opts` and performs
- a request to authentication server.
-
- :param http_client: client object that needs authentication
- :type http_client: HTTPClient
- :raises: AuthorizationFailure
- """
- self.sufficient_options()
- self._do_authenticate(http_client)
-
- @abc.abstractmethod
- def _do_authenticate(self, http_client):
- """Protected method for authentication.
- """
-
- def sufficient_options(self):
- """Check if all required options are present.
-
- :raises: AuthPluginOptionsMissing
- """
- missing = [opt
- for opt in self.opt_names
- if not self.opts.get(opt)]
- if missing:
- raise exceptions.AuthPluginOptionsMissing(missing)
-
- @abc.abstractmethod
- def token_and_endpoint(self, endpoint_type, service_type):
- """Return token and endpoint.
-
- :param service_type: Service type of the endpoint
- :type service_type: string
- :param endpoint_type: Type of endpoint.
- Possible values: public or publicURL,
- internal or internalURL,
- admin or adminURL
- :type endpoint_type: string
- :returns: tuple of token and endpoint strings
- :raises: EndpointException
- """
diff --git a/glanceclient/openstack/common/apiclient/client.py b/glanceclient/openstack/common/apiclient/client.py
deleted file mode 100644
index 0759a95..0000000
--- a/glanceclient/openstack/common/apiclient/client.py
+++ /dev/null
@@ -1,388 +0,0 @@
-# Copyright 2010 Jacob Kaplan-Moss
-# Copyright 2011 OpenStack Foundation
-# Copyright 2011 Piston Cloud Computing, Inc.
-# Copyright 2013 Alessio Ababilov
-# Copyright 2013 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.
-
-"""
-OpenStack Client interface. Handles the REST calls and responses.
-"""
-
-# E0202: An attribute inherited from %s hide this method
-# pylint: disable=E0202
-
-import hashlib
-import logging
-import time
-
-try:
- import simplejson as json
-except ImportError:
- import json
-
-from oslo_utils import encodeutils
-from oslo_utils import importutils
-import requests
-
-from glanceclient._i18n import _
-from glanceclient.openstack.common.apiclient import exceptions
-
-_logger = logging.getLogger(__name__)
-SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
-
-
-class HTTPClient(object):
- """This client handles sending HTTP requests to OpenStack servers.
-
- Features:
-
- - share authentication information between several clients to different
- services (e.g., for compute and image clients);
- - reissue authentication request for expired tokens;
- - encode/decode JSON bodies;
- - raise exceptions on HTTP errors;
- - pluggable authentication;
- - store authentication information in a keyring;
- - store time spent for requests;
- - register clients for particular services, so one can use
- `http_client.identity` or `http_client.compute`;
- - log requests and responses in a format that is easy to copy-and-paste
- into terminal and send the same request with curl.
- """
-
- user_agent = "glanceclient.openstack.common.apiclient"
-
- def __init__(self,
- auth_plugin,
- region_name=None,
- endpoint_type="publicURL",
- original_ip=None,
- verify=True,
- cert=None,
- timeout=None,
- timings=False,
- keyring_saver=None,
- debug=False,
- user_agent=None,
- http=None):
- self.auth_plugin = auth_plugin
-
- self.endpoint_type = endpoint_type
- self.region_name = region_name
-
- self.original_ip = original_ip
- self.timeout = timeout
- self.verify = verify
- self.cert = cert
-
- self.keyring_saver = keyring_saver
- self.debug = debug
- self.user_agent = user_agent or self.user_agent
-
- self.times = [] # [("item", starttime, endtime), ...]
- self.timings = timings
-
- # requests within the same session can reuse TCP connections from pool
- self.http = http or requests.Session()
-
- self.cached_token = None
- self.last_request_id = None
-
- def _safe_header(self, name, value):
- if name in SENSITIVE_HEADERS:
- # because in python3 byte string handling is ... ug
- v = value.encode('utf-8')
- h = hashlib.sha1(v)
- d = h.hexdigest()
- return encodeutils.safe_decode(name), "{SHA1}%s" % d
- else:
- return (encodeutils.safe_decode(name),
- encodeutils.safe_decode(value))
-
- def _http_log_req(self, method, url, kwargs):
- if not self.debug:
- return
-
- string_parts = [
- "curl -g -i",
- "-X '%s'" % method,
- "'%s'" % url,
- ]
-
- for element in kwargs['headers']:
- header = ("-H '%s: %s'" %
- self._safe_header(element, kwargs['headers'][element]))
- string_parts.append(header)
-
- _logger.debug("REQ: %s" % " ".join(string_parts))
- if 'data' in kwargs:
- _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
-
- def _http_log_resp(self, resp):
- if not self.debug:
- return
- _logger.debug(
- "RESP: [%s] %s\n",
- resp.status_code,
- resp.headers)
- if resp._content_consumed:
- _logger.debug(
- "RESP BODY: %s\n",
- resp.text)
-
- def serialize(self, kwargs):
- if kwargs.get('json') is not None:
- kwargs['headers']['Content-Type'] = 'application/json'
- kwargs['data'] = json.dumps(kwargs['json'])
- try:
- del kwargs['json']
- except KeyError:
- pass
-
- def get_timings(self):
- return self.times
-
- def reset_timings(self):
- self.times = []
-
- def request(self, method, url, **kwargs):
- """Send an http request with the specified characteristics.
-
- Wrapper around `requests.Session.request` to handle tasks such as
- setting headers, JSON encoding/decoding, and error handling.
-
- :param method: method of HTTP request
- :param url: URL of HTTP request
- :param kwargs: any other parameter that can be passed to
- requests.Session.request (such as `headers`) or `json`
- that will be encoded as JSON and used as `data` argument
- """
- kwargs.setdefault("headers", {})
- kwargs["headers"]["User-Agent"] = self.user_agent
- if self.original_ip:
- kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
- self.original_ip, self.user_agent)
- if self.timeout is not None:
- kwargs.setdefault("timeout", self.timeout)
- kwargs.setdefault("verify", self.verify)
- if self.cert is not None:
- kwargs.setdefault("cert", self.cert)
- self.serialize(kwargs)
-
- self._http_log_req(method, url, kwargs)
- if self.timings:
- start_time = time.time()
- resp = self.http.request(method, url, **kwargs)
- if self.timings:
- self.times.append(("%s %s" % (method, url),
- start_time, time.time()))
- self._http_log_resp(resp)
-
- self.last_request_id = resp.headers.get('x-openstack-request-id')
-
- if resp.status_code >= 400:
- _logger.debug(
- "Request returned failure status: %s",
- resp.status_code)
- raise exceptions.from_response(resp, method, url)
-
- return resp
-
- @staticmethod
- def concat_url(endpoint, url):
- """Concatenate endpoint and final URL.
-
- E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
- "http://keystone/v2.0/tokens".
-
- :param endpoint: the base URL
- :param url: the final URL
- """
- return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
-
- def client_request(self, client, method, url, **kwargs):
- """Send an http request using `client`'s endpoint and specified `url`.
-
- If request was rejected as unauthorized (possibly because the token is
- expired), issue one authorization attempt and send the request once
- again.
-
- :param client: instance of BaseClient descendant
- :param method: method of HTTP request
- :param url: URL of HTTP request
- :param kwargs: any other parameter that can be passed to
- `HTTPClient.request`
- """
-
- filter_args = {
- "endpoint_type": client.endpoint_type or self.endpoint_type,
- "service_type": client.service_type,
- }
- token, endpoint = (self.cached_token, client.cached_endpoint)
- just_authenticated = False
- if not (token and endpoint):
- try:
- token, endpoint = self.auth_plugin.token_and_endpoint(
- **filter_args)
- except exceptions.EndpointException:
- pass
- if not (token and endpoint):
- self.authenticate()
- just_authenticated = True
- token, endpoint = self.auth_plugin.token_and_endpoint(
- **filter_args)
- if not (token and endpoint):
- raise exceptions.AuthorizationFailure(
- _("Cannot find endpoint or token for request"))
-
- old_token_endpoint = (token, endpoint)
- kwargs.setdefault("headers", {})["X-Auth-Token"] = token
- self.cached_token = token
- client.cached_endpoint = endpoint
- # Perform the request once. If we get Unauthorized, then it
- # might be because the auth token expired, so try to
- # re-authenticate and try again. If it still fails, bail.
- try:
- return self.request(
- method, self.concat_url(endpoint, url), **kwargs)
- except exceptions.Unauthorized as unauth_ex:
- if just_authenticated:
- raise
- self.cached_token = None
- client.cached_endpoint = None
- if self.auth_plugin.opts.get('token'):
- self.auth_plugin.opts['token'] = None
- if self.auth_plugin.opts.get('endpoint'):
- self.auth_plugin.opts['endpoint'] = None
- self.authenticate()
- try:
- token, endpoint = self.auth_plugin.token_and_endpoint(
- **filter_args)
- except exceptions.EndpointException:
- raise unauth_ex
- if (not (token and endpoint) or
- old_token_endpoint == (token, endpoint)):
- raise unauth_ex
- self.cached_token = token
- client.cached_endpoint = endpoint
- kwargs["headers"]["X-Auth-Token"] = token
- return self.request(
- method, self.concat_url(endpoint, url), **kwargs)
-
- def add_client(self, base_client_instance):
- """Add a new instance of :class:`BaseClient` descendant.
-
- `self` will store a reference to `base_client_instance`.
-
- Example:
-
- >>> def test_clients():
- ... from keystoneclient.auth import keystone
- ... from openstack.common.apiclient import client
- ... auth = keystone.KeystoneAuthPlugin(
- ... username="user", password="pass", tenant_name="tenant",
- ... auth_url="http://auth:5000/v2.0")
- ... openstack_client = client.HTTPClient(auth)
- ... # create nova client
- ... from novaclient.v1_1 import client
- ... client.Client(openstack_client)
- ... # create keystone client
- ... from keystoneclient.v2_0 import client
- ... client.Client(openstack_client)
- ... # use them
- ... openstack_client.identity.tenants.list()
- ... openstack_client.compute.servers.list()
- """
- service_type = base_client_instance.service_type
- if service_type and not hasattr(self, service_type):
- setattr(self, service_type, base_client_instance)
-
- def authenticate(self):
- self.auth_plugin.authenticate(self)
- # Store the authentication results in the keyring for later requests
- if self.keyring_saver:
- self.keyring_saver.save(self)
-
-
-class BaseClient(object):
- """Top-level object to access the OpenStack API.
-
- This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
- will handle a bunch of issues such as authentication.
- """
-
- service_type = None
- endpoint_type = None # "publicURL" will be used
- cached_endpoint = None
-
- def __init__(self, http_client, extensions=None):
- self.http_client = http_client
- http_client.add_client(self)
-
- # Add in any extensions...
- if extensions:
- for extension in extensions:
- if extension.manager_class:
- setattr(self, extension.name,
- extension.manager_class(self))
-
- def client_request(self, method, url, **kwargs):
- return self.http_client.client_request(
- self, method, url, **kwargs)
-
- @property
- def last_request_id(self):
- return self.http_client.last_request_id
-
- def head(self, url, **kwargs):
- return self.client_request("HEAD", url, **kwargs)
-
- def get(self, url, **kwargs):
- return self.client_request("GET", url, **kwargs)
-
- def post(self, url, **kwargs):
- return self.client_request("POST", url, **kwargs)
-
- def put(self, url, **kwargs):
- return self.client_request("PUT", url, **kwargs)
-
- def delete(self, url, **kwargs):
- return self.client_request("DELETE", url, **kwargs)
-
- def patch(self, url, **kwargs):
- return self.client_request("PATCH", url, **kwargs)
-
- @staticmethod
- def get_class(api_name, version, version_map):
- """Returns the client class for the requested API version.
-
- :param api_name: the name of the API, e.g. 'compute', 'image', etc
- :param version: the requested API version
- :param version_map: a dict of client classes keyed by version
- :rtype: a client class for the requested API version
- """
- try:
- client_path = version_map[str(version)]
- except (KeyError, ValueError):
- msg = _("Invalid %(api_name)s client version '%(version)s'. "
- "Must be one of: %(version_map)s") % {
- 'api_name': api_name,
- 'version': version,
- 'version_map': ', '.join(version_map.keys())}
- raise exceptions.UnsupportedVersion(msg)
-
- return importutils.import_class(client_path)
diff --git a/glanceclient/openstack/common/apiclient/fake_client.py b/glanceclient/openstack/common/apiclient/fake_client.py
deleted file mode 100644
index b152933..0000000
--- a/glanceclient/openstack/common/apiclient/fake_client.py
+++ /dev/null
@@ -1,187 +0,0 @@
-# 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.
-
-"""
-A fake server that "responds" to API methods with pre-canned responses.
-
-All of these responses come from the spec, so if for some reason the spec's
-wrong the tests might raise AssertionError. I've indicated in comments the
-places where actual behavior differs from the spec.
-"""
-
-########################################################################
-#
-# 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.
-#
-########################################################################
-
-# W0102: Dangerous default value %s as argument
-# pylint: disable=W0102
-
-import json
-
-import requests
-import six
-from six.moves.urllib import parse
-
-from glanceclient.openstack.common.apiclient import client
-
-
-def assert_has_keys(dct, required=None, optional=None):
- required = required or []
- optional = optional or []
- for k in required:
- try:
- assert k in dct
- except AssertionError:
- extra_keys = set(dct.keys()).difference(set(required + optional))
- raise AssertionError("found unexpected keys: %s" %
- list(extra_keys))
-
-
-class TestResponse(requests.Response):
- """Wrap requests.Response and provide a convenient initialization."""
-
- def __init__(self, data):
- super(TestResponse, self).__init__()
- self._content_consumed = True
- if isinstance(data, dict):
- self.status_code = data.get('status_code', 200)
- # Fake the text attribute to streamline Response creation
- text = data.get('text', "")
- if isinstance(text, (dict, list)):
- self._content = json.dumps(text)
- default_headers = {
- "Content-Type": "application/json",
- }
- else:
- self._content = text
- default_headers = {}
- if six.PY3 and isinstance(self._content, six.string_types):
- self._content = self._content.encode('utf-8', 'strict')
- self.headers = data.get('headers') or default_headers
- else:
- self.status_code = data
-
- def __eq__(self, other):
- return (self.status_code == other.status_code and
- self.headers == other.headers and
- self._content == other._content)
-
-
-class FakeHTTPClient(client.HTTPClient):
-
- def __init__(self, *args, **kwargs):
- self.callstack = []
- self.fixtures = kwargs.pop("fixtures", None) or {}
- if not args and "auth_plugin" not in kwargs:
- args = (None, )
- super(FakeHTTPClient, self).__init__(*args, **kwargs)
-
- def assert_called(self, method, url, body=None, pos=-1):
- """Assert than an API method was just called."""
- expected = (method, url)
- called = self.callstack[pos][0:2]
- assert self.callstack, \
- "Expected %s %s but no calls were made." % expected
-
- assert expected == called, 'Expected %s %s; got %s %s' % \
- (expected + called)
-
- if body is not None:
- if self.callstack[pos][3] != body:
- raise AssertionError('%r != %r' %
- (self.callstack[pos][3], body))
-
- def assert_called_anytime(self, method, url, body=None):
- """Assert than an API method was called anytime in the test."""
- expected = (method, url)
-
- assert self.callstack, \
- "Expected %s %s but no calls were made." % expected
-
- found = False
- entry = None
- for entry in self.callstack:
- if expected == entry[0:2]:
- found = True
- break
-
- assert found, 'Expected %s %s; got %s' % \
- (method, url, self.callstack)
- if body is not None:
- assert entry[3] == body, "%s != %s" % (entry[3], body)
-
- self.callstack = []
-
- def clear_callstack(self):
- self.callstack = []
-
- def authenticate(self):
- pass
-
- def client_request(self, client, method, url, **kwargs):
- # Check that certain things are called correctly
- if method in ["GET", "DELETE"]:
- assert "json" not in kwargs
-
- # Note the call
- self.callstack.append(
- (method,
- url,
- kwargs.get("headers") or {},
- kwargs.get("json") or kwargs.get("data")))
- try:
- fixture = self.fixtures[url][method]
- except KeyError:
- pass
- else:
- return TestResponse({"headers": fixture[0],
- "text": fixture[1]})
-
- # Call the method
- args = parse.parse_qsl(parse.urlparse(url)[4])
- kwargs.update(args)
- munged_url = url.rsplit('?', 1)[0]
- munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
- munged_url = munged_url.replace('-', '_')
-
- callback = "%s_%s" % (method.lower(), munged_url)
-
- if not hasattr(self, callback):
- raise AssertionError('Called unknown API method: %s %s, '
- 'expected fakes method name: %s' %
- (method, url, callback))
-
- resp = getattr(self, callback)(**kwargs)
- if len(resp) == 3:
- status, headers, body = resp
- else:
- status, body = resp
- headers = {}
- self.last_request_id = headers.get('x-openstack-request-id',
- 'req-test')
- return TestResponse({
- "status_code": status,
- "text": body,
- "headers": headers,
- })
diff --git a/glanceclient/shell.py b/glanceclient/shell.py
index 86e6107..51e02a6 100755..100644
--- a/glanceclient/shell.py
+++ b/glanceclient/shell.py
@@ -38,11 +38,11 @@ from glanceclient._i18n import _
from glanceclient.common import utils
from glanceclient import exc
-from keystoneclient.auth.identity import v2 as v2_auth
-from keystoneclient.auth.identity import v3 as v3_auth
-from keystoneclient import discover
-from keystoneclient import exceptions as ks_exc
-from keystoneclient import session
+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")
@@ -51,10 +51,14 @@ SUPPORTED_VERSIONS = [1, 2]
class OpenStackImagesShell(object):
- def _append_global_identity_args(self, parser):
+ def _append_global_identity_args(self, parser, argv):
# register common identity args
- session.Session.register_cli_options(parser)
- v3_auth.Password.register_argparse_arguments(parser)
+ 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',
@@ -68,17 +72,9 @@ class OpenStackImagesShell(object):
dest='os_cert',
help='DEPRECATED! Use --os-cert.')
- parser.add_argument('--os-tenant-id',
- default=utils.env('OS_TENANT_ID'),
- help='Defaults to env[OS_TENANT_ID].')
-
parser.add_argument('--os_tenant_id',
help=argparse.SUPPRESS)
- parser.add_argument('--os-tenant-name',
- default=utils.env('OS_TENANT_NAME'),
- help='Defaults to env[OS_TENANT_NAME].')
-
parser.add_argument('--os_tenant_name',
help=argparse.SUPPRESS)
@@ -110,7 +106,19 @@ class OpenStackImagesShell(object):
parser.add_argument('--os_endpoint_type',
help=argparse.SUPPRESS)
- def get_base_parser(self):
+ 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(),
@@ -194,12 +202,12 @@ class OpenStackImagesShell(object):
'the profiling will not be triggered even '
'if osprofiler is enabled on server side.')
- self._append_global_identity_args(parser)
+ self._append_global_identity_args(parser, argv)
return parser
- def get_subcommand_parser(self, version):
- parser = self.get_base_parser()
+ def get_subcommand_parser(self, version, argv=None):
+ parser = self.get_base_parser(argv)
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
@@ -214,7 +222,8 @@ class OpenStackImagesShell(object):
def _find_actions(self, subparsers, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
- # I prefer to be hypen-separated instead of underscores.
+ # Replace underscores with hyphens in the commands
+ # displayed to the user
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
@@ -260,7 +269,7 @@ class OpenStackImagesShell(object):
v2_auth_url = None
v3_auth_url = None
try:
- ks_discover = discover.Discover(session=session, auth_url=auth_url)
+ 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:
@@ -284,9 +293,7 @@ class OpenStackImagesShell(object):
return (v2_auth_url, v3_auth_url)
- def _get_keystone_session(self, **kwargs):
- ks_session = session.Session.construct(kwargs)
-
+ 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(
@@ -346,10 +353,9 @@ class OpenStackImagesShell(object):
"may not able to handle Keystone V3 credentials. "
"Please provide a correct Keystone V3 auth_url.")
- ks_session.auth = auth
- return ks_session
+ return auth
- def _get_kwargs_for_create_session(self, args):
+ def _get_kwargs_to_create_auth_plugin(self, args):
if not args.os_username:
raise exc.CommandError(
_("You must provide a username via"
@@ -374,16 +380,11 @@ class OpenStackImagesShell(object):
"or prompted response"))
# Validate password flow auth
- project_info = (
- args.os_tenant_name or args.os_tenant_id or (
- args.os_project_name and (
- args.os_project_domain_name or
- args.os_project_domain_id
- )
- ) or args.os_project_id
- )
-
- if not project_info:
+ 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(
@@ -416,10 +417,6 @@ class OpenStackImagesShell(object):
'project_id': args.os_project_id,
'project_domain_name': args.os_project_domain_name,
'project_domain_id': args.os_project_domain_id,
- 'insecure': args.insecure,
- 'cacert': args.os_cacert,
- 'cert': args.os_cert,
- 'key': args.os_key
}
return kwargs
@@ -427,9 +424,7 @@ class OpenStackImagesShell(object):
endpoint = self._get_image_url(args)
auth_token = args.os_auth_token
- auth_req = (hasattr(args, 'func') and
- utils.is_authentication_required(args.func))
- if not auth_req or (endpoint and auth_token):
+ if endpoint and auth_token:
kwargs = {
'token': auth_token,
'insecure': args.insecure,
@@ -440,8 +435,19 @@ class OpenStackImagesShell(object):
'ssl_compression': args.ssl_compression
}
else:
- kwargs = self._get_kwargs_for_create_session(args)
- kwargs = {'session': self._get_keystone_session(**kwargs)}
+ 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)
@@ -457,7 +463,7 @@ class OpenStackImagesShell(object):
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
+ # it happens in Jenkins, as glanceclient can't write to
# /var/lib/jenkins).
msg = '%s' % e
print(encodeutils.safe_decode(msg), file=sys.stderr)
@@ -487,27 +493,21 @@ class OpenStackImagesShell(object):
def _get_subparser(api_version):
try:
- return self.get_subcommand_parser(api_version)
+ return self.get_subcommand_parser(api_version, argv)
except ImportError as e:
- if options.debug:
- traceback.print_exc()
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
- except Exception:
- if options.debug:
- traceback.print_exc()
- 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()
+ parser = self.get_base_parser(argv)
(options, args) = parser.parse_known_args(base_argv)
try:
@@ -519,7 +519,7 @@ class OpenStackImagesShell(object):
endpoint = self._get_image_url(options)
endpoint, url_version = utils.strip_version(endpoint)
except ValueError:
- # NOTE(flaper87): ValueError is raised if no endpoint is povided
+ # NOTE(flaper87): ValueError is raised if no endpoint is provided
url_version = None
# build available subcommands based on version
@@ -535,6 +535,7 @@ class OpenStackImagesShell(object):
# 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
@@ -581,6 +582,12 @@ class OpenStackImagesShell(object):
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)
@@ -595,12 +602,6 @@ class OpenStackImagesShell(object):
args.func(client, args)
except exc.Unauthorized:
raise exc.CommandError("Invalid OpenStack Identity credentials.")
- except Exception:
- # NOTE(kragniz) Print any exceptions raised to stderr if the
- # --debug flag is set
- if args.debug:
- traceback.print_exc()
- raise
finally:
if profile:
trace_id = osprofiler_profiler.get().get_base_id()
@@ -671,4 +672,6 @@ def main():
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))
diff --git a/glanceclient/tests/functional/base.py b/glanceclient/tests/functional/base.py
index 6029a06..a6306bf 100644
--- a/glanceclient/tests/functional/base.py
+++ b/glanceclient/tests/functional/base.py
@@ -13,7 +13,7 @@
import os
import os_client_config
-from tempest_lib.cli import base
+from tempest.lib.cli import base
def credentials(cloud='devstack-admin'):
diff --git a/glanceclient/tests/functional/test_readonly_glance.py b/glanceclient/tests/functional/test_readonly_glance.py
index 9c9989d..822bbcc 100644
--- a/glanceclient/tests/functional/test_readonly_glance.py
+++ b/glanceclient/tests/functional/test_readonly_glance.py
@@ -12,7 +12,7 @@
import re
-from tempest_lib import exceptions
+from tempest.lib import exceptions
from glanceclient.tests.functional import base
@@ -52,7 +52,7 @@ class SimpleReadOnlyGlanceClientTest(base.ClientTestBase):
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, thouhg it
+ # 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')
diff --git a/glanceclient/tests/unit/test_base.py b/glanceclient/tests/unit/test_base.py
index ddbc3d7..43bb71d 100644
--- a/glanceclient/tests/unit/test_base.py
+++ b/glanceclient/tests/unit/test_base.py
@@ -16,7 +16,7 @@
import testtools
-from glanceclient.openstack.common.apiclient import base
+from glanceclient.v1.apiclient import base
class TestBase(testtools.TestCase):
diff --git a/glanceclient/tests/unit/test_exc.py b/glanceclient/tests/unit/test_exc.py
index 575c62b..9a2d01f 100644
--- a/glanceclient/tests/unit/test_exc.py
+++ b/glanceclient/tests/unit/test_exc.py
@@ -68,3 +68,11 @@ class TestHTTPExceptions(testtools.TestCase):
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)
diff --git a/glanceclient/tests/unit/test_http.py b/glanceclient/tests/unit/test_http.py
index c18660e..ae19231 100644
--- a/glanceclient/tests/unit/test_http.py
+++ b/glanceclient/tests/unit/test_http.py
@@ -15,8 +15,8 @@
import functools
import json
-from keystoneclient.auth import token_endpoint
-from keystoneclient import session
+from keystoneauth1 import session
+from keystoneauth1 import token_endpoint
import mock
import requests
from requests_mock.contrib import fixture
@@ -157,10 +157,10 @@ class TestClient(testtools.TestCase):
http_client.get(path)
headers = self.mock.last_request.headers
- self.assertTrue('Accept-Language' not in headers)
+ self.assertNotIn('Accept-Language', headers)
def test_connection_timeout(self):
- """Should receive an InvalidEndpoint if connection timeout."""
+ """Verify a InvalidEndpoint is received if connection times out."""
def cb(request, context):
raise requests.exceptions.Timeout
@@ -172,11 +172,9 @@ class TestClient(testtools.TestCase):
self.assertIn(self.endpoint, comm_err.message)
def test_connection_refused(self):
- """
+ """Verify a CommunicationError is received if connection is refused.
- Should receive a CommunicationError if connection refused.
- And the error should list the host and port that refused the
- connection
+ The error should list the host and port that refused the connection.
"""
def cb(request, context):
raise requests.exceptions.ConnectionError()
@@ -201,23 +199,12 @@ class TestClient(testtools.TestCase):
self.assertEqual(text, resp.text)
def test_headers_encoding(self):
- if not hasattr(self.client, 'encode_headers'):
- self.skipTest('Cannot do header encoding check on SessionClient')
-
value = u'ni\xf1o'
headers = {"test": value, "none-val": None}
- encoded = self.client.encode_headers(headers)
+ encoded = http.encode_headers(headers)
self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"])
self.assertNotIn("none-val", encoded)
- def test_auth_token_header_encoding(self):
- # Tests that X-Auth-Token header is converted to ascii string, as
- # httplib in python 2.6 won't do the conversion
- value = u'ni\xf1o'
- http_client_object = http.HTTPClient(self.endpoint, token=value)
- self.assertEqual(b'ni\xc3\xb1o',
- http_client_object.session.headers['X-Auth-Token'])
-
def test_raw_request(self):
"""Verify the path being used for HTTP requests reflects accurately."""
headers = {"Content-Type": "text/plain"}
@@ -242,7 +229,15 @@ class TestClient(testtools.TestCase):
def test_get_connections_kwargs_http(self):
endpoint = 'http://example.com:9292'
test_client = http.HTTPClient(endpoint, token=u'adc123')
- self.assertEqual(test_client.timeout, 600.0)
+ 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"
@@ -382,3 +377,26 @@ class TestClient(testtools.TestCase):
self.assertThat(mock_log.call_args[0][0],
matchers.Not(matchers.MatchesRegex(token_regex)),
'token found in LOG.debug parameter')
+
+ 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'])
diff --git a/glanceclient/tests/unit/test_shell.py b/glanceclient/tests/unit/test_shell.py
index 2a33dc0..d175852 100644
--- a/glanceclient/tests/unit/test_shell.py
+++ b/glanceclient/tests/unit/test_shell.py
@@ -20,21 +20,24 @@ try:
except ImportError:
from ordereddict import OrderedDict
import hashlib
+import logging
import os
import sys
+import traceback
import uuid
import fixtures
-from keystoneclient import exceptions as ks_exc
-from keystoneclient import fixture as ks_fixture
+from keystoneauth1 import exceptions as ks_exc
+from keystoneauth1 import fixture as ks_fixture
import mock
-import requests
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
@@ -42,7 +45,8 @@ from glanceclient.v2 import schemas as schemas
import json
-DEFAULT_IMAGE_URL = 'http://127.0.0.1:5000/'
+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'
@@ -54,6 +58,8 @@ 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,
@@ -68,6 +74,15 @@ FAKE_V3_ENV = {'OS_USERNAME': DEFAULT_USERNAME,
'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)
@@ -78,7 +93,8 @@ _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)
+_s.add_standard_endpoints(public=DEFAULT_IMAGE_URL,
+ internal=DEFAULT_IMAGE_URL_INTERNAL)
class ShellTest(testutils.TestCase):
@@ -100,7 +116,9 @@ class ShellTest(testutils.TestCase):
self.requests = self.useFixture(rm_fixture.Fixture())
json_list = ks_fixture.DiscoveryList(DEFAULT_UNVERSIONED_AUTH_URL)
- self.requests.get(DEFAULT_IMAGE_URL, json=json_list, status_code=300)
+ 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)
@@ -150,17 +168,55 @@ class ShellTest(testutils.TestCase):
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_session') as et_mock:
+ 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_session') as et_mock:
+ with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
actual = shell.main('')
self.assertEqual(0, actual)
self.assertFalse(et_mock.called)
@@ -172,7 +228,21 @@ class ShellTest(testutils.TestCase):
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_session') as et_mock:
+ with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
+ actual = shell.main(argstr.split())
+ self.assertEqual(0, actual)
+ self.assertNotIn('<unavailable>', 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('<unavailable>', 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('<unavailable>', actual)
@@ -180,7 +250,9 @@ class ShellTest(testutils.TestCase):
def test_get_base_parser(self):
test_shell = openstack_shell.OpenStackImagesShell()
- actual_parser = test_shell.get_base_parser()
+ # 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,
@@ -328,18 +400,6 @@ class ShellTest(testutils.TestCase):
glance_shell.main(args)
self.assertEqual(1, mock_client.call_count)
- @mock.patch('glanceclient.v2.client.Client')
- def test_password_prompted_with_v2(self, v2_client):
- self.requests.post(self.token_url, exc=requests.ConnectionError)
-
- cli2 = mock.MagicMock()
- v2_client.return_value = cli2
- cli2.http_client.get.return_value = (None, {'versions': []})
- glance_shell = openstack_shell.OpenStackImagesShell()
- os.environ['OS_PASSWORD'] = 'password'
- self.assertRaises(exc.CommunicationError,
- glance_shell.main, ['image-list'])
-
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
@mock.patch('getpass.getpass', side_effect=EOFError)
@mock.patch('glanceclient.v2.client.Client')
@@ -357,7 +417,7 @@ class ShellTest(testutils.TestCase):
mock_getpass.assert_called_with('OS Password: ')
@mock.patch(
- 'glanceclient.shell.OpenStackImagesShell._get_keystone_session')
+ '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):
@@ -523,9 +583,28 @@ class ShellTest(testutils.TestCase):
except SystemExit:
self.fail('Unexpected SystemExit')
- # We expect the normal usage as a result
- self.assertIn('Command-line interface to the OpenStack Images API',
- sys.stdout.getvalue())
+ # 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):
@@ -560,7 +639,7 @@ class ShellTestWithKeystoneV3Auth(ShellTest):
glance_shell.main(args.split())
self.assertEqual(0, self.v3_auth.call_count)
- @mock.patch('keystoneclient.discover.Discover',
+ @mock.patch('keystoneauth1.discover.Discover',
side_effect=ks_exc.ClientException())
def test_api_discovery_failed_with_unversioned_auth_url(self,
discover):
@@ -586,6 +665,65 @@ class ShellTestWithKeystoneV3Auth(ShellTest):
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()
@@ -709,4 +847,116 @@ class ShellCacheSchemaTest(testutils.TestCase):
switch_version = self.shell._cache_schemas(self._make_args(options),
client,
home_dir=self.cache_dir)
- self.assertEqual(switch_version, True)
+ 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
diff --git a/glanceclient/tests/unit/v1/test_images.py b/glanceclient/tests/unit/v1/test_images.py
index 1de85ce..1f43b83 100644
--- a/glanceclient/tests/unit/v1/test_images.py
+++ b/glanceclient/tests/unit/v1/test_images.py
@@ -669,8 +669,8 @@ class ImageManagerTest(testtools.TestCase):
'x-image-meta-property-c': 'd',
}
expect = [('POST', '/v1/images', expect_headers, None)]
- self.assertEqual(self.api.calls, expect)
- self.assertEqual(image.id, '1')
+ 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'])
@@ -738,7 +738,7 @@ class ImageManagerTest(testtools.TestCase):
self.mgr.update('4', **fields)
expect_headers = {'x-glance-registry-purge-props': 'true'}
expect = [('PUT', '/v1/images/4', expect_headers, None)]
- self.assertEqual(self.api.calls, expect)
+ self.assertEqual(expect, self.api.calls)
expect_req_id = ['req-1234']
self.assertEqual(expect_req_id, fields['return_req_id'])
@@ -765,7 +765,7 @@ class ImageManagerTest(testtools.TestCase):
}
images = self.mgr.list(**fields)
next(images)
- self.assertEqual(fields['return_req_id'], ['req-1234'])
+ 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)
diff --git a/glanceclient/tests/unit/v1/test_shell.py b/glanceclient/tests/unit/v1/test_shell.py
index dd3e3cf..95bbd07 100644
--- a/glanceclient/tests/unit/v1/test_shell.py
+++ b/glanceclient/tests/unit/v1/test_shell.py
@@ -440,11 +440,10 @@ class ShellInvalidEndpointandParameterTest(utils.TestCase):
class ShellStdinHandlingTests(testtools.TestCase):
def _fake_update_func(self, *args, **kwargs):
- """
+ """Replace glanceclient.images.update with a fake.
- Function to replace glanceclient.images.update,
- to determine the parameters that would be supplied with the update
- request
+ To determine the parameters that would be supplied with the update
+ request.
"""
# Store passed in args
@@ -523,7 +522,7 @@ class ShellStdinHandlingTests(testtools.TestCase):
)
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
@@ -542,7 +541,7 @@ class ShellStdinHandlingTests(testtools.TestCase):
)
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.
@@ -575,7 +574,7 @@ class ShellStdinHandlingTests(testtools.TestCase):
self.assertIn('data', self.collected_args[1])
self.assertIsInstance(self.collected_args[1]['data'], file_type)
- self.assertEqual('Some Data',
+ self.assertEqual(b'Some Data',
self.collected_args[1]['data'].read())
finally:
@@ -600,7 +599,7 @@ class ShellStdinHandlingTests(testtools.TestCase):
self.assertIn('data', self.collected_args[1])
self.assertIsInstance(self.collected_args[1]['data'], file_type)
- self.assertEqual('Some Data\n',
+ self.assertEqual(b'Some Data\n',
self.collected_args[1]['data'].read())
finally:
diff --git a/glanceclient/tests/unit/v2/fixtures.py b/glanceclient/tests/unit/v2/fixtures.py
index 703d43b..e4ece7e 100644
--- a/glanceclient/tests/unit/v2/fixtures.py
+++ b/glanceclient/tests/unit/v2/fixtures.py
@@ -90,8 +90,8 @@ schema_fixture = {
"properties": {
"architecture": {
"description": "Operating system architecture as specified in "
- "http://docs.openstack.org/trunk/openstack-compute"
- "/admin/content/adding-images.html",
+ "http://docs.openstack.org/user-guide/common"
+ "/cli_manage_images.html",
"is_base": "false",
"type": "string"
},
@@ -319,3 +319,68 @@ schema_fixture = {
}
}
}
+
+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"
+ }
+ ]
+}
diff --git a/glanceclient/tests/unit/v2/test_images.py b/glanceclient/tests/unit/v2/test_images.py
index b095634..0ae3836 100644
--- a/glanceclient/tests/unit/v2/test_images.py
+++ b/glanceclient/tests/unit/v2/test_images.py
@@ -1056,10 +1056,8 @@ class TestController(testtools.TestCase):
new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}}
add_patch = {'path': '/locations/-', 'value': new_loc, 'op': 'add'}
self.controller.add_location(image_id, **new_loc)
- self.assertEqual(self.api.calls, [
- self._patch_req(image_id, [add_patch]),
- self._empty_get(image_id)
- ])
+ self.assertEqual([self._patch_req(image_id, [add_patch]),
+ self._empty_get(image_id)], self.api.calls)
@mock.patch.object(images.Controller, '_send_image_update_request',
side_effect=exc.HTTPBadRequest)
@@ -1077,10 +1075,9 @@ class TestController(testtools.TestCase):
del_patches = [{'path': '/locations/1', 'op': 'remove'},
{'path': '/locations/0', 'op': 'remove'}]
self.controller.delete_locations(image_id, url_set)
- self.assertEqual(self.api.calls, [
- self._empty_get(image_id),
- self._patch_req(image_id, del_patches)
- ])
+ 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'
@@ -1100,15 +1097,12 @@ class TestController(testtools.TestCase):
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': []},
- {'path': '/locations', 'op': 'replace',
'value': list(loc_map.values())}]
self.controller.update_location(image_id, **new_loc)
- self.assertEqual(self.api.calls, [
- self._empty_get(image_id),
- self._patch_req(image_id, mod_patch),
- self._empty_get(image_id)
- ])
+ self.assertEqual([self._empty_get(image_id),
+ self._patch_req(image_id, mod_patch),
+ self._empty_get(image_id)],
+ self.api.calls)
def test_update_tags(self):
image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
diff --git a/glanceclient/tests/unit/v2/test_schemas.py b/glanceclient/tests/unit/v2/test_schemas.py
index 60442a8..c01d8bd 100644
--- a/glanceclient/tests/unit/v2/test_schemas.py
+++ b/glanceclient/tests/unit/v2/test_schemas.py
@@ -130,7 +130,7 @@ class TestSchemaBasedModel(testtools.TestCase):
def setUp(self):
super(TestSchemaBasedModel, self).setUp()
self.model = warlock.model_factory(_SCHEMA.raw(),
- schemas.SchemaBasedModel)
+ base_class=schemas.SchemaBasedModel)
def test_patch_should_replace_missing_core_properties(self):
obj = {
diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py
index cddb925..e79a42c 100644
--- a/glanceclient/tests/unit/v2/test_shell_v2.py
+++ b/glanceclient/tests/unit/v2/test_shell_v2.py
@@ -644,6 +644,20 @@ class ShellV2Test(testtools.TestCase):
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])
diff --git a/glanceclient/tests/unit/v2/test_tasks.py b/glanceclient/tests/unit/v2/test_tasks.py
index 349a880..860b569 100644
--- a/glanceclient/tests/unit/v2/test_tasks.py
+++ b/glanceclient/tests/unit/v2/test_tasks.py
@@ -257,45 +257,45 @@ class TestController(testtools.TestCase):
def test_list_tasks(self):
# NOTE(flwang): cast to list since the controller returns a generator
tasks = list(self.controller.list())
- self.assertEqual(tasks[0].id, _PENDING_ID)
- self.assertEqual(tasks[0].type, 'import')
- self.assertEqual(tasks[0].status, 'pending')
- self.assertEqual(tasks[1].id, _PROCESSING_ID)
- self.assertEqual(tasks[1].type, 'import')
- self.assertEqual(tasks[1].status, 'processing')
+ 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):
# NOTE(flwang): cast to list since the controller returns a generator
tasks = list(self.controller.list(page_size=1))
- self.assertEqual(tasks[0].id, _PENDING_ID)
- self.assertEqual(tasks[0].type, 'import')
- self.assertEqual(tasks[1].id, _PROCESSING_ID)
- self.assertEqual(tasks[1].type, 'import')
+ 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 = list(self.controller.list(**filters))
- self.assertEqual(tasks[0].id, _OWNED_TASK_ID)
+ self.assertEqual(_OWNED_TASK_ID, tasks[0].id)
def test_list_tasks_with_wrong_status(self):
filters = {'filters': {'status': 'fake'}}
tasks = list(self.controller.list(**filters))
- self.assertEqual(len(tasks), 0)
+ self.assertEqual(0, len(tasks))
def test_list_tasks_with_type(self):
filters = {'filters': {'type': 'import'}}
tasks = list(self.controller.list(**filters))
- self.assertEqual(tasks[0].id, _OWNED_TASK_ID)
+ self.assertEqual(_OWNED_TASK_ID, tasks[0].id)
def test_list_tasks_with_wrong_type(self):
filters = {'filters': {'type': 'fake'}}
tasks = list(self.controller.list(**filters))
- self.assertEqual(len(tasks), 0)
+ self.assertEqual(0, len(tasks))
def test_list_tasks_for_owner(self):
filters = {'filters': {'owner': _OWNER_ID}}
tasks = list(self.controller.list(**filters))
- self.assertEqual(tasks[0].id, _OWNED_TASK_ID)
+ self.assertEqual(_OWNED_TASK_ID, tasks[0].id)
def test_list_tasks_for_fake_owner(self):
filters = {'filters': {'owner': _FAKE_OWNER_ID}}
@@ -347,8 +347,8 @@ class TestController(testtools.TestCase):
def test_get_task(self):
task = self.controller.get(_PENDING_ID)
- self.assertEqual(task.id, _PENDING_ID)
- self.assertEqual(task.type, 'import')
+ self.assertEqual(_PENDING_ID, task.id)
+ self.assertEqual('import', task.type)
def test_create_task(self):
properties = {
@@ -357,8 +357,8 @@ class TestController(testtools.TestCase):
'swift://cloud.foo/myaccount/mycontainer/path'},
}
task = self.controller.create(**properties)
- self.assertEqual(task.id, _PENDING_ID)
- self.assertEqual(task.type, 'import')
+ self.assertEqual(_PENDING_ID, task.id)
+ self.assertEqual('import', task.type)
def test_create_task_invalid_property(self):
properties = {
diff --git a/glanceclient/tests/utils.py b/glanceclient/tests/utils.py
index 2dc510c..6b03f31 100644
--- a/glanceclient/tests/utils.py
+++ b/glanceclient/tests/utils.py
@@ -74,7 +74,7 @@ class FakeSchemaAPI(FakeAPI):
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
@@ -101,7 +101,7 @@ class RawRequest(object):
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
@@ -118,6 +118,10 @@ class FakeResponse(object):
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)
@@ -151,6 +155,9 @@ class FakeResponse(object):
break
yield chunk
+ def release_conn(self, **kwargs):
+ pass
+
class TestCase(testtools.TestCase):
TEST_REQUEST_BASE = {
diff --git a/glanceclient/openstack/__init__.py b/glanceclient/v1/apiclient/__init__.py
index e69de29..e69de29 100644
--- a/glanceclient/openstack/__init__.py
+++ b/glanceclient/v1/apiclient/__init__.py
diff --git a/glanceclient/openstack/common/apiclient/base.py b/glanceclient/v1/apiclient/base.py
index c86613e..cb48096 100644
--- a/glanceclient/openstack/common/apiclient/base.py
+++ b/glanceclient/v1/apiclient/base.py
@@ -45,7 +45,7 @@ import six
from six.moves.urllib import parse
from glanceclient._i18n import _
-from glanceclient.openstack.common.apiclient import exceptions
+from glanceclient.v1.apiclient import exceptions
def getid(obj):
diff --git a/glanceclient/openstack/common/apiclient/exceptions.py b/glanceclient/v1/apiclient/exceptions.py
index 5bda5f0..5bda5f0 100644
--- a/glanceclient/openstack/common/apiclient/exceptions.py
+++ b/glanceclient/v1/apiclient/exceptions.py
diff --git a/glanceclient/openstack/common/apiclient/utils.py b/glanceclient/v1/apiclient/utils.py
index e5d6926..814a37b 100644
--- a/glanceclient/openstack/common/apiclient/utils.py
+++ b/glanceclient/v1/apiclient/utils.py
@@ -29,7 +29,7 @@ from oslo_utils import uuidutils
import six
from glanceclient._i18n import _
-from glanceclient.openstack.common.apiclient import exceptions
+from glanceclient.v1.apiclient import exceptions
def find_resource(manager, name_or_id, **find_args):
@@ -84,17 +84,15 @@ def find_resource(manager, name_or_id, **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.") % \
- {
+ "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)
diff --git a/glanceclient/v1/image_members.py b/glanceclient/v1/image_members.py
index 79242b5..144eeb5 100644
--- a/glanceclient/v1/image_members.py
+++ b/glanceclient/v1/image_members.py
@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-from glanceclient.openstack.common.apiclient import base
+from glanceclient.v1.apiclient import base
class ImageMember(base.Resource):
diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py
index 14c1921..182f1e5 100644
--- a/glanceclient/v1/images.py
+++ b/glanceclient/v1/images.py
@@ -21,7 +21,7 @@ import six
import six.moves.urllib.parse as urlparse
from glanceclient.common import utils
-from glanceclient.openstack.common.apiclient import base
+from glanceclient.v1.apiclient import base
UPDATE_PARAMS = ('name', 'disk_format', 'container_format', 'min_disk',
'min_ram', 'owner', 'size', 'is_public', 'protected',
@@ -202,7 +202,7 @@ class ImageManager(base.ManagerWithFind):
:param owner: If provided, only images with this owner (tenant id)
will be listed. An empty string ('') matches ownerless
images.
- :param return_request_id: If an empty list is provided, populate this
+ :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`
diff --git a/glanceclient/v1/shell.py b/glanceclient/v1/shell.py
index 3372951..eb14f6e 100644
--- a/glanceclient/v1/shell.py
+++ b/glanceclient/v1/shell.py
@@ -155,7 +155,7 @@ def do_image_show(gc, args):
@utils.arg('--file', metavar='<FILE>',
help='Local file to save downloaded image data to. '
'If this is not specified and there is no redirection '
- 'the image data will be not be saved.')
+ 'the image data will not be saved.')
@utils.arg('image', metavar='<IMAGE>', help='Name or ID of image to download.')
@utils.arg('--progress', action='store_true', default=False,
help='Show download progress bar.')
diff --git a/glanceclient/v1/versions.py b/glanceclient/v1/versions.py
index d65c2f6..fe4253f 100644
--- a/glanceclient/v1/versions.py
+++ b/glanceclient/v1/versions.py
@@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-from glanceclient.openstack.common.apiclient import base
+from glanceclient.v1.apiclient import base
class VersionManager(base.ManagerWithFind):
diff --git a/glanceclient/v2/image_schema.py b/glanceclient/v2/image_schema.py
index 1e1d3bf..d31f0f5 100644
--- a/glanceclient/v2/image_schema.py
+++ b/glanceclient/v2/image_schema.py
@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-_doc_url = "http://docs.openstack.org/trunk/openstack-compute/admin/content/adding-images.html" # noqa
+_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
@@ -22,32 +22,21 @@ _BASE_SCHEMA = {
"type": "string"
},
"name": "image",
- "links": [
- {
- "href": "{self}",
- "rel": "self"
- },
- {
- "href": "{file}",
- "rel": "enclosure"
- },
- {
- "href": "{schema}",
- "rel": "describedby"
- }
- ],
+ "links": [{
+ "href": "{self}",
+ "rel": "self"
+ }, {
+ "href": "{file}",
+ "rel": "enclosure"
+ }, {
+ "href": "{schema}",
+ "rel": "describedby"
+ }],
"properties": {
"container_format": {
- "enum": [
- "ami",
- "ari",
- "aki",
- "bare",
- "ovf",
- "ova",
- "docker"
- ],
- "type": "string",
+ "enum": [None, "ami", "ari", "aki", "bare", "ovf", "ova",
+ "docker"],
+ "type": ["null", "string"],
"description": "Format of the container"
},
"min_ram": {
@@ -58,17 +47,15 @@ _BASE_SCHEMA = {
"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",
+ "type": ["null", "string"],
"description": ("ID of image stored in Glance that should be "
"used as the ramdisk when booting an AMI-style "
- "image.")
+ "image."),
+ "is_base": False
},
"locations": {
"items": {
- "required": [
- "url",
- "metadata"
- ],
+ "required": ["url", "metadata"],
"type": "object",
"properties": {
"url": {
@@ -85,12 +72,12 @@ _BASE_SCHEMA = {
"file kept in external store")
},
"file": {
- "type": "string",
"readOnly": True,
- "description": ("An image file url")
+ "type": "string",
+ "description": "An image file url"
},
"owner": {
- "type": "string",
+ "type": ["null", "string"],
"description": "Owner of the image",
"maxLength": 255
},
@@ -102,62 +89,49 @@ _BASE_SCHEMA = {
"description": "An identifier for the image"
},
"size": {
- "type": "integer",
"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)
+ "as specified in %s" % _doc_url),
+ "is_base": False
},
"self": {
- "type": "string",
"readOnly": True,
- "description": ("An image self url")
+ "type": "string",
+ "description": "An image self url"
},
"disk_format": {
- "enum": [
- "ami",
- "ari",
- "aki",
- "vhd",
- "vmdk",
- "raw",
- "qcow2",
- "vdi",
- "iso"
- ],
- "type": "string",
+ "enum": [None, "ami", "ari", "aki", "vhd", "vmdk", "raw",
+ "qcow2", "vdi", "iso"],
+ "type": ["null", "string"],
"description": "Format of the disk"
},
"os_version": {
"type": "string",
- "description": ("Operating system version as "
- "specified by the distributor")
+ "description": "Operating system version as specified by the"
+ " distributor",
+ "is_base": False
},
"direct_url": {
- "type": "string",
"readOnly": True,
- "description": ("URL to access the image file kept in "
- "external store")
+ "type": "string",
+ "description": "URL to access the image file kept in external"
+ " store"
},
"schema": {
- "type": "string",
"readOnly": True,
- "description": ("An image schema url")
+ "type": "string",
+ "description": "An image schema url"
},
"status": {
- "enum": [
- "queued",
- "saving",
- "active",
- "killed",
- "deleted",
- "pending_delete"
- ],
- "type": "string",
"readOnly": True,
+ "enum": ["queued", "saving", "active", "killed", "deleted",
+ "pending_delete", "deactivated"],
+ "type": "string",
"description": "Status of the image"
},
"tags": {
@@ -169,60 +143,57 @@ _BASE_SCHEMA = {
"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": "string",
- "description": ("ID of image stored in Glance that should be "
- "used as the kernel when booting an AMI-style "
- "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": ["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"
- ],
+ "enum": ["public", "private"],
"type": "string",
"description": "Scope of image accessibility"
},
"updated_at": {
- "type": "string",
"readOnly": True,
- "description": ("Date and time of the last "
- "image modification")
+ "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.")
+ "description": "Amount of disk space (in GB) required to boot "
+ "image."
},
"virtual_size": {
- "type": "integer",
"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.)")
+ "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": "string",
+ "type": ["null", "string"],
"description": "Descriptive name for the image",
"maxLength": 255
},
"checksum": {
- "type": "string",
"readOnly": True,
+ "type": ["null", "string"],
"description": "md5 hash of image contents.",
"maxLength": 32
},
"created_at": {
- "type": "string",
"readOnly": True,
- "description": "Date and time of image registration "
+ "type": "string",
+ "description": "Date and time of image registration"
},
"protected": {
"type": "boolean",
@@ -231,7 +202,8 @@ _BASE_SCHEMA = {
"architecture": {
"type": "string",
"description": ("Operating system architecture as specified "
- "in %s" % _doc_url)
+ "in %s" % _doc_url),
+ "is_base": False
}
}
}
diff --git a/glanceclient/v2/image_tags.py b/glanceclient/v2/image_tags.py
index bcecd01..deebce2 100644
--- a/glanceclient/v2/image_tags.py
+++ b/glanceclient/v2/image_tags.py
@@ -27,7 +27,8 @@ class Controller(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('image')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
def update(self, image_id, tag_value):
"""Update an image with the given tag.
diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py
index ed6a001..f69fed5 100644
--- a/glanceclient/v2/images.py
+++ b/glanceclient/v2/images.py
@@ -39,16 +39,16 @@ class Controller(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('image')
- warlock_model = warlock.model_factory(schema.raw(),
- schemas.SchemaBasedModel)
+ 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(),
- schemas.SchemaBasedModel)
+ warlock_model = warlock.model_factory(
+ schema.raw(), base_class=schemas.SchemaBasedModel)
warlock_model.validate = lambda *args, **kwargs: None
return warlock_model
@@ -253,7 +253,7 @@ class Controller(object):
: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.
+ :param kwargs: Image attribute names and their new values.
"""
unvalidated_image = self.get(image_id)
image = self.model(**unvalidated_image)
@@ -348,20 +348,17 @@ class Controller(object):
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' % url)
+ 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
- # NOTE: The server (as of now) doesn't support modifying individual
- # location entries. So we must:
- # 1. Empty existing list of locations.
- # 2. Send another request to set 'locations' to the new list
- # of locations.
url_map[url]['metadata'] = metadata
patches = [{'op': 'replace',
'path': '/locations',
- 'value': p} for p in ([], list(url_map.values()))]
+ 'value': list(url_map.values())}]
self._send_image_update_request(image_id, patches)
return self.get(image_id)
diff --git a/glanceclient/v2/metadefs.py b/glanceclient/v2/metadefs.py
index 2344e33..4bee224 100644
--- a/glanceclient/v2/metadefs.py
+++ b/glanceclient/v2/metadefs.py
@@ -34,7 +34,8 @@ class NamespaceController(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/namespace')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
def create(self, **kwargs):
"""Create a namespace.
@@ -102,7 +103,8 @@ class NamespaceController(object):
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
+ :returns: generator over list of Namespaces
+
"""
ori_validate_fun = self.model.validate
@@ -185,7 +187,8 @@ class ResourceTypeController(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/resource_type')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
def associate(self, namespace, **kwargs):
"""Associate a resource type with a namespace."""
@@ -201,7 +204,7 @@ class ResourceTypeController(object):
return self.model(**body)
def deassociate(self, namespace, resource):
- """Deasociate a resource type with a namespace."""
+ """Deassociate a resource type with a namespace."""
url = '/v2/metadefs/namespaces/{0}/resource_types/{1}'. \
format(namespace, resource)
self.http_client.delete(url)
@@ -209,7 +212,7 @@ class ResourceTypeController(object):
def list(self):
"""Retrieve a listing of available resource types.
- :returns generator over list of resource_types
+ :returns: generator over list of resource_types
"""
url = '/v2/metadefs/resource_types'
@@ -233,7 +236,8 @@ class PropertyController(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/property')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
def create(self, namespace, **kwargs):
"""Create a property.
@@ -283,7 +287,7 @@ class PropertyController(object):
def list(self, namespace, **kwargs):
"""Retrieve a listing of metadata properties.
- :returns generator over list of objects
+ :returns: generator over list of objects
"""
url = '/v2/metadefs/namespaces/{0}/properties'.format(namespace)
@@ -313,7 +317,8 @@ class ObjectController(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/object')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
def create(self, namespace, **kwargs):
"""Create an object.
@@ -336,7 +341,7 @@ class ObjectController(object):
"""Update an object.
:param namespace: Name of a namespace the object belongs.
- :param prop_name: Name of an object (old one).
+ :param object_name: Name of an object (old one).
:param kwargs: Unpacked object.
"""
obj = self.get(namespace, object_name)
@@ -368,7 +373,7 @@ class ObjectController(object):
def list(self, namespace, **kwargs):
"""Retrieve a listing of metadata objects.
- :returns generator over list of objects
+ :returns: generator over list of objects
"""
url = '/v2/metadefs/namespaces/{0}/objects'.format(namespace,)
resp, body = self.http_client.get(url)
@@ -396,7 +401,8 @@ class TagController(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/tag')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
def create(self, namespace, tag_name):
"""Create a tag.
@@ -440,7 +446,7 @@ class TagController(object):
"""Update a tag.
:param namespace: Name of a namespace the Tag belongs.
- :param prop_name: Name of the Tag (old one).
+ :param tag_name: Name of the Tag (old one).
:param kwargs: Unpacked tag.
"""
tag = self.get(namespace, tag_name)
@@ -472,7 +478,7 @@ class TagController(object):
def list(self, namespace, **kwargs):
"""Retrieve a listing of metadata tags.
- :returns generator over list of tags.
+ :returns: generator over list of tags.
"""
url = '/v2/metadefs/namespaces/{0}/tags'.format(namespace)
resp, body = self.http_client.get(url)
diff --git a/glanceclient/v2/namespace_schema.py b/glanceclient/v2/namespace_schema.py
new file mode 100644
index 0000000..36c8833
--- /dev/null
+++ b/glanceclient/v2/namespace_schema.py
@@ -0,0 +1,243 @@
+# 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"
+ },
+ }
+}
diff --git a/glanceclient/v2/resource_type_schema.py b/glanceclient/v2/resource_type_schema.py
new file mode 100644
index 0000000..8ad04bf
--- /dev/null
+++ b/glanceclient/v2/resource_type_schema.py
@@ -0,0 +1,67 @@
+# 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"
+ }
+ }
+}
diff --git a/glanceclient/v2/schemas.py b/glanceclient/v2/schemas.py
index 9ba72c3..8247d31 100644
--- a/glanceclient/v2/schemas.py
+++ b/glanceclient/v2/schemas.py
@@ -71,7 +71,7 @@ class SchemaProperty(object):
def translate_schema_properties(schema_properties):
"""Parse the properties dictionary of a schema document.
- :returns list of SchemaProperty objects
+ :returns: list of SchemaProperty objects
"""
properties = []
for (name, prop) in schema_properties.items():
@@ -87,11 +87,10 @@ class Schema(object):
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.
- Checks if a property with a given name is known to the schema,
- i.e. is either a base property or a custom one registered in
- schema-image.json file
+ 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
diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py
index eaae374..dfd91fb 100644
--- a/glanceclient/v2/shell.py
+++ b/glanceclient/v2/shell.py
@@ -22,6 +22,8 @@ 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
@@ -270,7 +272,7 @@ def do_explain(gc, args):
@utils.arg('--file', metavar='<FILE>',
help=_('Local file to save downloaded image data to. '
'If this is not specified and there is no redirection '
- 'the image data will be not be saved.'))
+ 'the image data will not be saved.'))
@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to download.'))
@utils.arg('--progress', action='store_true', default=False,
help=_('Show download progress bar.'))
@@ -338,6 +340,10 @@ def do_image_delete(gc, args):
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)
@@ -426,6 +432,10 @@ 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:
@@ -446,6 +456,8 @@ def get_namespace_schema():
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
@@ -593,6 +605,8 @@ def get_resource_type_schema():
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
diff --git a/glanceclient/v2/tasks.py b/glanceclient/v2/tasks.py
index 4c06181..9c78020 100644
--- a/glanceclient/v2/tasks.py
+++ b/glanceclient/v2/tasks.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack LLC.
+# Copyright 2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
@@ -35,13 +35,15 @@ class Controller(object):
@utils.memoized_property
def model(self):
schema = self.schema_client.get('task')
- return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
+ return warlock.model_factory(schema.raw(),
+ base_class=schemas.SchemaBasedModel)
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
+ :returns: generator over list of Tasks
+
"""
def paginate(url):
resp, body = self.http_client.get(url)
diff --git a/releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml b/releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml
new file mode 100644
index 0000000..04eb2b9
--- /dev/null
+++ b/releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml
@@ -0,0 +1,11 @@
+---
+prelude: >
+ Switch to using keystoneauth for session and auth plugins.
+other:
+ - >
+ [`bp use-keystoneauth <https://blueprints.launchpad.net/python-glanceclient/+spec/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.
diff --git a/releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml b/releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml
new file mode 100644
index 0000000..452246f
--- /dev/null
+++ b/releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml
@@ -0,0 +1,6 @@
+---
+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.
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
index 1afe4be..01beeaa 100644
--- a/releasenotes/source/conf.py
+++ b/releasenotes/source/conf.py
@@ -275,3 +275,6 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False
+
+# -- Options for Internationalization output ------------------------------
+locale_dirs = ['locale/']
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index f90c430..8d2c2b0 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,3 +6,5 @@ glanceclient Release Notes
:maxdepth: 1
unreleased
+ newton
+ mitaka
diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst
new file mode 100644
index 0000000..e545609
--- /dev/null
+++ b/releasenotes/source/mitaka.rst
@@ -0,0 +1,6 @@
+===================================
+ Mitaka Series Release Notes
+===================================
+
+.. release-notes::
+ :branch: origin/stable/mitaka
diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst
new file mode 100644
index 0000000..97036ed
--- /dev/null
+++ b/releasenotes/source/newton.rst
@@ -0,0 +1,6 @@
+===================================
+ Newton Series Release Notes
+===================================
+
+.. release-notes::
+ :branch: origin/stable/newton
diff --git a/requirements.txt b/requirements.txt
index fbc914c..47e7814 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,12 @@
# 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>=1.6 # Apache-2.0
-Babel>=1.3 # BSD
-PrettyTable<0.8,>=0.7 # BSD
-python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0
-requests!=2.9.0,>=2.8.1 # Apache-2.0
-warlock<2,>=1.0.1 # Apache-2.0
+pbr>=1.8 # Apache-2.0
+Babel>=2.3.4 # BSD
+PrettyTable<0.8,>=0.7.1 # BSD
+keystoneauth1>=2.14.0 # Apache-2.0
+requests!=2.12.2,>=2.10.0 # Apache-2.0
+warlock!=1.3.0,<2,>=1.0.1 # Apache-2.0
six>=1.9.0 # MIT
-oslo.utils>=3.5.0 # Apache-2.0
+oslo.utils>=3.18.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 603c0cf..1cd9454 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,7 @@ description-file =
license = Apache License, Version 2.0
author = OpenStack
author-email = openstack-dev@lists.openstack.org
-home-page = http://www.openstack.org/
+home-page = http://docs.openstack.org/developer/python-glanceclient
classifier =
Development Status :: 5 - Production/Stable
Environment :: Console
@@ -20,6 +20,7 @@ classifier =
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
[files]
packages =
diff --git a/test-requirements.txt b/test-requirements.txt
index 4f56ff1..0fa3ae7 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -3,17 +3,16 @@
# process, which may cause wedges in the gate later.
hacking<0.11,>=0.10.0
-coverage>=3.6 # Apache-2.0
-discover # BSD
-mock>=1.2 # BSD
+coverage>=4.0 # Apache-2.0
+mock>=2.0 # BSD
ordereddict # MIT
-os-client-config>=1.13.1 # Apache-2.0
-oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
-reno>=0.1.1 # Apache2
-sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
+os-client-config>=1.22.0 # Apache-2.0
+oslosphinx>=4.7.0 # Apache-2.0
+reno>=1.8.0 # Apache-2.0
+sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
testscenarios>=0.4 # Apache-2.0/BSD
-fixtures>=1.3.1 # Apache-2.0/BSD
-requests-mock>=0.7.0 # Apache-2.0
-tempest-lib>=0.14.0 # Apache-2.0
+fixtures>=3.0.0 # Apache-2.0/BSD
+requests-mock>=1.1 # Apache-2.0
+tempest>=12.1.0 # Apache-2.0
diff --git a/tools/tox_install.sh b/tools/tox_install.sh
new file mode 100755
index 0000000..ee2df0c
--- /dev/null
+++ b/tools/tox_install.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+# Client constraint file contains this client version pin that is in conflict
+# with installing the client from source. We should replace the version pin in
+# the constraints file before applying it for from-source installation.
+
+ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner
+BRANCH_NAME=master
+CLIENT_NAME=python-glanceclient
+requirements_installed=$(echo "import openstack_requirements" | python 2>/dev/null ; echo $?)
+
+set -e
+
+CONSTRAINTS_FILE=$1
+shift
+
+install_cmd="pip install"
+mydir=$(mktemp -dt "$CLIENT_NAME-tox_install-XXXXXXX")
+trap "rm -rf $mydir" EXIT
+localfile=$mydir/upper-constraints.txt
+if [[ $CONSTRAINTS_FILE != http* ]]; then
+ CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE
+fi
+curl $CONSTRAINTS_FILE -k -o $localfile
+install_cmd="$install_cmd -c$localfile"
+
+if [ $requirements_installed -eq 0 ]; then
+ echo "ALREADY INSTALLED" > /tmp/tox_install.txt
+ echo "Requirements already installed; using existing package"
+elif [ -x "$ZUUL_CLONER" ]; then
+ echo "ZUUL CLONER" > /tmp/tox_install.txt
+ pushd $mydir
+ $ZUUL_CLONER --cache-dir \
+ /opt/git \
+ --branch $BRANCH_NAME \
+ git://git.openstack.org \
+ openstack/requirements
+ cd openstack/requirements
+ $install_cmd -e .
+ popd
+else
+ echo "PIP HARDCODE" > /tmp/tox_install.txt
+ if [ -z "$REQUIREMENTS_PIP_LOCATION" ]; then
+ REQUIREMENTS_PIP_LOCATION="git+https://git.openstack.org/openstack/requirements@$BRANCH_NAME#egg=requirements"
+ fi
+ $install_cmd -U -e ${REQUIREMENTS_PIP_LOCATION}
+fi
+
+# This is the main purpose of the script: Allow local installation of
+# the current repo. It is listed in constraints file and thus any
+# install will be constrained and we need to unconstrain it.
+edit-constraints $localfile -- $CLIENT_NAME "-e file://$PWD#egg=$CLIENT_NAME"
+
+$install_cmd -U $*
+exit $?
diff --git a/tox.ini b/tox.ini
index 0e14775..f333019 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,12 @@
[tox]
-envlist = py34,py27,pep8
+envlist = py35,py34,py27,pep8
minversion = 1.6
skipsdist = True
[testenv]
usedevelop = True
-install_command = pip install -U {opts} {packages}
+install_command =
+ {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
OS_STDOUT_NOCAPTURE=False
OS_STDERR_NOCAPTURE=False
@@ -21,6 +22,9 @@ commands = flake8
[testenv:venv]
commands = {posargs}
+[pbr]
+warnerror = True
+
[testenv:functional]
# See glanceclient/tests/functional/README.rst
# for information on running the functional tests.
@@ -38,13 +42,9 @@ commands=
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[flake8]
-# H233 Python 3.x incompatible use of print operator
-# H303 no wildcard import
-# H404 multi line docstring should start with a summary
-
-ignore = F403,F812,F821,H233,H303,H404
+ignore = F403,F812,F821
show-source = True
-exclude = .venv*,.tox,dist,*egg,build,.git,doc,*openstack/common*,*lib/python*,.update-venv
+exclude = .venv*,.tox,dist,*egg,build,.git,doc,*lib/python*,.update-venv
[hacking]
import_exceptions = six.moves,glanceclient._i18n