From 2a2c8eec2668ca345d05779feb3ab3be3ebb656b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 19 Sep 2014 08:51:38 +0000 Subject: [PATCH 0001/3303] Updated from global requirements Change-Id: I744a629cf685760ad96d60654d081fc495024ea8 --- requirements.txt | 4 ++-- test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 70eaf523a8..a53e52c653 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # 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. -cliff>=1.6.0 -oslo.i18n>=0.3.0 # Apache-2.0 +cliff>=1.7.0 # Apache-2.0 +oslo.i18n>=1.0.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 diff --git a/test-requirements.txt b/test-requirements.txt index 2e22b3dfa9..dd701c0053 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 -oslosphinx>=2.2.0.0a2 +oslosphinx>=2.2.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 From bfff44fc172d6e84180615fa777036bd5cba35ba Mon Sep 17 00:00:00 2001 From: Victor Silva Date: Fri, 19 Sep 2014 19:17:38 +0000 Subject: [PATCH 0002/3303] Fixing typo and improving docstring of find_domain This should make it easier to understand the purpose of find_domain - I believe the reason for which find_resource wasn't enough was not quite clear. Change-Id: I6a1cdfa86f52401d95c6da2cd38d7c95a140b4a1 --- openstackclient/identity/common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 48dc0c89b8..253729bddf 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -42,11 +42,11 @@ def find_service(identity_client, name_type_or_id): def find_domain(identity_client, name_or_id): """Find a domain. - If the user does not have permssions to access the v3 domain API, - assume that domain given is the id rather than the name. This - method is used by the project list command, so errors access the - domain will be ignored and if the user has access to the project - API, everything will work fine. + If the user does not have permissions to access the v3 domain API, e.g., + if the user is a project admin, assume that the domain given is the id + rather than the name. This method is used by the project list command, + so errors accessing the domain will be ignored and if the user has + access to the project API, everything will work fine. Closes bugs #1317478 and #1317485. """ From c8b3f2373387106b577fade2d00107677b7df619 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sun, 21 Sep 2014 12:02:11 -0400 Subject: [PATCH 0003/3303] Change help text for image save command Change-Id: Ib2aecb68ffa06f9ac831131944c98c49cf99c75a Closes-Bug: #1372070 --- openstackclient/image/v1/image.py | 2 +- openstackclient/image/v2/image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index cd746cf53e..465e9d7b15 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -333,7 +333,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to delete", + help="Name or ID of image to save", ) return parser diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index ec023b6448..c12ff11a09 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -105,7 +105,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to delete", + help="Name or ID of image to save", ) return parser From ffe976ce3edf2e4dc5dd50247a182ca690b4fdd9 Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Fri, 19 Sep 2014 14:24:53 +0300 Subject: [PATCH 0004/3303] Use oslo.utils Module `importutils` from common code was graduated to oslo.utils, so it would be great if we reuse this library. Remove unused strutils.py and gettextutils.py Change-Id: Iaae19fc5018d83103e5f15ff76d6da686bfdf5f8 --- openstack-common.conf | 3 - openstackclient/common/utils.py | 3 +- openstackclient/openstack/__init__.py | 0 openstackclient/openstack/common/__init__.py | 17 - .../openstack/common/gettextutils.py | 479 ------------------ .../openstack/common/importutils.py | 73 --- openstackclient/openstack/common/strutils.py | 311 ------------ requirements.txt | 1 + 8 files changed, 3 insertions(+), 884 deletions(-) delete mode 100644 openstackclient/openstack/__init__.py delete mode 100644 openstackclient/openstack/common/__init__.py delete mode 100644 openstackclient/openstack/common/gettextutils.py delete mode 100644 openstackclient/openstack/common/importutils.py delete mode 100644 openstackclient/openstack/common/strutils.py diff --git a/openstack-common.conf b/openstack-common.conf index ac923902b5..78b414c3f0 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,10 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -module=gettextutils module=install_venv_common -module=importutils -module=strutils # The base module to hold the copy of openstack.common base=openstackclient diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index c013deee66..5c5466dfc6 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -21,8 +21,9 @@ import six import time +from oslo.utils import importutils + from openstackclient.common import exceptions -from openstackclient.openstack.common import importutils def find_resource(manager, name_or_id): diff --git a/openstackclient/openstack/__init__.py b/openstackclient/openstack/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstackclient/openstack/common/__init__.py b/openstackclient/openstack/common/__init__.py deleted file mode 100644 index d1223eaf76..0000000000 --- a/openstackclient/openstack/common/__init__.py +++ /dev/null @@ -1,17 +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. - -import six - - -six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/openstackclient/openstack/common/gettextutils.py b/openstackclient/openstack/common/gettextutils.py deleted file mode 100644 index 0c82634b8f..0000000000 --- a/openstackclient/openstack/common/gettextutils.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright 2012 Red Hat, Inc. -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -gettext for openstack-common modules. - -Usual usage in an openstack.common module: - - from openstackclient.openstack.common.gettextutils import _ -""" - -import copy -import gettext -import locale -from logging import handlers -import os - -from babel import localedata -import six - -_AVAILABLE_LANGUAGES = {} - -# FIXME(dhellmann): Remove this when moving to oslo.i18n. -USE_LAZY = False - - -class TranslatorFactory(object): - """Create translator functions - """ - - def __init__(self, domain, localedir=None): - """Establish a set of translation functions for the domain. - - :param domain: Name of translation domain, - specifying a message catalog. - :type domain: str - :param lazy: Delays translation until a message is emitted. - Defaults to False. - :type lazy: Boolean - :param localedir: Directory with translation catalogs. - :type localedir: str - """ - self.domain = domain - if localedir is None: - localedir = os.environ.get(domain.upper() + '_LOCALEDIR') - self.localedir = localedir - - def _make_translation_func(self, domain=None): - """Return a new translation function ready for use. - - Takes into account whether or not lazy translation is being - done. - - The domain can be specified to override the default from the - factory, but the localedir from the factory is always used - because we assume the log-level translation catalogs are - installed in the same directory as the main application - catalog. - - """ - if domain is None: - domain = self.domain - t = gettext.translation(domain, - localedir=self.localedir, - fallback=True) - # Use the appropriate method of the translation object based - # on the python version. - m = t.gettext if six.PY3 else t.ugettext - - def f(msg): - """oslo.i18n.gettextutils translation function.""" - if USE_LAZY: - return Message(msg, domain=domain) - return m(msg) - return f - - @property - def primary(self): - "The default translation function." - return self._make_translation_func() - - def _make_log_translation_func(self, level): - return self._make_translation_func(self.domain + '-log-' + level) - - @property - def log_info(self): - "Translate info-level log messages." - return self._make_log_translation_func('info') - - @property - def log_warning(self): - "Translate warning-level log messages." - return self._make_log_translation_func('warning') - - @property - def log_error(self): - "Translate error-level log messages." - return self._make_log_translation_func('error') - - @property - def log_critical(self): - "Translate critical-level log messages." - return self._make_log_translation_func('critical') - - -# NOTE(dhellmann): When this module moves out of the incubator into -# oslo.i18n, these global variables can be moved to an integration -# module within each application. - -# Create the global translation functions. -_translators = TranslatorFactory('openstackclient') - -# 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 - -# NOTE(dhellmann): End of globals that will move to the application's -# integration module. - - -def enable_lazy(): - """Convenience function for configuring _() to use lazy gettext - - Call this at the start of execution to enable the gettextutils._ - function to use lazy gettext functionality. This is useful if - your project is importing _ directly instead of using the - gettextutils.install() way of importing the _ function. - """ - global USE_LAZY - USE_LAZY = True - - -def install(domain): - """Install a _() function using the given translation domain. - - Given a translation domain, install a _() function using gettext's - install() function. - - The main difference from gettext.install() is that we allow - overriding the default localedir (e.g. /usr/share/locale) using - a translation-domain-specific environment variable (e.g. - NOVA_LOCALEDIR). - - Note that to enable lazy translation, enable_lazy must be - called. - - :param domain: the translation domain - """ - from six import moves - tf = TranslatorFactory(domain) - moves.builtins.__dict__['_'] = tf.primary - - -class Message(six.text_type): - """A Message object is a unicode object that can be translated. - - Translation of Message is done explicitly using the translate() method. - For all non-translation intents and purposes, a Message is simply unicode, - and can be treated as such. - """ - - def __new__(cls, msgid, msgtext=None, params=None, - domain='openstackclient', *args): - """Create a new Message object. - - In order for translation to work gettext requires a message ID, this - msgid will be used as the base unicode text. It is also possible - for the msgid and the base unicode text to be different by passing - the msgtext parameter. - """ - # If the base msgtext is not given, we use the default translation - # of the msgid (which is in English) just in case the system locale is - # not English, so that the base text will be in that locale by default. - if not msgtext: - msgtext = Message._translate_msgid(msgid, domain) - # We want to initialize the parent unicode with the actual object that - # would have been plain unicode if 'Message' was not enabled. - msg = super(Message, cls).__new__(cls, msgtext) - msg.msgid = msgid - msg.domain = domain - msg.params = params - return msg - - def translate(self, desired_locale=None): - """Translate this message to the desired locale. - - :param desired_locale: The desired locale to translate the message to, - if no locale is provided the message will be - translated to the system's default locale. - - :returns: the translated message in unicode - """ - - translated_message = Message._translate_msgid(self.msgid, - self.domain, - desired_locale) - if self.params is None: - # No need for more translation - return translated_message - - # This Message object may have been formatted with one or more - # Message objects as substitution arguments, given either as a single - # argument, part of a tuple, or as one or more values in a dictionary. - # When translating this Message we need to translate those Messages too - translated_params = _translate_args(self.params, desired_locale) - - translated_message = translated_message % translated_params - - return translated_message - - @staticmethod - def _translate_msgid(msgid, domain, desired_locale=None): - if not desired_locale: - system_locale = locale.getdefaultlocale() - # If the system locale is not available to the runtime use English - if not system_locale[0]: - desired_locale = 'en_US' - else: - desired_locale = system_locale[0] - - locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') - lang = gettext.translation(domain, - localedir=locale_dir, - languages=[desired_locale], - fallback=True) - if six.PY3: - translator = lang.gettext - else: - translator = lang.ugettext - - translated_message = translator(msgid) - return translated_message - - def __mod__(self, other): - # When we mod a Message we want the actual operation to be performed - # by the parent class (i.e. unicode()), the only thing we do here is - # save the original msgid and the parameters in case of a translation - params = self._sanitize_mod_params(other) - unicode_mod = super(Message, self).__mod__(params) - modded = Message(self.msgid, - msgtext=unicode_mod, - params=params, - domain=self.domain) - return modded - - def _sanitize_mod_params(self, other): - """Sanitize the object being modded with this Message. - - - Add support for modding 'None' so translation supports it - - Trim the modded object, which can be a large dictionary, to only - those keys that would actually be used in a translation - - Snapshot the object being modded, in case the message is - translated, it will be used as it was when the Message was created - """ - if other is None: - params = (other,) - elif isinstance(other, dict): - # Merge the dictionaries - # Copy each item in case one does not support deep copy. - params = {} - if isinstance(self.params, dict): - for key, val in self.params.items(): - params[key] = self._copy_param(val) - for key, val in other.items(): - params[key] = self._copy_param(val) - else: - params = self._copy_param(other) - return params - - def _copy_param(self, param): - try: - return copy.deepcopy(param) - except Exception: - # Fallback to casting to unicode this will handle the - # python code-like objects that can't be deep-copied - return six.text_type(param) - - def __add__(self, other): - msg = _('Message objects do not support addition.') - raise TypeError(msg) - - def __radd__(self, other): - return self.__add__(other) - - if six.PY2: - def __str__(self): - # NOTE(luisg): Logging in python 2.6 tries to str() log records, - # and it expects specifically a UnicodeError in order to proceed. - msg = _('Message objects do not support str() because they may ' - 'contain non-ascii characters. ' - 'Please use unicode() or translate() instead.') - raise UnicodeError(msg) - - -def get_available_languages(domain): - """Lists the available languages for the given translation domain. - - :param domain: the domain to get languages for - """ - if domain in _AVAILABLE_LANGUAGES: - return copy.copy(_AVAILABLE_LANGUAGES[domain]) - - localedir = '%s_LOCALEDIR' % domain.upper() - find = lambda x: gettext.find(domain, - localedir=os.environ.get(localedir), - languages=[x]) - - # NOTE(mrodden): en_US should always be available (and first in case - # order matters) since our in-line message strings are en_US - language_list = ['en_US'] - # NOTE(luisg): Babel <1.0 used a function called list(), which was - # renamed to locale_identifiers() in >=1.0, the requirements master list - # requires >=0.9.6, uncapped, so defensively work with both. We can remove - # this check when the master list updates to >=1.0, and update all projects - list_identifiers = (getattr(localedata, 'list', None) or - getattr(localedata, 'locale_identifiers')) - locale_identifiers = list_identifiers() - - for i in locale_identifiers: - if find(i) is not None: - language_list.append(i) - - # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported - # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they - # are perfectly legitimate locales: - # https://github.com/mitsuhiko/babel/issues/37 - # In Babel 1.3 they fixed the bug and they support these locales, but - # they are still not explicitly "listed" by locale_identifiers(). - # That is why we add the locales here explicitly if necessary so that - # they are listed as supported. - aliases = {'zh': 'zh_CN', - 'zh_Hant_HK': 'zh_HK', - 'zh_Hant': 'zh_TW', - 'fil': 'tl_PH'} - for (locale_, alias) in six.iteritems(aliases): - if locale_ in language_list and alias not in language_list: - language_list.append(alias) - - _AVAILABLE_LANGUAGES[domain] = language_list - return copy.copy(language_list) - - -def translate(obj, desired_locale=None): - """Gets the translated unicode representation of the given object. - - If the object is not translatable it is returned as-is. - If the locale is None the object is translated to the system locale. - - :param obj: the object to translate - :param desired_locale: the locale to translate the message to, if None the - default system locale will be used - :returns: the translated object in unicode, or the original object if - it could not be translated - """ - message = obj - if not isinstance(message, Message): - # If the object to translate is not already translatable, - # let's first get its unicode representation - message = six.text_type(obj) - if isinstance(message, Message): - # Even after unicoding() we still need to check if we are - # running with translatable unicode before translating - return message.translate(desired_locale) - return obj - - -def _translate_args(args, desired_locale=None): - """Translates all the translatable elements of the given arguments object. - - This method is used for translating the translatable values in method - arguments which include values of tuples or dictionaries. - If the object is not a tuple or a dictionary the object itself is - translated if it is translatable. - - If the locale is None the object is translated to the system locale. - - :param args: the args to translate - :param desired_locale: the locale to translate the args to, if None the - default system locale will be used - :returns: a new args object with the translated contents of the original - """ - if isinstance(args, tuple): - return tuple(translate(v, desired_locale) for v in args) - if isinstance(args, dict): - translated_dict = {} - for (k, v) in six.iteritems(args): - translated_v = translate(v, desired_locale) - translated_dict[k] = translated_v - return translated_dict - return translate(args, desired_locale) - - -class TranslationHandler(handlers.MemoryHandler): - """Handler that translates records before logging them. - - The TranslationHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating them. This handler - depends on Message objects being logged, instead of regular strings. - - The handler can be configured declaratively in the logging.conf as follows: - - [handlers] - keys = translatedlog, translator - - [handler_translatedlog] - class = handlers.WatchedFileHandler - args = ('/var/log/api-localized.log',) - formatter = context - - [handler_translator] - class = openstack.common.log.TranslationHandler - target = translatedlog - args = ('zh_CN',) - - If the specified locale is not available in the system, the handler will - log in the default locale. - """ - - def __init__(self, locale=None, target=None): - """Initialize a TranslationHandler - - :param locale: locale to use for translating messages - :param target: logging.Handler object to forward - LogRecord objects to after translation - """ - # NOTE(luisg): In order to allow this handler to be a wrapper for - # other handlers, such as a FileHandler, and still be able to - # configure it using logging.conf, this handler has to extend - # MemoryHandler because only the MemoryHandlers' logging.conf - # parsing is implemented such that it accepts a target handler. - handlers.MemoryHandler.__init__(self, capacity=0, target=target) - self.locale = locale - - def setFormatter(self, fmt): - self.target.setFormatter(fmt) - - def emit(self, record): - # We save the message from the original record to restore it - # after translation, so other handlers are not affected by this - original_msg = record.msg - original_args = record.args - - try: - self._translate_and_log_record(record) - finally: - record.msg = original_msg - record.args = original_args - - def _translate_and_log_record(self, record): - record.msg = translate(record.msg, self.locale) - - # In addition to translating the message, we also need to translate - # arguments that were passed to the log method that were not part - # of the main message e.g., log.info(_('Some message %s'), this_one)) - record.args = _translate_args(record.args, self.locale) - - self.target.emit(record) diff --git a/openstackclient/openstack/common/importutils.py b/openstackclient/openstack/common/importutils.py deleted file mode 100644 index 69e8d8f121..0000000000 --- a/openstackclient/openstack/common/importutils.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Import related utilities and helper functions. -""" - -import sys -import traceback - - -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - try: - return getattr(sys.modules[mod_str], class_str) - except AttributeError: - raise ImportError('Class %s cannot be found (%s)' % - (class_str, - traceback.format_exception(*sys.exc_info()))) - - -def import_object(import_str, *args, **kwargs): - """Import a class and return an instance of it.""" - return import_class(import_str)(*args, **kwargs) - - -def import_object_ns(name_space, import_str, *args, **kwargs): - """Tries to import object from default namespace. - - Imports a class and return an instance of it, first by trying - to find the class in a default namespace, then failing back to - a full path if not found in the default namespace. - """ - import_value = "%s.%s" % (name_space, import_str) - try: - return import_class(import_value)(*args, **kwargs) - except ImportError: - return import_class(import_str)(*args, **kwargs) - - -def import_module(import_str): - """Import a module.""" - __import__(import_str) - return sys.modules[import_str] - - -def import_versioned_module(version, submodule=None): - module = 'openstackclient.v%s' % version - if submodule: - module = '.'.join((module, submodule)) - return import_module(module) - - -def try_import(import_str, default=None): - """Try to import a module and if it fails return default.""" - try: - return import_module(import_str) - except ImportError: - return default diff --git a/openstackclient/openstack/common/strutils.py b/openstackclient/openstack/common/strutils.py deleted file mode 100644 index ad3cb44c25..0000000000 --- a/openstackclient/openstack/common/strutils.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright 2011 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. - -""" -System-level utilities and helper functions. -""" - -import math -import re -import sys -import unicodedata - -import six - -from openstackclient.openstack.common.gettextutils import _ - - -UNIT_PREFIX_EXPONENT = { - 'k': 1, - 'K': 1, - 'Ki': 1, - 'M': 2, - 'Mi': 2, - 'G': 3, - 'Gi': 3, - 'T': 4, - 'Ti': 4, -} -UNIT_SYSTEM_INFO = { - 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')), - 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')), -} - -TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') -FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') - -SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") -SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") - - -# NOTE(flaper87): The following globals are used by `mask_password` -_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password'] - -# NOTE(ldbragst): Let's build a list of regex objects using the list of -# _SANITIZE_KEYS we already have. This way, we only have to add the new key -# to the list of _SANITIZE_KEYS and we can generate regular expressions -# for XML and JSON automatically. -_SANITIZE_PATTERNS_2 = [] -_SANITIZE_PATTERNS_1 = [] - -# NOTE(amrith): Some regular expressions have only one parameter, some -# have two parameters. Use different lists of patterns here. -_FORMAT_PATTERNS_1 = [r'(%(key)s\s*[=]\s*)[^\s^\'^\"]+'] -_FORMAT_PATTERNS_2 = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])', - r'(%(key)s\s+[\"\']).*?([\"\'])', - r'([-]{2}%(key)s\s+)[^\'^\"^=^\s]+([\s]*)', - r'(<%(key)s>).*?()', - r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])', - r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])', - r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?' - '[\'"]).*?([\'"])', - r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)'] - -for key in _SANITIZE_KEYS: - for pattern in _FORMAT_PATTERNS_2: - reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) - _SANITIZE_PATTERNS_2.append(reg_ex) - - for pattern in _FORMAT_PATTERNS_1: - reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) - _SANITIZE_PATTERNS_1.append(reg_ex) - - -def int_from_bool_as_string(subject): - """Interpret a string as a boolean and return either 1 or 0. - - Any string value in: - - ('True', 'true', 'On', 'on', '1') - - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - return bool_from_string(subject) and 1 or 0 - - -def bool_from_string(subject, strict=False, default=False): - """Interpret a string as a boolean. - - A case-insensitive match is performed such that strings matching 't', - 'true', 'on', 'y', 'yes', or '1' are considered True and, when - `strict=False`, anything else returns the value specified by 'default'. - - Useful for JSON-decoded stuff and config file parsing. - - If `strict=True`, unrecognized values, including None, will raise a - ValueError which is useful when parsing values passed in from an API call. - Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. - """ - if not isinstance(subject, six.string_types): - subject = six.text_type(subject) - - lowered = subject.strip().lower() - - if lowered in TRUE_STRINGS: - return True - elif lowered in FALSE_STRINGS: - return False - elif strict: - acceptable = ', '.join( - "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) - msg = _("Unrecognized value '%(val)s', acceptable values are:" - " %(acceptable)s") % {'val': subject, - 'acceptable': acceptable} - raise ValueError(msg) - else: - return default - - -def safe_decode(text, incoming=None, errors='strict'): - """Decodes incoming text/bytes string using `incoming` if they're not - already unicode. - - :param incoming: Text's current encoding - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: text or a unicode `incoming` encoded - representation of it. - :raises TypeError: If text is not an instance of str - """ - if not isinstance(text, (six.string_types, six.binary_type)): - raise TypeError("%s can't be decoded" % type(text)) - - if isinstance(text, six.text_type): - return text - - if not incoming: - incoming = (sys.stdin.encoding or - sys.getdefaultencoding()) - - try: - return text.decode(incoming, errors) - except UnicodeDecodeError: - # Note(flaper87) If we get here, it means that - # sys.stdin.encoding / sys.getdefaultencoding - # didn't return a suitable encoding to decode - # text. This happens mostly when global LANG - # var is not set correctly and there's no - # default encoding. In this case, most likely - # python will use ASCII or ANSI encoders as - # default encodings but they won't be capable - # of decoding non-ASCII characters. - # - # Also, UTF-8 is being used since it's an ASCII - # extension. - return text.decode('utf-8', errors) - - -def safe_encode(text, incoming=None, - encoding='utf-8', errors='strict'): - """Encodes incoming text/bytes string using `encoding`. - - If incoming is not specified, text is expected to be encoded with - current python's default encoding. (`sys.getdefaultencoding`) - - :param incoming: Text's current encoding - :param encoding: Expected encoding for text (Default UTF-8) - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: text or a bytestring `encoding` encoded - representation of it. - :raises TypeError: If text is not an instance of str - """ - if not isinstance(text, (six.string_types, six.binary_type)): - raise TypeError("%s can't be encoded" % type(text)) - - if not incoming: - incoming = (sys.stdin.encoding or - sys.getdefaultencoding()) - - if isinstance(text, six.text_type): - return text.encode(encoding, errors) - elif text and encoding != incoming: - # Decode text before encoding it with `encoding` - text = safe_decode(text, incoming, errors) - return text.encode(encoding, errors) - else: - return text - - -def string_to_bytes(text, unit_system='IEC', return_int=False): - """Converts a string into an float representation of bytes. - - The units supported for IEC :: - - Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it) - KB, KiB, MB, MiB, GB, GiB, TB, TiB - - The units supported for SI :: - - kb(it), Mb(it), Gb(it), Tb(it) - kB, MB, GB, TB - - Note that the SI unit system does not support capital letter 'K' - - :param text: String input for bytes size conversion. - :param unit_system: Unit system for byte size conversion. - :param return_int: If True, returns integer representation of text - in bytes. (default: decimal) - :returns: Numerical representation of text in bytes. - :raises ValueError: If text has an invalid value. - - """ - try: - base, reg_ex = UNIT_SYSTEM_INFO[unit_system] - except KeyError: - msg = _('Invalid unit system: "%s"') % unit_system - raise ValueError(msg) - match = reg_ex.match(text) - if match: - magnitude = float(match.group(1)) - unit_prefix = match.group(2) - if match.group(3) in ['b', 'bit']: - magnitude /= 8 - else: - msg = _('Invalid string format: %s') % text - raise ValueError(msg) - if not unit_prefix: - res = magnitude - else: - res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) - if return_int: - return int(math.ceil(res)) - return res - - -def to_slug(value, incoming=None, errors="strict"): - """Normalize string. - - Convert to lowercase, remove non-word characters, and convert spaces - to hyphens. - - Inspired by Django's `slugify` filter. - - :param value: Text to slugify - :param incoming: Text's current encoding - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: slugified unicode representation of `value` - :raises TypeError: If text is not an instance of str - """ - value = safe_decode(value, incoming, errors) - # NOTE(aababilov): no need to use safe_(encode|decode) here: - # encodings are always "ascii", error handling is always "ignore" - # and types are always known (first: unicode; second: str) - value = unicodedata.normalize("NFKD", value).encode( - "ascii", "ignore").decode("ascii") - value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() - return SLUGIFY_HYPHENATE_RE.sub("-", value) - - -def mask_password(message, secret="***"): - """Replace password with 'secret' in message. - - :param message: The string which includes security information. - :param secret: value with which to replace passwords. - :returns: The unicode value of message with the password fields masked. - - For example: - - >>> mask_password("'adminPass' : 'aaaaa'") - "'adminPass' : '***'" - >>> mask_password("'admin_pass' : 'aaaaa'") - "'admin_pass' : '***'" - >>> mask_password('"password" : "aaaaa"') - '"password" : "***"' - >>> mask_password("'original_password' : 'aaaaa'") - "'original_password' : '***'" - >>> mask_password("u'original_password' : u'aaaaa'") - "u'original_password' : u'***'" - """ - message = six.text_type(message) - - # NOTE(ldbragst): Check to see if anything in message contains any key - # specified in _SANITIZE_KEYS, if not then just return the message since - # we don't have to mask any passwords. - if not any(key in message for key in _SANITIZE_KEYS): - return message - - substitute = r'\g<1>' + secret + r'\g<2>' - for pattern in _SANITIZE_PATTERNS_2: - message = re.sub(pattern, substitute, message) - - substitute = r'\g<1>' + secret - for pattern in _SANITIZE_PATTERNS_1: - message = re.sub(pattern, substitute, message) - - return message diff --git a/requirements.txt b/requirements.txt index a53e52c653..4745c11252 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 +oslo.utils>=1.0.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 From 2d1225624c5115491288d53498906226ed53880f Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Sep 2014 14:52:44 +0800 Subject: [PATCH 0005/3303] v3 credential set always needs --user option Change-Id: Ieca76bb6ee2f328f4e33010623c25eb9c18e6952 Closes-Bug: #1372744 --- openstackclient/identity/v3/credential.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index 43d16c2962..f1e17b8502 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -151,11 +151,12 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user_id = utils.find_resource(identity_client.users, - parsed_args.user).id kwargs = {} - if user_id: - kwargs['user'] = user_id + if parsed_args.user: + user_id = utils.find_resource(identity_client.users, + parsed_args.user).id + if user_id: + kwargs['user'] = user_id if parsed_args.type: kwargs['type'] = parsed_args.type if parsed_args.data: From 1212ddb431c8ecdebbc89dc54a5854ac7794ace5 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 23 Sep 2014 16:43:31 -0400 Subject: [PATCH 0006/3303] Remove unused reference to keyring There's a unnecessary reference that is not being used. Change-Id: I5ac85d2331385e4a31970b63fd17e650f82046ca --- openstackclient/shell.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 24804343db..0c91ab6ef1 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -35,8 +35,6 @@ from openstackclient.common import utils -KEYRING_SERVICE = 'openstack' - DEFAULT_DOMAIN = 'default' From 7029cf37e268a789a65bab3b9a16e4491854106e Mon Sep 17 00:00:00 2001 From: wanghong Date: Wed, 24 Sep 2014 11:04:41 +0800 Subject: [PATCH 0007/3303] utils.find_resource does not catch right exception Currently, utils.find_resource catch NotFound exception defined in openstackclient. However, different client libraries raise different exceptions defined in thire own library. Change-Id: Idc40428e30e59f71dbdbfa0555c0066fddc441c2 Closes-Bug: #1371924 --- openstackclient/common/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 5c5466dfc6..818f8d4771 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -33,8 +33,15 @@ def find_resource(manager, name_or_id): try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) - except exceptions.NotFound: - pass + # FIXME(dtroyer): The exception to catch here is dependent on which + # client library the manager passed in belongs to. + # Eventually this should be pulled from a common set + # of client exceptions. + except Exception as ex: + if type(ex).__name__ == 'NotFound': + pass + else: + raise # Try directly using the passed value try: From 3ddd4e2646c4db4172c3d74a0f51eb6d3e91b176 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 25 Sep 2014 19:08:05 +0000 Subject: [PATCH 0008/3303] Updated from global requirements Change-Id: I2a8250d0b01651563cfe74704ce5a9f97dd9fdf4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4745c11252..04f448834f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 python-novaclient>=2.18.0 -python-cinderclient>=1.0.7 +python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=1.2.1,!=2.4.0 six>=1.7.0 From 207c8cf3ef9237d21cde704eff767523b5f12f35 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 18 Sep 2014 22:10:03 -0500 Subject: [PATCH 0009/3303] Test top-to-bottom: object-store containers Replicate the object-store container command tests but use requests_mock to test the entire stack down to the requests module. These will be useful regressions tests when the existing object-store lib modules are moved to the low-level API object. Change-Id: Ibf25be46156eb1009f1b66f02f2073d3913b846d --- openstackclient/tests/fakes.py | 8 +- openstackclient/tests/object/v1/fakes.py | 11 +- .../tests/object/v1/test_container_all.py | 341 ++++++++++++++++++ test-requirements.txt | 1 + 4 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 openstackclient/tests/object/v1/test_container_all.py diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 263640ee21..5a1fc005e4 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -47,12 +47,18 @@ def __init__(self, _stdout): self.stderr = sys.stderr +class FakeClient(object): + def __init__(self, **kwargs): + self.endpoint = kwargs['endpoint'] + self.token = kwargs['token'] + + class FakeClientManager(object): def __init__(self): self.compute = None self.identity = None self.image = None - self.object = None + self.object_store = None self.volume = None self.network = None self.session = None diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index 87f6cab694..f10437b63f 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -17,6 +17,9 @@ from openstackclient.tests import utils +ACCOUNT_ID = 'tqbfjotld' +ENDPOINT = 'https://0.0.0.0:6482/v1/' + ACCOUNT_ID + container_name = 'bit-bucket' container_bytes = 1024 container_count = 1 @@ -71,17 +74,11 @@ } -class FakeObjectv1Client(object): - def __init__(self, **kwargs): - self.endpoint = kwargs['endpoint'] - self.token = kwargs['token'] - - class TestObjectv1(utils.TestCommand): def setUp(self): super(TestObjectv1, self).setUp() - self.app.client_manager.object_store = FakeObjectv1Client( + self.app.client_manager.object_store = fakes.FakeClient( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py new file mode 100644 index 0000000000..29d78454fa --- /dev/null +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -0,0 +1,341 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +from requests_mock.contrib import fixture + +from keystoneclient import session +from openstackclient.object.v1 import container +from openstackclient.tests.object.v1 import fakes as object_fakes + + +class TestObjectAll(object_fakes.TestObjectv1): + + def setUp(self): + super(TestObjectAll, self).setUp() + + self.app.client_manager.session = session.Session() + self.requests_mock = self.useFixture(fixture.Fixture()) + + # TODO(dtroyer): move this to object_fakes.TestObjectv1 + self.app.client_manager.object_store.endpoint = object_fakes.ENDPOINT + + +class TestContainerCreate(TestObjectAll): + + def setUp(self): + super(TestContainerCreate, self).setUp() + + # Get the command object to test + self.cmd = container.CreateContainer(self.app, None) + + def test_object_create_container_single(self): + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/ernie', + headers={'x-trans-id': '314159'}, + status_code=200, + ) + + arglist = [ + 'ernie', + ] + verifylist = [( + 'containers', ['ernie'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('account', 'container', 'x-trans-id') + self.assertEqual(collist, columns) + datalist = [( + object_fakes.ACCOUNT_ID, + 'ernie', + '314159', + )] + self.assertEqual(datalist, list(data)) + + def test_object_create_container_more(self): + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/ernie', + headers={'x-trans-id': '314159'}, + status_code=200, + ) + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/bert', + headers={'x-trans-id': '42'}, + status_code=200, + ) + + arglist = [ + 'ernie', + 'bert', + ] + verifylist = [( + 'containers', ['ernie', 'bert'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('account', 'container', 'x-trans-id') + self.assertEqual(collist, columns) + datalist = [ + ( + object_fakes.ACCOUNT_ID, + 'ernie', + '314159', + ), + ( + object_fakes.ACCOUNT_ID, + 'bert', + '42', + ), + ] + self.assertEqual(datalist, list(data)) + + +class TestContainerDelete(TestObjectAll): + + def setUp(self): + super(TestContainerDelete, self).setUp() + + # Get the command object to test + self.cmd = container.DeleteContainer(self.app, None) + + def test_object_delete_container_single(self): + self.requests_mock.register_uri( + 'DELETE', + object_fakes.ENDPOINT + '/ernie', + status_code=200, + ) + + arglist = [ + 'ernie', + ] + verifylist = [( + 'containers', ['ernie'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Command.take_action() returns None + ret = self.cmd.take_action(parsed_args) + self.assertIsNone(ret) + + def test_object_delete_container_more(self): + self.requests_mock.register_uri( + 'DELETE', + object_fakes.ENDPOINT + '/ernie', + status_code=200, + ) + self.requests_mock.register_uri( + 'DELETE', + object_fakes.ENDPOINT + '/bert', + status_code=200, + ) + + arglist = [ + 'ernie', + 'bert', + ] + verifylist = [( + 'containers', ['ernie', 'bert'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Command.take_action() returns None + ret = self.cmd.take_action(parsed_args) + self.assertIsNone(ret) + + +class TestContainerList(TestObjectAll): + + def setUp(self): + super(TestContainerList, self).setUp() + + # Get the command object to test + self.cmd = container.ListContainer(self.app, None) + + def test_object_list_containers_no_options(self): + return_body = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + copy.deepcopy(object_fakes.CONTAINER_2), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + '?format=json', + json=return_body, + status_code=200, + ) + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Lister.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(collist, columns) + datalist = [ + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + (object_fakes.container_name_2, ), + ] + self.assertEqual(datalist, list(data)) + + def test_object_list_containers_prefix(self): + return_body = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + '?format=json&prefix=bit', + json=return_body, + status_code=200, + ) + + arglist = [ + '--prefix', 'bit', + ] + verifylist = [ + ('prefix', 'bit'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Lister.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(collist, columns) + datalist = [ + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + ] + self.assertEqual(datalist, list(data)) + + +class TestContainerSave(TestObjectAll): + + def setUp(self): + super(TestContainerSave, self).setUp() + + # Get the command object to test + self.cmd = container.SaveContainer(self.app, None) + +# TODO(dtroyer): need to mock out object_lib.save_object() to test this +# def test_object_save_container(self): +# return_body = [ +# copy.deepcopy(object_fakes.OBJECT), +# copy.deepcopy(object_fakes.OBJECT_2), +# ] +# # Initial container list request +# self.requests_mock.register_uri( +# 'GET', +# object_fakes.ENDPOINT + '/oscar?format=json', +# json=return_body, +# status_code=200, +# ) +# # Individual object save requests +# self.requests_mock.register_uri( +# 'GET', +# object_fakes.ENDPOINT + '/oscar/' + object_fakes.object_name_1, +# json=object_fakes.OBJECT, +# status_code=200, +# ) +# self.requests_mock.register_uri( +# 'GET', +# object_fakes.ENDPOINT + '/oscar/' + object_fakes.object_name_2, +# json=object_fakes.OBJECT_2, +# status_code=200, +# ) +# +# arglist = [ +# 'oscar', +# ] +# verifylist = [( +# 'container', 'oscar', +# )] +# parsed_args = self.check_parser(self.cmd, arglist, verifylist) +# +# # Command.take_action() returns None +# ret = self.cmd.take_action(parsed_args) +# self.assertIsNone(ret) + + +class TestContainerShow(TestObjectAll): + + def setUp(self): + super(TestContainerShow, self).setUp() + + # Get the command object to test + self.cmd = container.ShowContainer(self.app, None) + + def test_object_show_container(self): + headers = { + 'x-container-meta-owner': object_fakes.ACCOUNT_ID, + 'x-container-object-count': '42', + 'x-container-bytes-used': '123', + 'x-container-read': 'qaz', + 'x-container-write': 'wsx', + 'x-container-sync-to': 'edc', + 'x-container-sync-key': 'rfv', + } + self.requests_mock.register_uri( + 'HEAD', + object_fakes.ENDPOINT + '/ernie', + headers=headers, + status_code=200, + ) + + arglist = [ + 'ernie', + ] + verifylist = [( + 'container', 'ernie', + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'account', + 'bytes_used', + 'container', + 'object_count', + 'read_acl', + 'sync_key', + 'sync_to', + 'write_acl', + ) + self.assertEqual(collist, columns) + datalist = [ + object_fakes.ACCOUNT_ID, + '123', + 'ernie', + '42', + 'qaz', + 'rfv', + 'edc', + 'wsx', + ] + self.assertEqual(datalist, list(data)) diff --git a/test-requirements.txt b/test-requirements.txt index dd701c0053..3b95cf7db3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ discover fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 +requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 From e3b9b9658805f274283a498ed82014dce3833fe3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 18 Sep 2014 00:52:02 -0500 Subject: [PATCH 0010/3303] Add low-level API base class Adds the foundation of a low-level REST API client. This is the final prep stage in the conversion of the object-store commands from the old restapi interface to the keystoneclient.session-based API. * api.api.BaseAPI holds the common operations Change-Id: I8fba980e3eb2d787344f766507a9d0dae49dcadf --- openstackclient/api/__init__.py | 0 openstackclient/api/api.py | 349 +++++++++++++++++++++++++ openstackclient/tests/api/__init__.py | 0 openstackclient/tests/api/test_api.py | 362 ++++++++++++++++++++++++++ 4 files changed, 711 insertions(+) create mode 100644 openstackclient/api/__init__.py create mode 100644 openstackclient/api/api.py create mode 100644 openstackclient/tests/api/__init__.py create mode 100644 openstackclient/tests/api/test_api.py diff --git a/openstackclient/api/__init__.py b/openstackclient/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py new file mode 100644 index 0000000000..72a66e1cb4 --- /dev/null +++ b/openstackclient/api/api.py @@ -0,0 +1,349 @@ +# 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 API Library""" + +import simplejson as json + +from keystoneclient.openstack.common.apiclient \ + import exceptions as ksc_exceptions +from keystoneclient import session as ksc_session +from openstackclient.common import exceptions + + +class KeystoneSession(object): + """Wrapper for the Keystone Session + + Restore some requests.session.Session compatibility; + keystoneclient.session.Session.request() has the method and url + arguments swapped from the rest of the requests-using world. + + """ + + def __init__( + self, + session=None, + endpoint=None, + **kwargs + ): + """Base object that contains some common API objects and methods + + :param Session session: + The default session to be used for making the HTTP API calls. + :param string endpoint: + The URL from the Service Catalog to be used as the base for API + requests on this API. + """ + + super(KeystoneSession, self).__init__() + + # a requests.Session-style interface + self.session = session + self.endpoint = endpoint + + def _request(self, method, url, session=None, **kwargs): + """Perform call into session + + All API calls are funneled through this method to provide a common + place to finalize the passed URL and other things. + + :param string method: + The HTTP method name, i.e. ``GET``, ``PUT``, etc + :param string url: + The API-specific portion of the URL path + :param Session session: + HTTP client session + :param kwargs: + keyword arguments passed to requests.request(). + :return: the requests.Response object + """ + + if not session: + session = self.session + if not session: + session = ksc_session.Session() + + if self.endpoint: + if url: + url = '/'.join([self.endpoint.rstrip('/'), url.lstrip('/')]) + else: + url = self.endpoint.rstrip('/') + + # Why is ksc session backwards??? + return session.request(url, method, **kwargs) + + +class BaseAPI(KeystoneSession): + """Base API""" + + def __init__( + self, + session=None, + service_type=None, + endpoint=None, + **kwargs + ): + """Base object that contains some common API objects and methods + + :param Session session: + The default session to be used for making the HTTP API calls. + :param string service_type: + API name, i.e. ``identity`` or ``compute`` + :param string endpoint: + The URL from the Service Catalog to be used as the base for API + requests on this API. + """ + + super(BaseAPI, self).__init__(session=session, endpoint=endpoint) + + self.service_type = service_type + + # The basic action methods all take a Session and return dict/lists + + def create( + self, + url, + session=None, + method=None, + **params + ): + """Create a new resource + + :param string url: + The API-specific portion of the URL path + :param Session session: + HTTP client session + :param string method: + HTTP method (default POST) + """ + + if not method: + method = 'POST' + ret = self._request(method, url, session=session, **params) + # Should this move into _requests()? + try: + return ret.json() + except json.JSONDecodeError: + return ret + + def delete( + self, + url, + session=None, + **params + ): + """Delete a resource + + :param string url: + The API-specific portion of the URL path + :param Session session: + HTTP client session + """ + + return self._request('DELETE', url, **params) + + def list( + self, + path, + session=None, + body=None, + detailed=False, + **params + ): + """Return a list of resources + + GET ${ENDPOINT}/${PATH} + + path is often the object's plural resource type + + :param string path: + The API-specific portion of the URL path + :param Session session: + HTTP client session + :param body: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param bool detailed: + Adds '/details' to path for some APIs to return extended attributes + :returns: + JSON-decoded response, could be a list or a dict-wrapped-list + """ + + if detailed: + path = '/'.join([path.rstrip('/'), 'details']) + + if body: + ret = self._request( + 'POST', + path, + # service=self.service_type, + json=body, + params=params, + ) + else: + ret = self._request( + 'GET', + path, + # service=self.service_type, + params=params, + ) + try: + return ret.json() + except json.JSONDecodeError: + return ret + + # Layered actions built on top of the basic action methods do not + # explicitly take a Session but one may still be passed in kwargs + + def find_attr( + self, + path, + value=None, + attr=None, + resource=None, + ): + """Find a resource via attribute or ID + + Most APIs return a list wrapped by a dict with the resource + name as key. Some APIs (Identity) return a dict when a query + string is present and there is one return value. Take steps to + unwrap these bodies and return a single dict without any resource + wrappers. + + :param string path: + The API-specific portion of the URL path + :param string value: + value to search for + :param string attr: + attribute to use for resource search + :param string resource: + plural of the object resource name; defaults to path + For example: + n = find(netclient, 'network', 'networks', 'matrix') + """ + + # Default attr is 'name' + if attr is None: + attr = 'name' + + # Default resource is path - in many APIs they are the same + if resource is None: + resource = path + + def getlist(kw): + """Do list call, unwrap resource dict if present""" + ret = self.list(path, **kw) + if type(ret) == dict and resource in ret: + ret = ret[resource] + return ret + + # Search by attribute + kwargs = {attr: value} + data = getlist(kwargs) + if type(data) == dict: + return data + if len(data) == 1: + return data[0] + if len(data) > 1: + msg = "Multiple %s exist with %s='%s'" + raise ksc_exceptions.CommandError( + msg % (resource, attr, value), + ) + + # Search by id + kwargs = {'id': value} + data = getlist(kwargs) + if len(data) == 1: + return data[0] + msg = "No %s with a %s or ID of '%s' found" + raise exceptions.CommandError(msg % (resource, attr, value)) + + def find_bulk( + self, + path, + **kwargs + ): + """Bulk load and filter locally + + :param string path: + The API-specific portion of the URL path + :param kwargs: + A dict of AVPs to match - logical AND + :returns: list of resource dicts + """ + + items = self.list(path) + if type(items) == dict: + # strip off the enclosing dict + key = list(items.keys())[0] + items = items[key] + + ret = [] + for o in items: + try: + if all(o[attr] == kwargs[attr] for attr in kwargs.keys()): + ret.append(o) + except KeyError: + continue + + return ret + + def find_one( + self, + path, + **kwargs + ): + """Find a resource by name or ID + + :param string path: + The API-specific portion of the URL path + :returns: + resource dict + """ + + bulk_list = self.find_bulk(path, **kwargs) + num_bulk = len(bulk_list) + if num_bulk == 0: + msg = "none found" + raise ksc_exceptions.NotFound(msg) + elif num_bulk > 1: + msg = "many found" + raise RuntimeError(msg) + return bulk_list[0] + + def find( + self, + path, + value=None, + attr=None, + ): + """Find a single resource by name or ID + + :param string path: + The API-specific portion of the URL path + :param string search: + search expression + :param string attr: + name of attribute for secondary search + """ + + try: + ret = self._request('GET', "/%s/%s" % (path, value)).json() + except ksc_exceptions.NotFound: + kwargs = {attr: value} + try: + ret = self.find_one("/%s/detail" % (path), **kwargs) + except ksc_exceptions.NotFound: + msg = "%s not found" % value + raise ksc_exceptions.NotFound(msg) + + return ret diff --git a/openstackclient/tests/api/__init__.py b/openstackclient/tests/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/api/test_api.py b/openstackclient/tests/api/test_api.py new file mode 100644 index 0000000000..32042e4f3b --- /dev/null +++ b/openstackclient/tests/api/test_api.py @@ -0,0 +1,362 @@ +# 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 API Library Tests""" + +from requests_mock.contrib import fixture + +from keystoneclient import session +from openstackclient.api import api +from openstackclient.common import exceptions +from openstackclient.tests import utils + + +RESP_ITEM_1 = { + 'id': '1', + 'name': 'alpha', + 'status': 'UP', +} +RESP_ITEM_2 = { + 'id': '2', + 'name': 'beta', + 'status': 'DOWN', +} +RESP_ITEM_3 = { + 'id': '3', + 'name': 'delta', + 'status': 'UP', +} + +LIST_RESP = [RESP_ITEM_1, RESP_ITEM_2] + +LIST_BODY = { + 'p1': 'xxx', + 'p2': 'yyy', +} + + +class TestSession(utils.TestCase): + + BASE_URL = 'https://api.example.com:1234/vX' + + def setUp(self): + super(TestSession, self).setUp() + self.sess = session.Session() + self.requests_mock = self.useFixture(fixture.Fixture()) + + +class TestKeystoneSession(TestSession): + + def setUp(self): + super(TestKeystoneSession, self).setUp() + self.api = api.KeystoneSession( + session=self.sess, + endpoint=self.BASE_URL, + ) + + def test_session_request(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=RESP_ITEM_1, + status_code=200, + ) + ret = self.api._request('GET', '/qaz') + self.assertEqual(RESP_ITEM_1, ret.json()) + + +class TestBaseAPI(TestSession): + + def setUp(self): + super(TestBaseAPI, self).setUp() + self.api = api.BaseAPI( + session=self.sess, + endpoint=self.BASE_URL, + ) + + def test_create_post(self): + self.requests_mock.register_uri( + 'POST', + self.BASE_URL + '/qaz', + json=RESP_ITEM_1, + status_code=202, + ) + ret = self.api.create('qaz') + self.assertEqual(RESP_ITEM_1, ret) + + def test_create_put(self): + self.requests_mock.register_uri( + 'PUT', + self.BASE_URL + '/qaz', + json=RESP_ITEM_1, + status_code=202, + ) + ret = self.api.create('qaz', method='PUT') + self.assertEqual(RESP_ITEM_1, ret) + + def test_delete(self): + self.requests_mock.register_uri( + 'DELETE', + self.BASE_URL + '/qaz', + status_code=204, + ) + ret = self.api.delete('qaz') + self.assertEqual(204, ret.status_code) + + # find tests + + def test_find_attr_by_id(self): + + # All first requests (by name) will fail in this test + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=1', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?id=1', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', '1') + self.assertEqual(RESP_ITEM_1, ret) + + # value not found + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=0', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?id=0', + json={'qaz': []}, + status_code=200, + ) + self.assertRaises( + exceptions.CommandError, + self.api.find_attr, + 'qaz', + '0', + ) + + # Attribute other than 'name' + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?status=UP', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', 'UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + ret = self.api.find_attr('qaz', value='UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + + def test_find_attr_by_name(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=alpha', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', 'alpha') + self.assertEqual(RESP_ITEM_1, ret) + + # value not found + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=0', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?id=0', + json={'qaz': []}, + status_code=200, + ) + self.assertRaises( + exceptions.CommandError, + self.api.find_attr, + 'qaz', + '0', + ) + + # Attribute other than 'name' + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?status=UP', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', 'UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + ret = self.api.find_attr('qaz', value='UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + + def test_find_attr_path_resource(self): + + # Test resource different than path + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/wsx?name=1', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/wsx?id=1', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('wsx', '1', resource='qaz') + self.assertEqual(RESP_ITEM_1, ret) + + def test_find_bulk_none(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.find_bulk('qaz') + self.assertEqual(LIST_RESP, ret) + + def test_find_bulk_one(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.find_bulk('qaz', id='1') + self.assertEqual([LIST_RESP[0]], ret) + + ret = self.api.find_bulk('qaz', id='0') + self.assertEqual([], ret) + + ret = self.api.find_bulk('qaz', name='beta') + self.assertEqual([LIST_RESP[1]], ret) + + ret = self.api.find_bulk('qaz', error='bogus') + self.assertEqual([], ret) + + def test_find_bulk_two(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.find_bulk('qaz', id='1', name='alpha') + self.assertEqual([LIST_RESP[0]], ret) + + ret = self.api.find_bulk('qaz', id='1', name='beta') + self.assertEqual([], ret) + + ret = self.api.find_bulk('qaz', id='1', error='beta') + self.assertEqual([], ret) + + def test_find_bulk_dict(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json={'qaz': LIST_RESP}, + status_code=200, + ) + ret = self.api.find_bulk('qaz', id='1') + self.assertEqual([LIST_RESP[0]], ret) + + # list tests + + def test_list_no_body(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL, + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('') + self.assertEqual(LIST_RESP, ret) + + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz') + self.assertEqual(LIST_RESP, ret) + + def test_list_params(self): + params = {'format': 'json'} + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '?format=json', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('', **params) + self.assertEqual(LIST_RESP, ret) + + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?format=json', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', **params) + self.assertEqual(LIST_RESP, ret) + + def test_list_body(self): + self.requests_mock.register_uri( + 'POST', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', body=LIST_BODY) + self.assertEqual(LIST_RESP, ret) + + def test_list_detailed(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz/details', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', detailed=True) + self.assertEqual(LIST_RESP, ret) + + def test_list_filtered(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?attr=value', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', attr='value') + self.assertEqual(LIST_RESP, ret) + + def test_list_wrapped(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?attr=value', + json={'responses': LIST_RESP}, + status_code=200, + ) + ret = self.api.list('qaz', attr='value') + self.assertEqual({'responses': LIST_RESP}, ret) From 31018bf7c2c57c530d55ed1dd90b9b65d489d557 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 18 Sep 2014 00:54:52 -0500 Subject: [PATCH 0011/3303] Move object-store commands to low-level API api.object_store.APIv1 now contains the formerly top-level functions implementing the object-store REST client. This replaces the old-style ObjectClientv1 that is no longer necessary. Change-Id: I7d8fea326b214481e7d6b24119bd41777c6aa968 --- openstackclient/api/object_store_v1.py | 381 ++++++++++++++++++ openstackclient/object/client.py | 22 +- openstackclient/object/v1/container.py | 29 +- openstackclient/object/v1/lib/container.py | 170 -------- openstackclient/object/v1/lib/object.py | 221 ---------- openstackclient/object/v1/object.py | 42 +- .../tests/api/test_object_store_v1.py | 326 +++++++++++++++ .../tests/object/v1/lib/__init__.py | 0 .../tests/object/v1/lib/test_container.py | 207 ---------- .../tests/object/v1/lib/test_object.py | 295 -------------- .../tests/object/v1/test_container.py | 81 +--- .../tests/object/v1/test_container_all.py | 6 +- .../tests/object/v1/test_object.py | 59 +-- 13 files changed, 773 insertions(+), 1066 deletions(-) create mode 100644 openstackclient/api/object_store_v1.py delete mode 100644 openstackclient/object/v1/lib/container.py delete mode 100644 openstackclient/object/v1/lib/object.py create mode 100644 openstackclient/tests/api/test_object_store_v1.py delete mode 100644 openstackclient/tests/object/v1/lib/__init__.py delete mode 100644 openstackclient/tests/object/v1/lib/test_container.py delete mode 100644 openstackclient/tests/object/v1/lib/test_object.py diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py new file mode 100644 index 0000000000..f938b55ed5 --- /dev/null +++ b/openstackclient/api/object_store_v1.py @@ -0,0 +1,381 @@ +# 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. +# + +"""Object Store v1 API Library""" + +import os +import six + +try: + from urllib.parse import urlparse # noqa +except ImportError: + from urlparse import urlparse # noqa + +from openstackclient.api import api + + +class APIv1(api.BaseAPI): + """Object Store v1 API""" + + def __init__(self, **kwargs): + super(APIv1, self).__init__(**kwargs) + + def container_create( + self, + container=None, + ): + """Create a container + + :param string container: + name of container to create + :returns: + dict of returned headers + """ + + response = self.create(container, method='PUT') + url_parts = urlparse(self.endpoint) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + 'x-trans-id': response.headers.get('x-trans-id', None), + } + + return data + + def container_delete( + self, + container=None, + ): + """Delete a container + + :param string container: + name of container to delete + """ + + if container: + self.delete(container) + + def container_list( + self, + all_data=False, + limit=None, + marker=None, + end_marker=None, + prefix=None, + **params + ): + """Get containers in an account + + :param boolean all_data: + if True, return a full listing, else returns a max of + 10000 listings + :param integer limit: + query return count limit + :param string marker: + query marker + :param string end_marker: + query end_marker + :param string prefix: + query prefix + :returns: + list of container names + """ + + params['format'] = 'json' + + if all_data: + data = listing = self.container_list( + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + **params + ) + while listing: + marker = listing[-1]['name'] + listing = self.container_list( + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + **params + ) + if listing: + data.extend(listing) + return data + + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker + if end_marker: + params['end_marker'] = end_marker + if prefix: + params['prefix'] = prefix + + return self.list('', **params) + + def container_save( + self, + container=None, + ): + """Save all the content from a container + + :param string container: + name of container to save + """ + + objects = self.object_list(container=container) + for object in objects: + self.object_save(container=container, object=object['name']) + + def container_show( + self, + container=None, + ): + """Get container details + + :param string container: + name of container to show + :returns: + dict of returned headers + """ + + response = self._request('HEAD', container) + data = { + 'account': response.headers.get('x-container-meta-owner', None), + 'container': container, + 'object_count': response.headers.get( + 'x-container-object-count', + None, + ), + 'bytes_used': response.headers.get('x-container-bytes-used', None), + 'read_acl': response.headers.get('x-container-read', None), + 'write_acl': response.headers.get('x-container-write', None), + 'sync_to': response.headers.get('x-container-sync-to', None), + 'sync_key': response.headers.get('x-container-sync-key', None), + } + return data + + def object_create( + self, + container=None, + object=None, + ): + """Create an object inside a container + + :param string container: + name of container to store object + :param string object: + local path to object + :returns: + dict of returned headers + """ + + if container is None or object is None: + # TODO(dtroyer): What exception to raise here? + return {} + + full_url = "%s/%s" % (container, object) + response = self.create(full_url, method='PUT', data=open(object)) + url_parts = urlparse(self.endpoint) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + 'object': object, + 'x-trans-id': response.headers.get('X-Trans-Id', None), + 'etag': response.headers.get('Etag', None), + } + + return data + + def object_delete( + self, + container=None, + object=None, + ): + """Delete an object from a container + + :param string container: + name of container that stores object + :param string object: + name of object to delete + """ + + if container is None or object is None: + return + + self.delete("%s/%s" % (container, object)) + + def object_list( + self, + container=None, + all_data=False, + limit=None, + marker=None, + end_marker=None, + delimiter=None, + prefix=None, + **params + ): + """List objects in a container + + :param string container: + container name to get a listing for + :param boolean all_data: + if True, return a full listing, else returns a max of + 10000 listings + :param integer limit: + query return count limit + :param string marker: + query marker + :param string end_marker: + query end_marker + :param string prefix: + query prefix + :param string delimiter: + string to delimit the queries on + :returns: a tuple of (response headers, a list of objects) The response + headers will be a dict and all header names will be lowercase. + """ + + if container is None or object is None: + return None + + if all_data: + data = listing = self.object_list( + container=container, + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + delimiter=delimiter, + **params + ) + while listing: + if delimiter: + marker = listing[-1].get('name', listing[-1].get('subdir')) + else: + marker = listing[-1]['name'] + listing = self.object_list( + container=container, + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + delimiter=delimiter, + **params + ) + if listing: + data.extend(listing) + return data + + params = {} + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker + if end_marker: + params['end_marker'] = end_marker + if prefix: + params['prefix'] = prefix + if delimiter: + params['delimiter'] = delimiter + + return self.list(container, **params) + + def object_save( + self, + container=None, + object=None, + file=None, + ): + """Save an object stored in a container + + :param string container: + name of container that stores object + :param string object: + name of object to save + :param string file: + local name of object + """ + + if not file: + file = object + + response = self._request( + 'GET', + "%s/%s" % (container, object), + stream=True, + ) + if response.status_code == 200: + if not os.path.exists(os.path.dirname(file)): + os.makedirs(os.path.dirname(file)) + with open(file, 'wb') as f: + for chunk in response.iter_content(): + f.write(chunk) + + def object_show( + self, + container=None, + object=None, + ): + """Get object details + + :param string container: + container name for object to get + :param string object: + name of object to get + :returns: + dict of object properties + """ + + if container is None or object is None: + return {} + + response = self._request('HEAD', "%s/%s" % (container, object)) + data = { + 'account': response.headers.get('x-container-meta-owner', None), + 'container': container, + 'object': object, + 'content-type': response.headers.get('content-type', None), + } + if 'content-length' in response.headers: + data['content-length'] = response.headers.get( + 'content-length', + None, + ) + if 'last-modified' in response.headers: + data['last-modified'] = response.headers.get('last-modified', None) + if 'etag' in response.headers: + data['etag'] = response.headers.get('etag', None) + if 'x-object-manifest' in response.headers: + data['x-object-manifest'] = response.headers.get( + 'x-object-manifest', + None, + ) + for key, value in six.iteritems(response.headers): + if key.startswith('x-object-meta-'): + data[key[len('x-object-meta-'):].lower()] = value + elif key not in ( + 'content-type', + 'content-length', + 'last-modified', + 'etag', + 'date', + 'x-object-manifest', + 'x-container-meta-owner', + ): + data[key.lower()] = value + + return data diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index b81ffaaf44..887aa85b8e 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -17,6 +17,7 @@ import logging +from openstackclient.api import object_store_v1 from openstackclient.common import utils LOG = logging.getLogger(__name__) @@ -30,7 +31,7 @@ def make_client(instance): - """Returns an object service client.""" + """Returns an object-store API client.""" object_client = utils.get_client_class( API_NAME, @@ -42,9 +43,11 @@ def make_client(instance): endpoint = instance._url else: endpoint = instance.get_endpoint_for_service_type("object-store") - client = object_client( + + client = object_store_v1.APIv1( + session=instance.session, + service_type='object-store', endpoint=endpoint, - token=instance._token, ) return client @@ -61,16 +64,3 @@ def build_option_parser(parser): DEFAULT_OBJECT_API_VERSION + ' (Env: OS_OBJECT_API_VERSION)') return parser - - -class ObjectClientv1(object): - - def __init__( - self, - endpoint_type='publicURL', - endpoint=None, - token=None, - ): - self.endpoint_type = endpoint_type - self.endpoint = endpoint - self.token = token diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 9d55381cbe..ead3df45e9 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -24,7 +24,6 @@ from cliff import show from openstackclient.common import utils -from openstackclient.object.v1.lib import container as lib_container class CreateContainer(lister.Lister): @@ -47,10 +46,8 @@ def take_action(self, parsed_args): results = [] for container in parsed_args.containers: - data = lib_container.create_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - container, + data = self.app.client_manager.object_store.container_create( + container=container, ) results.append(data) @@ -81,10 +78,8 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) for container in parsed_args.containers: - lib_container.delete_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - container, + self.app.client_manager.object_store.container_delete( + container=container, ) @@ -150,9 +145,7 @@ def take_action(self, parsed_args): if parsed_args.all: kwargs['full_listing'] = True - data = lib_container.list_containers( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, + data = self.app.client_manager.object_store.container_list( **kwargs ) @@ -180,10 +173,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) - lib_container.save_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container + self.app.client_manager.object_store.container_save( + container=parsed_args.container, ) @@ -204,10 +195,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - data = lib_container.show_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, + data = self.app.client_manager.object_store.container_show( + container=parsed_args.container, ) return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py deleted file mode 100644 index 4293ff4a20..0000000000 --- a/openstackclient/object/v1/lib/container.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2010-2012 OpenStack Foundation -# Copyright 2013 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Object v1 API library""" - -try: - from urllib.parse import urlparse # noqa -except ImportError: - from urlparse import urlparse # noqa - -from openstackclient.object.v1.lib import object as object_lib - - -def create_container( - session, - url, - container, -): - """Create a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to create - :returns: dict of returned headers - """ - - response = session.put("%s/%s" % (url, container)) - url_parts = urlparse(url) - data = { - 'account': url_parts.path.split('/')[-1], - 'container': container, - 'x-trans-id': response.headers.get('x-trans-id', None), - } - - return data - - -def delete_container( - session, - url, - container, -): - """Delete a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to delete - """ - - session.delete("%s/%s" % (url, container)) - - -def list_containers( - session, - url, - marker=None, - limit=None, - end_marker=None, - prefix=None, - full_listing=False, -): - """Get containers in an account - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param marker: marker query - :param limit: limit query - :param end_marker: end_marker query - :param prefix: prefix query - :param full_listing: if True, return a full listing, else returns a max - of 10000 listings - :returns: list of containers - """ - - if full_listing: - data = listing = list_containers( - session, - url, - marker, - limit, - end_marker, - prefix, - ) - while listing: - marker = listing[-1]['name'] - listing = list_containers( - session, - url, - marker, - limit, - end_marker, - prefix, - ) - if listing: - data.extend(listing) - return data - - params = { - 'format': 'json', - } - if marker: - params['marker'] = marker - if limit: - params['limit'] = limit - if end_marker: - params['end_marker'] = end_marker - if prefix: - params['prefix'] = prefix - return session.get(url, params=params).json() - - -def save_container( - session, - url, - container -): - """Save all the content from a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to save - """ - - objects = object_lib.list_objects(session, url, container) - for object in objects: - object_lib.save_object(session, url, container, object['name']) - - -def show_container( - session, - url, - container, -): - """Get container details - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to show - :returns: dict of returned headers - """ - - response = session.head("%s/%s" % (url, container)) - data = { - 'account': response.headers.get('x-container-meta-owner', None), - 'container': container, - 'object_count': response.headers.get( - 'x-container-object-count', - None, - ), - 'bytes_used': response.headers.get('x-container-bytes-used', None), - 'read_acl': response.headers.get('x-container-read', None), - 'write_acl': response.headers.get('x-container-write', None), - 'sync_to': response.headers.get('x-container-sync-to', None), - 'sync_key': response.headers.get('x-container-sync-key', None), - } - - return data diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py deleted file mode 100644 index 7a23fc7698..0000000000 --- a/openstackclient/object/v1/lib/object.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright 2010-2012 OpenStack Foundation -# Copyright 2013 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Object v1 API library""" - -import os - -import six - -try: - from urllib.parse import urlparse # noqa -except ImportError: - from urlparse import urlparse # noqa - - -def create_object( - session, - url, - container, - object, -): - """Create an object, upload it to a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to store object - :param object: local path to object - :returns: dict of returned headers - """ - - full_url = "%s/%s/%s" % (url, container, object) - response = session.put(full_url, data=open(object)) - url_parts = urlparse(url) - data = { - 'account': url_parts.path.split('/')[-1], - 'container': container, - 'object': object, - 'x-trans-id': response.headers.get('X-Trans-Id', None), - 'etag': response.headers.get('Etag', None), - } - - return data - - -def delete_object( - session, - url, - container, - object, -): - """Delete an object stored in a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container that stores object - :param container: name of object to delete - """ - - session.delete("%s/%s/%s" % (url, container, object)) - - -def list_objects( - session, - url, - container, - marker=None, - limit=None, - end_marker=None, - delimiter=None, - prefix=None, - path=None, - full_listing=False, -): - """Get objects in a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: container name to get a listing for - :param marker: marker query - :param limit: limit query - :param end_marker: marker query - :param delimiter: string to delimit the queries on - :param prefix: prefix query - :param path: path query (equivalent: "delimiter=/" and "prefix=path/") - :param full_listing: if True, return a full listing, else returns a max - of 10000 listings - :returns: a tuple of (response headers, a list of objects) The response - headers will be a dict and all header names will be lowercase. - """ - - if full_listing: - data = listing = list_objects( - session, - url, - container, - marker, - limit, - end_marker, - delimiter, - prefix, - path, - ) - while listing: - if delimiter: - marker = listing[-1].get('name', listing[-1].get('subdir')) - else: - marker = listing[-1]['name'] - listing = list_objects( - session, - url, - container, - marker, - limit, - end_marker, - delimiter, - prefix, - path, - ) - if listing: - data.extend(listing) - return data - - params = { - 'format': 'json', - } - if marker: - params['marker'] = marker - if limit: - params['limit'] = limit - if end_marker: - params['end_marker'] = end_marker - if delimiter: - params['delimiter'] = delimiter - if prefix: - params['prefix'] = prefix - if path: - params['path'] = path - requrl = "%s/%s" % (url, container) - return session.get(requrl, params=params).json() - - -def save_object( - session, - url, - container, - obj, - file=None -): - """Save an object stored in a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container that stores object - :param object: name of object to save - :param file: local name of object - """ - - if not file: - file = obj - - response = session.get("%s/%s/%s" % (url, container, obj), stream=True) - if response.status_code == 200: - if not os.path.exists(os.path.dirname(file)): - os.makedirs(os.path.dirname(file)) - with open(file, 'wb') as f: - for chunk in response.iter_content(): - f.write(chunk) - - -def show_object( - session, - url, - container, - obj, -): - """Get object details - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: container name to get a listing for - :returns: dict of object properties - """ - - response = session.head("%s/%s/%s" % (url, container, obj)) - data = { - 'account': response.headers.get('x-container-meta-owner', None), - 'container': container, - 'object': obj, - 'content-type': response.headers.get('content-type', None), - } - if 'content-length' in response.headers: - data['content-length'] = response.headers.get('content-length', None) - if 'last-modified' in response.headers: - data['last-modified'] = response.headers.get('last-modified', None) - if 'etag' in response.headers: - data['etag'] = response.headers.get('etag', None) - if 'x-object-manifest' in response.headers: - data['x-object-manifest'] = response.headers.get( - 'x-object-manifest', None) - for key, value in six.iteritems(response.headers): - if key.startswith('x-object-meta-'): - data[key[len('x-object-meta-'):].lower()] = value - elif key not in ( - 'content-type', 'content-length', 'last-modified', - 'etag', 'date', 'x-object-manifest', 'x-container-meta-owner'): - data[key.lower()] = value - - return data diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index f0ea763300..cbe9da2fb4 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -24,7 +24,6 @@ from cliff import show from openstackclient.common import utils -from openstackclient.object.v1.lib import object as lib_object class CreateObject(lister.Lister): @@ -52,11 +51,9 @@ def take_action(self, parsed_args): results = [] for obj in parsed_args.objects: - data = lib_object.create_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - obj, + data = self.app.client_manager.object_store.object_create( + container=parsed_args.container, + object=obj, ) results.append(data) @@ -92,12 +89,9 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) for obj in parsed_args.objects: - lib_object.delete_object( - self.app.restapi, - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - obj, + self.app.client_manager.object_store.object_delete( + container=parsed_args.container, + object=obj, ) @@ -181,10 +175,8 @@ def take_action(self, parsed_args): if parsed_args.all: kwargs['full_listing'] = True - data = lib_object.list_objects( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, + data = self.app.client_manager.object_store.object_list( + container=parsed_args.container, **kwargs ) @@ -222,12 +214,10 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) - lib_object.save_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - parsed_args.object, - parsed_args.file, + self.app.client_manager.object_store.object_save( + container=parsed_args.container, + object=parsed_args.object, + file=parsed_args.file, ) @@ -253,11 +243,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - data = lib_object.show_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - parsed_args.object, + data = self.app.client_manager.object_store.object_show( + container=parsed_args.container, + object=parsed_args.object, ) return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py new file mode 100644 index 0000000000..5a376a454b --- /dev/null +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -0,0 +1,326 @@ +# 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. +# + +"""Object Store v1 API Library Tests""" + +from requests_mock.contrib import fixture + +from keystoneclient import session +from openstackclient.api import object_store_v1 as object_store +from openstackclient.tests import utils + + +FAKE_ACCOUNT = 'q12we34r' +FAKE_AUTH = '11223344556677889900' +FAKE_URL = 'http://gopher.com/v1/' + FAKE_ACCOUNT + +FAKE_CONTAINER = 'rainbarrel' +FAKE_OBJECT = 'spigot' + +LIST_CONTAINER_RESP = [ + 'qaz', + 'fred', +] + +LIST_OBJECT_RESP = [ + {'name': 'fred', 'bytes': 1234, 'content_type': 'text'}, + {'name': 'wilma', 'bytes': 5678, 'content_type': 'text'}, +] + + +class TestObjectAPIv1(utils.TestCase): + + def setUp(self): + super(TestObjectAPIv1, self).setUp() + sess = session.Session() + self.api = object_store.APIv1(session=sess, endpoint=FAKE_URL) + self.requests_mock = self.useFixture(fixture.Fixture()) + + +class TestContainer(TestObjectAPIv1): + + def setUp(self): + super(TestContainer, self).setUp() + + def test_container_create(self): + headers = { + 'x-trans-id': '1qaz2wsx', + } + self.requests_mock.register_uri( + 'PUT', + FAKE_URL + '/qaz', + headers=headers, + status_code=201, + ) + ret = self.api.container_create(container='qaz') + data = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'x-trans-id': '1qaz2wsx', + } + self.assertEqual(data, ret) + + def test_container_delete(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_URL + '/qaz', + status_code=204, + ) + ret = self.api.container_delete(container='qaz') + self.assertIsNone(ret) + + def test_container_list_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL, + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.container_list() + self.assertEqual(LIST_CONTAINER_RESP, ret) + + def test_container_list_prefix(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '?prefix=foo%2f&format=json', + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.container_list( + prefix='foo/', + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) + + def test_container_list_marker_limit_end(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '?marker=next&limit=2&end_marker=stop&format=json', + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.container_list( + marker='next', + limit=2, + end_marker='stop', + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) + +# def test_container_list_full_listing(self): +# sess = self.app.client_manager.session +# +# def side_effect(*args, **kwargs): +# rv = sess.get().json.return_value +# sess.get().json.return_value = [] +# sess.get().json.side_effect = None +# return rv +# +# resp = [{'name': 'is-name'}] +# sess.get().json.return_value = resp +# sess.get().json.side_effect = side_effect +# +# data = lib_container.list_containers( +# self.app.client_manager.session, +# fake_url, +# full_listing=True, +# ) +# +# # Check expected values +# sess.get.assert_called_with( +# fake_url, +# params={ +# 'format': 'json', +# 'marker': 'is-name', +# } +# ) +# self.assertEqual(resp, data) + + def test_container_show(self): + headers = { + 'X-Container-Meta-Owner': FAKE_ACCOUNT, + 'x-container-object-count': '1', + 'x-container-bytes-used': '577', + } + resp = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'object_count': '1', + 'bytes_used': '577', + 'read_acl': None, + 'write_acl': None, + 'sync_to': None, + 'sync_key': None, + } + self.requests_mock.register_uri( + 'HEAD', + FAKE_URL + '/qaz', + headers=headers, + status_code=204, + ) + ret = self.api.container_show(container='qaz') + self.assertEqual(resp, ret) + + +class TestObject(TestObjectAPIv1): + + def setUp(self): + super(TestObject, self).setUp() + + def test_object_create(self): + headers = { + 'etag': 'youreit', + 'x-trans-id': '1qaz2wsx', + } + self.requests_mock.register_uri( + 'PUT', + FAKE_URL + '/qaz/requirements.txt', + headers=headers, + status_code=201, + ) + ret = self.api.object_create( + container='qaz', + object='requirements.txt', + ) + data = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'object': 'requirements.txt', + 'etag': 'youreit', + 'x-trans-id': '1qaz2wsx', + } + self.assertEqual(data, ret) + + def test_object_delete(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_URL + '/qaz/wsx', + status_code=204, + ) + ret = self.api.object_delete( + container='qaz', + object='wsx', + ) + self.assertIsNone(ret) + + def test_object_list_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz', + json=LIST_OBJECT_RESP, + status_code=200, + ) + ret = self.api.object_list(container='qaz') + self.assertEqual(LIST_OBJECT_RESP, ret) + + def test_object_list_delimiter(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz?delimiter=%7C', + json=LIST_OBJECT_RESP, + status_code=200, + ) + ret = self.api.object_list( + container='qaz', + delimiter='|', + ) + self.assertEqual(LIST_OBJECT_RESP, ret) + + def test_object_list_prefix(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz?prefix=foo%2f', + json=LIST_OBJECT_RESP, + status_code=200, + ) + ret = self.api.object_list( + container='qaz', + prefix='foo/', + ) + self.assertEqual(LIST_OBJECT_RESP, ret) + + def test_object_list_marker_limit_end(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz?marker=next&limit=2&end_marker=stop', + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.object_list( + container='qaz', + marker='next', + limit=2, + end_marker='stop', + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) + +# def test_list_objects_full_listing(self): +# sess = self.app.client_manager.session +# +# def side_effect(*args, **kwargs): +# rv = sess.get().json.return_value +# sess.get().json.return_value = [] +# sess.get().json.side_effect = None +# return rv +# +# resp = [{'name': 'is-name'}] +# sess.get().json.return_value = resp +# sess.get().json.side_effect = side_effect +# +# data = lib_object.list_objects( +# sess, +# fake_url, +# fake_container, +# full_listing=True, +# ) +# +# # Check expected values +# sess.get.assert_called_with( +# fake_url + '/' + fake_container, +# params={ +# 'format': 'json', +# 'marker': 'is-name', +# } +# ) +# self.assertEqual(resp, data) + + def test_object_show(self): + headers = { + 'content-type': 'text/alpha', + 'content-length': '577', + 'last-modified': '20130101', + 'etag': 'qaz', + 'x-container-meta-owner': FAKE_ACCOUNT, + 'x-object-meta-wife': 'Wilma', + 'x-tra-header': 'yabba-dabba-do', + } + resp = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'object': FAKE_OBJECT, + 'content-type': 'text/alpha', + 'content-length': '577', + 'last-modified': '20130101', + 'etag': 'qaz', + 'wife': 'Wilma', + 'x-tra-header': 'yabba-dabba-do', + } + self.requests_mock.register_uri( + 'HEAD', + FAKE_URL + '/qaz/' + FAKE_OBJECT, + headers=headers, + status_code=204, + ) + ret = self.api.object_show( + container='qaz', + object=FAKE_OBJECT, + ) + self.assertEqual(resp, ret) diff --git a/openstackclient/tests/object/v1/lib/__init__.py b/openstackclient/tests/object/v1/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py deleted file mode 100644 index ce70b8356b..0000000000 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Test Object API library module""" - -import mock - -from openstackclient.object.v1.lib import container as lib_container -from openstackclient.tests import fakes -from openstackclient.tests.object.v1 import fakes as object_fakes - - -fake_account = 'q12we34r' -fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com/v1/' + fake_account - -fake_container = 'rainbarrel' - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.endpoint = fake_url - self.token = fake_auth - - -class TestContainer(object_fakes.TestObjectv1): - - def setUp(self): - super(TestContainer, self).setUp() - self.app.client_manager.session = mock.MagicMock() - - -class TestContainerList(TestContainer): - - def test_container_list_no_options(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - } - ) - self.assertEqual(resp, data) - - def test_container_list_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - marker='next', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'marker': 'next', - } - ) - self.assertEqual(resp, data) - - def test_container_list_limit(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - limit=5, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'limit': 5, - } - ) - self.assertEqual(resp, data) - - def test_container_list_end_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - end_marker='last', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'end_marker': 'last', - } - ) - self.assertEqual(resp, data) - - def test_container_list_prefix(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - prefix='foo/', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'prefix': 'foo/', - } - ) - self.assertEqual(resp, data) - - def test_container_list_full_listing(self): - sess = self.app.client_manager.session - - def side_effect(*args, **kwargs): - rv = sess.get().json.return_value - sess.get().json.return_value = [] - sess.get().json.side_effect = None - return rv - - resp = [{'name': 'is-name'}] - sess.get().json.return_value = resp - sess.get().json.side_effect = side_effect - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - full_listing=True, - ) - - # Check expected values - sess.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'marker': 'is-name', - } - ) - self.assertEqual(resp, data) - - -class TestContainerShow(TestContainer): - - def test_container_show_no_options(self): - resp = { - 'X-Container-Meta-Owner': fake_account, - 'x-container-object-count': 1, - 'x-container-bytes-used': 577, - } - self.app.client_manager.session.head.return_value = \ - fakes.FakeResponse(headers=resp) - - data = lib_container.show_container( - self.app.client_manager.session, - fake_url, - 'is-name', - ) - - # Check expected values - self.app.client_manager.session.head.assert_called_with( - fake_url + '/is-name', - ) - - data_expected = { - 'account': fake_account, - 'container': 'is-name', - 'object_count': 1, - 'bytes_used': 577, - 'read_acl': None, - 'write_acl': None, - 'sync_to': None, - 'sync_key': None, - } - self.assertEqual(data_expected, data) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py deleted file mode 100644 index f96732b468..0000000000 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Test Object API library module""" - -import mock - -from openstackclient.object.v1.lib import object as lib_object -from openstackclient.tests import fakes -from openstackclient.tests.object.v1 import fakes as object_fakes - - -fake_account = 'q12we34r' -fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com/v1/' + fake_account - -fake_container = 'rainbarrel' -fake_object = 'raindrop' - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.endpoint = fake_url - self.token = fake_auth - - -class TestObject(object_fakes.TestObjectv1): - - def setUp(self): - super(TestObject, self).setUp() - self.app.client_manager.session = mock.MagicMock() - - -class TestObjectListObjects(TestObject): - - def test_list_objects_no_options(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - marker='next', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'marker': 'next', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_limit(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - limit=5, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'limit': 5, - } - ) - self.assertEqual(resp, data) - - def test_list_objects_end_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - end_marker='last', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'end_marker': 'last', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_delimiter(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - delimiter='|', - ) - - # Check expected values - # NOTE(dtroyer): requests handles the URL encoding and we're - # mocking that so use the otherwise-not-legal - # pipe '|' char in the response. - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'delimiter': '|', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_prefix(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - prefix='foo/', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'prefix': 'foo/', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_path(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - path='next', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'path': 'next', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_full_listing(self): - sess = self.app.client_manager.session - - def side_effect(*args, **kwargs): - rv = sess.get().json.return_value - sess.get().json.return_value = [] - sess.get().json.side_effect = None - return rv - - resp = [{'name': 'is-name'}] - sess.get().json.return_value = resp - sess.get().json.side_effect = side_effect - - data = lib_object.list_objects( - sess, - fake_url, - fake_container, - full_listing=True, - ) - - # Check expected values - sess.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'marker': 'is-name', - } - ) - self.assertEqual(resp, data) - - -class TestObjectShowObjects(TestObject): - - def test_object_show_no_options(self): - resp = { - 'content-type': 'text/alpha', - 'x-container-meta-owner': fake_account, - } - self.app.client_manager.session.head.return_value = \ - fakes.FakeResponse(headers=resp) - - data = lib_object.show_object( - self.app.client_manager.session, - fake_url, - fake_container, - fake_object, - ) - - # Check expected values - self.app.client_manager.session.head.assert_called_with( - fake_url + '/%s/%s' % (fake_container, fake_object), - ) - - data_expected = { - 'account': fake_account, - 'container': fake_container, - 'object': fake_object, - 'content-type': 'text/alpha', - } - self.assertEqual(data_expected, data) - - def test_object_show_all_options(self): - resp = { - 'content-type': 'text/alpha', - 'content-length': 577, - 'last-modified': '20130101', - 'etag': 'qaz', - 'x-container-meta-owner': fake_account, - 'x-object-manifest': None, - 'x-object-meta-wife': 'Wilma', - 'x-tra-header': 'yabba-dabba-do', - } - self.app.client_manager.session.head.return_value = \ - fakes.FakeResponse(headers=resp) - - data = lib_object.show_object( - self.app.client_manager.session, - fake_url, - fake_container, - fake_object, - ) - - # Check expected values - self.app.client_manager.session.head.assert_called_with( - fake_url + '/%s/%s' % (fake_container, fake_object), - ) - - data_expected = { - 'account': fake_account, - 'container': fake_container, - 'object': fake_object, - 'content-type': 'text/alpha', - 'content-length': 577, - 'last-modified': '20130101', - 'etag': 'qaz', - 'x-object-manifest': None, - 'wife': 'Wilma', - 'x-tra-header': 'yabba-dabba-do', - } - self.assertEqual(data_expected, data) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index b72c79d653..70b88fc425 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -16,6 +16,7 @@ import copy import mock +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container from openstackclient.tests.object.v1 import fakes as object_fakes @@ -30,28 +31,20 @@ def __init__(self, endpoint=None, **kwargs): self.token = AUTH_TOKEN -class TestObject(object_fakes.TestObjectv1): +class TestContainer(object_fakes.TestObjectv1): def setUp(self): - super(TestObject, self).setUp() - - -class TestObjectClient(TestObject): - - def test_make_client(self): - self.assertEqual( - self.app.client_manager.object_store.endpoint, - AUTH_URL, - ) - self.assertEqual( - self.app.client_manager.object_store.token, - AUTH_TOKEN, + super(TestContainer, self).setUp() + self.app.client_manager.object_store = object_store.APIv1( + session=mock.Mock(), + service_type="object-store", ) + self.api = self.app.client_manager.object_store @mock.patch( - 'openstackclient.object.v1.container.lib_container.list_containers' + 'openstackclient.api.object_store_v1.APIv1.container_list' ) -class TestContainerList(TestObject): +class TestContainerList(TestContainer): def setUp(self): super(TestContainerList, self).setUp() @@ -77,8 +70,6 @@ def test_object_list_containers_no_options(self, c_mock): kwargs = { } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -113,8 +104,6 @@ def test_object_list_containers_prefix(self, c_mock): 'prefix': 'bit', } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -134,43 +123,10 @@ def test_object_list_containers_marker(self, c_mock): arglist = [ '--marker', object_fakes.container_name, - ] - verifylist = [ - ('marker', object_fakes.container_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'marker': object_fakes.container_name, - } - c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - **kwargs - ) - - collist = ('Name',) - self.assertEqual(columns, collist) - datalist = ( - (object_fakes.container_name, ), - (object_fakes.container_name_3, ), - ) - self.assertEqual(tuple(data), datalist) - - def test_object_list_containers_end_marker(self, c_mock): - c_mock.return_value = [ - copy.deepcopy(object_fakes.CONTAINER), - copy.deepcopy(object_fakes.CONTAINER_3), - ] - - arglist = [ '--end-marker', object_fakes.container_name_3, ] verifylist = [ + ('marker', object_fakes.container_name), ('end_marker', object_fakes.container_name_3), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -180,11 +136,10 @@ def test_object_list_containers_end_marker(self, c_mock): # Set expected values kwargs = { + 'marker': object_fakes.container_name, 'end_marker': object_fakes.container_name_3, } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -218,8 +173,6 @@ def test_object_list_containers_limit(self, c_mock): 'limit': 2, } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -252,8 +205,6 @@ def test_object_list_containers_long(self, c_mock): kwargs = { } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -296,8 +247,6 @@ def test_object_list_containers_all(self, c_mock): 'full_listing': True, } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -312,9 +261,9 @@ def test_object_list_containers_all(self, c_mock): @mock.patch( - 'openstackclient.object.v1.container.lib_container.show_container' + 'openstackclient.api.object_store_v1.APIv1.container_show' ) -class TestContainerShow(TestObject): +class TestContainerShow(TestContainer): def setUp(self): super(TestContainerShow, self).setUp() @@ -341,9 +290,7 @@ def test_container_show(self, c_mock): } # lib.container.show_container(api, url, container) c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, **kwargs ) diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 29d78454fa..53b60b9abb 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -16,6 +16,7 @@ from requests_mock.contrib import fixture from keystoneclient import session +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container from openstackclient.tests.object.v1 import fakes as object_fakes @@ -29,7 +30,10 @@ def setUp(self): self.requests_mock = self.useFixture(fixture.Fixture()) # TODO(dtroyer): move this to object_fakes.TestObjectv1 - self.app.client_manager.object_store.endpoint = object_fakes.ENDPOINT + self.app.client_manager.object_store = object_store.APIv1( + session=self.app.client_manager.session, + endpoint=object_fakes.ENDPOINT, + ) class TestContainerCreate(TestObjectAll): diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index 26d07b2ca7..22f06999e2 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -16,6 +16,7 @@ import copy import mock +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as obj from openstackclient.tests.object.v1 import fakes as object_fakes @@ -27,23 +28,15 @@ class TestObject(object_fakes.TestObjectv1): def setUp(self): super(TestObject, self).setUp() - - -class TestObjectClient(TestObject): - - def test_make_client(self): - self.assertEqual( - self.app.client_manager.object_store.endpoint, - AUTH_URL, - ) - self.assertEqual( - self.app.client_manager.object_store.token, - AUTH_TOKEN, + self.app.client_manager.object_store = object_store.APIv1( + session=mock.Mock(), + service_type="object-store", ) + self.api = self.app.client_manager.object_store @mock.patch( - 'openstackclient.object.v1.object.lib_object.list_objects' + 'openstackclient.api.object_store_v1.APIv1.object_list' ) class TestObjectList(TestObject): @@ -71,9 +64,7 @@ def test_object_list_objects_no_options(self, o_mock): columns, data = self.cmd.take_action(parsed_args) o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, ) collist = ('Name',) @@ -107,9 +98,7 @@ def test_object_list_objects_prefix(self, o_mock): 'prefix': 'floppy', } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -143,9 +132,7 @@ def test_object_list_objects_delimiter(self, o_mock): 'delimiter': '=', } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -179,9 +166,7 @@ def test_object_list_objects_marker(self, o_mock): 'marker': object_fakes.object_name_2, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -215,9 +200,7 @@ def test_object_list_objects_end_marker(self, o_mock): 'end_marker': object_fakes.object_name_2, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -251,9 +234,7 @@ def test_object_list_objects_limit(self, o_mock): 'limit': 2, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -287,9 +268,7 @@ def test_object_list_objects_long(self, o_mock): kwargs = { } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, **kwargs ) @@ -337,9 +316,7 @@ def test_object_list_objects_all(self, o_mock): 'full_listing': True, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, **kwargs ) @@ -353,7 +330,7 @@ def test_object_list_objects_all(self, o_mock): @mock.patch( - 'openstackclient.object.v1.object.lib_object.show_object' + 'openstackclient.api.object_store_v1.APIv1.object_show' ) class TestObjectShow(TestObject): @@ -384,10 +361,8 @@ def test_object_show(self, c_mock): } # lib.container.show_container(api, url, container) c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, - object_fakes.object_name_1, + container=object_fakes.container_name, + object=object_fakes.object_name_1, **kwargs ) From 742982af4bb94b73a78c06688732acf1c8127f8a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 19 Sep 2014 02:42:55 +0000 Subject: [PATCH 0012/3303] Add functional tests to osc Create a script that kicks off function tests that exercise openstackclient commands against a cloud. If no keystone/openstack process is detected, a devstack instance is spun up and the tests are run against that. There is also a hook added to tox.ini so that we can run these tests easily from a gate job. Change-Id: I3cc8b2b800de7ca74af506d2c7e8ee481fa985f0 --- functional/__init__.py | 0 functional/common/__init__.py | 0 functional/common/exceptions.py | 26 ++++++ functional/common/test.py | 129 ++++++++++++++++++++++++++++++ functional/harpoon.sh | 30 +++++++ functional/harpoonrc | 14 ++++ functional/tests/__init__.py | 0 functional/tests/test_identity.py | 35 ++++++++ post_test_hook.sh | 15 ++++ tox.ini | 4 + 10 files changed, 253 insertions(+) create mode 100644 functional/__init__.py create mode 100644 functional/common/__init__.py create mode 100644 functional/common/exceptions.py create mode 100644 functional/common/test.py create mode 100755 functional/harpoon.sh create mode 100644 functional/harpoonrc create mode 100644 functional/tests/__init__.py create mode 100644 functional/tests/test_identity.py create mode 100755 post_test_hook.sh diff --git a/functional/__init__.py b/functional/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functional/common/__init__.py b/functional/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functional/common/exceptions.py b/functional/common/exceptions.py new file mode 100644 index 0000000000..47c6071e28 --- /dev/null +++ b/functional/common/exceptions.py @@ -0,0 +1,26 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class CommandFailed(Exception): + def __init__(self, returncode, cmd, output, stderr): + super(CommandFailed, self).__init__() + self.returncode = returncode + self.cmd = cmd + self.stdout = output + self.stderr = stderr + + def __str__(self): + return ("Command '%s' returned non-zero exit status %d.\n" + "stdout:\n%s\n" + "stderr:\n%s" % (self.cmd, self.returncode, + self.stdout, self.stderr)) diff --git a/functional/common/test.py b/functional/common/test.py new file mode 100644 index 0000000000..c1bb0b101a --- /dev/null +++ b/functional/common/test.py @@ -0,0 +1,129 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re +import shlex +import subprocess +import testtools + +import six + +from functional.common import exceptions + + +def execute(cmd, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes specified command for the given action.""" + cmd = ' '.join([cmd, flags, action, params]) + cmd = shlex.split(cmd.encode('utf-8')) + result = '' + result_err = '' + stdout = subprocess.PIPE + stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE + proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) + result, result_err = proc.communicate() + if not fail_ok and proc.returncode != 0: + raise exceptions.CommandFailed(proc.returncode, cmd, result, + result_err) + return result + + +class TestCase(testtools.TestCase): + + delimiter_line = re.compile('^\+\-[\+\-]+\-\+$') + + def openstack(self, action, flags='', params='', fail_ok=False): + """Executes openstackclient command for the given action.""" + return execute('openstack', action, flags, params, fail_ok) + + def assert_table_structure(self, items, field_names): + """Verify that all items have keys listed in field_names.""" + for item in items: + for field in field_names: + self.assertIn(field, item) + + def assert_show_fields(self, items, field_names): + """Verify that all items have keys listed in field_names.""" + for item in items: + for key in six.iterkeys(item): + self.assertIn(key, field_names) + + def parse_show(self, raw_output): + """Return list of dicts with item values parsed from cli output.""" + + items = [] + table_ = self.table(raw_output) + for row in table_['values']: + item = {} + item[row[0]] = row[1] + items.append(item) + return items + + def parse_listing(self, raw_output): + """Return list of dicts with basic item parsed from cli output.""" + + items = [] + table_ = self.table(raw_output) + for row in table_['values']: + item = {} + for col_idx, col_key in enumerate(table_['headers']): + item[col_key] = row[col_idx] + items.append(item) + return items + + def table(self, output_lines): + """Parse single table from cli output. + + Return dict with list of column names in 'headers' key and + rows in 'values' key. + """ + table_ = {'headers': [], 'values': []} + columns = None + + if not isinstance(output_lines, list): + output_lines = output_lines.split('\n') + + if not output_lines[-1]: + # skip last line if empty (just newline at the end) + output_lines = output_lines[:-1] + + for line in output_lines: + if self.delimiter_line.match(line): + columns = self._table_columns(line) + continue + if '|' not in line: + continue + row = [] + for col in columns: + row.append(line[col[0]:col[1]].strip()) + if table_['headers']: + table_['values'].append(row) + else: + table_['headers'] = row + + return table_ + + def _table_columns(self, first_table_row): + """Find column ranges in output line. + + Return list of tuples (start,end) for each column + detected by plus (+) characters in delimiter line. + """ + positions = [] + start = 1 # there is '+' at 0 + while start < len(first_table_row): + end = first_table_row.find('+', start) + if end == -1: + break + positions.append((start, end)) + start = end + 1 + return positions diff --git a/functional/harpoon.sh b/functional/harpoon.sh new file mode 100755 index 0000000000..76c10ffb95 --- /dev/null +++ b/functional/harpoon.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +FUNCTIONAL_TEST_DIR=$(cd $(dirname "$0") && pwd) +source $FUNCTIONAL_TEST_DIR/harpoonrc + +OPENSTACKCLIENT_DIR=$FUNCTIONAL_TEST_DIR/.. + +if [[ -z $DEVSTACK_DIR ]]; then + echo "guessing location of devstack" + DEVSTACK_DIR=$OPENSTACKCLIENT_DIR/../devstack +fi + +function setup_credentials { + RC_FILE=$DEVSTACK_DIR/accrc/$HARPOON_USER/$HARPOON_TENANT + source $RC_FILE + echo 'sourcing' $RC_FILE + echo 'running tests with' + env | grep OS +} + +function run_tests { + cd $FUNCTIONAL_TEST_DIR + python -m testtools.run discover + rvalue=$? + cd $OPENSTACKCLIENT_DIR + exit $rvalue +} + +setup_credentials +run_tests diff --git a/functional/harpoonrc b/functional/harpoonrc new file mode 100644 index 0000000000..ed9201ca1e --- /dev/null +++ b/functional/harpoonrc @@ -0,0 +1,14 @@ +# Global options +#RECLONE=yes + +# Devstack options +#ADMIN_PASSWORD=openstack +#MYSQL_PASSWORD=openstack +#RABBIT_PASSWORD=openstack +#SERVICE_TOKEN=openstack +#SERVICE_PASSWORD=openstack + +# Harpoon options +HARPOON_USER=admin +HARPOON_TENANT=admin +#DEVSTACK_DIR=/opt/stack/devstack diff --git a/functional/tests/__init__.py b/functional/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py new file mode 100644 index 0000000000..5f8b4cb09c --- /dev/null +++ b/functional/tests/test_identity.py @@ -0,0 +1,35 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functional.common import exceptions +from functional.common import test + + +class IdentityV2Tests(test.TestCase): + """Functional tests for Identity V2 commands. """ + + def test_user_list(self): + field_names = ['ID', 'Name'] + raw_output = self.openstack('user list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, field_names) + + def test_user_get(self): + field_names = ['email', 'enabled', 'id', 'name', + 'project_id', 'username'] + raw_output = self.openstack('user show admin') + items = self.parse_show(raw_output) + self.assert_show_fields(items, field_names) + + def test_bad_user_command(self): + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'user unlist') diff --git a/post_test_hook.sh b/post_test_hook.sh new file mode 100755 index 0000000000..b82c1e62a9 --- /dev/null +++ b/post_test_hook.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# This is a script that kicks off a series of functional tests against an +# OpenStack cloud. It will attempt to create an instance if one is not +# available. Do not run this script unless you know what you're doing. +# For more information refer to: +# http://docs.openstack.org/developer/python-openstackclient/ + +set -xe + +OPENSTACKCLIENT_DIR=$(cd $(dirname "$0") && pwd) + +cd $OPENSTACKCLIENT_DIR +echo "Running openstackclient functional test suite" +sudo -H -u stack tox -e functional diff --git a/tox.ini b/tox.ini index 42cf42417b..b9493bc598 100644 --- a/tox.ini +++ b/tox.ini @@ -11,10 +11,14 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' +whitelist_externals = bash [testenv:pep8] commands = flake8 +[testenv:functional] +commands = bash -x {toxinidir}/functional/harpoon.sh + [testenv:venv] commands = {posargs} From 3842960f7189903f1ea5ab432cedc3ca803390d4 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 30 Sep 2014 18:28:01 -0400 Subject: [PATCH 0013/3303] Create a whole slew of functional tests for identity Complete the remaining identity v2 and v3 functional tests Change-Id: I193fd95e58a38caeb66d37c17cde75b983c48ca0 --- functional/tests/test_identity.py | 118 ++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py index 5f8b4cb09c..cdb0ed341b 100644 --- a/functional/tests/test_identity.py +++ b/functional/tests/test_identity.py @@ -10,26 +10,132 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import uuid + from functional.common import exceptions from functional.common import test +BASIC_LIST_HEADERS = ['ID', 'Name'] + class IdentityV2Tests(test.TestCase): """Functional tests for Identity V2 commands. """ + USER_FIELDS = ['email', 'enabled', 'id', 'name', 'project_id', 'username'] + PROJECT_FIELDS = ['enabled', 'id', 'name', 'description'] + def test_user_list(self): - field_names = ['ID', 'Name'] raw_output = self.openstack('user list') items = self.parse_listing(raw_output) - self.assert_table_structure(items, field_names) + self.assert_table_structure(items, BASIC_LIST_HEADERS) - def test_user_get(self): - field_names = ['email', 'enabled', 'id', 'name', - 'project_id', 'username'] + def test_user_show(self): raw_output = self.openstack('user show admin') items = self.parse_show(raw_output) - self.assert_show_fields(items, field_names) + self.assert_show_fields(items, self.USER_FIELDS) + + def test_user_create(self): + raw_output = self.openstack('user create mjordan --password bulls' + ' --email hoops@example.com') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.USER_FIELDS) + + def test_user_delete(self): + self.openstack('user create dummy') + raw_output = self.openstack('user delete dummy') + self.assertEqual(0, len(raw_output)) def test_bad_user_command(self): self.assertRaises(exceptions.CommandFailed, self.openstack, 'user unlist') + + def test_project_list(self): + raw_output = self.openstack('project list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_project_show(self): + raw_output = self.openstack('project show admin') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.PROJECT_FIELDS) + + def test_project_create(self): + raw_output = self.openstack('project create test-project') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.PROJECT_FIELDS) + + def test_project_delete(self): + self.openstack('project create dummy-project') + raw_output = self.openstack('project delete dummy-project') + self.assertEqual(0, len(raw_output)) + + +class IdentityV3Tests(test.TestCase): + """Functional tests for Identity V3 commands. """ + + DOMAIN_FIELDS = ['description', 'enabled', 'id', 'name', 'links'] + GROUP_FIELDS = ['description', 'domain_id', 'id', 'name', 'links'] + + def _create_dummy_group(self): + name = uuid.uuid4().hex + self.openstack('group create ' + name) + return name + + def _create_dummy_domain(self): + name = uuid.uuid4().hex + self.openstack('domain create ' + name) + return name + + def setUp(self): + super(IdentityV3Tests, self).setUp() + auth_url = os.environ.get('OS_AUTH_URL') + auth_url = auth_url.replace('v2.0', 'v3') + os.environ['OS_AUTH_URL'] = auth_url + os.environ['OS_IDENTITY_API_VERSION'] = '3' + os.environ['OS_USER_DOMAIN_ID'] = 'default' + os.environ['OS_PROJECT_DOMAIN_ID'] = 'default' + + def test_group_create(self): + raw_output = self.openstack('group create ' + uuid.uuid4().hex) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.GROUP_FIELDS) + + def test_group_list(self): + self._create_dummy_group() + raw_output = self.openstack('group list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_group_delete(self): + name = self._create_dummy_group() + raw_output = self.openstack('group delete ' + name) + self.assertEqual(0, len(raw_output)) + + def test_group_show(self): + name = self._create_dummy_group() + raw_output = self.openstack('group show ' + name) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.GROUP_FIELDS) + + def test_domain_create(self): + raw_output = self.openstack('domain create ' + uuid.uuid4().hex) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.DOMAIN_FIELDS) + + def test_domain_list(self): + self._create_dummy_domain() + raw_output = self.openstack('domain list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_domain_delete(self): + name = self._create_dummy_domain() + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'domain delete ' + name) + + def test_domain_show(self): + name = self._create_dummy_domain() + raw_output = self.openstack('domain show ' + name) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.DOMAIN_FIELDS) From d972b8364c891b8275434c231bcb76e56172f573 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 2 Oct 2014 13:12:41 -0400 Subject: [PATCH 0014/3303] Pass in domain and project as positional args, not kwargs The signature for users.set in keystoneclient dictates that domain and project be sent in, not domainId and projectId, which are being incorrectly sent in as 'extra' data. Closes-Bug: #1376833 Change-Id: I44df3e492f61eab2241f3758dee622417bb6f399 --- openstackclient/identity/v3/user.py | 4 ++-- openstackclient/tests/identity/v3/test_user.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 6ba54368a8..e4eb7526c2 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -308,11 +308,11 @@ def take_action(self, parsed_args): if parsed_args.project: project_id = utils.find_resource( identity_client.projects, parsed_args.project).id - kwargs['projectId'] = project_id + kwargs['project'] = project_id if parsed_args.domain: domain_id = utils.find_resource( identity_client.domains, parsed_args.domain).id - kwargs['domainId'] = domain_id + kwargs['domain'] = domain_id kwargs['enabled'] = user.enabled if parsed_args.enable: kwargs['enabled'] = True diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 42df57736e..bb59ebe568 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -842,7 +842,7 @@ def test_user_set_domain(self): # Set expected values kwargs = { 'enabled': True, - 'domainId': identity_fakes.domain_id, + 'domain': identity_fakes.domain_id, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -874,7 +874,7 @@ def test_user_set_project(self): # Set expected values kwargs = { 'enabled': True, - 'projectId': identity_fakes.project_id, + 'project': identity_fakes.project_id, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) From 693687e4ffebd38089cc55a168c743bf48df5603 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 2 Oct 2014 23:09:34 -0400 Subject: [PATCH 0015/3303] Remove duplicate env function in shell.py There already exists an env() function in utils. Let's use that one since it's common. Change-Id: I661984394cf0c0543b2f35bf76e3929dead54d1d --- openstackclient/shell.py | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 0c91ab6ef1..8db1656c85 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -18,7 +18,6 @@ import argparse import getpass import logging -import os import sys import traceback @@ -38,20 +37,6 @@ DEFAULT_DOMAIN = 'default' -def env(*vars, **kwargs): - """Search for the first defined of possibly many env vars - - Returns the first environment variable defined in vars, or - returns the default defined in kwargs. - - """ - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - class OpenStackShell(app.App): CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' @@ -191,26 +176,27 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-auth-url', metavar='', - default=env('OS_AUTH_URL'), + default=utils.env('OS_AUTH_URL'), help='Authentication URL (Env: OS_AUTH_URL)') parser.add_argument( '--os-domain-name', metavar='', - default=env('OS_DOMAIN_NAME'), + default=utils.env('OS_DOMAIN_NAME'), help='Domain name of the requested domain-level ' 'authorization scope (Env: OS_DOMAIN_NAME)', ) parser.add_argument( '--os-domain-id', metavar='', - default=env('OS_DOMAIN_ID'), + default=utils.env('OS_DOMAIN_ID'), help='Domain ID of the requested domain-level ' 'authorization scope (Env: OS_DOMAIN_ID)', ) parser.add_argument( '--os-project-name', metavar='', - default=env('OS_PROJECT_NAME', default=env('OS_TENANT_NAME')), + default=utils.env('OS_PROJECT_NAME', + default=utils.env('OS_TENANT_NAME')), help='Project name of the requested project-level ' 'authorization scope (Env: OS_PROJECT_NAME)', ) @@ -223,7 +209,8 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-project-id', metavar='', - default=env('OS_PROJECT_ID', default=env('OS_TENANT_ID')), + default=utils.env('OS_PROJECT_ID', + default=utils.env('OS_TENANT_ID')), help='Project ID of the requested project-level ' 'authorization scope (Env: OS_PROJECT_ID)', ) @@ -270,12 +257,12 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-region-name', metavar='', - default=env('OS_REGION_NAME'), + default=utils.env('OS_REGION_NAME'), help='Authentication region name (Env: OS_REGION_NAME)') parser.add_argument( '--os-cacert', metavar='', - default=env('OS_CACERT'), + default=utils.env('OS_CACERT'), help='CA certificate bundle file (Env: OS_CACERT)') verify_group = parser.add_mutually_exclusive_group() verify_group.add_argument( @@ -291,7 +278,7 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-default-domain', metavar='', - default=env( + default=utils.env( 'OS_DEFAULT_DOMAIN', default=DEFAULT_DOMAIN), help='Default domain ID, default=' + @@ -300,12 +287,12 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-token', metavar='', - default=env('OS_TOKEN'), + default=utils.env('OS_TOKEN'), help='Defaults to env[OS_TOKEN]') parser.add_argument( '--os-url', metavar='', - default=env('OS_URL'), + default=utils.env('OS_URL'), help='Defaults to env[OS_URL]') parser.add_argument( '--timing', From 1934b1b24347fbe528c02fd8eafcd4c569e96763 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 00:09:59 -0400 Subject: [PATCH 0016/3303] Place the command to generate docs on one line Change-Id: I99d78208c940bc6646327ee967e71187c32a159f --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 42cf42417b..2c3fb69073 100644 --- a/tox.ini +++ b/tox.ini @@ -25,8 +25,7 @@ commands = python setup.py test --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [testenv:docs] -commands= - python setup.py build_sphinx +commands = python setup.py build_sphinx [flake8] ignore = H305,H307,H402,H904 From 89bb5b0b8528ac793b9fc531a24ba709f73a8716 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 00:25:56 -0400 Subject: [PATCH 0017/3303] Add some code-blocks to the docs Add some basic highlighting for the docs Change-Id: Ifa740856f3ef636bdf0f60f3b7d082c68062fe9b --- doc/source/commands.rst | 8 +++++--- doc/source/plugins.rst | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 45e73a0594..8857f3d55d 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -47,11 +47,13 @@ objects. In badly formed English it is expressed as "(Take) object1 -Examples:: +Examples: - group add user +.. code-block:: bash - volume type list # 'volume type' is a two-word single object + $ group add user + + $ volume type list # 'volume type' is a two-word single object Command Arguments and Options diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst index 3b35ec08e8..0635f29ebe 100644 --- a/doc/source/plugins.rst +++ b/doc/source/plugins.rst @@ -14,7 +14,7 @@ Plugins are discovered by enumerating the entry points found under :py:mod:`openstack.cli.extension` and initializing the specified client module. -:: +.. code-block:: ini [entry_points] openstack.cli.extension = @@ -39,7 +39,7 @@ The client module must implement the following interface functions: OSC enumerates the plugin commands from the entry points in the usual manner defined for the API version: -:: +.. code-block:: ini openstack.oscplugin.v1 = plugin_list = oscplugin.v1.plugin:ListPlugin @@ -48,7 +48,7 @@ defined for the API version: Note that OSC defines the group name as :py:mod:`openstack..v` so the version should not contain the leading 'v' character. -:: +.. code-block:: python DEFAULT_OSCPLUGIN_API_VERSION = '1' From 0cb204e59b20f25b7a054b411507d6dabbc699ac Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 19:35:13 -0400 Subject: [PATCH 0018/3303] Update gitignore add .project and .pydevproject to gitignore Change-Id: Ic258ded80612d31bd3017fce65000b619026e844 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a8d9f3d69d..84079f2f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ build ChangeLog dist doc/build +# Development environment files +.project +.pydevproject From 388bbbac2ce6bf9baf2f9ceb6102b0b1f7072264 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 6 Oct 2014 03:37:46 -0400 Subject: [PATCH 0019/3303] Fix issues with object related commands 1) Can't create instance of swiftclient. Since we now create an API instance, creating a swiftclient instance won't work. Trying to do any object related command fails. 2) Listing objects in a container fails, we depend on the data returned in a specific way, during the API transition this must have slipped through. Needs regression/funcitonal tests to mame sure this doesn't happen again. Change-Id: I69079a0dc9f32b84e6f9307729d3dbbba549ac5e --- functional/tests/test_object.py | 91 ++++++++++++++++++++++++++ openstackclient/api/object_store_v1.py | 5 +- openstackclient/object/client.py | 6 -- 3 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 functional/tests/test_object.py diff --git a/functional/tests/test_object.py b/functional/tests/test_object.py new file mode 100644 index 0000000000..396f0cb7ac --- /dev/null +++ b/functional/tests/test_object.py @@ -0,0 +1,91 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from functional.common import test + +BASIC_LIST_HEADERS = ['Name'] +CONTAINER_FIELDS = ['account', 'container', 'x-trans-id'] +OBJECT_FIELDS = ['object', 'container', 'etag'] + + +class ObjectV1Tests(test.TestCase): + """Functional tests for Object V1 commands. """ + + CONTAINER_NAME = uuid.uuid4().hex + OBJECT_NAME = uuid.uuid4().hex + + # NOTE(stevemar): Not using setUp since we only want this to run once + with open(OBJECT_NAME, 'w') as f: + f.write('test content') + + def test_container_create(self): + raw_output = self.openstack('container create ' + self.CONTAINER_NAME) + items = self.parse_listing(raw_output) + self.assert_show_fields(items, CONTAINER_FIELDS) + + def test_container_delete(self): + container_tmp = uuid.uuid4().hex + self.openstack('container create ' + container_tmp) + raw_output = self.openstack('container delete ' + container_tmp) + self.assertEqual(0, len(raw_output)) + + def test_container_list(self): + raw_output = self.openstack('container list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_container_show(self): + self.openstack('container show ' + self.CONTAINER_NAME) + # TODO(stevemar): Assert returned fields + + def test_container_save(self): + self.openstack('container save ' + self.CONTAINER_NAME) + # TODO(stevemar): Assert returned fields + + def test_object_create(self): + raw_output = self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + items = self.parse_listing(raw_output) + self.assert_show_fields(items, OBJECT_FIELDS) + + def test_object_delete(self): + raw_output = self.openstack('object delete ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.assertEqual(0, len(raw_output)) + + def test_object_list(self): + raw_output = self.openstack('object list ' + self.CONTAINER_NAME) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_object_save(self): + self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.openstack('object save ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + # TODO(stevemar): Assert returned fields + + def test_object_save_with_filename(self): + self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.openstack('object save ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME + ' --file tmp.txt') + # TODO(stevemar): Assert returned fields + + def test_object_show(self): + self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.openstack('object show ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + # TODO(stevemar): Assert returned fields diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index f938b55ed5..57db906398 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -252,6 +252,7 @@ def object_list( if container is None or object is None: return None + params['format'] = 'json' if all_data: data = listing = self.object_list( container=container, @@ -280,7 +281,6 @@ def object_list( data.extend(listing) return data - params = {} if limit: params['limit'] = limit if marker: @@ -320,7 +320,8 @@ def object_save( ) if response.status_code == 200: if not os.path.exists(os.path.dirname(file)): - os.makedirs(os.path.dirname(file)) + if len(os.path.dirname(file)) > 0: + os.makedirs(os.path.dirname(file)) with open(file, 'wb') as f: for chunk in response.iter_content(): f.write(chunk) diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 887aa85b8e..1ac905c33e 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -33,12 +33,6 @@ def make_client(instance): """Returns an object-store API client.""" - object_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS) - LOG.debug('Instantiating object client: %s', object_client) - if instance._url: endpoint = instance._url else: From 30b0a41ce70d39c5f650b6a2889b9e46a0930dd3 Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Thu, 10 Apr 2014 18:43:51 +0200 Subject: [PATCH 0020/3303] Implement CRUD operations for Mapping objects Change-Id: I4b8f2e77e741cf74f50aba98ab975af7321b02c6 Implements: bp/add-openstackclient-federation-crud --- openstackclient/identity/v3/mapping.py | 209 +++++++++++++++ openstackclient/tests/identity/v3/fakes.py | 61 +++++ .../tests/identity/v3/test_mappings.py | 239 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 515 insertions(+) create mode 100644 openstackclient/identity/v3/mapping.py create mode 100644 openstackclient/tests/identity/v3/test_mappings.py diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py new file mode 100644 index 0000000000..ae5e03bd2d --- /dev/null +++ b/openstackclient/identity/v3/mapping.py @@ -0,0 +1,209 @@ +# Copyright 2014 CERN +# +# 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. +# + +"""Identity v3 federation mapping action implementations""" + +import json +import logging + +from cliff import command +from cliff import lister +from cliff import show +import six + +from openstackclient.common import exceptions +from openstackclient.common import utils + + +class _RulesReader(object): + """Helper class capable of reading rules from files""" + + def _read_rules(self, path): + """Read and parse rules from path + + Expect the file to contain a valid JSON structure. + + :param path: path to the file + :return: loaded and valid dictionary with rules + :raises exception.CommandError: In case the file cannot be + accessed or the content is not a valid JSON. + + Example of the content of the file: + [ + { + "local": [ + { + "group": { + "id": "85a868" + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Employee" + ] + }, + { + "type": "sn", + "any_one_of": [ + "Young" + ] + } + ] + } + ] + + """ + blob = utils.read_blob_file_contents(path) + try: + rules = json.loads(blob) + except ValueError as e: + raise exceptions.CommandError( + 'An error occurred when reading ' + 'rules from file %s: %s' % (path, e)) + else: + return rules + + +class CreateMapping(show.ShowOne, _RulesReader): + """Create new federation mapping""" + + log = logging.getLogger(__name__ + '.CreateMapping') + + def get_parser(self, prog_name): + parser = super(CreateMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='New mapping (must be unique)', + ) + parser.add_argument( + '--rules', + metavar='', required=True, + help='Filename with rules', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + rules = self._read_rules(parsed_args.rules) + mapping = identity_client.federation.mappings.create( + mapping_id=parsed_args.mapping, + rules=rules) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteMapping(command.Command): + """Delete federation mapping""" + + log = logging.getLogger(__name__ + '.DeleteMapping') + + def get_parser(self, prog_name): + parser = super(DeleteMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to delete', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + identity_client.federation.mappings.delete(parsed_args.mapping) + return + + +class ListMapping(lister.Lister): + """List federation mappings""" + log = logging.getLogger(__name__ + '.ListMapping') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + # NOTE(marek-denis): Since rules can be long and tedious I have decided + # to only list ids of the mappings. If somebody wants to check the + # rules, (s)he should show specific ones. + identity_client = self.app.client_manager.identity + data = identity_client.federation.mappings.list() + columns = ('ID',) + items = [utils.get_item_properties(s, columns) for s in data] + return (columns, items) + + +class SetMapping(show.ShowOne, _RulesReader): + """Update federation mapping""" + + log = logging.getLogger(__name__ + '.SetMapping') + + def get_parser(self, prog_name): + parser = super(SetMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to update.', + ) + parser.add_argument( + '--rules', + metavar='', required=True, + help='Filename with rules', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + rules = self._read_rules(parsed_args.rules) + + mapping = identity_client.federation.mappings.update( + mapping=parsed_args.mapping, + rules=rules) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) + + +class ShowMapping(show.ShowOne): + """Show federation mapping details""" + + log = logging.getLogger(__name__ + '.ShowMapping') + + def get_parser(self, prog_name): + parser = super(ShowMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to show', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + mapping = identity_client.federation.mappings.get(parsed_args.mapping) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index e9cda9ffbc..a88a905e6f 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -38,6 +38,65 @@ 'name': group_name, } +mapping_id = 'test_mapping' +mapping_rules_file_path = '/tmp/path/to/file' +# Copied from +# (https://github.com/openstack/keystone/blob\ +# master/keystone/tests/mapping_fixtures.py +EMPLOYEE_GROUP_ID = "0cd5e9" +DEVELOPER_GROUP_ID = "xyz" +MAPPING_RULES = [ + { + "local": [ + { + "group": { + "id": EMPLOYEE_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "Guest" + ] + } + ] + } +] + +MAPPING_RULES_2 = [ + { + "local": [ + { + "group": { + "id": DEVELOPER_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Contractor" + ] + } + ] + } +] + + +MAPPING_RESPONSE = { + "id": mapping_id, + "rules": MAPPING_RULES +} + +MAPPING_RESPONSE_2 = { + "id": mapping_id, + "rules": MAPPING_RULES_2 +} + project_id = '8-9-64' project_name = 'beatles' project_description = 'Fab Four' @@ -224,6 +283,8 @@ class FakeFederationManager(object): def __init__(self, **kwargs): self.identity_providers = mock.Mock() self.identity_providers.resource_class = fakes.FakeResource(None, {}) + self.mappings = mock.Mock() + self.mappings.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_mappings.py b/openstackclient/tests/identity/v3/test_mappings.py new file mode 100644 index 0000000000..f5c318fefd --- /dev/null +++ b/openstackclient/tests/identity/v3/test_mappings.py @@ -0,0 +1,239 @@ +# Copyright 2014 CERN. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import mock + +from openstackclient.common import exceptions +from openstackclient.identity.v3 import mapping +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestMapping(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestMapping, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.mapping_mock = federation_lib.mappings + self.mapping_mock.reset_mock() + + +class TestMappingCreate(TestMapping): + def setUp(self): + super(TestMappingCreate, self).setUp() + self.mapping_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + self.cmd = mapping.CreateMapping(self.app, None) + + def test_create_mapping(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.MAPPING_RULES + with mock.patch("openstackclient.identity.v3.mapping." + "CreateMapping._read_rules", mocker): + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.create.assert_called_with( + mapping_id=identity_fakes.mapping_id, + rules=identity_fakes.MAPPING_RULES) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) + + +class TestMappingDelete(TestMapping): + def setUp(self): + super(TestMappingDelete, self).setUp() + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True) + + self.mapping_mock.delete.return_value = None + self.cmd = mapping.DeleteMapping(self.app, None) + + def test_delete_mapping(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.mapping_mock.delete.assert_called_with( + identity_fakes.mapping_id) + + +class TestMappingList(TestMapping): + def setUp(self): + super(TestMappingList, self).setUp() + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + {'id': identity_fakes.mapping_id}, + loaded=True) + # Pretend list command returns list of two mappings. + # NOTE(marek-denis): We are returning FakeResources with mapping id + # only as ShowMapping class is implemented in a way where rules will + # not be displayed, only mapping ids. + self.mapping_mock.list.return_value = [ + fakes.FakeResource( + None, + {'id': identity_fakes.mapping_id}, + loaded=True, + ), + fakes.FakeResource( + None, + {'id': 'extra_mapping'}, + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = mapping.ListMapping(self.app, None) + + def test_mapping_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.list.assert_called_with() + + collist = ('ID',) + self.assertEqual(columns, collist) + + datalist = [(identity_fakes.mapping_id,), ('extra_mapping',)] + self.assertEqual(datalist, data) + + +class TestMappingShow(TestMapping): + def setUp(self): + super(TestMappingShow, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.cmd = mapping.ShowMapping(self.app, None) + + def test_mapping_show(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.get.assert_called_with( + identity_fakes.mapping_id) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) + + +class TestMappingSet(TestMapping): + + def setUp(self): + super(TestMappingSet, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.mapping_mock.update.return_value = fakes.FakeResource( + None, + identity_fakes.MAPPING_RESPONSE_2, + loaded=True + ) + + # Get the command object to test + self.cmd = mapping.SetMapping(self.app, None) + + def test_set_new_rules(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.MAPPING_RULES_2 + with mock.patch("openstackclient.identity.v3.mapping." + "SetMapping._read_rules", mocker): + columns, data = self.cmd.take_action(parsed_args) + self.mapping_mock.update.assert_called_with( + mapping=identity_fakes.mapping_id, + rules=identity_fakes.MAPPING_RULES_2) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES_2) + self.assertEqual(datalist, data) + + def test_set_rules_wrong_file_path(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 3178fe4467..d601fdfa22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -212,6 +212,12 @@ openstack.identity.v3 = identity_provider_set = openstackclient.identity.v3.identity_provider:SetIdentityProvider identity_provider_show = openstackclient.identity.v3.identity_provider:ShowIdentityProvider + mapping_create = openstackclient.identity.v3.mapping:CreateMapping + mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping + mapping_list = openstackclient.identity.v3.mapping:ListMapping + mapping_set = openstackclient.identity.v3.mapping:SetMapping + mapping_show = openstackclient.identity.v3.mapping:ShowMapping + policy_create = openstackclient.identity.v3.policy:CreatePolicy policy_delete = openstackclient.identity.v3.policy:DeletePolicy policy_list = openstackclient.identity.v3.policy:ListPolicy From 111d43ad8fcdd744b48b91475972366cf295f22e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 9 Jul 2014 13:49:10 -0400 Subject: [PATCH 0021/3303] Update compute server messages for translation Mark some of the messages from the server for translation implements bp use_i18n Change-Id: I503efcfb4ca3dec1c427b58ee4a85de9a241dacd --- openstackclient/compute/v2/server.py | 239 +++++++++++++-------------- 1 file changed, 118 insertions(+), 121 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ec7f212ddb..355774c316 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -25,11 +25,12 @@ from cliff import command from cliff import lister from cliff import show - from novaclient.v1_1 import servers + from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa def _format_servers_list_networks(networks): @@ -106,17 +107,17 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( 'volume', metavar='', - help='Volume to add (name or ID)', + help=_('Volume to add (name or ID)'), ) parser.add_argument( '--device', metavar='', - help='Server internal device name for volume', + help=_('Server internal device name for volume'), ) return parser @@ -152,12 +153,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to use', + help=_('Name or ID of server to use'), ) parser.add_argument( 'group', metavar='', - help='Name or ID of security group to add to server', + help=_('Name or ID of security group to add to server'), ) return parser @@ -189,91 +190,91 @@ def get_parser(self, prog_name): parser.add_argument( 'server_name', metavar='', - help='New server name') + help=_('New server name')) parser.add_argument( '--image', metavar='', required=True, - help='Create server from this image') + help=_('Create server from this image')) parser.add_argument( '--flavor', metavar='', required=True, - help='Create server with this flavor') + help=_('Create server with this flavor')) parser.add_argument( '--security-group', metavar='', action='append', default=[], - help='Security group to assign to this server ' - '(repeat for multiple groups)') + help=_('Security group to assign to this server ' + '(repeat for multiple groups)')) parser.add_argument( '--key-name', metavar='', - help='Keypair to inject into this server (optional extension)') + help=_('Keypair to inject into this server (optional extension)')) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Set a property on this server ' - '(repeat for multiple values)') + help=_('Set a property on this server ' + '(repeat for multiple values)')) parser.add_argument( '--file', metavar='', action='append', default=[], - help='File to inject into image before boot ' - '(repeat for multiple files)') + help=_('File to inject into image before boot ' + '(repeat for multiple files)')) parser.add_argument( '--user-data', metavar='', - help='User data file to serve from the metadata server') + help=_('User data file to serve from the metadata server')) parser.add_argument( '--availability-zone', metavar='', - help='Select an availability zone for the server') + help=_('Select an availability zone for the server')) parser.add_argument( '--block-device-mapping', metavar='', action='append', default=[], - help='Map block devices; map is ' - '::: ' - '(optional extension)') + help=_('Map block devices; map is ' + '::: ' + '(optional extension)')) parser.add_argument( '--nic', metavar='', action='append', default=[], - help='Specify NIC configuration (optional extension)') + help=_('Specify NIC configuration (optional extension)')) parser.add_argument( '--hint', metavar='', action='append', default=[], - help='Hints for the scheduler (optional extension)') + help=_('Hints for the scheduler (optional extension)')) parser.add_argument( '--config-drive', metavar='|True', default=False, - help='Use specified volume as the config drive, ' - 'or \'True\' to use an ephemeral drive') + help=_('Use specified volume as the config drive, ' + 'or \'True\' to use an ephemeral drive')) parser.add_argument( '--min', metavar='', type=int, default=1, - help='Minimum number of servers to launch (default=1)') + help=_('Minimum number of servers to launch (default=1)')) parser.add_argument( '--max', metavar='', type=int, default=1, - help='Maximum number of servers to launch (default=1)') + help=_('Maximum number of servers to launch (default=1)')) parser.add_argument( '--wait', action='store_true', - help='Wait for build to complete', + help=_('Wait for build to complete'), ) return parser @@ -300,13 +301,13 @@ def take_action(self, parsed_args): raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) if parsed_args.min > parsed_args.max: - msg = "min instances should be <= max instances" + msg = _("min instances should be <= max instances") raise exceptions.CommandError(msg) if parsed_args.min < 1: - msg = "min instances should be > 0" + msg = _("min instances should be > 0") raise exceptions.CommandError(msg) if parsed_args.max < 1: - msg = "max instances should be > 0" + msg = _("max instances should be > 0") raise exceptions.CommandError(msg) userdata = None @@ -315,8 +316,7 @@ def take_action(self, parsed_args): userdata = open(parsed_args.user_data) except IOError as e: msg = "Can't open '%s': %s" - raise exceptions.CommandError(msg % - (parsed_args.user_data, e)) + raise exceptions.CommandError(msg % (parsed_args.user_data, e)) block_device_mapping = dict(v.split('=', 1) for v in parsed_args.block_device_mapping) @@ -378,9 +378,9 @@ def take_action(self, parsed_args): ): sys.stdout.write('\n') else: - self.log.error('Error creating server: %s', + self.log.error(_('Error creating server: %s'), parsed_args.server_name) - sys.stdout.write('\nError creating server') + sys.stdout.write(_('\nError creating server')) raise SystemExit details = _prep_server_detail(compute_client, server) @@ -397,17 +397,17 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='Name of new image (default is server name)', + help=_('Name of new image (default is server name)'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for image create to complete', + help=_('Wait for image create to complete'), ) return parser @@ -437,11 +437,9 @@ def take_action(self, parsed_args): ): sys.stdout.write('\n') else: - self.log.error( - 'Error creating server snapshot: %s', - parsed_args.image_name, - ) - sys.stdout.write('\nError creating server snapshot') + self.log.error(_('Error creating server snapshot: %s'), + parsed_args.image_name) + sys.stdout.write(_('\nError creating server snapshot')) raise SystemExit image = utils.find_resource( @@ -449,9 +447,7 @@ def take_action(self, parsed_args): image_id, ) - info = {} - info.update(image._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(image._info))) class DeleteServer(command.Command): @@ -464,7 +460,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to delete') + help=_('Name or ID of server to delete')) return parser def take_action(self, parsed_args): @@ -486,50 +482,50 @@ def get_parser(self, prog_name): parser.add_argument( '--reservation-id', metavar='', - help='Only return instances that match the reservation') + help=_('Only return instances that match the reservation')) parser.add_argument( '--ip', metavar='', - help='Regular expression to match IP addresses') + help=_('Regular expression to match IP addresses')) parser.add_argument( '--ip6', metavar='', - help='Regular expression to match IPv6 addresses') + help=_('Regular expression to match IPv6 addresses')) parser.add_argument( '--name', metavar='', - help='Regular expression to match names') + help=_('Regular expression to match names')) parser.add_argument( '--status', metavar='', # FIXME(dhellmann): Add choices? - help='Search by server status') + help=_('Search by server status')) parser.add_argument( '--flavor', metavar='', - help='Search by flavor ID') + help=_('Search by flavor ID')) parser.add_argument( '--image', metavar='', - help='Search by image ID') + help=_('Search by image ID')) parser.add_argument( '--host', metavar='', - help='Search by hostname') + help=_('Search by hostname')) parser.add_argument( '--instance-name', metavar='', - help='Regular expression to match instance name (admin only)') + help=_('Regular expression to match instance name (admin only)')) parser.add_argument( '--all-projects', action='store_true', default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help='Include all projects (admin only)') + help=_('Include all projects (admin only)')) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -598,7 +594,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -632,17 +628,17 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server to migrate (name or ID)', + help=_('Server to migrate (name or ID)'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for resize to complete', + help=_('Wait for resize to complete'), ) parser.add_argument( '--live', metavar='', - help='Target hostname', + help=_('Target hostname'), ) migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( @@ -650,13 +646,13 @@ def get_parser(self, prog_name): dest='shared_migration', action='store_true', default=True, - help='Perform a shared live migration (default)', + help=_('Perform a shared live migration (default)'), ) migration_group.add_argument( '--block-migration', dest='shared_migration', action='store_false', - help='Perform a block live migration', + help=_('Perform a block live migration'), ) disk_group = parser.add_mutually_exclusive_group() disk_group.add_argument( @@ -664,13 +660,14 @@ def get_parser(self, prog_name): dest='disk_overcommit', action='store_false', default=False, - help='Do not over-commit disk on the destination host (default)', + help=_('Do not over-commit disk on the' + ' destination host (default)'), ) disk_group.add_argument( '--disk-overcommit', action='store_true', default=False, - help='Allow disk over-commit on the destination host', + help=_('Allow disk over-commit on the destination host'), ) return parser @@ -698,9 +695,9 @@ def take_action(self, parsed_args): server.id, callback=_show_progress, ): - sys.stdout.write('Complete\n') + sys.stdout.write(_('Complete\n')) else: - sys.stdout.write('\nError migrating server') + sys.stdout.write(_('\nError migrating server')) raise SystemExit @@ -714,7 +711,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -738,7 +735,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) group = parser.add_mutually_exclusive_group() group.add_argument( @@ -747,7 +744,7 @@ def get_parser(self, prog_name): action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, - help='Perform a hard reboot', + help=_('Perform a hard reboot'), ) group.add_argument( '--soft', @@ -755,12 +752,12 @@ def get_parser(self, prog_name): action='store_const', const=servers.REBOOT_SOFT, default=servers.REBOOT_SOFT, - help='Perform a soft reboot', + help=_('Perform a soft reboot'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for reboot to complete', + help=_('Wait for reboot to complete'), ) return parser @@ -777,9 +774,9 @@ def take_action(self, parsed_args): server.id, callback=_show_progress, ): - sys.stdout.write('\nReboot complete\n') + sys.stdout.write(_('\nReboot complete\n')) else: - sys.stdout.write('\nError rebooting server\n') + sys.stdout.write(_('\nError rebooting server\n')) raise SystemExit @@ -793,13 +790,13 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--image', metavar='', required=True, - help='Recreate server from this image', + help=_('Recreate server from this image'), ) parser.add_argument( '--password', @@ -809,7 +806,7 @@ def get_parser(self, prog_name): parser.add_argument( '--wait', action='store_true', - help='Wait for rebuild to complete', + help=_('Wait for rebuild to complete'), ) return parser @@ -830,9 +827,9 @@ def take_action(self, parsed_args): server.id, callback=_show_progress, ): - sys.stdout.write('\nComplete\n') + sys.stdout.write(_('\nComplete\n')) else: - sys.stdout.write('\nError rebuilding server') + sys.stdout.write(_('\nError rebuilding server')) raise SystemExit details = _prep_server_detail(compute_client, server) @@ -849,12 +846,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to use', + help=_('Name or ID of server to use'), ) parser.add_argument( 'group', metavar='', - help='Name or ID of security group to remove from server', + help=_('Name or ID of security group to remove from server'), ) return parser @@ -885,12 +882,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( 'volume', metavar='', - help='Volume to remove (name or ID)', + help=_('Volume to remove (name or ID)'), ) return parser @@ -925,7 +922,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -951,27 +948,27 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) phase_group.add_argument( '--flavor', metavar='', - help='Resize server to specified flavor', + help=_('Resize server to specified flavor'), ) phase_group.add_argument( '--verify', action="store_true", - help='Verify server resize is complete', + help=_('Verify server resize is complete'), ) phase_group.add_argument( '--revert', action="store_true", - help='Restore server state before resize', + help=_('Restore server state before resize'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for resize to complete', + help=_('Wait for resize to complete'), ) return parser @@ -996,9 +993,9 @@ def take_action(self, parsed_args): success_status=['active', 'verify_resize'], callback=_show_progress, ): - sys.stdout.write('Complete\n') + sys.stdout.write(_('Complete\n')) else: - sys.stdout.write('\nError resizing server') + sys.stdout.write(_('\nError resizing server')) raise SystemExit elif parsed_args.verify: compute_client.servers.confirm_resize(server) @@ -1016,7 +1013,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1040,24 +1037,24 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='New server name', + help=_('New server name'), ) parser.add_argument( '--root-password', action="store_true", - help='Set new root password (interactive only)', + help=_('Set new root password (interactive only)'), ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, - help='Property to add/change for this server ' - '(repeat option to set multiple properties)', + help=_('Property to add/change for this server ' + '(repeat option to set multiple properties)'), ) return parser @@ -1080,12 +1077,12 @@ def take_action(self, parsed_args): ) if parsed_args.root_password: - p1 = getpass.getpass('New password: ') - p2 = getpass.getpass('Retype new password: ') + p1 = getpass.getpass(_('New password: ')) + p2 = getpass.getpass(_('Retype new password: ')) if p1 == p2: server.change_password(p1) else: - msg = "Passwords do not match, password unchanged" + msg = _("Passwords do not match, password unchanged") raise exceptions.CommandError(msg) @@ -1099,13 +1096,13 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server to show (name or ID)', + help=_('Server to show (name or ID)'), ) parser.add_argument( '--diagnostics', action='store_true', default=False, - help='Display diagnostics information for a given server', + help=_('Display diagnostics information for a given server'), ) return parser @@ -1118,7 +1115,7 @@ def take_action(self, parsed_args): if parsed_args.diagnostics: (resp, data) = server.diagnostics() if not resp.status_code == 200: - sys.stderr.write("Error retrieving diagnostics data") + sys.stderr.write(_("Error retrieving diagnostics data")) return ({}, {}) else: data = _prep_server_detail(compute_client, server) @@ -1136,12 +1133,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--login', metavar='', - help='Login name (ssh -l option)', + help=_('Login name (ssh -l option)'), ) parser.add_argument( '-l', @@ -1152,7 +1149,7 @@ def get_parser(self, prog_name): '--port', metavar='', type=int, - help='Destination port (ssh -p option)', + help=_('Destination port (ssh -p option)'), ) parser.add_argument( '-p', @@ -1164,7 +1161,7 @@ def get_parser(self, prog_name): parser.add_argument( '--identity', metavar='', - help='Private key file (ssh -i option)', + help=_('Private key file (ssh -i option)'), ) parser.add_argument( '-i', @@ -1175,7 +1172,7 @@ def get_parser(self, prog_name): parser.add_argument( '--option', metavar='', - help='Options in ssh_config(5) format (ssh -o option)', + help=_('Options in ssh_config(5) format (ssh -o option)'), ) parser.add_argument( '-o', @@ -1189,14 +1186,14 @@ def get_parser(self, prog_name): dest='ipv4', action='store_true', default=False, - help='Use only IPv4 addresses', + help=_('Use only IPv4 addresses'), ) ip_group.add_argument( '-6', dest='ipv6', action='store_true', default=False, - help='Use only IPv6 addresses', + help=_('Use only IPv6 addresses'), ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( @@ -1205,7 +1202,7 @@ def get_parser(self, prog_name): action='store_const', const='public', default='public', - help='Use public IP address', + help=_('Use public IP address'), ) type_group.add_argument( '--private', @@ -1213,14 +1210,14 @@ def get_parser(self, prog_name): action='store_const', const='private', default='public', - help='Use private IP address', + help=_('Use private IP address'), ) type_group.add_argument( '--address-type', metavar='', dest='address_type', default='public', - help='Use other IP address (public, private, etc)', + help=_('Use other IP address (public, private, etc)'), ) parser.add_argument( '-v', @@ -1294,7 +1291,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1318,7 +1315,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1342,7 +1339,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1366,7 +1363,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1390,15 +1387,15 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from server ' - '(repeat to set multiple values)', + help=_('Property key to remove from server ' + '(repeat to set multiple values)'), ) return parser From 5b6c24fdb0154bbbf41f0b05211001d783b69635 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 26 Jun 2014 17:23:59 -0500 Subject: [PATCH 0022/3303] Update for cliff commandmanager >=1.6.1 Cliff 1.6.1 added CommandManager.load_commands() so we can adopt it rather than rolling our own. Also, that second group is Greek, not Latin. Jeez... Change-Id: I4a63c22f37bcfd0ef5d83c2dbd08b58fda0db35c --- openstackclient/common/commandmanager.py | 18 ++++-------------- .../tests/common/test_commandmanager.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 08710c7735..9901ea2089 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -16,7 +16,6 @@ """Modify cliff.CommandManager""" import logging -import pkg_resources import cliff.commandmanager @@ -35,23 +34,14 @@ def __init__(self, namespace, convert_underscores=True): self.group_list = [] super(CommandManager, self).__init__(namespace, convert_underscores) - def _load_commands(self, group=None): - if not group: - group = self.namespace - self.group_list.append(group) - for ep in pkg_resources.iter_entry_points(group): - cmd_name = ( - ep.name.replace('_', ' ') - if self.convert_underscores - else ep.name - ) - self.commands[cmd_name] = ep - return + def load_commands(self, namespace): + self.group_list.append(namespace) + return super(CommandManager, self).load_commands(namespace) def add_command_group(self, group=None): """Adds another group of command entrypoints""" if group: - self._load_commands(group) + self.load_commands(group) def get_command_groups(self): """Returns a list of the loaded command groups""" diff --git a/openstackclient/tests/common/test_commandmanager.py b/openstackclient/tests/common/test_commandmanager.py index 088ea21ea8..ca9ee9a70e 100644 --- a/openstackclient/tests/common/test_commandmanager.py +++ b/openstackclient/tests/common/test_commandmanager.py @@ -36,15 +36,15 @@ def __init__(self): class FakeCommandManager(commandmanager.CommandManager): commands = {} - def _load_commands(self, group=None): - if not group: + def load_commands(self, namespace): + if namespace == 'test': self.commands['one'] = FAKE_CMD_ONE self.commands['two'] = FAKE_CMD_TWO - self.group_list.append(self.namespace) - else: + self.group_list.append(namespace) + elif namespace == 'greek': self.commands['alpha'] = FAKE_CMD_ALPHA self.commands['beta'] = FAKE_CMD_BETA - self.group_list.append(group) + self.group_list.append(namespace) class TestCommandManager(utils.TestCase): @@ -62,7 +62,7 @@ def test_add_command_group(self): self.assertEqual(cmd_one, FAKE_CMD_ONE) # Load another command group - mgr.add_command_group('latin') + mgr.add_command_group('greek') # Find a new command cmd_alpha, name, args = mgr.find_command(['alpha']) @@ -82,7 +82,7 @@ def test_get_command_groups(self): self.assertEqual(cmd_mock, mock_cmd_one) # Load another command group - mgr.add_command_group('latin') + mgr.add_command_group('greek') gl = mgr.get_command_groups() - self.assertEqual(['test', 'latin'], gl) + self.assertEqual(['test', 'greek'], gl) From 14c61a0ace85a7b47403d4fba6c50320f717d37b Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Thu, 2 Oct 2014 09:36:13 +0200 Subject: [PATCH 0023/3303] CRUD operations for federated protocols Openstackclient needs to have a capability to manage federated protocols (like saml2, openid connect, abfab). This patch allows users to administrate such operations from the commandline. Change-Id: I59eef2acdda60c7ec795d1bfe31e8e960b4478a1 Implements: bp/add-openstackclient-federation-crud --- .../identity/v3/federation_protocol.py | 182 +++++++++++++++++ openstackclient/tests/identity/v3/fakes.py | 24 +++ .../tests/identity/v3/test_protocol.py | 185 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 397 insertions(+) create mode 100644 openstackclient/identity/v3/federation_protocol.py create mode 100644 openstackclient/tests/identity/v3/test_protocol.py diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py new file mode 100644 index 0000000000..adc4a28b14 --- /dev/null +++ b/openstackclient/identity/v3/federation_protocol.py @@ -0,0 +1,182 @@ +# 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. +# + + +"""Identity v3 Protocols actions implementations""" + +import logging +import six + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import utils + + +class CreateProtocol(show.ShowOne): + """Create new Federation Protocol tied to an Identity Provider""" + + log = logging.getLogger(__name__ + 'CreateProtocol') + + def get_parser(self, prog_name): + parser = super(CreateProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', + help=('Identity Provider you want to add the Protocol to ' + '(must already exist)'), required=True) + parser.add_argument( + '--mapping', + metavar='', required=True, + help='Mapping you want to be used (must already exist)') + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + protocol = identity_client.federation.protocols.create( + protocol_id=parsed_args.federation_protocol, + identity_provider=parsed_args.identity_provider, + mapping=parsed_args.mapping) + info = dict(protocol._info) + # NOTE(marek-denis): Identity provider is not included in a response + # from Keystone, however it should be listed to the user. Add it + # manually to the output list, simply reusing value provided by the + # user. + info['identity_provider'] = parsed_args.identity_provider + info['mapping'] = info.pop('mapping_id') + return zip(*sorted(six.iteritems(info))) + + +class DeleteProtocol(command.Command): + """Delete Federation Protocol tied to a Identity Provider""" + + log = logging.getLogger(__name__ + '.DeleteProtocol') + + def get_parser(self, prog_name): + parser = super(DeleteProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help='Identity Provider the Protocol is tied to') + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + identity_client.federation.protocols.delete( + parsed_args.identity_provider, parsed_args.federation_protocol) + return + + +class ListProtocols(lister.Lister): + """List Protocols tied to an Identity Provider""" + + log = logging.getLogger(__name__ + '.ListProtocols') + + def get_parser(self, prog_name): + parser = super(ListProtocols, self).get_parser(prog_name) + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help='Identity Provider the Protocol is tied to') + + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + protocols = identity_client.federation.protocols.list( + parsed_args.identity_provider) + columns = ('id', 'mapping') + response_attributes = ('id', 'mapping_id') + items = [utils.get_item_properties(s, response_attributes) + for s in protocols] + return (columns, items) + + +class SetProtocol(command.Command): + """Set Protocol tied to an Identity Provider""" + + log = logging.getLogger(__name__ + '.SetProtocol') + + def get_parser(self, prog_name): + parser = super(SetProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help=('Identity Provider you want to add the Protocol to ' + '(must already exist)')) + parser.add_argument( + '--mapping', + metavar='', required=True, + help='Mapping you want to be used (must already exist)') + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + protocol = identity_client.federation.protocols.update( + parsed_args.identity_provider, parsed_args.federation_protocol, + parsed_args.mapping) + info = dict(protocol._info) + # NOTE(marek-denis): Identity provider is not included in a response + # from Keystone, however it should be listed to the user. Add it + # manually to the output list, simply reusing value provided by the + # user. + info['identity_provider'] = parsed_args.identity_provider + info['mapping'] = info.pop('mapping_id') + return zip(*sorted(six.iteritems(info))) + + +class ShowProtocol(show.ShowOne): + """Show Protocol tied to an Identity Provider""" + + log = logging.getLogger(__name__ + '.ShowProtocol') + + def get_parser(self, prog_name): + parser = super(ShowProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help=('Identity Provider you want to add the Protocol to ' + '(must already exist)')) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + protocol = identity_client.federation.protocols.get( + parsed_args.identity_provider, parsed_args.federation_protocol) + info = dict(protocol._info) + info['mapping'] = info.pop('mapping_id') + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index a88a905e6f..b0df16f043 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -190,6 +190,28 @@ 'description': idp_description } +protocol_id = 'protocol' + +mapping_id = 'test_mapping' +mapping_id_updated = 'prod_mapping' + +PROTOCOL_ID_MAPPING = { + 'id': protocol_id, + 'mapping': mapping_id +} + +PROTOCOL_OUTPUT = { + 'id': protocol_id, + 'mapping_id': mapping_id, + 'identity_provider': idp_id +} + +PROTOCOL_OUTPUT_UPDATED = { + 'id': protocol_id, + 'mapping_id': mapping_id_updated, + 'identity_provider': idp_id +} + # Assignments ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID = { @@ -285,6 +307,8 @@ def __init__(self, **kwargs): self.identity_providers.resource_class = fakes.FakeResource(None, {}) self.mappings = mock.Mock() self.mappings.resource_class = fakes.FakeResource(None, {}) + self.protocols = mock.Mock() + self.protocols.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_protocol.py b/openstackclient/tests/identity/v3/test_protocol.py new file mode 100644 index 0000000000..3c9c3f0a7d --- /dev/null +++ b/openstackclient/tests/identity/v3/test_protocol.py @@ -0,0 +1,185 @@ +# Copyright 2014 CERN. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from openstackclient.identity.v3 import federation_protocol +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestProtocol(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestProtocol, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.protocols_mock = federation_lib.protocols + self.protocols_mock.reset_mock() + + +class TestProtocolCreate(TestProtocol): + + def setUp(self): + super(TestProtocolCreate, self).setUp() + + proto = copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT) + resource = fakes.FakeResource(None, proto, loaded=True) + self.protocols_mock.create.return_value = resource + self.cmd = federation_protocol.CreateProtocol(self.app, None) + + def test_create_protocol(self): + argslist = [ + identity_fakes.protocol_id, + '--identity-provider', identity_fakes.idp_id, + '--mapping', identity_fakes.mapping_id + ] + + verifylist = [ + ('federation_protocol', identity_fakes.protocol_id), + ('identity_provider', identity_fakes.idp_id), + ('mapping', identity_fakes.mapping_id) + ] + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.protocols_mock.create.assert_called_with( + protocol_id=identity_fakes.protocol_id, + identity_provider=identity_fakes.idp_id, + mapping=identity_fakes.mapping_id) + + collist = ('id', 'identity_provider', 'mapping') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.protocol_id, + identity_fakes.idp_id, + identity_fakes.mapping_id) + self.assertEqual(datalist, data) + + +class TestProtocolDelete(TestProtocol): + + def setUp(self): + super(TestProtocolDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.protocols_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT), + loaded=True, + ) + + self.protocols_mock.delete.return_value = None + self.cmd = federation_protocol.DeleteProtocol(self.app, None) + + def test_delete_identity_provider(self): + arglist = [ + '--identity-provider', identity_fakes.idp_id, + identity_fakes.protocol_id + ] + verifylist = [ + ('federation_protocol', identity_fakes.protocol_id), + ('identity_provider', identity_fakes.idp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.protocols_mock.delete.assert_called_with( + identity_fakes.idp_id, identity_fakes.protocol_id) + + +class TestProtocolList(TestProtocol): + + def setUp(self): + super(TestProtocolList, self).setUp() + + self.protocols_mock.get.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True) + + self.protocols_mock.list.return_value = [fakes.FakeResource( + None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True)] + + self.cmd = federation_protocol.ListProtocols(self.app, None) + + def test_list_protocols(self): + arglist = ['--identity-provider', identity_fakes.idp_id] + verifylist = [('identity_provider', identity_fakes.idp_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.protocols_mock.list.assert_called_with(identity_fakes.idp_id) + + +class TestProtocolSet(TestProtocol): + + def setUp(self): + super(TestProtocolSet, self).setUp() + self.protocols_mock.get.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_OUTPUT, loaded=True) + self.protocols_mock.update.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_OUTPUT_UPDATED, loaded=True) + + self.cmd = federation_protocol.SetProtocol(self.app, None) + + def test_set_new_mapping(self): + arglist = [ + identity_fakes.protocol_id, + '--identity-provider', identity_fakes.idp_id, + '--mapping', identity_fakes.mapping_id + ] + verifylist = [('identity_provider', identity_fakes.idp_id), + ('federation_protocol', identity_fakes.protocol_id), + ('mapping', identity_fakes.mapping_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.protocols_mock.update.assert_called_with( + identity_fakes.idp_id, identity_fakes.protocol_id, + identity_fakes.mapping_id) + + collist = ('id', 'identity_provider', 'mapping') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.protocol_id, identity_fakes.idp_id, + identity_fakes.mapping_id_updated) + self.assertEqual(datalist, data) + + +class TestProtocolShow(TestProtocol): + + def setUp(self): + super(TestProtocolShow, self).setUp() + self.protocols_mock.get.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_OUTPUT, loaded=False) + + self.cmd = federation_protocol.ShowProtocol(self.app, None) + + def test_show_protocol(self): + arglist = [identity_fakes.protocol_id, '--identity-provider', + identity_fakes.idp_id] + verifylist = [('federation_protocol', identity_fakes.protocol_id), + ('identity_provider', identity_fakes.idp_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.protocols_mock.get.assert_called_with(identity_fakes.idp_id, + identity_fakes.protocol_id) + + collist = ('id', 'identity_provider', 'mapping') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.protocol_id, + identity_fakes.idp_id, + identity_fakes.mapping_id) + self.assertEqual(datalist, data) diff --git a/setup.cfg b/setup.cfg index d601fdfa22..dc85967f42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -230,6 +230,12 @@ openstack.identity.v3 = project_set = openstackclient.identity.v3.project:SetProject project_show = openstackclient.identity.v3.project:ShowProject + federation_protocol_create = openstackclient.identity.v3.federation_protocol:CreateProtocol + federation_protocol_delete = openstackclient.identity.v3.federation_protocol:DeleteProtocol + federation_protocol_list = openstackclient.identity.v3.federation_protocol:ListProtocols + federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol + federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken request_token_create = openstackclient.identity.v3.token:CreateRequestToken From d32185cb34495b0af4b4e646a93aedf4d7f86d25 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 27 Aug 2014 23:26:42 -0500 Subject: [PATCH 0024/3303] Add 'command list' command * Add method to CommandManager to retrieve command names by group * Add ListCommands To list command groups loaded by cliff Change-Id: I37fe2471aa2fafa8aa223159452d52b1981021d6 --- openstackclient/common/commandmanager.py | 15 +++++++++ openstackclient/common/module.py | 16 ++++++++++ .../tests/common/test_commandmanager.py | 17 ++++++++++ openstackclient/tests/common/test_module.py | 32 +++++++++++++++++++ setup.cfg | 1 + 5 files changed, 81 insertions(+) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 9901ea2089..2d9575d9d2 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -16,6 +16,7 @@ """Modify cliff.CommandManager""" import logging +import pkg_resources import cliff.commandmanager @@ -46,3 +47,17 @@ def add_command_group(self, group=None): def get_command_groups(self): """Returns a list of the loaded command groups""" return self.group_list + + def get_command_names(self, group=None): + """Returns a list of commands loaded for the specified group""" + group_list = [] + if group is not None: + for ep in pkg_resources.iter_entry_points(group): + cmd_name = ( + ep.name.replace('_', ' ') + if self.convert_underscores + else ep.name + ) + group_list.append(cmd_name) + return group_list + return self.commands.keys() diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 7f9c52db22..356cdca3b0 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -19,9 +19,25 @@ import six import sys +from cliff import lister from cliff import show +class ListCommand(lister.Lister): + """List recognized commands by group""" + + auth_required = False + log = logging.getLogger(__name__ + '.ListCommand') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + cm = self.app.command_manager + groups = cm.get_command_groups() + + columns = ('Command Group', 'Commands') + return (columns, ((c, cm.get_command_names(group=c)) for c in groups)) + + class ListModule(show.ShowOne): """List module versions""" diff --git a/openstackclient/tests/common/test_commandmanager.py b/openstackclient/tests/common/test_commandmanager.py index ca9ee9a70e..e7803a48e4 100644 --- a/openstackclient/tests/common/test_commandmanager.py +++ b/openstackclient/tests/common/test_commandmanager.py @@ -86,3 +86,20 @@ def test_get_command_groups(self): gl = mgr.get_command_groups() self.assertEqual(['test', 'greek'], gl) + + def test_get_command_names(self): + mock_cmd_one = mock.Mock() + mock_cmd_one.name = 'one' + mock_cmd_two = mock.Mock() + mock_cmd_two.name = 'cmd two' + mock_pkg_resources = mock.Mock( + return_value=[mock_cmd_one, mock_cmd_two], + ) + with mock.patch( + 'pkg_resources.iter_entry_points', + mock_pkg_resources, + ) as iter_entry_points: + mgr = commandmanager.CommandManager('test') + assert iter_entry_points.called_once_with('test') + cmds = mgr.get_command_names('test') + self.assertEqual(['one', 'cmd two'], cmds) diff --git a/openstackclient/tests/common/test_module.py b/openstackclient/tests/common/test_module.py index ce1592e41c..6918c1b419 100644 --- a/openstackclient/tests/common/test_module.py +++ b/openstackclient/tests/common/test_module.py @@ -42,6 +42,38 @@ } +class TestCommandList(utils.TestCommand): + + def setUp(self): + super(TestCommandList, self).setUp() + + self.app.command_manager = mock.Mock() + self.app.command_manager.get_command_groups.return_value = ['test'] + self.app.command_manager.get_command_names.return_value = [ + 'one', + 'cmd two', + ] + + # Get the command object to test + self.cmd = osc_module.ListCommand(self.app, None) + + def test_command_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Command Group', 'Commands') + self.assertEqual(collist, columns) + datalist = (( + 'test', + ['one', 'cmd two'], + ), ) + self.assertEqual(datalist, tuple(data)) + + @mock.patch.dict( 'openstackclient.common.module.sys.modules', values=MODULES, diff --git a/setup.cfg b/setup.cfg index 3178fe4467..267c9e330d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ console_scripts = openstack = openstackclient.shell:main openstack.cli = + command_list = openstackclient.common.module:ListCommand module_list = openstackclient.common.module:ListModule openstack.cli.base = From 0c77a9fe8baa4df9ea2d0055db9c700af3cae310 Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Fri, 18 Jul 2014 19:18:25 +0200 Subject: [PATCH 0025/3303] Support for keystone auth plugins This patch allows the user to choose which authentication plugin to use with the CLI. The arguments needed by the auth plugins are automatically added to the argument parser. Some examples with the currently available authentication plugins:: OS_USERNAME=admin OS_PROJECT_NAME=admin OS_AUTH_URL=http://keystone:5000/v2.0 \ OS_PASSWORD=admin openstack user list OS_USERNAME=admin OS_PROJECT_DOMAIN_NAME=default OS_USER_DOMAIN_NAME=default \ OS_PROJECT_NAME=admin OS_AUTH_URL=http://keystone:5000/v3 OS_PASSWORD=admin \ OS_IDENTITY_API_VERSION=3 OS_AUTH_PLUGIN=v3password openstack project list OS_TOKEN=1234 OS_URL=http://service_url:35357/v2.0 \ OS_IDENTITY_API_VERSION=2.0 openstack user list The --os-auth-plugin option can be omitted; if so the CLI will attempt to guess which plugin to use from the other options. Change-Id: I330c20ddb8d96b3a4287c68b57c36c4a0f869669 Co-Authored-By: Florent Flament --- doc/source/man/openstack.rst | 34 ++++ openstackclient/api/auth.py | 180 ++++++++++++++++++ openstackclient/common/clientmanager.py | 125 +++++------- openstackclient/identity/client.py | 21 +- openstackclient/shell.py | 164 +++++----------- .../tests/common/test_clientmanager.py | 172 ++++++++++++----- openstackclient/tests/fakes.py | 136 +++++++++++++ openstackclient/tests/test_shell.py | 86 ++++----- requirements.txt | 1 + 9 files changed, 614 insertions(+), 305 deletions(-) create mode 100644 openstackclient/api/auth.py diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index b8dcbd6b66..de2bbe92fa 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -21,6 +21,10 @@ DESCRIPTION equivalent to the CLIs provided by the OpenStack project client libraries, but with a distinct and consistent command structure. + +AUTHENTICATION METHODS +====================== + :program:`openstack` uses a similar authentication scheme as the OpenStack project CLIs, with the credential information supplied either as environment variables or as options on the command line. The primary difference is the use of 'project' in the name of the options @@ -33,6 +37,15 @@ command line. The primary difference is the use of 'project' in the name of the export OS_USERNAME= export OS_PASSWORD= # (optional) +:program:`openstack` can use different types of authentication plugins provided by the keystoneclient library. The following default plugins are available: + +* ``token``: Authentication with a token +* ``password``: Authentication with a username and a password + +Refer to the keystoneclient library documentation for more details about these plugins and their options, and for a complete list of available plugins. +Please bear in mind that some plugins might not support all of the functionalities of :program:`openstack`; for example the v3unscopedsaml plugin can deliver only unscoped tokens, some commands might not be available through this authentication method. + +Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-url` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_URL` respectively). This method takes precedence over authentication plugins. OPTIONS ======= @@ -41,9 +54,16 @@ OPTIONS :program:`openstack` recognizes the following global topions: +:option:`--os-auth-plugin` + The authentication plugin to use when connecting to the Identity service. If this option is not set, :program:`openstack` will attempt to guess the authentication method to use based on the other options. + If this option is set, its version must match :option:`--os-identity-api-version` + :option:`--os-auth-url` Authentication URL +:option:`--os-url` + Service URL, when using a service token for authentication + :option:`--os-domain-name` | :option:`--os-domain-id` Domain-level authorization scope (name or ID) @@ -59,6 +79,9 @@ OPTIONS :option:`--os-password` Authentication password +:option:`--os-token` + Authenticated token or service token + :option:`--os-user-domain-name` | :option:`--os-user-domain-id` Domain name or id containing user @@ -86,6 +109,7 @@ OPTIONS :option:`--os-XXXX-api-version` Additional API version options will be available depending on the installed API libraries. + COMMANDS ======== @@ -174,9 +198,15 @@ ENVIRONMENT VARIABLES The following environment variables can be set to alter the behaviour of :program:`openstack`. Most of them have corresponding command-line options that take precedence if set. +:envvar:`OS_AUTH_PLUGIN` + The authentication plugin to use when connecting to the Identity service, its version must match the Identity API version + :envvar:`OS_AUTH_URL` Authentication URL +:envvar:`OS_URL` + Service URL (when using the service token) + :envvar:`OS_DOMAIN_NAME` Domain-level authorization scope (name or ID) @@ -189,6 +219,9 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_USERNAME` Authentication username +:envvar:`OS_TOKEN` + Authenticated or service token + :envvar:`OS_PASSWORD` Authentication password @@ -213,6 +246,7 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_XXXX_API_VERSION` Additional API version options will be available depending on the installed API libraries. + BUGS ==== diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py new file mode 100644 index 0000000000..2bd5271f7d --- /dev/null +++ b/openstackclient/api/auth.py @@ -0,0 +1,180 @@ +# 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. +# + +"""Authentication Library""" + +import argparse +import logging + +import stevedore + +from keystoneclient.auth import base + +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + + +LOG = logging.getLogger(__name__) + + +# Initialize the list of Authentication plugins early in order +# to get the command-line options +PLUGIN_LIST = stevedore.ExtensionManager( + base.PLUGIN_NAMESPACE, + invoke_on_load=False, + propagate_map_exceptions=True, +) +# TODO(dtroyer): add some method to list the plugins for the +# --os_auth_plugin option + +# Get the command line options so the help action has them available +OPTIONS_LIST = {} +for plugin in PLUGIN_LIST: + for o in plugin.plugin.get_options(): + os_name = o.dest.lower().replace('_', '-') + os_env_name = 'OS_' + os_name.upper().replace('-', '_') + OPTIONS_LIST.setdefault(os_name, {'env': os_env_name, 'help': ''}) + # TODO(mhu) simplistic approach, would be better to only add + # help texts if they vary from one auth plugin to another + # also the text rendering is ugly in the CLI ... + OPTIONS_LIST[os_name]['help'] += 'With %s: %s\n' % ( + plugin.name, + o.help, + ) + + +def _guess_authentication_method(options): + """If no auth plugin was specified, pick one based on other options""" + + if options.os_url: + # service token authentication, do nothing + return + auth_plugin = None + if options.os_password: + if options.os_identity_api_version == '3': + auth_plugin = 'v3password' + elif options.os_identity_api_version == '2.0': + auth_plugin = 'v2password' + else: + # let keystoneclient figure it out itself + auth_plugin = 'password' + elif options.os_token: + if options.os_identity_api_version == '3': + auth_plugin = 'v3token' + elif options.os_identity_api_version == '2.0': + auth_plugin = 'v2token' + else: + # let keystoneclient figure it out itself + auth_plugin = 'token' + else: + raise exc.CommandError( + "Could not figure out which authentication method " + "to use, please set --os-auth-plugin" + ) + LOG.debug("No auth plugin selected, picking %s from other " + "options" % auth_plugin) + options.os_auth_plugin = auth_plugin + + +def build_auth_params(cmd_options): + auth_params = {} + if cmd_options.os_url: + return {'token': cmd_options.os_token} + if cmd_options.os_auth_plugin: + auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) + plugin_options = auth_plugin.get_options() + for option in plugin_options: + option_name = 'os_' + option.dest + LOG.debug('fetching option %s' % option_name) + auth_params[option.dest] = getattr(cmd_options, option_name, None) + # grab tenant from project for v2.0 API compatibility + if cmd_options.os_auth_plugin.startswith("v2"): + auth_params['tenant_id'] = getattr( + cmd_options, + 'os_project_id', + None, + ) + auth_params['tenant_name'] = getattr( + cmd_options, + 'os_project_name', + None, + ) + else: + # delay the plugin choice, grab every option + plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) + for option in plugin_options: + option_name = 'os_' + option + LOG.debug('fetching option %s' % option_name) + auth_params[option] = getattr(cmd_options, option_name, None) + return auth_params + + +def build_auth_plugins_option_parser(parser): + """Auth plugins options builder + + Builds dynamically the list of options expected by each available + authentication plugin. + + """ + available_plugins = [plugin.name for plugin in PLUGIN_LIST] + parser.add_argument( + '--os-auth-plugin', + metavar='', + default=utils.env('OS_AUTH_PLUGIN'), + help='The authentication method to use. If this option is not set, ' + 'openstackclient will attempt to guess the authentication method ' + 'to use based on the other options. If this option is set, ' + 'the --os-identity-api-version argument must be consistent ' + 'with the version of the method.\nAvailable methods are ' + + ', '.join(available_plugins), + choices=available_plugins + ) + # make sur we catch old v2.0 env values + envs = { + 'OS_PROJECT_NAME': utils.env( + 'OS_PROJECT_NAME', + default=utils.env('OS_TENANT_NAME') + ), + 'OS_PROJECT_ID': utils.env( + 'OS_PROJECT_ID', + default=utils.env('OS_TENANT_ID') + ), + } + for o in OPTIONS_LIST: + # remove allusion to tenants from v2.0 API + if 'tenant' not in o: + parser.add_argument( + '--os-' + o, + metavar='' % o, + default=envs.get(OPTIONS_LIST[o]['env'], + utils.env(OPTIONS_LIST[o]['env'])), + help='%s\n(Env: %s)' % (OPTIONS_LIST[o]['help'], + OPTIONS_LIST[o]['env']), + ) + # add tenant-related options for compatibility + # this is deprecated but still used in some tempest tests... + parser.add_argument( + '--os-tenant-name', + metavar='', + dest='os_project_name', + default=utils.env('OS_TENANT_NAME'), + help=argparse.SUPPRESS, + ) + parser.add_argument( + '--os-tenant-id', + metavar='', + dest='os_project_id', + default=utils.env('OS_TENANT_ID'), + help=argparse.SUPPRESS, + ) + return parser diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 4206ad001c..0542b47362 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -19,9 +19,11 @@ import pkg_resources import sys -from keystoneclient.auth.identity import v2 as v2_auth -from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient.auth import base from keystoneclient import session +import requests + +from openstackclient.api import auth from openstackclient.identity import client as identity_client @@ -45,105 +47,66 @@ class ClientManager(object): """Manages access to API clients, including authentication.""" identity = ClientCache(identity_client.make_client) - def __init__(self, token=None, url=None, auth_url=None, - domain_id=None, domain_name=None, - project_name=None, project_id=None, - username=None, password=None, - user_domain_id=None, user_domain_name=None, - project_domain_id=None, project_domain_name=None, - region_name=None, api_version=None, verify=True, - trust_id=None, timing=None): - self._token = token - self._url = url - self._auth_url = auth_url - self._domain_id = domain_id - self._domain_name = domain_name - self._project_name = project_name - self._project_id = project_id - self._username = username - self._password = password - self._user_domain_id = user_domain_id - self._user_domain_name = user_domain_name - self._project_domain_id = project_domain_id - self._project_domain_name = project_domain_name - self._region_name = region_name + def __getattr__(self, name): + # this is for the auth-related parameters. + if name in ['_' + o.replace('-', '_') + for o in auth.OPTIONS_LIST]: + return self._auth_params[name[1:]] + + def __init__(self, auth_options, api_version=None, verify=True): + + if not auth_options.os_auth_plugin: + auth._guess_authentication_method(auth_options) + + self._auth_plugin = auth_options.os_auth_plugin + self._url = auth_options.os_url + self._auth_params = auth.build_auth_params(auth_options) + self._region_name = auth_options.os_region_name self._api_version = api_version - self._trust_id = trust_id self._service_catalog = None - self.timing = timing + self.timing = auth_options.timing + + # For compatability until all clients can be updated + if 'project_name' in self._auth_params: + self._project_name = self._auth_params['project_name'] + elif 'tenant_name' in self._auth_params: + self._project_name = self._auth_params['tenant_name'] # verify is the Requests-compatible form self._verify = verify # also store in the form used by the legacy client libs self._cacert = None - if verify is True or verify is False: + if isinstance(verify, bool): self._insecure = not verify else: self._cacert = verify self._insecure = False - ver_prefix = identity_client.AUTH_VERSIONS[ - self._api_version[identity_client.API_NAME] - ] - # Get logging from root logger root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - # NOTE(dtroyer): These plugins are hard-coded for the first step - # in using the new Keystone auth plugins. - - if self._url: - LOG.debug('Using token auth %s', ver_prefix) - if ver_prefix == 'v2': - self.auth = v2_auth.Token( - auth_url=url, - token=token, - ) - else: - self.auth = v3_auth.Token( - auth_url=url, - token=token, - ) - else: - LOG.debug('Using password auth %s', ver_prefix) - if ver_prefix == 'v2': - self.auth = v2_auth.Password( - auth_url=auth_url, - username=username, - password=password, - trust_id=trust_id, - tenant_id=project_id, - tenant_name=project_name, - ) - else: - self.auth = v3_auth.Password( - auth_url=auth_url, - username=username, - password=password, - trust_id=trust_id, - user_domain_id=user_domain_id, - user_domain_name=user_domain_name, - domain_id=domain_id, - domain_name=domain_name, - project_id=project_id, - project_name=project_name, - project_domain_id=project_domain_id, - project_domain_name=project_domain_name, - ) - - self.session = session.Session( - auth=self.auth, - verify=verify, - ) + self.session = None + if not self._url: + LOG.debug('Using auth plugin: %s' % self._auth_plugin) + auth_plugin = base.get_plugin_class(self._auth_plugin) + self.auth = auth_plugin.load_from_options(**self._auth_params) + # needed by SAML authentication + request_session = requests.session() + self.session = session.Session( + auth=self.auth, + session=request_session, + verify=verify, + ) self.auth_ref = None - if not self._url: - # Trigger the auth call + if not self._auth_plugin.endswith("token") and not self._url: + LOG.debug("Populate other password flow attributes") self.auth_ref = self.session.auth.get_auth_ref(self.session) - # Populate other password flow attributes self._token = self.session.auth.get_token(self.session) self._service_catalog = self.auth_ref.service_catalog + else: + self._token = self._auth_params.get('token') return @@ -156,7 +119,7 @@ def get_endpoint_for_service_type(self, service_type): service_type=service_type) else: # Hope we were given the correct URL. - endpoint = self._url + endpoint = self._auth_url or self._url return endpoint diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index a43b50e373..bc10a6d237 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -16,9 +16,9 @@ import logging from keystoneclient.v2_0 import client as identity_client_v2_0 +from openstackclient.api import auth from openstackclient.common import utils - LOG = logging.getLogger(__name__) DEFAULT_IDENTITY_API_VERSION = '2.0' @@ -47,16 +47,15 @@ def make_client(instance): # TODO(dtroyer): Something doesn't like the session.auth when using # token auth, chase that down. if instance._url: - LOG.debug('Using token auth') + LOG.debug('Using service token auth') client = identity_client( endpoint=instance._url, - token=instance._token, + token=instance._auth_params['token'], cacert=instance._cacert, - insecure=instance._insecure, - trust_id=instance._trust_id, + insecure=instance._insecure ) else: - LOG.debug('Using password auth') + LOG.debug('Using auth plugin: %s' % instance._auth_plugin) client = identity_client( session=instance.session, cacert=instance._cacert, @@ -66,7 +65,6 @@ def make_client(instance): # so we can remove it if not instance._url: instance.auth_ref = instance.auth.get_auth_ref(instance.session) - return client @@ -81,14 +79,7 @@ def build_option_parser(parser): help='Identity API version, default=' + DEFAULT_IDENTITY_API_VERSION + ' (Env: OS_IDENTITY_API_VERSION)') - parser.add_argument( - '--os-trust-id', - metavar='', - default=utils.env('OS_TRUST_ID'), - help='Trust ID to use when authenticating. ' - 'This can only be used with Keystone v3 API ' - '(Env: OS_TRUST_ID)') - return parser + return auth.build_auth_plugins_option_parser(parser) class IdentityClientv2_0(identity_client_v2_0.Client): diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 8db1656c85..626e3f7da5 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -15,7 +15,6 @@ """Command-line interface to the OpenStack APIs""" -import argparse import getpass import logging import sys @@ -171,89 +170,13 @@ def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( description, version) - - # Global arguments - parser.add_argument( - '--os-auth-url', - metavar='', - default=utils.env('OS_AUTH_URL'), - help='Authentication URL (Env: OS_AUTH_URL)') - parser.add_argument( - '--os-domain-name', - metavar='', - default=utils.env('OS_DOMAIN_NAME'), - help='Domain name of the requested domain-level ' - 'authorization scope (Env: OS_DOMAIN_NAME)', - ) - parser.add_argument( - '--os-domain-id', - metavar='', - default=utils.env('OS_DOMAIN_ID'), - help='Domain ID of the requested domain-level ' - 'authorization scope (Env: OS_DOMAIN_ID)', - ) - parser.add_argument( - '--os-project-name', - metavar='', - default=utils.env('OS_PROJECT_NAME', - default=utils.env('OS_TENANT_NAME')), - help='Project name of the requested project-level ' - 'authorization scope (Env: OS_PROJECT_NAME)', - ) - parser.add_argument( - '--os-tenant-name', - metavar='', - dest='os_project_name', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--os-project-id', - metavar='', - default=utils.env('OS_PROJECT_ID', - default=utils.env('OS_TENANT_ID')), - help='Project ID of the requested project-level ' - 'authorization scope (Env: OS_PROJECT_ID)', - ) - parser.add_argument( - '--os-tenant-id', - metavar='', - dest='os_project_id', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--os-username', - metavar='', - default=utils.env('OS_USERNAME'), - help='Authentication username (Env: OS_USERNAME)') - parser.add_argument( - '--os-password', - metavar='', - default=utils.env('OS_PASSWORD'), - help='Authentication password (Env: OS_PASSWORD)') - parser.add_argument( - '--os-user-domain-name', - metavar='', - default=utils.env('OS_USER_DOMAIN_NAME'), - help='Domain name of the user (Env: OS_USER_DOMAIN_NAME)') + # service token auth argument parser.add_argument( - '--os-user-domain-id', - metavar='', - default=utils.env('OS_USER_DOMAIN_ID'), - help='Domain ID of the user (Env: OS_USER_DOMAIN_ID)') - parser.add_argument( - '--os-project-domain-name', - metavar='', - default=utils.env('OS_PROJECT_DOMAIN_NAME'), - help='Domain name of the project which is the requested ' - 'project-level authorization scope ' - '(Env: OS_PROJECT_DOMAIN_NAME)') - parser.add_argument( - '--os-project-domain-id', - metavar='', - default=utils.env('OS_PROJECT_DOMAIN_ID'), - help='Domain ID of the project which is the requested ' - 'project-level authorization scope ' - '(Env: OS_PROJECT_DOMAIN_ID)') + '--os-url', + metavar='', + default=utils.env('OS_URL'), + help='Defaults to env[OS_URL]') + # Global arguments parser.add_argument( '--os-region-name', metavar='', @@ -284,16 +207,6 @@ def build_option_parser(self, description, version): help='Default domain ID, default=' + DEFAULT_DOMAIN + ' (Env: OS_DEFAULT_DOMAIN)') - parser.add_argument( - '--os-token', - metavar='', - default=utils.env('OS_TOKEN'), - help='Defaults to env[OS_TOKEN]') - parser.add_argument( - '--os-url', - metavar='', - default=utils.env('OS_URL'), - help='Defaults to env[OS_URL]') parser.add_argument( '--timing', default=False, @@ -306,20 +219,42 @@ def build_option_parser(self, description, version): def authenticate_user(self): """Verify the required authentication credentials are present""" - self.log.debug('validating authentication options') - if self.options.os_token or self.options.os_url: + self.log.debug("validating authentication options") + + # Assuming all auth plugins will be named in the same fashion, + # ie vXpluginName + if (not self.options.os_url and + self.options.os_auth_plugin.startswith('v') and + self.options.os_auth_plugin[1] != + self.options.os_identity_api_version[0]): + raise exc.CommandError( + "Auth plugin %s not compatible" + " with requested API version" % self.options.os_auth_plugin + ) + # TODO(mhu) All these checks should be exposed at the plugin level + # or just dropped altogether, as the client instantiation will fail + # anyway + if self.options.os_url and not self.options.os_token: + # service token needed + raise exc.CommandError( + "You must provide a service token via" + " either --os-token or env[OS_TOKEN]") + + if (self.options.os_auth_plugin.endswith('token') and + (self.options.os_token or self.options.os_auth_url)): # Token flow auth takes priority if not self.options.os_token: raise exc.CommandError( "You must provide a token via" " either --os-token or env[OS_TOKEN]") - if not self.options.os_url: + if not self.options.os_auth_url: raise exc.CommandError( "You must provide a service URL via" - " either --os-url or env[OS_URL]") + " either --os-auth-url or env[OS_AUTH_URL]") - else: + if (not self.options.os_url and + not self.options.os_auth_plugin.endswith('token')): # Validate password flow auth if not self.options.os_username: raise exc.CommandError( @@ -347,13 +282,15 @@ def authenticate_user(self): (self.options.os_domain_id or self.options.os_domain_name) or self.options.os_trust_id): - raise exc.CommandError( - "You must provide authentication scope as a project " - "or a domain via --os-project-id or env[OS_PROJECT_ID], " - "--os-project-name or env[OS_PROJECT_NAME], " - "--os-domain-id or env[OS_DOMAIN_ID], or" - "--os-domain-name or env[OS_DOMAIN_NAME], or " - "--os-trust-id or env[OS_TRUST_ID].") + if self.options.os_auth_plugin.endswith('password'): + raise exc.CommandError( + "You must provide authentication scope as a project " + "or a domain via --os-project-id " + "or env[OS_PROJECT_ID], " + "--os-project-name or env[OS_PROJECT_NAME], " + "--os-domain-id or env[OS_DOMAIN_ID], or" + "--os-domain-name or env[OS_DOMAIN_NAME], or " + "--os-trust-id or env[OS_TRUST_ID].") if not self.options.os_auth_url: raise exc.CommandError( @@ -375,24 +312,9 @@ def authenticate_user(self): "Pick one of project, domain or trust.") self.client_manager = clientmanager.ClientManager( - token=self.options.os_token, - url=self.options.os_url, - auth_url=self.options.os_auth_url, - domain_id=self.options.os_domain_id, - domain_name=self.options.os_domain_name, - project_name=self.options.os_project_name, - project_id=self.options.os_project_id, - user_domain_id=self.options.os_user_domain_id, - user_domain_name=self.options.os_user_domain_name, - project_domain_id=self.options.os_project_domain_id, - project_domain_name=self.options.os_project_domain_name, - username=self.options.os_username, - password=self.options.os_password, - region_name=self.options.os_region_name, + auth_options=self.options, verify=self.verify, - timing=self.options.timing, api_version=self.api_version, - trust_id=self.options.os_trust_id, ) return diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 0bb657adb0..18461fb7e8 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -12,34 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. # - import mock +from requests_mock.contrib import fixture from keystoneclient.auth.identity import v2 as auth_v2 +from keystoneclient.openstack.common import jsonutils +from keystoneclient import service_catalog + +from openstackclient.api import auth from openstackclient.common import clientmanager +from openstackclient.common import exceptions as exc +from openstackclient.tests import fakes from openstackclient.tests import utils -AUTH_REF = {'a': 1} -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" -USERNAME = "itchy" -PASSWORD = "scratchy" -SERVICE_CATALOG = {'sc': '123'} - -API_VERSION = { - 'identity': '2.0', -} - +API_VERSION = {"identity": "2.0"} -def FakeMakeClient(instance): - return FakeClient() - - -class FakeClient(object): - auth_ref = AUTH_REF - auth_token = AUTH_TOKEN - service_catalog = SERVICE_CATALOG +AUTH_REF = {'version': 'v2.0'} +AUTH_REF.update(fakes.TEST_RESPONSE_DICT['access']) +SERVICE_CATALOG = service_catalog.ServiceCatalogV2(AUTH_REF) class Container(object): @@ -49,6 +40,18 @@ def __init__(self): pass +class FakeOptions(object): + def __init__(self, **kwargs): + for option in auth.OPTIONS_LIST: + setattr(self, 'os_' + option.replace('-', '_'), None) + self.os_auth_plugin = None + self.os_identity_api_version = '2.0' + self.timing = None + self.os_region_name = None + self.os_url = None + self.__dict__.update(kwargs) + + class TestClientCache(utils.TestCase): def test_singleton(self): @@ -58,30 +61,38 @@ def test_singleton(self): self.assertEqual(c.attr, c.attr) -@mock.patch('keystoneclient.session.Session') class TestClientManager(utils.TestCase): def setUp(self): super(TestClientManager, self).setUp() - - clientmanager.ClientManager.identity = \ - clientmanager.ClientCache(FakeMakeClient) - - def test_client_manager_token(self, mock): + self.mock = mock.Mock() + self.requests = self.useFixture(fixture.Fixture()) + # fake v2password token retrieval + self.stub_auth(json=fakes.TEST_RESPONSE_DICT) + # fake v3password token retrieval + self.stub_auth(json=fakes.TEST_RESPONSE_DICT_V3, + url='/'.join([fakes.AUTH_URL, 'auth/tokens'])) + # fake password version endpoint discovery + self.stub_auth(json=fakes.TEST_VERSIONS, + url=fakes.AUTH_URL, + verb='GET') + + def test_client_manager_token(self): client_manager = clientmanager.ClientManager( - token=AUTH_TOKEN, - url=AUTH_URL, - verify=True, + auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, + os_auth_url=fakes.AUTH_URL, + os_auth_plugin='v2token'), api_version=API_VERSION, + verify=True ) self.assertEqual( - AUTH_TOKEN, + fakes.AUTH_TOKEN, client_manager._token, ) self.assertEqual( - AUTH_URL, - client_manager._url, + fakes.AUTH_URL, + client_manager._auth_url, ) self.assertIsInstance( client_manager.auth, @@ -90,26 +101,26 @@ def test_client_manager_token(self, mock): self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) - def test_client_manager_password(self, mock): + def test_client_manager_password(self): client_manager = clientmanager.ClientManager( - auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - verify=False, + auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD), api_version=API_VERSION, + verify=False, ) self.assertEqual( - AUTH_URL, + fakes.AUTH_URL, client_manager._auth_url, ) self.assertEqual( - USERNAME, + fakes.USERNAME, client_manager._username, ) self.assertEqual( - PASSWORD, + fakes.PASSWORD, client_manager._password, ) self.assertIsInstance( @@ -119,16 +130,87 @@ def test_client_manager_password(self, mock): self.assertTrue(client_manager._insecure) self.assertFalse(client_manager._verify) - def test_client_manager_password_verify_ca(self, mock): + # These need to stick around until the old-style clients are gone + self.assertEqual( + AUTH_REF, + client_manager.auth_ref, + ) + self.assertEqual( + fakes.AUTH_TOKEN, + client_manager._token, + ) + self.assertEqual( + dir(SERVICE_CATALOG), + dir(client_manager._service_catalog), + ) + + def stub_auth(self, json=None, url=None, verb=None, **kwargs): + subject_token = fakes.AUTH_TOKEN + base_url = fakes.AUTH_URL + if json: + text = jsonutils.dumps(json) + headers = {'X-Subject-Token': subject_token, + 'Content-Type': 'application/json'} + if not url: + url = '/'.join([base_url, 'tokens']) + url = url.replace("/?", "?") + if not verb: + verb = 'POST' + self.requests.register_uri(verb, + url, + headers=headers, + text=text) + + def test_client_manager_password_verify_ca(self): client_manager = clientmanager.ClientManager( - auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - verify='cafile', + auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD, + os_auth_plugin='v2password'), api_version=API_VERSION, + verify='cafile', ) self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) + + def _client_manager_guess_auth_plugin(self, auth_params, + api_version, auth_plugin): + auth_params['os_auth_plugin'] = auth_plugin + auth_params['os_identity_api_version'] = api_version + client_manager = clientmanager.ClientManager( + auth_options=FakeOptions(**auth_params), + api_version=API_VERSION, + verify=True + ) + self.assertEqual( + auth_plugin, + client_manager._auth_plugin, + ) + + def test_client_manager_guess_auth_plugin(self): + # test token auth + params = dict(os_token=fakes.AUTH_TOKEN, + os_auth_url=fakes.AUTH_URL) + self._client_manager_guess_auth_plugin(params, '2.0', 'v2token') + self._client_manager_guess_auth_plugin(params, '3', 'v3token') + self._client_manager_guess_auth_plugin(params, 'XXX', 'token') + # test service auth + params = dict(os_token=fakes.AUTH_TOKEN, os_url='test') + self._client_manager_guess_auth_plugin(params, 'XXX', '') + # test password auth + params = dict(os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD) + self._client_manager_guess_auth_plugin(params, '2.0', 'v2password') + self._client_manager_guess_auth_plugin(params, '3', 'v3password') + self._client_manager_guess_auth_plugin(params, 'XXX', 'password') + + def test_client_manager_guess_auth_plugin_failure(self): + self.assertRaises(exc.CommandError, + clientmanager.ClientManager, + auth_options=FakeOptions(os_auth_plugin=''), + api_version=API_VERSION, + verify=True) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 5a1fc005e4..f8b7bb6f39 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -22,6 +22,142 @@ AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" +USERNAME = "itchy" +PASSWORD = "scratchy" +TEST_RESPONSE_DICT = { + "access": { + "metadata": { + "is_admin": 0, + "roles": [ + "1234", + ] + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "adminURL": AUTH_URL + "/v2.0", + "id": "1234", + "internalURL": AUTH_URL + "/v2.0", + "publicURL": AUTH_URL + "/v2.0", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + } + ], + "token": { + "expires": "2035-01-01T00:00:01Z", + "id": AUTH_TOKEN, + "issued_at": "2013-01-01T00:00:01.692048", + "tenant": { + "description": None, + "enabled": True, + "id": "1234", + "name": "testtenant" + } + }, + "user": { + "id": "5678", + "name": USERNAME, + "roles": [ + { + "name": "testrole" + }, + ], + "roles_links": [], + "username": USERNAME + } + } +} +TEST_RESPONSE_DICT_V3 = { + "token": { + "audit_ids": [ + "a" + ], + "catalog": [ + ], + "expires_at": "2034-09-29T18:27:15.978064Z", + "extras": {}, + "issued_at": "2014-09-29T17:27:15.978097Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "bbb", + "name": "project" + }, + "roles": [ + ], + "user": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "aaa", + "name": USERNAME + } + } +} +TEST_VERSIONS = { + "versions": { + "values": [ + { + "id": "v3.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v3+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v3+xml" + } + ], + "status": "stable", + "updated": "2013-03-06T00:00:00Z" + }, + { + "id": "v2.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + }, + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml" + } + ], + "status": "stable", + "updated": "2014-04-17T00:00:00Z" + } + ] + } +} class FakeStdout: diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index c180289e7d..b0c1452ef9 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -34,6 +34,8 @@ DEFAULT_REGION_NAME = "ZZ9_Plural_Z_Alpha" DEFAULT_TOKEN = "token" DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/" +DEFAULT_AUTH_PLUGIN = "v2password" + DEFAULT_COMPUTE_API_VERSION = "2" DEFAULT_IDENTITY_API_VERSION = "2.0" @@ -106,6 +108,8 @@ def _assert_password_auth(self, cmd_options, default_args): default_args["region_name"]) self.assertEqual(_shell.options.os_trust_id, default_args["trust_id"]) + self.assertEqual(_shell.options.os_auth_plugin, + default_args['auth_plugin']) def _assert_token_auth(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -115,7 +119,8 @@ def _assert_token_auth(self, cmd_options, default_args): self.app.assert_called_with(["list", "role"]) self.assertEqual(_shell.options.os_token, default_args["os_token"]) - self.assertEqual(_shell.options.os_url, default_args["os_url"]) + self.assertEqual(_shell.options.os_auth_url, + default_args["os_auth_url"]) def _assert_cli(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -175,9 +180,9 @@ def test_only_url_flow(self): "auth_url": DEFAULT_AUTH_URL, "project_id": "", "project_name": "", + "user_domain_id": "", "domain_id": "", "domain_name": "", - "user_domain_id": "", "user_domain_name": "", "project_domain_id": "", "project_domain_name": "", @@ -185,6 +190,7 @@ def test_only_url_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -204,6 +210,7 @@ def test_only_project_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -223,44 +230,7 @@ def test_only_project_name_flow(self): "password": "", "region_name": "", "trust_id": "", - } - self._assert_password_auth(flag, kwargs) - - def test_only_tenant_id_flow(self): - flag = "--os-tenant-id " + DEFAULT_PROJECT_ID - kwargs = { - "auth_url": "", - "project_id": DEFAULT_PROJECT_ID, - "project_name": "", - "domain_id": "", - "domain_name": "", - "user_domain_id": "", - "user_domain_name": "", - "project_domain_id": "", - "project_domain_name": "", - "username": "", - "password": "", - "region_name": "", - "trust_id": "", - } - self._assert_password_auth(flag, kwargs) - - def test_only_tenant_name_flow(self): - flag = "--os-tenant-name " + DEFAULT_PROJECT_NAME - kwargs = { - "auth_url": "", - "project_id": "", - "project_name": DEFAULT_PROJECT_NAME, - "domain_id": "", - "domain_name": "", - "user_domain_id": "", - "user_domain_name": "", - "project_domain_id": "", - "project_domain_name": "", - "username": "", - "password": "", - "region_name": "", - "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -280,6 +250,7 @@ def test_only_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -299,6 +270,7 @@ def test_only_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -318,6 +290,7 @@ def test_only_user_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -337,6 +310,7 @@ def test_only_user_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -356,6 +330,7 @@ def test_only_project_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -375,6 +350,7 @@ def test_only_project_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -394,6 +370,7 @@ def test_only_username_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -413,6 +390,7 @@ def test_only_password_flow(self): "password": DEFAULT_PASSWORD, "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -432,6 +410,7 @@ def test_only_region_name_flow(self): "password": "", "region_name": DEFAULT_REGION_NAME, "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -451,6 +430,27 @@ def test_only_trust_id_flow(self): "password": "", "region_name": "", "trust_id": "1234", + "auth_plugin": "", + } + self._assert_password_auth(flag, kwargs) + + def test_only_auth_plugin_flow(self): + flag = "--os-auth-plugin " + "v2password" + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "", + "trust_id": "", + "auth_plugin": DEFAULT_AUTH_PLUGIN } self._assert_password_auth(flag, kwargs) @@ -460,7 +460,7 @@ def setUp(self): super(TestShellTokenAuth, self).setUp() env = { "OS_TOKEN": DEFAULT_TOKEN, - "OS_URL": DEFAULT_SERVICE_URL, + "OS_AUTH_URL": DEFAULT_SERVICE_URL, } self.orig_env, os.environ = os.environ, env.copy() @@ -472,7 +472,7 @@ def test_default_auth(self): flag = "" kwargs = { "os_token": DEFAULT_TOKEN, - "os_url": DEFAULT_SERVICE_URL + "os_auth_url": DEFAULT_SERVICE_URL } self._assert_token_auth(flag, kwargs) @@ -481,7 +481,7 @@ def test_empty_auth(self): flag = "" kwargs = { "os_token": "", - "os_url": "" + "os_auth_url": "" } self._assert_token_auth(flag, kwargs) diff --git a/requirements.txt b/requirements.txt index 04f448834f..447b534a5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=1.2.1,!=2.4.0 six>=1.7.0 +stevedore>=1.0.0 From bb71df9ced18f343911fb90e745b4d875672cf27 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 22:54:42 -0400 Subject: [PATCH 0026/3303] Mark identity v2 resources for translation mark v2 catalog, ec2, endpoint, project, role, service and token Change-Id: I14a5852bfee4ca9e25130d001fdadd7778ad0996 --- openstackclient/identity/v2_0/catalog.py | 3 +- openstackclient/identity/v2_0/ec2creds.py | 15 +++++----- openstackclient/identity/v2_0/endpoint.py | 17 +++++------ openstackclient/identity/v2_0/project.py | 35 ++++++++++++----------- openstackclient/identity/v2_0/role.py | 31 +++++++++----------- openstackclient/identity/v2_0/service.py | 19 ++++++------ openstackclient/identity/v2_0/token.py | 4 ++- 7 files changed, 64 insertions(+), 60 deletions(-) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 7bda1acb68..1a96fdf60f 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -20,6 +20,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.i18n import _ # noqa def _format_endpoints(eps=None): @@ -67,7 +68,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to display (type, name or ID)', + help=_('Service to display (type, name or ID)'), ) return parser diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 74c9d5ebab..fd8eef8463 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -24,6 +24,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateEC2Creds(show.ShowOne): @@ -36,12 +37,12 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help='Specify a project [admin only]', + help=_('Specify a project [admin only]'), ) parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser @@ -83,12 +84,12 @@ def get_parser(self, prog_name): parser.add_argument( 'access_key', metavar='', - help='Credentials access key', + help=_('Credentials access key'), ) parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser @@ -118,7 +119,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser @@ -156,12 +157,12 @@ def get_parser(self, prog_name): parser.add_argument( 'access_key', metavar='', - help='Credentials access key', + help=_('Credentials access key'), ) parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 36f52cad4d..f7d7883109 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -23,6 +23,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -36,24 +37,24 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='New endpoint service') + help=_('New endpoint service')) parser.add_argument( '--region', metavar='', - help='New endpoint region') + help=_('New endpoint region')) parser.add_argument( '--publicurl', metavar='', required=True, - help='New endpoint public URL') + help=_('New endpoint public URL')) parser.add_argument( '--adminurl', metavar='', - help='New endpoint admin URL') + help=_('New endpoint admin URL')) parser.add_argument( '--internalurl', metavar='', - help='New endpoint internal URL') + help=_('New endpoint internal URL')) return parser def take_action(self, parsed_args): @@ -84,7 +85,7 @@ def get_parser(self, prog_name): parser.add_argument( 'endpoint', metavar='', - help='ID of endpoint to delete') + help=_('ID of endpoint to delete')) return parser def take_action(self, parsed_args): @@ -105,7 +106,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -139,7 +140,7 @@ def get_parser(self, prog_name): parser.add_argument( 'endpoint_or_service', metavar='', - help='Endpoint ID or name, type or ID of service to display') + help=_('Endpoint ID or name, type or ID of service to display')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index ebd65df7d6..7ead0890d4 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -21,10 +21,11 @@ from cliff import command from cliff import lister from cliff import show - from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateProject(show.ShowOne): @@ -37,30 +38,30 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New project name', + help=_('New project name'), ) parser.add_argument( '--description', metavar='', - help='New project description', + help=_('New project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable project (default)', + help=_('Enable project (default)'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help=_('Disable project'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' - '(repeat option to set multiple properties)', + help=_('Property to add for this project ' + '(repeat option to set multiple properties)'), ) return parser @@ -97,7 +98,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to delete (name or ID)', + help=_('Project to delete (name or ID)'), ) return parser @@ -125,7 +126,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -153,35 +154,35 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to change (name or ID)', + help=_('Project to change (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='New project name', + help=_('New project name'), ) parser.add_argument( '--description', metavar='', - help='New project description', + help=_('New project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable project', + help=_('Enable project'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help=_('Disable project'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' - '(repeat option to set multiple properties)', + help=_('Property to add for this project ' + '(repeat option to set multiple properties)'), ) return parser @@ -233,7 +234,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to display (name or ID)') + help=_('Project to display (name or ID)')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index faf48ed95f..4c45004dae 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -24,6 +24,7 @@ from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class AddRole(show.ShowOne): @@ -36,18 +37,17 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Role name or ID to add to user') + help=_('Role name or ID to add to user')) parser.add_argument( '--project', metavar='', required=True, - help='Include project (name or ID)', - ) + help=_('Include project (name or ID)')) parser.add_argument( '--user', metavar='', required=True, - help='Name or ID of user to include') + help=_('Name or ID of user to include')) return parser def take_action(self, parsed_args): @@ -80,7 +80,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role_name', metavar='', - help='New role name') + help=_('New role name')) return parser def take_action(self, parsed_args): @@ -103,8 +103,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to delete', - ) + help=_('Name or ID of role to delete')) return parser def take_action(self, parsed_args): @@ -147,12 +146,11 @@ def get_parser(self, prog_name): 'user', metavar='', nargs='?', - help='Name or ID of user to include') + help=_('Name or ID of user to include')) parser.add_argument( '--project', metavar='', - help='Include project (name or ID)', - ) + help=_('Include project (name or ID)')) return parser def take_action(self, parsed_args): @@ -167,13 +165,13 @@ def take_action(self, parsed_args): if self.app.client_manager.auth_ref: parsed_args.project = auth_ref.project_id else: - msg = "Project must be specified" + msg = _("Project must be specified") raise exceptions.CommandError(msg) if not parsed_args.user: if self.app.client_manager.auth_ref: parsed_args.user = auth_ref.user_id else: - msg = "User must be specified" + msg = _("User must be specified") raise exceptions.CommandError(msg) project = utils.find_resource( @@ -213,18 +211,17 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Role name or ID to remove from user') + help=_('Role name or ID to remove from user')) parser.add_argument( '--project', metavar='', required=True, - help='Project to include (name or ID)', - ) + help=_('Project to include (name or ID)')) parser.add_argument( '--user', metavar='', required=True, - help='Name or ID of user') + help=_('Name or ID of user')) return parser def take_action(self, parsed_args): @@ -252,7 +249,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to display') + help=_('Name or ID of role to display')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 138ed3b097..458dce7cba 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -24,6 +24,7 @@ from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -37,18 +38,18 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New service name', + help=_('New service name'), ) parser.add_argument( '--type', metavar='', required=True, - help='New service type (compute, image, identity, volume, etc)', + help=_('New service type (compute, image, identity, volume, etc)'), ) parser.add_argument( '--description', metavar='', - help='New service description', + help=_('New service description'), ) return parser @@ -76,7 +77,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to delete (name or ID)', + help=_('Service to delete (name or ID)'), ) return parser @@ -99,7 +100,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -127,13 +128,13 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to display (type, name or ID)', + help=_('Service to display (type, name or ID)'), ) parser.add_argument( '--catalog', action='store_true', default=False, - help='Show service catalog information', + help=_('Show service catalog information'), ) return parser @@ -150,8 +151,8 @@ def take_action(self, parsed_args): info.update(service_endpoints[0]) return zip(*sorted(six.iteritems(info))) - msg = ("No service catalog with a type, name or ID of '%s' " - "exists." % (parsed_args.service)) + msg = _("No service catalog with a type, name or ID of '%s' " + "exists.") % (parsed_args.service) raise exceptions.CommandError(msg) else: service = common.find_service(identity_client, parsed_args.service) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index f3fedc0107..c8b003ee02 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -21,6 +21,8 @@ from cliff import command from cliff import show +from openstackclient.i18n import _ # noqa + class IssueToken(show.ShowOne): """Issue new token""" @@ -49,7 +51,7 @@ def get_parser(self, prog_name): parser.add_argument( 'token', metavar='', - help='Token to be deleted', + help=_('Token to be deleted'), ) return parser From 364071a90bfe9dcec1d02a349c33dc8422fc14f3 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 7 Oct 2014 02:15:15 -0400 Subject: [PATCH 0027/3303] Add domain parameters to user show for Identity V3 Update `user show` for Identity V3 to account for a domain argument, in doing so, also update `find resource` to be more flexible by allowing **kwargs. Also update `group show` and `project show` since they follow the same logic as a user within a group. Change-Id: Ib828e4dbeb0bd31164396069ce8a64c873179779 Closes-Bug: #1378165 --- openstackclient/common/utils.py | 28 +++++++++++++++++++++++--- openstackclient/identity/v3/group.py | 23 ++++++++++++++++----- openstackclient/identity/v3/project.py | 23 +++++++++++++++------ openstackclient/identity/v3/user.py | 22 +++++++++++++------- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 818f8d4771..9ad3823cc8 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -26,8 +26,27 @@ from openstackclient.common import exceptions -def find_resource(manager, name_or_id): - """Helper for the _find_* methods.""" +def find_resource(manager, name_or_id, **kwargs): + """Helper for the _find_* methods. + + :param manager: A client manager class + :param name_or_id: The resource we are trying to find + :param kwargs: To be used in calling .find() + :rtype: The found resource + + This method will attempt to find a resource in a variety of ways. + Primarily .get() methods will be called with `name_or_id` as an integer + value, and tried again as a string value. + + If both fail, then a .find() is attempted, which is essentially calling + a .list() function with a 'name' query parameter that is set to + `name_or_id`. + + Lastly, if any kwargs are passed in, they will be treated as additional + query parameters. This is particularly handy in the case of finding + resources in a domain. + + """ # Try to get entity as integer id try: @@ -49,7 +68,10 @@ def find_resource(manager, name_or_id): except Exception: pass - kwargs = {} + if len(kwargs) == 0: + kwargs = {} + + # Prepare the kwargs for calling find if 'NAME_ATTR' in manager.resource_class.__dict__: # novaclient does this for oddball resources kwargs[manager.resource_class.NAME_ATTR] = name_or_id diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 4eb144896d..fdb9501ad6 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -24,6 +24,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.identity import common class AddUserToGroup(command.Command): @@ -321,14 +322,26 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Name or ID of group to display') + help='Name or ID of group to display', + ) + parser.add_argument( + '--domain', + metavar='', + help='Domain where group resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - group = utils.find_resource(identity_client.groups, parsed_args.group) - info = {} - info.update(group._info) - return zip(*sorted(six.iteritems(info))) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + group = utils.find_resource(identity_client.groups, + parsed_args.group, + domain_id=domain.id) + else: + group = utils.find_resource(identity_client.groups, + parsed_args.group) + + return zip(*sorted(six.iteritems(group._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index fa935f0bf5..ec8e5a3b1a 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -264,15 +264,26 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Name or ID of project to display') + help='Name or ID of project to display', + ) + parser.add_argument( + '--domain', + metavar='', + help='Domain where project resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource(identity_client.projects, - parsed_args.project) - info = {} - info.update(project._info) - return zip(*sorted(six.iteritems(info))) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + project = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id) + else: + project = utils.find_resource(identity_client.projects, + parsed_args.project) + + return zip(*sorted(six.iteritems(project._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index e4eb7526c2..73bb7f1308 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -23,6 +23,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.identity import common class CreateUser(show.ShowOne): @@ -364,17 +365,24 @@ def get_parser(self, prog_name): metavar='', help='User to display (name or ID)', ) + parser.add_argument( + '--domain', + metavar='', + help='Domain where user resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + user = utils.find_resource(identity_client.users, + parsed_args.user, + domain_id=domain.id) + else: + user = utils.find_resource(identity_client.users, + parsed_args.user) - info = {} - info.update(user._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(user._info))) From b61db3eb725d19d991c9eac39546c2a6dbc6ffbd Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 22:29:47 -0400 Subject: [PATCH 0028/3303] Add translation markers for user v2 actions implements bp use_i18n Change-Id: I86508a232c9cf88695b7982dad0b9b02eaf8b3a1 --- MANIFEST.in | 2 + babel.cfg | 1 + openstackclient/i18n.py | 2 +- openstackclient/identity/v2_0/user.py | 41 +++++----- .../locale/python-openstackclient.pot | 78 +++++++++++++++++++ requirements.txt | 1 + setup.cfg | 14 ++++ test-requirements.txt | 2 - 8 files changed, 118 insertions(+), 23 deletions(-) create mode 100644 babel.cfg create mode 100644 python-openstackclient/locale/python-openstackclient.pot diff --git a/MANIFEST.in b/MANIFEST.in index fd87b80fdf..aeabc0d31a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,12 @@ include AUTHORS +include babel.cfg include ChangeLog include LICENSE include README.rst recursive-include doc * recursive-include tests * +recursive-include python-openstackclient *.po *.pot exclude .gitignore exclude .gitreview diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000000..efceab818b --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/openstackclient/i18n.py b/openstackclient/i18n.py index bd52d64838..3611b315c9 100644 --- a/openstackclient/i18n.py +++ b/openstackclient/i18n.py @@ -15,7 +15,7 @@ from oslo import i18n -_translators = i18n.TranslatorFactory(domain='openstackclient') +_translators = i18n.TranslatorFactory(domain='python-openstackclient') # The primary translation function using the well-known name "_" _ = _translators.primary diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 93ab94fe0e..729c6ac81f 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -21,9 +21,10 @@ from cliff import command from cliff import lister from cliff import show - from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateUser(show.ShowOne): @@ -36,39 +37,39 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New user name', + help=_('New user name'), ) parser.add_argument( '--password', metavar='', - help='New user password', + help=_('New user password'), ) parser.add_argument( '--password-prompt', dest="password_prompt", action="store_true", - help='Prompt interactively for password', + help=_('Prompt interactively for password'), ) parser.add_argument( '--email', metavar='', - help='New user email address', + help=_('New user email address'), ) parser.add_argument( '--project', metavar='', - help='Set default project (name or ID)', + help=_('Set default project (name or ID)'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable user (default)', + help=_('Enable user (default)'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable user', + help=_('Disable user'), ) return parser @@ -120,7 +121,7 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='User to delete (name or ID)', + help=_('User to delete (name or ID)'), ) return parser @@ -147,13 +148,13 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help='Filter users by project (name or ID)', + help=_('Filter users by project (name or ID)'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -237,44 +238,44 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='User to change (name or ID)', + help=_('User to change (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='New user name', + help=_('New user name'), ) parser.add_argument( '--password', metavar='', - help='New user password', + help=_('New user password'), ) parser.add_argument( '--password-prompt', dest="password_prompt", action="store_true", - help='Prompt interactively for password', + help=_('Prompt interactively for password'), ) parser.add_argument( '--email', metavar='', - help='New user email address', + help=_('New user email address'), ) parser.add_argument( '--project', metavar='', - help='New default project (name or ID)', + help=_('New default project (name or ID)'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable user (default)', + help=_('Enable user (default)'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable user', + help=_('Disable user'), ) return parser @@ -340,7 +341,7 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='User to display (name or ID)', + help=_('User to display (name or ID)'), ) return parser diff --git a/python-openstackclient/locale/python-openstackclient.pot b/python-openstackclient/locale/python-openstackclient.pot new file mode 100644 index 0000000000..5dcdac88b9 --- /dev/null +++ b/python-openstackclient/locale/python-openstackclient.pot @@ -0,0 +1,78 @@ +# Translations template for python-openstackclient. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the +# python-openstackclient project. +# FIRST AUTHOR , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: python-openstackclient 0.4.2.dev43.gaf67658\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-10-09 13:11-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: openstackclient/identity/v2_0/user.py:40 +#: openstackclient/identity/v2_0/user.py:246 +msgid "New user name" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:45 +#: openstackclient/identity/v2_0/user.py:251 +msgid "New user password" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:51 +#: openstackclient/identity/v2_0/user.py:257 +msgid "Prompt interactively for password" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:56 +#: openstackclient/identity/v2_0/user.py:262 +msgid "New user email address" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:61 +msgid "Set default project (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:67 +#: openstackclient/identity/v2_0/user.py:273 +msgid "Enable user (default)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:72 +#: openstackclient/identity/v2_0/user.py:278 +msgid "Disable user" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:124 +msgid "User to delete (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:151 +msgid "Filter users by project (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:157 +msgid "List additional fields in output" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:241 +msgid "User to change (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:267 +msgid "New default project (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:344 +msgid "User to display (name or ID)" +msgstr "" + diff --git a/requirements.txt b/requirements.txt index 04f448834f..f6d1c61c08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # 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. +Babel>=1.3 cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 3178fe4467..e97bbd2a6c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -320,3 +320,17 @@ upload-dir = doc/build/html [wheel] universal = 1 + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = python-openstackclient/locale/python-openstackclient.pot + +[update_catalog] +domain = python-openstackclient +output_dir = python-openstackclient/locale +input_file = python-openstackclient/locale/python-openstackclient.pot + +[compile_catalog] +directory = python-openstackclient/locale +domain = python-openstackclient diff --git a/test-requirements.txt b/test-requirements.txt index 3b95cf7db3..50ddc88e9c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,5 +13,3 @@ sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 WebOb>=1.2.3 - -Babel>=1.3 From f0c57e17c9a4b5bbe2f072a4eacefce3bcf30d45 Mon Sep 17 00:00:00 2001 From: Nathan Kinder Date: Tue, 7 Oct 2014 16:30:56 -0700 Subject: [PATCH 0029/3303] Allow --domain to be used for identity commands without lookup Performing create, list, or set operations for users, groups, and projects with the --domain option attempts to look up the domain for name to ID conversion. In the case of an environment using Keystone domains, it is desired to allow a domain admin to perform these operations for objects in their domain without allowing them to list or show domains. The current behavior prevents the domain admin from performing these operations since they will be forbidden to perform the underlying list_domains operation. This patch makes the domain lookup error a soft failure, and falls back to using the passed in domain argument directly as a domain ID in the request that it sends to Keystone. Change-Id: I5139097f8cedc53693f6f71297518917ac72e50a Closes-Bug: #1378565 --- openstackclient/identity/v3/group.py | 18 +++++++----------- openstackclient/identity/v3/project.py | 13 +++++++------ openstackclient/identity/v3/user.py | 17 +++++++---------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index fdb9501ad6..bdb4e02980 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -129,8 +129,8 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource(identity_client.domains, - parsed_args.domain).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None group = identity_client.groups.create( @@ -174,7 +174,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter group list by ', + help='Filter group list by (name or ID)', ) parser.add_argument( '--user', @@ -194,10 +194,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None @@ -301,10 +299,8 @@ def take_action(self, parsed_args): if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, parsed_args.domain).id - kwargs['domain'] = domain - + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id if not len(kwargs): sys.stderr.write("Group not updated, no arguments present") return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index ec8e5a3b1a..7b3c281cf9 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -74,7 +74,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None @@ -141,7 +142,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter by a specific domain', + help='Filter by a specific domain (name or ID)', ) return parser @@ -154,8 +155,8 @@ def take_action(self, parsed_args): columns = ('ID', 'Name') kwargs = {} if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) - kwargs['domain'] = domain.id + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id data = identity_client.projects.list(**kwargs) return (columns, (utils.get_item_properties( @@ -232,8 +233,8 @@ def take_action(self, parsed_args): if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) - kwargs['domain'] = domain.id + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.enable: diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 73bb7f1308..10921219d6 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -95,8 +95,8 @@ def take_action(self, parsed_args): project_id = None if parsed_args.domain: - domain_id = utils.find_resource( - identity_client.domains, parsed_args.domain).id + domain_id = common.find_domain(identity_client, + parsed_args.domain).id else: domain_id = None @@ -158,7 +158,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter group list by ', + help='Filter user list by (name or ID)', ) parser.add_argument( '--group', @@ -178,10 +178,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None @@ -311,9 +309,8 @@ def take_action(self, parsed_args): identity_client.projects, parsed_args.project).id kwargs['project'] = project_id if parsed_args.domain: - domain_id = utils.find_resource( - identity_client.domains, parsed_args.domain).id - kwargs['domain'] = domain_id + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id kwargs['enabled'] = user.enabled if parsed_args.enable: kwargs['enabled'] = True From 3af547a1a6e597ea1b38fb273195ac1ef00d29dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Oct 2014 14:25:50 -0700 Subject: [PATCH 0030/3303] Fix operation on clouds with availability-zones In a cloud with AZs, you can get multiple entries back from the service catalog - one for each AZ and then one that is AZ agnostic that's tied to the region. If the region_name is plumbed all the way through, this works as intended. Change-Id: I3b365ea306e8111fc80830672ae8080a5d1dc8e0 --- openstackclient/common/clientmanager.py | 4 ++-- openstackclient/compute/client.py | 2 +- openstackclient/network/client.py | 3 ++- openstackclient/volume/client.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..387721a453 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -110,13 +110,13 @@ def __init__(self, auth_options, api_version=None, verify=True): return - def get_endpoint_for_service_type(self, service_type): + def get_endpoint_for_service_type(self, service_type, region_name=None): """Return the endpoint URL for the service type.""" # See if we are using password flow auth, i.e. we have a # service catalog to select endpoints from if self._service_catalog: endpoint = self._service_catalog.url_for( - service_type=service_type) + service_type=service_type, region_name=region_name) else: # Hope we were given the correct URL. endpoint = self._auth_url or self._url diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index dc50507eb2..d473295b71 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -68,7 +68,7 @@ def make_client(instance): else: # password flow client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME) + API_NAME, region_name=instance._region_name) client.client.service_catalog = instance._service_catalog client.client.auth_token = instance._token return client diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index d3102da1eb..870fdad1db 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -35,7 +35,8 @@ def make_client(instance): LOG.debug('Instantiating network client: %s', network_client) if not instance._url: - instance._url = instance.get_endpoint_for_service_type("network") + instance._url = instance.get_endpoint_for_service_type( + "network", region_name=instance._region_name) return network_client( username=instance._username, tenant_name=instance._project_name, diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index f71fbe8bcd..58cb267e53 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -68,7 +68,7 @@ def make_client(instance): else: # password flow client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME) + API_NAME, region_name=instance._region_name) client.client.service_catalog = instance._service_catalog client.client.auth_token = instance._token From 7b046f951131a94cb3c345cd03a0bb5220720522 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 11 Oct 2014 22:37:59 +0000 Subject: [PATCH 0031/3303] Updated from global requirements Change-Id: I2ac5b9ac545c1bb6ec6279ecbe74e3301eb07a25 --- requirements.txt | 6 +++--- test-requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 447b534a5e..8bb42eddf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 -python-keystoneclient>=0.10.0 +python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 -requests>=1.2.1,!=2.4.0 +requests>=2.2.0,!=2.4.0 six>=1.7.0 -stevedore>=1.0.0 +stevedore>=1.0.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 3b95cf7db3..dea07e8d05 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 requests-mock>=0.4.0 # Apache-2.0 -sphinx>=1.1.2,!=1.2.0,<1.3 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 testtools>=0.9.34 WebOb>=1.2.3 From 1b3c7ec122504a41dc35aaf3f0693c9e20015df8 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 10 Oct 2014 14:17:11 -0400 Subject: [PATCH 0032/3303] Fix issue token for v3 Currently the code is broken as it references a part of keystoneclient that does not exist. Change-Id: I7fbc754537fbb4acffb166b5854840acfaef1fb8 Closes-Bug: #1379871 --- functional/tests/test_identity.py | 6 ++++++ openstackclient/identity/v3/token.py | 4 ++-- openstackclient/tests/identity/v3/fakes.py | 4 +++- openstackclient/tests/identity/v3/test_token.py | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py index cdb0ed341b..c5779a206d 100644 --- a/functional/tests/test_identity.py +++ b/functional/tests/test_identity.py @@ -76,6 +76,7 @@ class IdentityV3Tests(test.TestCase): DOMAIN_FIELDS = ['description', 'enabled', 'id', 'name', 'links'] GROUP_FIELDS = ['description', 'domain_id', 'id', 'name', 'links'] + TOKEN_FIELDS = ['expires', 'id', 'project_id', 'user_id'] def _create_dummy_group(self): name = uuid.uuid4().hex @@ -139,3 +140,8 @@ def test_domain_show(self): raw_output = self.openstack('domain show ' + name) items = self.parse_show(raw_output) self.assert_show_fields(items, self.DOMAIN_FIELDS) + + def test_token_issue(self): + raw_output = self.openstack('token issue') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.TOKEN_FIELDS) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 52ed439f15..aca5c66993 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -159,9 +159,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - identity_client = self.app.client_manager.identity + session = self.app.client_manager.identity.session - token = identity_client.service_catalog.get_token() + token = session.auth.auth_ref.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index b0df16f043..1ca1c55d5d 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -292,7 +292,9 @@ def __init__(self, **kwargs): self.roles.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) - self.service_catalog = mock.Mock() + self.session = mock.Mock() + self.session.auth.auth_ref.service_catalog.resource_class = \ + fakes.FakeResource(None, {}) self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) self.role_assignments = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index 8888b93186..dbe855555c 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -23,7 +23,8 @@ def setUp(self): super(TestToken, self).setUp() # Get a shortcut to the Service Catalog Mock - self.sc_mock = self.app.client_manager.identity.service_catalog + session = self.app.client_manager.identity.session + self.sc_mock = session.auth.auth_ref.service_catalog self.sc_mock.reset_mock() From a8d4b0eebb563c8aad33300938b1b32347eb0050 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 00:07:45 -0400 Subject: [PATCH 0033/3303] Remove 'links' section from several v3 Identity objects The links field in the returned objects from the v3 Identity API aren't really useful, so let's remove them. Managed to remove most of them from the core API. I'll likely remove the extension/contribution (oauth/federation) related ones in another patch. Also in this patch the code for setting services and projects was changed. Though not incorrect, it was not needed to copy the entire returned object, we should just need to pass in the fields we want to update. Change-Id: I164ca9ad8b28fa10b291e9115ef40753e387c547 --- openstackclient/identity/v3/credential.py | 2 ++ openstackclient/identity/v3/domain.py | 2 ++ openstackclient/identity/v3/endpoint.py | 2 ++ openstackclient/identity/v3/group.py | 6 +++--- openstackclient/identity/v3/policy.py | 2 ++ openstackclient/identity/v3/project.py | 15 ++++----------- openstackclient/identity/v3/role.py | 2 ++ openstackclient/identity/v3/service.py | 6 +++--- openstackclient/identity/v3/user.py | 6 +++--- openstackclient/tests/identity/v3/fakes.py | 9 +++++++++ .../tests/identity/v3/test_project.py | 16 ---------------- .../tests/identity/v3/test_service.py | 8 -------- 12 files changed, 32 insertions(+), 44 deletions(-) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index f1e17b8502..4d6889548b 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -73,6 +73,7 @@ def take_action(self, parsed_args): blob=parsed_args.data, project=project) + credential._info.pop('links') return zip(*sorted(six.iteritems(credential._info))) @@ -193,4 +194,5 @@ def take_action(self, parsed_args): credential = utils.find_resource(identity_client.credentials, parsed_args.credential) + credential._info.pop('links') return zip(*sorted(six.iteritems(credential._info))) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 49397afc46..d14da48682 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -66,6 +66,7 @@ def take_action(self, parsed_args): enabled=parsed_args.enabled, ) + domain._info.pop('links') return zip(*sorted(six.iteritems(domain._info))) @@ -187,4 +188,5 @@ def take_action(self, parsed_args): domain = utils.find_resource(identity_client.domains, parsed_args.domain) + domain._info.pop('links') return zip(*sorted(six.iteritems(domain._info))) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 39798b2dd4..0c077c5a34 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -81,6 +81,7 @@ def take_action(self, parsed_args): ) info = {} + endpoint._info.pop('links') info.update(endpoint._info) info['service_name'] = service.name info['service_type'] = service.type @@ -258,6 +259,7 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, endpoint.service_id) info = {} + endpoint._info.pop('links') info.update(endpoint._info) info['service_name'] = service.name info['service_type'] = service.type diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index bdb4e02980..d2ffca2733 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -138,9 +138,8 @@ def take_action(self, parsed_args): domain=domain, description=parsed_args.description) - info = {} - info.update(group._info) - return zip(*sorted(six.iteritems(info))) + group._info.pop('links') + return zip(*sorted(six.iteritems(group._info))) class DeleteGroup(command.Command): @@ -340,4 +339,5 @@ def take_action(self, parsed_args): group = utils.find_resource(identity_client.groups, parsed_args.group) + group._info.pop('links') return zip(*sorted(six.iteritems(group._info))) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 87f3cbe974..802880bfc2 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -55,6 +55,7 @@ def take_action(self, parsed_args): blob=blob, type=parsed_args.type ) + policy._info.pop('links') return zip(*sorted(six.iteritems(policy._info))) @@ -173,4 +174,5 @@ def take_action(self, parsed_args): policy = utils.find_resource(identity_client.policies, parsed_args.policy) + policy._info.pop('links') return zip(*sorted(six.iteritems(policy._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 7b3c281cf9..1cdeb15067 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -94,9 +94,8 @@ def take_action(self, parsed_args): **kwargs ) - info = {} - info.update(project._info) - return zip(*sorted(six.iteritems(info))) + project._info.pop('links') + return zip(*sorted(six.iteritems(project._info))) class DeleteProject(command.Command): @@ -229,7 +228,7 @@ def take_action(self, parsed_args): parsed_args.project, ) - kwargs = project._info + kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.domain: @@ -243,13 +242,6 @@ def take_action(self, parsed_args): kwargs['enabled'] = False if parsed_args.property: kwargs.update(parsed_args.property) - if 'id' in kwargs: - del kwargs['id'] - if 'domain_id' in kwargs: - # Hack around broken Identity API arg names - kwargs.update( - {'domain': kwargs.pop('domain_id')} - ) identity_client.projects.update(project.id, **kwargs) return @@ -287,4 +279,5 @@ def take_action(self, parsed_args): project = utils.find_resource(identity_client.projects, parsed_args.project) + project._info.pop('links') return zip(*sorted(six.iteritems(project._info))) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index d8de7c2291..a3c24b7af6 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -157,6 +157,7 @@ def take_action(self, parsed_args): role = identity_client.roles.create(name=parsed_args.name) + role._info.pop('links') return zip(*sorted(six.iteritems(role._info))) @@ -472,4 +473,5 @@ def take_action(self, parsed_args): parsed_args.role, ) + role._info.pop('links') return zip(*sorted(six.iteritems(role._info))) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 88301edc05..4f62226968 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -70,6 +70,7 @@ def take_action(self, parsed_args): enabled=enabled, ) + service._info.pop('links') return zip(*sorted(six.iteritems(service._info))) @@ -161,7 +162,7 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) - kwargs = service._info + kwargs = {} if parsed_args.type: kwargs['type'] = parsed_args.type if parsed_args.name: @@ -170,8 +171,6 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False - if 'id' in kwargs: - del kwargs['id'] identity_client.services.update( service.id, @@ -200,4 +199,5 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) + service._info.pop('links') return zip(*sorted(six.iteritems(service._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 10921219d6..9a3e00b83a 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -116,9 +116,8 @@ def take_action(self, parsed_args): enabled=enabled ) - info = {} - info.update(user._info) - return zip(*sorted(six.iteritems(info))) + user._info.pop('links') + return zip(*sorted(six.iteritems(user._info))) class DeleteUser(command.Command): @@ -382,4 +381,5 @@ def take_action(self, parsed_args): user = utils.find_resource(identity_client.users, parsed_args.user) + user._info.pop('links') return zip(*sorted(six.iteritems(user._info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index b0df16f043..8e8c8767ca 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -18,6 +18,7 @@ from openstackclient.tests import fakes from openstackclient.tests import utils +base_url = 'http://identity:5000/v3/' domain_id = 'd1' domain_name = 'oftheking' @@ -28,6 +29,7 @@ 'name': domain_name, 'description': domain_description, 'enabled': True, + 'links': base_url + 'domains/' + domain_id, } group_id = 'gr-010' @@ -36,6 +38,7 @@ GROUP = { 'id': group_id, 'name': group_name, + 'links': base_url + 'groups/' + group_id, } mapping_id = 'test_mapping' @@ -107,6 +110,7 @@ 'description': project_description, 'enabled': True, 'domain_id': domain_id, + 'links': base_url + 'projects/' + project_id, } PROJECT_2 = { @@ -115,6 +119,7 @@ 'description': project_description + 'plus four more', 'enabled': True, 'domain_id': domain_id, + 'links': base_url + 'projects/' + project_id, } role_id = 'r1' @@ -123,6 +128,7 @@ ROLE = { 'id': role_id, 'name': role_name, + 'links': base_url + 'roles/' + role_id, } service_id = 's-123' @@ -134,6 +140,7 @@ 'name': service_name, 'type': service_type, 'enabled': True, + 'links': base_url + 'services/' + service_id, } endpoint_id = 'e-123' @@ -148,6 +155,7 @@ 'interface': endpoint_interface, 'service_id': service_id, 'enabled': True, + 'links': base_url + 'endpoints/' + endpoint_id, } user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' @@ -162,6 +170,7 @@ 'email': user_email, 'enabled': True, 'domain_id': domain_id, + 'links': base_url + 'users/' + user_id, } token_expires = '2014-01-01T00:00:00Z' diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 2e7bc54b5c..1060a27795 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -533,9 +533,6 @@ def test_project_set_name(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, - 'enabled': True, 'name': 'qwerty', } # ProjectManager.update(project, name=, domain=, description=, @@ -564,9 +561,6 @@ def test_project_set_description(self): # Set expected values kwargs = { 'description': 'new desc', - 'domain': identity_fakes.domain_id, - 'enabled': True, - 'name': identity_fakes.project_name, } self.projects_mock.update.assert_called_with( identity_fakes.project_id, @@ -590,10 +584,7 @@ def test_project_set_enable(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, 'enabled': True, - 'name': identity_fakes.project_name, } self.projects_mock.update.assert_called_with( identity_fakes.project_id, @@ -617,10 +608,7 @@ def test_project_set_disable(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, 'enabled': False, - 'name': identity_fakes.project_name, } self.projects_mock.update.assert_called_with( identity_fakes.project_id, @@ -644,10 +632,6 @@ def test_project_set_property(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, - 'enabled': True, - 'name': identity_fakes.project_name, 'fee': 'fi', 'fo': 'fum', } diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 6733f7faef..57db77b12a 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -267,9 +267,7 @@ def test_service_set_type(self): # Set expected values kwargs = { - 'name': identity_fakes.service_name, 'type': identity_fakes.service_type, - 'enabled': True, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) self.services_mock.update.assert_called_with( @@ -297,8 +295,6 @@ def test_service_set_name(self): # Set expected values kwargs = { 'name': identity_fakes.service_name, - 'type': identity_fakes.service_type, - 'enabled': True, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) self.services_mock.update.assert_called_with( @@ -325,8 +321,6 @@ def test_service_set_enable(self): # Set expected values kwargs = { - 'name': identity_fakes.service_name, - 'type': identity_fakes.service_type, 'enabled': True, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) @@ -354,8 +348,6 @@ def test_service_set_disable(self): # Set expected values kwargs = { - 'name': identity_fakes.service_name, - 'type': identity_fakes.service_type, 'enabled': False, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) From c3c6edbe8a083aef0fb6aea3cb461ff8e715fc59 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 9 Oct 2014 15:16:07 -0500 Subject: [PATCH 0034/3303] Add plugin to support token-endpoint auth The ksc auth plugins do not have support for the original token-endpoint (aka token flow) auth where the user supplies a token (possibly the Keystone admin_token) and an API endpoint. This is used for bootstrapping Keystone but also has other uses when a scoped user token is provided. The api.auth:TokenEndpoint class is required to provide the same interface methods so all of the special-case code branches to support token-endpoint can be removed. Some additional cleanups related to ClientManager and creating the Compute client also were done to streamline using sessions. Change-Id: I1a6059afa845a591eff92567ca346c09010a93af --- openstackclient/api/auth.py | 69 ++++++++++++++++--- openstackclient/common/clientmanager.py | 41 ++++++----- openstackclient/compute/client.py | 21 ++---- .../tests/common/test_clientmanager.py | 48 +++++++++---- setup.cfg | 3 + 5 files changed, 124 insertions(+), 58 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 2bd5271f7d..e33b72d575 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -18,6 +18,8 @@ import stevedore +from oslo.config import cfg + from keystoneclient.auth import base from openstackclient.common import exceptions as exc @@ -53,14 +55,14 @@ ) -def _guess_authentication_method(options): +def select_auth_plugin(options): """If no auth plugin was specified, pick one based on other options""" - if options.os_url: - # service token authentication, do nothing - return auth_plugin = None - if options.os_password: + if options.os_url and options.os_token: + # service token authentication + auth_plugin = 'token_endpoint' + elif options.os_password: if options.os_identity_api_version == '3': auth_plugin = 'v3password' elif options.os_identity_api_version == '2.0': @@ -83,14 +85,13 @@ def _guess_authentication_method(options): ) LOG.debug("No auth plugin selected, picking %s from other " "options" % auth_plugin) - options.os_auth_plugin = auth_plugin + return auth_plugin def build_auth_params(cmd_options): auth_params = {} - if cmd_options.os_url: - return {'token': cmd_options.os_token} if cmd_options.os_auth_plugin: + LOG.debug('auth_plugin: %s', cmd_options.os_auth_plugin) auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) plugin_options = auth_plugin.get_options() for option in plugin_options: @@ -110,6 +111,7 @@ def build_auth_params(cmd_options): None, ) else: + LOG.debug('no auth_plugin') # delay the plugin choice, grab every option plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) for option in plugin_options: @@ -178,3 +180,54 @@ def build_auth_plugins_option_parser(parser): help=argparse.SUPPRESS, ) return parser + + +class TokenEndpoint(base.BaseAuthPlugin): + """Auth plugin to handle traditional token/endpoint usage + + Implements the methods required to handle token authentication + with a user-specified token and service endpoint; no Identity calls + are made for re-scoping, service catalog lookups or the like. + + The purpose of this plugin is to get rid of the special-case paths + in the code to handle this authentication format. Its primary use + is for bootstrapping the Keystone database. + """ + + def __init__(self, url, token, **kwargs): + """A plugin for static authentication with an existing token + + :param string url: Service endpoint + :param string token: Existing token + """ + super(TokenEndpoint, self).__init__() + self.endpoint = url + self.token = token + + def get_endpoint(self, session, **kwargs): + """Return the supplied endpoint""" + return self.endpoint + + def get_token(self, session): + """Return the supplied token""" + return self.token + + def get_auth_ref(self, session, **kwargs): + """Stub this method for compatibility""" + return None + + # Override this because it needs to be a class method... + @classmethod + def get_options(self): + options = super(TokenEndpoint, self).get_options() + + options.extend([ + # Maintain name 'url' for compatibility + cfg.StrOpt('url', + help='Specific service endpoint to use'), + cfg.StrOpt('token', + secret=True, + help='Authentication token to use'), + ]) + + return options diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..bcb81990ad 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -54,9 +54,10 @@ def __getattr__(self, name): return self._auth_params[name[1:]] def __init__(self, auth_options, api_version=None, verify=True): - + # If no plugin is named by the user, select one based on + # the supplied options if not auth_options.os_auth_plugin: - auth._guess_authentication_method(auth_options) + auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options) self._auth_plugin = auth_options.os_auth_plugin self._url = auth_options.os_url @@ -66,7 +67,7 @@ def __init__(self, auth_options, api_version=None, verify=True): self._service_catalog = None self.timing = auth_options.timing - # For compatability until all clients can be updated + # For compatibility until all clients can be updated if 'project_name' in self._auth_params: self._project_name = self._auth_params['project_name'] elif 'tenant_name' in self._auth_params: @@ -86,27 +87,25 @@ def __init__(self, auth_options, api_version=None, verify=True): root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - self.session = None - if not self._url: - LOG.debug('Using auth plugin: %s' % self._auth_plugin) - auth_plugin = base.get_plugin_class(self._auth_plugin) - self.auth = auth_plugin.load_from_options(**self._auth_params) - # needed by SAML authentication - request_session = requests.session() - self.session = session.Session( - auth=self.auth, - session=request_session, - verify=verify, - ) + LOG.debug('Using auth plugin: %s' % self._auth_plugin) + auth_plugin = base.get_plugin_class(self._auth_plugin) + self.auth = auth_plugin.load_from_options(**self._auth_params) + # needed by SAML authentication + request_session = requests.session() + self.session = session.Session( + auth=self.auth, + session=request_session, + verify=verify, + ) self.auth_ref = None - if not self._auth_plugin.endswith("token") and not self._url: - LOG.debug("Populate other password flow attributes") - self.auth_ref = self.session.auth.get_auth_ref(self.session) - self._token = self.session.auth.get_token(self.session) + if 'token' not in self._auth_params: + LOG.debug("Get service catalog") + self.auth_ref = self.auth.get_auth_ref(self.session) self._service_catalog = self.auth_ref.service_catalog - else: - self._token = self._auth_params.get('token') + + # This begone when clients no longer need it... + self._token = self.auth.get_token(self.session) return diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index dc50507eb2..6c03d24e3f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -44,33 +44,20 @@ def make_client(instance): extensions = [extension.Extension('list_extensions', list_extensions)] client = compute_client( - username=instance._username, - api_key=instance._password, - project_id=instance._project_name, - auth_url=instance._auth_url, - cacert=instance._cacert, - insecure=instance._insecure, - region_name=instance._region_name, - # FIXME(dhellmann): get endpoint_type from option? - endpoint_type='publicURL', + session=instance.session, extensions=extensions, - service_type=API_NAME, - # FIXME(dhellmann): what is service_name? - service_name='', http_log_debug=http_log_debug, timings=instance.timing, ) # Populate the Nova client to skip another auth query to Identity - if instance._url: - # token flow - client.client.management_url = instance._url - else: + if 'token' not in instance._auth_params: # password flow client.client.management_url = instance.get_endpoint_for_service_type( API_NAME) client.client.service_catalog = instance._service_catalog - client.client.auth_token = instance._token + client.client.auth_token = instance.auth.get_token(instance.session) + return client diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 18461fb7e8..5ec86d595d 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -76,6 +76,31 @@ def setUp(self): url=fakes.AUTH_URL, verb='GET') + def test_client_manager_token_endpoint(self): + + client_manager = clientmanager.ClientManager( + auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, + os_url=fakes.AUTH_URL, + os_auth_plugin='token_endpoint'), + api_version=API_VERSION, + verify=True + ) + self.assertEqual( + fakes.AUTH_URL, + client_manager._url, + ) + + self.assertEqual( + fakes.AUTH_TOKEN, + client_manager._token, + ) + self.assertIsInstance( + client_manager.auth, + auth.TokenEndpoint, + ) + self.assertFalse(client_manager._insecure) + self.assertTrue(client_manager._verify) + def test_client_manager_token(self): client_manager = clientmanager.ClientManager( @@ -176,8 +201,7 @@ def test_client_manager_password_verify_ca(self): self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) - def _client_manager_guess_auth_plugin(self, auth_params, - api_version, auth_plugin): + def _select_auth_plugin(self, auth_params, api_version, auth_plugin): auth_params['os_auth_plugin'] = auth_plugin auth_params['os_identity_api_version'] = api_version client_manager = clientmanager.ClientManager( @@ -190,25 +214,25 @@ def _client_manager_guess_auth_plugin(self, auth_params, client_manager._auth_plugin, ) - def test_client_manager_guess_auth_plugin(self): + def test_client_manager_select_auth_plugin(self): # test token auth params = dict(os_token=fakes.AUTH_TOKEN, os_auth_url=fakes.AUTH_URL) - self._client_manager_guess_auth_plugin(params, '2.0', 'v2token') - self._client_manager_guess_auth_plugin(params, '3', 'v3token') - self._client_manager_guess_auth_plugin(params, 'XXX', 'token') - # test service auth + self._select_auth_plugin(params, '2.0', 'v2token') + self._select_auth_plugin(params, '3', 'v3token') + self._select_auth_plugin(params, 'XXX', 'token') + # test token/endpoint auth params = dict(os_token=fakes.AUTH_TOKEN, os_url='test') - self._client_manager_guess_auth_plugin(params, 'XXX', '') + self._select_auth_plugin(params, 'XXX', 'token_endpoint') # test password auth params = dict(os_auth_url=fakes.AUTH_URL, os_username=fakes.USERNAME, os_password=fakes.PASSWORD) - self._client_manager_guess_auth_plugin(params, '2.0', 'v2password') - self._client_manager_guess_auth_plugin(params, '3', 'v3password') - self._client_manager_guess_auth_plugin(params, 'XXX', 'password') + self._select_auth_plugin(params, '2.0', 'v2password') + self._select_auth_plugin(params, '3', 'v3password') + self._select_auth_plugin(params, 'XXX', 'password') - def test_client_manager_guess_auth_plugin_failure(self): + def test_client_manager_select_auth_plugin_failure(self): self.assertRaises(exc.CommandError, clientmanager.ClientManager, auth_options=FakeOptions(os_auth_plugin=''), diff --git a/setup.cfg b/setup.cfg index dc85967f42..d9cced1523 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,9 @@ packages = console_scripts = openstack = openstackclient.shell:main +keystoneclient.auth.plugin = + token_endpoint = openstackclient.api.auth:TokenEndpoint + openstack.cli = module_list = openstackclient.common.module:ListModule From a388ce40f444330c0f7d5665a74761a9f58baa44 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 12 Oct 2014 09:16:38 -0700 Subject: [PATCH 0035/3303] Put pbr and six first in requirements list Each of them have scenarios where it's important that they install first. Change-Id: Ia9fff6e94651693f9fa74ca676771b19144bcd8b --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2bbe09d8b1..3fcde32154 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,17 @@ # 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>=0.6,!=0.7,<1.0 +six>=1.7.0 + Babel>=1.3 cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 -pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=2.2.0,!=2.4.0 -six>=1.7.0 stevedore>=1.0.0 # Apache-2.0 From bcf4b3caece9edc6a04be3ce59697301e48ad34a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 8 Oct 2014 23:22:24 -0500 Subject: [PATCH 0036/3303] Update use of open() in object API * Switch to use io.open() for py3 compatibility and simpler testing. * Open files in 'rb' mode to avoid translation on Windows Previously tests simply relied on files that were present in the repository to run tests using open(). Change the filenames to ensure that no longer happens. requests_mock doesn't have a way to match against the request body for PUT/POST; an attempt to add a new Matcher to do that worked but it needs to subclass the currently private adapter._Matcher class or duplicate most of its functionality. Change-Id: I8c30b41db20af8ecafe67e760e872fc08adec905 --- openstackclient/api/object_store_v1.py | 8 ++++++- .../tests/api/test_object_store_v1.py | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 57db906398..c52eeb3ac9 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -13,6 +13,7 @@ """Object Store v1 API Library""" +import io import os import six @@ -187,7 +188,12 @@ def object_create( return {} full_url = "%s/%s" % (container, object) - response = self.create(full_url, method='PUT', data=open(object)) + with io.open(object, 'rb') as f: + response = self.create( + full_url, + method='PUT', + data=f, + ) url_parts = urlparse(self.endpoint) data = { 'account': url_parts.path.split('/')[-1], diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py index 5a376a454b..b18a003db5 100644 --- a/openstackclient/tests/api/test_object_store_v1.py +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -13,6 +13,8 @@ """Object Store v1 API Library Tests""" +import mock + from requests_mock.contrib import fixture from keystoneclient import session @@ -175,30 +177,41 @@ class TestObject(TestObjectAPIv1): def setUp(self): super(TestObject, self).setUp() - def test_object_create(self): + @mock.patch('openstackclient.api.object_store_v1.io.open') + def base_object_create(self, file_contents, mock_open): + mock_open.read.return_value = file_contents + headers = { 'etag': 'youreit', 'x-trans-id': '1qaz2wsx', } + # TODO(dtroyer): When requests_mock gains the ability to + # match against request.body add this check + # https://review.openstack.org/127316 self.requests_mock.register_uri( 'PUT', - FAKE_URL + '/qaz/requirements.txt', + FAKE_URL + '/qaz/counter.txt', headers=headers, + # body=file_contents, status_code=201, ) ret = self.api.object_create( container='qaz', - object='requirements.txt', + object='counter.txt', ) data = { 'account': FAKE_ACCOUNT, 'container': 'qaz', - 'object': 'requirements.txt', + 'object': 'counter.txt', 'etag': 'youreit', 'x-trans-id': '1qaz2wsx', } self.assertEqual(data, ret) + def test_object_create(self): + self.base_object_create('111\n222\n333\n') + self.base_object_create(bytes([0x31, 0x00, 0x0d, 0x0a, 0x7f, 0xff])) + def test_object_delete(self): self.requests_mock.register_uri( 'DELETE', From 897418edca52d9856ef7381a5822fce3bcf8a804 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Oct 2014 11:13:48 -0500 Subject: [PATCH 0037/3303] Move plugin stuff to clientmanager The OSC plugins work by adding an object as an attribute to a ClientManager instance. The initialization and management of thos plugins belongs in clientmanager.py. At this point the only part not moved is the API version dict initialization bcause the timing and connection to the CommandManager initialization. It gets refactored anyway when API discovery becomes operational. Change-Id: If9cb9a0c45a3a577082a5cdbb793769211f20ebb --- openstackclient/common/clientmanager.py | 30 ++++++++++++++++++++++--- openstackclient/shell.py | 18 +++------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..09c5c25c7a 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -29,6 +29,8 @@ LOG = logging.getLogger(__name__) +PLUGIN_MODULES = [] + class ClientCache(object): """Descriptor class for caching created client handles.""" @@ -123,11 +125,13 @@ def get_endpoint_for_service_type(self, service_type): return endpoint -def get_extension_modules(group): - """Add extension clients""" +# Plugin Support + +def get_plugin_modules(group): + """Find plugin entry points""" mod_list = [] for ep in pkg_resources.iter_entry_points(group): - LOG.debug('found extension %r', ep.name) + LOG.debug('Found plugin %r', ep.name) __import__(ep.module_name) module = sys.modules[ep.module_name] @@ -136,6 +140,7 @@ def get_extension_modules(group): if init_func: init_func('x') + # Add the plugin to the ClientManager setattr( ClientManager, module.API_NAME, @@ -144,3 +149,22 @@ def get_extension_modules(group): ), ) return mod_list + + +def build_plugin_option_parser(parser): + """Add plugin options to the parser""" + + # Loop through extensions to get parser additions + for mod in PLUGIN_MODULES: + parser = mod.build_option_parser(parser) + return parser + + +# Get list of base plugin modules +PLUGIN_MODULES = get_plugin_modules( + 'openstack.cli.base', +) +# Append list of external plugin modules +PLUGIN_MODULES.extend(get_plugin_modules( + 'openstack.cli.extension', +)) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 626e3f7da5..1f9eb32b53 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -68,19 +68,6 @@ def __init__(self): # Assume TLS host certificate verification is enabled self.verify = True - # Get list of base modules - self.ext_modules = clientmanager.get_extension_modules( - 'openstack.cli.base', - ) - # Append list of extension modules - self.ext_modules.extend(clientmanager.get_extension_modules( - 'openstack.cli.extension', - )) - - # Loop through extensions to get parser additions - for mod in self.ext_modules: - self.parser = mod.build_option_parser(self.parser) - # NOTE(dtroyer): This hack changes the help action that Cliff # automatically adds to the parser so we can defer # its execution until after the api-versioned commands @@ -170,6 +157,7 @@ def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( description, version) + # service token auth argument parser.add_argument( '--os-url', @@ -214,7 +202,7 @@ def build_option_parser(self, description, version): help="Print API call timing info", ) - return parser + return clientmanager.build_plugin_option_parser(parser) def authenticate_user(self): """Verify the required authentication credentials are present""" @@ -332,7 +320,7 @@ def initialize_app(self, argv): self.default_domain = self.options.os_default_domain # Loop through extensions to get API versions - for mod in self.ext_modules: + for mod in clientmanager.PLUGIN_MODULES: version_opt = getattr(self.options, mod.API_VERSION_OPTION, None) if version_opt: api = mod.API_NAME From ca783f46595a7d90cea0ad8491b65aa5f9370a04 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Oct 2014 22:40:11 -0500 Subject: [PATCH 0038/3303] Close files on image create The file opened for --file was never closed. Close it if it is a file object. Change-Id: I7bd120a2413de42339771d01e8fd1894d38c3011 --- openstackclient/image/v1/image.py | 53 +++++++++++--------- openstackclient/tests/image/v1/test_image.py | 17 +++++-- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 465e9d7b15..32dd388c0f 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -15,6 +15,7 @@ """Image V1 Action Implementations""" +import io import logging import os import six @@ -214,10 +215,9 @@ def take_action(self, parsed_args): elif parsed_args.file: # Send an open file handle to glanceclient so it will # do a chunked transfer - kwargs["data"] = open(parsed_args.file, "rb") + kwargs["data"] = io.open(parsed_args.file, "rb") else: # Read file from stdin - kwargs["data"] = None if sys.stdin.isatty() is not True: if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) @@ -225,29 +225,36 @@ def take_action(self, parsed_args): # do a chunked transfer kwargs["data"] = sys.stdin + # Wrap the call to catch exceptions in order to close files try: - image = utils.find_resource( - image_client.images, - parsed_args.name, - ) - - # Preserve previous properties if any are being set now - if image.properties: - if parsed_args.properties: - image.properties.update(kwargs['properties']) - kwargs['properties'] = image.properties - - except exceptions.CommandError: - if not parsed_args.volume: - # This is normal for a create or reserve (create w/o an image) - # But skip for create from volume - image = image_client.images.create(**kwargs) - else: - # Update an existing reservation + try: + image = utils.find_resource( + image_client.images, + parsed_args.name, + ) - # If an image is specified via --file, --location or - # --copy-from let the API handle it - image = image_client.images.update(image.id, **kwargs) + # Preserve previous properties if any are being set now + if image.properties: + if parsed_args.properties: + image.properties.update(kwargs['properties']) + kwargs['properties'] = image.properties + + except exceptions.CommandError: + if not parsed_args.volume: + # This is normal for a create or reserve (create w/o + # an image), but skip for create from volume + image = image_client.images.create(**kwargs) + else: + # Update an existing reservation + + # If an image is specified via --file, --location or + # --copy-from let the API handle it + image = image_client.images.update(image.id, **kwargs) + finally: + # Clean up open files - make sure data isn't a string + if ('data' in kwargs and hasattr(kwargs['data'], 'close') and + kwargs['data'] != sys.stdin): + kwargs['data'].close() info = {} info.update(image._info) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index 3f97b151c2..a05669300e 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -139,14 +139,17 @@ def test_image_reserve_options(self): self.assertEqual(image_fakes.IMAGE_columns, columns) self.assertEqual(image_fakes.IMAGE_data, data) - @mock.patch('six.moves.builtins.open') - def test_image_create_file(self, open_mock): + @mock.patch('openstackclient.image.v1.image.io.open', name='Open') + def test_image_create_file(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = image_fakes.image_data mock_exception = { 'find.side_effect': exceptions.CommandError('x'), 'get.side_effect': exceptions.CommandError('x'), } self.images_mock.configure_mock(**mock_exception) - open_mock.return_value = image_fakes.image_data + arglist = [ '--file', 'filer', '--unprotected', @@ -169,7 +172,11 @@ def test_image_create_file(self, open_mock): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - open_mock.assert_called_with('filer', 'rb') + # Ensure input file is opened + mock_open.assert_called_with('filer', 'rb') + + # Ensure the input file is closed + mock_file.close.assert_called_with() # ImageManager.get(name) self.images_mock.get.assert_called_with(image_fakes.image_name) @@ -185,7 +192,7 @@ def test_image_create_file(self, open_mock): 'Alpha': '1', 'Beta': '2', }, - data=image_fakes.image_data, + data=mock_file, ) # Verify update() was not called, if it was show the args From 89217a6557e16872ac1af0e305ac09886a9e1255 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Oct 2014 16:30:38 -0500 Subject: [PATCH 0039/3303] Close files on server create, add tests The files opened for the --files and --user-data options were never closed, potentially leaking memory in a long-running client. Close them if they are file objects. Add a couple of basic tests for server create. Change-Id: I1658b0caa2d6af17308149cb52196ee28266ddf2 --- openstackclient/compute/v2/server.py | 17 +- openstackclient/tests/compute/v2/fakes.py | 1 + .../tests/compute/v2/test_server.py | 174 ++++++++++++++++++ openstackclient/tests/utils.py | 6 +- 4 files changed, 194 insertions(+), 4 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 355774c316..a6d645b9e9 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -17,6 +17,7 @@ import argparse import getpass +import io import logging import os import six @@ -296,7 +297,7 @@ def take_action(self, parsed_args): for f in parsed_args.file: dst, src = f.split('=', 1) try: - files[dst] = open(src) + files[dst] = io.open(src, 'rb') except IOError as e: raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) @@ -313,7 +314,7 @@ def take_action(self, parsed_args): userdata = None if parsed_args.user_data: try: - userdata = open(parsed_args.user_data) + userdata = io.open(parsed_args.user_data) except IOError as e: msg = "Can't open '%s': %s" raise exceptions.CommandError(msg % (parsed_args.user_data, e)) @@ -368,7 +369,17 @@ def take_action(self, parsed_args): self.log.debug('boot_args: %s', boot_args) self.log.debug('boot_kwargs: %s', boot_kwargs) - server = compute_client.servers.create(*boot_args, **boot_kwargs) + + # Wrap the call to catch exceptions in order to close files + try: + server = compute_client.servers.create(*boot_args, **boot_kwargs) + finally: + # Clean up open files - make sure they are not strings + for f in files: + if hasattr(f, 'close'): + f.close() + if hasattr(userdata, 'close'): + userdata.close() if parsed_args.wait: if utils.wait_for_status( diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index 9a7964db0d..e5ed5cbd7d 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -26,6 +26,7 @@ SERVER = { 'id': server_id, 'name': server_name, + 'metadata': {}, } extension_name = 'Multinic' diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index a98cd15690..50de5c6ab7 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -14,11 +14,13 @@ # import copy +import mock from openstackclient.compute.v2 import server from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes from openstackclient.tests.image.v2 import fakes as image_fakes +from openstackclient.tests import utils class TestServer(compute_fakes.TestComputev2): @@ -30,6 +32,10 @@ def setUp(self): self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() + # Get a shortcut to the ImageManager Mock + self.cimages_mock = self.app.client_manager.compute.images + self.cimages_mock.reset_mock() + # Get a shortcut to the FlavorManager Mock self.flavors_mock = self.app.client_manager.compute.flavors self.flavors_mock.reset_mock() @@ -39,6 +45,174 @@ def setUp(self): self.images_mock.reset_mock() +class TestServerCreate(TestServer): + + def setUp(self): + super(TestServerCreate, self).setUp() + + self.servers_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVER), + loaded=True, + ) + new_server = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVER), + loaded=True, + ) + new_server.__dict__['networks'] = {} + self.servers_mock.get.return_value = new_server + + self.image = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + self.cimages_mock.get.return_value = self.image + + self.flavor = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.FLAVOR), + loaded=True, + ) + self.flavors_mock.get.return_value = self.flavor + + # Get the command object to test + self.cmd = server.CreateServer(self.app, None) + + def test_server_create_no_options(self): + arglist = [ + compute_fakes.server_id, + ] + verifylist = [ + ('server_name', compute_fakes.server_id), + ] + try: + # Missing required args should bail here + self.check_parser(self.cmd, arglist, verifylist) + except utils.ParserException: + pass + + def test_server_create_minimal(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + compute_fakes.server_id, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('server_name', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping={}, + nics=[], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + compute_fakes.server_id, + self.image, + self.flavor, + **kwargs + ) + + collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') + self.assertEqual(columns, collist) + datalist = ( + '', + 'Large ()', + compute_fakes.server_id, + 'graven ()', + compute_fakes.server_name, + '', + ) + self.assertEqual(data, datalist) + + @mock.patch('openstackclient.compute.v2.server.io.open') + def test_server_create_userdata(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = '#!/bin/sh' + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--user-data', 'userdata.sh', + compute_fakes.server_id, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('user_data', 'userdata.sh'), + ('config_drive', False), + ('server_name', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Ensure the userdata file is opened + mock_open.assert_called_with('userdata.sh') + + # Ensure the userdata file is closed + mock_file.close.assert_called() + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=mock_file, + key_name=None, + availability_zone=None, + block_device_mapping={}, + nics=[], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + compute_fakes.server_id, + self.image, + self.flavor, + **kwargs + ) + + collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') + self.assertEqual(columns, collist) + datalist = ( + '', + 'Large ()', + compute_fakes.server_id, + 'graven ()', + compute_fakes.server_name, + '', + ) + self.assertEqual(data, datalist) + + class TestServerDelete(TestServer): def setUp(self): diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index 38d4725038..25f9852516 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -23,6 +23,10 @@ from openstackclient.tests import fakes +class ParserException(Exception): + pass + + class TestCase(testtools.TestCase): def setUp(self): testtools.TestCase.setUp(self) @@ -84,7 +88,7 @@ def check_parser(self, cmd, args, verify_args): try: parsed_args = cmd_parser.parse_args(args) except SystemExit: - raise Exception("Argument parse failed") + raise ParserException("Argument parse failed") for av in verify_args: attr, value = av if attr: From deda02331474632c47b88c166241cd65b2952269 Mon Sep 17 00:00:00 2001 From: wanghong Date: Fri, 17 Oct 2014 11:36:53 +0800 Subject: [PATCH 0040/3303] use jsonutils in oslo.serialization instead of keystoneclient keystoneclient/openstack/common/jsonutils.py is removed in this patch https://review.openstack.org/#/c/128454/ Now, we should use jsonutils in oslo.serialization package. Change-Id: I7c8e8e6d5dffa85244368fd578616c9b19f4fd21 --- openstackclient/tests/common/test_clientmanager.py | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 18461fb7e8..9e9749c6a4 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -16,8 +16,8 @@ from requests_mock.contrib import fixture from keystoneclient.auth.identity import v2 as auth_v2 -from keystoneclient.openstack.common import jsonutils from keystoneclient import service_catalog +from oslo.serialization import jsonutils from openstackclient.api import auth from openstackclient.common import clientmanager diff --git a/requirements.txt b/requirements.txt index 3fcde32154..54ea795079 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ Babel>=1.3 cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 +oslo.serialization>=1.0.0 # Apache-2.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 From 0de67016c7daa1712b568cb2e49728fac3eb57ad Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 Oct 2014 22:26:57 -0500 Subject: [PATCH 0041/3303] Remove now-unnecessary client creation hacks Clients that can use ksc Session don't need the old junk to fake auth anymore: * compute * volume Clients that still need to be fed credentials can pick directly from the auth object in clientmanager. The _token attribute is removed, the token can be retrieved from the auth object: openstackclient/tests/common/test_clientmanager.py This change will break any plugin that relies on getting a token from instance._token. They should be updated to use the above, or preferable, to use keystoneclient.session.Session to create its HTTP interface object. Change-Id: I877a29de97a42f85f12a14c274fc003e6fba5135 --- openstackclient/common/clientmanager.py | 3 --- openstackclient/compute/client.py | 9 +-------- openstackclient/image/client.py | 2 +- openstackclient/network/client.py | 2 +- .../tests/common/test_clientmanager.py | 11 +--------- openstackclient/volume/client.py | 20 ++----------------- 6 files changed, 6 insertions(+), 41 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 928ab6eea6..336c0da0e9 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -106,9 +106,6 @@ def __init__(self, auth_options, api_version=None, verify=True): self.auth_ref = self.auth.get_auth_ref(self.session) self._service_catalog = self.auth_ref.service_catalog - # This begone when clients no longer need it... - self._token = self.auth.get_token(self.session) - return def get_endpoint_for_service_type(self, service_type, region_name=None): diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index ff9ed88a0a..c87bbee700 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -43,6 +43,7 @@ def make_client(instance): http_log_debug = utils.get_effective_log_level() <= logging.DEBUG extensions = [extension.Extension('list_extensions', list_extensions)] + client = compute_client( session=instance.session, extensions=extensions, @@ -50,14 +51,6 @@ def make_client(instance): timings=instance.timing, ) - # Populate the Nova client to skip another auth query to Identity - if 'token' not in instance._auth_params: - # password flow - client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME, region_name=instance._region_name) - client.client.service_catalog = instance._service_catalog - client.client.auth_token = instance.auth.get_token(instance.session) - return client diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index a23d349e01..84f5943754 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -45,7 +45,7 @@ def make_client(instance): return image_client( instance._url, - token=instance._token, + token=instance.auth.get_token(instance.session), cacert=instance._cacert, insecure=instance._insecure, ) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 870fdad1db..e4ce2f6a08 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -44,7 +44,7 @@ def make_client(instance): region_name=instance._region_name, auth_url=instance._auth_url, endpoint_url=instance._url, - token=instance._token, + token=instance.auth.get_token(instance.session), insecure=instance._insecure, ca_cert=instance._cacert, ) diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index d0738c79bf..24adfa0e0b 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -89,10 +89,9 @@ def test_client_manager_token_endpoint(self): fakes.AUTH_URL, client_manager._url, ) - self.assertEqual( fakes.AUTH_TOKEN, - client_manager._token, + client_manager.auth.get_token(None), ) self.assertIsInstance( client_manager.auth, @@ -111,10 +110,6 @@ def test_client_manager_token(self): verify=True ) - self.assertEqual( - fakes.AUTH_TOKEN, - client_manager._token, - ) self.assertEqual( fakes.AUTH_URL, client_manager._auth_url, @@ -160,10 +155,6 @@ def test_client_manager_password(self): AUTH_REF, client_manager.auth_ref, ) - self.assertEqual( - fakes.AUTH_TOKEN, - client_manager._token, - ) self.assertEqual( dir(SERVICE_CATALOG), dir(client_manager._service_catalog), diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 58cb267e53..f4e2decb35 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -49,29 +49,13 @@ def make_client(instance): http_log_debug = utils.get_effective_log_level() <= logging.DEBUG extensions = [extension.Extension('list_extensions', list_extensions)] + client = volume_client( - username=instance._username, - api_key=instance._password, - project_id=instance._project_name, - auth_url=instance._auth_url, - cacert=instance._cacert, - insecure=instance._insecure, - region_name=instance._region_name, + session=instance.session, extensions=extensions, http_log_debug=http_log_debug, ) - # Populate the Cinder client to skip another auth query to Identity - if instance._url: - # token flow - client.client.management_url = instance._url - else: - # password flow - client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME, region_name=instance._region_name) - client.client.service_catalog = instance._service_catalog - client.client.auth_token = instance._token - return client From 2166d7d3afbbdc1659e4cffdb7bcd890cd00ec19 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 Oct 2014 23:43:38 -0500 Subject: [PATCH 0042/3303] Remove ClientManager._service_catalog Anything that needs a service catalog can get it directly from auth_ref.service_catalog, no need to carry the extra attribute. ClientManager.get_endpoint_for_service_type() reamins the proper method to get an endpoint for clients that still need one directly. Change-Id: I809091c9c71d08f29606d7fd8b500898ff2cb8ae --- openstackclient/common/clientmanager.py | 14 +++++------ openstackclient/identity/client.py | 24 ++++--------------- openstackclient/identity/v2_0/service.py | 3 ++- openstackclient/image/client.py | 8 ++++--- openstackclient/network/client.py | 10 ++++---- openstackclient/object/client.py | 8 +++---- .../tests/common/test_clientmanager.py | 2 +- 7 files changed, 29 insertions(+), 40 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 336c0da0e9..febcedf4b1 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -66,7 +66,6 @@ def __init__(self, auth_options, api_version=None, verify=True): self._auth_params = auth.build_auth_params(auth_options) self._region_name = auth_options.os_region_name self._api_version = api_version - self._service_catalog = None self.timing = auth_options.timing # For compatibility until all clients can be updated @@ -104,7 +103,6 @@ def __init__(self, auth_options, api_version=None, verify=True): if 'token' not in self._auth_params: LOG.debug("Get service catalog") self.auth_ref = self.auth.get_auth_ref(self.session) - self._service_catalog = self.auth_ref.service_catalog return @@ -112,12 +110,14 @@ def get_endpoint_for_service_type(self, service_type, region_name=None): """Return the endpoint URL for the service type.""" # See if we are using password flow auth, i.e. we have a # service catalog to select endpoints from - if self._service_catalog: - endpoint = self._service_catalog.url_for( - service_type=service_type, region_name=region_name) + if self.auth_ref: + endpoint = self.auth_ref.service_catalog.url_for( + service_type=service_type, + region_name=region_name, + ) else: - # Hope we were given the correct URL. - endpoint = self._auth_url or self._url + # Get the passed endpoint directly from the auth plugin + endpoint = self.auth.get_endpoint(self.session) return endpoint diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index bc10a6d237..8050d12096 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -44,27 +44,11 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating identity client: %s', identity_client) - # TODO(dtroyer): Something doesn't like the session.auth when using - # token auth, chase that down. - if instance._url: - LOG.debug('Using service token auth') - client = identity_client( - endpoint=instance._url, - token=instance._auth_params['token'], - cacert=instance._cacert, - insecure=instance._insecure - ) - else: - LOG.debug('Using auth plugin: %s' % instance._auth_plugin) - client = identity_client( - session=instance.session, - cacert=instance._cacert, - ) + LOG.debug('Using auth plugin: %s' % instance._auth_plugin) + client = identity_client( + session=instance.session, + ) - # TODO(dtroyer): the identity v2 role commands use this yet, fix that - # so we can remove it - if not instance._url: - instance.auth_ref = instance.auth.get_auth_ref(instance.session) return client diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 458dce7cba..e8848ddee2 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -141,9 +141,10 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref if parsed_args.catalog: - endpoints = identity_client.service_catalog.get_endpoints( + endpoints = auth_ref.service_catalog.get_endpoints( service_type=parsed_args.service) for (service, service_endpoints) in six.iteritems(endpoints): if service_endpoints: diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 84f5943754..c55ff85369 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -40,11 +40,13 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating image client: %s', image_client) - if not instance._url: - instance._url = instance.get_endpoint_for_service_type(API_NAME) + endpoint = instance.get_endpoint_for_service_type( + API_NAME, + region_name=instance._region_name, + ) return image_client( - instance._url, + endpoint, token=instance.auth.get_token(instance.session), cacert=instance._cacert, insecure=instance._insecure, diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index e4ce2f6a08..bb3e1b2397 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -34,16 +34,18 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating network client: %s', network_client) - if not instance._url: - instance._url = instance.get_endpoint_for_service_type( - "network", region_name=instance._region_name) + endpoint = instance.get_endpoint_for_service_type( + API_NAME, + region_name=instance._region_name, + ) + return network_client( username=instance._username, tenant_name=instance._project_name, password=instance._password, region_name=instance._region_name, auth_url=instance._auth_url, - endpoint_url=instance._url, + endpoint_url=endpoint, token=instance.auth.get_token(instance.session), insecure=instance._insecure, ca_cert=instance._cacert, diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 1ac905c33e..beb7c04fc1 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -33,10 +33,10 @@ def make_client(instance): """Returns an object-store API client.""" - if instance._url: - endpoint = instance._url - else: - endpoint = instance.get_endpoint_for_service_type("object-store") + endpoint = instance.get_endpoint_for_service_type( + 'object-store', + region_name=instance._region_name, + ) client = object_store_v1.APIv1( session=instance.session, diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 24adfa0e0b..a7b13c6c9c 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -157,7 +157,7 @@ def test_client_manager_password(self): ) self.assertEqual( dir(SERVICE_CATALOG), - dir(client_manager._service_catalog), + dir(client_manager.auth_ref.service_catalog), ) def stub_auth(self, json=None, url=None, verb=None, **kwargs): From f600c0eafbf9aff23b6efa1c7b94797a25873b99 Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 20 Oct 2014 15:29:53 +0800 Subject: [PATCH 0043/3303] only generate one clientmanager instance in interactive mode Currently, we repeated to generate clientmanager instance when run command in interactive mode. This should be avoided. Change-Id: I0536a690bc173be38af08a2e4443115532041efd Closes-Bug: #1383083 --- openstackclient/shell.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 1f9eb32b53..668e48b5fc 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -68,6 +68,8 @@ def __init__(self): # Assume TLS host certificate verification is enabled self.verify = True + self.client_manager = None + # NOTE(dtroyer): This hack changes the help action that Cliff # automatically adds to the parser so we can defer # its execution until after the api-versioned commands @@ -204,8 +206,12 @@ def build_option_parser(self, description, version): return clientmanager.build_plugin_option_parser(parser) - def authenticate_user(self): - """Verify the required authentication credentials are present""" + def initialize_clientmanager(self): + """Validating authentication options and generate a clientmanager""" + + if self.client_manager: + self.log.debug('The clientmanager has been initialized already') + return self.log.debug("validating authentication options") @@ -370,11 +376,11 @@ def prepare_to_run_command(self, cmd): return if cmd.best_effort: try: - self.authenticate_user() + self.initialize_clientmanager() except Exception: pass else: - self.authenticate_user() + self.initialize_clientmanager() return def clean_up(self, cmd, result, err): @@ -409,7 +415,7 @@ def clean_up(self, cmd, result, err): def interact(self): # NOTE(dtroyer): Maintain the old behaviour for interactive use as # this path does not call prepare_to_run_command() - self.authenticate_user() + self.initialize_clientmanager() super(OpenStackShell, self).interact() From cd368bb81690af5b4e99c0fd71b35fb00c9e0786 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 20 Oct 2014 12:47:39 -0500 Subject: [PATCH 0044/3303] Fix token issue after auth changeup IssueToken.take_action() was missed in updating the structure of the ClientManager. Also, TOKEN_WITH_TENANT_ID in v3 is just wrong... Closes-Bug: #1383396 Change-Id: If2dd82a26af1d743ee9df73e0c1aebce497bf22e --- openstackclient/identity/v3/token.py | 4 +--- openstackclient/tests/identity/v3/fakes.py | 4 ++-- openstackclient/tests/identity/v3/test_token.py | 10 ++++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index aca5c66993..5b09b69f61 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -159,9 +159,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - session = self.app.client_manager.identity.session - - token = session.auth.auth_ref.service_catalog.get_token() + token = self.app.client_manager.auth_ref.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 69c2590563..5844d160b9 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -176,10 +176,10 @@ token_expires = '2014-01-01T00:00:00Z' token_id = 'tttttttt-tttt-tttt-tttt-tttttttttttt' -TOKEN_WITH_TENANT_ID = { +TOKEN_WITH_PROJECT_ID = { 'expires': token_expires, 'id': token_id, - 'tenant_id': project_id, + 'project_id': project_id, 'user_id': user_id, } diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index dbe855555c..f43b6f5f24 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -13,6 +13,8 @@ # under the License. # +import mock + from openstackclient.identity.v3 import token from openstackclient.tests.identity.v3 import fakes as identity_fakes @@ -23,9 +25,9 @@ def setUp(self): super(TestToken, self).setUp() # Get a shortcut to the Service Catalog Mock - session = self.app.client_manager.identity.session - self.sc_mock = session.auth.auth_ref.service_catalog - self.sc_mock.reset_mock() + self.sc_mock = mock.Mock() + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.service_catalog = self.sc_mock class TestTokenIssue(TestToken): @@ -40,7 +42,7 @@ def test_token_issue_with_project_id(self): verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.sc_mock.get_token.return_value = \ - identity_fakes.TOKEN_WITH_TENANT_ID + identity_fakes.TOKEN_WITH_PROJECT_ID # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) From e063246b97a7f31a47aca0a5eb36d571f5df7236 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 20 Oct 2014 18:53:10 -0500 Subject: [PATCH 0045/3303] Clean up shell authentication * Remove the auth option checks as the auth plugins will validate their own options * Move the initialization of client_manager to the end of initialize_app() so it is always called. Note that no attempts to actually authenticate occur until the first use of one of the client attributes in client_manager. This leaves initialize_clientmanager() (formerly uathenticate_user()) empty so remove it. * Remove interact() as the client_manager has already been created And there is nothing left. * prepare_to_run_command() is reduced to trigger an authentication attempt for the best_effort auth commands, currently the only one is 'complete'. * Add prompt_for_password() to ask the user to enter a password when necessary. Passed to ClientManager in a new kward pw_func. Bug: 1355838 Change-Id: I9fdec9144c4c84f65aed1cf91ce41fe1895089b2 --- openstackclient/api/auth.py | 2 +- openstackclient/common/clientmanager.py | 46 +++++-- openstackclient/shell.py | 152 ++++++------------------ 3 files changed, 74 insertions(+), 126 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index e33b72d575..f6e99cdcf0 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -62,7 +62,7 @@ def select_auth_plugin(options): if options.os_url and options.os_token: # service token authentication auth_plugin = 'token_endpoint' - elif options.os_password: + elif options.os_username: if options.os_identity_api_version == '3': auth_plugin = 'v3password' elif options.os_identity_api_version == '2.0': diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index febcedf4b1..ae38f16077 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -55,17 +55,46 @@ def __getattr__(self, name): for o in auth.OPTIONS_LIST]: return self._auth_params[name[1:]] - def __init__(self, auth_options, api_version=None, verify=True): + def __init__( + self, + auth_options, + api_version=None, + verify=True, + pw_func=None, + ): + """Set up a ClientManager + + :param auth_options: + Options collected from the command-line, environment, or wherever + :param api_version: + Dict of API versions: key is API name, value is the version + :param verify: + TLS certificate verification; may be a boolean to enable or disable + server certificate verification, or a filename of a CA certificate + bundle to be used in verification (implies True) + :param pw_func: + Callback function for asking the user for a password. The function + takes an optional string for the prompt ('Password: ' on None) and + returns a string containig the password + """ + # If no plugin is named by the user, select one based on # the supplied options if not auth_options.os_auth_plugin: auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options) - self._auth_plugin = auth_options.os_auth_plugin + + # Horrible hack alert...must handle prompt for null password if + # password auth is requested. + if (self._auth_plugin.endswith('password') and + not auth_options.os_password): + auth_options.os_password = pw_func() + self._url = auth_options.os_url self._auth_params = auth.build_auth_params(auth_options) self._region_name = auth_options.os_region_name self._api_version = api_version + self._auth_ref = None self.timing = auth_options.timing # For compatibility until all clients can be updated @@ -99,13 +128,16 @@ def __init__(self, auth_options, api_version=None, verify=True): verify=verify, ) - self.auth_ref = None - if 'token' not in self._auth_params: - LOG.debug("Get service catalog") - self.auth_ref = self.auth.get_auth_ref(self.session) - return + @property + def auth_ref(self): + """Dereference will trigger an auth if it hasn't already""" + if not self._auth_ref: + LOG.debug("Get auth_ref") + self._auth_ref = self.auth.get_auth_ref(self.session) + return self._auth_ref + def get_endpoint_for_service_type(self, service_type, region_name=None): """Return the endpoint URL for the service type.""" # See if we are using password flow auth, i.e. we have a diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 668e48b5fc..e671ecc3dc 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -36,6 +36,30 @@ DEFAULT_DOMAIN = 'default' +def prompt_for_password(prompt=None): + """Prompt user for a password + + Propmpt for a password if stdin is a tty. + """ + + if not prompt: + prompt = 'Password: ' + pw = None + # If stdin is a tty, try prompting for the password + if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): + # Check for Ctl-D + try: + pw = getpass.getpass(prompt) + except EOFError: + pass + # No password because we did't have a tty or nothing was entered + if not pw: + raise exc.CommandError( + "No password entered, or found via --os-password or OS_PASSWORD", + ) + return pw + + class OpenStackShell(app.App): CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' @@ -206,112 +230,6 @@ def build_option_parser(self, description, version): return clientmanager.build_plugin_option_parser(parser) - def initialize_clientmanager(self): - """Validating authentication options and generate a clientmanager""" - - if self.client_manager: - self.log.debug('The clientmanager has been initialized already') - return - - self.log.debug("validating authentication options") - - # Assuming all auth plugins will be named in the same fashion, - # ie vXpluginName - if (not self.options.os_url and - self.options.os_auth_plugin.startswith('v') and - self.options.os_auth_plugin[1] != - self.options.os_identity_api_version[0]): - raise exc.CommandError( - "Auth plugin %s not compatible" - " with requested API version" % self.options.os_auth_plugin - ) - # TODO(mhu) All these checks should be exposed at the plugin level - # or just dropped altogether, as the client instantiation will fail - # anyway - if self.options.os_url and not self.options.os_token: - # service token needed - raise exc.CommandError( - "You must provide a service token via" - " either --os-token or env[OS_TOKEN]") - - if (self.options.os_auth_plugin.endswith('token') and - (self.options.os_token or self.options.os_auth_url)): - # Token flow auth takes priority - if not self.options.os_token: - raise exc.CommandError( - "You must provide a token via" - " either --os-token or env[OS_TOKEN]") - - if not self.options.os_auth_url: - raise exc.CommandError( - "You must provide a service URL via" - " either --os-auth-url or env[OS_AUTH_URL]") - - if (not self.options.os_url and - not self.options.os_auth_plugin.endswith('token')): - # Validate password flow auth - if not self.options.os_username: - raise exc.CommandError( - "You must provide a username via" - " either --os-username or env[OS_USERNAME]") - - if not self.options.os_password: - # No password, if we've got a tty, try prompting for it - if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): - # Check for Ctl-D - try: - self.options.os_password = getpass.getpass() - except EOFError: - pass - # No password because we did't have a tty or the - # user Ctl-D when prompted? - if not self.options.os_password: - raise exc.CommandError( - "You must provide a password via" - " either --os-password, or env[OS_PASSWORD], " - " or prompted response") - - if not ((self.options.os_project_id - or self.options.os_project_name) or - (self.options.os_domain_id - or self.options.os_domain_name) or - self.options.os_trust_id): - if self.options.os_auth_plugin.endswith('password'): - raise exc.CommandError( - "You must provide authentication scope as a project " - "or a domain via --os-project-id " - "or env[OS_PROJECT_ID], " - "--os-project-name or env[OS_PROJECT_NAME], " - "--os-domain-id or env[OS_DOMAIN_ID], or" - "--os-domain-name or env[OS_DOMAIN_NAME], or " - "--os-trust-id or env[OS_TRUST_ID].") - - if not self.options.os_auth_url: - raise exc.CommandError( - "You must provide an auth url via" - " either --os-auth-url or via env[OS_AUTH_URL]") - - if (self.options.os_trust_id and - self.options.os_identity_api_version != '3'): - raise exc.CommandError( - "Trusts can only be used with Identity API v3") - - if (self.options.os_trust_id and - ((self.options.os_project_id - or self.options.os_project_name) or - (self.options.os_domain_id - or self.options.os_domain_name))): - raise exc.CommandError( - "Authentication cannot be scoped to multiple targets. " - "Pick one of project, domain or trust.") - - self.client_manager = clientmanager.ClientManager( - auth_options=self.options, - verify=self.verify, - api_version=self.api_version, - ) - return - def initialize_app(self, argv): """Global app init bits: @@ -368,19 +286,23 @@ def initialize_app(self, argv): else: self.verify = not self.options.insecure + self.client_manager = clientmanager.ClientManager( + auth_options=self.options, + verify=self.verify, + api_version=self.api_version, + pw_func=prompt_for_password, + ) + def prepare_to_run_command(self, cmd): """Set up auth and API versions""" self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - if not cmd.auth_required: - return - if cmd.best_effort: + if cmd.auth_required and cmd.best_effort: try: - self.initialize_clientmanager() + # Trigger the Identity client to initialize + self.client_manager.auth_ref except Exception: pass - else: - self.initialize_clientmanager() return def clean_up(self, cmd, result, err): @@ -412,12 +334,6 @@ def clean_up(self, cmd, result, err): targs = tparser.parse_args(['-f', format]) tcmd.run(targs) - def interact(self): - # NOTE(dtroyer): Maintain the old behaviour for interactive use as - # this path does not call prepare_to_run_command() - self.initialize_clientmanager() - super(OpenStackShell, self).interact() - def main(argv=sys.argv[1:]): return OpenStackShell().run(argv) From e7bba3211a08a295a15997a552237b06e2238e3d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 21 Oct 2014 18:00:13 -0400 Subject: [PATCH 0046/3303] Include support for using oslo debugger in tests Simply run `tox -e debug ` to get an interactive debugging prompt Change-Id: I09e5b844a33c2f0fd4230f01fbc6c0aa8d752545 --- test-requirements.txt | 1 + tox.ini | 3 +++ 2 files changed, 4 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index 1bd4efb8a0..259ace266c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ discover fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 +oslotest>=1.2.0 # Apache-2.0 requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 diff --git a/tox.ini b/tox.ini index cac6f1169f..02dac23a7e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,9 @@ commands = {posargs} [testenv:cover] commands = python setup.py test --coverage --testr-args='{posargs}' +[testenv:debug] +commands = oslo_debug_helper -t openstackclient/tests {posargs} + [tox:jenkins] downloadcache = ~/cache/pip From c91d1ca66359fee1a1509f0ac6da4c32159bb59a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 22 Oct 2014 16:04:11 -0500 Subject: [PATCH 0047/3303] Beef up object-store tests * Add object top-to-bottom tests * Move some fakes around * Clean up existing object tests Change-Id: If8406da611c11bbd2b1bf5153e45b720b0eea442 --- openstackclient/tests/object/v1/fakes.py | 10 +- .../tests/object/v1/test_container.py | 28 +-- .../tests/object/v1/test_container_all.py | 35 ++-- .../tests/object/v1/test_object.py | 36 ++-- .../tests/object/v1/test_object_all.py | 177 ++++++++++++++++++ 5 files changed, 228 insertions(+), 58 deletions(-) create mode 100644 openstackclient/tests/object/v1/test_object_all.py diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index f10437b63f..6aef05b1e8 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -13,7 +13,8 @@ # under the License. # -from openstackclient.tests import fakes +from keystoneclient import session +from openstackclient.api import object_store_v1 as object_store from openstackclient.tests import utils @@ -78,7 +79,8 @@ class TestObjectv1(utils.TestCommand): def setUp(self): super(TestObjectv1, self).setUp() - self.app.client_manager.object_store = fakes.FakeClient( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, + self.app.client_manager.session = session.Session() + self.app.client_manager.object_store = object_store.APIv1( + session=self.app.client_manager.session, + endpoint=ENDPOINT, ) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index 70b88fc425..2ef2f7b52a 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -74,13 +74,13 @@ def test_object_list_containers_no_options(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), (object_fakes.container_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_prefix(self, c_mock): c_mock.return_value = [ @@ -108,12 +108,12 @@ def test_object_list_containers_prefix(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_marker(self, c_mock): c_mock.return_value = [ @@ -144,12 +144,12 @@ def test_object_list_containers_marker(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_limit(self, c_mock): c_mock.return_value = [ @@ -177,12 +177,12 @@ def test_object_list_containers_limit(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_long(self, c_mock): c_mock.return_value = [ @@ -209,7 +209,7 @@ def test_object_list_containers_long(self, c_mock): ) collist = ('Name', 'Bytes', 'Count') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( ( object_fakes.container_name, @@ -222,7 +222,7 @@ def test_object_list_containers_long(self, c_mock): object_fakes.container_count * 3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_all(self, c_mock): c_mock.return_value = [ @@ -251,13 +251,13 @@ def test_object_list_containers_all(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_2, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) @mock.patch( @@ -295,10 +295,10 @@ def test_container_show(self, c_mock): ) collist = ('bytes', 'count', 'name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( object_fakes.container_bytes, object_fakes.container_count, object_fakes.container_name, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 53b60b9abb..8b200e09e3 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -15,34 +15,25 @@ from requests_mock.contrib import fixture -from keystoneclient import session -from openstackclient.api import object_store_v1 as object_store -from openstackclient.object.v1 import container +from openstackclient.object.v1 import container as container_cmds from openstackclient.tests.object.v1 import fakes as object_fakes -class TestObjectAll(object_fakes.TestObjectv1): +class TestContainerAll(object_fakes.TestObjectv1): def setUp(self): - super(TestObjectAll, self).setUp() + super(TestContainerAll, self).setUp() - self.app.client_manager.session = session.Session() self.requests_mock = self.useFixture(fixture.Fixture()) - # TODO(dtroyer): move this to object_fakes.TestObjectv1 - self.app.client_manager.object_store = object_store.APIv1( - session=self.app.client_manager.session, - endpoint=object_fakes.ENDPOINT, - ) - -class TestContainerCreate(TestObjectAll): +class TestContainerCreate(TestContainerAll): def setUp(self): super(TestContainerCreate, self).setUp() # Get the command object to test - self.cmd = container.CreateContainer(self.app, None) + self.cmd = container_cmds.CreateContainer(self.app, None) def test_object_create_container_single(self): self.requests_mock.register_uri( @@ -115,13 +106,13 @@ def test_object_create_container_more(self): self.assertEqual(datalist, list(data)) -class TestContainerDelete(TestObjectAll): +class TestContainerDelete(TestContainerAll): def setUp(self): super(TestContainerDelete, self).setUp() # Get the command object to test - self.cmd = container.DeleteContainer(self.app, None) + self.cmd = container_cmds.DeleteContainer(self.app, None) def test_object_delete_container_single(self): self.requests_mock.register_uri( @@ -168,13 +159,13 @@ def test_object_delete_container_more(self): self.assertIsNone(ret) -class TestContainerList(TestObjectAll): +class TestContainerList(TestContainerAll): def setUp(self): super(TestContainerList, self).setUp() # Get the command object to test - self.cmd = container.ListContainer(self.app, None) + self.cmd = container_cmds.ListContainer(self.app, None) def test_object_list_containers_no_options(self): return_body = [ @@ -237,13 +228,13 @@ def test_object_list_containers_prefix(self): self.assertEqual(datalist, list(data)) -class TestContainerSave(TestObjectAll): +class TestContainerSave(TestContainerAll): def setUp(self): super(TestContainerSave, self).setUp() # Get the command object to test - self.cmd = container.SaveContainer(self.app, None) + self.cmd = container_cmds.SaveContainer(self.app, None) # TODO(dtroyer): need to mock out object_lib.save_object() to test this # def test_object_save_container(self): @@ -285,13 +276,13 @@ def setUp(self): # self.assertIsNone(ret) -class TestContainerShow(TestObjectAll): +class TestContainerShow(TestContainerAll): def setUp(self): super(TestContainerShow, self).setUp() # Get the command object to test - self.cmd = container.ShowContainer(self.app, None) + self.cmd = container_cmds.ShowContainer(self.app, None) def test_object_show_container(self): headers = { diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index 22f06999e2..305fe8f83b 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -68,12 +68,12 @@ def test_object_list_objects_no_options(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_1, ), (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_prefix(self, o_mock): o_mock.return_value = [ @@ -103,11 +103,11 @@ def test_object_list_objects_prefix(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_delimiter(self, o_mock): o_mock.return_value = [ @@ -137,11 +137,11 @@ def test_object_list_objects_delimiter(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_marker(self, o_mock): o_mock.return_value = [ @@ -171,11 +171,11 @@ def test_object_list_objects_marker(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_end_marker(self, o_mock): o_mock.return_value = [ @@ -205,11 +205,11 @@ def test_object_list_objects_end_marker(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_limit(self, o_mock): o_mock.return_value = [ @@ -239,11 +239,11 @@ def test_object_list_objects_limit(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_long(self, o_mock): o_mock.return_value = [ @@ -273,7 +273,7 @@ def test_object_list_objects_long(self, o_mock): ) collist = ('Name', 'Bytes', 'Hash', 'Content Type', 'Last Modified') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( ( object_fakes.object_name_1, @@ -290,7 +290,7 @@ def test_object_list_objects_long(self, o_mock): object_fakes.object_modified_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_all(self, o_mock): o_mock.return_value = [ @@ -321,12 +321,12 @@ def test_object_list_objects_all(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_1, ), (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) @mock.patch( @@ -367,7 +367,7 @@ def test_object_show(self, c_mock): ) collist = ('bytes', 'content_type', 'hash', 'last_modified', 'name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( object_fakes.object_bytes_1, object_fakes.object_content_type_1, @@ -375,4 +375,4 @@ def test_object_show(self, c_mock): object_fakes.object_modified_1, object_fakes.object_name_1, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/object/v1/test_object_all.py b/openstackclient/tests/object/v1/test_object_all.py new file mode 100644 index 0000000000..f2001e91d7 --- /dev/null +++ b/openstackclient/tests/object/v1/test_object_all.py @@ -0,0 +1,177 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +from requests_mock.contrib import fixture + +from openstackclient.object.v1 import object as object_cmds +from openstackclient.tests.object.v1 import fakes as object_fakes + + +class TestObjectAll(object_fakes.TestObjectv1): + def setUp(self): + super(TestObjectAll, self).setUp() + + self.requests_mock = self.useFixture(fixture.Fixture()) + + +class TestObjectCreate(TestObjectAll): + + def setUp(self): + super(TestObjectCreate, self).setUp() + + # Get the command object to test + self.cmd = object_cmds.CreateObject(self.app, None) + + +class TestObjectList(TestObjectAll): + + def setUp(self): + super(TestObjectList, self).setUp() + + # Get the command object to test + self.cmd = object_cmds.ListObject(self.app, None) + + def test_object_list_objects_no_options(self): + return_body = [ + copy.deepcopy(object_fakes.OBJECT), + copy.deepcopy(object_fakes.OBJECT_2), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + + '/' + + object_fakes.container_name + + '?format=json', + json=return_body, + status_code=200, + ) + + arglist = [ + object_fakes.container_name, + ] + verifylist = [ + ('container', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Lister.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(collist, columns) + datalist = [ + (object_fakes.object_name_1, ), + (object_fakes.object_name_2, ), + ] + self.assertEqual(datalist, list(data)) + + def test_object_list_objects_prefix(self): + return_body = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + + '/' + + object_fakes.container_name_2 + + '?prefix=floppy&format=json', + json=return_body, + status_code=200, + ) + + arglist = [ + '--prefix', 'floppy', + object_fakes.container_name_2, + ] + verifylist = [ + ('prefix', 'floppy'), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + +class TestObjectShow(TestObjectAll): + + def setUp(self): + super(TestObjectShow, self).setUp() + + # Get the command object to test + self.cmd = object_cmds.ShowObject(self.app, None) + + def test_object_show(self): + headers = { + 'content-type': 'text/plain', + 'content-length': '20', + 'last-modified': 'yesterday', + 'etag': '4c4e39a763d58392724bccf76a58783a', + 'x-container-meta-owner': object_fakes.ACCOUNT_ID, + 'x-object-manifest': 'manifest', + } + self.requests_mock.register_uri( + 'HEAD', + '/'.join([ + object_fakes.ENDPOINT, + object_fakes.container_name, + object_fakes.object_name_1, + ]), + headers=headers, + status_code=200, + ) + + arglist = [ + object_fakes.container_name, + object_fakes.object_name_1, + ] + verifylist = [ + ('container', object_fakes.container_name), + ('object', object_fakes.object_name_1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'account', + 'container', + 'content-length', + 'content-type', + 'etag', + 'last-modified', + 'object', + 'x-object-manifest', + ) + self.assertEqual(collist, columns) + datalist = ( + object_fakes.ACCOUNT_ID, + object_fakes.container_name, + '20', + 'text/plain', + '4c4e39a763d58392724bccf76a58783a', + 'yesterday', + object_fakes.object_name_1, + 'manifest', + ) + self.assertEqual(datalist, data) From f079b5b9c4c030293b4ebfdf84d8b768b3aa3515 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 22 Oct 2014 11:12:47 -0500 Subject: [PATCH 0048/3303] Change --os-auth-plugin to --os-auth-type User's don't know what a plugin is. * Internally, os_auth_type and/or auth_type represents what the user supplied. * auth_plugin_name is the name of the selected plugin * auth_plugin is the actual plugin object Plugin selection process: * if --os-auth-type is supplied: * if it matches against an available plugin, done * (if it can map to an availble plugin type, done; TODO in a followup) * if --os-auth-type is not supplied: * if --os-url and --os-token are supplied, select 'token_endpoint' * if --os-username supplied, select identity_api_version + 'password' * if --os-token supplied, select identity_api_version + 'token' Change-Id: Ice4535214e311ebf924087cf77f6d84d76f5f3ee --- openstackclient/api/auth.py | 64 +++++++++---------- openstackclient/common/clientmanager.py | 18 +++--- .../tests/common/test_clientmanager.py | 16 ++--- openstackclient/tests/test_shell.py | 36 +++++------ 4 files changed, 66 insertions(+), 68 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index f6e99cdcf0..e19c6b7931 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -36,8 +36,6 @@ invoke_on_load=False, propagate_map_exceptions=True, ) -# TODO(dtroyer): add some method to list the plugins for the -# --os_auth_plugin option # Get the command line options so the help action has them available OPTIONS_LIST = {} @@ -56,50 +54,53 @@ def select_auth_plugin(options): - """If no auth plugin was specified, pick one based on other options""" + """Pick an auth plugin based on --os-auth-type or other options""" + + auth_plugin_name = None + + if options.os_auth_type in [plugin.name for plugin in PLUGIN_LIST]: + # A direct plugin name was given, use it + return options.os_auth_type - auth_plugin = None if options.os_url and options.os_token: # service token authentication - auth_plugin = 'token_endpoint' + auth_plugin_name = 'token_endpoint' elif options.os_username: if options.os_identity_api_version == '3': - auth_plugin = 'v3password' + auth_plugin_name = 'v3password' elif options.os_identity_api_version == '2.0': - auth_plugin = 'v2password' + auth_plugin_name = 'v2password' else: # let keystoneclient figure it out itself - auth_plugin = 'password' + auth_plugin_name = 'password' elif options.os_token: if options.os_identity_api_version == '3': - auth_plugin = 'v3token' + auth_plugin_name = 'v3token' elif options.os_identity_api_version == '2.0': - auth_plugin = 'v2token' + auth_plugin_name = 'v2token' else: # let keystoneclient figure it out itself - auth_plugin = 'token' + auth_plugin_name = 'token' else: raise exc.CommandError( - "Could not figure out which authentication method " - "to use, please set --os-auth-plugin" + "Authentication type must be selected with --os-auth-type" ) - LOG.debug("No auth plugin selected, picking %s from other " - "options" % auth_plugin) - return auth_plugin + LOG.debug("Auth plugin %s selected" % auth_plugin_name) + return auth_plugin_name -def build_auth_params(cmd_options): +def build_auth_params(auth_plugin_name, cmd_options): auth_params = {} - if cmd_options.os_auth_plugin: - LOG.debug('auth_plugin: %s', cmd_options.os_auth_plugin) - auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) - plugin_options = auth_plugin.get_options() + if auth_plugin_name: + LOG.debug('auth_type: %s', auth_plugin_name) + auth_plugin_class = base.get_plugin_class(auth_plugin_name) + plugin_options = auth_plugin_class.get_options() for option in plugin_options: option_name = 'os_' + option.dest LOG.debug('fetching option %s' % option_name) auth_params[option.dest] = getattr(cmd_options, option_name, None) # grab tenant from project for v2.0 API compatibility - if cmd_options.os_auth_plugin.startswith("v2"): + if auth_plugin_name.startswith("v2"): auth_params['tenant_id'] = getattr( cmd_options, 'os_project_id', @@ -111,14 +112,14 @@ def build_auth_params(cmd_options): None, ) else: - LOG.debug('no auth_plugin') + LOG.debug('no auth_type') # delay the plugin choice, grab every option plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) for option in plugin_options: option_name = 'os_' + option LOG.debug('fetching option %s' % option_name) auth_params[option] = getattr(cmd_options, option_name, None) - return auth_params + return (auth_plugin_class, auth_params) def build_auth_plugins_option_parser(parser): @@ -130,15 +131,12 @@ def build_auth_plugins_option_parser(parser): """ available_plugins = [plugin.name for plugin in PLUGIN_LIST] parser.add_argument( - '--os-auth-plugin', - metavar='', - default=utils.env('OS_AUTH_PLUGIN'), - help='The authentication method to use. If this option is not set, ' - 'openstackclient will attempt to guess the authentication method ' - 'to use based on the other options. If this option is set, ' - 'the --os-identity-api-version argument must be consistent ' - 'with the version of the method.\nAvailable methods are ' + - ', '.join(available_plugins), + '--os-auth-type', + metavar='', + default=utils.env('OS_AUTH_TYPE'), + help='Select an auhentication type. Available types: ' + + ', '.join(available_plugins) + + '. Default: selected based on --os-username/--os-token', choices=available_plugins ) # make sur we catch old v2.0 env values diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index ae38f16077..adec842f13 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -19,7 +19,6 @@ import pkg_resources import sys -from keystoneclient.auth import base from keystoneclient import session import requests @@ -78,20 +77,22 @@ def __init__( returns a string containig the password """ - # If no plugin is named by the user, select one based on + # If no auth type is named by the user, select one based on # the supplied options - if not auth_options.os_auth_plugin: - auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options) - self._auth_plugin = auth_options.os_auth_plugin + self.auth_plugin_name = auth.select_auth_plugin(auth_options) # Horrible hack alert...must handle prompt for null password if # password auth is requested. - if (self._auth_plugin.endswith('password') and + if (self.auth_plugin_name.endswith('password') and not auth_options.os_password): auth_options.os_password = pw_func() + (auth_plugin, self._auth_params) = auth.build_auth_params( + self.auth_plugin_name, + auth_options, + ) + self._url = auth_options.os_url - self._auth_params = auth.build_auth_params(auth_options) self._region_name = auth_options.os_region_name self._api_version = api_version self._auth_ref = None @@ -117,8 +118,7 @@ def __init__( root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - LOG.debug('Using auth plugin: %s' % self._auth_plugin) - auth_plugin = base.get_plugin_class(self._auth_plugin) + LOG.debug('Using auth plugin: %s' % self.auth_plugin_name) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication request_session = requests.session() diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index a7b13c6c9c..8c27e5621d 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -44,7 +44,7 @@ class FakeOptions(object): def __init__(self, **kwargs): for option in auth.OPTIONS_LIST: setattr(self, 'os_' + option.replace('-', '_'), None) - self.os_auth_plugin = None + self.os_auth_type = None self.os_identity_api_version = '2.0' self.timing = None self.os_region_name = None @@ -81,7 +81,7 @@ def test_client_manager_token_endpoint(self): client_manager = clientmanager.ClientManager( auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, os_url=fakes.AUTH_URL, - os_auth_plugin='token_endpoint'), + os_auth_type='token_endpoint'), api_version=API_VERSION, verify=True ) @@ -105,7 +105,7 @@ def test_client_manager_token(self): client_manager = clientmanager.ClientManager( auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, os_auth_url=fakes.AUTH_URL, - os_auth_plugin='v2token'), + os_auth_type='v2token'), api_version=API_VERSION, verify=True ) @@ -183,7 +183,7 @@ def test_client_manager_password_verify_ca(self): auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, os_username=fakes.USERNAME, os_password=fakes.PASSWORD, - os_auth_plugin='v2password'), + os_auth_type='v2password'), api_version=API_VERSION, verify='cafile', ) @@ -192,8 +192,8 @@ def test_client_manager_password_verify_ca(self): self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) - def _select_auth_plugin(self, auth_params, api_version, auth_plugin): - auth_params['os_auth_plugin'] = auth_plugin + def _select_auth_plugin(self, auth_params, api_version, auth_plugin_name): + auth_params['os_auth_type'] = auth_plugin_name auth_params['os_identity_api_version'] = api_version client_manager = clientmanager.ClientManager( auth_options=FakeOptions(**auth_params), @@ -201,8 +201,8 @@ def _select_auth_plugin(self, auth_params, api_version, auth_plugin): verify=True ) self.assertEqual( - auth_plugin, - client_manager._auth_plugin, + auth_plugin_name, + client_manager.auth_plugin_name, ) def test_client_manager_select_auth_plugin(self): diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index b0c1452ef9..837a48afd7 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -108,8 +108,8 @@ def _assert_password_auth(self, cmd_options, default_args): default_args["region_name"]) self.assertEqual(_shell.options.os_trust_id, default_args["trust_id"]) - self.assertEqual(_shell.options.os_auth_plugin, - default_args['auth_plugin']) + self.assertEqual(_shell.options.os_auth_type, + default_args['auth_type']) def _assert_token_auth(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -190,7 +190,7 @@ def test_only_url_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -210,7 +210,7 @@ def test_only_project_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -230,7 +230,7 @@ def test_only_project_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -250,7 +250,7 @@ def test_only_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -270,7 +270,7 @@ def test_only_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -290,7 +290,7 @@ def test_only_user_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -310,7 +310,7 @@ def test_only_user_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -330,7 +330,7 @@ def test_only_project_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -350,7 +350,7 @@ def test_only_project_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -370,7 +370,7 @@ def test_only_username_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -390,7 +390,7 @@ def test_only_password_flow(self): "password": DEFAULT_PASSWORD, "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -410,7 +410,7 @@ def test_only_region_name_flow(self): "password": "", "region_name": DEFAULT_REGION_NAME, "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -430,12 +430,12 @@ def test_only_trust_id_flow(self): "password": "", "region_name": "", "trust_id": "1234", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) - def test_only_auth_plugin_flow(self): - flag = "--os-auth-plugin " + "v2password" + def test_only_auth_type_flow(self): + flag = "--os-auth-type " + "v2password" kwargs = { "auth_url": "", "project_id": "", @@ -450,7 +450,7 @@ def test_only_auth_plugin_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": DEFAULT_AUTH_PLUGIN + "auth_type": DEFAULT_AUTH_PLUGIN } self._assert_password_auth(flag, kwargs) From 8ba74451ee9efe21a0554c184f28e380fe714313 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 21 Oct 2014 09:51:47 -0500 Subject: [PATCH 0049/3303] Adjust some logging levels * Promote select messages to INFO so lower logging levels can be useful * Help more modules not say so much all the time Change-Id: I814023c1489595998ae74efe40ef439b3522ee74 --- openstackclient/common/clientmanager.py | 2 +- openstackclient/shell.py | 37 +++++++++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index adec842f13..0396e83dd2 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -118,7 +118,7 @@ def __init__( root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - LOG.debug('Using auth plugin: %s' % self.auth_plugin_name) + LOG.info('Using auth plugin: %s' % self.auth_plugin_name) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication request_session = requests.session() diff --git a/openstackclient/shell.py b/openstackclient/shell.py index e671ecc3dc..1198bae18a 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -137,18 +137,7 @@ def configure_logging(self): super(OpenStackShell, self).configure_logging() root_logger = logging.getLogger('') - # Requests logs some stuff at INFO that we don't want - # unless we have DEBUG - requests_log = logging.getLogger("requests") - requests_log.setLevel(logging.ERROR) - - # Other modules we don't want DEBUG output for so - # don't reset them below - iso8601_log = logging.getLogger("iso8601") - iso8601_log.setLevel(logging.ERROR) - # Set logging to the requested level - self.dump_stack_trace = False if self.options.verbose_level == 0: # --quiet root_logger.setLevel(logging.ERROR) @@ -161,11 +150,28 @@ def configure_logging(self): elif self.options.verbose_level >= 3: # Two or more --verbose root_logger.setLevel(logging.DEBUG) - requests_log.setLevel(logging.DEBUG) + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + + # Other modules we don't want DEBUG output for + cliff_log = logging.getLogger('cliff') + stevedore_log = logging.getLogger('stevedore') + iso8601_log = logging.getLogger("iso8601") if self.options.debug: # --debug forces traceback self.dump_stack_trace = True + requests_log.setLevel(logging.DEBUG) + cliff_log.setLevel(logging.DEBUG) + else: + self.dump_stack_trace = False + requests_log.setLevel(logging.ERROR) + cliff_log.setLevel(logging.ERROR) + + stevedore_log.setLevel(logging.ERROR) + iso8601_log.setLevel(logging.ERROR) def run(self, argv): try: @@ -295,8 +301,11 @@ def initialize_app(self, argv): def prepare_to_run_command(self, cmd): """Set up auth and API versions""" - self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - + self.log.info( + 'command: %s.%s', + cmd.__class__.__module__, + cmd.__class__.__name__, + ) if cmd.auth_required and cmd.best_effort: try: # Trigger the Identity client to initialize From 2c9d263611190996d64e35bc74a8575aeb25ed3e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 20 Oct 2014 11:43:29 -0500 Subject: [PATCH 0050/3303] Fix server create for boot-from-volume * server create required --image even when booting the server from a volume. Change options to require either --image or --volume to specify the server boot disk. Using --volume currently uses device 'vda' for the block mapping and ignores any other block mappings given in --block-device-mapping. * server create and server show are both affected by bug 1378842 where an excepion was thrown when no image ID was present in the returned server object, which is the case for a server booted from a volume. * Fix the remaining assertEqual() order problems in test_server.py Closes-Bug: 1378842 Closes-Bug: 1383338 Change-Id: I5daebf4e50a765d4920088dfead95b6295af6a4d --- openstackclient/compute/v2/server.py | 51 +++++++++++++++---- .../tests/compute/v2/test_server.py | 22 ++++---- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a6d645b9e9..d58df86c10 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -67,9 +67,10 @@ def _prep_server_detail(compute_client, server): # Convert the image blob to a name image_info = info.get('image', {}) - image_id = image_info.get('id', '') - image = utils.find_resource(compute_client.images, image_id) - info['image'] = "%s (%s)" % (image.name, image_id) + if image_info: + image_id = image_info.get('id', '') + image = utils.find_resource(compute_client.images, image_id) + info['image'] = "%s (%s)" % (image.name, image_id) # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) @@ -192,11 +193,17 @@ def get_parser(self, prog_name): 'server_name', metavar='', help=_('New server name')) - parser.add_argument( + disk_group = parser.add_mutually_exclusive_group( + required=True, + ) + disk_group.add_argument( '--image', metavar='', - required=True, help=_('Create server from this image')) + disk_group.add_argument( + '--volume', + metavar='', + help=_('Create server from this volume')) parser.add_argument( '--flavor', metavar='', @@ -282,10 +289,23 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute + volume_client = self.app.client_manager.volume # Lookup parsed_args.image - image = utils.find_resource(compute_client.images, - parsed_args.image) + image = None + if parsed_args.image: + image = utils.find_resource( + compute_client.images, + parsed_args.image, + ) + + # Lookup parsed_args.volume + volume = None + if parsed_args.volume: + volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ).id # Lookup parsed_args.flavor flavor = utils.find_resource(compute_client.flavors, @@ -319,8 +339,21 @@ def take_action(self, parsed_args): msg = "Can't open '%s': %s" raise exceptions.CommandError(msg % (parsed_args.user_data, e)) - block_device_mapping = dict(v.split('=', 1) - for v in parsed_args.block_device_mapping) + block_device_mapping = {} + if volume: + # When booting from volume, for now assume no other mappings + # This device value is likely KVM-specific + block_device_mapping = {'vda': volume} + else: + for dev_map in parsed_args.block_device_mapping: + dev_key, dev_vol = dev_map.split('=', 1) + block_volume = None + if dev_vol: + block_volume = utils.find_resource( + volume_client.volumes, + dev_vol, + ).id + block_device_mapping.update({dev_key: block_volume}) nics = [] for nic_str in parsed_args.nic: diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 50de5c6ab7..43aa7a70e6 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -134,17 +134,16 @@ def test_server_create_minimal(self): **kwargs ) - collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') - self.assertEqual(columns, collist) + collist = ('addresses', 'flavor', 'id', 'name', 'properties') + self.assertEqual(collist, columns) datalist = ( '', 'Large ()', compute_fakes.server_id, - 'graven ()', compute_fakes.server_name, '', ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) @mock.patch('openstackclient.compute.v2.server.io.open') def test_server_create_userdata(self, mock_open): @@ -200,17 +199,16 @@ def test_server_create_userdata(self, mock_open): **kwargs ) - collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') - self.assertEqual(columns, collist) + collist = ('addresses', 'flavor', 'id', 'name', 'properties') + self.assertEqual(collist, columns) datalist = ( '', 'Large ()', compute_fakes.server_id, - 'graven ()', compute_fakes.server_name, '', ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) class TestServerDelete(TestServer): @@ -288,14 +286,14 @@ def test_server_image_create_no_options(self): ) collist = ('id', 'is_public', 'name', 'owner') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, False, image_fakes.image_name, image_fakes.image_owner, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_server_image_create_name(self): arglist = [ @@ -318,14 +316,14 @@ def test_server_image_create_name(self): ) collist = ('id', 'is_public', 'name', 'owner') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, False, image_fakes.image_name, image_fakes.image_owner, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) class TestServerResize(TestServer): From 631ed3c8026e6b4539e7a8bf4adb8d2a7239b36a Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Thu, 25 Sep 2014 18:15:59 +0200 Subject: [PATCH 0051/3303] Unscoped federated user-specific commands A federated user can authenticate with the v3unscopedsaml plugin and list the domains and projects she is allowed to scope to. This patch introduces the new commands 'federation domain list' and 'federation project list'. Note that for these commands -and plugin- to be available, the lxml library must be installed. Change-Id: I2707b624befcfb0a01b40a094e12fd68a3ee7773 Co-Authored-By: Florent Flament --- doc/source/man/openstack.rst | 3 + openstackclient/identity/v3/unscoped_saml.py | 79 +++++++++++ openstackclient/tests/fakes.py | 1 + openstackclient/tests/identity/v3/fakes.py | 17 +++ .../tests/identity/v3/test_unscoped_saml.py | 128 ++++++++++++++++++ setup.cfg | 3 + 6 files changed, 231 insertions(+) create mode 100644 openstackclient/identity/v3/unscoped_saml.py create mode 100644 openstackclient/tests/identity/v3/test_unscoped_saml.py diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index de2bbe92fa..4a9df34a39 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -47,6 +47,9 @@ Please bear in mind that some plugins might not support all of the functionaliti Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-url` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_URL` respectively). This method takes precedence over authentication plugins. +.. NOTE:: + To use the ``v3unscopedsaml`` method, the lxml package will need to be installed. + OPTIONS ======= diff --git a/openstackclient/identity/v3/unscoped_saml.py b/openstackclient/identity/v3/unscoped_saml.py new file mode 100644 index 0000000000..affbaf3a87 --- /dev/null +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -0,0 +1,79 @@ +# 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. +# + +"""Identity v3 unscoped SAML auth action implementations. + +The first step of federated auth is to fetch an unscoped token. From there, +the user can list domains and projects they are allowed to access, and request +a scoped token.""" + +import logging + +from cliff import lister + +from openstackclient.common import exceptions +from openstackclient.common import utils + + +UNSCOPED_AUTH_PLUGINS = ['v3unscopedsaml', 'v3unscopedadfs'] + + +def auth_with_unscoped_saml(func): + """Check the unscoped federated context""" + def _decorated(self, parsed_args): + auth_plugin_name = self.app.client_manager.auth_plugin_name + if auth_plugin_name in UNSCOPED_AUTH_PLUGINS: + return func(self, parsed_args) + else: + msg = ('This command requires the use of an unscoped SAML ' + 'authentication plugin. Please use argument ' + '--os-auth-plugin with one of the following ' + 'plugins: ' + ', '.join(UNSCOPED_AUTH_PLUGINS)) + raise exceptions.CommandError(msg) + return _decorated + + +class ListAccessibleDomains(lister.Lister): + """List accessible domains""" + + log = logging.getLogger(__name__ + '.ListAccessibleDomains') + + @auth_with_unscoped_saml + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + columns = ('ID', 'Enabled', 'Name', 'Description') + identity_client = self.app.client_manager.identity + data = identity_client.federation.domains.list() + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class ListAccessibleProjects(lister.Lister): + """List accessible projects""" + + log = logging.getLogger(__name__ + '.ListAccessibleProjects') + + @auth_with_unscoped_saml + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + columns = ('ID', 'Domain ID', 'Enabled', 'Name') + identity_client = self.app.client_manager.identity + data = identity_client.federation.projects.list() + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index f8b7bb6f39..abad4cffe6 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -199,6 +199,7 @@ def __init__(self): self.network = None self.session = None self.auth_ref = None + self.auth_plugin_name = None class FakeModule(object): diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 5844d160b9..b195ed78b7 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -285,6 +285,19 @@ } +class FakeAuth(object): + def __init__(self, auth_method_class=None): + self._auth_method_class = auth_method_class + + def get_token(self, *args, **kwargs): + return token_id + + +class FakeSession(object): + def __init__(self, **kwargs): + self.auth = FakeAuth() + + class FakeIdentityv3Client(object): def __init__(self, **kwargs): self.domains = mock.Mock() @@ -320,6 +333,10 @@ def __init__(self, **kwargs): self.mappings.resource_class = fakes.FakeResource(None, {}) self.protocols = mock.Mock() self.protocols.resource_class = fakes.FakeResource(None, {}) + self.projects = mock.Mock() + self.projects.resource_class = fakes.FakeResource(None, {}) + self.domains = mock.Mock() + self.domains.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_unscoped_saml.py b/openstackclient/tests/identity/v3/test_unscoped_saml.py new file mode 100644 index 0000000000..6b2d3f5b1c --- /dev/null +++ b/openstackclient/tests/identity/v3/test_unscoped_saml.py @@ -0,0 +1,128 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from openstackclient.common import exceptions +from openstackclient.identity.v3 import unscoped_saml +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestUnscopedSAML(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestUnscopedSAML, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.projects_mock = federation_lib.projects + self.projects_mock.reset_mock() + self.domains_mock = federation_lib.domains + self.domains_mock.reset_mock() + + +class TestProjectList(TestUnscopedSAML): + + def setUp(self): + super(TestProjectList, self).setUp() + + self.projects_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = unscoped_saml.ListAccessibleProjects(self.app, None) + + def test_accessible_projects_list(self): + self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.projects_mock.list.assert_called_with() + + collist = ('ID', 'Domain ID', 'Enabled', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.project_id, + identity_fakes.domain_id, + True, + identity_fakes.project_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_accessible_projects_list_wrong_auth(self): + auth = identity_fakes.FakeAuth("wrong auth") + self.app.client_manager.identity.session.auth = auth + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + +class TestDomainList(TestUnscopedSAML): + + def setUp(self): + super(TestDomainList, self).setUp() + + self.domains_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = unscoped_saml.ListAccessibleDomains(self.app, None) + + def test_accessible_domains_list(self): + self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.domains_mock.list.assert_called_with() + + collist = ('ID', 'Enabled', 'Name', 'Description') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.domain_id, + True, + identity_fakes.domain_name, + identity_fakes.domain_description, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_accessible_domains_list_wrong_auth(self): + auth = identity_fakes.FakeAuth("wrong auth") + self.app.client_manager.identity.session.auth = auth + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index af601649de..c0519d11d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -240,6 +240,9 @@ openstack.identity.v3 = federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol + federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains + federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken request_token_create = openstackclient.identity.v3.token:CreateRequestToken From b19379363691e65a83f3109ba501d0262cad37f5 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 31 Oct 2014 15:15:54 +0100 Subject: [PATCH 0052/3303] Use fixtures from keystoneclient for static data We should use the fixture generation code from keystoneclient rather than keep our own copies of the token and discovery structure. Change-Id: I53c1d2935d1d65c39b8abea89427af2fc3edd181 --- openstackclient/tests/fakes.py | 145 +++------------------------------ 1 file changed, 11 insertions(+), 134 deletions(-) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index f8b7bb6f39..e32181f1f9 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -17,6 +17,7 @@ import six import sys +from keystoneclient import fixture import requests @@ -24,140 +25,16 @@ AUTH_URL = "http://0.0.0.0" USERNAME = "itchy" PASSWORD = "scratchy" -TEST_RESPONSE_DICT = { - "access": { - "metadata": { - "is_admin": 0, - "roles": [ - "1234", - ] - }, - "serviceCatalog": [ - { - "endpoints": [ - { - "adminURL": AUTH_URL + "/v2.0", - "id": "1234", - "internalURL": AUTH_URL + "/v2.0", - "publicURL": AUTH_URL + "/v2.0", - "region": "RegionOne" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - } - ], - "token": { - "expires": "2035-01-01T00:00:01Z", - "id": AUTH_TOKEN, - "issued_at": "2013-01-01T00:00:01.692048", - "tenant": { - "description": None, - "enabled": True, - "id": "1234", - "name": "testtenant" - } - }, - "user": { - "id": "5678", - "name": USERNAME, - "roles": [ - { - "name": "testrole" - }, - ], - "roles_links": [], - "username": USERNAME - } - } -} -TEST_RESPONSE_DICT_V3 = { - "token": { - "audit_ids": [ - "a" - ], - "catalog": [ - ], - "expires_at": "2034-09-29T18:27:15.978064Z", - "extras": {}, - "issued_at": "2014-09-29T17:27:15.978097Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "Default" - }, - "id": "bbb", - "name": "project" - }, - "roles": [ - ], - "user": { - "domain": { - "id": "default", - "name": "Default" - }, - "id": "aaa", - "name": USERNAME - } - } -} -TEST_VERSIONS = { - "versions": { - "values": [ - { - "id": "v3.0", - "links": [ - { - "href": AUTH_URL, - "rel": "self" - } - ], - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.openstack.identity-v3+json" - }, - { - "base": "application/xml", - "type": "application/vnd.openstack.identity-v3+xml" - } - ], - "status": "stable", - "updated": "2013-03-06T00:00:00Z" - }, - { - "id": "v2.0", - "links": [ - { - "href": AUTH_URL, - "rel": "self" - }, - { - "href": "http://docs.openstack.org/", - "rel": "describedby", - "type": "text/html" - } - ], - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.openstack.identity-v2.0+json" - }, - { - "base": "application/xml", - "type": "application/vnd.openstack.identity-v2.0+xml" - } - ], - "status": "stable", - "updated": "2014-04-17T00:00:00Z" - } - ] - } -} + +TEST_RESPONSE_DICT = fixture.V2Token(token_id=AUTH_TOKEN, + user_name=USERNAME) +_s = TEST_RESPONSE_DICT.add_service('identity', name='keystone') +_s.add_endpoint(AUTH_URL + '/v2.0') + +TEST_RESPONSE_DICT_V3 = fixture.V3Token(user_name=USERNAME) +TEST_RESPONSE_DICT_V3.set_project_scope() + +TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) class FakeStdout: From 59735bf10da67b1611ae6e6b34f282512b8b63bf Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 7 Nov 2014 15:15:43 +0100 Subject: [PATCH 0053/3303] Add cliff-tablib to requirements cliff-tablib gives cliff the ability to format list and show output in html, json, or yaml (http://cliff-tablib.readthedocs.org/). This patch adds cliff-tablib to requirements.txt so that it can be installed along with cliff. Change-Id: I4daab97642482e6f40cd8209ff5edd9c680092c0 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 54ea795079..287935a705 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ six>=1.7.0 Babel>=1.3 cliff>=1.7.0 # Apache-2.0 +cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 From 42d0b20ebc5fd7393d250fb72d30d5ce630dc54f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 7 Nov 2014 04:44:54 -0600 Subject: [PATCH 0054/3303] Add --or-show option to user create The --or-show option is added to create commands for the common case of needing to ensure an object exists and getting its properties if it does or creating a new one if it does not exist. Note that if the object exists, any additional options that would set values in a newly created object are ignored if the object exists. FakeResource needs the __name__ attribute to fall through utils.find_resource. Prove the concept on v2 user create then propogate once we're happy with it... Change-Id: I6268566514840c284e6a1d44b409a81d6699ef99 --- openstackclient/identity/v2_0/user.py | 30 +++++-- openstackclient/tests/fakes.py | 1 + .../tests/identity/v2_0/test_user.py | 79 +++++++++++++++++++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 729c6ac81f..2ebcba188e 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -71,6 +71,11 @@ def get_parser(self, prog_name): action='store_true', help=_('Disable user'), ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing user'), + ) return parser def take_action(self, parsed_args): @@ -91,13 +96,24 @@ def take_action(self, parsed_args): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) - user = identity_client.users.create( - parsed_args.name, - parsed_args.password, - parsed_args.email, - tenant_id=project_id, - enabled=enabled, - ) + try: + user = identity_client.users.create( + parsed_args.name, + parsed_args.password, + parsed_args.email, + tenant_id=project_id, + enabled=enabled, + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + user = utils.find_resource( + identity_client.users, + parsed_args.name, + ) + self.log.info('Returning existing user %s', user.name) + else: + raise e + # NOTE(dtroyer): The users.create() method wants 'tenant_id' but # the returned resource has 'tenantId'. Sigh. # We're using project_id now inside OSC so there. diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index abad4cffe6..3c0b060d17 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -210,6 +210,7 @@ def __init__(self, name, version): class FakeResource(object): def __init__(self, manager, info, loaded=False): + self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index e191431c94..f0ff41c8db 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -16,6 +16,7 @@ import copy import mock +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes @@ -342,6 +343,84 @@ def test_user_create_disable(self): ) self.assertEqual(data, datalist) + def test_user_create_or_show_exists(self): + def _raise_conflict(*args, **kwargs): + raise ksc_exc.Conflict(None) + + # need to make this throw an exception... + self.users_mock.create.side_effect = _raise_conflict + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--or-show', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # UserManager.create(name, password, email, tenant_id=, enabled=) + self.users_mock.get.assert_called_with(identity_fakes.user_name) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(datalist, data) + + def test_user_create_or_show_not_exists(self): + arglist = [ + '--or-show', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # UserManager.create(name, password, email, tenant_id=, enabled=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + None, + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(datalist, data) + class TestUserDelete(TestUser): From 46f6df5f23164836cc6a3601aa9d5b018192209f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 7 Nov 2014 15:04:46 -0600 Subject: [PATCH 0055/3303] Swap remaining assertEqual arguments Change-Id: I1abdebb298b93074657a7ba65a7186d814969780 --- .../tests/identity/v2_0/test_user.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index f0ff41c8db..a025dd7d1a 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -84,7 +84,7 @@ def test_user_create_no_options(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -92,7 +92,7 @@ def test_user_create_no_options(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_password(self): arglist = [ @@ -123,7 +123,7 @@ def test_user_create_password(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -131,7 +131,7 @@ def test_user_create_password(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_password_prompt(self): arglist = [ @@ -164,7 +164,7 @@ def test_user_create_password_prompt(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -172,7 +172,7 @@ def test_user_create_password_prompt(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_email(self): arglist = [ @@ -202,7 +202,7 @@ def test_user_create_email(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -210,7 +210,7 @@ def test_user_create_email(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_project(self): # Return the new project @@ -255,7 +255,7 @@ def test_user_create_project(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -263,7 +263,7 @@ def test_user_create_project(self): identity_fakes.user_name, identity_fakes.PROJECT_2['id'], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_enable(self): arglist = [ @@ -294,7 +294,7 @@ def test_user_create_enable(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -302,7 +302,7 @@ def test_user_create_enable(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_disable(self): arglist = [ @@ -333,7 +333,7 @@ def test_user_create_disable(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -341,7 +341,7 @@ def test_user_create_disable(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_or_show_exists(self): def _raise_conflict(*args, **kwargs): @@ -495,12 +495,12 @@ def test_user_list_no_options(self): self.users_mock.list.assert_called_with(tenant_id=None) collist = ('ID', 'Name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = (( identity_fakes.user_id, identity_fakes.user_name, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_user_list_project(self): arglist = [ @@ -518,12 +518,12 @@ def test_user_list_project(self): self.users_mock.list.assert_called_with(tenant_id=project_id) collist = ('ID', 'Name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = (( identity_fakes.user_id, identity_fakes.user_name, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_user_list_long(self): arglist = [ @@ -540,7 +540,7 @@ def test_user_list_long(self): self.users_mock.list.assert_called_with(tenant_id=None) collist = ('ID', 'Name', 'Project', 'Email', 'Enabled') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = (( identity_fakes.user_id, identity_fakes.user_name, @@ -548,7 +548,7 @@ def test_user_list_long(self): identity_fakes.user_email, True, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) class TestUserSet(TestUser): @@ -816,7 +816,7 @@ def test_user_show(self): self.users_mock.get.assert_called_with(identity_fakes.user_id) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -824,4 +824,4 @@ def test_user_show(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) From 951ca3a6f38655dfb169878160d57fe5bf062f4a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 8 Nov 2014 14:25:05 +0000 Subject: [PATCH 0056/3303] Updated from global requirements Change-Id: I778a0c00da51cdc52cd67d1b273d52e84d68992b --- requirements.txt | 2 +- test-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 287935a705..82be8e39ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,4 @@ python-novaclient>=2.18.0 python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=2.2.0,!=2.4.0 -stevedore>=1.0.0 # Apache-2.0 +stevedore>=1.1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 259ace266c..de5732cf24 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,8 +9,8 @@ fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 oslotest>=1.2.0 # Apache-2.0 -requests-mock>=0.4.0 # Apache-2.0 +requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 -testtools>=0.9.34 +testtools>=0.9.36 WebOb>=1.2.3 From ab89ef5876406c376621be178dd6407b325c435b Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Wed, 12 Nov 2014 15:51:55 +0200 Subject: [PATCH 0057/3303] Tests work fine with random PYTHONHASHSEED Change-Id: Iba6fc87bbff289ae2572a7eb132f5c946dfa0956 Related-Bug: #1348818 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 02dac23a7e..6e03d48640 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ skipdist = True usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} - PYTHONHASHSEED=0 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' From 27b0ff5cdaf5e446d60ae6a6201a7ce0132a13a4 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 12:07:43 -0500 Subject: [PATCH 0058/3303] cleanup files that are created for swift functional tests Currently this portion of code is also being run when running tox to debug local tests. Which is very annoying since a developer will end up with a bunch of uuid files. Rather than creating it once per run, we can have a setup/teardown that is handled safely. Change-Id: I49a0bb3d14f24c54da93458d1e3b9093a1120453 --- functional/tests/test_object.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/functional/tests/test_object.py b/functional/tests/test_object.py index 396f0cb7ac..4121524ace 100644 --- a/functional/tests/test_object.py +++ b/functional/tests/test_object.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import uuid from functional.common import test @@ -25,9 +26,11 @@ class ObjectV1Tests(test.TestCase): CONTAINER_NAME = uuid.uuid4().hex OBJECT_NAME = uuid.uuid4().hex - # NOTE(stevemar): Not using setUp since we only want this to run once - with open(OBJECT_NAME, 'w') as f: - f.write('test content') + def setUp(self): + super(ObjectV1Tests, self).setUp() + self.addCleanup(os.remove, self.OBJECT_NAME) + with open(self.OBJECT_NAME, 'w') as f: + f.write('test content') def test_container_create(self): raw_output = self.openstack('container create ' + self.CONTAINER_NAME) From 070fa5091d43ee8c1f8f23b83ba36ca9d960f617 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 13 Nov 2014 16:04:49 -0500 Subject: [PATCH 0059/3303] Remove links from federation related commands in identity v3 We should remove the 'links' portion from the returned object for the following commands: * create/show federation protocol * create/show mapping * create/show identity provider Change-Id: I55654cce1f89de8e532f9acd8092257be33efd85 --- openstackclient/identity/v3/federation_protocol.py | 2 ++ openstackclient/identity/v3/identity_provider.py | 11 +++++------ openstackclient/identity/v3/mapping.py | 10 ++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index adc4a28b14..693ec94eae 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -61,6 +61,7 @@ def take_action(self, parsed_args): # user. info['identity_provider'] = parsed_args.identity_provider info['mapping'] = info.pop('mapping_id') + info.pop('links', None) return zip(*sorted(six.iteritems(info))) @@ -179,4 +180,5 @@ def take_action(self, parsed_args): parsed_args.identity_provider, parsed_args.federation_protocol) info = dict(protocol._info) info['mapping'] = info.pop('mapping_id') + info.pop('links', None) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 5e8ee56689..8a1b22d0cf 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -65,9 +65,9 @@ def take_action(self, parsed_args): id=parsed_args.identity_provider_id, description=parsed_args.description, enabled=parsed_args.enabled) - info = {} - info.update(idp._info) - return zip(*sorted(six.iteritems(info))) + + idp._info.pop('links', None) + return zip(*sorted(six.iteritems(idp._info))) class DeleteIdentityProvider(command.Command): @@ -176,6 +176,5 @@ def take_action(self, parsed_args): identity_client.federation.identity_providers, parsed_args.identity_provider) - info = {} - info.update(identity_provider._info) - return zip(*sorted(six.iteritems(info))) + identity_provider._info.pop('links', None) + return zip(*sorted(six.iteritems(identity_provider._info))) diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index ae5e03bd2d..c530a40439 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -107,9 +107,8 @@ def take_action(self, parsed_args): mapping_id=parsed_args.mapping, rules=rules) - info = {} - info.update(mapping._info) - return zip(*sorted(six.iteritems(info))) + mapping._info.pop('links', None) + return zip(*sorted(six.iteritems(mapping._info))) class DeleteMapping(command.Command): @@ -204,6 +203,5 @@ def take_action(self, parsed_args): mapping = identity_client.federation.mappings.get(parsed_args.mapping) - info = {} - info.update(mapping._info) - return zip(*sorted(six.iteritems(info))) + mapping._info.pop('links', None) + return zip(*sorted(six.iteritems(mapping._info))) From 3e97e1775dccd12757588e667ba511b464f84234 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 13 Nov 2014 16:48:59 -0500 Subject: [PATCH 0060/3303] Remove links from oauth consumers This should be the last of the v3 identity objects that return a links section upon create or show. Change-Id: I45a3b43c303bfed73950095bec8860cbea7a559c --- openstackclient/identity/v3/consumer.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index 7f54603540..b7e57d8dd0 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -46,9 +46,8 @@ def take_action(self, parsed_args): consumer = identity_client.oauth1.consumers.create( parsed_args.description ) - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) + consumer._info.pop('links', None) + return zip(*sorted(six.iteritems(consumer._info))) class DeleteConsumer(command.Command): @@ -147,6 +146,5 @@ def take_action(self, parsed_args): consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer) - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) + consumer._info.pop('links', None) + return zip(*sorted(six.iteritems(consumer._info))) From 7242113a8f8bcb8c227b259376e34e28ef6c2b52 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 10:48:47 -0500 Subject: [PATCH 0061/3303] Add additional support for --or-show Add --or-show for the following: * v2 roles * v2 projects Change-Id: Ibbae19cda668575b9527fbd259f1298c48b8265b --- openstackclient/identity/v2_0/project.py | 27 ++++-- openstackclient/identity/v2_0/role.py | 18 +++- .../tests/identity/v2_0/test_project.py | 85 +++++++++++++++++++ .../tests/identity/v2_0/test_role.py | 71 ++++++++++++++++ 4 files changed, 194 insertions(+), 7 deletions(-) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 7ead0890d4..2d66b400d4 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -63,6 +63,11 @@ def get_parser(self, prog_name): help=_('Property to add for this project ' '(repeat option to set multiple properties)'), ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing project'), + ) return parser def take_action(self, parsed_args): @@ -76,12 +81,22 @@ def take_action(self, parsed_args): if parsed_args.property: kwargs = parsed_args.property.copy() - project = identity_client.tenants.create( - parsed_args.name, - description=parsed_args.description, - enabled=enabled, - **kwargs - ) + try: + project = identity_client.tenants.create( + parsed_args.name, + description=parsed_args.description, + enabled=enabled, + **kwargs + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + project = utils.find_resource( + identity_client.tenants, + parsed_args.name, + ) + self.log.info('Returning existing project %s', project.name) + else: + raise e info = {} info.update(project._info) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 4c45004dae..df69e857ad 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -21,6 +21,7 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import exceptions from openstackclient.common import utils @@ -81,12 +82,27 @@ def get_parser(self, prog_name): 'role_name', metavar='', help=_('New role name')) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing role'), + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = identity_client.roles.create(parsed_args.role_name) + try: + role = identity_client.roles.create(parsed_args.role_name) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + role = utils.find_resource( + identity_client.roles, + parsed_args.role_name, + ) + self.log.info('Returning existing role %s', role.name) + else: + raise e info = {} info.update(role._info) diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index d046cd4796..833b5d84d8 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -15,6 +15,8 @@ import copy +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes @@ -219,6 +221,89 @@ def test_project_create_property(self): ) self.assertEqual(data, datalist) + def test_project_create_or_show_exists(self): + def _raise_conflict(*args, **kwargs): + raise ksc_exc.Conflict(None) + + # need to make this throw an exception... + self.projects_mock.create.side_effect = _raise_conflict + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + arglist = [ + '--or-show', + identity_fakes.project_name, + ] + verifylist = [ + ('name', identity_fakes.project_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ProjectManager.create(name, description, enabled) + self.projects_mock.get.assert_called_with(identity_fakes.project_name) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(datalist, data) + + def test_project_create_or_show_not_exists(self): + arglist = [ + '--or-show', + identity_fakes.project_name, + ] + verifylist = [ + ('name', identity_fakes.project_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(datalist, data) + class TestProjectDelete(TestProject): diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index d515bd7cdb..67467747f7 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -16,6 +16,8 @@ import copy import mock +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.common import exceptions from openstackclient.identity.v2_0 import role from openstackclient.tests import fakes @@ -142,6 +144,75 @@ def test_role_create_no_options(self): ) self.assertEqual(data, datalist) + def test_role_create_or_show_exists(self): + def _raise_conflict(*args, **kwargs): + raise ksc_exc.Conflict(None) + + # need to make this throw an exception... + self.roles_mock.create.side_effect = _raise_conflict + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + arglist = [ + '--or-show', + identity_fakes.role_name, + ] + verifylist = [ + ('role_name', identity_fakes.role_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.get(name, description, enabled) + self.roles_mock.get.assert_called_with(identity_fakes.role_name) + + # RoleManager.create(name) + self.roles_mock.create.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(datalist, data) + + def test_role_create_or_show_not_exists(self): + arglist = [ + '--or-show', + identity_fakes.role_name, + ] + verifylist = [ + ('role_name', identity_fakes.role_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.create(name) + self.roles_mock.create.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(datalist, data) + class TestRoleDelete(TestRole): From 936722d59f4e28e4f6c578074eeb75152458d821 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 11 May 2014 13:26:35 -0500 Subject: [PATCH 0062/3303] Add arg to 'server image create' tests The 'protected' column was not being checked. Also add it to image.fakes.IMAGE. Change-Id: Ie431e9871a7da78b5a3924bfbc51d5575d994d86 --- openstackclient/tests/compute/v2/test_server.py | 10 ++++++---- openstackclient/tests/image/v2/fakes.py | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 43aa7a70e6..19c13440db 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -285,13 +285,14 @@ def test_server_image_create_no_options(self): compute_fakes.server_name, ) - collist = ('id', 'is_public', 'name', 'owner') + collist = ('id', 'is_public', 'name', 'owner', 'protected') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, - False, + image_fakes.image_public, image_fakes.image_name, image_fakes.image_owner, + image_fakes.image_protected, ) self.assertEqual(datalist, data) @@ -315,13 +316,14 @@ def test_server_image_create_name(self): 'img-nam', ) - collist = ('id', 'is_public', 'name', 'owner') + collist = ('id', 'is_public', 'name', 'owner', 'protected') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, - False, + image_fakes.image_public, image_fakes.image_name, image_fakes.image_owner, + image_fakes.image_protected, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 96255cd448..1b7edf08bf 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -22,12 +22,15 @@ image_id = 'im1' image_name = 'graven' image_owner = 'baal' +image_public = False +image_protected = False IMAGE = { 'id': image_id, 'name': image_name, - 'is_public': False, + 'is_public': image_public, 'owner': image_owner, + 'protected': image_protected, } From be3cbd22bdad19d610636c170f21c311ebe80dac Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 14 Nov 2014 17:01:49 -0600 Subject: [PATCH 0063/3303] Look harder to find DevStack Change-Id: Ice5cc560513c5ada1c7a525464cd2823d5979542 --- functional/harpoon.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/functional/harpoon.sh b/functional/harpoon.sh index 76c10ffb95..4e16391794 100755 --- a/functional/harpoon.sh +++ b/functional/harpoon.sh @@ -6,8 +6,15 @@ source $FUNCTIONAL_TEST_DIR/harpoonrc OPENSTACKCLIENT_DIR=$FUNCTIONAL_TEST_DIR/.. if [[ -z $DEVSTACK_DIR ]]; then - echo "guessing location of devstack" DEVSTACK_DIR=$OPENSTACKCLIENT_DIR/../devstack + if [[ ! -d $DEVSTACK_DIR ]]; then + DEVSTACK_DIR=$HOME/devstack + if [[ ! -d $DEVSTACK_DIR ]]; then + echo "Where did you hide DevStack? Set DEVSTACK_DIR and try again" + exit 1 + fi + fi + echo "Using DevStack found at $DEVSTACK_DIR" fi function setup_credentials { From 126b2c543617866e9e1ea45ef9c5770ce5f5dda9 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 16:11:37 -0500 Subject: [PATCH 0064/3303] Add an API example base and functional test base Add examples/common.py, which is a basic common setup that mimics OSC's configuration options and logging without the rest of the CLI. Also add the functional test tooling for examples to prevent bit rot. Co-Authored-By: Dean Troyer Change-Id: Ie92b675eafd93482ddc9a8ce0b0588e23ed50c35 --- examples/common.py | 264 ++++++++++++++++++++++++++++++ functional/common/test.py | 6 + functional/tests/test_examples.py | 22 +++ 3 files changed, 292 insertions(+) create mode 100755 examples/common.py create mode 100644 functional/tests/test_examples.py diff --git a/examples/common.py b/examples/common.py new file mode 100755 index 0000000000..ad3a5e492e --- /dev/null +++ b/examples/common.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# common.py - Common bits for API examples + +# 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. + +""" +API Examples + +This is a collection of common functions used by the example scripts. +It may also be run directly as a script to do basic testing of itself. + +common.object_parser() provides the common set of command-line arguments +used in the library CLIs for setting up authentication. This should make +playing with the example scripts against a running OpenStack simpler. + +common.configure_logging() provides the same basic logging control as +the OSC shell. + +common.make_session() does the minimal loading of a Keystone authentication +plugin and creates a Keystone client Session. + +""" + +import argparse +import logging +import os +import sys +import traceback + +from keystoneclient import session as ksc_session + +from openstackclient.api import auth + + +CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' +DEFAULT_VERBOSE_LEVEL = 1 +USER_AGENT = 'osc-examples' + +PARSER_DESCRIPTION = 'A demonstration framework' + +DEFAULT_IDENTITY_API_VERSION = '2.0' + +_logger = logging.getLogger(__name__) + +# --debug sets this True +dump_stack_trace = False + + +# Generally useful stuff often found in a utils module + +def env(*vars, **kwargs): + """Search for the first defined of possibly many env vars + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +# Common Example functions + +def base_parser(parser): + """Set up some of the common CLI options + + These are the basic options that match the library CLIs so + command-line/environment setups for those also work with these + demonstration programs. + + """ + + # Global arguments + parser.add_argument( + '--os-url', + metavar='', + default=env('OS_URL'), + help='Defaults to env[OS_URL]', + ) + parser.add_argument( + '--os-region-name', + metavar='', + default=env('OS_REGION_NAME'), + help='Authentication region name (Env: OS_REGION_NAME)', + ) + parser.add_argument( + '--os-cacert', + metavar='', + default=env('OS_CACERT'), + help='CA certificate bundle file (Env: OS_CACERT)', + ) + verify_group = parser.add_mutually_exclusive_group() + verify_group.add_argument( + '--verify', + action='store_true', + help='Verify server certificate (default)', + ) + verify_group.add_argument( + '--insecure', + action='store_true', + help='Disable server certificate verification', + ) + parser.add_argument( + '--timing', + default=False, + action='store_true', + help="Print API call timing info", + ) + parser.add_argument( + '-v', '--verbose', + action='count', + dest='verbose_level', + default=1, + help='Increase verbosity of output. Can be repeated.', + ) + parser.add_argument( + '--debug', + default=False, + action='store_true', + help='show tracebacks on errors', + ) + parser.add_argument( + 'rest', + nargs='*', + help='the rest of the args', + ) + return parser + + +def configure_logging(opts): + """Typical app logging setup + + Based on OSC/cliff + + """ + + global dump_stack_trace + + root_logger = logging.getLogger('') + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + requests_log.setLevel(logging.ERROR) + + # Other modules we don't want DEBUG output for so + # don't reset them below + iso8601_log = logging.getLogger("iso8601") + iso8601_log.setLevel(logging.ERROR) + + # Always send higher-level messages to the console via stderr + console = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter(CONSOLE_MESSAGE_FORMAT) + console.setFormatter(formatter) + root_logger.addHandler(console) + + # Set logging to the requested level + dump_stack_trace = False + if opts.verbose_level == 0: + # --quiet + root_logger.setLevel(logging.ERROR) + elif opts.verbose_level == 1: + # This is the default case, no --debug, --verbose or --quiet + root_logger.setLevel(logging.WARNING) + elif opts.verbose_level == 2: + # One --verbose + root_logger.setLevel(logging.INFO) + elif opts.verbose_level >= 3: + # Two or more --verbose + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + if opts.debug: + # --debug forces traceback + dump_stack_trace = True + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + return + + +def make_session(opts, **kwargs): + """Create our base session using simple auth from ksc plugins + + The arguments required in opts varies depending on the auth plugin + that is used. This example assumes Identity v2 will be used + and selects token auth if both os_url and os_token have been + provided, otherwise it uses password. + + :param Namespace opts: + A parser options Namespace containing the authentication + options to be used + :param dict kwargs: + Additional options passed directly to Session constructor + """ + + # If no auth type is named by the user, select one based on + # the supplied options + auth_plugin_name = auth.select_auth_plugin(opts) + + (auth_plugin, auth_params) = auth.build_auth_params( + auth_plugin_name, + opts, + ) + auth_p = auth_plugin.load_from_options(**auth_params) + + session = ksc_session.Session( + auth=auth_p, + **kwargs + ) + + return session + + +# Top-level functions + +def run(opts): + """Default run command""" + + # Do some basic testing here + sys.stdout.write("Default run command\n") + sys.stdout.write("Verbose level: %s\n" % opts.verbose_level) + sys.stdout.write("Debug: %s\n" % opts.debug) + sys.stdout.write("dump_stack_trace: %s\n" % dump_stack_trace) + + +def setup(): + """Parse command line and configure logging""" + opts = base_parser( + auth.build_auth_plugins_option_parser( + argparse.ArgumentParser(description='Object API Example') + ) + ).parse_args() + configure_logging(opts) + return opts + + +def main(opts, run): + try: + return run(opts) + except Exception as e: + if dump_stack_trace: + _logger.error(traceback.format_exc(e)) + else: + _logger.error('Exception raised: ' + str(e)) + return 1 + + +if __name__ == "__main__": + opts = setup() + sys.exit(main(opts, run)) diff --git a/functional/common/test.py b/functional/common/test.py index c1bb0b101a..464844fad1 100644 --- a/functional/common/test.py +++ b/functional/common/test.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import re import shlex import subprocess @@ -19,6 +20,11 @@ from functional.common import exceptions +COMMON_DIR = os.path.dirname(os.path.abspath(__file__)) +FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..')) +ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..')) +EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples') + def execute(cmd, action, flags='', params='', fail_ok=False, merge_stderr=False): diff --git a/functional/tests/test_examples.py b/functional/tests/test_examples.py new file mode 100644 index 0000000000..fdaa26b8d4 --- /dev/null +++ b/functional/tests/test_examples.py @@ -0,0 +1,22 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functional.common import test + + +class ExampleTests(test.TestCase): + """Functional tests for running examples.""" + + def test_common(self): + # NOTE(stevemar): If an examples has a non-zero return + # code, then execute will raise an error by default. + test.execute('python', test.EXAMPLE_DIR + '/common.py --debug') From 01a5ff6d3234457fd0f8268be13fca487a1793c2 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 12 Oct 2014 22:55:41 -0500 Subject: [PATCH 0065/3303] Add more session/api examples * examples/object_api.py - Example of using the Object_Store API * examples/osc-lib.py - Minimal client to use ClientManager as a library Also add matching functional tests Change-Id: I4243a21141a821420951d4b6352d41029cdcccbc --- examples/object_api.py | 106 ++++++++++++++++++++++++++++++ examples/osc-lib.py | 102 ++++++++++++++++++++++++++++ functional/tests/test_examples.py | 6 ++ 3 files changed, 214 insertions(+) create mode 100755 examples/object_api.py create mode 100755 examples/osc-lib.py diff --git a/examples/object_api.py b/examples/object_api.py new file mode 100755 index 0000000000..5c6bd9f053 --- /dev/null +++ b/examples/object_api.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# object_api.py - Example object-store API usage + +# 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. + +""" +Object Store API Examples + +This script shows the basic use of the low-level Object Store API + +""" + +import argparse +import logging +import sys + +import common + + +from openstackclient.api import object_store_v1 as object_store +from openstackclient.identity import client as identity_client + + +LOG = logging.getLogger('') + + +def run(opts): + """Run the examples""" + + # Set up certificate verification and CA bundle + # NOTE(dtroyer): This converts from the usual OpenStack way to the single + # requests argument and is an app-specific thing because + # we want to be like OpenStackClient. + if opts.os_cacert: + verify = opts.os_cacert + else: + verify = not opts.insecure + + # get a session + # common.make_session() does all the ugly work of mapping + # CLI options/env vars to the required plugin arguments. + # The returned session will have a configured auth object + # based on the selected plugin's available options. + # So to do...oh, just go to api.auth.py and look at what it does. + session = common.make_session(opts, verify=verify) + + # Extract an endpoint + auth_ref = session.auth.get_auth_ref(session) + + if opts.os_url: + endpoint = opts.os_url + else: + endpoint = auth_ref.service_catalog.url_for( + service_type='object-store', + endpoint_type='public', + ) + + # At this point we have a working session with a configured authentication + # plugin. From here on it is the app making the decisions. Need to + # talk to two clouds? Go back and make another session but with opts + # set to different credentials. Or use a config file and load it + # directly into the plugin. This example doesn't show that (yet). + # Want to work ahead? Look into the plugin load_from_*() methods + # (start in keystoneclient/auth/base.py). + + # This example is for the Object Store API so make one + obj_api = object_store.APIv1( + session=session, + service_type='object-store', + endpoint=endpoint, + ) + + # Do useful things with it + + c_list = obj_api.container_list() + print("Name\tCount\tBytes") + for c in c_list: + print("%s\t%d\t%d" % (c['name'], c['count'], c['bytes'])) + + if len(c_list) > 0: + # See what is in the first container + o_list = obj_api.object_list(c_list[0]['name']) + print("\nObject") + for o in o_list: + print("%s" % o) + + +if __name__ == "__main__": + opts = common.base_parser( + identity_client.build_option_parser( + argparse.ArgumentParser(description='Object API Example') + ) + ).parse_args() + + common.configure_logging(opts) + sys.exit(common.main(opts, run)) diff --git a/examples/osc-lib.py b/examples/osc-lib.py new file mode 100755 index 0000000000..69fc5d9849 --- /dev/null +++ b/examples/osc-lib.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# osc-lib.py - Example using OSC as a library + +# 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. + +""" +OpenStackClient Library Examples + +This script shows the basic use of the OpenStackClient ClientManager +as a library. + +""" + +import argparse +import logging +import sys + +import common + +from openstackclient.common import clientmanager + + +LOG = logging.getLogger('') + + +def run(opts): + """Run the examples""" + + # Loop through extensions to get API versions + # Currently API versions are statically selected. Once discovery + # is working this can go away... + api_version = {} + for mod in clientmanager.PLUGIN_MODULES: + version_opt = getattr(opts, mod.API_VERSION_OPTION, None) + if version_opt: + api = mod.API_NAME + api_version[api] = version_opt + + # Set up certificate verification and CA bundle + # NOTE(dtroyer): This converts from the usual OpenStack way to the single + # requests argument and is an app-specific thing because + # we want to be like OpenStackClient. + if opts.os_cacert: + verify = opts.os_cacert + else: + verify = not opts.insecure + + # Get a ClientManager + # Collect the auth and config options together and give them to + # ClientManager and it will wrangle all of the goons into place. + client_manager = clientmanager.ClientManager( + auth_options=opts, + verify=verify, + api_version=api_version, + ) + + # At this point we have a working client manager with a configured + # session and authentication plugin. From here on it is the app + # making the decisions. Need to talk to two clouds? Make another + # client manager with different opts. Or use a config file and load it + # directly into the plugin. This example doesn't show that (yet). + + # Do useful things with it + + # Look in the object store + c_list = client_manager.object_store.container_list() + print("Name\tCount\tBytes") + for c in c_list: + print("%s\t%d\t%d" % (c['name'], c['count'], c['bytes'])) + + if len(c_list) > 0: + # See what is in the first container + o_list = client_manager.object_store.object_list(c_list[0]['name']) + print("\nObject") + for o in o_list: + print("%s" % o) + + # Look at the compute flavors + flavor_list = client_manager.compute.flavors.list() + print("\nFlavors:") + for f in flavor_list: + print("%s" % f) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='ClientManager Example') + opts = common.base_parser( + clientmanager.build_plugin_option_parser(parser), + ).parse_args() + + common.configure_logging(opts) + sys.exit(common.main(opts, run)) diff --git a/functional/tests/test_examples.py b/functional/tests/test_examples.py index fdaa26b8d4..6e0e586724 100644 --- a/functional/tests/test_examples.py +++ b/functional/tests/test_examples.py @@ -20,3 +20,9 @@ def test_common(self): # NOTE(stevemar): If an examples has a non-zero return # code, then execute will raise an error by default. test.execute('python', test.EXAMPLE_DIR + '/common.py --debug') + + def test_object_api(self): + test.execute('python', test.EXAMPLE_DIR + '/object_api.py --debug') + + def test_osc_lib(self): + test.execute('python', test.EXAMPLE_DIR + '/osc-lib.py --debug') From 2b02beaa5182e678d9da00402a7c4f137710f813 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 14 Nov 2014 17:27:58 -0600 Subject: [PATCH 0066/3303] Liberalize version matching a bit For class-loading purposes we can just use the major version, so accept that. Only Identity and Compute were affected; Compute is included just to be pedantically complete. For command groups we also just use the major version so fix Compute and the version option handling. Change the internal default for Identity to a simple '2' so it is also consistent with the rest of the world. Then comes microversioning... Closes-Bug: #1292638 Change-Id: Ibaf823b31caa288a83de38d2c258860b128b87d8 --- openstackclient/compute/client.py | 1 + openstackclient/identity/client.py | 10 ++++++---- openstackclient/shell.py | 3 ++- openstackclient/tests/test_shell.py | 6 +++--- setup.cfg | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index c87bbee700..3725350ac1 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -27,6 +27,7 @@ API_NAME = 'compute' API_VERSIONS = { '1.1': 'novaclient.v1_1.client.Client', + '1': 'novaclient.v1_1.client.Client', '2': 'novaclient.v1_1.client.Client', } diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 8050d12096..daf24e12a5 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -15,23 +15,25 @@ import logging -from keystoneclient.v2_0 import client as identity_client_v2_0 +from keystoneclient.v2_0 import client as identity_client_v2 from openstackclient.api import auth from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_IDENTITY_API_VERSION = '2.0' +DEFAULT_IDENTITY_API_VERSION = '2' API_VERSION_OPTION = 'os_identity_api_version' API_NAME = 'identity' API_VERSIONS = { - '2.0': 'openstackclient.identity.client.IdentityClientv2_0', + '2.0': 'openstackclient.identity.client.IdentityClientv2', + '2': 'openstackclient.identity.client.IdentityClientv2', '3': 'keystoneclient.v3.client.Client', } # Translate our API version to auth plugin version prefix AUTH_VERSIONS = { '2.0': 'v2', + '2': 'v2', '3': 'v3', } @@ -66,7 +68,7 @@ def build_option_parser(parser): return auth.build_auth_plugins_option_parser(parser) -class IdentityClientv2_0(identity_client_v2_0.Client): +class IdentityClientv2(identity_client_v2.Client): """Tweak the earlier client class to deal with some changes""" def __getattr__(self, name): # Map v3 'projects' back to v2 'tenants' diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 1198bae18a..ac5556affa 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -255,7 +255,8 @@ def initialize_app(self, argv): if version_opt: api = mod.API_NAME self.api_version[api] = version_opt - version = '.v' + version_opt.replace('.', '_') + # Command groups deal only with major versions + version = '.v' + version_opt.replace('.', '_').split('_')[0] cmd_group = 'openstack.' + api.replace('-', '_') + version self.command_manager.add_command_group(cmd_group) self.log.debug( diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index 837a48afd7..8656d089fd 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -38,13 +38,13 @@ DEFAULT_COMPUTE_API_VERSION = "2" -DEFAULT_IDENTITY_API_VERSION = "2.0" -DEFAULT_IMAGE_API_VERSION = "v2" +DEFAULT_IDENTITY_API_VERSION = "2" +DEFAULT_IMAGE_API_VERSION = "2" DEFAULT_VOLUME_API_VERSION = "1" DEFAULT_NETWORK_API_VERSION = "2" LIB_COMPUTE_API_VERSION = "2" -LIB_IDENTITY_API_VERSION = "2.0" +LIB_IDENTITY_API_VERSION = "2" LIB_IMAGE_API_VERSION = "1" LIB_VOLUME_API_VERSION = "1" LIB_NETWORK_API_VERSION = "2" diff --git a/setup.cfg b/setup.cfg index c0519d11d6..8ce5e59c64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,7 +131,7 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer -openstack.identity.v2_0 = +openstack.identity.v2 = catalog_list = openstackclient.identity.v2_0.catalog:ListCatalog catalog_show = openstackclient.identity.v2_0.catalog:ShowCatalog From c1b376dc333398903007f3f26ebd81378071bdcb Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Mon, 17 Nov 2014 10:27:50 +0100 Subject: [PATCH 0067/3303] Add environment variable in the os-auth-type help Help for option --os-auth-type doesn't specify what environment variable configures it. This patch fixes that. Change-Id: Id2e29e477d5ca56339bd777fb73b5af13788615b --- openstackclient/api/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index e19c6b7931..bfb2f83a47 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -136,7 +136,8 @@ def build_auth_plugins_option_parser(parser): default=utils.env('OS_AUTH_TYPE'), help='Select an auhentication type. Available types: ' + ', '.join(available_plugins) + - '. Default: selected based on --os-username/--os-token', + '. Default: selected based on --os-username/--os-token' + + ' (Env: OS_AUTH_TYPE)', choices=available_plugins ) # make sur we catch old v2.0 env values From 0d56d0178bd78057b2e8c5a1a6360ffb453d3791 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 24 Oct 2014 10:34:41 -0500 Subject: [PATCH 0068/3303] Add authentication description doc This is represents the current operation Closes-Bug: #1337422 Change-Id: I8092e7723b563647e13b6e2f0b7901a16572b6c7 --- doc/source/authentication.rst | 86 +++++++++++++++++++++++++++++++++++ doc/source/index.rst | 1 + 2 files changed, 87 insertions(+) create mode 100644 doc/source/authentication.rst diff --git a/doc/source/authentication.rst b/doc/source/authentication.rst new file mode 100644 index 0000000000..5acfe33947 --- /dev/null +++ b/doc/source/authentication.rst @@ -0,0 +1,86 @@ +============== +Authentication +============== + +OpenStackClient leverages `python-keystoneclient`_ authentication +plugins to support a number of different authentication methods. + +.. _`python-keystoneclient`: http://docs.openstack.org/developer/python-keystoneclient/authentication-plugins.html + +Authentication Process +---------------------- + +The user provides some number of authentication credential options. +If an authentication type is not provided (``--os-auth-type``), the +authentication options are examined to determine if one of the default +types can be used. If no match is found an error is reported and OSC exits. + +Note that the authentication call to the Identity service has not yet +occurred. It is deferred until the last possible moment in order to +reduce the number of unnecessary queries to the server, such as when further +processing detects an invalid command. + +Authentication Plugins +---------------------- + +The Keystone client library implements the base set of plugins. Additional +plugins may be available from the Keystone project or other sources. + +There are at least three authentication types that are always available: + +* **Password**: A project, username and password are used to identify the + user. An optional domain may also be included. This is the most common + type and is the default any time a username is supplied. An authentication + URL for the Identity service is also required. [Required: ``--os-auth-url``, + ``--os-project-name``, ``--os-username``; Optional: ``--os-password``] +* **Token**: This is slightly different from the usual token authentication + (described below as token/endpoint) in that a token and an authentication + URL are supplied and the plugin retrieves a new token. + [Required: ``--os-auth-url``, ``--os-token``] +* **Token/Endpoint**: This is the original token authentication (known as 'token + flow' in the early CLI documentation in the OpenStack wiki). It requires + a token and a direct endpoint that is used in the API call. The difference + from the new Token type is this token is used as-is, no call is made + to the Identity service from the client. This type is most often used to + bootstrap a Keystone server where the token is the ``admin_token`` configured + in ``keystone.conf``. It will also work with other services and a regular + scoped token such as one obtained from a ``token issue`` command. + [Required: ``--os-url``, ``--os-token``] +* **Others**: Other authentication plugins such as SAML, Kerberos, and OAuth1.0 + are under development and also supported. To use them, they must be selected + by supplying the ``--os-auth-type`` option. + +Detailed Process +---------------- + +The authentication process in OpenStackClient is all contained in and handled +by the ``ClientManager`` object. + +* On import ``api.auth``: + + * obtains the list of installed Keystone authentication + plugins from the ``keystoneclient.auth.plugin`` entry point. + * builds a list of authentication options from the plugins. + +* A new ``ClientManager`` is created and supplied with the set of options from the + command line and/or environment: + + * If ``--os-auth-type`` is provided and is a valid and available plugin + it is used. + * If ``--os-auth-type`` is not provided an authentication plugin + is selected based on the existing options. This is a short-circuit + evaluation, the first match wins. + + * If ``--os-endpoint`` and ``--os-token`` are both present ``token_endpoint`` + is selected + * If ``--os-username`` is supplied ``password`` is selected + * If ``--os-token`` is supplied ``token`` is selected + * If no selection has been made by now exit with error + + * Load the selected plugin class. + +* When an operation that requires authentication is attempted ``ClientManager`` + makes the actual inital request to the Identity service. + + * if ``--os-auth-url`` is not supplied for any of the types except + Token/Endpoint, exit with an error. diff --git a/doc/source/index.rst b/doc/source/index.rst index 0f92b3f018..b6145a86b6 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -14,6 +14,7 @@ Contents: releases commands plugins + authentication man/openstack Getting Started From 79653afa7b7f4c3795f6fe3c972628729cb51b4d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 14 Nov 2014 01:59:14 -0500 Subject: [PATCH 0069/3303] Add --or-show support for v3 identity resources Add --or-show for the following: * v3 roles * v3 projects * v3 domains * v3 users * v3 groups Closes-Bug: #1390389 Change-Id: Id4ef043e5fda6be49a515eb3fe138c813c393ec9 --- openstackclient/identity/v3/domain.py | 26 ++++++++++++++++---- openstackclient/identity/v3/group.py | 26 ++++++++++++++++---- openstackclient/identity/v3/project.py | 30 +++++++++++++++++------ openstackclient/identity/v3/role.py | 17 ++++++++++++- openstackclient/identity/v3/user.py | 34 +++++++++++++++++++------- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index d14da48682..1233fea5e9 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -22,8 +22,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateDomain(show.ShowOne): @@ -55,16 +57,30 @@ def get_parser(self, prog_name): dest='enabled', action='store_false', help='Disable domain') + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing domain'), + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - domain = identity_client.domains.create( - name=parsed_args.name, - description=parsed_args.description, - enabled=parsed_args.enabled, - ) + + try: + domain = identity_client.domains.create( + name=parsed_args.name, + description=parsed_args.description, + enabled=parsed_args.enabled, + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + domain = utils.find_resource(identity_client.domains, + parsed_args.name) + self.log.info('Returning existing domain %s', domain.name) + else: + raise e domain._info.pop('links') return zip(*sorted(six.iteritems(domain._info))) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index d2ffca2733..14838bba57 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -22,8 +22,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -122,7 +124,11 @@ def get_parser(self, prog_name): '--domain', metavar='', help='References the domain ID or name which owns the group') - + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing group'), + ) return parser def take_action(self, parsed_args): @@ -133,10 +139,20 @@ def take_action(self, parsed_args): parsed_args.domain).id else: domain = None - group = identity_client.groups.create( - name=parsed_args.name, - domain=domain, - description=parsed_args.description) + + try: + group = identity_client.groups.create( + name=parsed_args.name, + domain=domain, + description=parsed_args.description) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + group = utils.find_resource(identity_client.groups, + parsed_args.name, + domain_id=domain) + self.log.info('Returning existing group %s', group.name) + else: + raise e group._info.pop('links') return zip(*sorted(six.iteritems(group._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 1cdeb15067..4fcf81278f 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -21,9 +21,11 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -67,6 +69,11 @@ def get_parser(self, prog_name): help='Property to add for this project ' '(repeat option to set multiple properties)', ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing project'), + ) return parser def take_action(self, parsed_args): @@ -86,13 +93,22 @@ def take_action(self, parsed_args): if parsed_args.property: kwargs = parsed_args.property.copy() - project = identity_client.projects.create( - name=parsed_args.name, - domain=domain, - description=parsed_args.description, - enabled=enabled, - **kwargs - ) + try: + project = identity_client.projects.create( + name=parsed_args.name, + domain=domain, + description=parsed_args.description, + enabled=enabled, + **kwargs + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + project = utils.find_resource(identity_client.projects, + parsed_args.name, + domain_id=domain) + self.log.info('Returning existing project %s', project.name) + else: + raise e project._info.pop('links') return zip(*sorted(six.iteritems(project._info))) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index a3c24b7af6..7801ca6563 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -22,8 +22,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class AddRole(command.Command): @@ -149,13 +151,26 @@ def get_parser(self, prog_name): metavar='', help='New role name', ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing role'), + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = identity_client.roles.create(name=parsed_args.name) + try: + role = identity_client.roles.create(name=parsed_args.name) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + role = utils.find_resource(identity_client.roles, + parsed_args.name) + self.log.info('Returning existing role %s', role.name) + else: + raise e role._info.pop('links') return zip(*sorted(six.iteritems(role._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 9a3e00b83a..63dd3b4f58 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -21,8 +21,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -80,6 +82,11 @@ def get_parser(self, prog_name): action='store_true', help='Disable user', ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing user'), + ) return parser def take_action(self, parsed_args): @@ -106,15 +113,24 @@ def take_action(self, parsed_args): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) - user = identity_client.users.create( - name=parsed_args.name, - domain=domain_id, - default_project=project_id, - password=parsed_args.password, - email=parsed_args.email, - description=parsed_args.description, - enabled=enabled - ) + try: + user = identity_client.users.create( + name=parsed_args.name, + domain=domain_id, + default_project=project_id, + password=parsed_args.password, + email=parsed_args.email, + description=parsed_args.description, + enabled=enabled + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + user = utils.find_resource(identity_client.users, + parsed_args.name, + domain_id=domain_id) + self.log.info('Returning existing user %s', user.name) + else: + raise e user._info.pop('links') return zip(*sorted(six.iteritems(user._info))) From 25f1c8b98a197c1a8fc0820485b29996a70b1ca9 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 18 Nov 2014 11:37:03 +0000 Subject: [PATCH 0070/3303] Updated from global requirements Change-Id: Ifd9110cf94dfd2f62e59939a7be1a88e919beb36 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index de5732cf24..4038b1bb0a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,5 +12,5 @@ oslotest>=1.2.0 # Apache-2.0 requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 -testtools>=0.9.36 +testtools>=0.9.36,!=1.2.0,!=1.4.0 WebOb>=1.2.3 From 6edc9b89ed674b0c7514f450ddeba7e81666b87c Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 3 Nov 2014 10:31:04 +0800 Subject: [PATCH 0071/3303] add keystone v3 region object Co-Authored-By: Steve Martinelli Change-Id: Ia6f607630dbf507681733c3ab3b9b7c55de30f49 Closes-Bug: #1387932 --- openstackclient/identity/v3/region.py | 205 +++++++++ openstackclient/tests/identity/v3/fakes.py | 15 + .../tests/identity/v3/test_region.py | 406 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 632 insertions(+) create mode 100644 openstackclient/identity/v3/region.py create mode 100644 openstackclient/tests/identity/v3/test_region.py diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py new file mode 100644 index 0000000000..cce3417d5a --- /dev/null +++ b/openstackclient/identity/v3/region.py @@ -0,0 +1,205 @@ +# 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. +# + +"""Identity v3 Region action implementations""" + +import logging +import six + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import utils +from openstackclient.i18n import _ # noqa + + +class CreateRegion(show.ShowOne): + """Create new region""" + + log = logging.getLogger(__name__ + '.CreateRegion') + + def get_parser(self, prog_name): + parser = super(CreateRegion, self).get_parser(prog_name) + # NOTE(stevemar): The API supports an optional region ID, but that + # seems like poor UX, we will only support user-defined IDs. + parser.add_argument( + 'region', + metavar='', + help=_('New region'), + ) + parser.add_argument( + '--parent-region', + metavar='', + help=_('The parent region of new region'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New region description'), + ) + parser.add_argument( + '--url', + metavar='', + help=_('New region url'), + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + region = identity_client.regions.create( + id=parsed_args.region, + url=parsed_args.url, + parent_region=parsed_args.parent_region, + description=parsed_args.description, + ) + + region._info['region'] = region._info.pop('id') + region._info['parent_region'] = region._info.pop('parent_region_id') + region._info.pop('links', None) + return zip(*sorted(six.iteritems(region._info))) + + +class DeleteRegion(command.Command): + """Delete region""" + + log = logging.getLogger(__name__ + '.DeleteRegion') + + def get_parser(self, prog_name): + parser = super(DeleteRegion, self).get_parser(prog_name) + parser.add_argument( + 'region', + metavar='', + help=_('Region to delete'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + identity_client.regions.delete(parsed_args.region) + return + + +class ListRegion(lister.Lister): + """List regions""" + + log = logging.getLogger(__name__ + '.ListRegion') + + def get_parser(self, prog_name): + parser = super(ListRegion, self).get_parser(prog_name) + parser.add_argument( + '--parent-region', + metavar='', + help=_('Filter by parent region'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + kwargs = {} + if parsed_args.parent_region: + kwargs['parent_region_id'] = parsed_args.parent_region + + columns_headers = ('Region', 'Parent Region', 'Description', 'URL') + columns = ('ID', 'Parent Region Id', 'Description', 'URL') + + data = identity_client.regions.list(**kwargs) + return (columns_headers, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class SetRegion(command.Command): + """Set region properties""" + + log = logging.getLogger(__name__ + '.SetRegion') + + def get_parser(self, prog_name): + parser = super(SetRegion, self).get_parser(prog_name) + parser.add_argument( + 'region', + metavar='', + help=_('Region to change'), + ) + parser.add_argument( + '--parent-region', + metavar='', + help=_('New parent region of the region'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New region description'), + ) + parser.add_argument( + '--url', + metavar='', + help=_('New region url'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + if (not parsed_args.url + and not parsed_args.parent_region + and not parsed_args.description): + return + + kwargs = {} + if parsed_args.url: + kwargs['url'] = parsed_args.url + if parsed_args.description: + kwargs['description'] = parsed_args.description + if parsed_args.parent_region: + kwargs['parent_region'] = parsed_args.parent_region + + identity_client.regions.update(parsed_args.region, **kwargs) + return + + +class ShowRegion(show.ShowOne): + """Show region""" + + log = logging.getLogger(__name__ + '.ShowRegion') + + def get_parser(self, prog_name): + parser = super(ShowRegion, self).get_parser(prog_name) + parser.add_argument( + 'region', + metavar='', + help=_('Region to display'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + region = utils.find_resource(identity_client.regions, + parsed_args.region) + + region._info['region'] = region._info.pop('id') + region._info['parent_region'] = region._info.pop('parent_region_id') + region._info.pop('links', None) + return zip(*sorted(six.iteritems(region._info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index b195ed78b7..7acaa7f156 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -122,6 +122,19 @@ 'links': base_url + 'projects/' + project_id, } +region_id = 'region_one' +region_url = 'http://localhost:1111' +region_parent_region_id = 'region_two' +region_description = 'region one' + +REGION = { + 'id': region_id, + 'url': region_url, + 'description': region_description, + 'parent_region_id': region_parent_region_id, + 'links': base_url + 'regions/' + region_id, +} + role_id = 'r1' role_name = 'roller' @@ -310,6 +323,8 @@ def __init__(self, **kwargs): self.oauth1.resource_class = fakes.FakeResource(None, {}) self.projects = mock.Mock() self.projects.resource_class = fakes.FakeResource(None, {}) + self.regions = mock.Mock() + self.regions.resource_class = fakes.FakeResource(None, {}) self.roles = mock.Mock() self.roles.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_region.py b/openstackclient/tests/identity/v3/test_region.py new file mode 100644 index 0000000000..7f6ced9f2d --- /dev/null +++ b/openstackclient/tests/identity/v3/test_region.py @@ -0,0 +1,406 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +from openstackclient.identity.v3 import region +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestRegion(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestRegion, self).setUp() + + # Get a shortcut to the RegionManager Mock + self.regions_mock = self.app.client_manager.identity.regions + self.regions_mock.reset_mock() + + +class TestRegionCreate(TestRegion): + + def setUp(self): + super(TestRegionCreate, self).setUp() + + self.regions_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ) + + # Get the command object to test + self.cmd = region.CreateRegion(self.app, None) + + def test_region_create_description(self): + arglist = [ + identity_fakes.region_id, + '--description', identity_fakes.region_description, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ('description', identity_fakes.region_description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': identity_fakes.region_description, + 'id': identity_fakes.region_id, + 'parent_region': None, + 'url': None, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + def test_region_create_no_options(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'id': identity_fakes.region_id, + 'parent_region': None, + 'url': None, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + def test_region_create_parent_region_id(self): + arglist = [ + identity_fakes.region_id, + '--parent-region', identity_fakes.region_parent_region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ('parent_region', identity_fakes.region_parent_region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'id': identity_fakes.region_id, + 'parent_region': identity_fakes.region_parent_region_id, + 'url': None, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + def test_region_create_url(self): + arglist = [ + identity_fakes.region_id, + '--url', identity_fakes.region_url, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ('url', identity_fakes.region_url), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'id': identity_fakes.region_id, + 'parent_region': None, + 'url': identity_fakes.region_url, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + +class TestRegionDelete(TestRegion): + + def setUp(self): + super(TestRegionDelete, self).setUp() + + self.regions_mock.delete.return_value = None + + # Get the command object to test + self.cmd = region.DeleteRegion(self.app, None) + + def test_region_delete_no_options(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.regions_mock.delete.assert_called_with( + identity_fakes.region_id, + ) + + +class TestRegionList(TestRegion): + + def setUp(self): + super(TestRegionList, self).setUp() + + self.regions_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = region.ListRegion(self.app, None) + + def test_region_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.regions_mock.list.assert_called_with() + + collist = ('Region', 'Parent Region', 'Description', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.region_id, + identity_fakes.region_parent_region_id, + identity_fakes.region_description, + identity_fakes.region_url, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_region_list_parent_region_id(self): + arglist = [ + '--parent-region', identity_fakes.region_parent_region_id, + ] + verifylist = [ + ('parent_region', identity_fakes.region_parent_region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.regions_mock.list.assert_called_with( + parent_region_id=identity_fakes.region_parent_region_id) + + collist = ('Region', 'Parent Region', 'Description', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.region_id, + identity_fakes.region_parent_region_id, + identity_fakes.region_description, + identity_fakes.region_url, + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestRegionSet(TestRegion): + + def setUp(self): + super(TestRegionSet, self).setUp() + + self.regions_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ) + + # Get the command object to test + self.cmd = region.SetRegion(self.app, None) + + def test_region_set_no_options(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.assertNotCalled(self.regions_mock.update) + + def test_region_set_description(self): + arglist = [ + '--description', 'qwerty', + identity_fakes.region_id, + ] + verifylist = [ + ('description', 'qwerty'), + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'description': 'qwerty', + } + self.regions_mock.update.assert_called_with( + identity_fakes.region_id, + **kwargs + ) + + def test_region_set_url(self): + arglist = [ + '--url', 'new url', + identity_fakes.region_id, + ] + verifylist = [ + ('url', 'new url'), + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'url': 'new url', + } + self.regions_mock.update.assert_called_with( + identity_fakes.region_id, + **kwargs + ) + + def test_region_set_parent_region_id(self): + arglist = [ + '--parent-region', 'new_parent', + identity_fakes.region_id, + ] + verifylist = [ + ('parent_region', 'new_parent'), + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'parent_region': 'new_parent', + } + self.regions_mock.update.assert_called_with( + identity_fakes.region_id, + **kwargs + ) + + +class TestRegionShow(TestRegion): + + def setUp(self): + super(TestRegionShow, self).setUp() + + self.regions_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ) + + # Get the command object to test + self.cmd = region.ShowRegion(self.app, None) + + def test_region_show(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.regions_mock.get.assert_called_with( + identity_fakes.region_id, + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) diff --git a/setup.cfg b/setup.cfg index c0519d11d6..e0cf5d1dc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -243,6 +243,12 @@ openstack.identity.v3 = federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects + region_create = openstackclient.identity.v3.region:CreateRegion + region_delete = openstackclient.identity.v3.region:DeleteRegion + region_list = openstackclient.identity.v3.region:ListRegion + region_set = openstackclient.identity.v3.region:SetRegion + region_show = openstackclient.identity.v3.region:ShowRegion + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken request_token_create = openstackclient.identity.v3.token:CreateRequestToken From 39116bf594e780caa924c46465205a110a4c8023 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 18 Nov 2014 09:02:04 -0600 Subject: [PATCH 0072/3303] Fix volume create --image 'volume create --image' should allow an image name to be used. Closes-Bug: #1383333 Change-Id: I996d46db321eef2d75c3d19b480319f8a78c09b3 --- openstackclient/tests/volume/v1/fakes.py | 23 +++ .../tests/volume/v1/test_volume.py | 136 ++++++++++++++++++ openstackclient/volume/v1/volume.py | 18 ++- 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index c0ffbd3409..3477819072 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -65,6 +65,24 @@ 'links': extension_links, } +# NOTE(dtroyer): duplicating here the minimum image info needed to test +# volume create --image until circular references can be +# avoided by refactoring the test fakes. + +image_id = 'im1' +image_name = 'graven' + + +IMAGE = { + 'id': image_id, + 'name': image_name, +} + + +class FakeImagev1Client(object): + def __init__(self, **kwargs): + self.images = mock.Mock() + class FakeVolumev1Client(object): def __init__(self, **kwargs): @@ -91,3 +109,8 @@ def setUp(self): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + self.app.client_manager.image = FakeImagev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index f020791a4b..cc5aeff80b 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -38,6 +38,10 @@ def setUp(self): self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() + # Get a shortcut to the ImageManager Mock + self.images_mock = self.app.client_manager.image.images + self.images_mock.reset_mock() + # TODO(dtroyer): The volume create tests are incomplete, only the minimal # options and the options that require additional processing @@ -389,3 +393,135 @@ def test_volume_create_properties(self): volume_fakes.volume_type, ) self.assertEqual(datalist, data) + + def test_volume_create_image_id(self): + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.IMAGE), + loaded=True, + ) + + arglist = [ + '--image', volume_fakes.image_id, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_id), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + None, + volume_fakes.image_id, + ) + + collist = ( + 'attach_status', + 'availability_zone', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + 'detached', + volume_fakes.volume_zone, + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + '', + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_image_name(self): + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.IMAGE), + loaded=True, + ) + + arglist = [ + '--image', volume_fakes.image_name, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_name), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + None, + volume_fakes.image_id, + ) + + collist = ( + 'attach_status', + 'availability_zone', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + 'detached', + volume_fakes.volume_zone, + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + '', + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 99abac52f7..84c216d320 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -99,6 +99,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + image_client = self.app.client_manager.image volume_client = self.app.client_manager.volume source_volume = None @@ -111,12 +112,23 @@ def take_action(self, parsed_args): project = None if parsed_args.project: project = utils.find_resource( - identity_client.tenants, parsed_args.project).id + identity_client.tenants, + parsed_args.project, + ).id user = None if parsed_args.user: user = utils.find_resource( - identity_client.users, parsed_args.user).id + identity_client.users, + parsed_args.user, + ).id + + image = None + if parsed_args.image: + image = utils.find_resource( + image_client.images, + parsed_args.image, + ).id volume = volume_client.volumes.create( parsed_args.size, @@ -129,7 +141,7 @@ def take_action(self, parsed_args): project, parsed_args.availability_zone, parsed_args.property, - parsed_args.image + image, ) # Map 'metadata' column to 'properties' volume._info.update( From 6dc128636e6161851272d534d47dfbd422f65161 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 18 Nov 2014 00:16:21 -0500 Subject: [PATCH 0073/3303] Enhance the theming for modules page Also fixes a few small docstring syntax errors Change-Id: I85eb968e32c1191cf5d60d02deff2ab7f3291074 --- .gitignore | 2 ++ doc/ext/__init__.py | 0 doc/ext/apidoc.py | 43 ++++++++++++++++++++++++ doc/source/conf.py | 8 ++++- doc/source/index.rst | 4 +-- openstackclient/api/api.py | 1 + openstackclient/common/commandmanager.py | 2 +- 7 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 doc/ext/__init__.py create mode 100644 doc/ext/apidoc.py diff --git a/.gitignore b/.gitignore index 84079f2f9f..5dc75afa47 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,9 @@ AUTHORS build ChangeLog dist +# Doc related doc/build +doc/source/api/ # Development environment files .project .pydevproject diff --git a/doc/ext/__init__.py b/doc/ext/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/ext/apidoc.py b/doc/ext/apidoc.py new file mode 100644 index 0000000000..5e18385a6d --- /dev/null +++ b/doc/ext/apidoc.py @@ -0,0 +1,43 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os.path as path + +from sphinx import apidoc + + +# NOTE(blk-u): pbr will run Sphinx multiple times when it generates +# documentation. Once for each builder. To run this extension we use the +# 'builder-inited' hook that fires at the beginning of a Sphinx build. +# We use ``run_already`` to make sure apidocs are only generated once +# even if Sphinx is run multiple times. +run_already = False + + +def run_apidoc(app): + global run_already + if run_already: + return + run_already = True + + package_dir = path.abspath(path.join(app.srcdir, '..', '..', + 'openstackclient')) + source_dir = path.join(app.srcdir, 'api') + apidoc.main(['apidoc', package_dir, '-f', + '-H', 'openstackclient Modules', + '-o', source_dir]) + + +def setup(app): + app.connect('builder-inited', run_apidoc) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7c7a00b313..e805a98767 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -22,6 +22,10 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +# NOTE(blk-u): Path for our Sphinx extension, remove when +# https://launchpad.net/bugs/1260495 is fixed. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -32,7 +36,9 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', - 'oslosphinx'] + 'oslosphinx', + 'ext.apidoc', + ] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] diff --git a/doc/source/index.rst b/doc/source/index.rst index 0f92b3f018..a3c6516d30 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -37,8 +37,8 @@ the openstack/python-openstackclient project using `Gerrit`_. .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://wiki.openstack.org/GerritWorkflow -Index -===== +Indices and Tables +================== * :ref:`genindex` * :ref:`modindex` diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 72a66e1cb4..67386aaf0b 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -227,6 +227,7 @@ def find_attr( attribute to use for resource search :param string resource: plural of the object resource name; defaults to path + For example: n = find(netclient, 'network', 'networks', 'matrix') """ diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 2d9575d9d2..b34bf7d6f3 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -28,7 +28,7 @@ class CommandManager(cliff.commandmanager.CommandManager): """Add additional functionality to cliff.CommandManager Load additional command groups after initialization - Add *_command_group() methods + Add _command_group() methods """ def __init__(self, namespace, convert_underscores=True): From 254910d3ce34c954551a0827aa8727d6367f48f3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 16:42:30 -0600 Subject: [PATCH 0074/3303] Begin copying wiki command list here * Sort by command objects * Drop the comparison to the project CLIs * Minor updates to command help to match docs Initially include the cross-API commands to establish the structure and format. Change-Id: I77a7b3c89e088b66aa62941e29ce0b65b532285b --- doc/source/command-list.rst | 9 ++ doc/source/command-objects/extension.rst | 41 ++++++ doc/source/command-objects/limits.rst | 28 ++++ doc/source/command-objects/quota.rst | 157 ++++++++++++++++++++++ doc/source/commands.rst | 14 +- doc/source/index.rst | 1 + doc/source/releases.rst | 2 +- openstackclient/common/extension.py | 12 +- openstackclient/common/quota.py | 2 +- openstackclient/identity/v2_0/endpoint.py | 2 +- 10 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 doc/source/command-list.rst create mode 100644 doc/source/command-objects/extension.rst create mode 100644 doc/source/command-objects/limits.rst create mode 100644 doc/source/command-objects/quota.rst diff --git a/doc/source/command-list.rst b/doc/source/command-list.rst new file mode 100644 index 0000000000..c4045b0406 --- /dev/null +++ b/doc/source/command-list.rst @@ -0,0 +1,9 @@ +============ +Command List +============ + +.. toctree:: + :glob: + :maxdepth: 2 + + command-objects/* diff --git a/doc/source/command-objects/extension.rst b/doc/source/command-objects/extension.rst new file mode 100644 index 0000000000..8f39a62529 --- /dev/null +++ b/doc/source/command-objects/extension.rst @@ -0,0 +1,41 @@ +========= +extension +========= + +Many OpenStack server APIs include API extensions that enable +additional functionality. + +extension list +-------------- + +List API extensions + +.. program:: extension list +.. code:: bash + + os extension list + [--compute] + [--identity] + [--network] + [--volume] + [--long] + +.. option:: --compute + + List extensions for the Compute API + +.. option:: --identity + + List extensions for the Identity API + +.. option:: --network + + List extensions for the Network API + +.. option:: --volume + + List extensions for the Volume API + +.. option:: --long + + List additional fields in output diff --git a/doc/source/command-objects/limits.rst b/doc/source/command-objects/limits.rst new file mode 100644 index 0000000000..ac388e0f56 --- /dev/null +++ b/doc/source/command-objects/limits.rst @@ -0,0 +1,28 @@ +====== +limits +====== + +The Compute and Volume APIs have resource usage limits. + +limits show +----------- + +Show compute and volume limits + +.. program:: limits show +.. code:: bash + + os limits show + --absolute [--reserved] | --rate + +.. option:: --absolute + + Show absolute limits + +.. option:: --rate + + Show rate limits + +.. option:: --reserved + + Include reservations count [only valid with :option:`--absolute`] diff --git a/doc/source/command-objects/quota.rst b/doc/source/command-objects/quota.rst new file mode 100644 index 0000000000..ba6712c083 --- /dev/null +++ b/doc/source/command-objects/quota.rst @@ -0,0 +1,157 @@ +===== +quota +===== + +Resource quotas appear in multiple APIs, OpenStackClient presents them as a single object with multiple properties. + +quota set +--------- + +Set quotas for project + +.. program:: quota set +.. code:: bash + + os quota set + # Compute settings + [--cores ] + [--fixed-ips ] + [--floating-ips ] + [--injected-file-size ] + [--injected-files ] + [--instances ] + [--key-pairs ] + [--properties ] + [--ram ] + + # Volume settings + [--gigabytes ] + [--snapshots ] + [--volumes ] + + + +Set quotas for class + +.. code:: bash + + os quota set + --class + # Compute settings + [--cores ] + [--fixed-ips ] + [--floating-ips ] + [--injected-file-size ] + [--injected-files ] + [--instances ] + [--key-pairs ] + [--properties ] + [--ram ] + + # Volume settings + [--gigabytes ] + [--snapshots ] + [--volumes ] + + + +.. option:: --class + + Set quotas for ```` + +.. option:: --properties + + New value for the properties quota + +.. option:: --ram + + New value for the ram quota + +.. option:: --secgroup-rules + + New value for the secgroup-rules quota + +.. option:: --instances + + New value for the instances quota + +.. option:: --key-pairs + + New value for the key-pairs quota + +.. option:: --fixed-ips + + New value for the fixed-ips quota + +.. option:: --secgroups + + New value for the secgroups quota + +.. option:: --injected-file-size + + New value for the injected-file-size quota + +.. option:: --floating-ips + + New value for the floating-ips quota + +.. option:: --injected-files + + New value for the injected-files quota + +.. option:: --cores + + New value for the cores quota + +.. option:: --injected-path-size + + New value for the injected-path-size quota + +.. option:: --gigabytes + + New value for the gigabytes quota + +.. option:: --volumes + + New value for the volumes quota + +.. option:: --snapshots + + New value for the snapshots quota + +quota show +---------- + +Show quotas for project + +.. program:: quota show +.. code:: bash + + os quota show + [--default] + + + +.. option:: --default + + Show default quotas for :ref:`\ ` + +.. _quota_show-project: +.. describe:: + + Show quotas for class + +.. code:: bash + + os quota show + --class + + +.. option:: --class + + Show quotas for :ref:`\ ` + +.. _quota_show-class: +.. describe:: + + Class to show \ No newline at end of file diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 8857f3d55d..37c62e8ec3 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -1,8 +1,4 @@ -======== -Commands -======== - - +================= Command Structure ================= @@ -83,7 +79,7 @@ referring to both Compute and Volume quotas. * ``credential``: Identity - specific to identity providers * ``domain``: Identity - a grouping of projects * ``endpoint``: Identity - the base URL used to contact a specific service -* ``extension``: Compute, Identity, Volume - additional APIs available +* ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc * ``group``: Identity - a grouping of users * ``host``: Compute - the physical computer running a hypervisor @@ -93,13 +89,13 @@ referring to both Compute and Volume quotas. * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server * ``keypair``: Compute - an SSH public key -* ``limits``: Compute, Volume - resource usage limits +* ``limits``: (**Compute**, **Volume**) resource usage limits * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: Object Store - a single file in the Object Store * ``policy``: Identity - determines authorization * ``project``: Identity - the owner of a group of resources -* ``quota``: Compute, Volume - limit on resource usage +* ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules @@ -149,7 +145,7 @@ Those actions with an opposite action are noted in parens if applicable. Implementation -============== +-------------- The command structure is designed to support seamless addition of plugin command modules via ``setuptools`` entry points. The plugin commands must diff --git a/doc/source/index.rst b/doc/source/index.rst index b6145a86b6..2edbb35d57 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -12,6 +12,7 @@ Contents: :maxdepth: 1 releases + command-list commands plugins authentication diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 89b4ad111e..909c362eaf 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -26,7 +26,7 @@ Release Notes * add ``object create`` and ``object delete`` commands * add initial support for global ``--timing`` options (similar to nova CLI) * complete Python 3 compatibility -* fix ``server resize` command +* fix ``server resize`` command * add authentication via ``--os-trust-id`` for Identity v3 * Add initial support for Network API, ``network create|delete|list|show`` diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index be7426daa0..dad7ed6285 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -24,7 +24,7 @@ class ListExtension(lister.Lister): - """List extension command""" + """List API extensions""" log = logging.getLogger(__name__ + '.ListExtension') @@ -40,11 +40,6 @@ def get_parser(self, prog_name): action='store_true', default=False, help='List extensions for the Identity API') - parser.add_argument( - '--long', - action='store_true', - default=False, - help='List additional fields in output') parser.add_argument( '--network', action='store_true', @@ -55,6 +50,11 @@ def get_parser(self, prog_name): action='store_true', default=False, help='List extensions for the Volume API') + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index e011fd3619..edf4ffdb98 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -72,7 +72,7 @@ def get_parser(self, prog_name): COMPUTE_QUOTAS.items(), VOLUME_QUOTAS.items()): parser.add_argument( '--%s' % v, - metavar='' % v, + metavar='<%s>' % v, type=int, help='New value for the %s quota' % v, ) diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index f7d7883109..c5189b62e7 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -28,7 +28,7 @@ class CreateEndpoint(show.ShowOne): - """Create endpoint command""" + """Create endpoint""" log = logging.getLogger(__name__ + '.CreateEndpoint') From 9eb30efbf3ee9105520e857fbd16363523ae44ee Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 17:52:37 -0600 Subject: [PATCH 0075/3303] Command object docs: aggregate, console *, keypair aggregate console log console url keypair Change-Id: Iec9b8404ed5febd061a5dfd674b76aaa8aba67bc --- doc/source/command-objects/aggregate.rst | 150 +++++++++++++++++++++ doc/source/command-objects/console-log.rst | 25 ++++ doc/source/command-objects/console-url.rst | 33 +++++ doc/source/command-objects/keypair.rst | 71 ++++++++++ doc/source/commands.rst | 8 +- openstackclient/compute/v2/aggregate.py | 22 +-- openstackclient/compute/v2/console.py | 4 +- openstackclient/compute/v2/keypair.py | 20 +-- 8 files changed, 306 insertions(+), 27 deletions(-) create mode 100644 doc/source/command-objects/aggregate.rst create mode 100644 doc/source/command-objects/console-log.rst create mode 100644 doc/source/command-objects/console-url.rst create mode 100644 doc/source/command-objects/keypair.rst diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst new file mode 100644 index 0000000000..474d811f88 --- /dev/null +++ b/doc/source/command-objects/aggregate.rst @@ -0,0 +1,150 @@ +========= +aggregate +========= + +Server aggregates provide a mechanism to group servers according to certain +criteria. + +aggregate add host +------------------ + +Add host to aggregate + +.. program aggregate add host +.. code:: bash + + os aggregate add host + + + +.. _aggregate_add_host-aggregate: +.. describe:: + + Aggregate (name or ID) + +.. describe:: + + Host to add to :ref:`\ ` + +aggregate create +---------------- + +Create a new aggregate + +.. program aggregate create +.. code:: bash + + os aggregate create + [--zone ] + [--property ] + + +.. option:: --zone + + Availability zone name + +.. option:: --property + + Property to add to this aggregate (repeat option to set multiple properties) + +.. describe:: + + New aggregate name + +aggregate delete +---------------- + +Delete an existing aggregate + +.. program aggregate delete +.. code:: bash + + os aggregate delete + + +.. describe:: + + Aggregate to delete (name or ID) + +aggregate list +-------------- + +List all aggregates + +.. program aggregate list +.. code:: bash + + os aggregate list + [--long] + +.. option:: --long + + List additional fields in output + +aggregate remove host +--------------------- + +Remove host from aggregate + +.. program aggregate remove host +.. code:: bash + + os aggregate remove host + + + +.. _aggregate_remove_host-aggregate: +.. describe:: + + Aggregate (name or ID) + +.. option:: + + Host to remove from :ref:`\ ` + +aggregate set +------------- + +Set aggregate properties + +.. program aggregate set +.. code:: bash + + os aggregate set + [--name ] + [--zone ] + [--property ] + + +.. option:: --name + + Set aggregate name + +.. option:: --zone + + Set availability zone name + +.. option:: --property + + Property to set on :ref:`\ ` + (repeat option to set multiple properties) + +.. _aggregate_set-aggregate: +.. describe:: + + Aggregate to modify (name or ID) + +aggregate show +-------------- + +Show a specific aggregate + +.. program aggregate show +.. code:: bash + + os aggregate show + + +.. describe:: + + Aggregate to show (name or ID) diff --git a/doc/source/command-objects/console-log.rst b/doc/source/command-objects/console-log.rst new file mode 100644 index 0000000000..8e56a07382 --- /dev/null +++ b/doc/source/command-objects/console-log.rst @@ -0,0 +1,25 @@ +=========== +console log +=========== + +Server console text dump + +console log show +---------------- + +Show server's console output + +.. program:: console log show +.. code:: bash + + os console log show + [--lines ] + + +.. option:: --lines + + Number of lines to display from the end of the log (default=all) + +.. describe:: + + Server to show log console log (name or ID) diff --git a/doc/source/command-objects/console-url.rst b/doc/source/command-objects/console-url.rst new file mode 100644 index 0000000000..45a0a52717 --- /dev/null +++ b/doc/source/command-objects/console-url.rst @@ -0,0 +1,33 @@ +=========== +console url +=========== + +Server remote console URL + +console url show +---------------- + +Show server's remote console URL + +.. program:: console url show +.. code:: bash + + os console url show + [--novnc | --xvpvnc | --spice] + + +.. option:: --novnc + + Show noVNC console URL (default) + +.. option:: --xvpvnc + + Show xpvnc console URL + +.. option:: --spice + + Show SPICE console URL + +.. describe:: + + Server to show URL (name or ID) diff --git a/doc/source/command-objects/keypair.rst b/doc/source/command-objects/keypair.rst new file mode 100644 index 0000000000..9ba0ee8f04 --- /dev/null +++ b/doc/source/command-objects/keypair.rst @@ -0,0 +1,71 @@ +======= +keypair +======= + +The badly named keypair is really the public key of an OpenSSH key pair to be +used for access to created servers. + +keypair create +-------------- + +Create new public key + +.. program keypair create +.. code:: bash + + os keypair create + [--public-key ] + + +.. option:: --public-key + + Filename for public key to add + +.. describe:: + + New public key name + +keypair delete +-------------- + +Delete a public key + +.. program keypair delete +.. code:: bash + + os keypair delete + + +.. describe:: + + Public key to delete + +keypair list +------------ + +List public key fingerprints + +.. program keypair list +.. code:: bash + + os keypair list + +keypair show +------------ + +Show public key details + +.. program keypair show +.. code:: bash + + os keypair show + [--public-key] + + +.. option:: --public-key + + Show only bare public key + +.. describe:: + + Public key to show diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 37c62e8ec3..506fbfc28f 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -70,10 +70,10 @@ the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token -* ``aggregate``: Compute - a grouping of servers +* ``aggregate``: (**Compute**) a grouping of servers * ``backup``: Volume - a volume copy -* ``console log``: Compute - a text dump of a server's console -* ``console url``: Compute - a URL to a server's remote console +* ``console log``: (**Compute**) server console text dump +* ``console url``: (**Compute**) server remote console URL * ``consumer``: Identity - OAuth-based delegatee * ``container``: Object Store - a grouping of objects * ``credential``: Identity - specific to identity providers @@ -88,7 +88,7 @@ referring to both Compute and Volume quotas. * ``image``: Image - a disk image * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server -* ``keypair``: Compute - an SSH public key +* ``keypair``: (**Compute**) an SSH public key * ``limits``: (**Compute**, **Volume**) resource usage limits * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 8fff4e6fcf..bfc2b115aa 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -37,12 +37,12 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate', + help='Aggregate (name or ID)', ) parser.add_argument( 'host', metavar='', - help='Host to add to aggregate', + help='Host to add to ', ) return parser @@ -119,7 +119,7 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate to delete', + help='Aggregate to delete (name or ID)', ) return parser @@ -197,12 +197,12 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate', + help='Aggregate (name or ID)', ) parser.add_argument( 'host', metavar='', - help='Host to remove from aggregate', + help='Host to remove from ', ) return parser @@ -235,23 +235,23 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate to display', + help='Aggregate to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', - help='New aggregate name', + metavar='', + help='Set aggregate name', ) parser.add_argument( "--zone", metavar="", - help="Availability zone name", + help="Set availability zone name", ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, - help='Property to add/change for this aggregate ' + help='Property to set on ' '(repeat option to set multiple properties)', ) return parser @@ -299,7 +299,7 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate to display', + help='Aggregate to show (name or ID)', ) return parser diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 8206f3024b..082a3a0c0f 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -35,7 +35,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help='Server to show console log (name or ID)', ) parser.add_argument( '--lines', @@ -76,7 +76,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help='Server to show URL (name or ID)', ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 22c07ef7f3..6a158d86ff 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -29,7 +29,7 @@ class CreateKeypair(show.ShowOne): - """Create new keypair""" + """Create new public key""" log = logging.getLogger(__name__ + '.CreateKeypair') @@ -38,7 +38,7 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New keypair name', + help='New public key name', ) parser.add_argument( '--public-key', @@ -80,7 +80,7 @@ def take_action(self, parsed_args): class DeleteKeypair(command.Command): - """Delete a keypair""" + """Delete a public key""" log = logging.getLogger(__name__ + '.DeleteKeypair') @@ -88,8 +88,8 @@ def get_parser(self, prog_name): parser = super(DeleteKeypair, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', - help='Name of keypair to delete', + metavar='', + help='Public key to delete', ) return parser @@ -101,7 +101,7 @@ def take_action(self, parsed_args): class ListKeypair(lister.Lister): - """List keypairs""" + """List public key fingerprints""" log = logging.getLogger(__name__ + ".ListKeypair") @@ -121,7 +121,7 @@ def take_action(self, parsed_args): class ShowKeypair(show.ShowOne): - """Show keypair details""" + """Show public key details""" log = logging.getLogger(__name__ + '.ShowKeypair') @@ -129,14 +129,14 @@ def get_parser(self, prog_name): parser = super(ShowKeypair, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', - help='Name of keypair to display', + metavar='', + help='Public key to show', ) parser.add_argument( '--public-key', action='store_true', default=False, - help='Include public key in output', + help='Show only bare public key', ) return parser From 3ad16897bb6c352790c8d928dfb016c2e8cb59c4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 21 Nov 2014 18:33:29 +0000 Subject: [PATCH 0076/3303] Updated from global requirements Change-Id: I2ae7af05f2052d8a8878e6477c8746cfdd1b74fa --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 4038b1bb0a..ac3e715093 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,5 +12,5 @@ oslotest>=1.2.0 # Apache-2.0 requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 -testtools>=0.9.36,!=1.2.0,!=1.4.0 +testtools>=0.9.36,!=1.2.0 WebOb>=1.2.3 From 04d30c1855f1229d986c7ad3bdae43a2b2d38990 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 18 Nov 2014 15:11:32 -0600 Subject: [PATCH 0077/3303] Command object docs: project, role, user project role user user role Change-Id: I445e09a3ffb69114912ae562a9285963a636bfd1 --- doc/source/command-objects/project.rst | 155 +++++++++++++++++ doc/source/command-objects/role.rst | 180 ++++++++++++++++++++ doc/source/command-objects/user-role.rst | 25 +++ doc/source/command-objects/user.rst | 204 +++++++++++++++++++++++ doc/source/commands.rst | 7 +- openstackclient/identity/v2_0/project.py | 22 +-- openstackclient/identity/v2_0/role.py | 37 ++-- openstackclient/identity/v2_0/user.py | 40 ++--- openstackclient/identity/v3/project.py | 38 ++--- openstackclient/identity/v3/role.py | 80 ++++----- openstackclient/identity/v3/user.py | 72 ++++---- 11 files changed, 718 insertions(+), 142 deletions(-) create mode 100644 doc/source/command-objects/project.rst create mode 100644 doc/source/command-objects/role.rst create mode 100644 doc/source/command-objects/user-role.rst create mode 100644 doc/source/command-objects/user.rst diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst new file mode 100644 index 0000000000..ba741d1dc1 --- /dev/null +++ b/doc/source/command-objects/project.rst @@ -0,0 +1,155 @@ +======= +project +======= + +Identity v2, v3 + +project create +-------------- + +Create new project + +.. program:: project create +.. code:: bash + + os project create + [--domain ] + [--description ] + [--enable | --disable] + [--property ] + + +.. option:: --domain + + Domain owning the project (name or ID) + + .. versionadded:: 3 + +.. option:: --description + + Project description + +.. option:: --enable + + Enable project (default) + +.. option:: --disable + + Disable project + +.. option:: --property + + Add a property to :ref:`\ ` + (repeat option to set multiple properties) + +.. _project_create-name: +.. describe:: + + New project name + +project delete +-------------- + +Delete an existing project + +.. program:: project delete +.. code:: bash + + os project delete + + +.. _project_delete-project: +.. describe:: + + Project to delete (name or ID) + +project list +------------ + +List projects + +.. program:: project list +.. code:: bash + + os project list + [--domain ] + [--long] + +.. option:: --domain + + Filter projects by :option:`\ <--domain>` (name or ID) + + .. versionadded:: 3 + +.. option:: --long + + List additional fields in output + +project set +----------- + +Set project properties + +.. program:: project set +.. code:: bash + + os project set + [--name ] + [--domain ] + [--description ] + [--enable | --disable] + [--property ] + + +.. option:: --name + + Set project name + +.. option:: --domain + + Set domain owning :ref:`\ ` (name or ID) + + .. versionadded:: 3 + +.. option:: --description + + Set project description + +.. option:: --enable + + Enable project (default) + +.. option:: --disable + + Disable project + +.. option:: --property + + Set a property on :ref:`\ ` + (repeat option to set multiple properties) + +.. _project_set-project: +.. describe:: + + Project to modify (name or ID) + +project show +------------ + +.. program:: project show +.. code:: bash + + os project show + [--domain ] + + +.. option:: --domain + + Domain owning :ref:`\ ` (name or ID) + + .. versionadded:: 3 + +.. _project_show-project: +.. describe:: + + Project to show (name or ID) diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst new file mode 100644 index 0000000000..1cc80d7d98 --- /dev/null +++ b/doc/source/command-objects/role.rst @@ -0,0 +1,180 @@ +==== +role +==== + +Identity v2, v3 + +role add +-------- + +Add role to a user or group in a project or domain + +.. program:: role add +.. code:: bash + + os role add + --domain | --project + --user | --group + + +.. option:: --domain + + Include `` (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Include `` (name or ID) + +.. option:: --user + + Include `` (name or ID) + +.. option:: --group + + Include `` (name or ID) + + .. versionadded:: 3 + +.. describe:: + + Role to add to ``:`` (name or ID) + +role create +----------- + +Create new role + +.. program:: role create +.. code:: bash + + os role create + + +.. describe:: + + New role name + +role delete +----------- + +Delete an existing role + +.. program:: role delete +.. code:: bash + + os role delete + + +.. option:: + + Role to delete (name or ID) + +role list +--------- + +List roles + +.. program:: role list +.. code:: bash + + os role list + [--domain | --project | --group ] + +.. option:: --domain + + Filter roles by (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Filter roles by (name or ID) + + .. versionadded:: 3 + +.. option:: --user + + Filter roles by (name or ID) + + .. versionadded:: 3 + +.. option:: --group + + Filter roles by (name or ID) + + .. versionadded:: 3 + +role remove +----------- + +Remove role from domain/project : user/group + +.. program:: role remove +.. code:: bash + + os role remove + [--domain | --project | --group ] + + +.. option:: --domain + + Include `` (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Include `` (name or ID) + +.. option:: --user + + Include `` (name or ID) + +.. option:: --group + + Include `` (name or ID) + + .. versionadded:: 3 + +.. describe:: + + Role to remove from ``:`` (name or ID) + +role set +-------- + +Set role properties + +.. versionadded:: 3 + +.. program:: role set +.. code:: bash + + os role set + [--name ] + + +.. option:: --name + + Set role name + +.. describe:: + + Role to modify (name or ID) + +role show +--------- + +.. program:: role show +.. code:: bash + + os role show + + +.. describe:: + + Role to show (name or ID) diff --git a/doc/source/command-objects/user-role.rst b/doc/source/command-objects/user-role.rst new file mode 100644 index 0000000000..a25e90ff8f --- /dev/null +++ b/doc/source/command-objects/user-role.rst @@ -0,0 +1,25 @@ +========= +user role +========= + +user role list +-------------- + +List user-role assignments + +*Removed in version 3.* + +.. program:: user role list +.. code:: bash + + os user role list + [--project ] + [] + +.. option:: --project + + Filter users by `` (name or ID) + +.. describe:: + + User to list (name or ID) diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst new file mode 100644 index 0000000000..53becf2799 --- /dev/null +++ b/doc/source/command-objects/user.rst @@ -0,0 +1,204 @@ +==== +user +==== + +Identity v2, v3 + +user create +----------- + +Create new user + +.. program:: user create +.. code:: bash + + os user create + [--domain ] + [--project ] + [--password ] + [--password-prompt] + [--email ] + [--description ] + [--enable | --disable] + [--or-show] + + +.. option:: --domain + + Default domain (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Default project (name or ID) + +.. option:: --password + + Set user password + +.. option:: --password-prompt + + Prompt interactively for password + +.. option:: --email + + Set user email address + +.. option:: --description + + User description + + .. versionadded:: 3 + +.. option:: --enable + + Enable user (default) + +.. option:: --disable + + Disable user + +.. option:: --or-show + + Return existing user + + If the username already exist return the existing user data and do not fail. + +.. describe:: + + New user name + +user delete +----------- + +Delete user + +.. program:: user delete +.. code:: bash + + os user delete + + +.. describe:: + + User to delete (name or ID) + +user list +--------- + +List users + +.. program:: user list +.. code:: bash + + os user list + [--domain ] + [--project ] + [--group ] + [--long] + +.. option:: --domain + + Filter users by `` (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Filter users by `` (name or ID) + + *Removed in version 3.* + +.. option:: --group + + Filter users by `` membership (name or ID) + + .. versionadded:: 3 + +.. option:: --long + + List additional fields in output + +user set +-------- + +Set user properties + +.. program:: user set +.. code:: bash + + os user set + [--name ] + [--domain ] + [--project ] + [--password ] + [--email ] + [--description ] + [--enable|--disable] + + +.. option:: --name + + Set user name + +.. option:: --domain + + Set default domain (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Set default project (name or ID) + +.. option:: --password + + Set user password + +.. option:: --password-prompt + + Prompt interactively for password + +.. option:: --email + + Set user email address + +.. option:: --description + + Set user description + + .. versionadded:: 3 + +.. option:: --enable + + Enable user (default) + +.. option:: --disable + + Disable user + +.. describe:: + + User to modify (name or ID) + +user show +--------- + +.. program:: user show +.. code:: bash + + os user show + [--domain ] + + +.. option:: --domain + + Domain owning :ref:`\ ` (name or ID) + + .. versionadded:: 3 + +.. _user_show-user: +.. describe:: + + User to show (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 506fbfc28f..250a8039d8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -94,7 +94,7 @@ referring to both Compute and Volume quotas. * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: Object Store - a single file in the Object Store * ``policy``: Identity - determines authorization -* ``project``: Identity - the owner of a group of resources +* ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization @@ -103,8 +103,9 @@ referring to both Compute and Volume quotas. * ``server``: Compute - a virtual machine instance * ``service``: Identity - a cloud service * ``snapshot``: Volume - a point-in-time copy of a volume -* ``token``: Identity - the magic text used to determine access -* ``user``: Identity - individuals using cloud resources +* ``token``: (**Identity**) a bearer token managed by Identity service +* ``user``: (**Identity**) individual cloud resources users +* ``user role``: (**Identity**) roles assigned to a user * ``volume``: Volume - block volumes * ``volume type``: Volume - deployment-specific types of volumes available diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 2d66b400d4..df759ce6a8 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -42,8 +42,8 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar='', - help=_('New project description'), + metavar='', + help=_('Project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -60,7 +60,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Property to add for this project ' + help=_('Add a property to ' '(repeat option to set multiple properties)'), ) parser.add_argument( @@ -104,7 +104,7 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete project""" + """Delete an existing project""" log = logging.getLogger(__name__ + '.DeleteProject') @@ -169,17 +169,17 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Project to change (name or ID)'), + help=_('Project to modify (name or ID)'), ) parser.add_argument( '--name', - metavar='', - help=_('New project name'), + metavar='', + help=_('Set project name'), ) parser.add_argument( '--description', - metavar='', - help=_('New project description'), + metavar='', + help=_('Set project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -196,7 +196,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Property to add for this project ' + help=_('Set a project property ' '(repeat option to set multiple properties)'), ) return parser @@ -249,7 +249,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Project to display (name or ID)')) + help=_('Project to show (name or ID)')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index df69e857ad..cec95095cc 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -38,17 +38,20 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role name or ID to add to user')) + help=_('Role to add to : (name or ID)'), + ) parser.add_argument( '--project', metavar='', required=True, - help=_('Include project (name or ID)')) + help=_('Include (name or ID)'), + ) parser.add_argument( '--user', metavar='', required=True, - help=_('Name or ID of user to include')) + help=_('Include (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -80,8 +83,9 @@ def get_parser(self, prog_name): parser = super(CreateRole, self).get_parser(prog_name) parser.add_argument( 'role_name', - metavar='', - help=_('New role name')) + metavar='', + help=_('New role name'), + ) parser.add_argument( '--or-show', action='store_true', @@ -110,7 +114,7 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete existing role""" + """Delete an existing role""" log = logging.getLogger(__name__ + '.DeleteRole') @@ -119,7 +123,8 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Name or ID of role to delete')) + help=_('Role to delete (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -162,11 +167,13 @@ def get_parser(self, prog_name): 'user', metavar='', nargs='?', - help=_('Name or ID of user to include')) + help=_('User to list (name or ID)'), + ) parser.add_argument( '--project', metavar='', - help=_('Include project (name or ID)')) + help=_('Filter users by (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -227,17 +234,20 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role name or ID to remove from user')) + help=_('Role to remove from : (name or ID)'), + ) parser.add_argument( '--project', metavar='', required=True, - help=_('Project to include (name or ID)')) + help=_('Include (name or ID)'), + ) parser.add_argument( '--user', metavar='', required=True, - help=_('Name or ID of user')) + help=_('Include (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -265,7 +275,8 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Name or ID of role to display')) + help=_('Role to show (name or ID)'), + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 2ebcba188e..955e19e770 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -36,13 +36,18 @@ def get_parser(self, prog_name): parser = super(CreateUser, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', + metavar='', help=_('New user name'), ) + parser.add_argument( + '--project', + metavar='', + help=_('Default project (name or ID)'), + ) parser.add_argument( '--password', - metavar='', - help=_('New user password'), + metavar='', + help=_('Set user password'), ) parser.add_argument( '--password-prompt', @@ -52,13 +57,8 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help=_('New user email address'), - ) - parser.add_argument( - '--project', - metavar='', - help=_('Set default project (name or ID)'), + metavar='', + help=_('Set user email address'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -258,13 +258,18 @@ def get_parser(self, prog_name): ) parser.add_argument( '--name', - metavar='', - help=_('New user name'), + metavar='', + help=_('Set user name'), + ) + parser.add_argument( + '--project', + metavar='', + help=_('Set default project (name or ID)'), ) parser.add_argument( '--password', metavar='', - help=_('New user password'), + help=_('Set user password'), ) parser.add_argument( '--password-prompt', @@ -274,13 +279,8 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help=_('New user email address'), - ) - parser.add_argument( - '--project', - metavar='', - help=_('New default project (name or ID)'), + metavar='', + help=_('Set user email address'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 4fcf81278f..3b0e92fd8c 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -43,13 +43,13 @@ def get_parser(self, prog_name): ) parser.add_argument( '--domain', - metavar='', + metavar='', help='Domain owning the project (name or ID)', ) parser.add_argument( '--description', - metavar='', - help='New project description', + metavar='', + help='Project description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -66,7 +66,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' + help='Add a property to ' '(repeat option to set multiple properties)', ) parser.add_argument( @@ -115,7 +115,7 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete project""" + """Delete an existing project""" log = logging.getLogger(__name__ + '.DeleteProject') @@ -148,17 +148,17 @@ class ListProject(lister.Lister): def get_parser(self, prog_name): parser = super(ListProject, self).get_parser(prog_name) + parser.add_argument( + '--domain', + metavar='', + help='Filter projects by (name or ID)', + ) parser.add_argument( '--long', action='store_true', default=False, help='List additional fields in output', ) - parser.add_argument( - '--domain', - metavar='', - help='Filter by a specific domain (name or ID)', - ) return parser def take_action(self, parsed_args): @@ -190,22 +190,22 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to change (name or ID)', + help='Project to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', - help='New project name', + metavar='', + help='Set project name', ) parser.add_argument( '--domain', - metavar='', - help='New domain owning the project (name or ID)', + metavar='', + help='Set domain owning (name or ID)', ) parser.add_argument( '--description', - metavar='', - help='New project description', + metavar='', + help='Set project description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -222,7 +222,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' + help='Set a property on ' '(repeat option to set multiple properties)', ) return parser @@ -278,7 +278,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where project resides (name or ID)', + help='Domain owning (name or ID)', ) return parser diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 7801ca6563..017ffd769f 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -38,29 +38,29 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to add', - ) - user_or_group = parser.add_mutually_exclusive_group() - user_or_group.add_argument( - '--user', - metavar='', - help='Name or ID of user to add a role', - ) - user_or_group.add_argument( - '--group', - metavar='', - help='Name or ID of group to add a role', + help='Role to add to (name or ID)', ) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', metavar='', - help='Name or ID of domain associated with user or group', + help='Include (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Name or ID of project associated with user or group', + help='Include `` (name or ID)', + ) + user_or_group = parser.add_mutually_exclusive_group() + user_or_group.add_argument( + '--user', + metavar='', + help='Include (name or ID)', + ) + user_or_group.add_argument( + '--group', + metavar='', + help='Include (name or ID)', ) return parser @@ -177,7 +177,7 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete existing role""" + """Delete an existing role""" log = logging.getLogger(__name__ + '.DeleteRole') @@ -186,7 +186,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to delete', + help='Role to delete (name or ID)', ) return parser @@ -214,23 +214,23 @@ def get_parser(self, prog_name): domain_or_project.add_argument( '--domain', metavar='', - help='Filter role list by ', + help='Filter roles by (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Filter role list by ', + help='Filter roles by (name or ID)', ) user_or_group = parser.add_mutually_exclusive_group() user_or_group.add_argument( '--user', metavar='', - help='Name or ID of user to list roles assigned to', + help='Filter roles by (name or ID)', ) user_or_group.add_argument( '--group', metavar='', - help='Name or ID of group to list roles assigned to', + help='Filter roles by (name or ID)', ) return parser @@ -320,7 +320,7 @@ def take_action(self, parsed_args): class RemoveRole(command.Command): - """Remove role command""" + """Remove role from domain/project : user/group""" log = logging.getLogger(__name__ + '.RemoveRole') @@ -329,29 +329,29 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to remove', - ) - user_or_group = parser.add_mutually_exclusive_group() - user_or_group.add_argument( - '--user', - metavar='', - help='Name or ID of user to remove a role', - ) - user_or_group.add_argument( - '--group', - metavar='', - help='Name or ID of group to remove a role', + help='Role to remove (name or ID)', ) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', metavar='', - help='Name or ID of domain associated with user or group', + help='Include (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Name or ID of project associated with user or group', + help='Include (name or ID)', + ) + user_or_group = parser.add_mutually_exclusive_group() + user_or_group.add_argument( + '--user', + metavar='', + help='Include (name or ID)', + ) + user_or_group.add_argument( + '--group', + metavar='', + help='Include (name or ID)', ) return parser @@ -431,7 +431,7 @@ def take_action(self, parsed_args): class SetRole(command.Command): - """Set role command""" + """Set role properties""" log = logging.getLogger(__name__ + '.SetRole') @@ -440,12 +440,12 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to update', + help='Role to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', - help='New role name', + metavar='', + help='Set role name', ) return parser @@ -475,7 +475,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to display', + help='Role to show (name or ID)', ) return parser diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 63dd3b4f58..10ffce36b8 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -37,13 +37,23 @@ def get_parser(self, prog_name): parser = super(CreateUser, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', + metavar='', help='New user name', ) + parser.add_argument( + '--domain', + metavar='', + help='Default domain (name or ID)', + ) + parser.add_argument( + '--project', + metavar='', + help='Default project (name or ID)', + ) parser.add_argument( '--password', - metavar='', - help='New user password', + metavar='', + help='Set user password', ) parser.add_argument( '--password-prompt', @@ -53,23 +63,13 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help='New user email address', - ) - parser.add_argument( - '--project', - metavar='', - help='Set default project (name or ID)', - ) - parser.add_argument( - '--domain', - metavar='', - help='New default domain name or ID', + metavar='', + help='Set user email address', ) parser.add_argument( '--description', metavar='', - help='Description for new user', + help='User description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -173,12 +173,12 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter user list by (name or ID)', + help='Filter users by (name or ID)', ) parser.add_argument( '--group', metavar='', - help='List memberships of (name or ID)', + help='Filter users by membership (name or ID)', ) parser.add_argument( '--long', @@ -240,13 +240,23 @@ def get_parser(self, prog_name): ) parser.add_argument( '--name', - metavar='', - help='New user name', + metavar='', + help='Set user name', + ) + parser.add_argument( + '--domain', + metavar='', + help='Set default domain (name or ID)', + ) + parser.add_argument( + '--project', + metavar='', + help='Set default project (name or ID)', ) parser.add_argument( '--password', - metavar='', - help='New user password', + metavar='', + help='Set user password', ) parser.add_argument( '--password-prompt', @@ -256,23 +266,13 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help='New user email address', - ) - parser.add_argument( - '--domain', - metavar='', - help='New domain name or ID', - ) - parser.add_argument( - '--project', - metavar='', - help='New project name or ID', + metavar='', + help='Set user email address', ) parser.add_argument( '--description', metavar='', - help='New description', + help='Set user description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -380,7 +380,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where user resides (name or ID)', + help='Domain owning (name or ID)', ) return parser From 4b239eea4290522a24ed4242d983dc69ff7e382e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 19 Nov 2014 15:31:25 -0500 Subject: [PATCH 0078/3303] Add support for domains when deleting identity v3 resources Currently, only deleting via IDs is possible for groups, projects and users. We should have an optional --domain argument that allows for a name to be specified for the resource. (Since these are all namespaced by domains). Change-Id: I18ace3db85a3969f0b97678d432d6f8368baa9cd --- doc/source/command-objects/project.rst | 6 ++++++ doc/source/command-objects/user.rst | 8 ++++++++ openstackclient/identity/v3/group.py | 16 +++++++++++++++- openstackclient/identity/v3/project.py | 17 +++++++++++++---- openstackclient/identity/v3/user.py | 17 +++++++++++++---- 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index ba741d1dc1..0be4e80680 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -58,6 +58,12 @@ Delete an existing project os project delete +.. option:: --domain + + Domain owning :ref:`\ <_project_delete-project>` (name or ID) + + .. versionadded:: 3 + .. _project_delete-project: .. describe:: diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index 53becf2799..24a2725b7d 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -80,6 +80,14 @@ Delete user os user delete +.. option:: --domain + + Domain owning :ref:`\ <_user_delete-user>` (name or ID) + + .. versionadded:: 3 + +.. _user_delete-user: + .. describe:: User to delete (name or ID) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 14838bba57..5d3cf6422a 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -169,12 +169,26 @@ def get_parser(self, prog_name): 'group', metavar='', help='Name or ID of group to delete') + parser.add_argument( + '--domain', + metavar='', + help='Domain where group resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - group = utils.find_resource(identity_client.groups, parsed_args.group) + + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + group = utils.find_resource(identity_client.groups, + parsed_args.group, + domain_id=domain.id) + else: + group = utils.find_resource(identity_client.groups, + parsed_args.group) + identity_client.groups.delete(group.id) return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 3b0e92fd8c..1e3977ba36 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -126,16 +126,25 @@ def get_parser(self, prog_name): metavar='', help='Project to delete (name or ID)', ) + parser.add_argument( + '--domain', + metavar='', + help='Domain owning (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + project = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id) + else: + project = utils.find_resource(identity_client.projects, + parsed_args.project) identity_client.projects.delete(project.id) return diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 10ffce36b8..665dd4bb16 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -148,16 +148,25 @@ def get_parser(self, prog_name): metavar='', help='User to delete (name or ID)', ) + parser.add_argument( + '--domain', + metavar='', + help='Domain owning (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + user = utils.find_resource(identity_client.users, + parsed_args.user, + domain_id=domain.id) + else: + user = utils.find_resource(identity_client.users, + parsed_args.user) identity_client.users.delete(user.id) return From 5bc768bbc2c0ffa592a2e419cecdb898b9b65a83 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 20 Nov 2014 18:42:00 -0500 Subject: [PATCH 0079/3303] Add the ability to list projects based on a user Essentially performing GET /users/{user_id}/projects Change-Id: Iae6ddfc86a856fa24fbe293ec4af52ea671390f8 Closes-Bug: #1394793 --- doc/source/command-objects/project.rst | 7 +++++++ openstackclient/identity/v3/project.py | 24 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 0be4e80680..305e611495 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -79,6 +79,7 @@ List projects os project list [--domain ] + [--user ] [--long] .. option:: --domain @@ -87,6 +88,12 @@ List projects .. versionadded:: 3 +.. option:: --user + + Filter projects by :option:`\ <--user>` (name or ID) + + .. versionadded:: 3 + .. option:: --long List additional fields in output diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 1e3977ba36..e9adfe348a 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -162,6 +162,11 @@ def get_parser(self, prog_name): metavar='', help='Filter projects by (name or ID)', ) + parser.add_argument( + '--user', + metavar='', + help='Filter projects by (name or ID)', + ) parser.add_argument( '--long', action='store_true', @@ -178,9 +183,24 @@ def take_action(self, parsed_args): else: columns = ('ID', 'Name') kwargs = {} + + domain_id = None if parsed_args.domain: - kwargs['domain'] = common.find_domain(identity_client, - parsed_args.domain).id + domain_id = common.find_domain(identity_client, + parsed_args.domain).id + kwargs['domain'] = domain_id + + if parsed_args.user: + if parsed_args.domain: + user_id = utils.find_resource(identity_client.users, + parsed_args.user, + domain_id=domain_id).id + else: + user_id = utils.find_resource(identity_client.users, + parsed_args.user).id + + kwargs['user'] = user_id + data = identity_client.projects.list(**kwargs) return (columns, (utils.get_item_properties( From ac4950b46e65e5b3e4a98a0b0ce2a2c80747b3e8 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 21:56:18 -0600 Subject: [PATCH 0080/3303] Command object docs: server, server image server server image Some cosmetic changes in the command source, sorting classes, help strings, etc. Change-Id: I3f68dae77b9fe02bc6866684e05aeff943dd9cc3 --- doc/source/command-objects/server-image.rst | 27 + doc/source/command-objects/server.rst | 544 ++++++++++++++++++++ doc/source/commands.rst | 3 +- openstackclient/compute/v2/server.py | 186 ++++--- 4 files changed, 680 insertions(+), 80 deletions(-) create mode 100644 doc/source/command-objects/server-image.rst create mode 100644 doc/source/command-objects/server.rst diff --git a/doc/source/command-objects/server-image.rst b/doc/source/command-objects/server-image.rst new file mode 100644 index 0000000000..681f6e4f2c --- /dev/null +++ b/doc/source/command-objects/server-image.rst @@ -0,0 +1,27 @@ +============ +server image +============ + +A server image is a disk image created from a running server instance. The +image is created in the Image store. + +server image create +------------------- + +Create a new disk image from a running server + +.. code:: bash + + os server image create + [--name ] + [--wait] + + +:option:`--name` + Name of new image (default is server name) + +:option:`--wait` + Wait for image create to complete + +:option:`` + Server (name or ID) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst new file mode 100644 index 0000000000..4f07632957 --- /dev/null +++ b/doc/source/command-objects/server.rst @@ -0,0 +1,544 @@ +====== +server +====== + + +server add security group +------------------------- + +Add security group to server + +.. code:: bash + + os server add security group + + + +:option:`` + Server (name or ID) + +:option:`` + Security group to add (name or ID) + +server add volume +----------------- + +Add volume to server + +.. code:: bash + + os server add volume + [--device ] + + + +:option:`--device` + Server internal device name for volume + +:option:`` + Server (name or ID) + +:option:`` + Volume to add (name or ID) + +server create +------------- + +Create a new server + +.. code:: bash + + os server create + --image | --volume + --flavor + [--security-group [...] ] + [--key-name ] + [--property [...] ] + [--file ] [...] ] + [--user-data ] + [--availability-zone ] + [--block-device-mapping [...] ] + [--nic [...] ] + [--hint [...] ] + [--config-drive |True ] + [--min ] + [--max ] + [--wait] + + +:option:`--image` + Create server from this image + +:option:`--volume` + Create server from this volume + +:option:`--flavor` + Create server with this flavor + +:option:`--security-group` + Security group to assign to this server (repeat for multiple groups) + +:option:`--key-name` + Keypair to inject into this server (optional extension) + +:option:`--property` + Set a property on this server (repeat for multiple values) + +:option:`--file` + File to inject into image before boot (repeat for multiple files) + +:option:`--user-data` + User data file to serve from the metadata server + +:option:`--availability-zone` + Select an availability zone for the server + +:option:`--block-device-mapping` + Map block devices; map is ::: (optional extension) + +:option:`--nic` + Specify NIC configuration (optional extension) + +:option:`--hint` + Hints for the scheduler (optional extension) + +:option:`--config-drive` |True + Use specified volume as the config drive, or 'True' to use an ephemeral drive + +:option:`--min` + Minimum number of servers to launch (default=1) + +:option:`--max` + Maximum number of servers to launch (default=1) + +:option:`--wait` + Wait for build to complete + +:option:`` + New server name + +server delete +------------- + +Delete server command + +.. code:: bash + + os server delete + + +:option:`` + Server (name or ID) + +server list +----------- + +List servers + +.. code:: bash + + os server list + [--reservation-id ] + [--ip ] + [--ip6 ] + [--name ] + [--instance-name ] + [--status ] + [--flavor ] + [--image ] + [--host ] + [--all-projects] + [--long] + +:option:`--reservation-id` + Only return instances that match the reservation + +:option:`--ip` + Regular expression to match IP addresses + +:option:`--ip6` + Regular expression to match IPv6 addresses + +:option:`--name` + Regular expression to match names + +:option:`--instance-name` + Regular expression to match instance name (admin only) + +:option:`--status` + Search by server status + +:option:`--flavor` + Search by flavor ID + +:option:`--image` + Search by image ID + +:option:`--host` + Search by hostname + +:option:`--all-projects` + Include all projects (admin only) + +:option:`--long` + List additional fields in output + +server lock +----------- + +Lock server + +.. code:: bash + + os server lock + + +:option:`` + Server (name or ID) + +server migrate +-------------- + +Migrate server to different host + +.. code:: bash + + os server migrate + --live + [--shared-migration | --block-migration] + [--disk-overcommit | --no-disk-overcommit] + [--wait] + + +:option:`--wait` + Wait for resize to complete + +:option:`--live` + Target hostname + +:option:`--shared-migration` + Perform a shared live migration (default) + +:option:`--block-migration` + Perform a block live migration + +:option:`--disk-overcommit` + Allow disk over-commit on the destination host + +:option:`--no-disk-overcommit` + Do not over-commit disk on the destination host (default) + +:option:`` + Server to migrate (name or ID) + +server pause +------------ + +Pause server + +.. code:: bash + + os server pause + + +:option:`` + Server (name or ID) + +server reboot +------------- + +Perform a hard or soft server reboot + +.. code:: bash + + os server reboot + [--hard | --soft] + [--wait] + + +:option:`--hard` + Perform a hard reboot + +:option:`--soft` + Perform a soft reboot + +:option:`--wait` + Wait for reboot to complete + +:option:`` + Server (name or ID) + +server rebuild +-------------- + +Rebuild server + +.. code:: bash + + os server rebuild + --image + [--password ] + [--wait] + + +:option:`--image` + Recreate server from this image + +:option:`--password` + Set the password on the rebuilt instance + +:option:`--wait` + Wait for rebuild to complete + +:option:`` + Server (name or ID) + +server remove security group +---------------------------- + +Remove security group from server + +.. code:: bash + + os server remove security group + + + +:option:`` + Name or ID of server to use + +:option:`` + Name or ID of security group to remove from server + +server remove volume +-------------------- + +Remove volume from server + +.. code:: bash + + os server remove volume + + + +:option:`` + Server (name or ID) + +:option:`` + Volume to remove (name or ID) + +server rescue +------------- + +Put server in rescue mode + +.. code:: bash + + os server rescue + + +:option:`` + Server (name or ID) + +server resize +------------- + +Scale server to a new flavor + +.. code:: bash + + os server resize + --flavor + [--wait] + + + os server resize + --verify | --revert + + +:option:`--flavor` + Resize server to specified flavor + +:option:`--verify` + Verify server resize is complete + +:option:`--revert` + Restore server state before resize + +:option:`--wait` + Wait for resize to complete + +:option:`` + Server (name or ID) + +A resize operation is implemented by creating a new server and copying +the contents of the original disk into a new one. It is also a two-step +process for the user: the first is to perform the resize, the second is +to either confirm (verify) success and release the old server, or to declare +a revert to release the new server and restart the old one. + +server resume +------------- + +Resume server + +.. code:: bash + + os server resume + + +:option:`` + Server (name or ID) + +server set +---------- + +Set server properties + +.. code:: bash + + os server set + --name + --property + [--property ] ... + --root-password + + +:option:`--name` + New server name + +:option:`--root-password` + Set new root password (interactive only) + +:option:`--property` + Property to add/change for this server (repeat option to set + multiple properties) + +:option:`` + Server (name or ID) + +server show +----------- + +Show server details + +.. code:: bash + + os server show + [--diagnostics] + + +:option:`--diagnostics` + Display server diagnostics information + +:option:`` + Server (name or ID) + +server ssh +---------- + +Ssh to server + +.. code:: bash + + os server ssh + [--login ] + [--port ] + [--identity ] + [--option ] + [--public | --private | --address-type ] + + +:option:`--login` + Login name (ssh -l option) + +:option:`--port` + Destination port (ssh -p option) + +:option:`--identity` + Private key file (ssh -i option) + +:option:`--option` + Options in ssh_config(5) format (ssh -o option) + +:option:`--public` + Use public IP address + +:option:`--private` + Use private IP address + +:option:`--address-type` + Use other IP address (public, private, etc) + +:option:`` + Server (name or ID) + +server suspend +-------------- + +Suspend server + +.. code:: bash + + os server suspend + + +:option:`` + Server (name or ID) + +server unlock +------------- + +Unlock server + +.. code:: bash + + os server unlock + + +:option:`` + Server (name or ID) + +server unpause +-------------- + +Unpause server + +.. code:: bash + + os server unpause + + +:option:`` + Server (name or ID) + +server unrescue +--------------- + +Restore server from rescue mode + +.. code:: bash + + os server unrescue + + +:option:`` + Server (name or ID) + +server unset +------------ + +Unset server properties + +.. code:: bash + + os server unset + --property + [--property ] ... + + +:option:`--property` + Property key to remove from server (repeat to set multiple values) + +:option:`` + Server (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 250a8039d8..7b38134e42 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -100,7 +100,8 @@ referring to both Compute and Volume quotas. * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules * ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access -* ``server``: Compute - a virtual machine instance +* ``server``: (**Compute**) virtual machine instance +* ``server image``: (**Compute**) saved server disk image * ``service``: Identity - a cloud service * ``snapshot``: Volume - a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index d58df86c10..5ab1d5f3ff 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -99,27 +99,22 @@ def _show_progress(progress): sys.stdout.flush() -class AddServerVolume(command.Command): - """Add volume to server""" +class AddServerSecurityGroup(command.Command): + """Add security group to server""" - log = logging.getLogger(__name__ + '.AddServerVolume') + log = logging.getLogger(__name__ + '.AddServerSecurityGroup') def get_parser(self, prog_name): - parser = super(AddServerVolume, self).get_parser(prog_name) + parser = super(AddServerSecurityGroup, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_('Server (name or ID)'), ) parser.add_argument( - 'volume', - metavar='', - help=_('Volume to add (name or ID)'), - ) - parser.add_argument( - '--device', - metavar='', - help=_('Server internal device name for volume'), + 'group', + metavar='', + help=_('Security group to add (name or ID)'), ) return parser @@ -127,40 +122,41 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.volume server = utils.find_resource( compute_client.servers, parsed_args.server, ) - volume = utils.find_resource( - volume_client.volumes, - parsed_args.volume, + security_group = utils.find_resource( + compute_client.security_groups, + parsed_args.group, ) - compute_client.volumes.create_server_volume( - server.id, - volume.id, - parsed_args.device, - ) + server.add_security_group(security_group.name) + return -class AddServerSecurityGroup(command.Command): - """Add security group to server""" +class AddServerVolume(command.Command): + """Add volume to server""" - log = logging.getLogger(__name__ + '.AddServerSecurityGroup') + log = logging.getLogger(__name__ + '.AddServerVolume') def get_parser(self, prog_name): - parser = super(AddServerSecurityGroup, self).get_parser(prog_name) + parser = super(AddServerVolume, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', - help=_('Name or ID of server to use'), + help=_('Server (name or ID)'), ) parser.add_argument( - 'group', - metavar='', - help=_('Name or ID of security group to add to server'), + 'volume', + metavar='', + help=_('Volume to add (name or ID)'), + ) + parser.add_argument( + '--device', + metavar='', + help=_('Server internal device name for volume'), ) return parser @@ -168,18 +164,22 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute + volume_client = self.app.client_manager.volume server = utils.find_resource( compute_client.servers, parsed_args.server, ) - security_group = utils.find_resource( - compute_client.security_groups, - parsed_args.group, + volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, ) - server.add_security_group(security_group.name) - return + compute_client.volumes.create_server_volume( + server.id, + volume.id, + parsed_args.device, + ) class CreateServer(show.ShowOne): @@ -192,55 +192,65 @@ def get_parser(self, prog_name): parser.add_argument( 'server_name', metavar='', - help=_('New server name')) + help=_('New server name'), + ) disk_group = parser.add_mutually_exclusive_group( required=True, ) disk_group.add_argument( '--image', metavar='', - help=_('Create server from this image')) + help=_('Create server from this image'), + ) disk_group.add_argument( '--volume', metavar='', - help=_('Create server from this volume')) + help=_('Create server from this volume'), + ) parser.add_argument( '--flavor', metavar='', required=True, - help=_('Create server with this flavor')) + help=_('Create server with this flavor'), + ) parser.add_argument( '--security-group', metavar='', action='append', default=[], help=_('Security group to assign to this server ' - '(repeat for multiple groups)')) + '(repeat for multiple groups)'), + ) parser.add_argument( '--key-name', metavar='', - help=_('Keypair to inject into this server (optional extension)')) + help=_('Keypair to inject into this server (optional extension)'), + ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, help=_('Set a property on this server ' - '(repeat for multiple values)')) + '(repeat for multiple values)'), + ) parser.add_argument( '--file', metavar='', action='append', default=[], help=_('File to inject into image before boot ' - '(repeat for multiple files)')) + '(repeat for multiple files)'), + ) parser.add_argument( '--user-data', metavar='', - help=_('User data file to serve from the metadata server')) + help=_('User data file to serve from the metadata server'), + ) parser.add_argument( '--availability-zone', metavar='', - help=_('Select an availability zone for the server')) + help=_('Select an availability zone for the server'), + ) parser.add_argument( '--block-device-mapping', metavar='', @@ -248,37 +258,43 @@ def get_parser(self, prog_name): default=[], help=_('Map block devices; map is ' '::: ' - '(optional extension)')) + '(optional extension)'), + ) parser.add_argument( '--nic', metavar='', action='append', default=[], - help=_('Specify NIC configuration (optional extension)')) + help=_('Specify NIC configuration (optional extension)'), + ) parser.add_argument( '--hint', metavar='', action='append', default=[], - help=_('Hints for the scheduler (optional extension)')) + help=_('Hints for the scheduler (optional extension)'), + ) parser.add_argument( '--config-drive', metavar='|True', default=False, help=_('Use specified volume as the config drive, ' - 'or \'True\' to use an ephemeral drive')) + 'or \'True\' to use an ephemeral drive'), + ) parser.add_argument( '--min', metavar='', type=int, default=1, - help=_('Minimum number of servers to launch (default=1)')) + help=_('Minimum number of servers to launch (default=1)'), + ) parser.add_argument( '--max', metavar='', type=int, default=1, - help=_('Maximum number of servers to launch (default=1)')) + help=_('Maximum number of servers to launch (default=1)'), + ) parser.add_argument( '--wait', action='store_true', @@ -504,7 +520,8 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help=_('Name or ID of server to delete')) + help=_('Server (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -526,50 +543,61 @@ def get_parser(self, prog_name): parser.add_argument( '--reservation-id', metavar='', - help=_('Only return instances that match the reservation')) + help=_('Only return instances that match the reservation'), + ) parser.add_argument( '--ip', metavar='', - help=_('Regular expression to match IP addresses')) + help=_('Regular expression to match IP addresses'), + ) parser.add_argument( '--ip6', metavar='', - help=_('Regular expression to match IPv6 addresses')) + help=_('Regular expression to match IPv6 addresses'), + ) parser.add_argument( '--name', - metavar='', - help=_('Regular expression to match names')) + metavar='', + help=_('Regular expression to match names'), + ) + parser.add_argument( + '--instance-name', + metavar='', + help=_('Regular expression to match instance name (admin only)'), + ) parser.add_argument( '--status', metavar='', # FIXME(dhellmann): Add choices? - help=_('Search by server status')) + help=_('Search by server status'), + ) parser.add_argument( '--flavor', metavar='', - help=_('Search by flavor ID')) + help=_('Search by flavor'), + ) parser.add_argument( '--image', metavar='', - help=_('Search by image ID')) + help=_('Search by image'), + ) parser.add_argument( '--host', metavar='', - help=_('Search by hostname')) - parser.add_argument( - '--instance-name', - metavar='', - help=_('Regular expression to match instance name (admin only)')) + help=_('Search by hostname'), + ) parser.add_argument( '--all-projects', action='store_true', default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help=_('Include all projects (admin only)')) + help=_('Include all projects (admin only)'), + ) parser.add_argument( '--long', action='store_true', default=False, - help=_('List additional fields in output')) + help=_('List additional fields in output'), + ) return parser def take_action(self, parsed_args): @@ -672,12 +700,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help=_('Server to migrate (name or ID)'), - ) - parser.add_argument( - '--wait', - action='store_true', - help=_('Wait for resize to complete'), + help=_('Server (name or ID)'), ) parser.add_argument( '--live', @@ -699,6 +722,12 @@ def get_parser(self, prog_name): help=_('Perform a block live migration'), ) disk_group = parser.add_mutually_exclusive_group() + disk_group.add_argument( + '--disk-overcommit', + action='store_true', + default=False, + help=_('Allow disk over-commit on the destination host'), + ) disk_group.add_argument( '--no-disk-overcommit', dest='disk_overcommit', @@ -707,11 +736,10 @@ def get_parser(self, prog_name): help=_('Do not over-commit disk on the' ' destination host (default)'), ) - disk_group.add_argument( - '--disk-overcommit', + parser.add_argument( + '--wait', action='store_true', - default=False, - help=_('Allow disk over-commit on the destination host'), + help=_('Wait for resize to complete'), ) return parser @@ -1140,13 +1168,13 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help=_('Server to show (name or ID)'), + help=_('Server (name or ID)'), ) parser.add_argument( '--diagnostics', action='store_true', default=False, - help=_('Display diagnostics information for a given server'), + help=_('Display server diagnostics information'), ) return parser @@ -1439,7 +1467,7 @@ def get_parser(self, prog_name): action='append', default=[], help=_('Property key to remove from server ' - '(repeat to set multiple values)'), + '(repeat to unset multiple values)'), ) return parser From f7024281587be4178e3cc702aee0bfc6f001d144 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 17 Nov 2014 23:54:43 -0500 Subject: [PATCH 0081/3303] 1.0.0 release notes Release notes for our next cut of osc. Change-Id: Ic3b0d557f2a380c4b5a05903ff7394be7b961b55 --- doc/source/releases.rst | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 909c362eaf..ad9c59e191 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,57 @@ Release Notes ============= +1.0.0 (04 Dec 2014) +=================== + +* Bug 1337422_: document different ways to authenticate +* Bug 1383333_: Creating volume from image required image ID +* Bug 1292638_: Perhaps API Versions should Match Easier +* Bug 1390389_: create with a soft fail (create or show) for keystone operations +* Bug 1387932_: add keystone v3 region object +* Bug 1378842_: OSC fails to show server details if booted from volume +* Bug 1383338_: server create problems in boot-from-volume +* Bug 1337685_: Add the ability to list networks extensions +* Bug 1355838_: Don't make calls to Keystone for authN if insufficient args are present +* Bug 1371924_: strings are being treated as numbers +* Bug 1372070_: help text in error on openstack image save +* Bug 1372744_: v3 credential set always needs --user option +* Bug 1376833_: odd behavior when editing the domain of a user through Keystone v3 API +* Bug 1378165_: Domains should be supported for 'user show' command +* Bug 1378565_: The '--domain' arg for identity commands should not require domain lookup +* Bug 1379871_: token issue for identity v3 is broken +* Bug 1383083_: repeated to generate clientmanager in interactive mode +* Added functional tests framework and identity/object tests +* Authentication Plugin Support +* Use keystoneclient.session as the base HTTP transport +* implement swift client commands +* clean up 'links' section in keystone v3 resources +* Add cliff-tablib to requirements +* Include support for using oslo debugger in tests +* Close file handlers that were left open +* Added framework for i18n support, and marked Identity v2.0 files for translation +* Add 'command list' command +* CRUD Support for ``OS-FEDERATION`` resources (protocol, mappings, identity providers) + +.. _1337422: https://bugs.launchpad.net/bugs/1337422 +.. _1383333: https://bugs.launchpad.net/bugs/1383333 +.. _1292638: https://bugs.launchpad.net/bugs/1292638 +.. _1390389: https://bugs.launchpad.net/bugs/1390389 +.. _1387932: https://bugs.launchpad.net/bugs/1387932 +.. _1378842: https://bugs.launchpad.net/bugs/1378842 +.. _1383338: https://bugs.launchpad.net/bugs/1383338 +.. _1337685: https://bugs.launchpad.net/bugs/1337685 +.. _1355838: https://bugs.launchpad.net/bugs/1355838 +.. _1371924: https://bugs.launchpad.net/bugs/1371924 +.. _1372070: https://bugs.launchpad.net/bugs/1372070 +.. _1372744: https://bugs.launchpad.net/bugs/1372744 +.. _1376833: https://bugs.launchpad.net/bugs/1376833 +.. _1378165: https://bugs.launchpad.net/bugs/1378165 +.. _1378565: https://bugs.launchpad.net/bugs/1378565 +.. _1379871: https://bugs.launchpad.net/bugs/1379871 +.. _1383083: https://bugs.launchpad.net/bugs/1383083 + + 0.4.1 (08 Sep 2014) =================== From 625a8ae42d5a4cded26ba4d7c76b4d4d88bf9f20 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 2 Dec 2014 16:57:22 -0600 Subject: [PATCH 0082/3303] Add documentation of interactive mode This is a light description with some examples. Change-Id: Iff9ad904a150f2bb7673bd4106cf26bcefec08b9 --- doc/source/index.rst | 1 + doc/source/interactive.rst | 111 +++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 doc/source/interactive.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 8806daa069..9d4989f22e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,6 +16,7 @@ Contents: commands plugins authentication + interactive man/openstack Getting Started diff --git a/doc/source/interactive.rst b/doc/source/interactive.rst new file mode 100644 index 0000000000..4822d89f59 --- /dev/null +++ b/doc/source/interactive.rst @@ -0,0 +1,111 @@ +================ +Interactive Mode +================ + +OpenStackClient has an interactive mode, similar to the :program:`virsh(1)` or +:program:`lvm(8)` commands on Linux. This mode is useful for executing a +series of commands without having to reload the CLI, or more importantly, +without having to re-authenticate to the cloud. + +Enter interactive mode by issuing the :command:`openstack` command with no +subcommand. An :code:`(openstack)` prompt will be displayed. Interactive mode +is terminated with :command:`exit`. + +Authentication +============== + +Authentication happens exactly as before, using the same global command line +options and environment variables, except it only happens once. +The credentials are cached and re-used for subsequent commands. This means +that to work with multiple clouds interactive mode must be ended so a +authentication to the second cloud can occur. + +Scripting +========= + +Using interactive mode inside scripts sounds counter-intuitive, but the same +single-authentication benefit can be achieved by passing OSC commands to +the CLI via :code:`stdin`. + +Sample session: + +.. code-block:: bash + + # assume auth credentials are in the environment + $ openstack + (openstack) keypair list + +--------+-------------------------------------------------+ + | Name | Fingerprint | + +--------+-------------------------------------------------+ + | bunsen | a5:da:0c:52:e8:52:42:a3:4f:b8:22:62:7b:e4:e8:89 | + | beaker | 45:9c:50:56:7c:fc:3a:b6:b5:60:02:2f:41:fb:a9:4c | + +--------+-------------------------------------------------+ + (openstack) image list + +--------------------------------------+----------------+ + | ID | Name | + +--------------------------------------+----------------+ + | 78b23835-c800-4d95-9d2a-e4de59a553d8 | OpenWRT r42884 | + | 2e45d43a-7c25-45f1-b012-06ac313e2f6b | Fedora 20 | + | de3a8396-3bae-42de-84bd-f4e398b8c320 | CirrOS | + +--------------------------------------+----------------+ + (openstack) flavor list + +--------------------------------------+----------+--------+--------+-----------+------+-------+-------------+-----------+-------------+ + | ID | Name | RAM | Disk | Ephemeral | Swap | VCPUs | RXTX Factor | Is Public | Extra Specs | + +--------------------------------------+----------+--------+--------+-----------+------+-------+-------------+-----------+-------------+ + | 12594680-56f7-4da2-8322-7266681b3070 | m1.small | 2048 | 20 | 0 | | 1 | | True | | + | 9274f903-0cc7-4a95-9124-1968018e355d | m1.tiny | 512 | 5 | 0 | | 1 | | True | | + +--------------------------------------+----------+--------+--------+-----------+------+-------+-------------+-----------+-------------+ + (openstack) server create --image CirrOS --flavor m1.small --key-name beaker sample-server + +-----------------------------+-------------------------------------------------+ + | Field | Value | + +-----------------------------+-------------------------------------------------+ + | config_drive | | + | created | 2014-11-19T18:08:41Z | + | flavor | m1.small (12594680-56f7-4da2-8322-7266681b3070) | + | id | 3a9a7f82-e902-4948-9245-52b045c76a1d | + | image | CirrOS (de3a8396-3bae-42de-84bd-f4e398b8c320) | + | key_name | bunsen | + | name | sample-server | + | progress | 0 | + | properties | | + | security_groups | [{u'name': u'default'}] | + | status | BUILD | + | tenant_id | 53c93c7952594d9ba16bd7072a165ce8 | + | updated | 2014-11-19T18:08:42Z | + | user_id | 1e4eea54c7124688a8092bec6e2dbee6 | + +-----------------------------+-------------------------------------------------+ + +A similar session can be issued all at once: + +.. code-block:: bash + + $ openstack < keypair list + > flavor show m1.small + > EOF + (openstack) +--------+-------------------------------------------------+ + | Name | Fingerprint | + +--------+-------------------------------------------------+ + | bunsen | a5:da:0c:52:e8:52:42:a3:4f:b8:22:62:7b:e4:e8:89 | + | beaker | 45:9c:50:56:7c:fc:3a:b6:b5:60:02:2f:41:fb:a9:4c | + +--------+-------------------------------------------------+ + (openstack) +----------------------------+--------------------------------------+ + | Field | Value | + +----------------------------+--------------------------------------+ + | OS-FLV-DISABLED:disabled | False | + | OS-FLV-EXT-DATA:ephemeral | 0 | + | disk | 20 | + | id | 12594680-56f7-4da2-8322-7266681b3070 | + | name | m1.small | + | os-flavor-access:is_public | True | + | ram | 2048 | + | swap | | + | vcpus | 1 | + +----------------------------+--------------------------------------+ + +Limitations +=========== + +The obvious limitations to Interactive Mode is that it is not a Domain Specific +Language (DSL), just a simple command processor. That means there are no variables +or flow control. From 13672123fccdd76a62416b88443e78269a80343a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 4 Dec 2014 15:34:02 -0500 Subject: [PATCH 0083/3303] Safely pop project parent id Since we don't support multitenancy yet, we should just pop the parent id of a project. When keystoneclient supports mulittenancy we should bring everything in at once (CRUD), and these changes should be removed. Change-Id: I82c7c825502124a24ccdbadf09ecb2748887ca5d --- openstackclient/identity/v2_0/project.py | 8 +++++--- openstackclient/identity/v3/project.py | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index df759ce6a8..b2f99425c7 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -98,9 +98,9 @@ def take_action(self, parsed_args): else: raise e - info = {} - info.update(project._info) - return zip(*sorted(six.iteritems(info))) + # TODO(stevemar): Remove the line below when we support multitenancy + project._info.pop('parent_id', None) + return zip(*sorted(six.iteritems(project._info))) class DeleteProject(command.Command): @@ -279,4 +279,6 @@ def take_action(self, parsed_args): else: raise e + # TODO(stevemar): Remove the line below when we support multitenancy + info.pop('parent_id', None) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index e9adfe348a..2c2d408ec3 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -111,6 +111,8 @@ def take_action(self, parsed_args): raise e project._info.pop('links') + # TODO(stevemar): Remove the line below when we support multitenancy + project._info.pop('parent_id', None) return zip(*sorted(six.iteritems(project._info))) @@ -325,4 +327,6 @@ def take_action(self, parsed_args): parsed_args.project) project._info.pop('links') + # TODO(stevemar): Remove the line below when we support multitenancy + project._info.pop('parent_id', None) return zip(*sorted(six.iteritems(project._info))) From 6a61dbc86fd2b45ea3126ed20f9f863109b794fb Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 22:59:57 -0600 Subject: [PATCH 0084/3303] Command object docs: catalog, credentials, endpoint, region, token catalog credentials endpoint region token Change-Id: Icd7ec7fd207488b2ceb0280722aa9a684aeeac28 --- doc/source/command-objects/catalog.rst | 20 +++++ doc/source/command-objects/credentials.rst | 25 ++++++ doc/source/command-objects/endpoint.rst | 45 +++++++++++ doc/source/command-objects/region.rst | 94 ++++++++++++++++++++++ doc/source/command-objects/token.rst | 19 +++++ doc/source/commands.rst | 6 +- 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 doc/source/command-objects/catalog.rst create mode 100644 doc/source/command-objects/credentials.rst create mode 100644 doc/source/command-objects/endpoint.rst create mode 100644 doc/source/command-objects/region.rst create mode 100644 doc/source/command-objects/token.rst diff --git a/doc/source/command-objects/catalog.rst b/doc/source/command-objects/catalog.rst new file mode 100644 index 0000000000..99746dd716 --- /dev/null +++ b/doc/source/command-objects/catalog.rst @@ -0,0 +1,20 @@ +======= +catalog +======= + +Identity v2 + +catalog list +------------ + +.. code:: bash + + os catalog list + +catalog show +------------ + +.. code:: bash + + os catalog show + diff --git a/doc/source/command-objects/credentials.rst b/doc/source/command-objects/credentials.rst new file mode 100644 index 0000000000..ea8fc08fff --- /dev/null +++ b/doc/source/command-objects/credentials.rst @@ -0,0 +1,25 @@ +=========== +credentials +=========== + +credentials create +------------------ + +.. ''[consider rolling the ec2 creds into this too]'' + +.. code:: bash + + os credentials create + --x509 + [] + [] + +credentials show +---------------- + +.. code:: bash + + os credentials show + [--token] + [--user] + [--x509 [--root]] diff --git a/doc/source/command-objects/endpoint.rst b/doc/source/command-objects/endpoint.rst new file mode 100644 index 0000000000..128ddfa021 --- /dev/null +++ b/doc/source/command-objects/endpoint.rst @@ -0,0 +1,45 @@ +======== +endpoint +======== + +Identity v2, v3 + +endpoint create +--------------- + +.. program:: endpoint create +.. code:: bash + + os endpoint create + --publicurl + [--adminurl ] + [--internalurl ] + [--region ] + + +endpoint delete +--------------- + +.. program:: endpoint delete +.. code:: bash + + os endpoint delete + + +endpoint list +------------- + +.. program:: endpoint list +.. code:: bash + + os endpoint list + [--long] + +endpoint show +------------- + +.. program:: endpoint show +.. code:: bash + + os endpoint show + diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst new file mode 100644 index 0000000000..788ed6facb --- /dev/null +++ b/doc/source/command-objects/region.rst @@ -0,0 +1,94 @@ +====== +region +====== + +Identity v3 + +region create +------------- + +Create new region + +.. code:: bash + + os region create + [--parent-region ] + [--description ] + [--url ] + + +:option:`--parent-region` + Parent region + +:option:`--description` + New region description + +:option:`--url` + New region URL + +:option:`` + New region ID + +region delete +------------- + +Delete region + +.. code:: bash + + os region delete + + +:option:`` + Region to delete + +region list +----------- + +List regions + +.. code:: bash + + os region list + [--parent-region ] + +:option:`--parent-region` + Filter by a specific parent region + +region set +---------- + +Set region properties + +.. code:: bash + + os region set + [--parent-region ] + [--description ] + [--url ] + + +:option:`--parent-region` + New parent region + +:option:`--description` + New region description + +:option:`--url` + New region URL + +:option:`` + Region ID to modify + +region show +----------- + +Show region + +.. code:: bash + + os region show + + +:option:`` + Region ID to modify diff --git a/doc/source/command-objects/token.rst b/doc/source/command-objects/token.rst new file mode 100644 index 0000000000..aec87d2850 --- /dev/null +++ b/doc/source/command-objects/token.rst @@ -0,0 +1,19 @@ +===== +token +===== + +Identity v2, v3 + +token issue +----------- + +.. code:: bash + + os token issue + +token revoke +------------ + +.. code:: bash + + os token revoke diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 250a8039d8..e15e000925 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -72,13 +72,14 @@ referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token * ``aggregate``: (**Compute**) a grouping of servers * ``backup``: Volume - a volume copy +* ``catalog``: (**Identity**) service catalog * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL * ``consumer``: Identity - OAuth-based delegatee * ``container``: Object Store - a grouping of objects -* ``credential``: Identity - specific to identity providers +* ``credentials``: (**Identity**) specific to identity providers * ``domain``: Identity - a grouping of projects -* ``endpoint``: Identity - the base URL used to contact a specific service +* ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc * ``group``: Identity - a grouping of users @@ -96,6 +97,7 @@ referring to both Compute and Volume quotas. * ``policy``: Identity - determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions +* ``region``: (**Identity**) * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules From 1dc0e2b5529dd144443e948ad2b8e4a128c2ab9c Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Fri, 5 Dec 2014 03:30:40 +0000 Subject: [PATCH 0085/3303] Workflow documentation is now in infra-manual Replace URLs for workflow documentation to appropriate parts of the OpenStack Project Infrastructure Manual. Change-Id: Id09c9bdf8804c1ed90e49606e76ffbff1d96a7c2 --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 8806daa069..2c85a65eb9 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -37,7 +37,7 @@ the openstack/python-openstackclient project using `Gerrit`_. .. _on OpenStack's Git server: https://git.openstack.org/cgit/openstack/python-openstackclient/tree .. _Launchpad: https://launchpad.net/python-openstackclient -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow +.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow Indices and Tables ================== From 62a2083a785bcfac25b2b7e409e1c7e066f9edff Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 5 Dec 2014 12:54:19 -0600 Subject: [PATCH 0086/3303] Fix ec2 credentials commands for new auth These commands were not updated for the new authentication model. Closes-Bug: 1399757 Change-Id: I5d4beb9d1fa6914fef5e4c7b459cdd967e614b24 --- openstackclient/identity/v2_0/ec2creds.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index fd8eef8463..7a4dea6610 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -57,7 +57,7 @@ def take_action(self, parsed_args): ).id else: # Get the project from the current auth - project = identity_client.auth_tenant_id + project = self.app.client_manager.auth_ref.project_id if parsed_args.user: user = utils.find_resource( identity_client.users, @@ -65,7 +65,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id creds = identity_client.ec2.create(user, project) @@ -104,7 +104,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id identity_client.ec2.delete(user, parsed_args.access_key) @@ -134,7 +134,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id columns = ('access', 'secret', 'tenant_id', 'user_id') column_headers = ('Access', 'Secret', 'Project ID', 'User ID') @@ -177,7 +177,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id creds = identity_client.ec2.get(user, parsed_args.access_key) From 1a25cbaf8f2c1643181ef6233f72a57aaac5404d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 5 Dec 2014 12:54:19 -0600 Subject: [PATCH 0087/3303] Followup for ec2 credentials command fix Add functional tests for 'ec2 credentials' commands. Also fix tenant_id in output for create and show. Change-Id: I6ba3249b67408571624709e17f8aa2ac6d80237d --- functional/tests/test_identity.py | 46 +++++++++++++++++++++++ openstackclient/identity/v2_0/ec2creds.py | 12 ++++++ 2 files changed, 58 insertions(+) diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py index c5779a206d..b328115446 100644 --- a/functional/tests/test_identity.py +++ b/functional/tests/test_identity.py @@ -24,6 +24,19 @@ class IdentityV2Tests(test.TestCase): USER_FIELDS = ['email', 'enabled', 'id', 'name', 'project_id', 'username'] PROJECT_FIELDS = ['enabled', 'id', 'name', 'description'] + EC2_CREDENTIALS_FIELDS = [ + 'access', + 'project_id', + 'secret', + 'trust_id', + 'user_id', + ] + EC2_CREDENTIALS_LIST_HEADERS = [ + 'Access', + 'Secret', + 'Project ID', + 'User ID', + ] def test_user_list(self): raw_output = self.openstack('user list') @@ -70,6 +83,39 @@ def test_project_delete(self): raw_output = self.openstack('project delete dummy-project') self.assertEqual(0, len(raw_output)) + def test_ec2_credentials_create(self): + create_output = self.openstack('ec2 credentials create') + create_items = self.parse_show(create_output) + self.openstack( + 'ec2 credentials delete %s' % create_items[0]['access'], + ) + self.assert_show_fields(create_items, self.EC2_CREDENTIALS_FIELDS) + + def test_ec2_credentials_delete(self): + create_output = self.openstack('ec2 credentials create') + create_items = self.parse_show(create_output) + raw_output = self.openstack( + 'ec2 credentials delete %s' % create_items[0]['access'], + ) + self.assertEqual(0, len(raw_output)) + + def test_ec2_credentials_list(self): + raw_output = self.openstack('ec2 credentials list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.EC2_CREDENTIALS_LIST_HEADERS) + + def test_ec2_credentials_show(self): + create_output = self.openstack('ec2 credentials create') + create_items = self.parse_show(create_output) + show_output = self.openstack( + 'ec2 credentials show %s' % create_items[0]['access'], + ) + items = self.parse_show(show_output) + self.openstack( + 'ec2 credentials delete %s' % create_items[0]['access'], + ) + self.assert_show_fields(items, self.EC2_CREDENTIALS_FIELDS) + class IdentityV3Tests(test.TestCase): """Functional tests for Identity V3 commands. """ diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 7a4dea6610..a20ffd4b57 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -71,6 +71,12 @@ def take_action(self, parsed_args): info = {} info.update(creds._info) + + if 'tenant_id' in info: + info.update( + {'project_id': info.pop('tenant_id')} + ) + return zip(*sorted(six.iteritems(info))) @@ -183,4 +189,10 @@ def take_action(self, parsed_args): info = {} info.update(creds._info) + + if 'tenant_id' in info: + info.update( + {'project_id': info.pop('tenant_id')} + ) + return zip(*sorted(six.iteritems(info))) From c7a5ead8c738c85016e49026b9e38c65018c3ba6 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 8 Dec 2014 12:40:22 -0600 Subject: [PATCH 0088/3303] Release 1.0.1 Fix 'ec2 credentials' regression Change-Id: Ieb22f6c535ff42a14162cafc88df6099486f9afe --- doc/source/releases.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index ad9c59e191..0f0f33f6e6 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,14 @@ Release Notes ============= +1.0.1 (08 Dec 2014) +=================== + +* Bug 1399757_: EC2 credentials create fails + +.. _1399757: https://bugs.launchpad.net/bugs/1399757 + + 1.0.0 (04 Dec 2014) =================== From 52d22359f100c287c375edc73b6c4bd61be4c8da Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 4 Dec 2014 15:09:03 -0600 Subject: [PATCH 0089/3303] Tweaks after the fact Change-Id: Id96203de023b3b8bde1984a61c41dd9bc1711de4 --- doc/source/command-objects/server.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 4f07632957..482602eefc 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -139,8 +139,8 @@ List servers os server list [--reservation-id ] - [--ip ] - [--ip6 ] + [--ip ] + [--ip6 ] [--name ] [--instance-name ] [--status ] @@ -210,9 +210,6 @@ Migrate server to different host [--wait] -:option:`--wait` - Wait for resize to complete - :option:`--live` Target hostname @@ -228,6 +225,9 @@ Migrate server to different host :option:`--no-disk-overcommit` Do not over-commit disk on the destination host (default) +:option:`--wait` + Wait for resize to complete + :option:`` Server to migrate (name or ID) From a4208a7201bac0d6f361864e6f37bfd6b3626978 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 12 Dec 2014 22:21:32 +0000 Subject: [PATCH 0090/3303] Updated from global requirements Change-Id: I3b1cd7aac5c9603dfaccbd4ae30d07cbf7c96da2 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 82be8e39ab..35d2396d31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ Babel>=1.3 cliff>=1.7.0 # Apache-2.0 cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 -oslo.utils>=1.0.0 # Apache-2.0 +oslo.utils>=1.1.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 -python-glanceclient>=0.14.0 +python-glanceclient>=0.15.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 python-cinderclient>=1.1.0 From 381b47ff05559113ae99f61f3c230b806dcaa9ca Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 9 Dec 2014 14:47:51 -0500 Subject: [PATCH 0091/3303] list availability zones for compute Adds the command `os availability zone list` Change-Id: I77bf52a9b84a62c3771a4838c9ea0c3af03eedb2 Closes-Bug: #1400795 --- .../compute/v2/availability_zone.py | 102 ++++++++++++++++++ setup.cfg | 2 + 2 files changed, 104 insertions(+) create mode 100644 openstackclient/compute/v2/availability_zone.py diff --git a/openstackclient/compute/v2/availability_zone.py b/openstackclient/compute/v2/availability_zone.py new file mode 100644 index 0000000000..c7c9241491 --- /dev/null +++ b/openstackclient/compute/v2/availability_zone.py @@ -0,0 +1,102 @@ +# 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. +# + +"""Compute v2 Availability Zone action implementations""" + +import copy +import logging + +from cliff import lister +from novaclient import exceptions as nova_exceptions +import six + +from openstackclient.common import utils +from openstackclient.i18n import _ # noqa + + +def _xform_availability_zone(az, include_extra): + result = [] + zone_info = {} + if hasattr(az, 'zoneState'): + zone_info['zone_status'] = ('available' if az.zoneState['available'] + else 'not available') + if hasattr(az, 'zoneName'): + zone_info['zone_name'] = az.zoneName + + if not include_extra: + result.append(zone_info) + return result + + if hasattr(az, 'hosts') and az.hosts: + for host, services in six.iteritems(az.hosts): + host_info = copy.deepcopy(zone_info) + host_info['host_name'] = host + + for svc, state in six.iteritems(services): + info = copy.deepcopy(host_info) + info['service_name'] = svc + info['service_status'] = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + result.append(info) + else: + zone_info['host_name'] = '' + zone_info['service_name'] = '' + zone_info['service_status'] = '' + result.append(zone_info) + return result + + +class ListAvailabilityZone(lister.Lister): + """List Availability Zones and their status""" + + log = logging.getLogger(__name__ + '.ListAvailabilityZone') + + def get_parser(self, prog_name): + parser = super(ListAvailabilityZone, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_('List additional fields in output'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + if parsed_args.long: + columns = ('Zone Name', 'Zone Status', + 'Host Name', 'Service Name', 'Service Status') + else: + columns = ('Zone Name', 'Zone Status') + + compute_client = self.app.client_manager.compute + try: + data = compute_client.availability_zones.list() + except nova_exceptions.Forbidden as e: # policy doesn't allow + try: + data = compute_client.availability_zones.list(detailed=False) + except Exception: + raise e + + # Argh, the availability zones are not iterable... + result = [] + for zone in data: + result += _xform_availability_zone(zone, parsed_args.long) + + return (columns, + (utils.get_dict_properties( + s, columns + ) for s in result)) diff --git a/setup.cfg b/setup.cfg index 02b7751d43..e1e5a1ec4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,8 @@ openstack.common = quota_show = openstackclient.common.quota:ShowQuota openstack.compute.v2 = + availability_zone_list = openstackclient.compute.v2.availability_zone:ListAvailabilityZone + compute_agent_create = openstackclient.compute.v2.agent:CreateAgent compute_agent_delete = openstackclient.compute.v2.agent:DeleteAgent compute_agent_list = openstackclient.compute.v2.agent:ListAgent From 25a7c1f27f1ec83edfe1e33e83116ca3eda3ba94 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 17 Dec 2014 18:17:56 +1000 Subject: [PATCH 0092/3303] Don't import form keystoneclient.openstack.common The keystoneclient.openstack.common directory is where we sync files from oslo incubator. It is not a public directory and should not be being consumed by openstackclient. Change-Id: I011bb95c2c824e2dbc4b822ca922ae77b8d9b955 --- openstackclient/api/api.py | 3 +-- openstackclient/compute/v2/security_group.py | 2 +- openstackclient/identity/v2_0/project.py | 2 +- openstackclient/identity/v2_0/role.py | 2 +- openstackclient/identity/v2_0/user.py | 2 +- openstackclient/identity/v3/domain.py | 2 +- openstackclient/identity/v3/group.py | 2 +- openstackclient/identity/v3/project.py | 2 +- openstackclient/identity/v3/role.py | 2 +- openstackclient/identity/v3/user.py | 2 +- openstackclient/tests/identity/v2_0/test_project.py | 2 +- openstackclient/tests/identity/v2_0/test_role.py | 2 +- openstackclient/tests/identity/v2_0/test_user.py | 2 +- 13 files changed, 13 insertions(+), 14 deletions(-) diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 67386aaf0b..90b4e9c38f 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -15,8 +15,7 @@ import simplejson as json -from keystoneclient.openstack.common.apiclient \ - import exceptions as ksc_exceptions +from keystoneclient import exceptions as ksc_exceptions from keystoneclient import session as ksc_session from openstackclient.common import exceptions diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index cd33085707..f7ffb1d115 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -23,7 +23,7 @@ from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from novaclient.v1_1 import security_group_rules from openstackclient.common import parseractions from openstackclient.common import utils diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index b2f99425c7..6450d814a7 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index cec95095cc..475baa2c5d 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import exceptions from openstackclient.common import utils diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 955e19e770..7f1ea6da72 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 1233fea5e9..9e50fe54c6 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -22,7 +22,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 5d3cf6422a..b617cd7693 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -22,7 +22,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 2c2d408ec3..42412e410b 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 017ffd769f..f86854bc4c 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -22,7 +22,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 665dd4bb16..037af70e25 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index 833b5d84d8..bb69b99d24 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -15,7 +15,7 @@ import copy -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 67467747f7..ee1f19cd8e 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -16,7 +16,7 @@ import copy import mock -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import exceptions from openstackclient.identity.v2_0 import role diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index a025dd7d1a..598e1d2b0f 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -16,7 +16,7 @@ import copy import mock -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes From 71d9c8b5b343011bb0c9d90dab9cb75510d4c618 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 22 Dec 2014 13:20:41 -0500 Subject: [PATCH 0093/3303] Properly format 'attached to' column list when listing volumes Previously, no data was being returned for the 'attached to' field when listing volumes. Dig into the the returned array to format the column. Change-Id: Iebd79e5ddcb4a335703d9b2675aa7128995de918 Closes-Bug: #1404931 --- openstackclient/volume/v1/volume.py | 41 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 84c216d320..b78779bd71 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -221,6 +221,25 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.compute + + def _format_attach(attachments): + """Return a formatted string of a volume's attached instances + + :param volume: a volume.attachments field + :rtype: a string of formatted instances + """ + + msg = '' + for attachment in attachments: + server = attachment['server_id'] + if server in server_cache.keys(): + server = server_cache[server].name + device = attachment['device'] + msg += 'Attached to %s on %s ' % (server, device) + return msg + if parsed_args.long: columns = ( 'ID', @@ -229,7 +248,7 @@ def take_action(self, parsed_args): 'Size', 'Volume Type', 'Bootable', - 'Attached to', + 'Attachments', 'Metadata', ) column_headers = ( @@ -239,7 +258,7 @@ def take_action(self, parsed_args): 'Size', 'Type', 'Bootable', - 'Attached', + 'Attached to', 'Properties', ) else: @@ -248,28 +267,38 @@ def take_action(self, parsed_args): 'Display Name', 'Status', 'Size', - 'Attached to', + 'Attachments', ) column_headers = ( 'ID', 'Display Name', 'Status', 'Size', - 'Attached', + 'Attached to', ) + + # Cache the server list + server_cache = {} + try: + for s in compute_client.servers.list(): + server_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + search_opts = { 'all_tenants': parsed_args.all_projects, 'display_name': parsed_args.name, 'status': parsed_args.status, } - volume_client = self.app.client_manager.volume data = volume_client.volumes.list(search_opts=search_opts) return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Metadata': utils.format_dict}, + formatters={'Metadata': utils.format_dict, + 'Attachments': _format_attach}, ) for s in data)) From 470b7e53a8d7e7ba088b934c49163412c8ee5ed9 Mon Sep 17 00:00:00 2001 From: wanghong Date: Wed, 10 Dec 2014 11:47:54 +0800 Subject: [PATCH 0094/3303] add multi-delete support for compute/image/net/volume This is part1, add support for these objects: compute.server imagev1.image imagev2.image network.network volume.volume volume.backup volume.snapshot Closes-Bug: #1400597 Change-Id: Ice21fee85203a8a55417e0ead8b509b8fd6705c1 --- doc/source/command-objects/server.rst | 8 +++---- openstackclient/compute/v2/server.py | 14 +++++++----- openstackclient/image/v1/image.py | 18 ++++++++------- openstackclient/image/v2/image.py | 18 ++++++++------- openstackclient/network/v2/network.py | 13 ++++++----- .../tests/compute/v2/test_server.py | 2 +- openstackclient/tests/image/v1/test_image.py | 2 +- openstackclient/tests/image/v2/test_image.py | 2 +- .../tests/network/v2/test_network.py | 2 +- openstackclient/volume/v1/backup.py | 14 +++++++----- openstackclient/volume/v1/snapshot.py | 14 +++++++----- openstackclient/volume/v1/volume.py | 22 ++++++++++--------- 12 files changed, 71 insertions(+), 58 deletions(-) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 482602eefc..7d5cdce56c 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -117,15 +117,15 @@ Create a new server :option:`` New server name -server delete -------------- +server(s) delete +---------------- -Delete server command +Delete server(s) command .. code:: bash os server delete - + [ ...] :option:`` Server (name or ID) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 5ab1d5f3ff..a5d8b0c30a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -511,25 +511,27 @@ def take_action(self, parsed_args): class DeleteServer(command.Command): - """Delete server command""" + """Delete server(s)""" log = logging.getLogger(__name__ + '.DeleteServer') def get_parser(self, prog_name): parser = super(DeleteServer, self).get_parser(prog_name) parser.add_argument( - 'server', + 'servers', metavar='', - help=_('Server (name or ID)'), + nargs="+", + help=_('Server(s) to delete (name or ID)'), ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute - server = utils.find_resource( - compute_client.servers, parsed_args.server) - compute_client.servers.delete(server.id) + for server in parsed_args.servers: + server_obj = utils.find_resource( + compute_client.servers, server) + compute_client.servers.delete(server_obj.id) return diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 32dd388c0f..ca1eead4d8 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -262,16 +262,17 @@ def take_action(self, parsed_args): class DeleteImage(command.Command): - """Delete an image""" + """Delete image(s)""" log = logging.getLogger(__name__ + ".DeleteImage") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) parser.add_argument( - "image", + "images", metavar="", - help="Name or ID of image to delete", + nargs="+", + help="Image(s) to delete (name or ID)", ) return parser @@ -279,11 +280,12 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - image_client.images.delete(image.id) + for image in parsed_args.images: + image_obj = utils.find_resource( + image_client.images, + image, + ) + image_client.images.delete(image_obj.id) class ListImage(lister.Lister): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index c12ff11a09..63351c6d5e 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -27,16 +27,17 @@ class DeleteImage(command.Command): - """Delete an image""" + """Delete image(s)""" log = logging.getLogger(__name__ + ".DeleteImage") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) parser.add_argument( - "image", + "images", metavar="", - help="Name or ID of image to delete", + nargs="+", + help="Image(s) to delete (name or ID)", ) return parser @@ -44,11 +45,12 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - image_client.images.delete(image.id) + for image in parsed_args.images: + image_obj = utils.find_resource( + image_client.images, + image, + ) + image_client.images.delete(image_obj.id) class ListImage(lister.Lister): diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index f34666ba88..0d68f82dc1 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -86,26 +86,27 @@ def get_body(self, parsed_args): class DeleteNetwork(command.Command): - """Delete a network""" + """Delete network(s)""" log = logging.getLogger(__name__ + '.DeleteNetwork') def get_parser(self, prog_name): parser = super(DeleteNetwork, self).get_parser(prog_name) parser.add_argument( - 'identifier', + 'networks', metavar="", - help=("Name or identifier of network to delete") + nargs="+", + help=("Network(s) to delete (name or ID)") ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) client = self.app.client_manager.network - _id = common.find(client, 'network', 'networks', - parsed_args.identifier) delete_method = getattr(client, "delete_network") - delete_method(_id) + for network in parsed_args.networks: + _id = common.find(client, 'network', 'networks', network) + delete_method(_id) return diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 19c13440db..d51beef369 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -232,7 +232,7 @@ def test_server_delete_no_options(self): compute_fakes.server_id, ] verifylist = [ - ('server', compute_fakes.server_id), + ('servers', [compute_fakes.server_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index a05669300e..4d21ac4858 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -288,7 +288,7 @@ def test_image_delete_no_options(self): image_fakes.image_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('images', [image_fakes.image_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index 3e9eeebb1d..bc61a89fb6 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -51,7 +51,7 @@ def test_image_delete_no_options(self): image_fakes.image_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('images', [image_fakes.image_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index 468db5e039..b893229f65 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -120,7 +120,7 @@ def test_delete(self): FAKE_NAME, ] verifylist = [ - ('identifier', FAKE_NAME), + ('networks', [FAKE_NAME]), ] lister = mock.Mock(return_value={RESOURCES: [RECORD]}) self.app.client_manager.network.list_networks = lister diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 992fa7be71..8c16ba256f 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -73,25 +73,27 @@ def take_action(self, parsed_args): class DeleteBackup(command.Command): - """Delete backup command""" + """Delete backup(s)""" log = logging.getLogger(__name__ + '.DeleteBackup') def get_parser(self, prog_name): parser = super(DeleteBackup, self).get_parser(prog_name) parser.add_argument( - 'backup', + 'backups', metavar='', - help='Name or ID of backup to delete', + nargs="+", + help='Backup(s) to delete (name or ID)', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume - backup_id = utils.find_resource(volume_client.backups, - parsed_args.backup).id - volume_client.backups.delete(backup_id) + for backup in parsed_args.backups: + backup_id = utils.find_resource(volume_client.backups, + backup).id + volume_client.backups.delete(backup_id) return diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index 9cc3c4c1d5..c9e1baca92 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -74,25 +74,27 @@ def take_action(self, parsed_args): class DeleteSnapshot(command.Command): - """Delete snapshot command""" + """Delete snapshot(s)""" log = logging.getLogger(__name__ + '.DeleteSnapshot') def get_parser(self, prog_name): parser = super(DeleteSnapshot, self).get_parser(prog_name) parser.add_argument( - 'snapshot', + 'snapshots', metavar='', - help='Name or ID of snapshot to delete', + nargs="+", + help='Snapshot(s) to delete (name or ID)', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume - snapshot_id = utils.find_resource(volume_client.volume_snapshots, - parsed_args.snapshot).id - volume_client.volume_snapshots.delete(snapshot_id) + for snapshot in parsed_args.snapshots: + snapshot_id = utils.find_resource(volume_client.volume_snapshots, + snapshot).id + volume_client.volume_snapshots.delete(snapshot_id) return diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 84c216d320..73ae3a7f9c 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -155,35 +155,37 @@ def take_action(self, parsed_args): class DeleteVolume(command.Command): - """Delete a volume""" + """Delete volume(s)""" log = logging.getLogger(__name__ + '.DeleteVolume') def get_parser(self, prog_name): parser = super(DeleteVolume, self).get_parser(prog_name) parser.add_argument( - 'volume', + 'volumes', metavar='', - help='Volume to delete (name or ID)', + nargs="+", + help='Volume(s) to delete (name or ID)', ) parser.add_argument( '--force', dest='force', action='store_true', default=False, - help='Attempt forced removal of a volume, regardless of state', + help='Attempt forced removal of volume(s), regardless of state', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume - volume = utils.find_resource( - volume_client.volumes, parsed_args.volume) - if parsed_args.force: - volume_client.volumes.force_delete(volume.id) - else: - volume_client.volumes.delete(volume.id) + for volume in parsed_args.volumes: + volume_obj = utils.find_resource( + volume_client.volumes, volume) + if parsed_args.force: + volume_client.volumes.force_delete(volume_obj.id) + else: + volume_client.volumes.delete(volume_obj.id) return From d8f1cbd98461d4c2989384d29c7e2a99223468a9 Mon Sep 17 00:00:00 2001 From: wanghong Date: Wed, 10 Dec 2014 14:09:01 +0800 Subject: [PATCH 0095/3303] add multi-delete support for identity This is part2. Add support for these objects: identity.project(v2.0) identity.role(v2.0) identity.user(v2.0) identity.project(v3) identity.role(v3) identity.user(v3) identity.group(v3) Closes-Bug: #1400597 Change-Id: I270434d657cf4ddc23c3aba2c704d6ef184b0dbc --- doc/source/command-objects/project.rst | 10 ++++---- doc/source/command-objects/role.rst | 10 ++++---- doc/source/command-objects/user.rst | 10 ++++---- openstackclient/identity/v2_0/project.py | 19 +++++++------- openstackclient/identity/v2_0/role.py | 19 +++++++------- openstackclient/identity/v2_0/user.py | 19 +++++++------- openstackclient/identity/v3/group.py | 25 +++++++++++-------- openstackclient/identity/v3/project.py | 25 +++++++++++-------- openstackclient/identity/v3/role.py | 19 +++++++------- openstackclient/identity/v3/user.py | 25 +++++++++++-------- .../tests/identity/v2_0/test_project.py | 2 +- .../tests/identity/v2_0/test_role.py | 2 +- .../tests/identity/v2_0/test_user.py | 2 +- .../tests/identity/v3/test_project.py | 2 +- .../tests/identity/v3/test_role.py | 2 +- .../tests/identity/v3/test_user.py | 2 +- 16 files changed, 103 insertions(+), 90 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 305e611495..56f2196f53 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -47,16 +47,16 @@ Create new project New project name -project delete --------------- +project(s) delete +----------------- -Delete an existing project +Delete project(s) -.. program:: project delete +.. program:: project(s) delete .. code:: bash os project delete - + [ ...] .. option:: --domain diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 1cc80d7d98..4c37fc38f8 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -56,16 +56,16 @@ Create new role New role name -role delete ------------ +role(s) delete +-------------- -Delete an existing role +Delete role(s) -.. program:: role delete +.. program:: role(s) delete .. code:: bash os role delete - + [ ...] .. option:: diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index 24a2725b7d..281e8e0d86 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -69,16 +69,16 @@ Create new user New user name -user delete ------------ +user(s) delete +-------------- -Delete user +Delete user(s) -.. program:: user delete +.. program:: user(s) delete .. code:: bash os user delete - + [ ...] .. option:: --domain diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 6450d814a7..9b195600fc 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -104,16 +104,17 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete an existing project""" + """Delete project(s)""" log = logging.getLogger(__name__ + '.DeleteProject') def get_parser(self, prog_name): parser = super(DeleteProject, self).get_parser(prog_name) parser.add_argument( - 'project', + 'projects', metavar='', - help=_('Project to delete (name or ID)'), + nargs="+", + help=_('Project(s) to delete (name or ID)'), ) return parser @@ -121,12 +122,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.tenants, - parsed_args.project, - ) - - identity_client.tenants.delete(project.id) + for project in parsed_args.projects: + project_obj = utils.find_resource( + identity_client.tenants, + project, + ) + identity_client.tenants.delete(project_obj.id) return diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 475baa2c5d..d03664e07b 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -114,16 +114,17 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete an existing role""" + """Delete role(s)""" log = logging.getLogger(__name__ + '.DeleteRole') def get_parser(self, prog_name): parser = super(DeleteRole, self).get_parser(prog_name) parser.add_argument( - 'role', + 'roles', metavar='', - help=_('Role to delete (name or ID)'), + nargs="+", + help=_('Role(s) to delete (name or ID)'), ) return parser @@ -131,12 +132,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = utils.find_resource( - identity_client.roles, - parsed_args.role, - ) - - identity_client.roles.delete(role.id) + for role in parsed_args.roles: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) return diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 7f1ea6da72..b5bbce3bd3 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -128,16 +128,17 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): - """Delete user""" + """Delete user(s)""" log = logging.getLogger(__name__ + '.DeleteUser') def get_parser(self, prog_name): parser = super(DeleteUser, self).get_parser(prog_name) parser.add_argument( - 'user', + 'users', metavar='', - help=_('User to delete (name or ID)'), + nargs="+", + help=_('User(s) to delete (name or ID)'), ) return parser @@ -145,12 +146,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) - - identity_client.users.delete(user.id) + for user in parsed_args.users: + user_obj = utils.find_resource( + identity_client.users, + user, + ) + identity_client.users.delete(user_obj.id) return diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index b617cd7693..327a64d5c0 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -159,16 +159,17 @@ def take_action(self, parsed_args): class DeleteGroup(command.Command): - """Delete group command""" + """Delete group(s)""" log = logging.getLogger(__name__ + '.DeleteGroup') def get_parser(self, prog_name): parser = super(DeleteGroup, self).get_parser(prog_name) parser.add_argument( - 'group', + 'groups', metavar='', - help='Name or ID of group to delete') + nargs="+", + help='Group(s) to delete (name or ID)') parser.add_argument( '--domain', metavar='', @@ -180,16 +181,18 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) - group = utils.find_resource(identity_client.groups, - parsed_args.group, - domain_id=domain.id) - else: - group = utils.find_resource(identity_client.groups, - parsed_args.group) - - identity_client.groups.delete(group.id) + for group in parsed_args.groups: + if domain is not None: + group_obj = utils.find_resource(identity_client.groups, + group, + domain_id=domain.id) + else: + group_obj = utils.find_resource(identity_client.groups, + group) + identity_client.groups.delete(group_obj.id) return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 42412e410b..28eb427716 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -117,16 +117,17 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete an existing project""" + """Delete project(s)""" log = logging.getLogger(__name__ + '.DeleteProject') def get_parser(self, prog_name): parser = super(DeleteProject, self).get_parser(prog_name) parser.add_argument( - 'project', + 'projects', metavar='', - help='Project to delete (name or ID)', + nargs="+", + help='Project(s) to delete (name or ID)', ) parser.add_argument( '--domain', @@ -139,16 +140,18 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) - project = utils.find_resource(identity_client.projects, - parsed_args.project, - domain_id=domain.id) - else: - project = utils.find_resource(identity_client.projects, - parsed_args.project) - - identity_client.projects.delete(project.id) + for project in parsed_args.projects: + if domain is not None: + project_obj = utils.find_resource(identity_client.projects, + project, + domain_id=domain.id) + else: + project_obj = utils.find_resource(identity_client.projects, + project) + identity_client.projects.delete(project_obj.id) return diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index f86854bc4c..d680278eea 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -177,16 +177,17 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete an existing role""" + """Delete role(s)""" log = logging.getLogger(__name__ + '.DeleteRole') def get_parser(self, prog_name): parser = super(DeleteRole, self).get_parser(prog_name) parser.add_argument( - 'role', + 'roles', metavar='', - help='Role to delete (name or ID)', + nargs="+", + help='Role(s) to delete (name or ID)', ) return parser @@ -194,12 +195,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = utils.find_resource( - identity_client.roles, - parsed_args.role, - ) - - identity_client.roles.delete(role.id) + for role in parsed_args.roles: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) return diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 037af70e25..dc5468ff3a 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -137,16 +137,17 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): - """Delete user""" + """Delete user(s)""" log = logging.getLogger(__name__ + '.DeleteUser') def get_parser(self, prog_name): parser = super(DeleteUser, self).get_parser(prog_name) parser.add_argument( - 'user', + 'users', metavar='', - help='User to delete (name or ID)', + nargs="+", + help='User(s) to delete (name or ID)', ) parser.add_argument( '--domain', @@ -159,16 +160,18 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) - user = utils.find_resource(identity_client.users, - parsed_args.user, - domain_id=domain.id) - else: - user = utils.find_resource(identity_client.users, - parsed_args.user) - - identity_client.users.delete(user.id) + for user in parsed_args.users: + if domain is not None: + user_obj = utils.find_resource(identity_client.users, + user, + domain_id=domain.id) + else: + user_obj = utils.find_resource(identity_client.users, + user) + identity_client.users.delete(user_obj.id) return diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index bb69b99d24..0c5ef77f62 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -326,7 +326,7 @@ def test_project_delete_no_options(self): identity_fakes.project_id, ] verifylist = [ - ('project', identity_fakes.project_id), + ('projects', [identity_fakes.project_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index ee1f19cd8e..2e3a286375 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -234,7 +234,7 @@ def test_role_delete_no_options(self): identity_fakes.role_name, ] verifylist = [ - ('role', identity_fakes.role_name), + ('roles', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index 598e1d2b0f..ccdf240ec1 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -443,7 +443,7 @@ def test_user_delete_no_options(self): identity_fakes.user_id, ] verifylist = [ - ('user', identity_fakes.user_id), + ('users', [identity_fakes.user_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 1060a27795..d16f9732f2 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -353,7 +353,7 @@ def test_project_delete_no_options(self): identity_fakes.project_id, ] verifylist = [ - ('project', identity_fakes.project_id), + ('projects', [identity_fakes.project_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 3d2a402bc7..1a9e6aa7ef 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -271,7 +271,7 @@ def test_role_delete_no_options(self): identity_fakes.role_name, ] verifylist = [ - ('role', identity_fakes.role_name), + ('roles', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index bb59ebe568..9740ed82f2 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -461,7 +461,7 @@ def test_user_delete_no_options(self): identity_fakes.user_id, ] verifylist = [ - ('user', identity_fakes.user_id), + ('users', [identity_fakes.user_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) From ea53cc357a1f5d783b128cb9562ebc6c14203105 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 23 Dec 2014 16:30:47 -0600 Subject: [PATCH 0096/3303] Revert some docs changes from multi-delete The headers in the doc files are the commands, not a description. I missed thiese in the original reviews: https://review.openstack.org/140567 https://review.openstack.org/140581 Change-Id: Iae2631f6b485e8c568ff305e5992c193f80ebe71 --- doc/source/command-objects/project.rst | 6 +++--- doc/source/command-objects/role.rst | 6 +++--- doc/source/command-objects/server.rst | 8 ++++---- doc/source/command-objects/user.rst | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 56f2196f53..6b55b424bf 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -47,12 +47,12 @@ Create new project New project name -project(s) delete ------------------ +project delete +-------------- Delete project(s) -.. program:: project(s) delete +.. program:: project delete .. code:: bash os project delete diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 4c37fc38f8..19195eb554 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -56,12 +56,12 @@ Create new role New role name -role(s) delete --------------- +role delete +----------- Delete role(s) -.. program:: role(s) delete +.. program:: role delete .. code:: bash os role delete diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 7d5cdce56c..2f5aef1070 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -117,10 +117,10 @@ Create a new server :option:`` New server name -server(s) delete ----------------- +server delete +------------- -Delete server(s) command +Delete server(s) .. code:: bash @@ -128,7 +128,7 @@ Delete server(s) command [ ...] :option:`` - Server (name or ID) + Server to delete (name or ID) server list ----------- diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index 281e8e0d86..e54c65673c 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -69,12 +69,12 @@ Create new user New user name -user(s) delete --------------- +user delete +----------- Delete user(s) -.. program:: user(s) delete +.. program:: user delete .. code:: bash os user delete From 36ab944d2ecac5227880a6b09b4184bff4c0aba8 Mon Sep 17 00:00:00 2001 From: lin-hua-cheng Date: Mon, 22 Dec 2014 15:09:09 -0800 Subject: [PATCH 0097/3303] Allow service description to be set for KS V3 Change-Id: Ibf84882c9a9f408268c225190436fc1a534e1017 Closes-Bug: #1404997 --- openstackclient/identity/v3/service.py | 14 +++ openstackclient/tests/identity/v3/fakes.py | 2 + .../tests/identity/v3/test_service.py | 87 ++++++++++++++++++- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 4f62226968..f4c5d42629 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -43,6 +43,11 @@ def get_parser(self, prog_name): metavar='', help='New service name', ) + parser.add_argument( + '--description', + metavar='', + help='New service description', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -67,6 +72,7 @@ def take_action(self, parsed_args): service = identity_client.services.create( name=parsed_args.name, type=parsed_args.type, + description=parsed_args.description, enabled=enabled, ) @@ -137,6 +143,11 @@ def get_parser(self, prog_name): metavar='', help='New service name', ) + parser.add_argument( + '--description', + metavar='', + help='New service description', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -156,6 +167,7 @@ def take_action(self, parsed_args): if (not parsed_args.name and not parsed_args.type + and not parsed_args.description and not parsed_args.enable and not parsed_args.disable): return @@ -167,6 +179,8 @@ def take_action(self, parsed_args): kwargs['type'] = parsed_args.type if parsed_args.name: kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description if parsed_args.enable: kwargs['enabled'] = True if parsed_args.disable: diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 7acaa7f156..fbdac4ed15 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -147,11 +147,13 @@ service_id = 's-123' service_name = 'Texaco' service_type = 'gas' +service_description = 'oil brand' SERVICE = { 'id': service_id, 'name': service_name, 'type': service_type, + 'description': service_description, 'enabled': True, 'links': base_url + 'services/' + service_id, } diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 57db77b12a..5e4dc58508 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -51,6 +51,7 @@ def test_service_create_name(self): ] verifylist = [ ('name', identity_fakes.service_name), + ('description', None), ('enable', False), ('disable', False), ('type', identity_fakes.service_type), @@ -64,12 +65,50 @@ def test_service_create_name(self): self.services_mock.create.assert_called_with( name=identity_fakes.service_name, type=identity_fakes.service_type, + description=None, enabled=True, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, + True, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_description(self): + arglist = [ + '--description', identity_fakes.service_description, + identity_fakes.service_type, + ] + verifylist = [ + ('name', None), + ('description', identity_fakes.service_description), + ('enable', False), + ('disable', False), + ('type', identity_fakes.service_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name=, type=, enabled=, **kwargs) + self.services_mock.create.assert_called_with( + name=None, + type=identity_fakes.service_type, + description=identity_fakes.service_description, + enabled=True, + ) + + collist = ('description', 'enabled', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, @@ -84,6 +123,7 @@ def test_service_create_enable(self): ] verifylist = [ ('name', None), + ('description', None), ('enable', True), ('disable', False), ('type', identity_fakes.service_type), @@ -97,12 +137,14 @@ def test_service_create_enable(self): self.services_mock.create.assert_called_with( name=None, type=identity_fakes.service_type, + description=None, enabled=True, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, @@ -117,6 +159,7 @@ def test_service_create_disable(self): ] verifylist = [ ('name', None), + ('description', None), ('enable', False), ('disable', True), ('type', identity_fakes.service_type), @@ -130,12 +173,14 @@ def test_service_create_disable(self): self.services_mock.create.assert_called_with( name=None, type=identity_fakes.service_type, + description=None, enabled=False, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, @@ -239,6 +284,7 @@ def test_service_set_no_options(self): verifylist = [ ('type', None), ('name', None), + ('description', None), ('enable', False), ('disable', False), ('service', identity_fakes.service_name), @@ -256,6 +302,7 @@ def test_service_set_type(self): verifylist = [ ('type', identity_fakes.service_type), ('name', None), + ('description', None), ('enable', False), ('disable', False), ('service', identity_fakes.service_name), @@ -283,6 +330,7 @@ def test_service_set_name(self): verifylist = [ ('type', None), ('name', identity_fakes.service_name), + ('description', None), ('enable', False), ('disable', False), ('service', identity_fakes.service_name), @@ -302,6 +350,34 @@ def test_service_set_name(self): **kwargs ) + def test_service_set_description(self): + arglist = [ + '--description', identity_fakes.service_description, + identity_fakes.service_name, + ] + verifylist = [ + ('type', None), + ('name', None), + ('description', identity_fakes.service_description), + ('enable', False), + ('disable', False), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.service_description, + } + # ServiceManager.update(service, name=, type=, enabled=, **kwargs) + self.services_mock.update.assert_called_with( + identity_fakes.service_id, + **kwargs + ) + def test_service_set_enable(self): arglist = [ '--enable', @@ -310,6 +386,7 @@ def test_service_set_enable(self): verifylist = [ ('type', None), ('name', None), + ('description', None), ('enable', True), ('disable', False), ('service', identity_fakes.service_name), @@ -337,6 +414,7 @@ def test_service_set_disable(self): verifylist = [ ('type', None), ('name', None), + ('description', None), ('enable', False), ('disable', True), ('service', identity_fakes.service_name), @@ -388,9 +466,10 @@ def test_service_show(self): identity_fakes.service_name, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, From d240b709b9f2638382daa8b18227b9baf021596d Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Dec 2014 11:08:24 +0800 Subject: [PATCH 0098/3303] add doc for domain command Change-Id: I8b5575a5f27362fa375746b955e1f17a5a8b29a6 --- doc/source/command-objects/domain.rst | 115 ++++++++++++++++++++++++++ openstackclient/identity/v3/domain.py | 24 +++--- 2 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/domain.rst diff --git a/doc/source/command-objects/domain.rst b/doc/source/command-objects/domain.rst new file mode 100644 index 0000000000..057296ac5e --- /dev/null +++ b/doc/source/command-objects/domain.rst @@ -0,0 +1,115 @@ +====== +domain +====== + +Identity v3 + +domain create +------------- + +Create new domain + +.. program:: domain create +.. code:: bash + + os domain create + [--description ] + [--enable | --disable] + [--or-show] + + +.. option:: --description + + New domain description + +.. option:: --enable + + Enable domain (default) + +.. option:: --disable + + Disable domain + +.. option:: --or-show + + Return existing domain + + If the domain already exists, return the existing domain data and do not fail. + +.. option:: + + New domain name + +domain delete +------------- + +Delete domain + +.. program:: domain delete +.. code:: bash + + os domain delete + + +.. option:: + + Domain to delete (name or ID) + +domain list +----------- + +List domains + +.. program:: domain list +.. code:: bash + + os domain list + +domain set +---------- + +Set domain properties + +.. program:: domain set +.. code:: bash + + os domain set + [--name ] + [--description ] + [--enable | --disable] + + +.. option:: --name + + New domain name + +.. option:: --description + + New domain description + +.. option:: --enable + + Enable domain + +.. option:: --disable + + Disable domain + +.. option:: + + Domain to modify (name or ID) + +domain show +----------- + +Show domain details + +.. program:: domain show +.. code:: bash + + os domain show + + +.. option:: + + Domain to display (name or ID) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 9e50fe54c6..727f5b1838 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -29,7 +29,7 @@ class CreateDomain(show.ShowOne): - """Create domain command""" + """Create new domain""" log = logging.getLogger(__name__ + '.CreateDomain') @@ -42,7 +42,7 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar='', + metavar='', help='New domain description', ) enable_group = parser.add_mutually_exclusive_group() @@ -87,7 +87,7 @@ def take_action(self, parsed_args): class DeleteDomain(command.Command): - """Delete domain command""" + """Delete domain""" log = logging.getLogger(__name__ + '.DeleteDomain') @@ -96,7 +96,7 @@ def get_parser(self, prog_name): parser.add_argument( 'domain', metavar='', - help='Name or ID of domain to delete', + help='Domain to delete (name or ID)', ) return parser @@ -110,7 +110,7 @@ def take_action(self, parsed_args): class ListDomain(lister.Lister): - """List domain command""" + """List domains""" log = logging.getLogger(__name__ + '.ListDomain') @@ -126,7 +126,7 @@ def take_action(self, parsed_args): class SetDomain(command.Command): - """Set domain command""" + """Set domain properties""" log = logging.getLogger(__name__ + '.SetDomain') @@ -135,16 +135,16 @@ def get_parser(self, prog_name): parser.add_argument( 'domain', metavar='', - help='Name or ID of domain to change', + help='Domain to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', + metavar='', help='New domain name', ) parser.add_argument( '--description', - metavar='', + metavar='', help='New domain description', ) enable_group = parser.add_mutually_exclusive_group() @@ -152,7 +152,7 @@ def get_parser(self, prog_name): '--enable', dest='enabled', action='store_true', - help='Enable domain (default)', + help='Enable domain', ) enable_group.add_argument( '--disable', @@ -185,7 +185,7 @@ def take_action(self, parsed_args): class ShowDomain(show.ShowOne): - """Show domain command""" + """Show domain details""" log = logging.getLogger(__name__ + '.ShowDomain') @@ -194,7 +194,7 @@ def get_parser(self, prog_name): parser.add_argument( 'domain', metavar='', - help='Name or ID of domain to display', + help='Domain to display (name or ID)', ) return parser From e3ba13b32091831a7e5750d670921fce29424067 Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Dec 2014 11:43:02 +0800 Subject: [PATCH 0099/3303] add doc for role assignment command Change-Id: I594d444b6d1ec4e72bed03394178293737f26069 --- .../command-objects/role_assignment.rst | 45 +++++++++++++++++++ .../identity/v3/role_assignment.py | 12 ++--- 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 doc/source/command-objects/role_assignment.rst diff --git a/doc/source/command-objects/role_assignment.rst b/doc/source/command-objects/role_assignment.rst new file mode 100644 index 0000000000..749d883e54 --- /dev/null +++ b/doc/source/command-objects/role_assignment.rst @@ -0,0 +1,45 @@ +=============== +role assignment +=============== + +Identity v3 + +role assignment list +-------------------- + +List role assignments + +.. program:: role assignment list +.. code:: bash + + os role assignment list + [--role ] + [--user ] + [--group ] + [--domain ] + [--project ] + [--effective] + +.. option:: --role + + Role to filter (name or ID) + +.. option:: --user + + User to filter (name or ID) + +.. option:: --group + + Group to filter (name or ID) + +.. option:: --domain + + Domain to filter (name or ID) + +.. option:: --project + + Project to filter (name or ID) + +.. option:: --effective + + Returns only effective role assignments (defaults to False) diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index 5cc97e8d0f..f053b608c5 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -21,7 +21,7 @@ class ListRoleAssignment(lister.Lister): - """Lists role assignments according to the given filters""" + """List role assignments""" log = logging.getLogger(__name__ + '.ListRoleAssignment') @@ -36,29 +36,29 @@ def get_parser(self, prog_name): parser.add_argument( '--role', metavar='', - help='Name or ID of role to filter', + help='Role to filter (name or ID)', ) user_or_group = parser.add_mutually_exclusive_group() user_or_group.add_argument( '--user', metavar='', - help='Name or ID of user to filter', + help='User to filter (name or ID)', ) user_or_group.add_argument( '--group', metavar='', - help='Name or ID of group to filter', + help='Group to filter (name or ID)', ) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', metavar='', - help='Name or ID of domain to filter', + help='Domain to filter (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Name or ID of project to filter', + help='Project to filter (name or ID)', ) return parser From e5d71221adb17d05d14a1a692aed39dfc217151f Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Dec 2014 12:14:32 +0800 Subject: [PATCH 0100/3303] add doc for group command Change-Id: Iaaa0aeb42f9f940af63863f5d09011b5f7529281 --- doc/source/command-objects/group.rst | 192 +++++++++++++++++++++++++++ openstackclient/identity/v3/group.py | 36 ++--- 2 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 doc/source/command-objects/group.rst diff --git a/doc/source/command-objects/group.rst b/doc/source/command-objects/group.rst new file mode 100644 index 0000000000..85a0c5cd79 --- /dev/null +++ b/doc/source/command-objects/group.rst @@ -0,0 +1,192 @@ +===== +group +===== + +Identity v3 + +group add user +-------------- + +Add user to group + +.. program:: group add user +.. code:: bash + + os group add user + + + +.. option:: + + Group that user will be added to (name or ID) + +.. option:: + + User to add to group (name or ID) + +group contains user +------------------- + +Check user in group + +.. program:: group contains user +.. code:: bash + + os group contains user + + + +.. option:: + + Group to check if user belongs to (name or ID) + +.. option:: + + User to check (name or ID) + +group create +------------ + +Create new group + +.. program:: group create +.. code:: bash + + os group create + [--domain ] + [--description ] + [--or-show] + + +.. option:: --domain + + References the domain ID or name which owns the group + +.. option:: --description + + New group description + +.. option:: --or-show + + Return existing group + + If the group already exists, return the existing group data and do not fail. + +.. option:: + + New group name + +group delete +------------ + +Delete group + +.. program:: group delete +.. code:: bash + + os group delete + [--domain ] + [ ...] + +.. option:: --domain + + Domain where group resides (name or ID) + +.. option:: + + Group(s) to delete (name or ID) + +group list +---------- + +List groups + +.. program:: group list +.. code:: bash + + os group list + [--domain ] + [--user ] + [--long] + +.. option:: --domain + + Filter group list by (name or ID) + +.. option:: --user + + List group memberships for (name or ID) + +.. option:: --long + + List additional fields in output (defaults to false) + +group remove user +----------------- + +Remove user from group + +.. program:: group remove user +.. code:: bash + + os group remove user + + + +.. option:: + + Group that user will be removed from (name or ID) + +.. option:: + + User to remove from group (name or ID) + +group set +--------- + +Set group properties + +.. program:: group set +.. code:: bash + + os group set + [--name ] + [--domain ] + [--description ] + + +.. option:: --name + + New group name + +.. option:: --domain + + New domain that will now own the group (name or ID) + +.. option:: --description + + New group description + +.. option:: + + Group to modify (name or ID) + +group show +---------- + +Show group details + +.. program:: group show +.. code:: bash + + os group show + [--domain ] + + +.. option:: --domain + + Domain where group resides (name or ID) + +.. option:: + + Group to display (name or ID) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 327a64d5c0..fbd8dd720b 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -39,12 +39,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group name or ID that user will be added to', + help='Group that user will be added to (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User name or ID to add to group', + help='User to add to group (name or ID)', ) return parser @@ -68,7 +68,7 @@ def take_action(self, parsed_args): class CheckUserInGroup(command.Command): - """Checks that user is in a specific group""" + """Check user in group""" log = logging.getLogger(__name__ + '.CheckUserInGroup') @@ -77,12 +77,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group name or ID that user will be added to', + help='Group to check if user belongs to (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User name or ID to add to group', + help='User to check (name or ID)', ) return parser @@ -106,7 +106,7 @@ def take_action(self, parsed_args): class CreateGroup(show.ShowOne): - """Create group command""" + """Create new group""" log = logging.getLogger(__name__ + '.CreateGroup') @@ -118,11 +118,11 @@ def get_parser(self, prog_name): help='New group name') parser.add_argument( '--description', - metavar='', + metavar='', help='New group description') parser.add_argument( '--domain', - metavar='', + metavar='', help='References the domain ID or name which owns the group') parser.add_argument( '--or-show', @@ -268,12 +268,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group name or ID that user will be removed from', + help='Group that user will be removed from (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User name or ID to remove from group', + help='User to remove from group (name or ID)', ) return parser @@ -297,7 +297,7 @@ def take_action(self, parsed_args): class SetGroup(command.Command): - """Set group command""" + """Set group properties""" log = logging.getLogger(__name__ + '.SetGroup') @@ -306,18 +306,18 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Name or ID of group to change') + help='Group to modify (name or ID)') parser.add_argument( '--name', - metavar='', + metavar='', help='New group name') parser.add_argument( '--domain', - metavar='', - help='New domain name or ID that will now own the group') + metavar='', + help='New domain that will now own the group (name or ID)') parser.add_argument( '--description', - metavar='', + metavar='', help='New group description') return parser @@ -341,7 +341,7 @@ def take_action(self, parsed_args): class ShowGroup(show.ShowOne): - """Show group command""" + """Show group details""" log = logging.getLogger(__name__ + '.ShowGroup') @@ -350,7 +350,7 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Name or ID of group to display', + help='Group to display (name or ID)', ) parser.add_argument( '--domain', From 1927b03cc740378fcc1d146803cee988ec93e6a2 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 24 Dec 2014 06:08:19 -0700 Subject: [PATCH 0101/3303] Compute calls ignore region selection Calls to compute commands ignore region selection. The region is not passed to the get_endpoint call. Change-Id: I1ccfc56d7cb27a00b8982232a40ace21f2c0e9a2 Closes-Bug: 1405416 --- openstackclient/compute/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 3725350ac1..166747d59f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -50,6 +50,7 @@ def make_client(instance): extensions=extensions, http_log_debug=http_log_debug, timings=instance.timing, + region_name=instance._region_name, ) return client From 6e3c9a3d2dbcffd3789d11c65703cec07bf49ee3 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 19:09:58 -0500 Subject: [PATCH 0102/3303] Fix a few issues with 'usage list' * Added number of servers column, was missing * Added a new line character after the initial usage message Change-Id: I6c4e5bda6ba9ceafa92ecf13987c56d0bbe99961 --- openstackclient/compute/v2/usage.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index ed98af2672..05d6038f0b 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -60,12 +60,14 @@ def _format_project(project): compute_client = self.app.client_manager.compute columns = ( "tenant_id", + "server_usages", "total_memory_mb_usage", "total_vcpus_usage", "total_local_gb_usage" ) column_headers = ( "Project", + "Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours" @@ -84,7 +86,7 @@ def _format_project(project): else: end = now + datetime.timedelta(days=1) - usage_list = compute_client.usage.list(start, end) + usage_list = compute_client.usage.list(start, end, detailed=True) # Cache the project list project_cache = {} @@ -95,8 +97,8 @@ def _format_project(project): # Just forget it if there's any trouble pass - if len(usage_list) > 0: - sys.stdout.write("Usage from %s to %s:" % ( + if parsed_args.formatter == 'table' and len(usage_list) > 0: + sys.stdout.write("Usage from %s to %s: \n" % ( start.strftime(dateformat), end.strftime(dateformat), )) @@ -106,6 +108,7 @@ def _format_project(project): s, columns, formatters={ 'tenant_id': _format_project, + 'server_usages': lambda x: len(x), 'total_memory_mb_usage': lambda x: float("%.2f" % x), 'total_vcpus_usage': lambda x: float("%.2f" % x), 'total_local_gb_usage': lambda x: float("%.2f" % x), From 5761a0f0b75277140c081ab61b7f3c41edc82e31 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 22:25:01 -0500 Subject: [PATCH 0103/3303] Add usage show command Should show basic usage by project id, if not specified then use the project id the user is authN'ing with. Change-Id: I0284a5efd84075b18e1a7117cc9f8f7fecf16274 Closes-Bug: #1400796 --- openstackclient/compute/v2/usage.py | 74 +++++++++++++++++++++++++++++ setup.cfg | 2 + 2 files changed, 76 insertions(+) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 05d6038f0b..c71ecb1873 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -20,6 +20,8 @@ import sys from cliff import lister +from cliff import show +import six from openstackclient.common import utils @@ -114,3 +116,75 @@ def _format_project(project): 'total_local_gb_usage': lambda x: float("%.2f" % x), }, ) for s in usage_list)) + + +class ShowUsage(show.ShowOne): + """Show resource usage for a single project. """ + + log = logging.getLogger(__name__ + ".ShowUsage") + + def get_parser(self, prog_name): + parser = super(ShowUsage, self).get_parser(prog_name) + parser.add_argument( + "--start", + metavar="", + default=None, + help="Usage range start date, ex 2012-01-20" + " (default: 4 weeks ago)." + ) + parser.add_argument( + "--end", + metavar="", + default=None, + help="Usage range end date, ex 2012-01-20 (default: tomorrow)." + ) + parser.add_argument( + "--project", + metavar="", + default=None, + help="Name or ID of project to show usage for." + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + identity_client = self.app.client_manager.identity + compute_client = self.app.client_manager.compute + dateformat = "%Y-%m-%d" + now = datetime.datetime.utcnow() + + if parsed_args.start: + start = datetime.datetime.strptime(parsed_args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if parsed_args.end: + end = datetime.datetime.strptime(parsed_args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ).id + else: + # Get the project from the current auth + project = self.app.client_manager.auth_ref.project_id + + usage = compute_client.usage.get(project, start, end) + + if parsed_args.formatter == 'table': + sys.stdout.write("Usage from %s to %s on project %s: \n" % ( + start.strftime(dateformat), + end.strftime(dateformat), + project + )) + + info = {} + info['Servers'] = len(usage.server_usages) + info['RAM MB-Hours'] = float("%.2f" % usage.total_memory_mb_usage) + info['CPU Hours'] = float("%.2f" % usage.total_vcpus_usage) + info['Disk GB-Hours'] = float("%.2f" % usage.total_local_gb_usage) + return zip(*sorted(six.iteritems(info))) diff --git a/setup.cfg b/setup.cfg index 02b7751d43..9a92140a85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,6 +131,8 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer + usage_show = openstackclient.compute.v2.usage:ShowUsage + openstack.identity.v2 = catalog_list = openstackclient.identity.v2_0.catalog:ListCatalog catalog_show = openstackclient.identity.v2_0.catalog:ShowCatalog From 3ccf1a2606385bdded7301b6a3fc2b12ece2aca7 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 22:28:59 -0500 Subject: [PATCH 0104/3303] Rename `os project usage list` to `os usage list` There really isn't anything project specific about the command, it should really just be `os usage list`. For at least one development cycle we should keep the old command. Change-Id: I4d1df801576c259b527e87369f3121b94393cfa8 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 9a92140a85..3b208f5517 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,6 +131,7 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer + usage_list = openstackclient.compute.v2.usage:ListUsage usage_show = openstackclient.compute.v2.usage:ShowUsage openstack.identity.v2 = From 7ea5f89043b34c379f774577dee78560275fa797 Mon Sep 17 00:00:00 2001 From: zhiyuan_cai Date: Mon, 29 Dec 2014 10:30:52 +0800 Subject: [PATCH 0105/3303] Catch exception when getting quota Quota show command will list both the quotas of nova and cinder. But if cinder service is not enabled, EndpointNotFound exception will be raised and thus the command is broken. Catch this exception so quotas of nova can be listed. Change-Id: If2d2820675aa6a12e407d608fed846b21c953b2d Closes-Bug: #1390507 --- openstackclient/common/quota.py | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index edf4ffdb98..6d04b5c99d 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -147,6 +147,21 @@ def get_parser(self, prog_name): ) return parser + def get_quota(self, client, parsed_args): + try: + if parsed_args.quota_class: + quota = client.quota_classes.get(parsed_args.project) + elif parsed_args.default: + quota = client.quotas.defaults(parsed_args.project) + else: + quota = client.quotas.get(parsed_args.project) + except Exception as e: + if type(e).__name__ == 'EndpointNotFound': + return {} + else: + raise e + return quota._info + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) @@ -159,23 +174,12 @@ def take_action(self, parsed_args): # does not exist. If this is determined to be the # intended behaviour of the API we will validate # the argument with Identity ourselves later. - if parsed_args.quota_class: - compute_quota = compute_client.quota_classes.get( - parsed_args.project) - volume_quota = volume_client.quota_classes.get( - parsed_args.project) - elif parsed_args.default: - compute_quota = compute_client.quotas.defaults( - parsed_args.project) - volume_quota = volume_client.quotas.defaults( - parsed_args.project) - else: - compute_quota = compute_client.quotas.get(parsed_args.project) - volume_quota = volume_client.quotas.get(parsed_args.project) + compute_quota_info = self.get_quota(compute_client, parsed_args) + volume_quota_info = self.get_quota(volume_client, parsed_args) info = {} - info.update(compute_quota._info) - info.update(volume_quota._info) + info.update(compute_quota_info) + info.update(volume_quota_info) # Map the internal quota names to the external ones for k, v in itertools.chain( From d5caa6a26baf2fccbf276080fef60c81ed4beae3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 23 Dec 2014 17:04:12 -0600 Subject: [PATCH 0106/3303] Command object docs: container, object Change-Id: Ie3df543a28cbee0cc809310a05f431c97b2c7e70 --- doc/source/command-objects/container.rst | 105 +++++++++++++++++ doc/source/command-objects/object.rst | 140 +++++++++++++++++++++++ openstackclient/object/v1/container.py | 18 +-- openstackclient/object/v1/object.py | 32 +++--- 4 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 doc/source/command-objects/container.rst create mode 100644 doc/source/command-objects/object.rst diff --git a/doc/source/command-objects/container.rst b/doc/source/command-objects/container.rst new file mode 100644 index 0000000000..3afaeb9217 --- /dev/null +++ b/doc/source/command-objects/container.rst @@ -0,0 +1,105 @@ +========= +container +========= + +Object Store v1 + +container create +---------------- + +Create new container + +.. program:: container create +.. code:: bash + + os container create + [ ...] + +.. option:: + + New container name(s) + +container delete +---------------- + +Delete container + +.. program:: container delete +.. code:: bash + + os container delete + [ ...] + +.. option:: + + Container(s) to delete + +container list +-------------- + +List containers + +.. program:: container list +.. code::bash + + os container list + [--prefix ] + [--marker ] + [--end-marker ] + [--limit ] + [--long] + [--all] + +.. option:: --prefix + + Filter list using + +.. option:: --marker + + Anchor for paging + +.. option:: --end-marker + + End anchor for paging + +.. option:: --limit + + Limit the number of containers returned + +.. option:: --long + + List additional fields in output + +.. options:: --all + + List all containers (default is 10000) + +container save +-------------- + +Save container contents locally + +.. program:: container save +.. code:: bash + + os container save + + +.. option:: + + Container to save + +container show +-------------- + +Show container details + +.. program:: container show +.. code:: bash + + os container show + [] + +.. option:: + + Container to display diff --git a/doc/source/command-objects/object.rst b/doc/source/command-objects/object.rst new file mode 100644 index 0000000000..5cbc95d71c --- /dev/null +++ b/doc/source/command-objects/object.rst @@ -0,0 +1,140 @@ +====== +object +====== + +Object Store v1 + +object create +------------- + +Upload object to container + +.. program:: object create +.. code:: bash + + os object create + + [ ...] + +.. option:: + + Container for new object + +.. option:: + + Local filename(s) to upload + +object delete +------------- + +Delete object from container + +.. program:: object delete +.. code:: bash + + os object delete + + [ ...] + +.. option:: + + Delete object(s) from + +.. option:: + + Object(s) to delete + +list object +----------- + +List objects + +.. program object list +.. code:: bash + + os object list + [--prefix ] + [--delimiter ] + [--marker ] + [--end-marker ] + [--limit ] + [--long] + [--all] + ] + +.. option:: --prefix + + Filter list using + +.. option:: --delimiter + + Roll up items with + +.. option:: --marker + + Anchor for paging + +.. option:: --end-marker + + End anchor for paging + +.. option:: --limit + + Limit number of objects returned + +.. option:: --long + + List additional fields in output + +.. options:: --all + + List all objects in (default is 10000) + +.. option:: + + Container to list + +object save +----------- + +Save object locally + +.. program:: object save +.. code:: bash + + os object save + [--file ] + [] + [] + +.. option:: --file + + Destination filename (defaults to object name) + +.. option:: + + Download from + +.. option:: + + Object to save + +object show +----------- + +Show object details + +.. program:: object show +.. code:: bash + + os object show + + + +.. option:: + + Display from + +.. option:: + + Object to display diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index ead3df45e9..b75888e408 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -27,7 +27,7 @@ class CreateContainer(lister.Lister): - """Create a container""" + """Create new container""" log = logging.getLogger(__name__ + '.CreateContainer') @@ -35,9 +35,9 @@ def get_parser(self, prog_name): parser = super(CreateContainer, self).get_parser(prog_name) parser.add_argument( 'containers', - metavar='', + metavar='', nargs="+", - help='Container name(s) to create', + help='New container name(s)', ) return parser @@ -60,7 +60,7 @@ def take_action(self, parsed_args): class DeleteContainer(command.Command): - """Delete a container""" + """Delete container""" log = logging.getLogger(__name__ + '.DeleteContainer') @@ -70,7 +70,7 @@ def get_parser(self, prog_name): 'containers', metavar='', nargs="+", - help='Container name(s) to delete', + help='Container(s) to delete', ) return parser @@ -157,7 +157,7 @@ def take_action(self, parsed_args): class SaveContainer(command.Command): - """Save the contents of a container locally""" + """Save container contents locally""" log = logging.getLogger(__name__ + ".SaveContainer") @@ -166,7 +166,7 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container name to save', + help='Container to save', ) return parser @@ -179,7 +179,7 @@ def take_action(self, parsed_args): class ShowContainer(show.ShowOne): - """Show container information""" + """Show container details""" log = logging.getLogger(__name__ + '.ShowContainer') @@ -188,7 +188,7 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container name to display', + help='Container to display', ) return parser diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index cbe9da2fb4..781dd047fa 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -27,7 +27,7 @@ class CreateObject(lister.Lister): - """Upload an object to a container""" + """Upload object to container""" log = logging.getLogger(__name__ + '.CreateObject') @@ -36,13 +36,13 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container to store new object', + help='Container for new object', ) parser.add_argument( 'objects', - metavar='', + metavar='', nargs="+", - help='Local path of object(s) to upload', + help='Local filename(s) to upload', ) return parser @@ -66,7 +66,7 @@ def take_action(self, parsed_args): class DeleteObject(command.Command): - """Delete an object within a container""" + """Delete object from container""" log = logging.getLogger(__name__ + '.DeleteObject') @@ -75,11 +75,11 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container that stores the object to delete', + help='Delete object(s) from ', ) parser.add_argument( 'objects', - metavar='', + metavar='', nargs="+", help='Object(s) to delete', ) @@ -104,8 +104,8 @@ def get_parser(self, prog_name): parser = super(ListObject, self).get_parser(prog_name) parser.add_argument( "container", - metavar="", - help="List contents of container-name", + metavar="", + help="Container to list", ) parser.add_argument( "--prefix", @@ -188,7 +188,7 @@ def take_action(self, parsed_args): class SaveObject(command.Command): - """Save an object locally""" + """Save object locally""" log = logging.getLogger(__name__ + ".SaveObject") @@ -197,17 +197,17 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", - help="Downloaded object filename [defaults to object name]", + help="Destination filename (defaults to object name)", ) parser.add_argument( 'container', metavar='', - help='Container name that has the object', + help='Download from ', ) parser.add_argument( "object", metavar="", - help="Name of the object to save", + help="Object to save", ) return parser @@ -222,7 +222,7 @@ def take_action(self, parsed_args): class ShowObject(show.ShowOne): - """Show object information""" + """Show object details""" log = logging.getLogger(__name__ + '.ShowObject') @@ -231,12 +231,12 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container name for object to display', + help='Display from ', ) parser.add_argument( 'object', metavar='', - help='Object name to display', + help='Object to display', ) return parser From 4a07e63e7ea4b99b10e4a3fd9ed06c0cf6ee905f Mon Sep 17 00:00:00 2001 From: lin-hua-cheng Date: Fri, 19 Dec 2014 18:40:40 -0800 Subject: [PATCH 0107/3303] type should be required for v2.0 service create Updated the service name to be optional, mostly matching the cli arguments with v3 service create. Implemented the following changes on service create: - if only a single positional is present, it's a . This is not currently legal so it is considered a new case. - if --type option is present the positional is handled as ; display deprecation message - if --name option is present the positional is handled as . Making --type optional is new, but back-compatible - Made --name and --type mutually exclusive. - only '--name ' shall appear in the help output Change-Id: I8fd4adba3d8cd00d5a8cacc2c494d99d492c45a3 Closes-Bug: #1404073 --- openstackclient/identity/v2_0/service.py | 41 ++++++++-- .../tests/identity/v2_0/test_service.py | 75 ++++++++++++++++++- 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index e8848ddee2..0b98a90360 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -15,6 +15,7 @@ """Service action implementations""" +import argparse import logging import six @@ -36,15 +37,20 @@ class CreateService(show.ShowOne): def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) parser.add_argument( - 'name', - metavar='', - help=_('New service name'), + 'type_or_name', + metavar='', + help=_('New service type (compute, image, identity, volume, etc)'), ) - parser.add_argument( + type_or_name_group = parser.add_mutually_exclusive_group() + type_or_name_group.add_argument( '--type', metavar='', - required=True, - help=_('New service type (compute, image, identity, volume, etc)'), + help=argparse.SUPPRESS, + ) + type_or_name_group.add_argument( + '--name', + metavar='', + help=_('New service name'), ) parser.add_argument( '--description', @@ -57,9 +63,28 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + type_or_name = parsed_args.type_or_name + name = parsed_args.name + type = parsed_args.type + + # If only a single positional is present, it's a . + # This is not currently legal so it is considered a new case. + if not type and not name: + type = type_or_name + # If --type option is present then positional is handled as ; + # display deprecation message. + elif type: + name = type_or_name + self.log.warning(_('The argument --type is deprecated, use service' + ' create --name type instead.')) + # If --name option is present the positional is handled as . + # Making --type optional is new, but back-compatible + elif name: + type = type_or_name + service = identity_client.services.create( - parsed_args.name, - parsed_args.type, + name, + type, parsed_args.description) info = {} diff --git a/openstackclient/tests/identity/v2_0/test_service.py b/openstackclient/tests/identity/v2_0/test_service.py index 6c93574bf5..a0adea4e9e 100644 --- a/openstackclient/tests/identity/v2_0/test_service.py +++ b/openstackclient/tests/identity/v2_0/test_service.py @@ -44,14 +44,80 @@ def setUp(self): # Get the command object to test self.cmd = service.CreateService(self.app, None) - def test_service_create_name_type(self): + def test_service_create_with_type_positional(self): + arglist = [ + identity_fakes.service_type, + ] + verifylist = [ + ('type_or_name', identity_fakes.service_type), + ('type', None), + ('description', None), + ('name', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, service_type, description) + self.services_mock.create.assert_called_with( + None, + identity_fakes.service_type, + None, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_with_type_option(self): arglist = [ '--type', identity_fakes.service_type, identity_fakes.service_name, ] verifylist = [ + ('type_or_name', identity_fakes.service_name), ('type', identity_fakes.service_type), ('description', None), + ('name', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, service_type, description) + self.services_mock.create.assert_called_with( + identity_fakes.service_name, + identity_fakes.service_type, + None, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_with_name_option(self): + arglist = [ + '--name', identity_fakes.service_name, + identity_fakes.service_type, + ] + verifylist = [ + ('type_or_name', identity_fakes.service_type), + ('type', None), + ('description', None), ('name', identity_fakes.service_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -78,12 +144,13 @@ def test_service_create_name_type(self): def test_service_create_description(self): arglist = [ - '--type', identity_fakes.service_type, + '--name', identity_fakes.service_name, '--description', identity_fakes.service_description, - identity_fakes.service_name, + identity_fakes.service_type, ] verifylist = [ - ('type', identity_fakes.service_type), + ('type_or_name', identity_fakes.service_type), + ('type', None), ('description', identity_fakes.service_description), ('name', identity_fakes.service_name), ] From b81d0f4d08e7f6a26ed60a7aceaf1e3d46f1c7ac Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 30 Dec 2014 23:23:39 -0500 Subject: [PATCH 0108/3303] Bunch of formatting tweaks to server-image docs Change-Id: Id2dad09ea75e0615519862db007700389db8cd51 --- doc/source/command-objects/server-image.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/source/command-objects/server-image.rst b/doc/source/command-objects/server-image.rst index 681f6e4f2c..4577b25b20 100644 --- a/doc/source/command-objects/server-image.rst +++ b/doc/source/command-objects/server-image.rst @@ -10,6 +10,7 @@ server image create Create a new disk image from a running server +.. program:: server image create .. code:: bash os server image create @@ -17,11 +18,14 @@ Create a new disk image from a running server [--wait] -:option:`--name` +.. option:: --name + Name of new image (default is server name) -:option:`--wait` +.. option:: --wait + Wait for image create to complete -:option:`` +.. describe:: + Server (name or ID) From caef59a4a8bd49d785f60d035fd02e3fc06c8252 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 31 Dec 2014 02:54:36 -0500 Subject: [PATCH 0109/3303] Add docs for listing availability zones Change-Id: I4c005e1d8089b46feca6cd3266f63c408648f074 --- .../command-objects/availability_zone.rst | 20 +++++++++++++++++++ doc/source/commands.rst | 1 + .../compute/v2/availability_zone.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 doc/source/command-objects/availability_zone.rst diff --git a/doc/source/command-objects/availability_zone.rst b/doc/source/command-objects/availability_zone.rst new file mode 100644 index 0000000000..3743523088 --- /dev/null +++ b/doc/source/command-objects/availability_zone.rst @@ -0,0 +1,20 @@ +================= +availability zone +================= + +Compute v2 + +availability zone list +---------------------- + +List availability zones and their status + +.. program availability zone list +.. code:: bash + + os availability zone list + [--long] + +.. option:: --long + + List additional fields in output diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..50c72f6330 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -70,6 +70,7 @@ the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token +* ``availability zone``: (**Compute**) a logical partition of hosts or volume services * ``aggregate``: (**Compute**) a grouping of servers * ``backup``: Volume - a volume copy * ``catalog``: (**Identity**) service catalog diff --git a/openstackclient/compute/v2/availability_zone.py b/openstackclient/compute/v2/availability_zone.py index c7c9241491..648c0ee4ce 100644 --- a/openstackclient/compute/v2/availability_zone.py +++ b/openstackclient/compute/v2/availability_zone.py @@ -59,7 +59,7 @@ def _xform_availability_zone(az, include_extra): class ListAvailabilityZone(lister.Lister): - """List Availability Zones and their status""" + """List availability zones and their status""" log = logging.getLogger(__name__ + '.ListAvailabilityZone') From b5ce0f145f9342a01370ac34a7a11d40df703249 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 10:05:37 -0600 Subject: [PATCH 0110/3303] Command docs: region Fix up formatting fro region command docs and help Change-Id: Icf8c03da38b30fc69e7fe70f9c14aaa99881d320 --- doc/source/command-objects/region.rst | 68 +++++++++++++++++---------- doc/source/commands.rst | 2 +- openstackclient/identity/v3/region.py | 38 +++++++-------- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst index 788ed6facb..d1aedb3161 100644 --- a/doc/source/command-objects/region.rst +++ b/doc/source/command-objects/region.rst @@ -9,24 +9,30 @@ region create Create new region +.. program:: region create .. code:: bash os region create [--parent-region ] - [--description ] - [--url ] + [--description ] + [--url ] -:option:`--parent-region` - Parent region +.. option:: --parent-region + + Parent region ID + +.. option:: --description -:option:`--description` New region description -:option:`--url` +.. option:: --url + New region URL -:option:`` +.. _region_create-region-id: +.. describe:: + New region ID region delete @@ -34,61 +40,75 @@ region delete Delete region +.. program:: region delete .. code:: bash os region delete - + + +.. _region_delete-region-id: +.. describe:: -:option:`` - Region to delete + Region ID to delete region list ----------- List regions +.. program:: region list .. code:: bash os region list [--parent-region ] -:option:`--parent-region` - Filter by a specific parent region +.. option:: --parent-region + + Filter by parent region ID region set ---------- Set region properties +.. program:: region set .. code:: bash os region set [--parent-region ] - [--description ] - [--url ] - + [--description ] + [--url ] + -:option:`--parent-region` - New parent region +.. option:: --parent-region + + New parent region ID + +.. option:: --description -:option:`--description` New region description -:option:`--url` +.. option:: --url + New region URL -:option:`` +.. _region_set-region-id: +.. describe:: + Region ID to modify region show ----------- -Show region +Display region details +.. program:: region show .. code:: bash os region show - + -:option:`` - Region ID to modify +.. _region_show-region-id: +.. describe:: + + Region ID to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..6aaa25e768 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -97,7 +97,7 @@ referring to both Compute and Volume quotas. * ``policy``: Identity - determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions -* ``region``: (**Identity**) +* ``region``: (**Identity**) a subset of an OpenStack deployment * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index cce3417d5a..5fb7391362 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -35,22 +35,22 @@ def get_parser(self, prog_name): # seems like poor UX, we will only support user-defined IDs. parser.add_argument( 'region', - metavar='', - help=_('New region'), + metavar='', + help=_('New region ID'), ) parser.add_argument( '--parent-region', - metavar='', - help=_('The parent region of new region'), + metavar='', + help=_('Parent region ID'), ) parser.add_argument( '--description', - metavar='', + metavar='', help=_('New region description'), ) parser.add_argument( '--url', - metavar='', + metavar='', help=_('New region url'), ) @@ -82,8 +82,8 @@ def get_parser(self, prog_name): parser = super(DeleteRegion, self).get_parser(prog_name) parser.add_argument( 'region', - metavar='', - help=_('Region to delete'), + metavar='', + help=_('Region ID to delete'), ) return parser @@ -104,8 +104,8 @@ def get_parser(self, prog_name): parser = super(ListRegion, self).get_parser(prog_name) parser.add_argument( '--parent-region', - metavar='', - help=_('Filter by parent region'), + metavar='', + help=_('Filter by parent region ID'), ) return parser @@ -137,22 +137,22 @@ def get_parser(self, prog_name): parser = super(SetRegion, self).get_parser(prog_name) parser.add_argument( 'region', - metavar='', - help=_('Region to change'), + metavar='', + help=_('Region ID to modify'), ) parser.add_argument( '--parent-region', - metavar='', - help=_('New parent region of the region'), + metavar='', + help=_('New parent region ID'), ) parser.add_argument( '--description', - metavar='', + metavar='', help=_('New region description'), ) parser.add_argument( '--url', - metavar='', + metavar='', help=_('New region url'), ) return parser @@ -179,7 +179,7 @@ def take_action(self, parsed_args): class ShowRegion(show.ShowOne): - """Show region""" + """Display region details""" log = logging.getLogger(__name__ + '.ShowRegion') @@ -187,8 +187,8 @@ def get_parser(self, prog_name): parser = super(ShowRegion, self).get_parser(prog_name) parser.add_argument( 'region', - metavar='', - help=_('Region to display'), + metavar='', + help=_('Region ID to display'), ) return parser From f18f264ed7b12fb73c3760514dcb226e28189572 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 09:54:21 -0600 Subject: [PATCH 0111/3303] Command docs: domain Change the implementation of --enable|--disable on domain create and set commands to our usual style. Change-Id: I10f2b96281a114fa3cf3b001394844770b2a8632 --- doc/source/command-objects/domain.rst | 8 +++---- doc/source/commands.rst | 2 +- openstackclient/identity/v3/domain.py | 24 ++++++++++--------- .../tests/identity/v3/test_domain.py | 10 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/source/command-objects/domain.rst b/doc/source/command-objects/domain.rst index 057296ac5e..66697ac3ea 100644 --- a/doc/source/command-objects/domain.rst +++ b/doc/source/command-objects/domain.rst @@ -36,7 +36,7 @@ Create new domain If the domain already exists, return the existing domain data and do not fail. -.. option:: +.. describe:: New domain name @@ -51,7 +51,7 @@ Delete domain os domain delete -.. option:: +.. describe:: Domain to delete (name or ID) @@ -95,7 +95,7 @@ Set domain properties Disable domain -.. option:: +.. describe:: Domain to modify (name or ID) @@ -110,6 +110,6 @@ Show domain details os domain show -.. option:: +.. describe:: Domain to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..f82d307ea2 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -78,7 +78,7 @@ referring to both Compute and Volume quotas. * ``consumer``: Identity - OAuth-based delegatee * ``container``: Object Store - a grouping of objects * ``credentials``: (**Identity**) specific to identity providers -* ``domain``: Identity - a grouping of projects +* ``domain``: (**Identity**) a grouping of projects * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 727f5b1838..189f097052 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -48,15 +48,14 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable domain') + help='Enable domain (default)', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable domain') + action='store_true', + help='Disable domain', + ) parser.add_argument( '--or-show', action='store_true', @@ -68,11 +67,15 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + enabled = True + if parsed_args.disable: + enabled = False + try: domain = identity_client.domains.create( name=parsed_args.name, description=parsed_args.description, - enabled=parsed_args.enabled, + enabled=enabled, ) except ksc_exc.Conflict as e: if parsed_args.or_show: @@ -150,13 +153,11 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', help='Enable domain', ) enable_group.add_argument( '--disable', - dest='disabled', action='store_true', help='Disable domain', ) @@ -172,9 +173,10 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.description: kwargs['description'] = parsed_args.description - if parsed_args.enabled: + + if parsed_args.enable: kwargs['enabled'] = True - if parsed_args.disabled: + if parsed_args.disable: kwargs['enabled'] = False if not kwargs: diff --git a/openstackclient/tests/identity/v3/test_domain.py b/openstackclient/tests/identity/v3/test_domain.py index 8dad5bcc7a..cfec10e7c3 100644 --- a/openstackclient/tests/identity/v3/test_domain.py +++ b/openstackclient/tests/identity/v3/test_domain.py @@ -46,7 +46,6 @@ def test_domain_create_no_options(self): identity_fakes.domain_name, ] verifylist = [ - ('enabled', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -81,7 +80,6 @@ def test_domain_create_description(self): ] verifylist = [ ('description', 'new desc'), - ('enabled', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -115,7 +113,7 @@ def test_domain_create_enable(self): identity_fakes.domain_name, ] verifylist = [ - ('enabled', True), + ('enable', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -149,7 +147,7 @@ def test_domain_create_disable(self): identity_fakes.domain_name, ] verifylist = [ - ('enabled', False), + ('disable', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -333,7 +331,7 @@ def test_domain_set_enable(self): identity_fakes.domain_id, ] verifylist = [ - ('enabled', True), + ('enable', True), ('domain', identity_fakes.domain_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -356,7 +354,7 @@ def test_domain_set_disable(self): identity_fakes.domain_id, ] verifylist = [ - ('disabled', True), + ('disable', True), ('domain', identity_fakes.domain_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) From 0720c7819902d5ae27884afa49c973902467a50a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 12:09:50 -0600 Subject: [PATCH 0112/3303] Command docs: flavor Change-Id: Ie85ff7706ef08b70ab8ba99533465d90904cf393 --- doc/source/command-objects/flavor.rst | 105 ++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/compute/v2/flavor.py | 31 +++++--- 3 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 doc/source/command-objects/flavor.rst diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst new file mode 100644 index 0000000000..4c98e85889 --- /dev/null +++ b/doc/source/command-objects/flavor.rst @@ -0,0 +1,105 @@ +====== +flavor +====== + +flavor create +------------- + +Create new flavor + +.. program:: flavor create +.. code:: bash + + os flavor create + [--id ] + [--ram ] + [--disk ] + [--ephemeral-disk ] + [--swap ] + [--vcpus ] + [--rxtx-factor ] + [--public | --private] + + +.. option:: --id + + Unique flavor ID; 'auto' creates a UUID (default: auto) + +.. option:: --ram + + Memory size in MB (default 256M) + +.. option:: --disk + + Disk size in GB (default 0G) + +.. option:: --ephemeral-disk + + Ephemeral disk size in GB (default 0G) + +.. option:: --swap + + Swap space size in GB (default 0G) + +.. option:: --vcpus + + Number of vcpus (default 1) + +.. option:: --rxtx-factor + + RX/TX factor (default 1) + +.. option:: --public + + Flavor is available to other projects (default) + +.. option:: --private + + Flavor is not available to other projects + +.. _flavor_create-flavor-name: +.. describe:: + + New flavor name + +flavor delete +------------- + +Delete a flavor + +.. program:: flavor delete +.. code:: bash + + os flavor delete + + +.. _flavor_delete-flavor: +.. describe:: + + Flavor to delete (name or ID) + +flavor list +----------- + +List flavors + +.. program:: flavor list +.. code:: bash + + os flavor list + +flavor show +----------- + +Display flavor details + +.. program:: flavor show +.. code:: bash + + os flavor show + + +.. _flavor_show-flavor: +.. describe:: + + Flavor to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 50c72f6330..98084ea102 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -82,7 +82,7 @@ referring to both Compute and Volume quotas. * ``domain``: Identity - a grouping of projects * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions -* ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc +* ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc * ``group``: Identity - a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 6dd00b1b9e..6f3788a029 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -34,64 +34,71 @@ def get_parser(self, prog_name): parser = super(CreateFlavor, self).get_parser(prog_name) parser.add_argument( "name", - metavar="", + metavar="", help="New flavor name", ) parser.add_argument( "--id", metavar="", default='auto', - help="Unique flavor ID; 'auto' will create a UUID " - "(default: auto)") + help="Unique flavor ID; 'auto' creates a UUID " + "(default: auto)", + ) parser.add_argument( "--ram", type=int, metavar="", default=256, - help="Memory size in MB (default 256M)") + help="Memory size in MB (default 256M)", + ) parser.add_argument( "--disk", type=int, metavar="", default=0, - help="Disk size in GB (default 0G)") + help="Disk size in GB (default 0G)", + ) parser.add_argument( "--ephemeral", type=int, metavar="", + default=0, help="Ephemeral disk size in GB (default 0G)", - default=0) + ) parser.add_argument( "--swap", type=int, metavar="", + default=0, help="Swap space size in GB (default 0G)", - default=0) + ) parser.add_argument( "--vcpus", type=int, metavar="", default=1, - help="Number of vcpus (default 1)") + help="Number of vcpus (default 1)", + ) parser.add_argument( "--rxtx-factor", type=int, metavar="", + default=1, help="RX/TX factor (default 1)", - default=1) + ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", dest="public", action="store_true", default=True, - help="Flavor is accessible to other projects (default)", + help="Flavor is available to other projects (default)", ) public_group.add_argument( "--private", dest="public", action="store_false", - help="Flavor is inaccessible to other projects", + help="Flavor is not available to other projects", ) return parser @@ -168,7 +175,7 @@ def take_action(self, parsed_args): class ShowFlavor(show.ShowOne): - """Show flavor details""" + """Display flavor details""" log = logging.getLogger(__name__ + ".ShowFlavor") From 480921d0e8009e20372037cba4fb6e381760a9c1 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 31 Dec 2014 02:49:15 -0500 Subject: [PATCH 0113/3303] Add docs for usage show/list Change-Id: Iaf911d69a0b63d705f8789a4640018a428b87be6 --- doc/source/command-objects/usage.rst | 50 ++++++++++++++++++++++++++++ doc/source/commands.rst | 1 + openstackclient/compute/v2/usage.py | 12 +++---- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 doc/source/command-objects/usage.rst diff --git a/doc/source/command-objects/usage.rst b/doc/source/command-objects/usage.rst new file mode 100644 index 0000000000..551176c700 --- /dev/null +++ b/doc/source/command-objects/usage.rst @@ -0,0 +1,50 @@ +===== +usage +===== + +Compute v2 + +usage list +---------- + +List resource usage per project + +.. program:: usage list +.. code:: bash + + os usage list + --start + --end + +.. option:: --start + + Usage range start date, ex 2012-01-20 (default: 4 weeks ago). + +.. option:: --end + + Usage range end date, ex 2012-01-20 (default: tomorrow) + +usage show +---------- + +Show resource usage for a single project. + +.. program:: usage show +.. code:: bash + + os usage show + --project + --start + --end + +.. option:: --project + + Name or ID of project to show usage for. + +.. option:: --start + + Usage range start date, ex 2012-01-20 (default: 4 weeks ago). + +.. option:: --end + + Usage range end date, ex 2012-01-20 (default: tomorrow) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..37d3107de8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -107,6 +107,7 @@ referring to both Compute and Volume quotas. * ``service``: Identity - a cloud service * ``snapshot``: Volume - a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service +* ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user * ``volume``: Volume - block volumes diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index c71ecb1873..308241cf25 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -125,6 +125,12 @@ class ShowUsage(show.ShowOne): def get_parser(self, prog_name): parser = super(ShowUsage, self).get_parser(prog_name) + parser.add_argument( + "--project", + metavar="", + default=None, + help="Name or ID of project to show usage for." + ) parser.add_argument( "--start", metavar="", @@ -138,12 +144,6 @@ def get_parser(self, prog_name): default=None, help="Usage range end date, ex 2012-01-20 (default: tomorrow)." ) - parser.add_argument( - "--project", - metavar="", - default=None, - help="Name or ID of project to show usage for." - ) return parser def take_action(self, parsed_args): From db6986bec6d7799a297e7cf3c5a6da2d5a101541 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 30 Dec 2014 23:37:36 -0500 Subject: [PATCH 0114/3303] Add missing content for token commands Minor tweaks and added some content to token issue/revoke Change-Id: Icdad6354f008f9c109d263e115acd10ff113695a --- doc/source/command-objects/token.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/source/command-objects/token.rst b/doc/source/command-objects/token.rst index aec87d2850..22260f0dec 100644 --- a/doc/source/command-objects/token.rst +++ b/doc/source/command-objects/token.rst @@ -7,6 +7,9 @@ Identity v2, v3 token issue ----------- +Issue new token + +.. program:: token issue .. code:: bash os token issue @@ -14,6 +17,16 @@ token issue token revoke ------------ +*Identity version 2 only.* + +Revoke existing token + +.. program:: token revoke .. code:: bash os token revoke + + +.. describe:: + + Token to be deleted From e7ec6bc6e447ce5f455551874fdf09828c440500 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 22:49:13 -0500 Subject: [PATCH 0115/3303] Rename column to `default project id` for long listing v3 user Previously this column was coming up as empty, since user's have a `default project id`, not just `project id`. Change-Id: I3d7f7eb600e9526b9c6cc2a8c5d6009b9100b1f5 --- openstackclient/identity/v3/user.py | 13 ++-- openstackclient/tests/identity/v3/fakes.py | 2 +- .../tests/identity/v3/test_user.py | 61 +++++++++++-------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 665dd4bb16..d7ccff53db 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -15,6 +15,7 @@ """Identity v3 User action implementations""" +import copy import logging import six @@ -217,17 +218,21 @@ def take_action(self, parsed_args): # List users if parsed_args.long: - columns = ('ID', 'Name', 'Project Id', 'Domain Id', - 'Description', 'Email', 'Enabled') + columns = ['ID', 'Name', 'Default Project Id', 'Domain Id', + 'Description', 'Email', 'Enabled'] + column_headers = copy.deepcopy(columns) + column_headers[2] = 'Project' + column_headers[3] = 'Domain' else: - columns = ('ID', 'Name') + columns = ['ID', 'Name'] + column_headers = copy.deepcopy(columns) data = identity_client.users.list( domain=domain, group=group, ) return ( - columns, + column_headers, (utils.get_item_properties( s, columns, formatters={}, diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 7acaa7f156..88716e975a 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -179,7 +179,7 @@ USER = { 'id': user_id, 'name': user_name, - 'project_id': project_id, + 'default_project_id': project_id, 'email': user_email, 'enabled': True, 'domain_id': domain_id, diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index bb59ebe568..af58b230c5 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -102,15 +102,16 @@ def test_user_create_no_options(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -147,15 +148,16 @@ def test_user_create_password(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -195,15 +197,16 @@ def test_user_create_password_prompt(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -239,15 +242,16 @@ def test_user_create_email(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -260,7 +264,7 @@ def test_user_create_project(self): ) # Set up to return an updated user USER_2 = copy.deepcopy(identity_fakes.USER) - USER_2['project_id'] = identity_fakes.PROJECT_2['id'] + USER_2['default_project_id'] = identity_fakes.PROJECT_2['id'] self.users_mock.create.return_value = fakes.FakeResource( None, USER_2, @@ -298,15 +302,16 @@ def test_user_create_project(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.PROJECT_2['id'], identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.PROJECT_2['id'], ) self.assertEqual(data, datalist) @@ -342,15 +347,16 @@ def test_user_create_domain(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -385,15 +391,16 @@ def test_user_create_enable(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -427,15 +434,16 @@ def test_user_create_disable(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -524,7 +532,7 @@ def test_user_list_no_options(self): **kwargs ) - collist = ('ID', 'Name') + collist = ['ID', 'Name'] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -554,7 +562,7 @@ def test_user_list_domain(self): **kwargs ) - collist = ('ID', 'Name') + collist = ['ID', 'Name'] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -584,7 +592,7 @@ def test_user_list_group(self): **kwargs ) - collist = ('ID', 'Name') + collist = ['ID', 'Name'] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -614,15 +622,15 @@ def test_user_list_long(self): **kwargs ) - collist = ( + collist = [ 'ID', 'Name', - 'Project Id', - 'Domain Id', + 'Project', + 'Domain', 'Description', 'Email', 'Enabled', - ) + ] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -1020,14 +1028,15 @@ def test_user_show(self): self.users_mock.get.assert_called_with(identity_fakes.user_id) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) From 4f7777ca0e1451f77d6935e15f87d27a950b5de4 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 09:59:28 -0600 Subject: [PATCH 0116/3303] Command docs: ec2 credentials Add ec2 credentials docs Change-Id: I1699d1c8e9859153557081966654646966a3268d --- .../command-objects/ec2-credentials.rst | 98 +++++++++++++++++++ doc/source/commands.rst | 1 + openstackclient/identity/v2_0/ec2creds.py | 14 +-- 3 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 doc/source/command-objects/ec2-credentials.rst diff --git a/doc/source/command-objects/ec2-credentials.rst b/doc/source/command-objects/ec2-credentials.rst new file mode 100644 index 0000000000..a5b6754947 --- /dev/null +++ b/doc/source/command-objects/ec2-credentials.rst @@ -0,0 +1,98 @@ +=============== +ec2 credentials +=============== + +Identity v2 + +ec2 credentials create +---------------------- + +Create EC2 credentials + +.. program:: ec2 credentials create +.. code-block:: bash + + os ec2 credentials create + [--project ] + [--user ] + +.. option:: --project + + Specify an alternate project (default: current authenticated project) + +.. option:: --user + + Specify an alternate user (default: current authenticated user) + +The :option:`--project` and :option:`--user` options are typically only +useful for admin users, but may be allowed for other users depending on +the policy of the cloud and the roles granted to the user. + +ec2 credentials delete +---------------------- + +Delete EC2 credentials + +.. program:: ec2 credentials delete +.. code-block:: bash + + os ec2 credentials delete + [--user ] + + +.. option:: --user + + Specify a user + +.. _ec2_credentials_delete-access-key: +.. describe:: access-key + + Credentials access key + +The :option:`--user` option is typically only useful for admin users, but +may be allowed for other users depending on the policy of the cloud and +the roles granted to the user. + +ec2 credentials list +-------------------- + +List EC2 credentials + +.. program:: ec2 credentials list +.. code-block:: bash + + os ec2 credentials list + [--user ] + +.. option:: --user + + Filter list by + +The :option:`--user` option is typically only useful for admin users, but +may be allowed for other users depending on the policy of the cloud and +the roles granted to the user. + +ec2 credentials show +-------------------- + +Display EC2 credentials details + +.. program:: ec2 credentials show +.. code-block:: bash + + os ec2 credentials show + [--user ] + + +.. option:: --user + + Specify a user + +.. _ec2_credentials_show-access-key: +.. describe:: access-key + + Credentials access key + +The :option:`--user` option is typically only useful for admin users, but +may be allowed for other users depending on the policy of the cloud and +the roles granted to the user. diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 0040700e86..7e15053f95 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -80,6 +80,7 @@ referring to both Compute and Volume quotas. * ``container``: Object Store - a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects +* ``ec2 cedentials``: (**Identity**) AWS EC2-compatibile credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index a20ffd4b57..90553eb1f9 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -37,12 +37,14 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help=_('Specify a project [admin only]'), + help=_('Specify an alternate project' + ' (default: current authenticated project)'), ) parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify an alternate user' + ' (default: current authenticated user)'), ) return parser @@ -95,7 +97,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify a user'), ) return parser @@ -125,7 +127,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify a user'), ) return parser @@ -154,7 +156,7 @@ def take_action(self, parsed_args): class ShowEC2Creds(show.ShowOne): - """Show EC2 credentials""" + """Display EC2 credentials details""" log = logging.getLogger(__name__ + '.ShowEC2Creds') @@ -168,7 +170,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify a user'), ) return parser From b56da8dde2ef78f057e67b0b307ee7ce2dff2d7d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 30 Dec 2014 17:46:02 -0600 Subject: [PATCH 0117/3303] Add endpoint v3 docs (update: change version description formats for API versioning) Change-Id: I499ea1d80ad6ad6392468305f761e695d7261e33 --- doc/source/command-objects/endpoint.rst | 161 +++++++++++++++++++++- openstackclient/identity/v2_0/endpoint.py | 47 ++++--- openstackclient/identity/v3/endpoint.py | 71 ++++++---- 3 files changed, 225 insertions(+), 54 deletions(-) diff --git a/doc/source/command-objects/endpoint.rst b/doc/source/command-objects/endpoint.rst index 128ddfa021..7846a1b1f5 100644 --- a/doc/source/command-objects/endpoint.rst +++ b/doc/source/command-objects/endpoint.rst @@ -7,39 +7,190 @@ Identity v2, v3 endpoint create --------------- +Create new endpoint + +*Identity version 2 only* + +.. program:: endpoint create +.. code:: bash + + os endpoint create + --publicurl + [--adminurl ] + [--internalurl ] + [--region ] + + +.. option:: --publicurl + + New endpoint public URL (required) + +.. option:: --adminurl + + New endpoint admin URL + +.. option:: --internalurl + + New endpoint internal URL + +.. option:: --region + + New endpoint region ID + +.. _endpoint_create-endpoint: +.. describe:: + + New endpoint service (name or ID) + +*Identity version 3 only* + .. program:: endpoint create .. code:: bash os endpoint create - --publicurl - [--adminurl ] - [--internalurl ] - [--region ] + [--region + [--enable | --disable] + + + +.. option:: --region + + New endpoint region ID + +.. option:: --enable + + Enable endpoint (default) + +.. option:: --disable + + Disable endpoint + +.. describe:: + + New endpoint service (name or ID) + +.. describe:: + + New endpoint interface type (admin, public or internal) + +.. describe:: + + New endpoint URL endpoint delete --------------- +Delete endpoint + .. program:: endpoint delete .. code:: bash os endpoint delete +.. _endpoint_delete-endpoint: +.. describe:: + + Endpoint ID to delete + endpoint list ------------- +List endpoints + .. program:: endpoint list .. code:: bash os endpoint list + [--service ] + [--region ] [--long] +.. option:: --service + + Filter by service + + *Identity version 3 only* + +.. option:: --interface + + Filter by interface type (admin, public or internal) + + *Identity version 3 only* + +.. option:: --region + + Filter by region ID + + *Identity version 3 only* + +.. option:: --long + + List additional fields in output + + *Identity version 2 only* + +endpoint set +------------ + +Set endpoint properties + +*Identity version 3 only* + +.. program:: endpoint set +.. code:: bash + + os endpoint set + [--region ] + [--interface ] + [--url ] + [--service ] + [--enable | --disable] + + +.. option:: --region + + New endpoint region ID + +.. option:: --interface + + New endpoint interface type (admin, public or internal) + +.. option:: --url + + New endpoint URL + +.. option:: --service + + New endpoint service (name or ID) + +.. option:: --enable + + Enable endpoint + +.. option:: --disable + + Disable endpoint + +.. _endpoint_set-endpoint: +.. describe:: + + Endpoint ID to modify + endpoint show ------------- +Display endpoint details + .. program:: endpoint show .. code:: bash os endpoint show - + + +.. _endpoint_show-endpoint: +.. describe:: + + Endpoint ID to display diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index c5189b62e7..370a931d40 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -28,7 +28,7 @@ class CreateEndpoint(show.ShowOne): - """Create endpoint""" + """Create new endpoint""" log = logging.getLogger(__name__ + '.CreateEndpoint') @@ -36,25 +36,30 @@ def get_parser(self, prog_name): parser = super(CreateEndpoint, self).get_parser(prog_name) parser.add_argument( 'service', - metavar='', - help=_('New endpoint service')) - parser.add_argument( - '--region', - metavar='', - help=_('New endpoint region')) + metavar='', + help=_('New endpoint service (name or ID)'), + ) parser.add_argument( '--publicurl', - metavar='', + metavar='', required=True, - help=_('New endpoint public URL')) + help=_('New endpoint public URL (required)'), + ) parser.add_argument( '--adminurl', - metavar='', - help=_('New endpoint admin URL')) + metavar='', + help=_('New endpoint admin URL'), + ) parser.add_argument( '--internalurl', - metavar='', - help=_('New endpoint internal URL')) + metavar='', + help=_('New endpoint internal URL'), + ) + parser.add_argument( + '--region', + metavar='', + help=_('New endpoint region ID'), + ) return parser def take_action(self, parsed_args): @@ -76,7 +81,7 @@ def take_action(self, parsed_args): class DeleteEndpoint(command.Command): - """Delete endpoint command""" + """Delete endpoint""" log = logging.getLogger(__name__ + '.DeleteEndpoint') @@ -85,7 +90,7 @@ def get_parser(self, prog_name): parser.add_argument( 'endpoint', metavar='', - help=_('ID of endpoint to delete')) + help=_('Endpoint ID to delete')) return parser def take_action(self, parsed_args): @@ -96,7 +101,7 @@ def take_action(self, parsed_args): class ListEndpoint(lister.Lister): - """List endpoint command""" + """List endpoints""" log = logging.getLogger(__name__ + '.ListEndpoint') @@ -106,7 +111,8 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help=_('List additional fields in output')) + help=_('List additional fields in output'), + ) return parser def take_action(self, parsed_args): @@ -131,7 +137,7 @@ def take_action(self, parsed_args): class ShowEndpoint(show.ShowOne): - """Show endpoint command""" + """Display endpoint details""" log = logging.getLogger(__name__ + '.ShowEndpoint') @@ -139,8 +145,9 @@ def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint_or_service', - metavar='', - help=_('Endpoint ID or name, type or ID of service to display')) + metavar='', + help=_('Endpoint ID to display'), + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 0c077c5a34..5b8104e514 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -28,7 +28,7 @@ class CreateEndpoint(show.ShowOne): - """Create endpoint command""" + """Create new endpoint""" log = logging.getLogger(__name__ + '.CreateEndpoint') @@ -37,27 +37,31 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Name or ID of new endpoint service') + help='New endpoint service (name or ID)', + ) parser.add_argument( 'interface', metavar='', choices=['admin', 'public', 'internal'], - help='New endpoint interface, must be admin, public or internal') + help='New endpoint interface type (admin, public or internal)', + ) parser.add_argument( 'url', metavar='', - help='New endpoint URL') + help='New endpoint URL', + ) parser.add_argument( '--region', - metavar='', - help='New endpoint region') + metavar='', + help='New endpoint region ID', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', dest='enabled', action='store_true', default=True, - help='Enable endpoint', + help='Enable endpoint (default)', ) enable_group.add_argument( '--disable', @@ -89,7 +93,7 @@ def take_action(self, parsed_args): class DeleteEndpoint(command.Command): - """Delete endpoint command""" + """Delete endpoint""" log = logging.getLogger(__name__ + '.DeleteEndpoint') @@ -97,8 +101,9 @@ def get_parser(self, prog_name): parser = super(DeleteEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint', - metavar='', - help='ID of endpoint to delete') + metavar='', + help='Endpoint ID to delete', + ) return parser def take_action(self, parsed_args): @@ -111,7 +116,7 @@ def take_action(self, parsed_args): class ListEndpoint(lister.Lister): - """List endpoint command""" + """List endpoints""" log = logging.getLogger(__name__ + '.ListEndpoint') @@ -120,17 +125,19 @@ def get_parser(self, prog_name): parser.add_argument( '--service', metavar='', - help='Filter by a specific service') + help='Filter by service', + ) parser.add_argument( '--interface', metavar='', choices=['admin', 'public', 'internal'], - help='Filter by a specific interface, must be admin, public or' - ' internal') + help='Filter by interface type (admin, public or internal)', + ) parser.add_argument( '--region', - metavar='', - help='Filter by a specific region') + metavar='', + help='Filter by region ID', + ) return parser def take_action(self, parsed_args): @@ -160,7 +167,7 @@ def take_action(self, parsed_args): class SetEndpoint(command.Command): - """Set endpoint command""" + """Set endpoint properties""" log = logging.getLogger(__name__ + '.SetEndpoint') @@ -168,25 +175,30 @@ def get_parser(self, prog_name): parser = super(SetEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint', - metavar='', - help='ID of endpoint to update') + metavar='', + help='Endpoint ID to modify', + ) + parser.add_argument( + '--region', + metavar='', + help='New endpoint region ID', + ) parser.add_argument( '--interface', metavar='', choices=['admin', 'public', 'internal'], - help='New endpoint interface, must be admin|public|internal') + help='New endpoint interface type (admin, public or internal)', + ) parser.add_argument( '--url', metavar='', - help='New endpoint URL') + help='New endpoint URL', + ) parser.add_argument( '--service', metavar='', - help='Name or ID of new endpoint service') - parser.add_argument( - '--region', - metavar='', - help='New endpoint region') + help='New endpoint service (name or ID)', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -238,7 +250,7 @@ def take_action(self, parsed_args): class ShowEndpoint(show.ShowOne): - """Show endpoint command""" + """Display endpoint details""" log = logging.getLogger(__name__ + '.ShowEndpoint') @@ -246,8 +258,9 @@ def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint', - metavar='', - help='ID of endpoint to display') + metavar='', + help='Endpoint ID to display', + ) return parser def take_action(self, parsed_args): From 3807354cfecd887094fe55ba7944161d75e38d21 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 09:56:20 -0600 Subject: [PATCH 0118/3303] Command docs: group Fix up formatting for group command docs and help Change-Id: Icda79842d52da90d5eac2b0fdbc0d576d371378d --- doc/source/command-objects/group.rst | 46 ++++++++++++++-------------- doc/source/commands.rst | 2 +- openstackclient/identity/v3/group.py | 39 ++++++++++++----------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/doc/source/command-objects/group.rst b/doc/source/command-objects/group.rst index 85a0c5cd79..3e7e806f17 100644 --- a/doc/source/command-objects/group.rst +++ b/doc/source/command-objects/group.rst @@ -16,18 +16,18 @@ Add user to group -.. option:: +.. describe:: - Group that user will be added to (name or ID) + Group to contain (name or ID) -.. option:: +.. describe:: - User to add to group (name or ID) + User to add to (name or ID) group contains user ------------------- -Check user in group +Check user membership in group .. program:: group contains user .. code:: bash @@ -36,11 +36,11 @@ Check user in group -.. option:: +.. describe:: - Group to check if user belongs to (name or ID) + Group to check (name or ID) -.. option:: +.. describe:: User to check (name or ID) @@ -60,7 +60,7 @@ Create new group .. option:: --domain - References the domain ID or name which owns the group + Domain to contain new group (name or ID) .. option:: --description @@ -72,7 +72,7 @@ Create new group If the group already exists, return the existing group data and do not fail. -.. option:: +.. describe:: New group name @@ -90,9 +90,9 @@ Delete group .. option:: --domain - Domain where group resides (name or ID) + Domain containing group(s) (name or ID) -.. option:: +.. describe:: Group(s) to delete (name or ID) @@ -115,11 +115,11 @@ List groups .. option:: --user - List group memberships for (name or ID) + Filter group list by (name or ID) .. option:: --long - List additional fields in output (defaults to false) + List additional fields in output group remove user ----------------- @@ -133,13 +133,13 @@ Remove user from group -.. option:: +.. describe:: - Group that user will be removed from (name or ID) + Group containing (name or ID) -.. option:: +.. describe:: - User to remove from group (name or ID) + User to remove from (name or ID) group set --------- @@ -161,20 +161,20 @@ Set group properties .. option:: --domain - New domain that will now own the group (name or ID) + New domain to contain (name or ID) .. option:: --description New group description -.. option:: +.. describe:: Group to modify (name or ID) group show ---------- -Show group details +Display group details .. program:: group show .. code:: bash @@ -185,8 +185,8 @@ Show group details .. option:: --domain - Domain where group resides (name or ID) + Domain containing (name or ID) -.. option:: +.. describe:: Group to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index eba0a22f71..d137a2707c 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -83,7 +83,7 @@ referring to both Compute and Volume quotas. * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc -* ``group``: Identity - a grouping of users +* ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager * ``identity provider``: Identity - a source of users and authentication diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index fbd8dd720b..94e101f31a 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -39,12 +39,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group that user will be added to (name or ID)', + help='Group to contain (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User to add to group (name or ID)', + help='User to add to (name or ID)', ) return parser @@ -68,7 +68,7 @@ def take_action(self, parsed_args): class CheckUserInGroup(command.Command): - """Check user in group""" + """Check user membership in group""" log = logging.getLogger(__name__ + '.CheckUserInGroup') @@ -77,7 +77,7 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group to check if user belongs to (name or ID)', + help='Group to check (name or ID)', ) parser.add_argument( 'user', @@ -115,15 +115,18 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New group name') - parser.add_argument( - '--description', - metavar='', - help='New group description') + help='New group name', + ) parser.add_argument( '--domain', metavar='', - help='References the domain ID or name which owns the group') + help='Domain to contain new group (name or ID)', + ) + parser.add_argument( + '--description', + metavar='', + help='New group description', + ) parser.add_argument( '--or-show', action='store_true', @@ -173,7 +176,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where group resides (name or ID)', + help='Domain containing group(s) (name or ID)', ) return parser @@ -211,7 +214,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help='List group memberships for (name or ID)', + help='Filter group list by (name or ID)', ) parser.add_argument( '--long', @@ -259,7 +262,7 @@ def take_action(self, parsed_args): class RemoveUserFromGroup(command.Command): - """Remove user to group""" + """Remove user from group""" log = logging.getLogger(__name__ + '.RemoveUserFromGroup') @@ -268,12 +271,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group that user will be removed from (name or ID)', + help='Group containing (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User to remove from group (name or ID)', + help='User to remove from (name or ID)', ) return parser @@ -314,7 +317,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='New domain that will now own the group (name or ID)') + help='New domain to contain (name or ID)') parser.add_argument( '--description', metavar='', @@ -341,7 +344,7 @@ def take_action(self, parsed_args): class ShowGroup(show.ShowOne): - """Show group details""" + """Display group details""" log = logging.getLogger(__name__ + '.ShowGroup') @@ -355,7 +358,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where group resides (name or ID)', + help='Domain containing (name or ID)', ) return parser From 369ae3f9f06c90e8d57b71befa6bc22a843c2f7d Mon Sep 17 00:00:00 2001 From: zhiyuan_cai Date: Sun, 4 Jan 2015 11:26:18 +0800 Subject: [PATCH 0119/3303] Check if service.name available before access Currently v3 endpoint commands access service.name directly, while name is not a required attribute of service. So if we associate an endpoint to a service without name, we will get an AttributeError executing v3 endpoint commands later. This patch addresses this issue by checking if service.name is available before accessing it. Change-Id: I3dd686ef02a2e21e2049a49cb55634385c2ecfaf Closes-Bug: #1406737 --- openstackclient/identity/v3/endpoint.py | 13 ++- openstackclient/tests/identity/v3/fakes.py | 8 ++ .../tests/identity/v3/test_endpoint.py | 102 ++++++++++++++++-- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 0c077c5a34..eba7ca1471 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -27,6 +27,13 @@ from openstackclient.identity import common +def get_service_name(service): + if hasattr(service, 'name'): + return service.name + else: + return '' + + class CreateEndpoint(show.ShowOne): """Create endpoint command""" @@ -83,7 +90,7 @@ def take_action(self, parsed_args): info = {} endpoint._info.pop('links') info.update(endpoint._info) - info['service_name'] = service.name + info['service_name'] = get_service_name(service) info['service_type'] = service.type return zip(*sorted(six.iteritems(info))) @@ -150,7 +157,7 @@ def take_action(self, parsed_args): for ep in data: service = common.find_service(identity_client, ep.service_id) - ep.service_name = service.name + ep.service_name = get_service_name(service) ep.service_type = service.type return (columns, (utils.get_item_properties( @@ -261,6 +268,6 @@ def take_action(self, parsed_args): info = {} endpoint._info.pop('links') info.update(endpoint._info) - info['service_name'] = service.name + info['service_name'] = get_service_name(service) info['service_type'] = service.type return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 3afb0cd90d..68e67519dc 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -158,6 +158,14 @@ 'links': base_url + 'services/' + service_id, } +SERVICE_WITHOUT_NAME = { + 'id': service_id, + 'type': service_type, + 'description': service_description, + 'enabled': True, + 'links': base_url + 'services/' + service_id, +} + endpoint_id = 'e-123' endpoint_url = 'http://127.0.0.1:35357' endpoint_region = 'RegionOne' diff --git a/openstackclient/tests/identity/v3/test_endpoint.py b/openstackclient/tests/identity/v3/test_endpoint.py index ea05326e20..ecfa71ab3c 100644 --- a/openstackclient/tests/identity/v3/test_endpoint.py +++ b/openstackclient/tests/identity/v3/test_endpoint.py @@ -31,6 +31,9 @@ def setUp(self): self.services_mock = self.app.client_manager.identity.services self.services_mock.reset_mock() + def get_fake_service_name(self): + return identity_fakes.service_name + class TestEndpointCreate(TestEndpoint): @@ -92,7 +95,7 @@ def test_endpoint_create_no_options(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -139,7 +142,7 @@ def test_endpoint_create_region(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -185,7 +188,7 @@ def test_endpoint_create_enable(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -231,7 +234,7 @@ def test_endpoint_create_disable(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -309,7 +312,7 @@ def test_endpoint_list_no_options(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -319,10 +322,10 @@ def test_endpoint_list_no_options(self): def test_endpoint_list_service(self): arglist = [ - '--service', identity_fakes.service_name, + '--service', identity_fakes.service_id, ] verifylist = [ - ('service', identity_fakes.service_name), + ('service', identity_fakes.service_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -341,7 +344,7 @@ def test_endpoint_list_service(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -373,7 +376,7 @@ def test_endpoint_list_interface(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -405,7 +408,7 @@ def test_endpoint_list_region(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -664,8 +667,85 @@ def test_endpoint_show(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) self.assertEqual(datalist, data) + + +class TestEndpointCreateServiceWithoutName(TestEndpointCreate): + + def setUp(self): + super(TestEndpointCreate, self).setUp() + + self.endpoints_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE_WITHOUT_NAME), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.CreateEndpoint(self.app, None) + + def get_fake_service_name(self): + return '' + + +class TestEndpointListServiceWithoutName(TestEndpointList): + + def setUp(self): + super(TestEndpointList, self).setUp() + + self.endpoints_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ), + ] + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE_WITHOUT_NAME), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ListEndpoint(self.app, None) + + def get_fake_service_name(self): + return '' + + +class TestEndpointShowServiceWithoutName(TestEndpointShow): + + def setUp(self): + super(TestEndpointShow, self).setUp() + + self.endpoints_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE_WITHOUT_NAME), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ShowEndpoint(self.app, None) + + def get_fake_service_name(self): + return '' From 34975edd1456b65986f24f8b17390e3ef99b4ac4 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:36:35 -0500 Subject: [PATCH 0120/3303] tweak the server command docs the formatting used for the server commands is not the same as the other command docs, this patch addresses that issue. Change-Id: I5f31cf6a317d9eb35ec46185800fade3dd956dc4 --- doc/source/command-objects/server.rst | 246 +++++++++++++++++--------- 1 file changed, 164 insertions(+), 82 deletions(-) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 2f5aef1070..48cefe6ada 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -14,10 +14,12 @@ Add security group to server -:option:`` +.. describe:: + Server (name or ID) -:option:`` +.. describe:: + Security group to add (name or ID) server add volume @@ -32,13 +34,16 @@ Add volume to server -:option:`--device` +.. option:: --device + Server internal device name for volume -:option:`` +.. describe:: + Server (name or ID) -:option:`` +.. describe:: + Volume to add (name or ID) server create @@ -66,55 +71,72 @@ Create a new server [--wait] -:option:`--image` +.. option:: --image + Create server from this image -:option:`--volume` +.. option:: --volume + Create server from this volume -:option:`--flavor` +.. option:: --flavor + Create server with this flavor -:option:`--security-group` +.. option:: --security-group + Security group to assign to this server (repeat for multiple groups) -:option:`--key-name` +.. option:: --key-name + Keypair to inject into this server (optional extension) -:option:`--property` +.. option:: --property + Set a property on this server (repeat for multiple values) -:option:`--file` +.. option:: --file + File to inject into image before boot (repeat for multiple files) -:option:`--user-data` +.. option:: --user-data + User data file to serve from the metadata server -:option:`--availability-zone` +.. option:: --availability-zone + Select an availability zone for the server -:option:`--block-device-mapping` +.. option:: --block-device-mapping + Map block devices; map is ::: (optional extension) -:option:`--nic` +.. option:: --nic + Specify NIC configuration (optional extension) -:option:`--hint` +.. option:: --hint + Hints for the scheduler (optional extension) -:option:`--config-drive` |True +.. option:: --config-drive |True + Use specified volume as the config drive, or 'True' to use an ephemeral drive -:option:`--min` +.. option:: --min + Minimum number of servers to launch (default=1) -:option:`--max` +.. option:: --max + Maximum number of servers to launch (default=1) -:option:`--wait` +.. option:: --wait + Wait for build to complete -:option:`` +.. describe:: + New server name server delete @@ -127,7 +149,8 @@ Delete server(s) os server delete [ ...] -:option:`` +.. describe:: + Server to delete (name or ID) server list @@ -150,37 +173,48 @@ List servers [--all-projects] [--long] -:option:`--reservation-id` +.. option:: --reservation-id + Only return instances that match the reservation -:option:`--ip` +.. option:: --ip + Regular expression to match IP addresses -:option:`--ip6` +.. option:: --ip6 + Regular expression to match IPv6 addresses -:option:`--name` +.. option:: --name + Regular expression to match names -:option:`--instance-name` +.. option:: --instance-name + Regular expression to match instance name (admin only) -:option:`--status` +.. option:: --status + Search by server status -:option:`--flavor` +.. option:: --flavor + Search by flavor ID -:option:`--image` +.. option:: --image + Search by image ID -:option:`--host` +.. option:: --host + Search by hostname -:option:`--all-projects` +.. option:: --all-projects + Include all projects (admin only) -:option:`--long` +.. option:: --long + List additional fields in output server lock @@ -193,7 +227,8 @@ Lock server os server lock -:option:`` +.. describe:: + Server (name or ID) server migrate @@ -210,25 +245,32 @@ Migrate server to different host [--wait] -:option:`--live` +.. option:: --live + Target hostname -:option:`--shared-migration` +.. option:: --shared-migration + Perform a shared live migration (default) -:option:`--block-migration` +.. option:: --block-migration + Perform a block live migration -:option:`--disk-overcommit` +.. option:: --disk-overcommit + Allow disk over-commit on the destination host -:option:`--no-disk-overcommit` +.. option:: --no-disk-overcommit + Do not over-commit disk on the destination host (default) -:option:`--wait` +.. option:: --wait + Wait for resize to complete -:option:`` +.. describe:: + Server to migrate (name or ID) server pause @@ -241,7 +283,8 @@ Pause server os server pause -:option:`` +.. describe:: + Server (name or ID) server reboot @@ -256,16 +299,20 @@ Perform a hard or soft server reboot [--wait] -:option:`--hard` +.. option:: --hard + Perform a hard reboot -:option:`--soft` +.. option:: --soft + Perform a soft reboot -:option:`--wait` +.. option:: --wait + Wait for reboot to complete -:option:`` +.. describe:: + Server (name or ID) server rebuild @@ -281,16 +328,20 @@ Rebuild server [--wait] -:option:`--image` +.. option:: --image + Recreate server from this image -:option:`--password` +.. option:: --password + Set the password on the rebuilt instance -:option:`--wait` +.. option:: --wait + Wait for rebuild to complete -:option:`` +.. describe:: + Server (name or ID) server remove security group @@ -304,10 +355,12 @@ Remove security group from server -:option:`` +.. describe:: + Name or ID of server to use -:option:`` +.. describe:: + Name or ID of security group to remove from server server remove volume @@ -321,10 +374,12 @@ Remove volume from server -:option:`` +.. describe:: + Server (name or ID) -:option:`` +.. describe:: + Volume to remove (name or ID) server rescue @@ -337,7 +392,8 @@ Put server in rescue mode os server rescue -:option:`` +.. describe:: + Server (name or ID) server resize @@ -356,19 +412,24 @@ Scale server to a new flavor --verify | --revert -:option:`--flavor` +.. option:: --flavor + Resize server to specified flavor -:option:`--verify` +.. option:: --verify + Verify server resize is complete -:option:`--revert` +.. option:: --revert + Restore server state before resize -:option:`--wait` +.. option:: --wait + Wait for resize to complete -:option:`` +.. describe:: + Server (name or ID) A resize operation is implemented by creating a new server and copying @@ -387,7 +448,8 @@ Resume server os server resume -:option:`` +.. describe:: + Server (name or ID) server set @@ -404,17 +466,21 @@ Set server properties --root-password -:option:`--name` +.. option:: --name + New server name -:option:`--root-password` +.. option:: --root-password + Set new root password (interactive only) -:option:`--property` +.. option:: --property + Property to add/change for this server (repeat option to set multiple properties) -:option:`` +.. describe:: + Server (name or ID) server show @@ -428,10 +494,12 @@ Show server details [--diagnostics] -:option:`--diagnostics` +.. option:: --diagnostics + Display server diagnostics information -:option:`` +.. describe:: + Server (name or ID) server ssh @@ -449,28 +517,36 @@ Ssh to server [--public | --private | --address-type ] -:option:`--login` +.. option:: --login + Login name (ssh -l option) -:option:`--port` +.. option:: --port + Destination port (ssh -p option) -:option:`--identity` +.. option:: --identity + Private key file (ssh -i option) -:option:`--option` +.. option:: --option + Options in ssh_config(5) format (ssh -o option) -:option:`--public` +.. option:: --public + Use public IP address -:option:`--private` +.. option:: --private + Use private IP address -:option:`--address-type` +.. option:: --address-type + Use other IP address (public, private, etc) -:option:`` +.. describe:: + Server (name or ID) server suspend @@ -483,7 +559,8 @@ Suspend server os server suspend -:option:`` +.. describe:: + Server (name or ID) server unlock @@ -496,7 +573,8 @@ Unlock server os server unlock -:option:`` +.. describe:: + Server (name or ID) server unpause @@ -509,7 +587,8 @@ Unpause server os server unpause -:option:`` +.. describe:: + Server (name or ID) server unrescue @@ -522,7 +601,8 @@ Restore server from rescue mode os server unrescue -:option:`` +.. describe:: + Server (name or ID) server unset @@ -537,8 +617,10 @@ Unset server properties [--property ] ... -:option:`--property` +.. option:: --property + Property key to remove from server (repeat to set multiple values) -:option:`` +.. describe:: + Server (name or ID) From ca92608974a8fe9a54951d0ea6b24ab59a5b7a06 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:27:47 -0500 Subject: [PATCH 0121/3303] Command doc: volume type Change-Id: I7e36daa027639d6a782043d4181c1b328335975a --- doc/source/command-objects/volume-type.rst | 95 ++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/type.py | 14 ++-- 3 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 doc/source/command-objects/volume-type.rst diff --git a/doc/source/command-objects/volume-type.rst b/doc/source/command-objects/volume-type.rst new file mode 100644 index 0000000000..0898df528b --- /dev/null +++ b/doc/source/command-objects/volume-type.rst @@ -0,0 +1,95 @@ +=========== +volume type +=========== + +Volume v1 + +volume type create +------------------ + +Create new volume type + +.. program:: volume type create +.. code:: bash + + os volume type create + [--property [...] ] + + +.. option:: --property + + Set a property on this volume type (repeat option to set multiple properties) + +.. describe:: + + New volume type name + +volume type delete +------------------ + +Delete volume type + +.. program:: volume type delete +.. code:: bash + + os volume type delete + + +.. describe:: + + Volume type to delete (name or ID) + +volume type list +---------------- + +List volume types + +.. program:: volume type list +.. code:: bash + + os volume type list + [--long] + +.. option:: --long + + List additional fields in output + +volume type set +--------------- + +Set volume type properties + +.. program:: volume type set +.. code:: bash + + os volume type set + [--property [...] ] + + +.. option:: --property + + Property to add or modify for this volume type (repeat option to set multiple properties) + +.. describe:: + + Volume type to modify (name or ID) + +volume type unset +----------------- + +Unset volume type properties + +.. program:: volume type unset +.. code:: bash + + os volume type unset + [--property ] + + +.. option:: --property + + Property to remove from volume type (repeat option to remove multiple properties) + +.. describe:: + + Volume type to modify (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4b2e6355e4..5bec1a8518 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -113,7 +113,7 @@ referring to both Compute and Volume quotas. * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user * ``volume``: Volume - block volumes -* ``volume type``: Volume - deployment-specific types of volumes available +* ``volume type``: (**Volume**) deployment-specific types of volumes available Actions ------- diff --git a/openstackclient/volume/v1/type.py b/openstackclient/volume/v1/type.py index 71bfc9eaba..46d1828b1e 100644 --- a/openstackclient/volume/v1/type.py +++ b/openstackclient/volume/v1/type.py @@ -71,7 +71,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume_type', metavar='', - help='Name or ID of volume type to delete', + help='Volume type to delete (name or ID)', ) return parser @@ -115,7 +115,7 @@ def take_action(self, parsed_args): class SetVolumeType(command.Command): - """Set volume type property""" + """Set volume type properties""" log = logging.getLogger(__name__ + '.SetVolumeType') @@ -124,13 +124,13 @@ def get_parser(self, prog_name): parser.add_argument( 'volume_type', metavar='', - help='Volume type name or ID to update', + help='Volume type to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add/change for this volume type ' + help='Property to add or modify for this volume type ' '(repeat option to set multiple properties)', ) return parser @@ -148,7 +148,7 @@ def take_action(self, parsed_args): class UnsetVolumeType(command.Command): - """Unset volume type property""" + """Unset volume type properties""" log = logging.getLogger(__name__ + '.UnsetVolumeType') @@ -157,14 +157,14 @@ def get_parser(self, prog_name): parser.add_argument( 'volume_type', metavar='', - help='Type ID or name to remove', + help='Volume type to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from volume ' + help='Property to remove from volume type ' '(repeat option to remove multiple properties)', ) return parser From 6b196d1a17531b56d0a19b95a81a21c6a2ccc625 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:44:33 -0500 Subject: [PATCH 0122/3303] Update the command list We've been making changes to the commands.rst file as we add command docs, looks like we missed a few. Also fixed a few typos Change-Id: Ie93280a7e5ba37303a1984a68870b5a4fc5c6e06 --- doc/source/commands.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4b2e6355e4..bb3db18755 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -77,13 +77,13 @@ referring to both Compute and Volume quotas. * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL * ``consumer``: Identity - OAuth-based delegatee -* ``container``: Object Store - a grouping of objects +* ``container``: (**Object Store**) a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects -* ``ec2 cedentials``: (**Identity**) AWS EC2-compatibile credentials +* ``ec2 cedentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions -* ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc +* ``flavor``: (**Compute**) predefined server configurations: ram, root disk, etc * ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager @@ -95,13 +95,14 @@ referring to both Compute and Volume quotas. * ``limits``: (**Compute**, **Volume**) resource usage limits * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources -* ``object``: Object Store - a single file in the Object Store +* ``object``: (**Object Store**) a single file in the Object Store * ``policy``: Identity - determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``region``: (**Identity**) a subset of an OpenStack deployment * ``request token``: Identity - temporary OAuth-based token -* ``role``: Identity - a policy object used to determine authorization +* ``role``: (**Identity**) a policy object used to determine authorization +* ``role assignment``: (**Identity**) a relationship between roles, users or groups, and domains or projects * ``security group``: Compute, Network - groups of network access rules * ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access * ``server``: (**Compute**) virtual machine instance From 265ca582f02c5cf107c677051c65d43534aaddbb Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:53:46 -0500 Subject: [PATCH 0123/3303] Command docs: volume Change-Id: Id1e500d5fb19ffdeb0d1bde9e22c3143c0873d0c --- doc/source/command-objects/volume.rst | 189 ++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/volume.py | 45 +++--- 3 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 doc/source/command-objects/volume.rst diff --git a/doc/source/command-objects/volume.rst b/doc/source/command-objects/volume.rst new file mode 100644 index 0000000000..2eec2d7b25 --- /dev/null +++ b/doc/source/command-objects/volume.rst @@ -0,0 +1,189 @@ +====== +volume +====== + +Volume v1 + +volume create +------------- + +Create new volume + +.. program:: volume create +.. code:: bash + + os volume create + --size + [--snapshot-id ] + [--description ] + [--type ] + [--user ] + [--project ] + [--availability-zone ] + [--image ] + [--source ] + [--property [...] ] + + +.. option:: --size (required) + + New volume size in GB + +.. option:: --snapshot-id + + Use as source of new volume + +.. option:: --description + + New volume description + +.. option:: --type + + Use as the new volume type + +.. option:: --user + + Specify an alternate user (name or ID) + +.. option:: --project + + Specify an alternate project (name or ID) + +.. option:: --availability-zone + + Create new volume in + +.. option:: --image + + Use as source of new volume (name or ID) + +.. option:: --source + + Volume to clone (name or ID) + +.. option:: --property + + Set a property on this volume (repeat option to set multiple properties) + +.. describe:: + + New volume name + +The :option:`--project` and :option:`--user` options are typically only +useful for admin users, but may be allowed for other users depending on +the policy of the cloud and the roles granted to the user. + +volume delete +------------- + +Delete volume(s) + +.. program:: volume delete +.. code:: bash + + os volume delete + [--force] + [ ...] + +.. option:: --force + + Attempt forced removal of volume(s), regardless of state (defaults to False) + +.. describe:: + + Volume(s) to delete (name or ID) + +volume list +----------- + +List volumes + +.. program:: volume list +.. code:: bash + + os volume list + [--status ] + [--name ] + [--all-projects] + [--long] + +.. option:: --status + + Filter results by status + +.. option:: --name + + Filter results by name + +.. option:: --all-projects + + Include all projects (admin only) + +.. option:: --long + + List additional fields in output + +volume set +---------- + +Set volume properties + +.. program:: volume set +.. code:: bash + + os volume set + [--name ] + [--description ] + [--property [...] ] + + +.. option:: --name + + New volume name + +.. option:: --description + + New volume description + +.. option:: --property + + Property to add or modify for this volume (repeat option to set multiple properties) + +.. describe:: + + Volume to modify (name or ID) + +volume show +----------- + +Show volume details + +.. program:: volume show +.. code:: bash + + os volume show + + +.. describe:: + + Volume to display (name or ID) + +volume unset +------------ + +Unset volume properties + +.. program:: volume unset +.. code:: bash + + os volume unset + [--property ] + + +.. option:: --property + + Property to remove from volume (repeat option to remove multiple properties) + +.. describe:: + + Volume to modify (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 5bec1a8518..f5f6d01513 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -112,7 +112,7 @@ referring to both Compute and Volume quotas. * ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user -* ``volume``: Volume - block volumes +* ``volume``: (**Volume**) block volumes * ``volume type``: (**Volume**) deployment-specific types of volumes available Actions diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index d82f6b4b76..e59331fad3 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -36,14 +36,14 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='Name of the new volume', + help='New volume name', ) parser.add_argument( '--size', metavar='', required=True, type=int, - help='New volume size', + help='New volume size in GB', ) parser.add_argument( '--snapshot-id', @@ -53,45 +53,45 @@ def get_parser(self, prog_name): parser.add_argument( '--description', metavar='', - help='Description of the volume', + help='New volume description', ) parser.add_argument( '--type', metavar='', - help='Type of volume', + help='Use as the new volume type', ) parser.add_argument( '--user', metavar='', - help='Specify a different user (admin only)', + help='Specify an alternate user (name or ID)', ) parser.add_argument( '--project', metavar='', - help='Specify a different project (admin only)', + help='Specify an alternate project (name or ID)', ) parser.add_argument( '--availability-zone', metavar='', help='Create new volume in ', ) - parser.add_argument( - '--property', - metavar='', - action=parseractions.KeyValueAction, - help='Property to store for this volume ' - '(repeat option to set multiple properties)', - ) parser.add_argument( '--image', metavar='', - help='Use as source of new volume', + help='Use as source of new volume (name or ID)', ) parser.add_argument( '--source', metavar='', help='Volume to clone (name or ID)', ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Set a property on this volume ' + '(repeat option to set multiple properties)', + ) return parser @@ -172,7 +172,8 @@ def get_parser(self, prog_name): dest='force', action='store_true', default=False, - help='Attempt forced removal of volume(s), regardless of state', + help='Attempt forced removal of volume(s), regardless of state ' + '(defaults to False)', ) return parser @@ -216,7 +217,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Display properties', + help='List additional fields in output', ) return parser @@ -318,19 +319,19 @@ def get_parser(self, prog_name): ) parser.add_argument( '--name', - metavar='', + metavar='', help='New volume name', ) parser.add_argument( '--description', - metavar='', + metavar='', help='New volume description', ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add/change for this volume ' + help='Property to add or modify for this volume ' '(repeat option to set multiple properties)', ) return parser @@ -400,15 +401,15 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='Volume to change (name or ID)', + help='Volume to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from volume ' - '(repeat to set multiple values)', + help='Property to remove from volume ' + '(repeat option to remove multiple properties)', ) return parser From 55b85403744840e8327a6184ab3464d602e93e3f Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 20:02:53 -0500 Subject: [PATCH 0124/3303] Fixup backup list output Name and Description were not appearing at all, and we didn't have a --long alternative, which had a bunch of useful information. Closes-Bug: #1408585 Change-Id: I7ca42a8d23ad60f6b9cc862799cb08a3e491b6e8 --- openstackclient/volume/v1/backup.py | 54 ++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 8c16ba256f..eab388a59f 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -15,6 +15,7 @@ """Volume v1 Backup action implementations""" +import copy import logging import six @@ -102,20 +103,55 @@ class ListBackup(lister.Lister): log = logging.getLogger(__name__ + '.ListBackup') + def get_parser(self, prog_name): + parser = super(ListBackup, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - columns = ( - 'ID', - 'Display Name', - 'Display Description', - 'Status', - 'Size' - ) + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].display_name + return volume + + if parsed_args.long: + columns = ['ID', 'Name', 'Description', 'Status', 'Size', + 'Availability Zone', 'Volume ID', 'Container'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + else: + columns = ['ID', 'Name', 'Description', 'Status', 'Size'] + column_headers = columns + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + data = self.app.client_manager.volume.backups.list() - return (columns, + + return (column_headers, (utils.get_item_properties( s, columns, - formatters={}, + formatters={'Volume ID': _format_volume_id}, ) for s in data)) From 79d0e21a4519de57a25ccd4a06a26795dba5636d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 20:26:35 -0500 Subject: [PATCH 0125/3303] Command doc: backup Change-Id: Iecd4dbddea637bd6540d94b37253a9ba434c9db3 --- doc/source/command-objects/backup.rst | 104 ++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/backup.py | 22 +++--- 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/backup.rst diff --git a/doc/source/command-objects/backup.rst b/doc/source/command-objects/backup.rst new file mode 100644 index 0000000000..ec201aa3d5 --- /dev/null +++ b/doc/source/command-objects/backup.rst @@ -0,0 +1,104 @@ +====== +backup +====== + +Volume v1 + +backup create +------------- + +Create new backup + +.. program:: backup create +.. code:: bash + + os backup create + [--container ] + [--name ] + [--description ] + + +.. option:: --container + + Optional backup container name + +.. option:: --name + + Name of the backup + +.. option:: --description + + Description of the backup + +.. _backup_create-backup: +.. describe:: + + Volume to backup (name or ID) + +backup delete +------------- + +Delete backup(s) + +.. program:: backup delete +.. code:: bash + + os backup delete + [ ...] + +.. _backup_delete-backup: +.. describe:: + + Backup(s) to delete (ID only) + +backup list +----------- + +List backups + +.. program:: backup list +.. code:: bash + + os backup list + +.. _backup_list-backup: +.. option:: --long + + List additional fields in output + +backup restore +-------------- + +Restore backup + +.. program:: backup restore +.. code:: bash + + os backup restore + + + +.. _backup_restore-backup: +.. describe:: + + Backup to restore (ID only) + +.. describe:: + + Volume to restore to (name or ID) + +backup show +----------- + +Display backup details + +.. program:: backup show +.. code:: bash + + os backup show + + +.. _backup_show-backup: +.. describe:: + + Backup to display (ID only) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4b2e6355e4..4649b012c2 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -72,7 +72,7 @@ referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token * ``availability zone``: (**Compute**) a logical partition of hosts or volume services * ``aggregate``: (**Compute**) a grouping of servers -* ``backup``: Volume - a volume copy +* ``backup``: (**Volume**) a volume copy * ``catalog``: (**Identity**) service catalog * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index eab388a59f..71c8ed3832 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -27,7 +27,7 @@ class CreateBackup(show.ShowOne): - """Create backup command""" + """Create new backup""" log = logging.getLogger(__name__ + '.CreateBackup') @@ -36,13 +36,13 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='The name or ID of the volume to backup', + help='Volume to backup (name or ID)', ) parser.add_argument( '--container', metavar='', required=False, - help='Optional backup container name.', + help='Optional backup container name', ) parser.add_argument( '--name', @@ -84,7 +84,7 @@ def get_parser(self, prog_name): 'backups', metavar='', nargs="+", - help='Backup(s) to delete (name or ID)', + help='Backup(s) to delete (ID only)', ) return parser @@ -99,7 +99,7 @@ def take_action(self, parsed_args): class ListBackup(lister.Lister): - """List backup command""" + """List backups""" log = logging.getLogger(__name__ + '.ListBackup') @@ -156,7 +156,7 @@ def _format_volume_id(volume_id): class RestoreBackup(command.Command): - """Restore backup command""" + """Restore backup""" log = logging.getLogger(__name__ + '.RestoreBackup') @@ -165,11 +165,11 @@ def get_parser(self, prog_name): parser.add_argument( 'backup', metavar='', - help='ID of backup to restore') + help='Backup to restore (ID only)') parser.add_argument( 'volume', - metavar='', - help='ID of volume to restore to') + metavar='', + help='Volume to restore to (name or ID)') return parser def take_action(self, parsed_args): @@ -184,7 +184,7 @@ def take_action(self, parsed_args): class ShowBackup(show.ShowOne): - """Show backup command""" + """Display backup details""" log = logging.getLogger(__name__ + '.ShowBackup') @@ -193,7 +193,7 @@ def get_parser(self, prog_name): parser.add_argument( 'backup', metavar='', - help='Name or ID of backup to display') + help='Backup to display (ID only)') return parser def take_action(self, parsed_args): From e8be3b64c1956b58fa0a5b6d460c8bf07085951c Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 6 Jan 2015 01:43:49 -0500 Subject: [PATCH 0126/3303] Command doc: mapping Also tweaked the code for `mapping set` as it was previously using cliff Show instead of cliff Command. Change-Id: I0ea1383a9f2dddf4b2f717b2aa16bbd60ab1720c --- doc/source/command-objects/mapping.rst | 91 ++++++++++++++++++++++++++ doc/source/commands.rst | 1 + openstackclient/identity/v3/mapping.py | 39 ++++++----- 3 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 doc/source/command-objects/mapping.rst diff --git a/doc/source/command-objects/mapping.rst b/doc/source/command-objects/mapping.rst new file mode 100644 index 0000000000..5c7535bd1c --- /dev/null +++ b/doc/source/command-objects/mapping.rst @@ -0,0 +1,91 @@ +======= +mapping +======= + +Identity v3 + +`Requires: OS-FEDERATION extension` + +mapping create +-------------- + +Create new mapping + +.. program:: mapping create +.. code:: bash + + os mapping create + --rules + + +.. option:: --rules + + Filename that contains a set of mapping rules (required) + +.. _mapping_create-mapping: +.. describe:: + + New mapping name (must be unique) + +mapping delete +-------------- + +Delete a mapping + +.. program:: mapping delete +.. code:: bash + + os mapping delete + + +.. _mapping_delete-mapping: +.. describe:: + + Mapping to delete + +mapping list +------------ + +List mappings + +.. program:: mapping list +.. code:: bash + + os mapping list + +mapping set +----------- + +Set mapping properties + +.. program:: mapping set +.. code:: bash + + os mapping set + [--rules ] + + +.. option:: --rules + + Filename that contains a new set of mapping rules + +.. _mapping_set-mapping: +.. describe:: + + Mapping to modify + +mapping show +------------ + +Display mapping details + +.. program:: mapping show +.. code:: bash + + os mapping show + + +.. _mapping_show-mapping: +.. describe:: + + Mapping to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index bb3db18755..8976fc029a 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -93,6 +93,7 @@ referring to both Compute and Volume quotas. * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server * ``keypair``: (**Compute**) an SSH public key * ``limits``: (**Compute**, **Volume**) resource usage limits +* ``mapping``: (**Identity**) a definition to translate identity provider attributes to Identity concepts * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: (**Object Store**) a single file in the Object Store diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index c530a40439..a1f60438f6 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -80,7 +80,7 @@ def _read_rules(self, path): class CreateMapping(show.ShowOne, _RulesReader): - """Create new federation mapping""" + """Create new mapping""" log = logging.getLogger(__name__ + '.CreateMapping') @@ -89,12 +89,12 @@ def get_parser(self, prog_name): parser.add_argument( 'mapping', metavar='', - help='New mapping (must be unique)', + help='New mapping name (must be unique)', ) parser.add_argument( '--rules', - metavar='', required=True, - help='Filename with rules', + metavar='', required=True, + help='Filename that contains a set of mapping rules (required)', ) return parser @@ -112,7 +112,7 @@ def take_action(self, parsed_args): class DeleteMapping(command.Command): - """Delete federation mapping""" + """Delete a mapping""" log = logging.getLogger(__name__ + '.DeleteMapping') @@ -120,7 +120,7 @@ def get_parser(self, prog_name): parser = super(DeleteMapping, self).get_parser(prog_name) parser.add_argument( 'mapping', - metavar='', + metavar='', help='Mapping to delete', ) return parser @@ -134,7 +134,7 @@ def take_action(self, parsed_args): class ListMapping(lister.Lister): - """List federation mappings""" + """List mappings""" log = logging.getLogger(__name__ + '.ListMapping') def take_action(self, parsed_args): @@ -149,8 +149,8 @@ def take_action(self, parsed_args): return (columns, items) -class SetMapping(show.ShowOne, _RulesReader): - """Update federation mapping""" +class SetMapping(command.Command, _RulesReader): + """Set mapping properties""" log = logging.getLogger(__name__ + '.SetMapping') @@ -159,12 +159,12 @@ def get_parser(self, prog_name): parser.add_argument( 'mapping', metavar='', - help='Mapping to update.', + help='Mapping to modify', ) parser.add_argument( '--rules', - metavar='', required=True, - help='Filename with rules', + metavar='', + help='Filename that contains a new set of mapping rules', ) return parser @@ -172,19 +172,22 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + if not parsed_args.rules: + self.app.log.error("No changes requested") + return + rules = self._read_rules(parsed_args.rules) mapping = identity_client.federation.mappings.update( mapping=parsed_args.mapping, rules=rules) - info = {} - info.update(mapping._info) - return zip(*sorted(six.iteritems(info))) + mapping._info.pop('links', None) + return zip(*sorted(six.iteritems(mapping._info))) class ShowMapping(show.ShowOne): - """Show federation mapping details""" + """Display mapping details""" log = logging.getLogger(__name__ + '.ShowMapping') @@ -192,8 +195,8 @@ def get_parser(self, prog_name): parser = super(ShowMapping, self).get_parser(prog_name) parser.add_argument( 'mapping', - metavar='', - help='Mapping to show', + metavar='', + help='Mapping to display', ) return parser From c9cf126a83459e1b843153a91dfe86d975d0d8bb Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 6 Jan 2015 01:59:59 -0500 Subject: [PATCH 0127/3303] Command doc: identity provider Change-Id: Ie73accfaa3d45205a2521e6e61efd16142c460b2 --- .../command-objects/identity-provider.rst | 100 ++++++++++++++++++ doc/source/commands.rst | 2 +- .../identity/v3/identity_provider.py | 31 +++--- 3 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 doc/source/command-objects/identity-provider.rst diff --git a/doc/source/command-objects/identity-provider.rst b/doc/source/command-objects/identity-provider.rst new file mode 100644 index 0000000000..f08cde0188 --- /dev/null +++ b/doc/source/command-objects/identity-provider.rst @@ -0,0 +1,100 @@ +================= +identity provider +================= + +Identity v3 + +`Requires: OS-FEDERATION extension` + +identity provider create +------------------------ + +Create new identity provider + +.. program:: identity provider create +.. code:: bash + + os identity provider create + [--description ] + [--enable | --disable] + + +.. option:: --description + + New identity provider description + +.. option:: --enable + + Enable the identity provider (default) + +.. option:: --disable + + Disable the identity provider + +.. describe:: + + New identity provider name (must be unique) + +identity provider delete +------------------------ + +Delete an identity provider + +.. program:: identity provider delete +.. code:: bash + + os identity provider delete + + +.. describe:: + + Identity provider to delete + +identity provider list +---------------------- + +List identity providers + +.. program:: identity provider list +.. code:: bash + + os identity provider list + +identity provider set +--------------------- + +Set identity provider properties + +.. program:: identity provider set +.. code:: bash + + os identity provider set + [--enable | --disable] + + +.. option:: --enable + + Enable the identity provider + +.. option:: --disable + + Disable the identity provider + +.. describe:: + + Identity provider to modify + +identity provider show +---------------------- + +Display identity provider details + +.. program:: identity provider show +.. code:: bash + + os identity provider show + + +.. describe:: + + Identity provider to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 8976fc029a..cbe46ef7e7 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -87,7 +87,7 @@ referring to both Compute and Volume quotas. * ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager -* ``identity provider``: Identity - a source of users and authentication +* ``identity provider``: (**Identity**) a source of users and authentication * ``image``: Image - a disk image * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 8a1b22d0cf..f46341a16b 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -15,7 +15,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -33,22 +32,21 @@ def get_parser(self, prog_name): parser = super(CreateIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider_id', - metavar='', - help='New identity provider ID (must be unique)' + metavar='', + help='New identity provider name (must be unique)' ) parser.add_argument( '--description', metavar='', help='New identity provider description', ) - enable_identity_provider = parser.add_mutually_exclusive_group() enable_identity_provider.add_argument( '--enable', dest='enabled', action='store_true', default=True, - help='Enable identity provider', + help='Enable identity provider (default)', ) enable_identity_provider.add_argument( '--disable', @@ -79,8 +77,8 @@ def get_parser(self, prog_name): parser = super(DeleteIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='Identity provider ID to delete', + metavar='', + help='Identity provider to delete', ) return parser @@ -118,10 +116,9 @@ def get_parser(self, prog_name): parser = super(SetIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='Identity provider ID to change', + metavar='', + help='Identity provider to modify', ) - enable_identity_provider = parser.add_mutually_exclusive_group() enable_identity_provider.add_argument( '--enable', @@ -144,19 +141,17 @@ def take_action(self, parsed_args): elif parsed_args.disable is True: enabled = False else: - sys.stdout.write("Identity Provider not updated, " - "no arguments present") + self.log.error("No changes requested") return (None, None) identity_provider = federation_client.identity_providers.update( parsed_args.identity_provider, enabled=enabled) - info = {} - info.update(identity_provider._info) - return zip(*sorted(six.iteritems(info))) + identity_provider._info.pop('links', None) + return zip(*sorted(six.iteritems(identity_provider._info))) class ShowIdentityProvider(show.ShowOne): - """Show identity provider details""" + """Display identity provider details""" log = logging.getLogger(__name__ + '.ShowIdentityProvider') @@ -164,8 +159,8 @@ def get_parser(self, prog_name): parser = super(ShowIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='Identity provider ID to show', + metavar='', + help='Identity provider to display', ) return parser From a0c63dedf41bfa9549d6b95902b91f7bfb2b5f55 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 6 Jan 2015 02:29:56 -0500 Subject: [PATCH 0128/3303] Command doc: federation protocol Change-Id: I1289eb0caf31fca21c5c377cf13aebd1434a00ee --- .../command-objects/federation-protocol.rst | 112 ++++++++++++++++++ doc/source/commands.rst | 1 + .../identity/v3/federation_protocol.py | 66 ++++++----- 3 files changed, 152 insertions(+), 27 deletions(-) create mode 100644 doc/source/command-objects/federation-protocol.rst diff --git a/doc/source/command-objects/federation-protocol.rst b/doc/source/command-objects/federation-protocol.rst new file mode 100644 index 0000000000..0ed0980a54 --- /dev/null +++ b/doc/source/command-objects/federation-protocol.rst @@ -0,0 +1,112 @@ +=================== +federation protocol +=================== + +Identity v3 + +`Requires: OS-FEDERATION extension` + +federation protocol create +-------------------------- + +Create new federation protocol + +.. program:: federation protocol create +.. code:: bash + + os federation protocol create + --identity-provider + --mapping + + +.. option:: --identity-provider + + Identity provider that will support the new federation protocol (name or ID) (required) + +.. option:: --mapping + + Mapping that is to be used (name or ID) (required) + +.. describe:: + + New federation protocol name (must be unique per identity provider) + +federation protocol delete +-------------------------- + +Delete a federation protocol + +.. program:: federation protocol delete +.. code:: bash + + os federation protocol delete + --identity-provider + + +.. option:: --identity-provider + + Identity provider that supports (name or ID) (required) + +.. describe:: + + Federation protocol to delete (name or ID) + +federation protocol list +------------------------ + +List federation protocols + +.. program:: federation protocol list +.. code:: bash + + os federation protocol list + --identity-provider + +.. option:: --identity-provider + + Identity provider to list (name or ID) (required) + +federation protocol set +----------------------- + +Set federation protocol properties + +.. program:: federation protocol set +.. code:: bash + + os federation protocol set + --identity-provider + [--mapping ] + + +.. option:: --identity-provider + + Identity provider that supports (name or ID) (required) + +.. option:: --mapping + + Mapping that is to be used (name or ID) + +.. describe:: + + Federation protocol to modify (name or ID) + +federation protocol show +------------------------ + +Display federation protocol details + +.. program:: federation protocol show +.. code:: bash + + os federation protocol show + --identity-provider + + +.. option:: --identity-provider + + Identity provider that supports (name or ID) (required) + +.. describe:: + + Federation protocol to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index cbe46ef7e7..a09dcb9ec8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -83,6 +83,7 @@ referring to both Compute and Volume quotas. * ``ec2 cedentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions +* ``federation protocol``: (**Identity**) the underlying protocol used while federating identities * ``flavor``: (**Compute**) predefined server configurations: ram, root disk, etc * ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 693ec94eae..5a651165b1 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -25,7 +25,7 @@ class CreateProtocol(show.ShowOne): - """Create new Federation Protocol tied to an Identity Provider""" + """Create new federation protocol""" log = logging.getLogger(__name__ + 'CreateProtocol') @@ -34,16 +34,19 @@ def get_parser(self, prog_name): parser.add_argument( 'federation_protocol', metavar='', - help='Protocol (must be unique per Identity Provider') + help='New federation protocol name (must be unique per identity ' + ' provider)') parser.add_argument( '--identity-provider', metavar='', - help=('Identity Provider you want to add the Protocol to ' - '(must already exist)'), required=True) + required=True, + help='Identity provider that will support the new federation ' + ' protocol (name or ID) (required)') parser.add_argument( '--mapping', - metavar='', required=True, - help='Mapping you want to be used (must already exist)') + metavar='', + required=True, + help='Mapping that is to be used (name or ID) (required)') return parser @@ -66,7 +69,7 @@ def take_action(self, parsed_args): class DeleteProtocol(command.Command): - """Delete Federation Protocol tied to a Identity Provider""" + """Delete a federation protocol""" log = logging.getLogger(__name__ + '.DeleteProtocol') @@ -74,12 +77,14 @@ def get_parser(self, prog_name): parser = super(DeleteProtocol, self).get_parser(prog_name) parser.add_argument( 'federation_protocol', - metavar='', - help='Protocol (must be unique per Identity Provider') + metavar='', + help='Federation protocol to delete (name or ID)') parser.add_argument( '--identity-provider', - metavar='', required=True, - help='Identity Provider the Protocol is tied to') + metavar='', + required=True, + help='Identity provider that supports ' + '(name or ID) (required)') return parser @@ -92,7 +97,7 @@ def take_action(self, parsed_args): class ListProtocols(lister.Lister): - """List Protocols tied to an Identity Provider""" + """List federation protocols""" log = logging.getLogger(__name__ + '.ListProtocols') @@ -100,8 +105,9 @@ def get_parser(self, prog_name): parser = super(ListProtocols, self).get_parser(prog_name) parser.add_argument( '--identity-provider', - metavar='', required=True, - help='Identity Provider the Protocol is tied to') + metavar='', + required=True, + help='Identity provider to list (name or ID) (required)') return parser @@ -118,7 +124,7 @@ def take_action(self, parsed_args): class SetProtocol(command.Command): - """Set Protocol tied to an Identity Provider""" + """Set federation protocol properties""" log = logging.getLogger(__name__ + '.SetProtocol') @@ -127,21 +133,26 @@ def get_parser(self, prog_name): parser.add_argument( 'federation_protocol', metavar='', - help='Protocol (must be unique per Identity Provider') + help='Federation protocol to modify (name or ID)') parser.add_argument( '--identity-provider', - metavar='', required=True, - help=('Identity Provider you want to add the Protocol to ' - '(must already exist)')) + metavar='', + required=True, + help='Identity provider that supports ' + '(name or ID) (required)') parser.add_argument( '--mapping', - metavar='', required=True, - help='Mapping you want to be used (must already exist)') + metavar='', + help='Mapping that is to be used (name or ID)') return parser def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + if not parsed_args.mapping: + self.app.log.error("No changes requested") + return + protocol = identity_client.federation.protocols.update( parsed_args.identity_provider, parsed_args.federation_protocol, parsed_args.mapping) @@ -156,7 +167,7 @@ def take_action(self, parsed_args): class ShowProtocol(show.ShowOne): - """Show Protocol tied to an Identity Provider""" + """Display federation protocol details""" log = logging.getLogger(__name__ + '.ShowProtocol') @@ -164,13 +175,14 @@ def get_parser(self, prog_name): parser = super(ShowProtocol, self).get_parser(prog_name) parser.add_argument( 'federation_protocol', - metavar='', - help='Protocol (must be unique per Identity Provider') + metavar='', + help='Federation protocol to display (name or ID)') parser.add_argument( '--identity-provider', - metavar='', required=True, - help=('Identity Provider you want to add the Protocol to ' - '(must already exist)')) + metavar='', + required=True, + help=('Identity provider that supports ' + '(name or ID) (required)')) return parser def take_action(self, parsed_args): From 0ff28d5251f9e25eafdc628e29b093b7c694ea48 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 15:16:41 -0500 Subject: [PATCH 0129/3303] Allow user list to filter by project Adds a --project filter to `os user list`, which really calls the role assignment manager behind the scenes. Change-Id: I57a75018f12ed3acdf8f6611b6b58bd974f91da2 Closes-Bug: #1397251 --- doc/source/command-objects/user.rst | 18 +++---- openstackclient/identity/v3/user.py | 53 ++++++++++++++++--- .../tests/identity/v3/test_user.py | 47 ++++++++++++++++ 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index e54c65673c..a9a98fe18f 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -101,28 +101,26 @@ List users .. code:: bash os user list - [--domain ] [--project ] - [--group ] + [--domain ] + [--group | --project ] [--long] -.. option:: --domain - - Filter users by `` (name or ID) - - .. versionadded:: 3 - .. option:: --project Filter users by `` (name or ID) - *Removed in version 3.* +.. option:: --domain + + Filter users by `` (name or ID) + + *Identity version 3 only* .. option:: --group Filter users by `` membership (name or ID) - .. versionadded:: 3 + *Identity version 3 only* .. option:: --long diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index a60c8c83fa..4fb7b6d1e0 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -188,11 +188,17 @@ def get_parser(self, prog_name): metavar='', help='Filter users by (name or ID)', ) - parser.add_argument( + project_or_group = parser.add_mutually_exclusive_group() + project_or_group.add_argument( '--group', metavar='', help='Filter users by membership (name or ID)', ) + project_or_group.add_argument( + '--project', + metavar='', + help='Filter users by (name or ID)', + ) parser.add_argument( '--long', action='store_true', @@ -219,7 +225,44 @@ def take_action(self, parsed_args): else: group = None - # List users + if parsed_args.project: + if domain is not None: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + domain_id=domain + ).id + else: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ).id + + assignments = identity_client.role_assignments.list( + project=project) + + # NOTE(stevemar): If a user has more than one role on a project + # then they will have two entries in the returned data. Since we + # are looking for any role, let's just track unique user IDs. + user_ids = set() + for assignment in assignments: + if hasattr(assignment, 'user'): + user_ids.add(assignment.user['id']) + + # NOTE(stevemar): Call find_resource once we have unique IDs, so + # it's fewer trips to the Identity API, then collect the data. + data = [] + for user_id in user_ids: + user = utils.find_resource(identity_client.users, user_id) + data.append(user) + + else: + data = identity_client.users.list( + domain=domain, + group=group, + ) + + # Column handling if parsed_args.long: columns = ['ID', 'Name', 'Default Project Id', 'Domain Id', 'Description', 'Email', 'Enabled'] @@ -228,11 +271,7 @@ def take_action(self, parsed_args): column_headers[3] = 'Domain' else: columns = ['ID', 'Name'] - column_headers = copy.deepcopy(columns) - data = identity_client.users.list( - domain=domain, - group=group, - ) + column_headers = columns return ( column_headers, diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index dd517e19f2..35dd98ee9e 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -44,6 +44,11 @@ def setUp(self): self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() + # Shortcut for RoleAssignmentManager Mock + self.role_assignments_mock = self.app.client_manager.identity.\ + role_assignments + self.role_assignments_mock.reset_mock() + class TestUserCreate(TestUser): @@ -511,6 +516,21 @@ def setUp(self): loaded=True, ) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ) + ] + # Get the command object to test self.cmd = user.ListUser(self.app, None) @@ -643,6 +663,33 @@ def test_user_list_long(self): ), ) self.assertEqual(datalist, tuple(data)) + def test_user_list_project(self): + arglist = [ + '--project', identity_fakes.project_name, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'project': identity_fakes.project_id, + } + + self.role_assignments_mock.list.assert_called_with(**kwargs) + self.users_mock.get.assert_called_with(identity_fakes.user_id) + + collist = ['ID', 'Name'] + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(datalist, tuple(data)) + class TestUserSet(TestUser): From c885c72cba459ca853de10ec1685d70d9c2b7ca2 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 01:29:35 -0500 Subject: [PATCH 0130/3303] Command doc: consumer Change-Id: Ie687e1d7f80810106a64204828299f9d143b8d7c --- doc/source/command-objects/consumer.rst | 83 +++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/consumer.py | 22 +++---- 3 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/consumer.rst diff --git a/doc/source/command-objects/consumer.rst b/doc/source/command-objects/consumer.rst new file mode 100644 index 0000000000..59ace845e0 --- /dev/null +++ b/doc/source/command-objects/consumer.rst @@ -0,0 +1,83 @@ +======== +consumer +======== + +Identity v3 + +`Requires: OS-OAUTH1 extension` + +consumer create +--------------- + +Create new consumer + +.. program:: consumer create +.. code:: bash + + os consumer create + [--description ] + +.. option:: --description + + New consumer description + +consumer delete +--------------- + +Delete a consumer + +.. program:: consumer delete +.. code:: bash + + os consumer delete + + +.. describe:: + + Consumer to delete + +consumer list +------------- + +List consumers + +.. program:: consumer list +.. code:: bash + + os consumer list + +consumer set +------------ + +Set consumer properties + +.. program:: consumer set +.. code:: bash + + os consumer set + [--description ] + + +.. option:: --description + + New consumer description + +.. describe:: + + Consumer to modify + +consumer show +------------- + +Display consumer details + +.. program:: consumer show +.. code:: bash + + os consumer show + + +.. _consumer_show-consumer: +.. describe:: + + Consumer to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index bb3db18755..9d60984354 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -76,7 +76,7 @@ referring to both Compute and Volume quotas. * ``catalog``: (**Identity**) service catalog * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL -* ``consumer``: Identity - OAuth-based delegatee +* ``consumer``: (**Identity**) OAuth-based delegatee * ``container``: (**Object Store**) a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index b7e57d8dd0..c5e263926c 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -27,7 +27,7 @@ class CreateConsumer(show.ShowOne): - """Create consumer command""" + """Create new consumer""" log = logging.getLogger(__name__ + '.CreateConsumer') @@ -35,7 +35,7 @@ def get_parser(self, prog_name): parser = super(CreateConsumer, self).get_parser(prog_name) parser.add_argument( '--description', - metavar='', + metavar='', help='New consumer description', ) return parser @@ -51,7 +51,7 @@ def take_action(self, parsed_args): class DeleteConsumer(command.Command): - """Delete consumer command""" + """Delete a consumer""" log = logging.getLogger(__name__ + '.DeleteConsumer') @@ -60,7 +60,7 @@ def get_parser(self, prog_name): parser.add_argument( 'consumer', metavar='', - help='ID of consumer to delete', + help='Consumer to delete', ) return parser @@ -74,7 +74,7 @@ def take_action(self, parsed_args): class ListConsumer(lister.Lister): - """List consumer command""" + """List consumers""" log = logging.getLogger(__name__ + '.ListConsumer') @@ -90,7 +90,7 @@ def take_action(self, parsed_args): class SetConsumer(command.Command): - """Set consumer command""" + """Set consumer properties""" log = logging.getLogger(__name__ + '.SetConsumer') @@ -99,11 +99,11 @@ def get_parser(self, prog_name): parser.add_argument( 'consumer', metavar='', - help='ID of consumer to change', + help='Consumer to modify', ) parser.add_argument( '--description', - metavar='', + metavar='', help='New consumer description', ) return parser @@ -118,7 +118,7 @@ def take_action(self, parsed_args): kwargs['description'] = parsed_args.description if not len(kwargs): - sys.stdout.write("Consumer not updated, no arguments present") + sys.stdout.write('Consumer not updated, no arguments present') return consumer = identity_client.oauth1.consumers.update( @@ -127,7 +127,7 @@ def take_action(self, parsed_args): class ShowConsumer(show.ShowOne): - """Show consumer command""" + """Display consumer details""" log = logging.getLogger(__name__ + '.ShowConsumer') @@ -136,7 +136,7 @@ def get_parser(self, prog_name): parser.add_argument( 'consumer', metavar='', - help='ID of consumer to display', + help='Consumer to display', ) return parser From 6025fa83f193fe422cff1fdc93f658ec457e0136 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:02:55 -0500 Subject: [PATCH 0131/3303] Request token creation docs + tweaks Added command docs, and changed request token to take in name or id of a project, and also support a domain option. Change-Id: I87363274e5b7a0c687e234f5a4bcaaf166d28840 --- doc/source/command-objects/request-token.rst | 37 ++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/token.py | 43 ++++++++++++++----- .../tests/identity/v3/test_oauth.py | 12 +++++- 4 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 doc/source/command-objects/request-token.rst diff --git a/doc/source/command-objects/request-token.rst b/doc/source/command-objects/request-token.rst new file mode 100644 index 0000000000..501f67a57f --- /dev/null +++ b/doc/source/command-objects/request-token.rst @@ -0,0 +1,37 @@ +============= +request token +============= + +Identity v3 + +`Requires: OS-OAUTH1 extension` + +request token create +-------------------- + +Create a request token + +.. program:: request token create +.. code:: bash + + os request token create + --consumer-key + --consumer-secret + --project + [--domain ] + +.. option:: --consumer-key + + Consumer key (required) + +.. option:: --description + + Consumer secret (required) + +.. option:: --project + + Project that consumer wants to access (name or ID) (required) + +.. option:: --domain + + Domain owning (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 9d60984354..1136f0a2bd 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -100,7 +100,7 @@ referring to both Compute and Volume quotas. * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``region``: (**Identity**) a subset of an OpenStack deployment -* ``request token``: Identity - temporary OAuth-based token +* ``request token``: (**Identity**) temporary OAuth-based token * ``role``: (**Identity**) a policy object used to determine authorization * ``role assignment``: (**Identity**) a relationship between roles, users or groups, and domains or projects * ``security group``: Compute, Network - groups of network access rules diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 5b09b69f61..86f31a2a4d 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -20,6 +20,9 @@ from cliff import show +from openstackclient.common import utils +from openstackclient.identity import common + class AuthorizeRequestToken(show.ShowOne): """Authorize request token""" @@ -53,6 +56,7 @@ def take_action(self, parsed_args): verifier_pin = identity_client.oauth1.request_tokens.authorize( parsed_args.request_key, roles) + info = {} info.update(verifier_pin._info) return zip(*sorted(six.iteritems(info))) @@ -110,7 +114,7 @@ def take_action(self, parsed_args): class CreateRequestToken(show.ShowOne): - """Create request token""" + """Create a request token""" log = logging.getLogger(__name__ + '.CreateRequestToken') @@ -119,33 +123,50 @@ def get_parser(self, prog_name): parser.add_argument( '--consumer-key', metavar='', - help='Consumer key', + help='Consumer key (required)', required=True ) parser.add_argument( '--consumer-secret', metavar='', - help='Consumer secret', + help='Consumer secret (required)', required=True ) parser.add_argument( - '--project-id', - metavar='', - help='Requested project ID', + '--project', + metavar='', + help='Project that consumer wants to access (name or ID)' + ' (required)', required=True ) + parser.add_argument( + '--domain', + metavar='', + help='Domain owning (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - token_client = self.app.client_manager.identity.oauth1.request_tokens + + identity_client = self.app.client_manager.identity + + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + project = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id) + else: + project = utils.find_resource(identity_client.projects, + parsed_args.project) + + token_client = identity_client.oauth1.request_tokens + request_token = token_client.create( parsed_args.consumer_key, parsed_args.consumer_secret, - parsed_args.project_id) - info = {} - info.update(request_token._info) - return zip(*sorted(six.iteritems(info))) + project.id) + return zip(*sorted(six.iteritems(request_token._info))) class IssueToken(show.ShowOne): diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index 15ba04e33f..36a65e4cf9 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -26,6 +26,8 @@ def setUp(self): self.access_tokens_mock.reset_mock() self.request_tokens_mock = identity_client.oauth1.request_tokens self.request_tokens_mock.reset_mock() + self.projects_mock = identity_client.projects + self.projects_mock.reset_mock() class TestRequestTokenCreate(TestOAuth1): @@ -39,18 +41,24 @@ def setUp(self): loaded=True, ) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.cmd = token.CreateRequestToken(self.app, None) def test_create_request_tokens(self): arglist = [ '--consumer-key', identity_fakes.consumer_id, '--consumer-secret', identity_fakes.consumer_secret, - '--project-id', identity_fakes.project_id, + '--project', identity_fakes.project_id, ] verifylist = [ ('consumer_key', identity_fakes.consumer_id), ('consumer_secret', identity_fakes.consumer_secret), - ('project_id', identity_fakes.project_id), + ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) From 0d7a50d3848484d6562dbd6af87de7836365821a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:54:26 -0500 Subject: [PATCH 0132/3303] Command doc: image Change-Id: Ib1563b58351315dc2a44ad77882f8c834a1214c0 --- doc/source/command-objects/image.rst | 245 +++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/image/v1/image.py | 20 +-- openstackclient/image/v2/image.py | 8 +- 4 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 doc/source/command-objects/image.rst diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst new file mode 100644 index 0000000000..d9b77266f2 --- /dev/null +++ b/doc/source/command-objects/image.rst @@ -0,0 +1,245 @@ +====== +image +====== + +Image v1, v2 + +image create +------------ + +*Only supported for Image v1* + +Create/upload an image + +.. program:: image create +.. code:: bash + + os image create + [--id ] + [--store ] + [--container-format ] + [--disk-format ] + [--owner ] + [--size ] + [--min-disk ] + [--min-ram ] + [--location ] + [--copy-from ] + [--file ] + [--volume ] + [--force] + [--checksum ] + [--protected | --unprotected] + [--public | --private] + [--property [...] ] + + +.. option:: --id + + Image ID to reserve + +.. option:: --store + + Upload image to this store + +.. option:: --container-format + + Image container format (default: bare) + +.. option:: --disk-format + + Image disk format (default: raw) + +.. option:: --owner + + Image owner project name or ID + +.. option:: --size + + Image size, in bytes (only used with --location and --copy-from) + +.. option:: --min-disk + + Minimum disk size needed to boot image, in gigabytes + +.. option:: --min-ram + + Minimum RAM size needed to boot image, in megabytes + +.. option:: --location + + Download image from an existing URL + +.. option:: --copy-from + + Copy image from the data store (similar to --location) + +.. option:: --file + + Upload image from local file + +.. option:: --volume + + Create image from a volume + +.. option:: --force + + Force image creation if volume is in use (only meaningful with --volume) + +.. option:: --checksum + + Image hash used for verification + +.. option:: --protected + + Prevent image from being deleted + +.. option:: --unprotected + + Allow image to be deleted (default) + +.. option:: --public + + Image is accessible to the public + +.. option:: --private + + Image is inaccessible to the public (default) + +.. option:: --property + + Set a property on this image (repeat for multiple values) + +.. describe:: + + New image name + +image delete +------------ + +Delete image(s) + +.. program:: image delete +.. code:: bash + + os image delete + + +.. describe:: + + Image(s) to delete (name or ID) + +image list +---------- + +List available images + +.. program:: image list +.. code:: bash + + os image list + [--page-size ] + [--long] + +.. option:: --page-size + + Number of images to request in each paginated request + +.. option:: --long + + List additional fields in output + +image save +---------- + +Save an image locally + +.. program:: image save +.. code:: bash + + os image save + --file + + +.. option:: --file + + Downloaded image save filename (default: stdout) + +.. describe:: + + Image to save (name or ID) + +image set +--------- + +*Only supported for Image v1* + +Set image properties + +.. program:: image set +.. code:: bash + + os image set + [--name ] + [--owner ] + [--min-disk ] + [--min-ram ] + [--protected | --unprotected] + [--public | --private] + [--property [...] ] + + +.. option:: --name + + New image name + +.. option:: --owner + + New image owner project (name or ID) + +.. option:: --min-disk + + Minimum disk size needed to boot image, in gigabytes + +.. option:: --min-ram + + Minimum RAM size needed to boot image, in megabytes + +.. option:: --protected + + Prevent image from being deleted + +.. option:: --unprotected + + Allow image to be deleted (default) + +.. option:: --public + + Image is accessible to the public + +.. option:: --private + + Image is inaccessible to the public (default) + +.. option:: --property + + Set a property on this image (repeat for multiple values) + +.. describe:: + + Image to modify (name or ID) + +image show +---------- + +Display image details + +.. program:: image show +.. code:: bash + + os image show + + +.. describe:: + + Image to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4f8b55792a..01175bd72e 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -89,7 +89,7 @@ referring to both Compute and Volume quotas. * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager * ``identity provider``: (**Identity**) a source of users and authentication -* ``image``: Image - a disk image +* ``image``: (**Image**) a disk image * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server * ``keypair``: (**Compute**) an SSH public key diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index ca1eead4d8..d7ece25462 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -49,7 +49,7 @@ def get_parser(self, prog_name): parser = super(CreateImage, self).get_parser(prog_name) parser.add_argument( "name", - metavar="", + metavar="", help="New image name", ) parser.add_argument( @@ -159,7 +159,7 @@ def get_parser(self, prog_name): dest="properties", metavar="", action=parseractions.KeyValueAction, - help="Set an image property " + help="Set a property on this image " "(repeat option to set multiple properties)", ) return parser @@ -337,12 +337,12 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", - help="Downloaded image save filename [default: stdout]", + help="Downloaded image save filename (default: stdout)", ) parser.add_argument( "image", metavar="", - help="Name or ID of image to save", + help="Image to save (name or ID)", ) return parser @@ -360,7 +360,7 @@ def take_action(self, parsed_args): class SetImage(show.ShowOne): - """Change image properties""" + """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") @@ -369,7 +369,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Image name or ID to change", + help="Image to modify (name or ID)", ) parser.add_argument( "--name", @@ -379,7 +379,7 @@ def get_parser(self, prog_name): parser.add_argument( "--owner", metavar="", - help="New image owner project name or ID", + help="New image owner project (name or ID)", ) parser.add_argument( "--min-disk", @@ -420,7 +420,7 @@ def get_parser(self, prog_name): dest="properties", metavar="", action=parseractions.KeyValueAction, - help="Set an image property " + help="Set a property on this image " "(repeat option to set multiple properties)", ) return parser @@ -474,7 +474,7 @@ def take_action(self, parsed_args): class ShowImage(show.ShowOne): - """Show image details""" + """Display image details""" log = logging.getLogger(__name__ + ".ShowImage") @@ -483,7 +483,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to display", + help="Image to display (name or ID)", ) return parser diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 63351c6d5e..d5ee692ce8 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -102,12 +102,12 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", - help="Downloaded image save filename [default: stdout]", + help="Downloaded image save filename (default: stdout)", ) parser.add_argument( "image", metavar="", - help="Name or ID of image to save", + help="Image to save (name or ID)", ) return parser @@ -125,7 +125,7 @@ def take_action(self, parsed_args): class ShowImage(show.ShowOne): - """Show image details""" + """Display image details""" log = logging.getLogger(__name__ + ".ShowImage") @@ -134,7 +134,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to display", + help="Image to display (name or ID)", ) return parser From 017073327052280b3067096bf5a570b0123e1408 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 21:41:39 -0500 Subject: [PATCH 0133/3303] Fix up snapshot command Several issues with the current snapshot command were resolved: * --long for list was added to include volume id/name, and properties * changed output from metadata to properties * added new option to set properties with 'snapshot set' * added new command to unset properties with 'snapshot unset' Change-Id: I5902cfe876cefada701d4d658a50a4282ff300d6 --- openstackclient/volume/v1/snapshot.py | 129 +++++++++++++++++++++++--- setup.cfg | 1 + 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index c9e1baca92..c3189f0859 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -15,14 +15,15 @@ """Volume v1 Snapshot action implementations""" +import copy import logging import six -import sys from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -70,6 +71,10 @@ def take_action(self, parsed_args): parsed_args.description ) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) @@ -103,20 +108,61 @@ class ListSnapshot(lister.Lister): log = logging.getLogger(__name__ + '.ListSnapshot') + def get_parser(self, prog_name): + parser = super(ListSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - columns = ( - 'ID', - 'Display Name', - 'Display Description', - 'Status', - 'Size' - ) + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].display_name + return volume + + if parsed_args.long: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size', 'Created At', 'Volume ID', 'Metadata'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + column_headers[7] = 'Properties' + else: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size'] + column_headers = copy.deepcopy(columns) + + # Always update Name and Description + column_headers[1] = 'Name' + column_headers[2] = 'Description' + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + data = self.app.client_manager.volume.volume_snapshots.list() - return (columns, + return (column_headers, (utils.get_item_properties( s, columns, - formatters={}, + formatters={'Metadata': utils.format_dict, + 'Volume ID': _format_volume_id}, ) for s in data)) @@ -133,12 +179,19 @@ def get_parser(self, prog_name): help='Name or ID of snapshot to change') parser.add_argument( '--name', - metavar='', + metavar='', help='New snapshot name') parser.add_argument( '--description', - metavar='', + metavar='', help='New snapshot description') + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add/change for this snapshot ' + '(repeat option to set multiple properties)', + ) return parser def take_action(self, parsed_args): @@ -146,15 +199,21 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.set_metadata(snapshot.id, + parsed_args.property) + kwargs = {} if parsed_args.name: kwargs['display_name'] = parsed_args.name if parsed_args.description: kwargs['display_description'] = parsed_args.description - if not kwargs: - sys.stdout.write("Snapshot not updated, no arguments present") + if not kwargs and not parsed_args.property: + self.app.log.error("No changes requested\n") return + snapshot.update(**kwargs) return @@ -178,4 +237,46 @@ def take_action(self, parsed_args): snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) + + +class UnsetSnapshot(command.Command): + """Unset snapshot properties""" + + log = logging.getLogger(__name__ + '.UnsetSnapshot') + + def get_parser(self, prog_name): + parser = super(UnsetSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='', + help='snapshot to change (name or ID)', + ) + parser.add_argument( + '--property', + metavar='', + action='append', + default=[], + help='Property key to remove from snapshot ' + '(repeat to remove multiple values)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + parsed_args.property, + ) + else: + self.app.log.error("No changes requested\n") + return diff --git a/setup.cfg b/setup.cfg index 8d4e1c5009..224b4c6434 100644 --- a/setup.cfg +++ b/setup.cfg @@ -326,6 +326,7 @@ openstack.volume.v1 = snapshot_list = openstackclient.volume.v1.snapshot:ListSnapshot snapshot_set = openstackclient.volume.v1.snapshot:SetSnapshot snapshot_show = openstackclient.volume.v1.snapshot:ShowSnapshot + snapshot_unset = openstackclient.volume.v1.snapshot:UnsetSnapshot volume_create = openstackclient.volume.v1.volume:CreateVolume volume_delete = openstackclient.volume.v1.volume:DeleteVolume From 460b530d8bb31890e65bc60a7004a391efbcc128 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 22:01:37 -0500 Subject: [PATCH 0134/3303] Command doc: snapshot Change-Id: Ibe5cd0a8422788762e0c52b702b7bd54e6a46813 --- doc/source/command-objects/snapshot.rst | 133 ++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/snapshot.py | 22 ++-- 3 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/snapshot.rst diff --git a/doc/source/command-objects/snapshot.rst b/doc/source/command-objects/snapshot.rst new file mode 100644 index 0000000000..7bfd1d9203 --- /dev/null +++ b/doc/source/command-objects/snapshot.rst @@ -0,0 +1,133 @@ +======== +snapshot +======== + +Volume v1 + +snapshot create +--------------- + +Create new snapshot + +.. program:: snapshot create +.. code:: bash + + os snapshot create + [--name ] + [--description ] + [--force] + + +.. option:: --name + + Name of the snapshot + +.. option:: --description + + Description of the snapshot + +.. option:: --force + + Create a snapshot attached to an instance. Default is False + +.. _snapshot_create-snapshot: +.. describe:: + + Volume to snapshot (name or ID) + +snapshot delete +--------------- + +Delete snapshot(s) + +.. program:: snapshot delete +.. code:: bash + + os snapshot delete + [ ...] + +.. _snapshot_delete-snapshot: +.. describe:: + + Snapshot(s) to delete (name or ID) + +snapshot list +------------- + +List snapshots + +.. program:: snapshot list +.. code:: bash + + os snapshot list + +.. option:: --long + + List additional fields in output + +snapshot set +------------ + +Set snapshot properties + +.. program:: snapshot set +.. code:: bash + + os snapshot set + [--name ] + [--description ] + [--property [...] ] + + +.. _snapshot_restore-snapshot: +.. option:: --name + + New snapshot name + +.. option:: --description + + New snapshot description + +.. option:: --property + + Property to add or modify for this snapshot (repeat option to set multiple properties) + +.. describe:: + + Snapshot to modify (name or ID) + +snapshot show +------------- + +Display snapshot details + +.. program:: snapshot show +.. code:: bash + + os snapshot show + + +.. _snapshot_show-snapshot: +.. describe:: + + Snapshot to display (name or ID) + +snapshot unset +-------------- + +Unset snapshot properties + +.. program:: snapshot unset +.. code:: bash + + os snapshot unset + [--property ] + + +.. option:: --property + + Property to remove from snapshot (repeat option to remove multiple properties) + +.. describe:: + + Snapshot to modify (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4649b012c2..56ce5235d0 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -107,7 +107,7 @@ referring to both Compute and Volume quotas. * ``server``: (**Compute**) virtual machine instance * ``server image``: (**Compute**) saved server disk image * ``service``: Identity - a cloud service -* ``snapshot``: Volume - a point-in-time copy of a volume +* ``snapshot``: (**Volume**) a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service * ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index c3189f0859..5ec2b3c5ca 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -28,7 +28,7 @@ class CreateSnapshot(show.ShowOne): - """Create snapshot command""" + """Create new snapshot""" log = logging.getLogger(__name__ + '.CreateSnapshot') @@ -37,7 +37,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='The name or ID of the volume to snapshot', + help='Volume to snapshot (name or ID)', ) parser.add_argument( '--name', @@ -104,7 +104,7 @@ def take_action(self, parsed_args): class ListSnapshot(lister.Lister): - """List snapshot command""" + """List snapshots""" log = logging.getLogger(__name__ + '.ListSnapshot') @@ -167,7 +167,7 @@ def _format_volume_id(volume_id): class SetSnapshot(command.Command): - """Set snapshot command""" + """Set snapshot properties""" log = logging.getLogger(__name__ + '.SetSnapshot') @@ -176,14 +176,14 @@ def get_parser(self, prog_name): parser.add_argument( 'snapshot', metavar='', - help='Name or ID of snapshot to change') + help='Snapshot to modify (name or ID)') parser.add_argument( '--name', - metavar='', + metavar='', help='New snapshot name') parser.add_argument( '--description', - metavar='', + metavar='', help='New snapshot description') parser.add_argument( '--property', @@ -219,7 +219,7 @@ def take_action(self, parsed_args): class ShowSnapshot(show.ShowOne): - """Show snapshot command""" + """Display snapshot details""" log = logging.getLogger(__name__ + '.ShowSnapshot') @@ -228,7 +228,7 @@ def get_parser(self, prog_name): parser.add_argument( 'snapshot', metavar='', - help='Name or ID of snapshot to display') + help='Snapshot to display (name or ID)') return parser def take_action(self, parsed_args): @@ -254,14 +254,14 @@ def get_parser(self, prog_name): parser.add_argument( 'snapshot', metavar='', - help='snapshot to change (name or ID)', + help='Snapshot to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from snapshot ' + help='Property to remove from snapshot ' '(repeat to remove multiple values)', ) return parser From d9c217e5bc40c35af899c34c5965356b8849586e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:14:06 -0500 Subject: [PATCH 0135/3303] Request token authorize Command doc and tweaks to the code Change-Id: I8f251bf9ca77f16b01a509844e79ddde82048b0d --- doc/source/command-objects/request-token.rst | 20 ++++++++++++++ openstackclient/identity/v3/token.py | 26 ++++++++++++------- .../tests/identity/v3/test_oauth.py | 12 +++++++-- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/doc/source/command-objects/request-token.rst b/doc/source/command-objects/request-token.rst index 501f67a57f..84081cb17b 100644 --- a/doc/source/command-objects/request-token.rst +++ b/doc/source/command-objects/request-token.rst @@ -6,6 +6,26 @@ Identity v3 `Requires: OS-OAUTH1 extension` +request token authorize +----------------------- + +Authorize a request token + +.. program:: request token authorize +.. code:: bash + + os request token authorize + --request-key + --role + +.. option:: --request-key + + Request token to authorize (ID only) (required) + +.. option:: --role + + Roles to authorize (name or ID) (repeat to set multiple values) (required) + request token create -------------------- diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 86f31a2a4d..bea2ddeb80 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -25,7 +25,7 @@ class AuthorizeRequestToken(show.ShowOne): - """Authorize request token""" + """Authorize a request token""" log = logging.getLogger(__name__ + '.AuthorizeRequestToken') @@ -34,13 +34,16 @@ def get_parser(self, prog_name): parser.add_argument( '--request-key', metavar='', - help='Request token key', + help='Request token to authorize (ID only) (required)', required=True ) parser.add_argument( - '--role-ids', - metavar='', - help='Requested role IDs', + '--role', + metavar='', + action='append', + default=[], + help='Roles to authorize (name or ID) ' + '(repeat to set multiple values) (required)', required=True ) return parser @@ -49,17 +52,20 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + # NOTE(stevemar): We want a list of role ids roles = [] - for r_id in parsed_args.role_ids.split(): - roles.append(r_id) + for role in parsed_args.role: + role_id = utils.find_resource( + identity_client.roles, + role, + ).id + roles.append(role_id) verifier_pin = identity_client.oauth1.request_tokens.authorize( parsed_args.request_key, roles) - info = {} - info.update(verifier_pin._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(verifier_pin._info))) class CreateAccessToken(show.ShowOne): diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index 36a65e4cf9..435042d1cb 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -28,6 +28,8 @@ def setUp(self): self.request_tokens_mock.reset_mock() self.projects_mock = identity_client.projects self.projects_mock.reset_mock() + self.roles_mock = identity_client.roles + self.roles_mock.reset_mock() class TestRequestTokenCreate(TestOAuth1): @@ -85,6 +87,12 @@ class TestRequestTokenAuthorize(TestOAuth1): def setUp(self): super(TestRequestTokenAuthorize, self).setUp() + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + copied_verifier = copy.deepcopy(identity_fakes.OAUTH_VERIFIER) resource = fakes.FakeResource(None, copied_verifier, loaded=True) self.request_tokens_mock.authorize.return_value = resource @@ -93,11 +101,11 @@ def setUp(self): def test_authorize_request_tokens(self): arglist = [ '--request-key', identity_fakes.request_token_id, - '--role-ids', identity_fakes.role_id, + '--role', identity_fakes.role_name, ] verifylist = [ ('request_key', identity_fakes.request_token_id), - ('role_ids', identity_fakes.role_id), + ('role', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) From d2943d2592b2de98a59c44445c5cd2cc564740da Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:22:31 -0500 Subject: [PATCH 0136/3303] Command doc: access token Change-Id: I1b7103e28273f0a63c7d6b6003317b9e69702b05 --- doc/source/command-objects/access-token.rst | 42 +++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/token.py | 16 ++++---- 3 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 doc/source/command-objects/access-token.rst diff --git a/doc/source/command-objects/access-token.rst b/doc/source/command-objects/access-token.rst new file mode 100644 index 0000000000..fd22e761f8 --- /dev/null +++ b/doc/source/command-objects/access-token.rst @@ -0,0 +1,42 @@ +============ +access token +============ + +Identity v3 + +`Requires: OS-OAUTH1 extension` + +access token create +------------------- + +Create an access token + +.. program:: access token create +.. code:: bash + + os access token create + --consumer-key + --consumer-secret + --request-key + --request-secret + --verifier + +.. option:: --consumer-key + + Consumer key (required) + +.. option:: --consumer-secret + + Consumer secret (required) + +.. option:: --request-key + + Request token to exchange for access token (required) + +.. option:: --request-secret + + Secret associated with (required) + +.. option:: --verifier + + Verifier associated with (required) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index a4822c1996..3a7fdd231a 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -69,7 +69,7 @@ overlapping purposes there will be options to select which object to use, or the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. -* ``access token``: Identity - long-lived OAuth-based token +* ``access token``: (**Identity**) long-lived OAuth-based token * ``availability zone``: (**Compute**) a logical partition of hosts or volume services * ``aggregate``: (**Compute**) a grouping of servers * ``backup``: (**Volume**) a volume copy diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index bea2ddeb80..7000b62ce0 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -69,7 +69,7 @@ def take_action(self, parsed_args): class CreateAccessToken(show.ShowOne): - """Create access token""" + """Create an access token""" log = logging.getLogger(__name__ + '.CreateAccessToken') @@ -78,31 +78,31 @@ def get_parser(self, prog_name): parser.add_argument( '--consumer-key', metavar='', - help='Consumer key', + help='Consumer key (required)', required=True ) parser.add_argument( '--consumer-secret', metavar='', - help='Consumer secret', + help='Consumer secret (required)', required=True ) parser.add_argument( '--request-key', metavar='', - help='Request token key', + help='Request token to exchange for access token (required)', required=True ) parser.add_argument( '--request-secret', metavar='', - help='Request token secret', + help='Secret associated with (required)', required=True ) parser.add_argument( '--verifier', metavar='', - help='Verifier Pin', + help='Verifier associated with (required)', required=True ) return parser @@ -114,9 +114,7 @@ def take_action(self, parsed_args): parsed_args.consumer_key, parsed_args.consumer_secret, parsed_args.request_key, parsed_args.request_secret, parsed_args.verifier) - info = {} - info.update(access_token._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(access_token._info))) class CreateRequestToken(show.ShowOne): From 80499dc5a196ebc1ecffd56d2a776c9efc401998 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 9 Jan 2015 18:35:54 +0000 Subject: [PATCH 0137/3303] Updated from global requirements Change-Id: Iad84313636ee2f53777cdf05d60a322f7a252f27 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 35d2396d31..80b2599d29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ Babel>=1.3 cliff>=1.7.0 # Apache-2.0 cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 -oslo.utils>=1.1.0 # Apache-2.0 +oslo.utils>=1.2.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 python-glanceclient>=0.15.0 python-keystoneclient>=0.11.1 From 3b99c178949bc0864f927ac610a12fc666537162 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 19:12:18 -0500 Subject: [PATCH 0138/3303] Add versioning to the docs that missed it Change-Id: I8cb90e0d5aca58c4992271e007af91f8cf782643 --- doc/source/command-objects/aggregate.rst | 2 ++ doc/source/command-objects/console-log.rst | 2 ++ doc/source/command-objects/console-url.rst | 2 ++ doc/source/command-objects/credentials.rst | 2 ++ doc/source/command-objects/flavor.rst | 2 ++ doc/source/command-objects/keypair.rst | 2 ++ doc/source/command-objects/limits.rst | 2 ++ doc/source/command-objects/quota.rst | 2 ++ doc/source/command-objects/server-image.rst | 2 ++ doc/source/command-objects/server.rst | 1 + doc/source/command-objects/user-role.rst | 2 ++ 11 files changed, 21 insertions(+) diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst index 474d811f88..6497f63274 100644 --- a/doc/source/command-objects/aggregate.rst +++ b/doc/source/command-objects/aggregate.rst @@ -5,6 +5,8 @@ aggregate Server aggregates provide a mechanism to group servers according to certain criteria. +Compute v2 + aggregate add host ------------------ diff --git a/doc/source/command-objects/console-log.rst b/doc/source/command-objects/console-log.rst index 8e56a07382..9eafb61ae2 100644 --- a/doc/source/command-objects/console-log.rst +++ b/doc/source/command-objects/console-log.rst @@ -4,6 +4,8 @@ console log Server console text dump +Compute v2 + console log show ---------------- diff --git a/doc/source/command-objects/console-url.rst b/doc/source/command-objects/console-url.rst index 45a0a52717..9cab87981e 100644 --- a/doc/source/command-objects/console-url.rst +++ b/doc/source/command-objects/console-url.rst @@ -4,6 +4,8 @@ console url Server remote console URL +Compute v2 + console url show ---------------- diff --git a/doc/source/command-objects/credentials.rst b/doc/source/command-objects/credentials.rst index ea8fc08fff..9f4aabe4f3 100644 --- a/doc/source/command-objects/credentials.rst +++ b/doc/source/command-objects/credentials.rst @@ -2,6 +2,8 @@ credentials =========== +Identity v3 + credentials create ------------------ diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 4c98e85889..31b8e90227 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -2,6 +2,8 @@ flavor ====== +Compute v2 + flavor create ------------- diff --git a/doc/source/command-objects/keypair.rst b/doc/source/command-objects/keypair.rst index 9ba0ee8f04..89d12a30ea 100644 --- a/doc/source/command-objects/keypair.rst +++ b/doc/source/command-objects/keypair.rst @@ -5,6 +5,8 @@ keypair The badly named keypair is really the public key of an OpenSSH key pair to be used for access to created servers. +Compute v2 + keypair create -------------- diff --git a/doc/source/command-objects/limits.rst b/doc/source/command-objects/limits.rst index ac388e0f56..1eae4889f5 100644 --- a/doc/source/command-objects/limits.rst +++ b/doc/source/command-objects/limits.rst @@ -4,6 +4,8 @@ limits The Compute and Volume APIs have resource usage limits. +Compute v2, Volume v1 + limits show ----------- diff --git a/doc/source/command-objects/quota.rst b/doc/source/command-objects/quota.rst index ba6712c083..053fb47acc 100644 --- a/doc/source/command-objects/quota.rst +++ b/doc/source/command-objects/quota.rst @@ -4,6 +4,8 @@ quota Resource quotas appear in multiple APIs, OpenStackClient presents them as a single object with multiple properties. +Compute v2, Volume v1 + quota set --------- diff --git a/doc/source/command-objects/server-image.rst b/doc/source/command-objects/server-image.rst index 4577b25b20..8b48934298 100644 --- a/doc/source/command-objects/server-image.rst +++ b/doc/source/command-objects/server-image.rst @@ -5,6 +5,8 @@ server image A server image is a disk image created from a running server instance. The image is created in the Image store. +Compute v2 + server image create ------------------- diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 48cefe6ada..360ec24edf 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -2,6 +2,7 @@ server ====== +Compute v2 server add security group ------------------------- diff --git a/doc/source/command-objects/user-role.rst b/doc/source/command-objects/user-role.rst index a25e90ff8f..8283f91110 100644 --- a/doc/source/command-objects/user-role.rst +++ b/doc/source/command-objects/user-role.rst @@ -2,6 +2,8 @@ user role ========= +Identity v2 + user role list -------------- From ffb783215986318aded3eef0d97b603062f7c7ee Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 19:58:17 -0500 Subject: [PATCH 0139/3303] Rework role list v2 for --user and --project `os user role list` does the same as v3's `os role list`. We should rework v2's `role list` to basically call `os user role list` under the covers. Closes-Bug: #1409179 Change-Id: I9839f6be139d6a6a3f6bbf79957e218dd8e03fe3 --- openstackclient/identity/v2_0/role.py | 69 ++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index d03664e07b..295de07e7f 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -146,10 +146,75 @@ class ListRole(lister.Lister): log = logging.getLogger(__name__ + '.ListRole') + def get_parser(self, prog_name): + parser = super(ListRole, self).get_parser(prog_name) + parser.add_argument( + '--project', + metavar='', + help='Filter roles by (name or ID)', + ) + parser.add_argument( + '--user', + metavar='', + help='Filter roles by (name or ID)', + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - columns = ('ID', 'Name') - data = self.app.client_manager.identity.roles.list() + identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref + + # No user or project specified, list all roles in the system + if not parsed_args.user and not parsed_args.project: + columns = ('ID', 'Name') + data = identity_client.roles.list() + elif parsed_args.user and parsed_args.project: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + data = identity_client.roles.roles_for_user(user.id, project.id) + + elif parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + if self.app.client_manager.auth_ref: + project = utils.find_resource( + identity_client.projects, + auth_ref.project_id + ) + else: + msg = _("Project must be specified") + raise exceptions.CommandError(msg) + data = identity_client.roles.roles_for_user(user.id, project.id) + elif parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + if self.app.client_manager.auth_ref: + user = utils.find_resource( + identity_client.users, + auth_ref.user_id + ) + else: + msg = _("User must be specified") + raise exceptions.CommandError(msg) + data = identity_client.roles.roles_for_user(user.id, project.id) + + if parsed_args.user or parsed_args.project: + columns = ('ID', 'Name', 'Project', 'User') + for user_role in data: + user_role.user = user.name + user_role.project = project.name + return (columns, (utils.get_item_properties( s, columns, From a8f60a8aa1c8cbbf0d1384131854a422705e7c78 Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 12 Jan 2015 12:08:43 +0800 Subject: [PATCH 0140/3303] fix some small issues in catalog show I think there are three issues we should fix: 1. wrong indentation of 'continue' 2. currently, name is optional for service, but according to the currrent logic, if a service doesn't have name attribute we will select it anyway 3. we always loop all catalogs Change-Id: I9fce66677affa396b6a12afea76e87cab9215a58 --- openstackclient/identity/v2_0/catalog.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 1a96fdf60f..dfc99b47f3 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -83,17 +83,12 @@ def take_action(self, parsed_args): data = None for service in sc.get_data(): - if ( - 'name' in service and - service['name'] != parsed_args.service and - 'type' in service and - service['type'] != parsed_args.service - ): - continue - - data = service - data['endpoints'] = _format_endpoints(data['endpoints']) - if 'endpoints_links' in data: - data.pop('endpoints_links') + if (service.get('name') == parsed_args.service or + service.get('type') == parsed_args.service): + data = service + data['endpoints'] = _format_endpoints(data['endpoints']) + if 'endpoints_links' in data: + data.pop('endpoints_links') + break return zip(*sorted(six.iteritems(data))) From 6ebbd278cfcedc77402b66481762a2217953fa6e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 10:03:28 -0600 Subject: [PATCH 0141/3303] Command docs: add service Co-Authored-By: Lin Hua Cheng Change-Id: Icd39e6d769fd4c4797fcf4ef9eb97c71ed166b3b Closes-Bug: #1404434 --- doc/source/command-objects/service.rst | 144 +++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v2_0/service.py | 6 +- openstackclient/identity/v3/service.py | 32 +++-- 4 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 doc/source/command-objects/service.rst diff --git a/doc/source/command-objects/service.rst b/doc/source/command-objects/service.rst new file mode 100644 index 0000000000..d4f63de883 --- /dev/null +++ b/doc/source/command-objects/service.rst @@ -0,0 +1,144 @@ +======= +service +======= + +Identity v2, v3 + +service create +-------------- + +Create new service + +.. program:: service create +.. code-block:: bash + + os service create + [--name ] + [--description ] + [--enable | --disable] + + +.. option:: --name + + New service name + +.. option:: --description + + New service description + +.. option:: --enable + + Enable service (default) + + *Identity version 3 only* + +.. option:: --disable + + Disable service + + *Identity version 3 only* + +.. _service_create-type: +.. describe:: + + New service type (compute, image, identity, volume, etc) + +service delete +-------------- + +Delete service + +.. program:: service delete +.. code-block:: bash + + os service delete + + +:option:`` + Service to delete (type, name or ID) + +service list +------------ + +List services + +.. program:: service list +.. code-block:: bash + + os service list + [--long] + +.. option:: --long + + List additional fields in output + + *Identity version 2 only* + +Returns service fields ID and Name, `--long` adds Type and Description +to the output. When Identity API version 3 is selected all columns are +always displayed, `--long` is silently accepted for backward-compatibility. + +service set +----------- + +Set service properties + +* Identity version 3 only* + +.. program:: service set +.. code-block:: bash + + os service set + [--type ] + [--name ] + [--description ] + [--enable | --disable] + + +.. option:: --type + + New service type (compute, image, identity, volume, etc) + +.. option:: --name + + New service name + +.. option:: --description + + New service description + +.. option:: --enable + + Enable service + +.. option:: --disable + + Disable service + +.. _service_set-service: +.. describe:: + + Service to update (type, name or ID) + +service show +------------ + +Display service details + +.. program:: service show +.. code-block:: bash + + os service show + [--catalog] + + +.. option:: --catalog + + Show service catalog information + + *Identity version 2 only* + +.. _service_show-service: +.. describe:: + + Service to display (type, name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 3a7fdd231a..6fe91a1f76 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -109,7 +109,7 @@ referring to both Compute and Volume quotas. * ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access * ``server``: (**Compute**) virtual machine instance * ``server image``: (**Compute**) saved server disk image -* ``service``: Identity - a cloud service +* ``service``: (**Identity**) a cloud service * ``snapshot``: (**Volume**) a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service * ``usage``: (**Compute**) display host resources being consumed diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 0b98a90360..208f7fbcee 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -44,7 +44,7 @@ def get_parser(self, prog_name): type_or_name_group = parser.add_mutually_exclusive_group() type_or_name_group.add_argument( '--type', - metavar='', + metavar='', help=argparse.SUPPRESS, ) type_or_name_group.add_argument( @@ -54,7 +54,7 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar='', + metavar='', help=_('New service description'), ) return parser @@ -144,7 +144,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index f4c5d42629..126292538c 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -15,6 +15,7 @@ """Identity v3 Service action implementations""" +import argparse import logging import six @@ -35,12 +36,12 @@ def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) parser.add_argument( 'type', - metavar='', + metavar='', help='New service type (compute, image, identity, volume, etc)', ) parser.add_argument( '--name', - metavar='', + metavar='', help='New service name', ) parser.add_argument( @@ -52,12 +53,12 @@ def get_parser(self, prog_name): enable_group.add_argument( '--enable', action='store_true', - help='Enable project', + help='Enable service (default)', ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help='Disable service', ) return parser @@ -90,7 +91,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to delete (name or ID)', + help='Service to delete (type or ID)', ) return parser @@ -109,6 +110,17 @@ class ListService(lister.Lister): log = logging.getLogger(__name__ + '.ListService') + def get_parser(self, prog_name): + """The --long option is here for compatibility only.""" + parser = super(ListService, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=argparse.SUPPRESS, + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) @@ -131,11 +143,11 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to update (name or ID)', + help='Service to update (type, name or ID)', ) parser.add_argument( '--type', - metavar='', + metavar='', help='New service type (compute, image, identity, volume, etc)', ) parser.add_argument( @@ -152,12 +164,12 @@ def get_parser(self, prog_name): enable_group.add_argument( '--enable', action='store_true', - help='Enable project', + help='Enable service', ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help='Disable service', ) return parser @@ -194,7 +206,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') From b17c475f8a3c5dee7b9ef86e73620b9f819a8942 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 12 Jan 2015 15:22:39 -0500 Subject: [PATCH 0142/3303] Upgrade hacking to 0.10 Also resolve the only error that was produced. Change-Id: Ic81ab01aa0cddc15bb27419d7fec3e5a6d4ec0c7 --- openstackclient/tests/fakes.py | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 93d4294d3d..e25751d643 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -37,7 +37,7 @@ TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) -class FakeStdout: +class FakeStdout(object): def __init__(self): self.content = [] diff --git a/test-requirements.txt b/test-requirements.txt index ac3e715093..e897e60152 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=0.9.1,<0.10 +hacking>=0.10.0,<0.11 coverage>=3.6 discover From fa84566dac0b16bb02438521b0787c495ff8db6b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 13 Jan 2015 00:15:54 +0000 Subject: [PATCH 0143/3303] Updated from global requirements Change-Id: I4be717979bd4371bc544312d556934aef4f3a629 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 80b2599d29..d73bf06567 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ cliff>=1.7.0 # Apache-2.0 cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.2.0 # Apache-2.0 -oslo.serialization>=1.0.0 # Apache-2.0 +oslo.serialization>=1.2.0 # Apache-2.0 python-glanceclient>=0.15.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 From 673e0d88ffc299230823204793eb4b4a18a37530 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sat, 10 Jan 2015 02:43:20 -0500 Subject: [PATCH 0144/3303] Command doc: policy Also tweaked a bunch of the code to not show 'blob', but 'rules' instead. Change-Id: I6db798d272ff416a77f169c0e912d2096fa02504 --- doc/source/command-objects/policy.rst | 95 +++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/policy.py | 63 ++++++++++-------- 3 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 doc/source/command-objects/policy.rst diff --git a/doc/source/command-objects/policy.rst b/doc/source/command-objects/policy.rst new file mode 100644 index 0000000000..195a89f25e --- /dev/null +++ b/doc/source/command-objects/policy.rst @@ -0,0 +1,95 @@ +====== +policy +====== + +Identity v3 + +policy create +------------- + +Create new policy + +.. program:: policy create +.. code:: bash + + os policy create + [--type ] + + +.. option:: --type + + New MIME type of the policy rules file (defaults to application/json) + +.. describe:: + + New serialized policy rules file + +policy delete +------------- + +Delete policy + +.. program:: policy delete +.. code:: bash + + os policy delete + + +.. describe:: + + Policy to delete + +policy list +----------- + +List policies + +.. program:: policy list +.. code:: bash + + os policy list + [--long] + +.. option:: --long + + List additional fields in output + +policy set +---------- + +Set policy properties + +.. program:: policy set +.. code:: bash + + os policy set + [--type ] + [--rules ] + + +.. option:: --type + + New MIME type of the policy rules file + +.. describe:: --rules + + New serialized policy rules file + +.. describe:: + + Policy to modify + +policy show +----------- + +Display policy details + +.. program:: policy show +.. code:: bash + + os policy show + + +.. describe:: + + Policy to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 6fe91a1f76..6dbaf11720 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -98,7 +98,7 @@ referring to both Compute and Volume quotas. * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: (**Object Store**) a single file in the Object Store -* ``policy``: Identity - determines authorization +* ``policy``: (**Identity**) determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``region``: (**Identity**) a subset of an OpenStack deployment diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 802880bfc2..8293542317 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -27,7 +27,7 @@ class CreatePolicy(show.ShowOne): - """Create policy command""" + """Create new policy""" log = logging.getLogger(__name__ + '.CreatePolicy') @@ -35,20 +35,21 @@ def get_parser(self, prog_name): parser = super(CreatePolicy, self).get_parser(prog_name) parser.add_argument( '--type', - metavar='', + metavar='', default="application/json", - help='New MIME type of the policy blob - i.e.: application/json', + help='New MIME type of the policy rules file ' + '(defaults to application/json)', ) parser.add_argument( - 'blob_file', - metavar='', - help='New policy rule set itself, as a serialized blob, in a file', + 'rules', + metavar='', + help='New serialized policy rules file', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - blob = utils.read_blob_file_contents(parsed_args.blob_file) + blob = utils.read_blob_file_contents(parsed_args.rules) identity_client = self.app.client_manager.identity policy = identity_client.policies.create( @@ -56,11 +57,12 @@ def take_action(self, parsed_args): ) policy._info.pop('links') + policy._info.update({'rules': policy._info.pop('blob')}) return zip(*sorted(six.iteritems(policy._info))) class DeletePolicy(command.Command): - """Delete policy command""" + """Delete policy""" log = logging.getLogger(__name__ + '.DeletePolicy') @@ -68,8 +70,8 @@ def get_parser(self, prog_name): parser = super(DeletePolicy, self).get_parser(prog_name) parser.add_argument( 'policy', - metavar='', - help='ID of policy to delete', + metavar='', + help='Policy to delete', ) return parser @@ -81,28 +83,30 @@ def take_action(self, parsed_args): class ListPolicy(lister.Lister): - """List policy command""" + """List policies""" log = logging.getLogger(__name__ + '.ListPolicy') def get_parser(self, prog_name): parser = super(ListPolicy, self).get_parser(prog_name) parser.add_argument( - '--include-blob', + '--long', action='store_true', default=False, - help='Additional fields are listed in output', + help='List additional fields in output', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - if parsed_args.include_blob: + if parsed_args.long: columns = ('ID', 'Type', 'Blob') + column_headers = ('ID', 'Type', 'Rules') else: columns = ('ID', 'Type') + column_headers = columns data = self.app.client_manager.identity.policies.list() - return (columns, + return (column_headers, (utils.get_item_properties( s, columns, formatters={}, @@ -110,7 +114,7 @@ def take_action(self, parsed_args): class SetPolicy(command.Command): - """Set policy command""" + """Set policy properties""" log = logging.getLogger(__name__ + '.SetPolicy') @@ -118,18 +122,18 @@ def get_parser(self, prog_name): parser = super(SetPolicy, self).get_parser(prog_name) parser.add_argument( 'policy', - metavar='', - help='ID of policy to change', + metavar='', + help='Policy to modify', ) parser.add_argument( '--type', - metavar='', - help='New MIME Type of the policy blob - i.e.: application/json', + metavar='', + help='New MIME type of the policy rules file', ) parser.add_argument( - '--blob-file', - metavar='', - help='New policy rule set itself, as a serialized blob, in a file', + '--rules', + metavar='', + help='New serialized policy rules file', ) return parser @@ -138,8 +142,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity blob = None - if parsed_args.blob_file: - blob = utils.read_blob_file_contents(parsed_args.blob_file) + if parsed_args.rules: + blob = utils.read_blob_file_contents(parsed_args.rules) kwargs = {} if blob: @@ -148,14 +152,14 @@ def take_action(self, parsed_args): kwargs['type'] = parsed_args.type if not kwargs: - sys.stdout.write("Policy not updated, no arguments present \n") + sys.stdout.write('Policy not updated, no arguments present \n') return identity_client.policies.update(parsed_args.policy, **kwargs) return class ShowPolicy(show.ShowOne): - """Show policy command""" + """Display policy details""" log = logging.getLogger(__name__ + '.ShowPolicy') @@ -163,8 +167,8 @@ def get_parser(self, prog_name): parser = super(ShowPolicy, self).get_parser(prog_name) parser.add_argument( 'policy', - metavar='', - help='ID of policy to display', + metavar='', + help='Policy to display', ) return parser @@ -175,4 +179,5 @@ def take_action(self, parsed_args): parsed_args.policy) policy._info.pop('links') + policy._info.update({'rules': policy._info.pop('blob')}) return zip(*sorted(six.iteritems(policy._info))) From 019c155e9b308dab002f23064b969452bc3d7a89 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 19:13:03 -0500 Subject: [PATCH 0145/3303] Fine tune some of the helps commands try and add some consistency with the show and delete commands. replace 'show x' with 'display x' change 'delete a y' with just 'delete y' Change-Id: I47dfa8ee23ac5c41b355796415eb515155832f65 --- doc/source/command-objects/aggregate.rst | 4 ++-- doc/source/command-objects/catalog.rst | 4 ++++ doc/source/command-objects/consumer.rst | 2 +- doc/source/command-objects/container.rst | 2 +- doc/source/command-objects/domain.rst | 2 +- doc/source/command-objects/federation-protocol.rst | 2 +- doc/source/command-objects/flavor.rst | 2 +- doc/source/command-objects/identity-provider.rst | 2 +- doc/source/command-objects/keypair.rst | 6 +++--- doc/source/command-objects/mapping.rst | 2 +- doc/source/command-objects/object.rst | 2 +- doc/source/command-objects/project.rst | 4 +++- doc/source/command-objects/region.rst | 4 ++-- doc/source/command-objects/role.rst | 6 ++++-- doc/source/command-objects/user.rst | 4 +++- openstackclient/compute/v2/aggregate.py | 4 ++-- openstackclient/compute/v2/flavor.py | 2 +- openstackclient/compute/v2/keypair.py | 6 +++--- openstackclient/identity/v2_0/catalog.py | 2 +- openstackclient/identity/v2_0/project.py | 4 ++-- openstackclient/identity/v2_0/role.py | 8 ++++---- openstackclient/identity/v2_0/service.py | 2 +- openstackclient/identity/v2_0/user.py | 2 +- openstackclient/identity/v3/consumer.py | 2 +- openstackclient/identity/v3/domain.py | 2 +- openstackclient/identity/v3/federation_protocol.py | 2 +- openstackclient/identity/v3/identity_provider.py | 2 +- openstackclient/identity/v3/mapping.py | 2 +- openstackclient/identity/v3/project.py | 4 ++-- openstackclient/identity/v3/region.py | 4 ++-- openstackclient/identity/v3/role.py | 4 ++-- openstackclient/identity/v3/service.py | 2 +- openstackclient/identity/v3/user.py | 2 +- openstackclient/object/v1/container.py | 2 +- openstackclient/object/v1/object.py | 2 +- 35 files changed, 59 insertions(+), 49 deletions(-) diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst index 6497f63274..2a40d23440 100644 --- a/doc/source/command-objects/aggregate.rst +++ b/doc/source/command-objects/aggregate.rst @@ -139,7 +139,7 @@ Set aggregate properties aggregate show -------------- -Show a specific aggregate +Display aggregate details .. program aggregate show .. code:: bash @@ -149,4 +149,4 @@ Show a specific aggregate .. describe:: - Aggregate to show (name or ID) + Aggregate to display (name or ID) diff --git a/doc/source/command-objects/catalog.rst b/doc/source/command-objects/catalog.rst index 99746dd716..4e67f3a4e3 100644 --- a/doc/source/command-objects/catalog.rst +++ b/doc/source/command-objects/catalog.rst @@ -7,6 +7,8 @@ Identity v2 catalog list ------------ +List services in the service catalog + .. code:: bash os catalog list @@ -14,6 +16,8 @@ catalog list catalog show ------------ +Display service catalog details + .. code:: bash os catalog show diff --git a/doc/source/command-objects/consumer.rst b/doc/source/command-objects/consumer.rst index 59ace845e0..91294fa20b 100644 --- a/doc/source/command-objects/consumer.rst +++ b/doc/source/command-objects/consumer.rst @@ -24,7 +24,7 @@ Create new consumer consumer delete --------------- -Delete a consumer +Delete consumer .. program:: consumer delete .. code:: bash diff --git a/doc/source/command-objects/container.rst b/doc/source/command-objects/container.rst index 3afaeb9217..845933d4cd 100644 --- a/doc/source/command-objects/container.rst +++ b/doc/source/command-objects/container.rst @@ -92,7 +92,7 @@ Save container contents locally container show -------------- -Show container details +Display container details .. program:: container show .. code:: bash diff --git a/doc/source/command-objects/domain.rst b/doc/source/command-objects/domain.rst index 66697ac3ea..94473570de 100644 --- a/doc/source/command-objects/domain.rst +++ b/doc/source/command-objects/domain.rst @@ -102,7 +102,7 @@ Set domain properties domain show ----------- -Show domain details +Display domain details .. program:: domain show .. code:: bash diff --git a/doc/source/command-objects/federation-protocol.rst b/doc/source/command-objects/federation-protocol.rst index 0ed0980a54..5b4ea48ace 100644 --- a/doc/source/command-objects/federation-protocol.rst +++ b/doc/source/command-objects/federation-protocol.rst @@ -34,7 +34,7 @@ Create new federation protocol federation protocol delete -------------------------- -Delete a federation protocol +Delete federation protocol .. program:: federation protocol delete .. code:: bash diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 31b8e90227..0083da0dec 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -67,7 +67,7 @@ Create new flavor flavor delete ------------- -Delete a flavor +Delete flavor .. program:: flavor delete .. code:: bash diff --git a/doc/source/command-objects/identity-provider.rst b/doc/source/command-objects/identity-provider.rst index f08cde0188..47e274dd56 100644 --- a/doc/source/command-objects/identity-provider.rst +++ b/doc/source/command-objects/identity-provider.rst @@ -38,7 +38,7 @@ Create new identity provider identity provider delete ------------------------ -Delete an identity provider +Delete identity provider .. program:: identity provider delete .. code:: bash diff --git a/doc/source/command-objects/keypair.rst b/doc/source/command-objects/keypair.rst index 89d12a30ea..04c5721f91 100644 --- a/doc/source/command-objects/keypair.rst +++ b/doc/source/command-objects/keypair.rst @@ -30,7 +30,7 @@ Create new public key keypair delete -------------- -Delete a public key +Delete public key .. program keypair delete .. code:: bash @@ -55,7 +55,7 @@ List public key fingerprints keypair show ------------ -Show public key details +Display public key details .. program keypair show .. code:: bash @@ -70,4 +70,4 @@ Show public key details .. describe:: - Public key to show + Public key to display diff --git a/doc/source/command-objects/mapping.rst b/doc/source/command-objects/mapping.rst index 5c7535bd1c..25af474061 100644 --- a/doc/source/command-objects/mapping.rst +++ b/doc/source/command-objects/mapping.rst @@ -30,7 +30,7 @@ Create new mapping mapping delete -------------- -Delete a mapping +Delete mapping .. program:: mapping delete .. code:: bash diff --git a/doc/source/command-objects/object.rst b/doc/source/command-objects/object.rst index 5cbc95d71c..c45c10516f 100644 --- a/doc/source/command-objects/object.rst +++ b/doc/source/command-objects/object.rst @@ -122,7 +122,7 @@ Save object locally object show ----------- -Show object details +Display object details .. program:: object show .. code:: bash diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 6b55b424bf..b39edb4d6a 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -149,6 +149,8 @@ Set project properties project show ------------ +Display project details + .. program:: project show .. code:: bash @@ -165,4 +167,4 @@ project show .. _project_show-project: .. describe:: - Project to show (name or ID) + Project to display (name or ID) diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst index d1aedb3161..cb4a059eab 100644 --- a/doc/source/command-objects/region.rst +++ b/doc/source/command-objects/region.rst @@ -95,7 +95,7 @@ Set region properties .. _region_set-region-id: .. describe:: - Region ID to modify + Region to modify region show ----------- @@ -111,4 +111,4 @@ Display region details .. _region_show-region-id: .. describe:: - Region ID to display + Region to display diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 19195eb554..57161b06be 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -142,7 +142,7 @@ Remove role from domain/project : user/group .. describe:: - Role to remove from ``:`` (name or ID) + Role to remove (name or ID) role set -------- @@ -169,6 +169,8 @@ Set role properties role show --------- +Display role details + .. program:: role show .. code:: bash @@ -177,4 +179,4 @@ role show .. describe:: - Role to show (name or ID) + Role to display (name or ID) diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index a9a98fe18f..9c81a40326 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -191,6 +191,8 @@ Set user properties user show --------- +Display user details + .. program:: user show .. code:: bash @@ -207,4 +209,4 @@ user show .. _user_show-user: .. describe:: - User to show (name or ID) + User to display (name or ID) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index bfc2b115aa..84ed5c7d27 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -290,7 +290,7 @@ def take_action(self, parsed_args): class ShowAggregate(show.ShowOne): - """Show a specific aggregate""" + """Display aggregate details""" log = logging.getLogger(__name__ + '.ShowAggregate') @@ -299,7 +299,7 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Aggregate to show (name or ID)', + help='Aggregate to display (name or ID)', ) return parser diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 6f3788a029..bb89a85b5e 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -125,7 +125,7 @@ def take_action(self, parsed_args): class DeleteFlavor(command.Command): - """Delete a flavor""" + """Delete flavor""" log = logging.getLogger(__name__ + ".DeleteFlavor") diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 6a158d86ff..edf25f8358 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -80,7 +80,7 @@ def take_action(self, parsed_args): class DeleteKeypair(command.Command): - """Delete a public key""" + """Delete public key""" log = logging.getLogger(__name__ + '.DeleteKeypair') @@ -121,7 +121,7 @@ def take_action(self, parsed_args): class ShowKeypair(show.ShowOne): - """Show public key details""" + """Display public key details""" log = logging.getLogger(__name__ + '.ShowKeypair') @@ -130,7 +130,7 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='Public key to show', + help='Public key to display', ) parser.add_argument( '--public-key', diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 1a96fdf60f..330dcf3b13 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -59,7 +59,7 @@ def take_action(self, parsed_args): class ShowCatalog(show.ShowOne): - """Show service catalog details""" + """Display service catalog details""" log = logging.getLogger(__name__ + '.ShowCatalog') diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 9b195600fc..d01807a600 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -241,7 +241,7 @@ def take_action(self, parsed_args): class ShowProject(show.ShowOne): - """Show project details""" + """Display project details""" log = logging.getLogger(__name__ + '.ShowProject') @@ -250,7 +250,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Project to show (name or ID)')) + help=_('Project to display (name or ID)')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index d03664e07b..f5f901ee81 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -226,7 +226,7 @@ def take_action(self, parsed_args): class RemoveRole(command.Command): - """Remove role from project:user""" + """Remove role from project : user""" log = logging.getLogger(__name__ + '.RemoveRole') @@ -235,7 +235,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role to remove from : (name or ID)'), + help=_('Role to remove (name or ID)'), ) parser.add_argument( '--project', @@ -267,7 +267,7 @@ def take_action(self, parsed_args): class ShowRole(show.ShowOne): - """Show single role""" + """Display role details""" log = logging.getLogger(__name__ + '.ShowRole') @@ -276,7 +276,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role to show (name or ID)'), + help=_('Role to display (name or ID)'), ) return parser diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 0b98a90360..82d4fd7201 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -144,7 +144,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index b5bbce3bd3..500aeec32e 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -349,7 +349,7 @@ def take_action(self, parsed_args): class ShowUser(show.ShowOne): - """Show user details""" + """Display user details""" log = logging.getLogger(__name__ + '.ShowUser') diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index c5e263926c..ffbd5104cf 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -51,7 +51,7 @@ def take_action(self, parsed_args): class DeleteConsumer(command.Command): - """Delete a consumer""" + """Delete consumer""" log = logging.getLogger(__name__ + '.DeleteConsumer') diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 189f097052..38f99a9772 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -187,7 +187,7 @@ def take_action(self, parsed_args): class ShowDomain(show.ShowOne): - """Show domain details""" + """Display domain details""" log = logging.getLogger(__name__ + '.ShowDomain') diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 5a651165b1..57e8255e9c 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -69,7 +69,7 @@ def take_action(self, parsed_args): class DeleteProtocol(command.Command): - """Delete a federation protocol""" + """Delete federation protocol""" log = logging.getLogger(__name__ + '.DeleteProtocol') diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index f46341a16b..691446da4d 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -69,7 +69,7 @@ def take_action(self, parsed_args): class DeleteIdentityProvider(command.Command): - """Delete an identity provider""" + """Delete identity provider""" log = logging.getLogger(__name__ + '.DeleteIdentityProvider') diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index a1f60438f6..c79331ec7d 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -112,7 +112,7 @@ def take_action(self, parsed_args): class DeleteMapping(command.Command): - """Delete a mapping""" + """Delete mapping""" log = logging.getLogger(__name__ + '.DeleteMapping') diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 28eb427716..9a54e5cd08 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -298,7 +298,7 @@ def take_action(self, parsed_args): class ShowProject(show.ShowOne): - """Show project command""" + """Display project details""" log = logging.getLogger(__name__ + '.ShowProject') @@ -307,7 +307,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Name or ID of project to display', + help='Project to display (name or ID)', ) parser.add_argument( '--domain', diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index 5fb7391362..5cb51fc5ec 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -138,7 +138,7 @@ def get_parser(self, prog_name): parser.add_argument( 'region', metavar='', - help=_('Region ID to modify'), + help=_('Region to modify'), ) parser.add_argument( '--parent-region', @@ -188,7 +188,7 @@ def get_parser(self, prog_name): parser.add_argument( 'region', metavar='', - help=_('Region ID to display'), + help=_('Region to display'), ) return parser diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index d680278eea..0376070907 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -467,7 +467,7 @@ def take_action(self, parsed_args): class ShowRole(show.ShowOne): - """Show single role""" + """Display role details""" log = logging.getLogger(__name__ + '.ShowRole') @@ -476,7 +476,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Role to show (name or ID)', + help='Role to display (name or ID)', ) return parser diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index f4c5d42629..c6dfa8d089 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -194,7 +194,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 4fb7b6d1e0..7b847eedb8 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -422,7 +422,7 @@ def take_action(self, parsed_args): class ShowUser(show.ShowOne): - """Show user details""" + """Display user details""" log = logging.getLogger(__name__ + '.ShowUser') diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index b75888e408..bc4fdec81b 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -179,7 +179,7 @@ def take_action(self, parsed_args): class ShowContainer(show.ShowOne): - """Show container details""" + """Display container details""" log = logging.getLogger(__name__ + '.ShowContainer') diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 781dd047fa..752d78423e 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -222,7 +222,7 @@ def take_action(self, parsed_args): class ShowObject(show.ShowOne): - """Show object details""" + """Display object details""" log = logging.getLogger(__name__ + '.ShowObject') From c04b49ef07defdecadcf614d9ff4cde4b3dd029b Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 23:44:25 -0500 Subject: [PATCH 0146/3303] Tweaks to the catalog doc and show command Looks like providing a service id isn't working, so it the help message was reduced to just type and name. Added a bit more to the docs, too. Change-Id: Id7f8b48bdf99773ad55ca7f204f3c779f84633d5 --- doc/source/command-objects/catalog.rst | 6 ++++++ openstackclient/identity/v2_0/catalog.py | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/command-objects/catalog.rst b/doc/source/command-objects/catalog.rst index 4e67f3a4e3..89db95d5d4 100644 --- a/doc/source/command-objects/catalog.rst +++ b/doc/source/command-objects/catalog.rst @@ -9,6 +9,7 @@ catalog list List services in the service catalog +.. program:: catalog list .. code:: bash os catalog list @@ -18,7 +19,12 @@ catalog show Display service catalog details +.. program:: catalog show .. code:: bash os catalog show + +.. describe:: + + Service to display (type or name) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 330dcf3b13..3aeca91827 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -68,7 +68,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help=_('Service to display (type, name or ID)'), + help=_('Service to display (type or name)'), ) return parser @@ -96,4 +96,9 @@ def take_action(self, parsed_args): if 'endpoints_links' in data: data.pop('endpoints_links') + if not data: + self.app.log.error('service %s not found\n' % + parsed_args.service) + return ([], []) + return zip(*sorted(six.iteritems(data))) From 37eda86099d9213cb43a19f3778dc4ca54a84f10 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 13 Jan 2015 17:03:01 -0600 Subject: [PATCH 0147/3303] Copy HIG from wiki This is a basic port of the Human Interface Guide from the OpenStack wiki to our docs. Content changes for updating, etc, will follow in a separate review. Change-Id: Id031cd6a27e045b249e16d00e41be24c55fb3c29 --- doc/source/humaninterfaceguide.rst | 360 +++++++++++++++++++++++++++++ doc/source/index.rst | 1 + 2 files changed, 361 insertions(+) create mode 100644 doc/source/humaninterfaceguide.rst diff --git a/doc/source/humaninterfaceguide.rst b/doc/source/humaninterfaceguide.rst new file mode 100644 index 0000000000..371e4f7093 --- /dev/null +++ b/doc/source/humaninterfaceguide.rst @@ -0,0 +1,360 @@ +===================== +Human Interface Guide +===================== + +*Note: This page covers the OpenStackClient CLI only but looks familiar +because it was derived from the Horizon HIG.* + +Overview +======== + +What is a HIG? +The Human Interface Guidelines document was created for OpenStack developers +in order to direct the creation of new OpenStackClient command interfaces. + +Personas +======== + +Personas are archetypal users of the system. Keep these types of users in +mind when designing the interface. + +Alice the admin +--------------- + +Alice is an administrator who is responsible for maintaining the OpenStack +cloud installation. She has many years of experience with Linux systems +administration. + +Darren the deployer +------------------- + +Darren is responsible for doing the initial OpenStack deployment on the +host machines. + +Emile the end-user +------------------ + +Emile uses the cloud to do software development inside of the virtual +machines. She uses the command-line tools because she finds it quicker +than using the dashboard. + +Principles +========== + +The principles established in this section define the high-level priorities +to be used when designing and evaluating interactions for the OpenStack +command line interface. Principles are broad in scope and can be considered +the philosophical foundation for the OpenStack experience; while they may +not describe the tactical implementation of design, they should be used +when deciding between multiple courses of design. + +A significant theme for designing for the OpenStack experience concerns +focusing on common uses of the system rather than adding complexity to support +functionality that is rarely used. + +Consistency +=========== + +Consistency between OpenStack experiences will ensure that the command line +interface feels like a single experience instead of a jumble of disparate +products. Fractured experiences only serve to undermine user expectations +about how they should interact with the system, creating an unreliable user +experience. To avoid this, each interaction and visual representation within +the system must be used uniformly and predictably. The architecture and elements +detailed in this document will provide a strong foundation for establishing a +consistent experience. + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* Do the command actions adhere to a consistent application of actions? +* Has a new type of command subject or output been introduced? +* Does the design use command elements (options and arguments) as defined? + (See Core Elements.) +* Can any newly proposed command elements (actions or subjects) be accomplished + with existing elemetns? + +* Does the design adhere to the structural model of the core experience? + (See Core Architecture.) +* Are any data objects displayed or manipulated in a way contradictory to how + they are handled elsewhere in the core experience? + +Simplicity +---------- + +To best support new users and create straight forward interactions, designs +should be as simple as possible. When crafting new commands, designs should +minimize the amount of noise present in output: large amounts of +nonessential data, overabundance of possible actions, etc. Designs should +focus on the intent of the command, requiring only the necessary components +and either removing superfluous elements or making +them accessible through optional arguments. An example of this principle occurs +in OpenStack’s use of tables: only the most often used columns are shown by +default. Further data may be accessed through the output control options, +allowing users to specify the types of data that they find useful in their +day-to-day work. + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* Can options be used to combine otherwise similar commands? + +* How many of the displayed elements are relevant to the majority of users? +* If multiple actions are required for the user to complete a task, is each + step required or can the process be more efficient? + +User-Centered Design +-------------------- + +Commands should be design based on how a user will interact with the system +and not how the system’s backend is organized. While database structures and +APIs may define what is possible, they often do not define good user +experience; consider user goals and the way in which users will want to +interact with their data, then design for these work flows and mold the +interface to the user, not the user to the interface. + +Commands should be discoverable via the interface itself. + +To determine a list of available commands, use the :code:`-h` or +:code:`--help` options: + +.. code-block:: bash + + $ openstack --help + +For help with an individual command, use the :code:`help` command: + +.. code-block:: bash + + $ openstack help server create + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* How quickly can a user figure out how to accomplish a given task? +* Has content been grouped and ordered according to usage relationships? +* Do work flows support user goals or add complexity? + +Transparency +------------ + +Make sure users understand the current state of their infrastructure and +interactions. For example, users should be able to access information about +the state of each machine/virtual machine easily, without having to actively +seek out this information. Whenever the user initiates an action, make sure +a confirmation is displayed[1] to show that an input has been received. Upon +completion of a process, make sure the user is informed. Ensure that the user +never questions the state of their environment. + +[1] This goes against the common UNIX philosophy of only reporting error +conditions and output that is specifically requested. + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* Does the user receive feedback when initiating a process? +* When a process is completed? +* Does the user have quick access to the state of their infrastructure? + + +Architecture +============ + +Command Structure +----------------- + +OpenStackClient has a consistent and predictable format for all of its commands. + +* The top level command name is :code:`openstack` +* Sub-commands take the form: + +.. code-block:: bash + + openstack [] [] [] + +Subcommands shall have three distinct parts to its commands (in order that they appear): + +* global options +* command object(s) and action +* command options and arguments + +Output formats: + +* user-friendly tables with headers, etc +* machine-parsable delimited + +Notes: + +* All long options names shall begin with two dashes ('--') and use a single dash + ('-') internally between words (:code:`--like-this`). Underscores ('_') shall not + be used in option names. +* Authentication options conform to the common CLI authentication guidelines in + :doc:`authentication`. + +Global Options +~~~~~~~~~~~~~~ + +Global options are global in the sense that they apply to every command +invocation regardless of action to be performed. They include authentication +credentials and API version selection. Most global options have a corresponding +environment variable that may also be used to set the value. If both are present, +the command-line option takes priority. The environment variable names are derived +from the option name by dropping the leading dashes ('--'), converting each embedded +dash ('-') to an underscore ('_'), and converting to upper case. + +For example, :code:`--os-username` can be set from the environment via +:code:`OS_USERNAME`. + +--help +++++++ + +The standard :code:`--help` global option displays the documentation for invoking +the program and a list of the available commands on standard output. All other +options and commands are ignored when this is present. The traditional short +form help option (:code:`-h`) is also available. + +--version ++++++++++ + +The standard :code:`--version` option displays the name and version on standard +output. All other options and commands are ignored when this is present. + +Command Object(s) and Action +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Commands consist of an object described by one or more words followed by an action. Commands that require two objects have the primary object ahead of the action and the secondary object after the action. Any positional arguments identifying the objects shall appear in the same order as the objects. In badly formed English it is expressed as "(Take) object1 (and perform) action (using) object2 (to it)." + + [] + +Examples: + +* :code:`group add user ` +* :code:`volume type list` # Note that :code:`volume type` is a two-word + single object + +The :code:`help` command is unique as it appears in front of a normal command +and displays the help text for that command rather than execute it. + +Object names are always specified in command in their singular form. This is +contrary to natural language use. + +Command Arguments and Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each command may have its own set of options distinct from the global options. +They follow the same style as the global options and always appear between +the command and any positional arguments the command requires. + +Option Forms +++++++++++++ + +* **boolean**: boolean options shall use a form of :code:`--|--` + (preferred) or :code:`--