From 4a5f614947151299a5b51f8b3dd3e4f7c47d324d Mon Sep 17 00:00:00 2001 From: Steve McLellan Date: Wed, 21 May 2014 18:01:57 -0500 Subject: [PATCH] Extend CLI functionality Adds some CLI functionality to murano CLI - environment show, create, delete, rename - deployment list - category list - package list, download, import, show, delete Updates openstack-common, adds jsonutils and timeutils Partially implements: blueprint murano-cli-client Change-Id: I66215f56bcf8eba3329803650e70437175c49376 --- muranoclient/common/utils.py | 11 + .../openstack/common/apiclient/base.py | 11 +- .../openstack/common/apiclient/exceptions.py | 77 ++++--- muranoclient/openstack/common/gettextutils.py | 194 ++++++++++------ muranoclient/openstack/common/jsonutils.py | 184 +++++++++++++++ muranoclient/openstack/common/strutils.py | 16 +- muranoclient/openstack/common/timeutils.py | 210 ++++++++++++++++++ muranoclient/shell.py | 16 +- muranoclient/v1/services.py | 8 + muranoclient/v1/shell.py | 151 +++++++++++++ openstack-common.conf | 2 +- run_tests.sh | 88 ++++++-- 12 files changed, 828 insertions(+), 140 deletions(-) create mode 100644 muranoclient/openstack/common/jsonutils.py create mode 100644 muranoclient/openstack/common/timeutils.py diff --git a/muranoclient/common/utils.py b/muranoclient/common/utils.py index 4aa943f8..abdbfe42 100644 --- a/muranoclient/common/utils.py +++ b/muranoclient/common/utils.py @@ -15,13 +15,16 @@ from __future__ import print_function + import os import prettytable import sys +import textwrap import uuid from muranoclient.common import exceptions from muranoclient.openstack.common import importutils +from muranoclient.openstack.common import jsonutils from muranoclient.openstack.common import strutils @@ -35,6 +38,14 @@ def arg(*args, **kwargs): return _decorator +def json_formatter(js): + return jsonutils.dumps(js, indent=2) + + +def text_wrap_formatter(d): + return '\n'.join(textwrap.wrap(d or '', 55)) + + def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) diff --git a/muranoclient/openstack/common/apiclient/base.py b/muranoclient/openstack/common/apiclient/base.py index 873cc97b..396f5770 100644 --- a/muranoclient/openstack/common/apiclient/base.py +++ b/muranoclient/openstack/common/apiclient/base.py @@ -30,6 +30,7 @@ import six from six.moves.urllib import parse from muranoclient.openstack.common.apiclient import exceptions +from muranoclient.openstack.common.gettextutils import _ from muranoclient.openstack.common import strutils @@ -219,7 +220,10 @@ class ManagerWithFind(BaseManager): matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() @@ -373,7 +377,10 @@ class CrudManager(BaseManager): num = len(rl) if num == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch diff --git a/muranoclient/openstack/common/apiclient/exceptions.py b/muranoclient/openstack/common/apiclient/exceptions.py index ada1344f..37a9aaf9 100644 --- a/muranoclient/openstack/common/apiclient/exceptions.py +++ b/muranoclient/openstack/common/apiclient/exceptions.py @@ -25,6 +25,8 @@ import sys import six +from muranoclient.openstack.common.gettextutils import _ + class ClientException(Exception): """The base exception class for all exceptions this library raises. @@ -36,7 +38,7 @@ class MissingArgs(ClientException): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing - msg = "Missing argument(s): %s" % ", ".join(missing) + msg = _("Missing arguments: %s") % ", ".join(missing) super(MissingArgs, self).__init__(msg) @@ -69,7 +71,7 @@ class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( - "Authentication failed. Missing options: %s" % + _("Authentication failed. Missing options: %s") % ", ".join(opt_names)) self.opt_names = opt_names @@ -78,7 +80,7 @@ class AuthSystemNotFound(AuthorizationFailure): """User has specified a AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( - "AuthSystemNotFound: %s" % repr(auth_system)) + _("AuthSystemNotFound: %s") % repr(auth_system)) self.auth_system = auth_system @@ -101,7 +103,7 @@ class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( - "AmbiguousEndpoints: %s" % repr(endpoints)) + _("AmbiguousEndpoints: %s") % repr(endpoints)) self.endpoints = endpoints @@ -109,7 +111,7 @@ class HttpError(ClientException): """The base exception class for all HTTP exceptions. """ http_status = 0 - message = "HTTP Error" + message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, @@ -129,7 +131,7 @@ class HttpError(ClientException): class HTTPRedirection(HttpError): """HTTP Redirection.""" - message = "HTTP Redirection" + message = _("HTTP Redirection") class HTTPClientError(HttpError): @@ -137,7 +139,7 @@ class HTTPClientError(HttpError): Exception for cases in which the client seems to have erred. """ - message = "HTTP Client Error" + message = _("HTTP Client Error") class HttpServerError(HttpError): @@ -146,7 +148,7 @@ class HttpServerError(HttpError): Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ - message = "HTTP Server Error" + message = _("HTTP Server Error") class MultipleChoices(HTTPRedirection): @@ -156,7 +158,7 @@ class MultipleChoices(HTTPRedirection): """ http_status = 300 - message = "Multiple Choices" + message = _("Multiple Choices") class BadRequest(HTTPClientError): @@ -165,7 +167,7 @@ class BadRequest(HTTPClientError): The request cannot be fulfilled due to bad syntax. """ http_status = 400 - message = "Bad Request" + message = _("Bad Request") class Unauthorized(HTTPClientError): @@ -175,7 +177,7 @@ class Unauthorized(HTTPClientError): is required and has failed or has not yet been provided. """ http_status = 401 - message = "Unauthorized" + message = _("Unauthorized") class PaymentRequired(HTTPClientError): @@ -184,7 +186,7 @@ class PaymentRequired(HTTPClientError): Reserved for future use. """ http_status = 402 - message = "Payment Required" + message = _("Payment Required") class Forbidden(HTTPClientError): @@ -194,7 +196,7 @@ class Forbidden(HTTPClientError): to it. """ http_status = 403 - message = "Forbidden" + message = _("Forbidden") class NotFound(HTTPClientError): @@ -204,7 +206,7 @@ class NotFound(HTTPClientError): in the future. """ http_status = 404 - message = "Not Found" + message = _("Not Found") class MethodNotAllowed(HTTPClientError): @@ -214,7 +216,7 @@ class MethodNotAllowed(HTTPClientError): by that resource. """ http_status = 405 - message = "Method Not Allowed" + message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): @@ -224,7 +226,7 @@ class NotAcceptable(HTTPClientError): acceptable according to the Accept headers sent in the request. """ http_status = 406 - message = "Not Acceptable" + message = _("Not Acceptable") class ProxyAuthenticationRequired(HTTPClientError): @@ -233,7 +235,7 @@ class ProxyAuthenticationRequired(HTTPClientError): The client must first authenticate itself with the proxy. """ http_status = 407 - message = "Proxy Authentication Required" + message = _("Proxy Authentication Required") class RequestTimeout(HTTPClientError): @@ -242,7 +244,7 @@ class RequestTimeout(HTTPClientError): The server timed out waiting for the request. """ http_status = 408 - message = "Request Timeout" + message = _("Request Timeout") class Conflict(HTTPClientError): @@ -252,7 +254,7 @@ class Conflict(HTTPClientError): in the request, such as an edit conflict. """ http_status = 409 - message = "Conflict" + message = _("Conflict") class Gone(HTTPClientError): @@ -262,7 +264,7 @@ class Gone(HTTPClientError): not be available again. """ http_status = 410 - message = "Gone" + message = _("Gone") class LengthRequired(HTTPClientError): @@ -272,7 +274,7 @@ class LengthRequired(HTTPClientError): required by the requested resource. """ http_status = 411 - message = "Length Required" + message = _("Length Required") class PreconditionFailed(HTTPClientError): @@ -282,7 +284,7 @@ class PreconditionFailed(HTTPClientError): put on the request. """ http_status = 412 - message = "Precondition Failed" + message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): @@ -291,7 +293,7 @@ class RequestEntityTooLarge(HTTPClientError): The request is larger than the server is willing or able to process. """ http_status = 413 - message = "Request Entity Too Large" + message = _("Request Entity Too Large") def __init__(self, *args, **kwargs): try: @@ -308,7 +310,7 @@ class RequestUriTooLong(HTTPClientError): The URI provided was too long for the server to process. """ http_status = 414 - message = "Request-URI Too Long" + message = _("Request-URI Too Long") class UnsupportedMediaType(HTTPClientError): @@ -318,7 +320,7 @@ class UnsupportedMediaType(HTTPClientError): not support. """ http_status = 415 - message = "Unsupported Media Type" + message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): @@ -328,7 +330,7 @@ class RequestedRangeNotSatisfiable(HTTPClientError): supply that portion. """ http_status = 416 - message = "Requested Range Not Satisfiable" + message = _("Requested Range Not Satisfiable") class ExpectationFailed(HTTPClientError): @@ -337,7 +339,7 @@ class ExpectationFailed(HTTPClientError): The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 - message = "Expectation Failed" + message = _("Expectation Failed") class UnprocessableEntity(HTTPClientError): @@ -347,7 +349,7 @@ class UnprocessableEntity(HTTPClientError): errors. """ http_status = 422 - message = "Unprocessable Entity" + message = _("Unprocessable Entity") class InternalServerError(HttpServerError): @@ -356,7 +358,7 @@ class InternalServerError(HttpServerError): A generic error message, given when no more specific message is suitable. """ http_status = 500 - message = "Internal Server Error" + message = _("Internal Server Error") # NotImplemented is a python keyword. @@ -367,7 +369,7 @@ class HttpNotImplemented(HttpServerError): the ability to fulfill the request. """ http_status = 501 - message = "Not Implemented" + message = _("Not Implemented") class BadGateway(HttpServerError): @@ -377,7 +379,7 @@ class BadGateway(HttpServerError): response from the upstream server. """ http_status = 502 - message = "Bad Gateway" + message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): @@ -386,7 +388,7 @@ class ServiceUnavailable(HttpServerError): The server is currently unavailable. """ http_status = 503 - message = "Service Unavailable" + message = _("Service Unavailable") class GatewayTimeout(HttpServerError): @@ -396,7 +398,7 @@ class GatewayTimeout(HttpServerError): response from the upstream server. """ http_status = 504 - message = "Gateway Timeout" + message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): @@ -405,7 +407,7 @@ class HttpVersionNotSupported(HttpServerError): The server does not support the HTTP protocol version used in the request. """ http_status = 505 - message = "HTTP Version Not Supported" + message = _("HTTP Version Not Supported") # _code_map contains all the classes that have http_status attribute. @@ -423,12 +425,17 @@ def from_response(response, method, url): :param method: HTTP method used for request :param url: URL used for request """ + + req_id = response.headers.get("x-openstack-request-id") + #NOTE(hdd) true for older versions of nova and cinder + if not req_id: + req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, - "request_id": response.headers.get("x-compute-request-id"), + "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] diff --git a/muranoclient/openstack/common/gettextutils.py b/muranoclient/openstack/common/gettextutils.py index b7dfcbd2..b3cb4894 100644 --- a/muranoclient/openstack/common/gettextutils.py +++ b/muranoclient/openstack/common/gettextutils.py @@ -32,24 +32,113 @@ import os from babel import localedata import six -_localedir = os.environ.get('muranoclient'.upper() + '_LOCALEDIR') -_t = gettext.translation('muranoclient', localedir=_localedir, fallback=True) - -# We use separate translation catalogs for each log level, so set up a -# mapping between the log level name and the translator. The domain -# for the log level is project_name + "-log-" + log_level so messages -# for each level end up in their own catalog. -_t_log_levels = dict( - (level, gettext.translation('muranoclient' + '-log-' + level, - localedir=_localedir, - fallback=True)) - for level in ['info', 'warning', 'error', 'critical'] -) - _AVAILABLE_LANGUAGES = {} + +# FIXME(dhellmann): Remove this when moving to oslo.i18n. USE_LAZY = False +class TranslatorFactory(object): + """Create translator functions + """ + + def __init__(self, domain, lazy=False, 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 + self.lazy = lazy + 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 + if self.lazy: + return functools.partial(Message, domain=domain) + t = gettext.translation( + domain, + localedir=self.localedir, + fallback=True, + ) + if six.PY3: + return t.gettext + return t.ugettext + + @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('muranoclient') + +# 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 @@ -58,41 +147,18 @@ def enable_lazy(): your project is importing _ directly instead of using the gettextutils.install() way of importing the _ function. """ - global USE_LAZY + # FIXME(dhellmann): This function will be removed in oslo.i18n, + # because the TranslatorFactory makes it superfluous. + global _, _LI, _LW, _LE, _LC, USE_LAZY + tf = TranslatorFactory('muranoclient', lazy=True) + _ = tf.primary + _LI = tf.log_info + _LW = tf.log_warning + _LE = tf.log_error + _LC = tf.log_critical USE_LAZY = True -def _(msg): - if USE_LAZY: - return Message(msg, domain='muranoclient') - else: - if six.PY3: - return _t.gettext(msg) - return _t.ugettext(msg) - - -def _log_translation(msg, level): - """Build a single translation of a log message - """ - if USE_LAZY: - return Message(msg, domain='muranoclient' + '-log-' + level) - else: - translator = _t_log_levels[level] - if six.PY3: - return translator.gettext(msg) - return translator.ugettext(msg) - -# 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 = functools.partial(_log_translation, level='info') -_LW = functools.partial(_log_translation, level='warning') -_LE = functools.partial(_log_translation, level='error') -_LC = functools.partial(_log_translation, level='critical') - - def install(domain, lazy=False): """Install a _() function using the given translation domain. @@ -112,26 +178,9 @@ def install(domain, lazy=False): any available locale. """ if lazy: - # NOTE(mrodden): Lazy gettext functionality. - # - # The following introduces a deferred way to do translations on - # messages in OpenStack. We override the standard _() function - # and % (format string) operation to build Message objects that can - # later be translated when we have more information. - def _lazy_gettext(msg): - """Create and return a Message object. - - Lazy gettext function for a given domain, it is a factory method - for a project/module to get a lazy gettext function for its own - translation domain (i.e. nova, glance, cinder, etc.) - - Message encapsulates a string so that we can translate - it later when needed. - """ - return Message(msg, domain=domain) - from six import moves - moves.builtins.__dict__['_'] = _lazy_gettext + tf = TranslatorFactory(domain, lazy=True) + moves.builtins.__dict__['_'] = tf.primary else: localedir = '%s_LOCALEDIR' % domain.upper() if six.PY3: @@ -274,13 +323,14 @@ class Message(six.text_type): def __radd__(self, other): return self.__add__(other) - 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) + 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): diff --git a/muranoclient/openstack/common/jsonutils.py b/muranoclient/openstack/common/jsonutils.py new file mode 100644 index 00000000..aa1e1f9f --- /dev/null +++ b/muranoclient/openstack/common/jsonutils.py @@ -0,0 +1,184 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# 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. + +''' +JSON related utilities. + +This module provides a few things: + + 1) A handy function for getting an object down to something that can be + JSON serialized. See to_primitive(). + + 2) Wrappers around loads() and dumps(). The dumps() wrapper will + automatically use to_primitive() for you if needed. + + 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson + is available. +''' + + +import datetime +import functools +import inspect +import itertools +import sys + +if sys.version_info < (2, 7): + # On Python <= 2.6, json module is not C boosted, so try to use + # simplejson module if available + try: + import simplejson as json + except ImportError: + import json +else: + import json + +import six +import six.moves.xmlrpc_client as xmlrpclib + +from muranoclient.openstack.common import gettextutils +from muranoclient.openstack.common import importutils +from muranoclient.openstack.common import timeutils + +netaddr = importutils.try_import("netaddr") + +_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod, + inspect.isfunction, inspect.isgeneratorfunction, + inspect.isgenerator, inspect.istraceback, inspect.isframe, + inspect.iscode, inspect.isbuiltin, inspect.isroutine, + inspect.isabstract] + +_simple_types = (six.string_types + six.integer_types + + (type(None), bool, float)) + + +def to_primitive(value, convert_instances=False, convert_datetime=True, + level=0, max_depth=3): + """Convert a complex object into primitives. + + Handy for JSON serialization. We can optionally handle instances, + but since this is a recursive function, we could have cyclical + data structures. + + To handle cyclical data structures we could track the actual objects + visited in a set, but not all objects are hashable. Instead we just + track the depth of the object inspections and don't go too deep. + + Therefore, convert_instances=True is lossy ... be aware. + + """ + # handle obvious types first - order of basic types determined by running + # full tests on nova project, resulting in the following counts: + # 572754 + # 460353 + # 379632 + # 274610 + # 199918 + # 114200 + # 51817 + # 26164 + # 6491 + # 283 + # 19 + if isinstance(value, _simple_types): + return value + + if isinstance(value, datetime.datetime): + if convert_datetime: + return timeutils.strtime(value) + else: + return value + + # value of itertools.count doesn't get caught by nasty_type_tests + # and results in infinite loop when list(value) is called. + if type(value) == itertools.count: + return six.text_type(value) + + # FIXME(vish): Workaround for LP bug 852095. Without this workaround, + # tests that raise an exception in a mocked method that + # has a @wrap_exception with a notifier will fail. If + # we up the dependency to 0.5.4 (when it is released) we + # can remove this workaround. + if getattr(value, '__module__', None) == 'mox': + return 'mock' + + if level > max_depth: + return '?' + + # The try block may not be necessary after the class check above, + # but just in case ... + try: + recursive = functools.partial(to_primitive, + convert_instances=convert_instances, + convert_datetime=convert_datetime, + level=level, + max_depth=max_depth) + if isinstance(value, dict): + return dict((k, recursive(v)) for k, v in six.iteritems(value)) + elif isinstance(value, (list, tuple)): + return [recursive(lv) for lv in value] + + # It's not clear why xmlrpclib created their own DateTime type, but + # for our purposes, make it a datetime type which is explicitly + # handled + if isinstance(value, xmlrpclib.DateTime): + value = datetime.datetime(*tuple(value.timetuple())[:6]) + + if convert_datetime and isinstance(value, datetime.datetime): + return timeutils.strtime(value) + elif isinstance(value, gettextutils.Message): + return value.data + elif hasattr(value, 'iteritems'): + return recursive(dict(value.iteritems()), level=level + 1) + elif hasattr(value, '__iter__'): + return recursive(list(value)) + elif convert_instances and hasattr(value, '__dict__'): + # Likely an instance of something. Watch for cycles. + # Ignore class member vars. + return recursive(value.__dict__, level=level + 1) + elif netaddr and isinstance(value, netaddr.IPAddress): + return six.text_type(value) + else: + if any(test(value) for test in _nasty_type_tests): + return six.text_type(value) + return value + except TypeError: + # Class objects are tricky since they may define something like + # __iter__ defined but it isn't callable as list(). + return six.text_type(value) + + +def dumps(value, default=to_primitive, **kwargs): + return json.dumps(value, default=default, **kwargs) + + +def loads(s): + return json.loads(s) + + +def load(fp): + return json.load(fp) + + +try: + import anyjson +except ImportError: + pass +else: + anyjson._modules.append((__name__, 'dumps', TypeError, + 'loads', ValueError, 'load')) + anyjson.force_implementation(__name__) diff --git a/muranoclient/openstack/common/strutils.py b/muranoclient/openstack/common/strutils.py index 6a44d9da..e27948bb 100644 --- a/muranoclient/openstack/common/strutils.py +++ b/muranoclient/openstack/common/strutils.py @@ -78,7 +78,7 @@ def bool_from_string(subject, strict=False, default=False): Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ if not isinstance(subject, six.string_types): - subject = str(subject) + subject = six.text_type(subject) lowered = subject.strip().lower() @@ -159,19 +159,13 @@ def safe_encode(text, incoming=None, sys.getdefaultencoding()) if isinstance(text, six.text_type): - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) + return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) - - return text + return text.encode(encoding, errors) + else: + return text def string_to_bytes(text, unit_system='IEC', return_int=False): diff --git a/muranoclient/openstack/common/timeutils.py b/muranoclient/openstack/common/timeutils.py new file mode 100644 index 00000000..52688a02 --- /dev/null +++ b/muranoclient/openstack/common/timeutils.py @@ -0,0 +1,210 @@ +# 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. + +""" +Time related utilities and helper functions. +""" + +import calendar +import datetime +import time + +import iso8601 +import six + + +# ISO 8601 extended time format with microseconds +_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' +_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' +PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND + + +def isotime(at=None, subsecond=False): + """Stringify time in ISO 8601 format.""" + if not at: + at = utcnow() + st = at.strftime(_ISO8601_TIME_FORMAT + if not subsecond + else _ISO8601_TIME_FORMAT_SUBSECOND) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + st += ('Z' if tz == 'UTC' else tz) + return st + + +def parse_isotime(timestr): + """Parse time from ISO 8601 format.""" + try: + return iso8601.parse_date(timestr) + except iso8601.ParseError as e: + raise ValueError(six.text_type(e)) + except TypeError as e: + raise ValueError(six.text_type(e)) + + +def strtime(at=None, fmt=PERFECT_TIME_FORMAT): + """Returns formatted utcnow.""" + if not at: + at = utcnow() + return at.strftime(fmt) + + +def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): + """Turn a formatted time back into a datetime.""" + return datetime.datetime.strptime(timestr, fmt) + + +def normalize_time(timestamp): + """Normalize time in arbitrary timezone to UTC naive object.""" + offset = timestamp.utcoffset() + if offset is None: + return timestamp + return timestamp.replace(tzinfo=None) - offset + + +def is_older_than(before, seconds): + """Return True if before is older than seconds.""" + if isinstance(before, six.string_types): + before = parse_strtime(before).replace(tzinfo=None) + else: + before = before.replace(tzinfo=None) + + return utcnow() - before > datetime.timedelta(seconds=seconds) + + +def is_newer_than(after, seconds): + """Return True if after is newer than seconds.""" + if isinstance(after, six.string_types): + after = parse_strtime(after).replace(tzinfo=None) + else: + after = after.replace(tzinfo=None) + + return after - utcnow() > datetime.timedelta(seconds=seconds) + + +def utcnow_ts(): + """Timestamp version of our utcnow function.""" + if utcnow.override_time is None: + # NOTE(kgriffs): This is several times faster + # than going through calendar.timegm(...) + return int(time.time()) + + return calendar.timegm(utcnow().timetuple()) + + +def utcnow(): + """Overridable version of utils.utcnow.""" + if utcnow.override_time: + try: + return utcnow.override_time.pop(0) + except AttributeError: + return utcnow.override_time + return datetime.datetime.utcnow() + + +def iso8601_from_timestamp(timestamp): + """Returns a iso8601 formatted date from timestamp.""" + return isotime(datetime.datetime.utcfromtimestamp(timestamp)) + + +utcnow.override_time = None + + +def set_time_override(override_time=None): + """Overrides utils.utcnow. + + Make it return a constant time or a list thereof, one at a time. + + :param override_time: datetime instance or list thereof. If not + given, defaults to the current UTC time. + """ + utcnow.override_time = override_time or datetime.datetime.utcnow() + + +def advance_time_delta(timedelta): + """Advance overridden time using a datetime.timedelta.""" + assert(not utcnow.override_time is None) + try: + for dt in utcnow.override_time: + dt += timedelta + except TypeError: + utcnow.override_time += timedelta + + +def advance_time_seconds(seconds): + """Advance overridden time by seconds.""" + advance_time_delta(datetime.timedelta(0, seconds)) + + +def clear_time_override(): + """Remove the overridden time.""" + utcnow.override_time = None + + +def marshall_now(now=None): + """Make an rpc-safe datetime with microseconds. + + Note: tzinfo is stripped, but not required for relative times. + """ + if not now: + now = utcnow() + return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, + minute=now.minute, second=now.second, + microsecond=now.microsecond) + + +def unmarshall_time(tyme): + """Unmarshall a datetime dict.""" + return datetime.datetime(day=tyme['day'], + month=tyme['month'], + year=tyme['year'], + hour=tyme['hour'], + minute=tyme['minute'], + second=tyme['second'], + microsecond=tyme['microsecond']) + + +def delta_seconds(before, after): + """Return the difference between two timing objects. + + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + return total_seconds(delta) + + +def total_seconds(delta): + """Return the total seconds of datetime.timedelta object. + + Compute total seconds of datetime.timedelta, datetime.timedelta + doesn't have method total_seconds in Python2.6, calculate it manually. + """ + try: + return delta.total_seconds() + except AttributeError: + return ((delta.days * 24 * 3600) + delta.seconds + + float(delta.microseconds) / (10 ** 6)) + + +def is_soon(dt, window): + """Determines if time is going to happen in the next window seconds. + + :param dt: the time + :param window: minimum seconds to remain to consider the time not soon + + :return: True if expiration is within the given duration + """ + soon = (utcnow() + datetime.timedelta(seconds=window)) + return normalize_time(dt) <= soon diff --git a/muranoclient/shell.py b/muranoclient/shell.py index bb20abb0..e5f5c99b 100644 --- a/muranoclient/shell.py +++ b/muranoclient/shell.py @@ -19,14 +19,17 @@ Command-line interface to the Murano Project. from __future__ import print_function import argparse +import httplib2 import logging +import six import sys -import httplib2 from keystoneclient.v2_0 import client as ksclient from muranoclient import client as apiclient from muranoclient.common import exceptions from muranoclient.common import utils +from muranoclient.openstack.common import strutils + logger = logging.getLogger(__name__) @@ -302,15 +305,20 @@ class HelpFormatter(argparse.HelpFormatter): super(HelpFormatter, self).start_section(heading) -def main(): +def main(args=None): + if args is None: + args = sys.argv[1:] try: - MuranoShell().main(sys.argv[1:]) + MuranoShell().main(args) except KeyboardInterrupt: print('... terminating murano client', file=sys.stderr) sys.exit(1) except Exception as e: - print(utils.exception_to_str(e), file=sys.stderr) + if '--debug' in args or '-d' in args: + raise + else: + print(strutils.safe_encode(six.text_type(e)), file=sys.stderr) sys.exit(1) diff --git a/muranoclient/v1/services.py b/muranoclient/v1/services.py index 9e241b9d..ec9d53fc 100644 --- a/muranoclient/v1/services.py +++ b/muranoclient/v1/services.py @@ -50,6 +50,14 @@ class Service(base.Resource): class ServiceManager(base.Manager): resource_class = Service + def list(self, environment_id, session_id=None): + if session_id: + headers = {'X-Configuration-Session': session_id} + else: + headers = {} + return self._list("/v1/environments/{0}/services". + format(environment_id), headers=headers) + @normalize_path def get(self, environment_id, path, session_id=None): if session_id: diff --git a/muranoclient/v1/shell.py b/muranoclient/v1/shell.py index e64e99a4..31b9645e 100644 --- a/muranoclient/v1/shell.py +++ b/muranoclient/v1/shell.py @@ -12,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +import sys + +from muranoclient.common import exceptions from muranoclient.common import utils @@ -21,3 +24,151 @@ def do_environment_list(mc, args={}): field_labels = ['ID', 'Name', 'Created', 'Updated'] fields = ['id', 'name', 'created', 'updated'] utils.print_list(environments, fields, field_labels, sortby=0) + + +@utils.arg("name", help="Environment name") +def do_environment_create(mc, args): + """Create an environment.""" + mc.environments.create({"name": args.name}) + do_environment_list(mc) + + +@utils.arg("id", help="Environment id") +def do_environment_delete(mc, args): + """Delete an environment.""" + try: + mc.environments.delete(args.id) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Environment %s not found" % args.id) + else: + do_environment_list(mc) + + +@utils.arg("id", help="Environment id") +@utils.arg("name", help="Name to which the environment will be renamed") +def do_environment_rename(mc, args): + """Rename an environment.""" + try: + mc.environments.update(args.id, args.name) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Environment %s not found" % args.id) + else: + do_environment_list(mc) + + +@utils.arg("id", help="Environment id") +def do_environment_show(mc, args): + """Display environment details.""" + try: + environment = mc.environments.get(args.id) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Environment %s not found" % args.id) + else: + formatters = { + "id": utils.text_wrap_formatter, + "created": utils.text_wrap_formatter, + "name": utils.text_wrap_formatter, + "tenant_id": utils.text_wrap_formatter, + "services": utils.json_formatter, + + } + utils.print_dict(environment.to_dict(), formatters=formatters) + + +@utils.arg("environment_id", + help="Environment id for which to list deployments") +def do_deployment_list(mc, args): + """List deployments for an environment.""" + try: + deployments = mc.deployments.list(args.environment_id) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Environment %s not found" % args.id) + else: + field_labels = ["ID", "State", "Created", "Updated", "Finished"] + fields = ["id", "state", "created", "updated", "finished"] + utils.print_list(deployments, fields, field_labels, sortby=0) + + +def do_category_list(mc, args): + """List all available categories.""" + categories = mc.packages.categories() + print(categories) + + +@utils.arg("--include-disabled", default=False, action="store_true") +def do_package_list(mc, args={}): + """List available packages.""" + filter_args = { + "include_disabled": getattr(args, 'include_disabled', False), + } + packages = mc.packages.filter(**filter_args) + field_labels = ["ID", "Name", "FQN", "Author", "Is Public"] + fields = ["id", "name", "fully_qualified_name", "author", "is_public"] + utils.print_list(packages, fields, field_labels, sortby=0) + + +@utils.arg("package_id", + help="Package ID to download") +@utils.arg("filename", metavar="file", nargs="?", + help="Filename for download (defaults to stdout)") +def do_package_download(mc, args): + """Download a package to a filename or stdout.""" + def download_to_fh(package_id, fh): + fh.write(mc.packages.download(package_id)) + + try: + if not args.filename: + download_to_fh(args.package_id, sys.stdout) + else: + with open(args.filename, 'wb') as fh: + download_to_fh(args.package_id, fh) + print("Package downloaded to %s" % args.filename) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Package %s not found" % args.package_id) + + +@utils.arg("package_id", + help="Package ID to show") +def do_package_show(mc, args): + """Display details for a package.""" + try: + package = mc.packages.get(args.package_id) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Package %s not found" % args.package_id) + else: + to_display = dict( + id=package.id, + type=package.type, + owner_id=package.owner_id, + name=package.name, + fully_qualified_name=package.fully_qualified_name, + is_public=package.is_public, + enabled=package.enabled, + class_definitions=", ".join(package.class_definitions) + ) + formatters = { + 'class_definitions': utils.text_wrap_formatter, + } + utils.print_dict(to_display, formatters) + + +@utils.arg("package_id", + help="Package ID to delete") +def do_package_delete(mc, args): + """Delete a package.""" + try: + mc.packages.delete(args.package_id) + except exceptions.HTTPNotFound: + raise exceptions.CommandError("Package %s not found" % args.package_id) + else: + do_package_list(mc) + + +@utils.arg("filename", metavar="file", help="Zip file containing package") +@utils.arg("category", nargs="+", + help="One or more categories to which the package belongs") +def do_package_import(mc, args): + """Import a package. `file` should be the path to a zip file.""" + data = {"categories": args.category} + mc.packages.create(data, ((args.filename, open(args.filename, 'rb')),)) + do_package_list(mc) diff --git a/openstack-common.conf b/openstack-common.conf index 78d97c4b..e989b9b7 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=importutils,strutils,gettextutils,apiclient.base,apiclient.exceptions +modules=importutils,strutils,gettextutils,apiclient.base,apiclient.exceptions,jsonutils # The base module to hold the copy of openstack.common base=muranoclient diff --git a/run_tests.sh b/run_tests.sh index 80095dae..8df97946 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,6 +6,10 @@ function usage { echo "" echo " -p, --pep8 Just run pep8" echo " -h, --help Print this usage message" + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." + echo " -u, --update Update the virtual environment with any newer package versions" echo "" echo "This script is deprecated and currently retained for compatibility." echo 'You can run the full test suite for multiple environments by running "tox".' @@ -14,19 +18,29 @@ function usage { exit } -command -v tox > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo 'This script requires "tox" to run.' - echo 'You can install it with "pip install tox".' - exit 1; -fi - just_pep8=0 +always_venv=0 +never_venv=0 +wrapper= +update=0 +force=0 + +export NOSE_WITH_OPENSTACK=1 +export NOSE_OPENSTACK_COLOR=1 +export NOSE_OPENSTACK_RED=0.05 +export NOSE_OPENSTACK_YELLOW=0.025 +export NOSE_OPENSTACK_SHOW_ELAPSED=1 +export NOSE_OPENSTACK_STDOUT=1 + function process_option { case "$1" in -h|--help) usage;; -p|--pep8) let just_pep8=1;; + -V|--virtual-env) let always_venv=1; let never_venv=0;; + -f|--force) let force=1;; + -u|--update) update=1;; + -N|--no-virtual-env) let always_venv=0; let never_venv=1;; esac } @@ -34,16 +48,60 @@ for arg in "$@"; do process_option $arg done +function run_tests { + # Cleanup *pyc + ${wrapper} find . -type f -name "*.pyc" -delete + # Just run the test suites in current environment + ${wrapper} $NOSETESTS +} + +function run_pep8 { + echo "Running pep8 ..." + PEP8_EXCLUDE=".venv,.tox,dist,doc,openstack,build" + PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat --select=H402" + PEP8_IGNORE="--ignore=E125,E126,E711,E712" + PEP8_INCLUDE="." + pep8 $PEP8_OPTIONS $PEP8_INCLUDE $PEP8_IGNORE +} + +NOSETESTS="nosetests $noseopts $noseargs" + +if [ $never_venv -eq 0 ] +then + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + echo "Cleaning virtualenv..." + rm -rf ${venv} + fi + if [ $update -eq 1 ]; then + echo "Updating virtualenv..." + python tools/install_venv.py + fi + if [ -e ${venv} ]; then + wrapper="${with_venv}" + else + if [ $always_venv -eq 1 ]; then + # Automatically install the virtualenv + python tools/install_venv.py + wrapper="${with_venv}" + else + echo -e "No virtual environment found...create one? (Y/n) \c" + read use_ve + if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then + # Install the virtualenv and run the test suite in it + python tools/install_venv.py + wrapper=${with_venv} + fi + fi + fi +fi + if [ $just_pep8 -eq 1 ]; then - tox -e pep8 + run_pep8 exit fi -tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit -if [ ${PIPESTATUS[0]} -ne 0 ]; then - exit ${PIPESTATUS[0]} -fi +run_tests || exit + +run_pep8 -if [ -z "$toxargs" ]; then - tox -e pep8 -fi