From 5259f00827aa6e40dee38240762cb28d8f9c2cde Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Wed, 29 Oct 2014 16:51:15 +0000 Subject: [PATCH] Convert strutils to oslo.utils.encodeutils Convert the encode/decode functions from oslo-incubator to use oslo.utils encodeutils, as the incubator functions are now deprecated. Also syncs oslo-incubator to 62394a3 to purge usage of strutils from the openstack/common modules. Note includes oslo fix https://review.openstack.org/#/c/133290/ which we need or the python3 tests won't pass. Change-Id: I630fe3f3ce14ae745a8417bfe6552acd31341c9c Partial-Bug: #1380629 --- heatclient/common/http.py | 11 +- heatclient/openstack/common/__init__.py | 17 -- heatclient/openstack/common/_i18n.py | 40 +++ heatclient/openstack/common/apiclient/auth.py | 4 +- heatclient/openstack/common/apiclient/base.py | 58 +++-- .../openstack/common/apiclient/client.py | 48 +++- .../openstack/common/apiclient/exceptions.py | 89 ++++--- .../openstack/common/apiclient/fake_client.py | 8 +- .../openstack/common/apiclient/utils.py | 87 +++++++ heatclient/openstack/common/cliutils.py | 118 +++------ heatclient/openstack/common/strutils.py | 245 ------------------ heatclient/shell.py | 5 +- heatclient/tests/test_shell.py | 14 +- heatclient/v1/events.py | 7 +- heatclient/v1/resource_types.py | 7 +- heatclient/v1/resources.py | 11 +- heatclient/v1/shell.py | 2 +- openstack-common.conf | 2 +- requirements.txt | 1 + 19 files changed, 331 insertions(+), 443 deletions(-) create mode 100644 heatclient/openstack/common/_i18n.py create mode 100644 heatclient/openstack/common/apiclient/utils.py delete mode 100644 heatclient/openstack/common/strutils.py diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 1659f71d..68245f37 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -24,10 +24,10 @@ import six from six.moves.urllib import parse from oslo.serialization import jsonutils +from oslo.utils import encodeutils from heatclient import exc from heatclient.openstack.common import importutils -from heatclient.openstack.common import strutils LOG = logging.getLogger(__name__) USER_AGENT = 'python-heatclient' @@ -84,15 +84,20 @@ class HTTPClient(object): else: self.verify_cert = kwargs.get('ca_file', get_system_ca_file()) + # FIXME(shardy): We need this for compatibility with the oslo apiclient + # we should move to inheriting this class from the oslo HTTPClient + self.last_request_id = None + def safe_header(self, name, value): if name in SENSITIVE_HEADERS: # because in python3 byte string handling is ... ug v = value.encode('utf-8') h = hashlib.sha1(v) d = h.hexdigest() - return strutils.safe_decode(name), "{SHA1}%s" % d + return encodeutils.safe_decode(name), "{SHA1}%s" % d else: - return strutils.safe_decode(name), strutils.safe_decode(value) + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) def log_curl_request(self, method, url, kwargs): curl = ['curl -i -X %s' % method] diff --git a/heatclient/openstack/common/__init__.py b/heatclient/openstack/common/__init__.py index d1223eaf..e69de29b 100644 --- a/heatclient/openstack/common/__init__.py +++ b/heatclient/openstack/common/__init__.py @@ -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/heatclient/openstack/common/_i18n.py b/heatclient/openstack/common/_i18n.py new file mode 100644 index 00000000..54206f4a --- /dev/null +++ b/heatclient/openstack/common/_i18n.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html + +""" + +import oslo.i18n + + +# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the +# application name when this module is synced into the separate +# repository. It is OK to have more than one translation function +# using the same domain, since there will still only be one message +# catalog. +_translators = oslo.i18n.TranslatorFactory(domain='heatclient') + +# 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 diff --git a/heatclient/openstack/common/apiclient/auth.py b/heatclient/openstack/common/apiclient/auth.py index 0535748e..9c7f97e7 100644 --- a/heatclient/openstack/common/apiclient/auth.py +++ b/heatclient/openstack/common/apiclient/auth.py @@ -213,8 +213,8 @@ class BaseAuthPlugin(object): :type service_type: string :param endpoint_type: Type of endpoint. Possible values: public or publicURL, - internal or internalURL, - admin or adminURL + internal or internalURL, + admin or adminURL :type endpoint_type: string :returns: tuple of token and endpoint strings :raises: EndpointException diff --git a/heatclient/openstack/common/apiclient/base.py b/heatclient/openstack/common/apiclient/base.py index 14b57669..c3058618 100644 --- a/heatclient/openstack/common/apiclient/base.py +++ b/heatclient/openstack/common/apiclient/base.py @@ -26,11 +26,12 @@ Base utilities to build API operation managers and objects on top of. import abc import copy +from oslo.utils import strutils import six from six.moves.urllib import parse +from heatclient.openstack.common._i18n import _ from heatclient.openstack.common.apiclient import exceptions -from heatclient.openstack.common import strutils def getid(obj): @@ -74,8 +75,8 @@ class HookableMixin(object): :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' - :param **args: args to be passed to every hook function - :param **kwargs: kwargs to be passed to every hook function + :param args: args to be passed to every hook function + :param kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: @@ -98,12 +99,13 @@ class BaseManager(HookableMixin): super(BaseManager, self).__init__() self.client = client - def _list(self, url, response_key, obj_class=None, json=None): + def _list(self, url, response_key=None, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST @@ -117,7 +119,7 @@ class BaseManager(HookableMixin): if obj_class is None: obj_class = self.resource_class - data = body[response_key] + data = body[response_key] if response_key is not None else body # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: @@ -127,15 +129,17 @@ class BaseManager(HookableMixin): return [obj_class(self, res, loaded=True) for res in data if res] - def _get(self, url, response_key): + def _get(self, url, response_key=None): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, - e.g., 'server' + e.g., 'server'. If response_key is None - all response body + will be used. """ body = self.client.get(url).json() - return self.resource_class(self, body[response_key], loaded=True) + data = body[response_key] if response_key is not None else body + return self.resource_class(self, data, loaded=True) def _head(self, url): """Retrieve request headers for an object. @@ -145,21 +149,23 @@ class BaseManager(HookableMixin): resp = self.client.head(url) return resp.status_code == 204 - def _post(self, url, json, response_key, return_raw=False): + def _post(self, url, json, response_key=None, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'server'. If response_key is None - all response body + will be used. :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() + data = body[response_key] if response_key is not None else body if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) + return data + return self.resource_class(self, data) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. @@ -168,7 +174,8 @@ class BaseManager(HookableMixin): :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. """ resp = self.client.put(url, json=json) # PUT requests may not return a body @@ -186,7 +193,8 @@ class BaseManager(HookableMixin): :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. """ body = self.client.patch(url, json=json).json() if response_key is not None: @@ -219,7 +227,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 +384,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 @@ -441,8 +455,10 @@ class Resource(object): def human_id(self): """Human-readable ID which can be used for bash completion. """ - if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return strutils.to_slug(getattr(self, self.NAME_ATTR)) + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) return None def _add_details(self, info): @@ -456,7 +472,7 @@ class Resource(object): def __getattr__(self, k): if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once + # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) @@ -479,6 +495,8 @@ class Resource(object): new = self.manager.get(self.id) if new: self._add_details(new._info) + self._add_details( + {'x_request_id': self.manager.client.last_request_id}) def __eq__(self, other): if not isinstance(other, Resource): diff --git a/heatclient/openstack/common/apiclient/client.py b/heatclient/openstack/common/apiclient/client.py index 8f671855..af60f166 100644 --- a/heatclient/openstack/common/apiclient/client.py +++ b/heatclient/openstack/common/apiclient/client.py @@ -25,6 +25,7 @@ OpenStack Client interface. Handles the REST calls and responses. # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 +import hashlib import logging import time @@ -33,19 +34,22 @@ try: except ImportError: import json +from oslo.utils import encodeutils +from oslo.utils import importutils import requests +from heatclient.openstack.common._i18n import _ from heatclient.openstack.common.apiclient import exceptions -from heatclient.openstack.common import importutils - _logger = logging.getLogger(__name__) +SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) class HTTPClient(object): """This client handles sending HTTP requests to OpenStack servers. Features: + - share authentication information between several clients to different services (e.g., for compute and image clients); - reissue authentication request for expired tokens; @@ -96,6 +100,18 @@ class HTTPClient(object): self.http = http or requests.Session() self.cached_token = None + self.last_request_id = None + + def _safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return encodeutils.safe_decode(name), "{SHA1}%s" % d + else: + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) def _http_log_req(self, method, url, kwargs): if not self.debug: @@ -108,7 +124,8 @@ class HTTPClient(object): ] for element in kwargs['headers']: - header = "-H '%s: %s'" % (element, kwargs['headers'][element]) + header = ("-H '%s: %s'" % + self._safe_header(element, kwargs['headers'][element])) string_parts.append(header) _logger.debug("REQ: %s" % " ".join(string_parts)) @@ -151,10 +168,10 @@ class HTTPClient(object): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' requests.Session.request (such as `headers`) or `json` + requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ - kwargs.setdefault("headers", kwargs.get("headers", {})) + kwargs.setdefault("headers", {}) kwargs["headers"]["User-Agent"] = self.user_agent if self.original_ip: kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( @@ -175,6 +192,8 @@ class HTTPClient(object): start_time, time.time())) self._http_log_resp(resp) + self.last_request_id = resp.headers.get('x-openstack-request-id') + if resp.status_code >= 400: _logger.debug( "Request returned failure status: %s", @@ -206,7 +225,7 @@ class HTTPClient(object): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' `HTTPClient.request` + `HTTPClient.request` """ filter_args = { @@ -228,7 +247,7 @@ class HTTPClient(object): **filter_args) if not (token and endpoint): raise exceptions.AuthorizationFailure( - "Cannot find endpoint or token for request") + _("Cannot find endpoint or token for request")) old_token_endpoint = (token, endpoint) kwargs.setdefault("headers", {})["X-Auth-Token"] = token @@ -245,6 +264,10 @@ class HTTPClient(object): raise self.cached_token = None client.cached_endpoint = None + if self.auth_plugin.opts.get('token'): + self.auth_plugin.opts['token'] = None + if self.auth_plugin.opts.get('endpoint'): + self.auth_plugin.opts['endpoint'] = None self.authenticate() try: token, endpoint = self.auth_plugin.token_and_endpoint( @@ -321,6 +344,10 @@ class BaseClient(object): return self.http_client.client_request( self, method, url, **kwargs) + @property + def last_request_id(self): + return self.http_client.last_request_id + def head(self, url, **kwargs): return self.client_request("HEAD", url, **kwargs) @@ -351,8 +378,11 @@ class BaseClient(object): try: client_path = version_map[str(version)] except (KeyError, ValueError): - msg = "Invalid %s client version '%s'. must be one of: %s" % ( - (api_name, version, ', '.join(version_map.keys()))) + msg = _("Invalid %(api_name)s client version '%(version)s'. " + "Must be one of: %(version_map)s") % { + 'api_name': api_name, + 'version': version, + 'version_map': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/heatclient/openstack/common/apiclient/exceptions.py b/heatclient/openstack/common/apiclient/exceptions.py index ada1344f..745ad065 100644 --- a/heatclient/openstack/common/apiclient/exceptions.py +++ b/heatclient/openstack/common/apiclient/exceptions.py @@ -25,6 +25,8 @@ import sys import six +from heatclient.openstack.common._i18n import _ + class ClientException(Exception): """The base exception class for all exceptions this library raises. @@ -32,14 +34,6 @@ class ClientException(Exception): pass -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) - super(MissingArgs, self).__init__(msg) - - class ValidationError(ClientException): """Error in validation on API client side.""" pass @@ -69,16 +63,16 @@ 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 class AuthSystemNotFound(AuthorizationFailure): - """User has specified a AuthSystem that is not installed.""" + """User has specified an 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 +95,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 +103,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 +123,7 @@ class HttpError(ClientException): class HTTPRedirection(HttpError): """HTTP Redirection.""" - message = "HTTP Redirection" + message = _("HTTP Redirection") class HTTPClientError(HttpError): @@ -137,7 +131,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 +140,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 +150,7 @@ class MultipleChoices(HTTPRedirection): """ http_status = 300 - message = "Multiple Choices" + message = _("Multiple Choices") class BadRequest(HTTPClientError): @@ -165,7 +159,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 +169,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 +178,7 @@ class PaymentRequired(HTTPClientError): Reserved for future use. """ http_status = 402 - message = "Payment Required" + message = _("Payment Required") class Forbidden(HTTPClientError): @@ -194,7 +188,7 @@ class Forbidden(HTTPClientError): to it. """ http_status = 403 - message = "Forbidden" + message = _("Forbidden") class NotFound(HTTPClientError): @@ -204,7 +198,7 @@ class NotFound(HTTPClientError): in the future. """ http_status = 404 - message = "Not Found" + message = _("Not Found") class MethodNotAllowed(HTTPClientError): @@ -214,7 +208,7 @@ class MethodNotAllowed(HTTPClientError): by that resource. """ http_status = 405 - message = "Method Not Allowed" + message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): @@ -224,7 +218,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 +227,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 +236,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 +246,7 @@ class Conflict(HTTPClientError): in the request, such as an edit conflict. """ http_status = 409 - message = "Conflict" + message = _("Conflict") class Gone(HTTPClientError): @@ -262,7 +256,7 @@ class Gone(HTTPClientError): not be available again. """ http_status = 410 - message = "Gone" + message = _("Gone") class LengthRequired(HTTPClientError): @@ -272,7 +266,7 @@ class LengthRequired(HTTPClientError): required by the requested resource. """ http_status = 411 - message = "Length Required" + message = _("Length Required") class PreconditionFailed(HTTPClientError): @@ -282,7 +276,7 @@ class PreconditionFailed(HTTPClientError): put on the request. """ http_status = 412 - message = "Precondition Failed" + message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): @@ -291,7 +285,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 +302,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 +312,7 @@ class UnsupportedMediaType(HTTPClientError): not support. """ http_status = 415 - message = "Unsupported Media Type" + message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): @@ -328,7 +322,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 +331,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 +341,7 @@ class UnprocessableEntity(HTTPClientError): errors. """ http_status = 422 - message = "Unprocessable Entity" + message = _("Unprocessable Entity") class InternalServerError(HttpServerError): @@ -356,7 +350,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 +361,7 @@ class HttpNotImplemented(HttpServerError): the ability to fulfill the request. """ http_status = 501 - message = "Not Implemented" + message = _("Not Implemented") class BadGateway(HttpServerError): @@ -377,7 +371,7 @@ class BadGateway(HttpServerError): response from the upstream server. """ http_status = 502 - message = "Bad Gateway" + message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): @@ -386,7 +380,7 @@ class ServiceUnavailable(HttpServerError): The server is currently unavailable. """ http_status = 503 - message = "Service Unavailable" + message = _("Service Unavailable") class GatewayTimeout(HttpServerError): @@ -396,7 +390,7 @@ class GatewayTimeout(HttpServerError): response from the upstream server. """ http_status = 504 - message = "Gateway Timeout" + message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): @@ -405,7 +399,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 +417,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"] @@ -440,8 +439,8 @@ def from_response(response, method, url): except ValueError: pass else: - if isinstance(body, dict): - error = list(body.values())[0] + if isinstance(body, dict) and isinstance(body.get("error"), dict): + error = body["error"] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif content_type.startswith("text/"): diff --git a/heatclient/openstack/common/apiclient/fake_client.py b/heatclient/openstack/common/apiclient/fake_client.py index eb10e0fe..44ae68ca 100644 --- a/heatclient/openstack/common/apiclient/fake_client.py +++ b/heatclient/openstack/common/apiclient/fake_client.py @@ -33,7 +33,9 @@ from six.moves.urllib import parse from heatclient.openstack.common.apiclient import client -def assert_has_keys(dct, required=[], optional=[]): +def assert_has_keys(dct, required=None, optional=None): + required = required or [] + optional = optional or [] for k in required: try: assert k in dct @@ -79,7 +81,7 @@ class FakeHTTPClient(client.HTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and not "auth_plugin" in kwargs: + if not args and "auth_plugin" not in kwargs: args = (None, ) super(FakeHTTPClient, self).__init__(*args, **kwargs) @@ -166,6 +168,8 @@ class FakeHTTPClient(client.HTTPClient): else: status, body = resp headers = {} + self.last_request_id = headers.get('x-openstack-request-id', + 'req-test') return TestResponse({ "status_code": status, "text": body, diff --git a/heatclient/openstack/common/apiclient/utils.py b/heatclient/openstack/common/apiclient/utils.py new file mode 100644 index 00000000..63004bc8 --- /dev/null +++ b/heatclient/openstack/common/apiclient/utils.py @@ -0,0 +1,87 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.utils import encodeutils +import six + +from heatclient.openstack.common._i18n import _ +from heatclient.openstack.common.apiclient import exceptions +from heatclient.openstack.common import uuidutils + + +def find_resource(manager, name_or_id, **find_args): + """Look for resource in a given manager. + + Used as a helper for the _find_* methods. + Example: + + .. code-block:: python + + def _find_hypervisor(cs, hypervisor): + #Get a hypervisor by name or ID. + return cliutils.find_resource(cs.hypervisors, hypervisor) + """ + # first try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # now try to get entity as uuid + try: + if six.PY2: + tmp_id = encodeutils.safe_encode(name_or_id) + else: + tmp_id = encodeutils.safe_decode(name_or_id) + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # for str id which is not uuid + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + try: + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + resource = getattr(manager, 'resource_class', None) + name_attr = resource.NAME_ATTR if resource else 'name' + kwargs = {name_attr: name_or_id} + kwargs.update(find_args) + return manager.find(**kwargs) + except exceptions.NotFound: + msg = _("No %(name)s with a name or " + "ID of '%(name_or_id)s' exists.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = _("Multiple %(name)s matches found for " + "'%(name_or_id)s', use an ID to be more specific.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) diff --git a/heatclient/openstack/common/cliutils.py b/heatclient/openstack/common/cliutils.py index e493fd8d..47e97f81 100644 --- a/heatclient/openstack/common/cliutils.py +++ b/heatclient/openstack/common/cliutils.py @@ -24,14 +24,21 @@ import os import sys import textwrap +from oslo.utils import encodeutils +from oslo.utils import strutils import prettytable import six from six import moves -from heatclient.openstack.common.apiclient import exceptions -from heatclient.openstack.common.gettextutils import _ -from heatclient.openstack.common import strutils -from heatclient.openstack.common import uuidutils +from heatclient.openstack.common._i18n import _ + + +class MissingArgs(Exception): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = _("Missing arguments: %s") % ", ".join(missing) + super(MissingArgs, self).__init__(msg) def validate_args(fn, *args, **kwargs): @@ -56,7 +63,7 @@ def validate_args(fn, *args, **kwargs): required_args = argspec.args[:len(argspec.args) - num_defaults] def isbound(method): - return getattr(method, 'im_self', None) is not None + return getattr(method, '__self__', None) is not None if isbound(fn): required_args.pop(0) @@ -64,7 +71,7 @@ def validate_args(fn, *args, **kwargs): missing = [arg for arg in required_args if arg not in kwargs] missing = missing[len(args):] if missing: - raise exceptions.MissingArgs(missing) + raise MissingArgs(missing) def arg(*args, **kwargs): @@ -132,7 +139,7 @@ def isunauthenticated(func): def print_list(objs, fields, formatters=None, sortby_index=0, - mixed_case_fields=None): + mixed_case_fields=None, field_labels=None): """Print a list or objects as a table, one row per object. :param objs: iterable of :class:`Resource` @@ -141,14 +148,22 @@ def print_list(objs, fields, formatters=None, sortby_index=0, :param sortby_index: index of the field for sorting table rows :param mixed_case_fields: fields corresponding to object attributes that have mixed case names (e.g., 'serverId') + :param field_labels: Labels to use in the heading of the table, default to + fields. """ formatters = formatters or {} mixed_case_fields = mixed_case_fields or [] + field_labels = field_labels or fields + if len(field_labels) != len(fields): + raise ValueError(_("Field labels list %(labels)s has different number " + "of elements than fields list %(fields)s"), + {'labels': field_labels, 'fields': fields}) + if sortby_index is None: kwargs = {} else: - kwargs = {'sortby': fields[sortby_index]} - pt = prettytable.PrettyTable(fields, caching=False) + kwargs = {'sortby': field_labels[sortby_index]} + pt = prettytable.PrettyTable(field_labels) pt.align = 'l' for o in objs: @@ -165,7 +180,10 @@ def print_list(objs, fields, formatters=None, sortby_index=0, row.append(data) pt.add_row(row) - print(strutils.safe_encode(pt.get_string(**kwargs))) + if six.PY3: + print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) + else: + print(encodeutils.safe_encode(pt.get_string(**kwargs))) def print_dict(dct, dict_property="Property", wrap=0): @@ -175,7 +193,7 @@ def print_dict(dct, dict_property="Property", wrap=0): :param dict_property: name of the first column :param wrap: wrapping for the second column """ - pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) + pt = prettytable.PrettyTable([dict_property, 'Value']) pt.align = 'l' for k, v in six.iteritems(dct): # convert dict to str to check length @@ -193,7 +211,11 @@ def print_dict(dct, dict_property="Property", wrap=0): col1 = '' else: pt.add_row([k, v]) - print(strutils.safe_encode(pt.get_string())) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string()).decode()) + else: + print(encodeutils.safe_encode(pt.get_string())) def get_password(max_password_prompts=3): @@ -217,76 +239,16 @@ def get_password(max_password_prompts=3): return pw -def find_resource(manager, name_or_id, **find_args): - """Look for resource in a given manager. - - Used as a helper for the _find_* methods. - Example: - - def _find_hypervisor(cs, hypervisor): - #Get a hypervisor by name or ID. - return cliutils.find_resource(cs.hypervisors, hypervisor) - """ - # first try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid - try: - tmp_id = strutils.safe_encode(name_or_id) - - if uuidutils.is_uuid_like(tmp_id): - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # for str id which is not uuid - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - - try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - resource = getattr(manager, 'resource_class', None) - name_attr = resource.NAME_ATTR if resource else 'name' - kwargs = {name_attr: name_or_id} - kwargs.update(find_args) - return manager.find(**kwargs) - except exceptions.NotFound: - msg = _("No %(name)s with a name or " - "ID of '%(name_or_id)s' exists.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = _("Multiple %(name)s matches found for " - "'%(name_or_id)s', use an ID to be more specific.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - - def service_type(stype): """Adds 'service_type' attribute to decorated function. Usage: - @service_type('volume') - def mymethod(f): - ... + + .. code-block:: python + + @service_type('volume') + def mymethod(f): + ... """ def inner(f): f.service_type = stype diff --git a/heatclient/openstack/common/strutils.py b/heatclient/openstack/common/strutils.py deleted file mode 100644 index d6aef268..00000000 --- a/heatclient/openstack/common/strutils.py +++ /dev/null @@ -1,245 +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 heatclient.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]+") - - -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 = str(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): - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - 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 - - -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) diff --git a/heatclient/shell.py b/heatclient/shell.py index c3733aed..7dcfcd06 100644 --- a/heatclient/shell.py +++ b/heatclient/shell.py @@ -23,6 +23,8 @@ import sys import six import six.moves.urllib.parse as urlparse +from oslo.utils import encodeutils + from keystoneclient.auth.identity import v2 as v2_auth from keystoneclient.auth.identity import v3 as v3_auth from keystoneclient import discover @@ -35,7 +37,6 @@ from heatclient.common import utils from heatclient import exc from heatclient.openstack.common.gettextutils import _ from heatclient.openstack.common import importutils -from heatclient.openstack.common import strutils logger = logging.getLogger(__name__) osprofiler_profiler = importutils.try_import("osprofiler.profiler") @@ -662,7 +663,7 @@ def main(args=None): if '--debug' in args or '-d' in args: raise else: - print(strutils.safe_encode(six.text_type(e)), file=sys.stderr) + print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr) sys.exit(1) if __name__ == "__main__": diff --git a/heatclient/tests/test_shell.py b/heatclient/tests/test_shell.py index 2bd83b25..61f2aa75 100644 --- a/heatclient/tests/test_shell.py +++ b/heatclient/tests/test_shell.py @@ -27,11 +27,11 @@ import testtools import uuid from oslo.serialization import jsonutils +from oslo.utils import encodeutils from keystoneclient.fixture import v2 as ks_v2_fixture from keystoneclient.fixture import v3 as ks_v3_fixture -from heatclient.openstack.common import strutils from mox3 import mox from heatclient.common import http @@ -1806,7 +1806,7 @@ class ShellTestEvents(ShellBase): http.HTTPClient.json_request( 'GET', '/stacks/%s/resources/%s/events' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode( + parse.quote(encodeutils.safe_encode( resource_name), ''))).AndReturn((resp, resp_dict)) self.m.ReplayAll() @@ -1864,7 +1864,7 @@ class ShellTestEvents(ShellBase): 'GET', '/stacks/%s/resources/%s/events/%s' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode( + parse.quote(encodeutils.safe_encode( resource_name), ''), parse.quote(self.event_id_one, '') )).AndReturn((resp, resp_dict)) @@ -2053,7 +2053,7 @@ class ShellTestResources(ShellBase): 'GET', '/stacks/%s/resources/%s' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode( + parse.quote(encodeutils.safe_encode( resource_name), '') )).AndReturn((resp, resp_dict)) @@ -2099,7 +2099,7 @@ class ShellTestResources(ShellBase): 'POST', '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode( + parse.quote(encodeutils.safe_encode( resource_name), '') ), data={'message': 'Content'}).AndReturn((resp, '')) @@ -2125,7 +2125,7 @@ class ShellTestResources(ShellBase): 'POST', '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode( + parse.quote(encodeutils.safe_encode( resource_name), '') ), data=None).AndReturn((resp, '')) @@ -2192,7 +2192,7 @@ class ShellTestResources(ShellBase): 'POST', '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode( + parse.quote(encodeutils.safe_encode( resource_name), '') ), data={'message': 'Content'}).AndReturn((resp, '')) diff --git a/heatclient/v1/events.py b/heatclient/v1/events.py index f63d30c9..205e8aba 100644 --- a/heatclient/v1/events.py +++ b/heatclient/v1/events.py @@ -16,8 +16,9 @@ import six from six.moves.urllib import parse +from oslo.utils import encodeutils + from heatclient.openstack.common.apiclient import base -from heatclient.openstack.common import strutils from heatclient.v1 import stacks DEFAULT_PAGE_SIZE = 20 @@ -61,7 +62,7 @@ class EventManager(stacks.StackChildManager): stack_id = self._resolve_stack_id(stack_id) url = '/stacks/%s/resources/%s/events' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode(resource_name), '')) + parse.quote(encodeutils.safe_encode(resource_name), '')) if params: url += '?%s' % parse.urlencode(params, True) @@ -77,7 +78,7 @@ class EventManager(stacks.StackChildManager): stack_id = self._resolve_stack_id(stack_id) url_str = '/stacks/%s/resources/%s/events/%s' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode(resource_name), ''), + parse.quote(encodeutils.safe_encode(resource_name), ''), parse.quote(event_id, '')) resp, body = self.client.json_request('GET', url_str) return Event(self, body['event']) diff --git a/heatclient/v1/resource_types.py b/heatclient/v1/resource_types.py index 9ffb80b6..527e3bf7 100644 --- a/heatclient/v1/resource_types.py +++ b/heatclient/v1/resource_types.py @@ -13,8 +13,9 @@ from six.moves.urllib import parse +from oslo.utils import encodeutils + from heatclient.openstack.common.apiclient import base -from heatclient.openstack.common import strutils class ResourceType(base.Resource): @@ -43,12 +44,12 @@ class ResourceTypeManager(base.BaseManager): :param resource_type: name of the resource type to get the details for """ url_str = '/resource_types/%s' % ( - parse.quote(strutils.safe_encode(resource_type), '')) + parse.quote(encodeutils.safe_encode(resource_type), '')) resp, body = self.client.json_request('GET', url_str) return body def generate_template(self, resource_type): url_str = '/resource_types/%s/template' % ( - parse.quote(strutils.safe_encode(resource_type), '')) + parse.quote(encodeutils.safe_encode(resource_type), '')) resp, body = self.client.json_request('GET', url_str) return body diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index 82c62acb..24d9b189 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -15,8 +15,9 @@ from six.moves.urllib import parse +from oslo.utils import encodeutils + from heatclient.openstack.common.apiclient import base -from heatclient.openstack.common import strutils from heatclient.v1 import stacks DEFAULT_PAGE_SIZE = 20 @@ -57,7 +58,7 @@ class ResourceManager(stacks.StackChildManager): stack_id = self._resolve_stack_id(stack_id) url_str = '/stacks/%s/resources/%s' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode(resource_name), '')) + parse.quote(encodeutils.safe_encode(resource_name), '')) resp, body = self.client.json_request('GET', url_str) return Resource(self, body['resource']) @@ -70,7 +71,7 @@ class ResourceManager(stacks.StackChildManager): stack_id = self._resolve_stack_id(stack_id) url_str = '/stacks/%s/resources/%s/metadata' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode(resource_name), '')) + parse.quote(encodeutils.safe_encode(resource_name), '')) resp, body = self.client.json_request('GET', url_str) return body['metadata'] @@ -83,7 +84,7 @@ class ResourceManager(stacks.StackChildManager): stack_id = self._resolve_stack_id(stack_id) url_str = '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), - parse.quote(strutils.safe_encode(resource_name), '')) + parse.quote(encodeutils.safe_encode(resource_name), '')) resp, body = self.client.json_request('POST', url_str, data=data) return body @@ -92,6 +93,6 @@ class ResourceManager(stacks.StackChildManager): instead. """ url_str = '/resource_types/%s/template' % ( - parse.quote(strutils.safe_encode(resource_name), '')) + parse.quote(encodeutils.safe_encode(resource_name), '')) resp, body = self.client.json_request('GET', url_str) return body diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 9730a18d..1e233010 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -19,10 +19,10 @@ from six.moves.urllib import request import yaml from oslo.serialization import jsonutils +from oslo.utils import strutils from heatclient.common import template_utils from heatclient.common import utils -from heatclient.openstack.common import strutils import heatclient.exc as exc diff --git a/openstack-common.conf b/openstack-common.conf index 2b51a390..febc13f4 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,gettextutils,strutils,apiclient.base,apiclient.exceptions +modules=apiclient module=cliutils # The base module to hold the copy of openstack.common diff --git a/requirements.txt b/requirements.txt index 3651bbcb..60936c73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ argparse iso8601>=0.1.9 PrettyTable>=0.7,<0.8 oslo.serialization>=1.0.0 # Apache-2.0 +oslo.utils>=1.0.0 # Apache-2.0 python-keystoneclient>=0.11.1 PyYAML>=3.1.0 requests>=2.2.0,!=2.4.0