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
This commit is contained in:
Steven Hardy 2014-10-29 16:51:15 +00:00
parent aea6e7dcbc
commit 5259f00827
19 changed files with 331 additions and 443 deletions

View File

@ -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]

View File

@ -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'))

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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/"):

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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__":

View File

@ -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, ''))

View File

@ -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'])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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