catch up neutronclient change

Change-Id: I1354fe5378566dec66e7cac311a394cb5498c734
This commit is contained in:
Isaku Yamahata
2015-04-08 18:18:25 -07:00
parent 4291ce92df
commit e843dbd1b6
14 changed files with 947 additions and 469 deletions

View File

@@ -22,11 +22,12 @@ import logging
import os import os
from keystoneclient import access from keystoneclient import access
from keystoneclient import adapter
import requests import requests
from tackerclient.common import exceptions from tackerclient.common import exceptions
from tackerclient.common import utils from tackerclient.common import utils
from tackerclient.openstack.common.gettextutils import _ from tackerclient.i18n import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -39,12 +40,14 @@ else:
_requests_log_level = logging.WARNING _requests_log_level = logging.WARNING
logging.getLogger("requests").setLevel(_requests_log_level) logging.getLogger("requests").setLevel(_requests_log_level)
MAX_URI_LEN = 8192
class HTTPClient(object): class HTTPClient(object):
"""Handles the REST calls and responses, include authn.""" """Handles the REST calls and responses, include authn."""
USER_AGENT = 'python-tackerclient' USER_AGENT = 'python-tackerclient'
CONTENT_TYPE = 'application/json'
def __init__(self, username=None, user_id=None, def __init__(self, username=None, user_id=None,
tenant_name=None, tenant_id=None, tenant_name=None, tenant_id=None,
@@ -69,7 +72,6 @@ class HTTPClient(object):
self.auth_token = token self.auth_token = token
self.auth_tenant_id = None self.auth_tenant_id = None
self.auth_user_id = None self.auth_user_id = None
self.content_type = 'application/json'
self.endpoint_url = endpoint_url self.endpoint_url = endpoint_url
self.auth_strategy = auth_strategy self.auth_strategy = auth_strategy
self.log_credentials = log_credentials self.log_credentials = log_credentials
@@ -83,17 +85,8 @@ class HTTPClient(object):
kargs.setdefault('headers', kwargs.get('headers', {})) kargs.setdefault('headers', kwargs.get('headers', {}))
kargs['headers']['User-Agent'] = self.USER_AGENT kargs['headers']['User-Agent'] = self.USER_AGENT
if 'content_type' in kwargs:
kargs['headers']['Content-Type'] = kwargs['content_type']
kargs['headers']['Accept'] = kwargs['content_type']
else:
kargs['headers']['Content-Type'] = self.content_type
kargs['headers']['Accept'] = self.content_type
if 'body' in kwargs: if 'body' in kwargs:
kargs['body'] = kwargs['body'] kargs['body'] = kwargs['body']
args = utils.safe_encode_list(args)
kargs = utils.safe_encode_dict(kargs)
if self.log_credentials: if self.log_credentials:
log_kargs = kargs log_kargs = kargs
@@ -112,8 +105,7 @@ class HTTPClient(object):
_logger.debug("throwing ConnectionFailed : %s", e) _logger.debug("throwing ConnectionFailed : %s", e)
raise exceptions.ConnectionFailed(reason=e) raise exceptions.ConnectionFailed(reason=e)
utils.http_log_resp(_logger, resp, body) utils.http_log_resp(_logger, resp, body)
status_code = self.get_status_code(resp) if resp.status_code == 401:
if status_code == 401:
raise exceptions.Unauthorized(message=body) raise exceptions.Unauthorized(message=body)
return resp, body return resp, body
@@ -132,24 +124,40 @@ class HTTPClient(object):
elif not self.endpoint_url: elif not self.endpoint_url:
self.endpoint_url = self._get_endpoint_url() self.endpoint_url = self._get_endpoint_url()
def request(self, url, method, **kwargs): def request(self, url, method, body=None, headers=None, **kwargs):
kwargs.setdefault('headers', kwargs.get('headers', {})) """Request without authentication."""
kwargs['headers']['User-Agent'] = self.USER_AGENT
kwargs['headers']['Accept'] = 'application/json' content_type = kwargs.pop('content_type', None) or 'application/json'
if 'body' in kwargs: headers = headers or {}
kwargs['headers']['Content-Type'] = 'application/json' headers.setdefault('Accept', content_type)
kwargs['data'] = kwargs['body']
del kwargs['body'] if body:
headers.setdefault('Content-Type', content_type)
headers['User-Agent'] = self.USER_AGENT
resp = requests.request( resp = requests.request(
method, method,
url, url,
data=body,
headers=headers,
verify=self.verify_cert, verify=self.verify_cert,
timeout=self.timeout,
**kwargs) **kwargs)
return resp, resp.text return resp, resp.text
def _check_uri_length(self, action):
uri_len = len(self.endpoint_url) + len(action)
if uri_len > MAX_URI_LEN:
raise exceptions.RequestURITooLong(
excess=uri_len - MAX_URI_LEN)
def do_request(self, url, method, **kwargs): def do_request(self, url, method, **kwargs):
# Ensure client always has correct uri - do not guesstimate anything
self.authenticate_and_fetch_endpoint_url() self.authenticate_and_fetch_endpoint_url()
self._check_uri_length(url)
# Perform the request once. If we get a 401 back then it # Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to # might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail. # re-authenticate and try again. If it still fails, bail.
@@ -206,8 +214,7 @@ class HTTPClient(object):
body=json.dumps(body), body=json.dumps(body),
content_type="application/json", content_type="application/json",
allow_redirects=True) allow_redirects=True)
status_code = self.get_status_code(resp) if resp.status_code != 200:
if status_code != 200:
raise exceptions.Unauthorized(message=resp_body) raise exceptions.Unauthorized(message=resp_body)
if resp_body: if resp_body:
try: try:
@@ -250,7 +257,7 @@ class HTTPClient(object):
body = json.loads(body) body = json.loads(body)
for endpoint in body.get('endpoints', []): for endpoint in body.get('endpoints', []):
if (endpoint['type'] == 'servicevm' and if (endpoint['type'] == 'servicevm' and
endpoint.get('region') == self.region_name): endpoint.get('region') == self.region_name):
if self.endpoint_type not in endpoint: if self.endpoint_type not in endpoint:
raise exceptions.EndpointTypeNotFound( raise exceptions.EndpointTypeNotFound(
type_=self.endpoint_type) type_=self.endpoint_type)
@@ -264,13 +271,122 @@ class HTTPClient(object):
'auth_user_id': self.auth_user_id, 'auth_user_id': self.auth_user_id,
'endpoint_url': self.endpoint_url} 'endpoint_url': self.endpoint_url}
def get_status_code(self, response):
"""Returns the integer status code from the response.
Either a Webob.Response (used in testing) or requests.Response class SessionClient(adapter.Adapter):
is returned.
""" def request(self, *args, **kwargs):
if hasattr(response, 'status_int'): kwargs.setdefault('authenticated', False)
return response.status_int kwargs.setdefault('raise_exc', False)
content_type = kwargs.pop('content_type', None) or 'application/json'
headers = kwargs.setdefault('headers', {})
headers.setdefault('Accept', content_type)
try:
kwargs.setdefault('data', kwargs.pop('body'))
except KeyError:
pass
if kwargs.get('data'):
headers.setdefault('Content-Type', content_type)
resp = super(SessionClient, self).request(*args, **kwargs)
return resp, resp.text
def _check_uri_length(self, url):
uri_len = len(self.endpoint_url) + len(url)
if uri_len > MAX_URI_LEN:
raise exceptions.RequestURITooLong(
excess=uri_len - MAX_URI_LEN)
def do_request(self, url, method, **kwargs):
kwargs.setdefault('authenticated', True)
self._check_uri_length(url)
return self.request(url, method, **kwargs)
@property
def endpoint_url(self):
# NOTE(jamielennox): This is used purely by the CLI and should be
# removed when the CLI gets smarter.
return self.get_endpoint()
@property
def auth_token(self):
# NOTE(jamielennox): This is used purely by the CLI and should be
# removed when the CLI gets smarter.
return self.get_token()
def authenticate(self):
# NOTE(jamielennox): This is used purely by the CLI and should be
# removed when the CLI gets smarter.
self.get_token()
def get_auth_info(self):
auth_info = {'auth_token': self.auth_token,
'endpoint_url': self.endpoint_url}
# NOTE(jamielennox): This is the best we can do here. It will work
# with identity plugins which is the primary case but we should
# deprecate it's usage as much as possible.
try:
get_access = (self.auth or self.session.auth).get_access
except AttributeError:
pass
else: else:
return response.status_code auth_ref = get_access(self.session)
auth_info['auth_tenant_id'] = auth_ref.project_id
auth_info['auth_user_id'] = auth_ref.user_id
return auth_info
# FIXME(bklei): Should refactor this to use kwargs and only
# explicitly list arguments that are not None.
def construct_http_client(username=None,
user_id=None,
tenant_name=None,
tenant_id=None,
password=None,
auth_url=None,
token=None,
region_name=None,
timeout=None,
endpoint_url=None,
insecure=False,
endpoint_type='publicURL',
log_credentials=None,
auth_strategy='keystone',
ca_cert=None,
service_type='servicevm',
session=None,
**kwargs):
if session:
kwargs.setdefault('user_agent', 'python-tackerclient')
kwargs.setdefault('interface', endpoint_type)
return SessionClient(session=session,
service_type=service_type,
region_name=region_name,
**kwargs)
else:
# FIXME(bklei): username and password are now optional. Need
# to test that they were provided in this mode. Should also
# refactor to use kwargs.
return HTTPClient(username=username,
password=password,
tenant_id=tenant_id,
tenant_name=tenant_name,
user_id=user_id,
auth_url=auth_url,
token=token,
endpoint_url=endpoint_url,
insecure=insecure,
timeout=timeout,
region_name=region_name,
endpoint_type=endpoint_type,
service_type=service_type,
ca_cert=ca_cert,
log_credentials=log_credentials,
auth_strategy=auth_strategy)

View File

@@ -1,26 +0,0 @@
# Copyright 2011 VMware, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import gettext
t = gettext.translation('tackerclient', fallback=True)
try:
ugettext = t.ugettext # Python 2
except AttributeError:
ugettext = t.gettext # Python 3
def _(msg):
return ugettext(msg)

View File

@@ -27,8 +27,7 @@ LOG = logging.getLogger(__name__)
class ClientCache(object): class ClientCache(object):
"""Descriptor class for caching created client handles. """Descriptor class for caching created client handles."""
"""
def __init__(self, factory): def __init__(self, factory):
self.factory = factory self.factory = factory
@@ -42,8 +41,7 @@ class ClientCache(object):
class ClientManager(object): class ClientManager(object):
"""Manages access to API clients, including authentication. """Manages access to API clients, including authentication."""
"""
tacker = ClientCache(tacker_client.make_client) tacker = ClientCache(tacker_client.make_client)
def __init__(self, token=None, url=None, def __init__(self, token=None, url=None,
@@ -61,6 +59,11 @@ class ClientManager(object):
ca_cert=None, ca_cert=None,
log_credentials=False, log_credentials=False,
service_type=None, service_type=None,
timeout=None,
retries=0,
raise_errors=True,
session=None,
auth=None,
): ):
self._token = token self._token = token
self._url = url self._url = url
@@ -79,11 +82,16 @@ class ClientManager(object):
self._insecure = insecure self._insecure = insecure
self._ca_cert = ca_cert self._ca_cert = ca_cert
self._log_credentials = log_credentials self._log_credentials = log_credentials
self._timeout = timeout
self._retries = retries
self._raise_errors = raise_errors
self._session = session
self._auth = auth
return return
def initialize(self): def initialize(self):
if not self._url: if not self._url:
httpclient = client.HTTPClient( httpclient = client.construct_http_client(
username=self._username, username=self._username,
user_id=self._user_id, user_id=self._user_id,
tenant_name=self._tenant_name, tenant_name=self._tenant_name,
@@ -95,6 +103,9 @@ class ClientManager(object):
endpoint_type=self._endpoint_type, endpoint_type=self._endpoint_type,
insecure=self._insecure, insecure=self._insecure,
ca_cert=self._ca_cert, ca_cert=self._ca_cert,
timeout=self._timeout,
session=self._session,
auth=self._auth,
log_credentials=self._log_credentials) log_credentials=self._log_credentials)
httpclient.authenticate() httpclient.authenticate()
# Populate other password flow attributes # Populate other password flow attributes

View File

@@ -14,16 +14,11 @@
# under the License. # under the License.
# #
"""
OpenStack base command
"""
from cliff import command from cliff import command
class OpenStackCommand(command.Command): class OpenStackCommand(command.Command):
"""Base class for OpenStack commands """Base class for OpenStack commands."""
"""
api = None api = None

View File

@@ -15,7 +15,7 @@
EXT_NS = '_extension_ns' EXT_NS = '_extension_ns'
XML_NS_V10 = 'http://openstack.org/tacker/api/v1.0' XML_NS_V20 = 'http://openstack.org/tacker/api/v1.0'
XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"
XSI_ATTR = "xsi:nil" XSI_ATTR = "xsi:nil"
XSI_NIL_ATTR = "xmlns:xsi" XSI_NIL_ATTR = "xmlns:xsi"
@@ -33,6 +33,7 @@ TYPE_FLOAT = "float"
TYPE_LIST = "list" TYPE_LIST = "list"
TYPE_DICT = "dict" TYPE_DICT = "dict"
PLURALS = {'templates': 'template', PLURALS = {'templates': 'template',
'devices': 'device', 'devices': 'device',
'services': 'service'} 'services': 'service'}

View File

@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from tackerclient.common import _ from tackerclient.i18n import _
""" """
Tacker base exception handling. Tacker base exception handling.
@@ -30,12 +30,11 @@ Exceptions are classified into three categories:
class TackerException(Exception): class TackerException(Exception):
"""Base Tacker Exception """Base Tacker Exception.
To correctly use this class, inherit from it and define To correctly use this class, inherit from it and define
a 'message' property. That message will get printf'd a 'message' property. That message will get printf'd
with the keyword arguments provided to the constructor. with the keyword arguments provided to the constructor.
""" """
message = _("An unknown exception occurred.") message = _("An unknown exception occurred.")
@@ -60,6 +59,8 @@ class TackerClientException(TackerException):
blocks. The actual error message is the one generated on the server side. blocks. The actual error message is the one generated on the server side.
""" """
status_code = 0
def __init__(self, message=None, **kwargs): def __init__(self, message=None, **kwargs):
if 'status_code' in kwargs: if 'status_code' in kwargs:
self.status_code = kwargs['status_code'] self.status_code = kwargs['status_code']
@@ -139,6 +140,10 @@ class IpAddressInUseClient(Conflict):
pass pass
class InvalidIpForNetworkClient(BadRequest):
pass
class OverQuotaClient(Conflict): class OverQuotaClient(Conflict):
pass pass
@@ -153,6 +158,10 @@ class IpAddressGenerationFailureClient(Conflict):
pass pass
class MacAddressInUseClient(Conflict):
pass
class ExternalIpAddressExhaustedClient(BadRequest): class ExternalIpAddressExhaustedClient(BadRequest):
pass pass
@@ -212,8 +221,8 @@ class CommandError(TackerCLIError):
class UnsupportedVersion(TackerCLIError): class UnsupportedVersion(TackerCLIError):
"""Indicates that the user is trying to use an unsupported """Indicates that the user is trying to use an unsupported version of
version of the API the API.
""" """
pass pass

View File

@@ -0,0 +1,86 @@
# Copyright 2015 Rackspace Hosting Inc.
# 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.
#
from stevedore import extension
from tackerclient.tacker import v1_0 as tackerV10
def _discover_via_entry_points():
emgr = extension.ExtensionManager('tackerclient.extension',
invoke_on_load=False)
return ((ext.name, ext.plugin) for ext in emgr)
class TackerClientExtension(tackerV10.TackerCommand):
pagination_support = False
_formatters = {}
sorting_support = False
class ClientExtensionShow(TackerClientExtension, tackerV10.ShowCommand):
def get_data(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionShow, self).get_data(parsed_args)
class ClientExtensionList(TackerClientExtension, tackerV10.ListCommand):
def get_data(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionList, self).get_data(parsed_args)
class ClientExtensionDelete(TackerClientExtension, tackerV10.DeleteCommand):
def run(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionDelete, self).run(parsed_args)
class ClientExtensionCreate(TackerClientExtension, tackerV10.CreateCommand):
def get_data(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionCreate, self).get_data(parsed_args)
class ClientExtensionUpdate(TackerClientExtension, tackerV10.UpdateCommand):
def run(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionUpdate, self).run(parsed_args)

View File

@@ -12,23 +12,23 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
#
###
### Codes from tacker wsgi
###
import logging import logging
from xml.etree import ElementTree as etree from xml.etree import ElementTree as etree
from xml.parsers import expat from xml.parsers import expat
from oslo.serialization import jsonutils
import six
from tackerclient.common import constants from tackerclient.common import constants
from tackerclient.common import exceptions as exception from tackerclient.common import exceptions as exception
from tackerclient.openstack.common.gettextutils import _ from tackerclient.i18n import _
from tackerclient.openstack.common import jsonutils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
if six.PY3:
long = int
class ActionDispatcher(object): class ActionDispatcher(object):
"""Maps method name to local methods through action name.""" """Maps method name to local methods through action name."""
@@ -58,7 +58,7 @@ class JSONDictSerializer(DictSerializer):
def default(self, data): def default(self, data):
def sanitizer(obj): def sanitizer(obj):
return unicode(obj) return six.text_type(obj)
return jsonutils.dumps(data, default=sanitizer) return jsonutils.dumps(data, default=sanitizer)
@@ -67,16 +67,16 @@ class XMLDictSerializer(DictSerializer):
def __init__(self, metadata=None, xmlns=None): def __init__(self, metadata=None, xmlns=None):
"""XMLDictSerializer constructor. """XMLDictSerializer constructor.
:param metadata: information needed to deserialize xml into :param metadata: information needed to deserialize XML into
a dictionary. a dictionary.
:param xmlns: XML namespace to include with serialized xml :param xmlns: XML namespace to include with serialized XML
""" """
super(XMLDictSerializer, self).__init__() super(XMLDictSerializer, self).__init__()
self.metadata = metadata or {} self.metadata = metadata or {}
if not xmlns: if not xmlns:
xmlns = self.metadata.get('xmlns') xmlns = self.metadata.get('xmlns')
if not xmlns: if not xmlns:
xmlns = constants.XML_NS_V10 xmlns = constants.XML_NS_V20
self.xmlns = xmlns self.xmlns = xmlns
def default(self, data): def default(self, data):
@@ -93,13 +93,13 @@ class XMLDictSerializer(DictSerializer):
root_key = constants.VIRTUAL_ROOT_KEY root_key = constants.VIRTUAL_ROOT_KEY
root_value = None root_value = None
else: else:
link_keys = [k for k in data.iterkeys() or [] link_keys = [k for k in six.iterkeys(data) or []
if k.endswith('_links')] if k.endswith('_links')]
if link_keys: if link_keys:
links = data.pop(link_keys[0], None) links = data.pop(link_keys[0], None)
has_atom = True has_atom = True
root_key = (len(data) == 1 and root_key = (len(data) == 1 and
data.keys()[0] or constants.VIRTUAL_ROOT_KEY) list(data.keys())[0] or constants.VIRTUAL_ROOT_KEY)
root_value = data.get(root_key, data) root_value = data.get(root_key, data)
doc = etree.Element("_temp_root") doc = etree.Element("_temp_root")
used_prefixes = [] used_prefixes = []
@@ -122,8 +122,8 @@ class XMLDictSerializer(DictSerializer):
self._add_xmlns(node, used_prefixes, has_atom) self._add_xmlns(node, used_prefixes, has_atom)
return etree.tostring(node, encoding='UTF-8') return etree.tostring(node, encoding='UTF-8')
#NOTE (ameade): the has_atom should be removed after all of the # NOTE(ameade): the has_atom should be removed after all of the
# xml serializers and view builders have been updated to the current # XML serializers and view builders have been updated to the current
# spec that required all responses include the xmlns:atom, the has_atom # spec that required all responses include the xmlns:atom, the has_atom
# flag is to prevent current tests from breaking # flag is to prevent current tests from breaking
def _add_xmlns(self, node, used_prefixes, has_atom=False): def _add_xmlns(self, node, used_prefixes, has_atom=False):
@@ -142,7 +142,7 @@ class XMLDictSerializer(DictSerializer):
result = etree.SubElement(parent, nodename) result = etree.SubElement(parent, nodename)
if ":" in nodename: if ":" in nodename:
used_prefixes.append(nodename.split(":", 1)[0]) used_prefixes.append(nodename.split(":", 1)[0])
#TODO(bcwaldon): accomplish this without a type-check # TODO(bcwaldon): accomplish this without a type-check
if isinstance(data, list): if isinstance(data, list):
if not data: if not data:
result.set( result.set(
@@ -158,7 +158,7 @@ class XMLDictSerializer(DictSerializer):
for item in data: for item in data:
self._to_xml_node(result, metadata, singular, item, self._to_xml_node(result, metadata, singular, item,
used_prefixes) used_prefixes)
#TODO(bcwaldon): accomplish this without a type-check # TODO(bcwaldon): accomplish this without a type-check
elif isinstance(data, dict): elif isinstance(data, dict):
if not data: if not data:
result.set( result.set(
@@ -191,13 +191,10 @@ class XMLDictSerializer(DictSerializer):
result.set( result.set(
constants.TYPE_ATTR, constants.TYPE_ATTR,
constants.TYPE_FLOAT) constants.TYPE_FLOAT)
LOG.debug(_("Data %(data)s type is %(type)s"), LOG.debug("Data %(data)s type is %(type)s",
{'data': data, {'data': data,
'type': type(data)}) 'type': type(data)})
if isinstance(data, str): result.text = six.text_type(data)
result.text = unicode(data, 'utf-8')
else:
result.text = unicode(data)
return result return result
def _create_link_nodes(self, xml_doc, links): def _create_link_nodes(self, xml_doc, links):
@@ -235,14 +232,14 @@ class XMLDeserializer(TextDeserializer):
def __init__(self, metadata=None): def __init__(self, metadata=None):
"""XMLDeserializer constructor. """XMLDeserializer constructor.
:param metadata: information needed to deserialize xml into :param metadata: information needed to deserialize XML into
a dictionary. a dictionary.
""" """
super(XMLDeserializer, self).__init__() super(XMLDeserializer, self).__init__()
self.metadata = metadata or {} self.metadata = metadata or {}
xmlns = self.metadata.get('xmlns') xmlns = self.metadata.get('xmlns')
if not xmlns: if not xmlns:
xmlns = constants.XML_NS_V10 xmlns = constants.XML_NS_V20
self.xmlns = xmlns self.xmlns = xmlns
def _get_key(self, tag): def _get_key(self, tag):
@@ -290,7 +287,7 @@ class XMLDeserializer(TextDeserializer):
parseError = False parseError = False
# Python2.7 # Python2.7
if (hasattr(etree, 'ParseError') and if (hasattr(etree, 'ParseError') and
isinstance(e, getattr(etree, 'ParseError'))): isinstance(e, getattr(etree, 'ParseError'))):
parseError = True parseError = True
# Python2.6 # Python2.6
elif isinstance(e, expat.ExpatError): elif isinstance(e, expat.ExpatError):
@@ -340,9 +337,9 @@ class XMLDeserializer(TextDeserializer):
result = dict() result = dict()
for attr in node.keys(): for attr in node.keys():
if (attr == 'xmlns' or if (attr == 'xmlns' or
attr.startswith('xmlns:') or attr.startswith('xmlns:') or
attr == constants.XSI_ATTR or attr == constants.XSI_ATTR or
attr == constants.TYPE_ATTR): attr == constants.TYPE_ATTR):
continue continue
result[self._get_key(attr)] = node.get(attr) result[self._get_key(attr)] = node.get(attr)
children = list(node) children = list(node)
@@ -392,7 +389,6 @@ class Serializer(object):
"""Deserialize a string to a dictionary. """Deserialize a string to a dictionary.
The string must be in the format of a supported MIME type. The string must be in the format of a supported MIME type.
""" """
return self.get_deserialize_handler(content_type).deserialize( return self.get_deserialize_handler(content_type).deserialize(
datastring) datastring)

View File

@@ -17,21 +17,22 @@
"""Utilities and helper functions.""" """Utilities and helper functions."""
import datetime import argparse
import json
import logging import logging
import os import os
import sys
from tackerclient.common import _ from oslo.utils import encodeutils
from oslo.utils import importutils
import six
from tackerclient.common import exceptions from tackerclient.common import exceptions
from tackerclient.openstack.common import strutils from tackerclient.i18n import _
def env(*vars, **kwargs): def env(*vars, **kwargs):
"""Returns the first environment variable set. """Returns the first environment variable set.
if none are non-empty, defaults to '' or keyword arg default. If none are non-empty, defaults to '' or keyword arg default.
""" """
for v in vars: for v in vars:
value = os.environ.get(v) value = os.environ.get(v)
@@ -40,52 +41,8 @@ def env(*vars, **kwargs):
return kwargs.get('default', '') return kwargs.get('default', '')
def to_primitive(value):
if isinstance(value, list) or isinstance(value, tuple):
o = []
for v in value:
o.append(to_primitive(v))
return o
elif isinstance(value, dict):
o = {}
for k, v in value.iteritems():
o[k] = to_primitive(v)
return o
elif isinstance(value, datetime.datetime):
return str(value)
elif hasattr(value, 'iteritems'):
return to_primitive(dict(value.iteritems()))
elif hasattr(value, '__iter__'):
return to_primitive(list(value))
else:
return value
def dumps(value, indent=None):
try:
return json.dumps(value, indent=indent)
except TypeError:
pass
return json.dumps(to_primitive(value))
def loads(s):
return json.loads(s)
def import_class(import_str):
"""Returns a class from a string including module and class.
:param import_str: a string representation of the class name
:rtype: the requested class
"""
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
def get_client_class(api_name, version, version_map): def get_client_class(api_name, version, version_map):
"""Returns the client class for the requested API version """Returns the client class for the requested API version.
:param api_name: the name of the API, e.g. 'compute', 'image', etc :param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version :param version: the requested API version
@@ -101,10 +58,10 @@ def get_client_class(api_name, version, version_map):
'map_keys': ', '.join(version_map.keys())} 'map_keys': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg) raise exceptions.UnsupportedVersion(msg)
return import_class(client_path) return importutils.import_class(client_path)
def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): def get_item_properties(item, fields, mixed_case_fields=(), formatters=None):
"""Return a tuple containing the item properties. """Return a tuple containing the item properties.
:param item: a single item resource (e.g. Server, Tenant, etc) :param item: a single item resource (e.g. Server, Tenant, etc)
@@ -113,6 +70,9 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
:param formatters: dictionary mapping field names to callables :param formatters: dictionary mapping field names to callables
to format the values to format the values
""" """
if formatters is None:
formatters = {}
row = [] row = []
for field in fields: for field in fields:
@@ -136,22 +96,17 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
def str2bool(strbool): def str2bool(strbool):
if strbool is None: if strbool is None:
return None return None
else: return strbool.lower() == 'true'
return strbool.lower() == 'true'
def str2dict(strdict): def str2dict(strdict):
'''Convert key1=value1,key2=value2,... string into dictionary. """Convert key1=value1,key2=value2,... string into dictionary.
:param strdict: key1=value1,key2=value2 :param strdict: key1=value1,key2=value2
''' """
_info = {} if not strdict:
if not strdict: return {}
return _info return dict([kv.split('=', 1) for kv in strdict.split(',')])
for kv_str in strdict.split(","):
k, v = kv_str.split("=", 1)
_info.update({k: v})
return _info
def http_log_req(_logger, args, kwargs): def http_log_req(_logger, args, kwargs):
@@ -171,27 +126,27 @@ def http_log_req(_logger, args, kwargs):
if 'body' in kwargs and kwargs['body']: if 'body' in kwargs and kwargs['body']:
string_parts.append(" -d '%s'" % (kwargs['body'])) string_parts.append(" -d '%s'" % (kwargs['body']))
string_parts = safe_encode_list(string_parts) req = encodeutils.safe_encode("".join(string_parts))
_logger.debug(_("\nREQ: %s\n"), "".join(string_parts)) _logger.debug("\nREQ: %s\n", req)
def http_log_resp(_logger, resp, body): def http_log_resp(_logger, resp, body):
if not _logger.isEnabledFor(logging.DEBUG): if not _logger.isEnabledFor(logging.DEBUG):
return return
_logger.debug(_("RESP:%(code)s %(headers)s %(body)s\n"), _logger.debug("RESP:%(code)s %(headers)s %(body)s\n",
{'code': resp.status_code, {'code': resp.status_code,
'headers': resp.headers, 'headers': resp.headers,
'body': body}) 'body': body})
def _safe_encode_without_obj(data): def _safe_encode_without_obj(data):
if isinstance(data, basestring): if isinstance(data, six.string_types):
return strutils.safe_encode(data) return encodeutils.safe_encode(data)
return data return data
def safe_encode_list(data): def safe_encode_list(data):
return map(_safe_encode_without_obj, data) return list(map(_safe_encode_without_obj, data))
def safe_encode_dict(data): def safe_encode_dict(data):
@@ -203,4 +158,16 @@ def safe_encode_dict(data):
return (k, safe_encode_dict(v)) return (k, safe_encode_dict(v))
return (k, _safe_encode_without_obj(v)) return (k, _safe_encode_without_obj(v))
return dict(map(_encode_item, data.items())) return dict(list(map(_encode_item, data.items())))
def add_boolean_argument(parser, name, **kwargs):
for keyword in ('metavar', 'choices'):
kwargs.pop(keyword, None)
default = kwargs.pop('default', argparse.SUPPRESS)
parser.add_argument(
name,
metavar='{True,False}',
choices=['True', 'true', 'False', 'false'],
default=default,
**kwargs)

View File

@@ -16,7 +16,7 @@
import netaddr import netaddr
from tackerclient.common import exceptions from tackerclient.common import exceptions
from tackerclient.openstack.common.gettextutils import _ from tackerclient.i18n import _
def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None): def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
@@ -29,7 +29,7 @@ def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
else: else:
int_val = val int_val = val
if ((min_value is None or min_value <= int_val) and if ((min_value is None or min_value <= int_val) and
(max_value is None or int_val <= max_value)): (max_value is None or int_val <= max_value)):
return return
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass

28
tackerclient/i18n.py Normal file
View File

@@ -0,0 +1,28 @@
# 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 import i18n
_translators = i18n.TranslatorFactory(domain='tackerclient')
# 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

@@ -21,18 +21,30 @@ Command-line interface to the Tacker APIs
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import getpass
import inspect
import itertools
import logging import logging
import os import os
import sys import sys
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import discover
from keystoneclient.openstack.common.apiclient import exceptions as ks_exc
from keystoneclient import session
from oslo.utils import encodeutils
import six.moves.urllib.parse as urlparse
from cliff import app from cliff import app
from cliff import commandmanager from cliff import commandmanager
from tackerclient.common import clientmanager from tackerclient.common import clientmanager
from tackerclient.common import command as openstack_command
from tackerclient.common import exceptions as exc from tackerclient.common import exceptions as exc
from tackerclient.common import extension as client_extension
from tackerclient.common import utils from tackerclient.common import utils
from tackerclient.openstack.common.gettextutils import _ from tackerclient.i18n import _
from tackerclient.openstack.common import strutils
from tackerclient.tacker.v1_0 import extension from tackerclient.tacker.v1_0 import extension
from tackerclient.tacker.v1_0.vm import device from tackerclient.tacker.v1_0.vm import device
from tackerclient.tacker.v1_0.vm import device_template from tackerclient.tacker.v1_0.vm import device_template
@@ -70,7 +82,23 @@ def env(*_vars, **kwargs):
return kwargs.get('default', '') return kwargs.get('default', '')
def check_non_negative_int(value):
try:
value = int(value)
except ValueError:
raise argparse.ArgumentTypeError(_("invalid int value: %r") % value)
if value < 0:
raise argparse.ArgumentTypeError(_("input value %d is negative") %
value)
return value
class BashCompletionCommand(openstack_command.OpenStackCommand):
"""Prints all of the commands and options for bash-completion."""
resource = "bash_completion"
COMMAND_V1 = { COMMAND_V1 = {
'bash-completion': BashCompletionCommand,
'ext-list': extension.ListExt, 'ext-list': extension.ListExt,
'ext-show': extension.ShowExt, 'ext-show': extension.ShowExt,
'device-template-create': device_template.CreateDeviceTemplate, 'device-template-create': device_template.CreateDeviceTemplate,
@@ -134,6 +162,11 @@ class TackerShell(app.App):
for k, v in self.commands[apiversion].items(): for k, v in self.commands[apiversion].items():
self.command_manager.add_command(k, v) self.command_manager.add_command(k, v)
self._register_extensions(VERSION)
# Pop the 'complete' to correct the outputs of 'tacker help'.
self.command_manager.commands.pop('complete')
# This is instantiated in initialize_app() only when using # This is instantiated in initialize_app() only when using
# password flow auth # password flow auth
self.auth_client = None self.auth_client = None
@@ -169,20 +202,64 @@ class TackerShell(app.App):
action='store_const', action='store_const',
dest='verbose_level', dest='verbose_level',
const=0, const=0,
help=_('Suppress output except warnings and errors')) help=_('Suppress output except warnings and errors.'))
parser.add_argument( parser.add_argument(
'-h', '--help', '-h', '--help',
action=HelpAction, action=HelpAction,
nargs=0, nargs=0,
default=self, # tricky default=self, # tricky
help=_("Show this help message and exit")) help=_("Show this help message and exit."))
# Global arguments parser.add_argument(
'-r', '--retries',
metavar="NUM",
type=check_non_negative_int,
default=0,
help=_("How many times the request to the Tacker server should "
"be retried if it fails."))
# FIXME(bklei): this method should come from python-keystoneclient
self._append_global_identity_args(parser)
return parser
def _append_global_identity_args(self, parser):
# FIXME(bklei): these are global identity (Keystone) arguments which
# should be consistent and shared by all service clients. Therefore,
# they should be provided by python-keystoneclient. We will need to
# refactor this code once this functionality is available in
# python-keystoneclient.
#
# Note: At that time we'll need to decide if we can just abandon
# the deprecated args (--service-type and --endpoint-type).
parser.add_argument(
'--os-service-type', metavar='<os-service-type>',
default=env('OS_SERVICEVM_SERVICE_TYPE', default='servicevm'),
help=_('Defaults to env[OS_SERVICEVM_SERVICE_TYPE] or servicevm.'))
parser.add_argument(
'--os-endpoint-type', metavar='<os-endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help=_('Defaults to env[OS_ENDPOINT_TYPE] or publicURL.'))
# FIXME(bklei): --service-type is deprecated but kept in for
# backward compatibility.
parser.add_argument(
'--service-type', metavar='<service-type>',
default=env('OS_SERVICEVM_SERVICE_TYPE', default='servicevm'),
help=_('DEPRECATED! Use --os-service-type.'))
# FIXME(bklei): --endpoint-type is deprecated but kept in for
# backward compatibility.
parser.add_argument(
'--endpoint-type', metavar='<endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help=_('DEPRECATED! Use --os-endpoint-type.'))
parser.add_argument( parser.add_argument(
'--os-auth-strategy', metavar='<auth-strategy>', '--os-auth-strategy', metavar='<auth-strategy>',
default=env('OS_AUTH_STRATEGY', default='keystone'), default=env('OS_AUTH_STRATEGY', default='keystone'),
help=_('Authentication strategy (Env: OS_AUTH_STRATEGY' help=_('DEPRECATED! Only keystone is supported.'))
', default keystone). For now, any other value will'
' disable the authentication'))
parser.add_argument( parser.add_argument(
'--os_auth_strategy', '--os_auth_strategy',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
@@ -190,28 +267,49 @@ class TackerShell(app.App):
parser.add_argument( parser.add_argument(
'--os-auth-url', metavar='<auth-url>', '--os-auth-url', metavar='<auth-url>',
default=env('OS_AUTH_URL'), default=env('OS_AUTH_URL'),
help=_('Authentication URL (Env: OS_AUTH_URL)')) help=_('Authentication URL, defaults to env[OS_AUTH_URL].'))
parser.add_argument( parser.add_argument(
'--os_auth_url', '--os_auth_url',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument( project_name_group = parser.add_mutually_exclusive_group()
project_name_group.add_argument(
'--os-tenant-name', metavar='<auth-tenant-name>', '--os-tenant-name', metavar='<auth-tenant-name>',
default=env('OS_TENANT_NAME'), default=env('OS_TENANT_NAME'),
help=_('Authentication tenant name (Env: OS_TENANT_NAME)')) help=_('Authentication tenant name, defaults to '
'env[OS_TENANT_NAME].'))
project_name_group.add_argument(
'--os-project-name',
metavar='<auth-project-name>',
default=utils.env('OS_PROJECT_NAME'),
help='Another way to specify tenant name. '
'This option is mutually exclusive with '
' --os-tenant-name. '
'Defaults to env[OS_PROJECT_NAME].')
parser.add_argument( parser.add_argument(
'--os_tenant_name', '--os_tenant_name',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument( project_id_group = parser.add_mutually_exclusive_group()
project_id_group.add_argument(
'--os-tenant-id', metavar='<auth-tenant-id>', '--os-tenant-id', metavar='<auth-tenant-id>',
default=env('OS_TENANT_ID'), default=env('OS_TENANT_ID'),
help=_('Authentication tenant ID (Env: OS_TENANT_ID)')) help=_('Authentication tenant ID, defaults to '
'env[OS_TENANT_ID].'))
project_id_group.add_argument(
'--os-project-id',
metavar='<auth-project-id>',
default=utils.env('OS_PROJECT_ID'),
help='Another way to specify tenant ID. '
'This option is mutually exclusive with '
' --os-tenant-id. '
'Defaults to env[OS_PROJECT_ID].')
parser.add_argument( parser.add_argument(
'--os-username', metavar='<auth-username>', '--os-username', metavar='<auth-username>',
default=utils.env('OS_USERNAME'), default=utils.env('OS_USERNAME'),
help=_('Authentication username (Env: OS_USERNAME)')) help=_('Authentication username, defaults to env[OS_USERNAME].'))
parser.add_argument( parser.add_argument(
'--os_username', '--os_username',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
@@ -222,54 +320,115 @@ class TackerShell(app.App):
help=_('Authentication user ID (Env: OS_USER_ID)')) help=_('Authentication user ID (Env: OS_USER_ID)'))
parser.add_argument( parser.add_argument(
'--os-password', metavar='<auth-password>', '--os_user_id',
default=utils.env('OS_PASSWORD'),
help=_('Authentication password (Env: OS_PASSWORD)'))
parser.add_argument(
'--os_password',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument( parser.add_argument(
'--os-region-name', metavar='<auth-region-name>', '--os-user-domain-id',
default=env('OS_REGION_NAME'), metavar='<auth-user-domain-id>',
help=_('Authentication region name (Env: OS_REGION_NAME)')) default=utils.env('OS_USER_DOMAIN_ID'),
help='OpenStack user domain ID. '
'Defaults to env[OS_USER_DOMAIN_ID].')
parser.add_argument( parser.add_argument(
'--os_region_name', '--os_user_domain_id',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument( parser.add_argument(
'--os-token', metavar='<token>', '--os-user-domain-name',
default=env('OS_TOKEN'), metavar='<auth-user-domain-name>',
help=_('Defaults to env[OS_TOKEN]')) default=utils.env('OS_USER_DOMAIN_NAME'),
help='OpenStack user domain name. '
'Defaults to env[OS_USER_DOMAIN_NAME].')
parser.add_argument( parser.add_argument(
'--os_token', '--os_user_domain_name',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument( parser.add_argument(
'--service-type', metavar='<service-type>', '--os_project_id',
default=env('OS_SERVICEVM_SERVICE_TYPE', default='servicevm'),
help=_('Defaults to env[OS_SERVICEVM_SERVICE_TYPE] or servicevm.'))
parser.add_argument(
'--endpoint-type', metavar='<endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help=_('Defaults to env[OS_ENDPOINT_TYPE] or publicURL.'))
parser.add_argument(
'--os-url', metavar='<url>',
default=env('OS_URL'),
help=_('Defaults to env[OS_URL]'))
parser.add_argument(
'--os_url',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument(
'--os_project_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-project-domain-id',
metavar='<auth-project-domain-id>',
default=utils.env('OS_PROJECT_DOMAIN_ID'),
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
parser.add_argument(
'--os-project-domain-name',
metavar='<auth-project-domain-name>',
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
parser.add_argument(
'--os-cert',
metavar='<certificate>',
default=utils.env('OS_CERT'),
help=_("Path of certificate file to use in SSL "
"connection. This file can optionally be "
"prepended with the private key. Defaults "
"to env[OS_CERT]."))
parser.add_argument( parser.add_argument(
'--os-cacert', '--os-cacert',
metavar='<ca-certificate>', metavar='<ca-certificate>',
default=env('OS_CACERT', default=None), default=env('OS_CACERT', default=None),
help=_("Specify a CA bundle file to use in " help=_("Specify a CA bundle file to use in "
"verifying a TLS (https) server certificate. " "verifying a TLS (https) server certificate. "
"Defaults to env[OS_CACERT]")) "Defaults to env[OS_CACERT]."))
parser.add_argument(
'--os-key',
metavar='<key>',
default=utils.env('OS_KEY'),
help=_("Path of client key to use in SSL "
"connection. This option is not necessary "
"if your key is prepended to your certificate "
"file. Defaults to env[OS_KEY]."))
parser.add_argument(
'--os-password', metavar='<auth-password>',
default=utils.env('OS_PASSWORD'),
help=_('Authentication password, defaults to env[OS_PASSWORD].'))
parser.add_argument(
'--os_password',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-region-name', metavar='<auth-region-name>',
default=env('OS_REGION_NAME'),
help=_('Authentication region name, defaults to '
'env[OS_REGION_NAME].'))
parser.add_argument(
'--os_region_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-token', metavar='<token>',
default=env('OS_TOKEN'),
help=_('Authentication token, defaults to env[OS_TOKEN].'))
parser.add_argument(
'--os_token',
help=argparse.SUPPRESS)
parser.add_argument(
'--http-timeout', metavar='<seconds>',
default=env('OS_NETWORK_TIMEOUT', default=None), type=float,
help=_('Timeout in seconds to wait for an HTTP response. Defaults '
'to env[OS_NETWORK_TIMEOUT] or None if not specified.'))
parser.add_argument(
'--os-url', metavar='<url>',
default=env('OS_URL'),
help=_('Defaults to env[OS_URL].'))
parser.add_argument(
'--os_url',
help=argparse.SUPPRESS)
parser.add_argument( parser.add_argument(
'--insecure', '--insecure',
@@ -280,8 +439,6 @@ class TackerShell(app.App):
"not be verified against any certificate authorities. " "not be verified against any certificate authorities. "
"This option should be used with caution.")) "This option should be used with caution."))
return parser
def _bash_completion(self): def _bash_completion(self):
"""Prints all of the commands and options for bash-completion.""" """Prints all of the commands and options for bash-completion."""
commands = set() commands = set()
@@ -297,6 +454,26 @@ class TackerShell(app.App):
options.add(option) options.add(option)
print(' '.join(commands | options)) print(' '.join(commands | options))
def _register_extensions(self, version):
for name, module in itertools.chain(
client_extension._discover_via_entry_points()):
self._extend_shell_commands(module, version)
def _extend_shell_commands(self, module, version):
classes = inspect.getmembers(module, inspect.isclass)
for cls_name, cls in classes:
if (issubclass(cls, client_extension.TackerClientExtension) and
hasattr(cls, 'shell_command')):
cmd = cls.shell_command
if hasattr(cls, 'versions'):
if version not in cls.versions:
continue
try:
self.command_manager.add_command(cmd, cls)
self.commands[version][cmd] = cls
except TypeError:
pass
def run(self, argv): def run(self, argv):
"""Equivalent to the main program for the application. """Equivalent to the main program for the application.
@@ -309,7 +486,7 @@ class TackerShell(app.App):
help_pos = -1 help_pos = -1
help_command_pos = -1 help_command_pos = -1
for arg in argv: for arg in argv:
if arg == 'bash-completion': if arg == 'bash-completion' and help_command_pos == -1:
self._bash_completion() self._bash_completion()
return 0 return 0
if arg in self.commands[self.api_version]: if arg in self.commands[self.api_version]:
@@ -331,27 +508,22 @@ class TackerShell(app.App):
self.interactive_mode = not remainder self.interactive_mode = not remainder
self.initialize_app(remainder) self.initialize_app(remainder)
except Exception as err: except Exception as err:
if self.options.verbose_level == self.DEBUG_LEVEL: if self.options.verbose_level >= self.DEBUG_LEVEL:
self.log.exception(unicode(err)) self.log.exception(err)
raise raise
else: else:
self.log.error(unicode(err)) self.log.error(err)
return 1 return 1
result = 1
if self.interactive_mode: if self.interactive_mode:
_argv = [sys.argv[0]] _argv = [sys.argv[0]]
sys.argv = _argv sys.argv = _argv
result = self.interact() return self.interact()
else: return self.run_subcommand(remainder)
result = self.run_subcommand(remainder)
return result
def run_subcommand(self, argv): def run_subcommand(self, argv):
subcommand = self.command_manager.find_command(argv) subcommand = self.command_manager.find_command(argv)
cmd_factory, cmd_name, sub_argv = subcommand cmd_factory, cmd_name, sub_argv = subcommand
cmd = cmd_factory(self, self.options) cmd = cmd_factory(self, self.options)
err = None
result = 1
try: try:
self.prepare_to_run_command(cmd) self.prepare_to_run_command(cmd)
full_name = (cmd_name full_name = (cmd_name
@@ -360,29 +532,12 @@ class TackerShell(app.App):
) )
cmd_parser = cmd.get_parser(full_name) cmd_parser = cmd.get_parser(full_name)
return run_command(cmd, cmd_parser, sub_argv) return run_command(cmd, cmd_parser, sub_argv)
except Exception as err: except Exception as e:
if self.options.verbose_level == self.DEBUG_LEVEL: if self.options.verbose_level >= self.DEBUG_LEVEL:
self.log.exception(unicode(err)) self.log.exception("%s", e)
else:
self.log.error(unicode(err))
try:
self.clean_up(cmd, result, err)
except Exception as err2:
if self.options.verbose_level == self.DEBUG_LEVEL:
self.log.exception(unicode(err2))
else:
self.log.error(_('Could not clean up: %s'), unicode(err2))
if self.options.verbose_level == self.DEBUG_LEVEL:
raise raise
else: self.log.error("%s", e)
try: return 1
self.clean_up(cmd, result, None)
except Exception as err3:
if self.options.verbose_level == self.DEBUG_LEVEL:
self.log.exception(unicode(err3))
else:
self.log.error(_('Could not clean up: %s'), unicode(err3))
return result
def authenticate_user(self): def authenticate_user(self):
"""Make sure the user has provided all of the authentication """Make sure the user has provided all of the authentication
@@ -394,43 +549,74 @@ class TackerShell(app.App):
if not self.options.os_token: if not self.options.os_token:
raise exc.CommandError( raise exc.CommandError(
_("You must provide a token via" _("You must provide a token via"
" either --os-token or env[OS_TOKEN]")) " either --os-token or env[OS_TOKEN]"
" when providing a service URL"))
if not self.options.os_url: if not self.options.os_url:
raise exc.CommandError( raise exc.CommandError(
_("You must provide a service URL via" _("You must provide a service URL via"
" either --os-url or env[OS_URL]")) " either --os-url or env[OS_URL]"
" when providing a token"))
else: else:
# Validate password flow auth # Validate password flow auth
project_info = (self.options.os_tenant_name or
self.options.os_tenant_id or
(self.options.os_project_name and
(self.options.os_project_domain_name or
self.options.os_project_domain_id)) or
self.options.os_project_id)
if (not self.options.os_username if (not self.options.os_username
and not self.options.os_user_id): and not self.options.os_user_id):
raise exc.CommandError( raise exc.CommandError(
_("You must provide a username or user ID via" _("You must provide a username or user ID via"
" --os-username, env[OS_USERNAME] or" " --os-username, env[OS_USERNAME] or"
" --os-user_id, env[OS_USER_ID]")) " --os-user-id, env[OS_USER_ID]"))
if not self.options.os_password: if not self.options.os_password:
raise exc.CommandError( # No password, If we've got a tty, try prompting for it
_("You must provide a password via" if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
" either --os-password or env[OS_PASSWORD]")) # Check for Ctl-D
try:
self.options.os_password = getpass.getpass(
'OS Password: ')
except EOFError:
pass
# No password because we didn't have a tty or the
# user Ctl-D when prompted.
if not self.options.os_password:
raise exc.CommandError(
_("You must provide a password via"
" either --os-password or env[OS_PASSWORD]"))
if (not self.options.os_tenant_name if (not project_info):
and not self.options.os_tenant_id): # tenent is deprecated in Keystone v3. Use the latest
# terminology instead.
raise exc.CommandError( raise exc.CommandError(
_("You must provide a tenant_name or tenant_id via" _("You must provide a project_id or project_name ("
" --os-tenant-name, env[OS_TENANT_NAME]" "with project_domain_name or project_domain_id) "
" --os-tenant-id, or via env[OS_TENANT_ID]")) "via "
" --os-project-id (env[OS_PROJECT_ID])"
" --os-project-name (env[OS_PROJECT_NAME]),"
" --os-project-domain-id "
"(env[OS_PROJECT_DOMAIN_ID])"
" --os-project-domain-name "
"(env[OS_PROJECT_DOMAIN_NAME])"))
if not self.options.os_auth_url: if not self.options.os_auth_url:
raise exc.CommandError( raise exc.CommandError(
_("You must provide an auth url via" _("You must provide an auth url via"
" either --os-auth-url or via env[OS_AUTH_URL]")) " either --os-auth-url or via env[OS_AUTH_URL]"))
auth_session = self._get_keystone_session()
auth = auth_session.auth
else: # not keystone else: # not keystone
if not self.options.os_url: if not self.options.os_url:
raise exc.CommandError( raise exc.CommandError(
_("You must provide a service URL via" _("You must provide a service URL via"
" either --os-url or env[OS_URL]")) " either --os-url or env[OS_URL]"))
auth_session = None
auth = None
self.client_manager = clientmanager.ClientManager( self.client_manager = clientmanager.ClientManager(
token=self.options.os_token, token=self.options.os_token,
@@ -444,10 +630,18 @@ class TackerShell(app.App):
region_name=self.options.os_region_name, region_name=self.options.os_region_name,
api_version=self.api_version, api_version=self.api_version,
auth_strategy=self.options.os_auth_strategy, auth_strategy=self.options.os_auth_strategy,
service_type=self.options.service_type, # FIXME (bklei) honor deprecated service_type and
endpoint_type=self.options.endpoint_type, # endpoint type until they are removed
service_type=self.options.os_service_type or
self.options.service_type,
endpoint_type=self.options.os_endpoint_type or self.endpoint_type,
insecure=self.options.insecure, insecure=self.options.insecure,
ca_cert=self.options.os_cacert, ca_cert=self.options.os_cacert,
timeout=self.options.http_timeout,
retries=self.options.retries,
raise_errors=False,
session=auth_session,
auth=auth,
log_credentials=True) log_credentials=True)
return return
@@ -471,11 +665,6 @@ class TackerShell(app.App):
if self.interactive_mode or cmd_name != 'help': if self.interactive_mode or cmd_name != 'help':
self.authenticate_user() self.authenticate_user()
def clean_up(self, cmd, result, err):
self.log.debug('clean_up %s', cmd.__class__.__name__)
if err:
self.log.debug(_('Got an error: %s'), unicode(err))
def configure_logging(self): def configure_logging(self):
"""Create logging handlers for any log output.""" """Create logging handlers for any log output."""
root_logger = logging.getLogger('') root_logger = logging.getLogger('')
@@ -489,24 +678,118 @@ class TackerShell(app.App):
self.INFO_LEVEL: logging.INFO, self.INFO_LEVEL: logging.INFO,
self.DEBUG_LEVEL: logging.DEBUG, self.DEBUG_LEVEL: logging.DEBUG,
}.get(self.options.verbose_level, logging.DEBUG) }.get(self.options.verbose_level, logging.DEBUG)
console.setLevel(console_level) # The default log level is INFO, in this situation, set the
# log level of the console to WARNING, to avoid displaying
# useless messages. This equals using "--quiet"
if console_level == logging.INFO:
console.setLevel(logging.WARNING)
else:
console.setLevel(console_level)
if logging.DEBUG == console_level: if logging.DEBUG == console_level:
formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT) formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT)
else: else:
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
logging.getLogger('iso8601.iso8601').setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
console.setFormatter(formatter) console.setFormatter(formatter)
root_logger.addHandler(console) root_logger.addHandler(console)
return return
def get_v2_auth(self, v2_auth_url):
return v2_auth.Password(
v2_auth_url,
username=self.options.os_username,
password=self.options.os_password,
tenant_id=self.options.os_tenant_id,
tenant_name=self.options.os_tenant_name)
def get_v3_auth(self, v3_auth_url):
project_id = self.options.os_project_id or self.options.os_tenant_id
project_name = (self.options.os_project_name or
self.options.os_tenant_name)
return v3_auth.Password(
v3_auth_url,
username=self.options.os_username,
password=self.options.os_password,
user_id=self.options.os_user_id,
user_domain_name=self.options.os_user_domain_name,
user_domain_id=self.options.os_user_domain_id,
project_id=project_id,
project_name=project_name,
project_domain_name=self.options.os_project_domain_name,
project_domain_id=self.options.os_project_domain_id
)
def _discover_auth_versions(self, session, auth_url):
# discover the API versions the server is supporting base on the
# given URL
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
return (ks_discover.url_for('2.0'), ks_discover.url_for('3.0'))
except ks_exc.ClientException:
# Identity service may not support discover API version.
# Lets try to figure out the API version from the original URL.
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
return (None, auth_url)
elif path.startswith('/v2'):
return (auth_url, None)
else:
# not enough information to determine the auth version
msg = _('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url. Identity service may not support API '
'version discovery. Please provide a versioned '
'auth_url instead.')
raise exc.CommandError(msg)
def _get_keystone_session(self):
# first create a Keystone session
cacert = self.options.os_cacert or None
cert = self.options.os_cert or None
key = self.options.os_key or None
insecure = self.options.insecure or False
ks_session = session.Session.construct(dict(cacert=cacert,
cert=cert,
key=key,
insecure=insecure))
# discover the supported keystone versions using the given url
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
session=ks_session,
auth_url=self.options.os_auth_url)
# Determine which authentication plugin to use. First inspect the
# auth_url to see the supported version. If both v3 and v2 are
# supported, then use the highest version if possible.
user_domain_name = self.options.os_user_domain_name or None
user_domain_id = self.options.os_user_domain_id or None
project_domain_name = self.options.os_project_domain_name or None
project_domain_id = self.options.os_project_domain_id or None
domain_info = (user_domain_name or user_domain_id or
project_domain_name or project_domain_id)
if (v2_auth_url and not domain_info) or not v3_auth_url:
ks_session.auth = self.get_v2_auth(v2_auth_url)
else:
ks_session.auth = self.get_v3_auth(v3_auth_url)
return ks_session
def main(argv=sys.argv[1:]): def main(argv=sys.argv[1:]):
try: try:
return TackerShell(TACKER_API_VERSION).run(map(strutils.safe_decode, return TackerShell(TACKER_API_VERSION).run(
argv)) list(map(encodeutils.safe_decode, argv)))
except KeyboardInterrupt:
print("... terminating tacker client", file=sys.stderr)
return 130
except exc.TackerClientException: except exc.TackerClientException:
return 1 return 1
except Exception as e: except Exception as e:
print(unicode(e)) print(e)
return 1 return 1

View File

@@ -43,10 +43,15 @@ def make_client(instance):
region_name=instance._region_name, region_name=instance._region_name,
auth_url=instance._auth_url, auth_url=instance._auth_url,
endpoint_url=url, endpoint_url=url,
endpoint_type=instance._endpoint_type,
token=instance._token, token=instance._token,
auth_strategy=instance._auth_strategy, auth_strategy=instance._auth_strategy,
insecure=instance._insecure, insecure=instance._insecure,
ca_cert=instance._ca_cert) ca_cert=instance._ca_cert,
retries=instance._retries,
raise_errors=instance._raise_errors,
session=instance._session,
auth=instance._auth)
return client return client
else: else:
raise exceptions.UnsupportedVersion(_("API version %s is not " raise exceptions.UnsupportedVersion(_("API version %s is not "

View File

@@ -1,4 +1,5 @@
# Copyright 2012 OpenStack Foundation. # Copyright 2012 OpenStack Foundation.
# Copyright 2015 Hewlett-Packard Development Company, L.P.
# All Rights Reserved # All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -16,31 +17,29 @@
import logging import logging
import time import time
import urllib
import requests import requests
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
from tackerclient import client from tackerclient import client
from tackerclient.common import _
from tackerclient.common import constants from tackerclient.common import constants
from tackerclient.common import exceptions from tackerclient.common import exceptions
from tackerclient.common import serializer from tackerclient.common import serializer
from tackerclient.common import utils from tackerclient.common import utils
from tackerclient.i18n import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def exception_handler_v10(status_code, error_content): def exception_handler_v10(status_code, error_content):
"""Exception handler for API v1.0 client """Exception handler for API v1.0 client.
This routine generates the appropriate This routine generates the appropriate Tacker exception according to
Tacker exception according to the contents of the the contents of the response body.
response body
:param status_code: HTTP error status code :param status_code: HTTP error status code
:param error_content: deserialized body of error response :param error_content: deserialized body of error response
""" """
error_dict = None error_dict = None
if isinstance(error_content, dict): if isinstance(error_content, dict):
@@ -87,8 +86,7 @@ def exception_handler_v10(status_code, error_content):
class APIParamsCall(object): class APIParamsCall(object):
"""A Decorator to add support for format and tenant overriding """A Decorator to add support for format and tenant overriding and filters.
and filters
""" """
def __init__(self, function): def __init__(self, function):
self.function = function self.function = function
@@ -104,7 +102,7 @@ class APIParamsCall(object):
return with_params return with_params
class Client(object): class ClientBase(object):
"""Client for the OpenStack Tacker v1.0 API. """Client for the OpenStack Tacker v1.0 API.
:param string username: Username for authentication. (optional) :param string username: Username for authentication. (optional)
@@ -113,6 +111,8 @@ class Client(object):
:param string token: Token for authentication. (optional) :param string token: Token for authentication. (optional)
:param string tenant_name: Tenant name. (optional) :param string tenant_name: Tenant name. (optional)
:param string tenant_id: Tenant id. (optional) :param string tenant_id: Tenant id. (optional)
:param string auth_strategy: 'keystone' by default, 'noauth' for no
authentication against keystone. (optional)
:param string auth_url: Keystone service endpoint for authorization. :param string auth_url: Keystone service endpoint for authorization.
:param string service_type: Network service type to pull from the :param string service_type: Network service type to pull from the
keystone catalog (e.g. 'network') (optional) keystone catalog (e.g. 'network') (optional)
@@ -128,7 +128,17 @@ class Client(object):
:param integer timeout: Allows customization of the timeout for client :param integer timeout: Allows customization of the timeout for client
http requests. (optional) http requests. (optional)
:param bool insecure: SSL certificate validation. (optional) :param bool insecure: SSL certificate validation. (optional)
:param bool log_credentials: Allow for logging of passwords or not.
Defaults to False. (optional)
:param string ca_cert: SSL CA bundle file to use. (optional) :param string ca_cert: SSL CA bundle file to use. (optional)
:param integer retries: How many times idempotent (GET, PUT, DELETE)
requests to Tacker server should be retried if
they fail (default: 0).
:param bool raise_errors: If True then exceptions caused by connection
failure are propagated to the caller.
(default: True)
:param session: Keystone client auth session to use. (optional)
:param auth: Keystone auth plugin to use. (optional)
Example:: Example::
@@ -143,20 +153,84 @@ class Client(object):
""" """
extensions_path = "/extensions"
extension_path = "/extensions/%s"
device_templates_path = '/device-templates'
device_template_path = '/device-templates/%s'
devices_path = '/devices'
device_path = '/devices/%s'
interface_attach_path = '/devices/%s/attach_interface'
interface_detach_path = '/devices/%s/detach_interface'
# API has no way to report plurals, so we have to hard code them # API has no way to report plurals, so we have to hard code them
# This variable should be overridden by a child class.
EXTED_PLURALS = {} EXTED_PLURALS = {}
# 8192 Is the default max URI len for eventlet.wsgi.server
MAX_URI_LEN = 8192 def __init__(self, **kwargs):
"""Initialize a new client for the Tacker v1.0 API."""
super(ClientBase, self).__init__()
self.retries = kwargs.pop('retries', 0)
self.raise_errors = kwargs.pop('raise_errors', True)
self.httpclient = client.construct_http_client(**kwargs)
self.version = '1.0'
self.format = 'json'
self.action_prefix = "/v%s" % (self.version)
self.retry_interval = 1
def _handle_fault_response(self, status_code, response_body):
# Create exception with HTTP status code and message
_logger.debug("Error message: %s", response_body)
# Add deserialized error message to exception arguments
try:
des_error_body = self.deserialize(response_body, status_code)
except Exception:
# If unable to deserialized body it is probably not a
# Tacker error
des_error_body = {'message': response_body}
# Raise the appropriate exception
exception_handler_v10(status_code, des_error_body)
def do_request(self, method, action, body=None, headers=None, params=None):
# Add format and tenant_id
action += ".%s" % self.format
action = self.action_prefix + action
if type(params) is dict and params:
params = utils.safe_encode_dict(params)
action += '?' + urlparse.urlencode(params, doseq=1)
if body:
body = self.serialize(body)
resp, replybody = self.httpclient.do_request(
action, method, body=body,
content_type=self.content_type())
status_code = resp.status_code
if status_code in (requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
requests.codes.no_content):
return self.deserialize(replybody, status_code)
else:
if not replybody:
replybody = resp.reason
self._handle_fault_response(status_code, replybody)
def get_auth_info(self):
return self.httpclient.get_auth_info()
def serialize(self, data):
"""Serializes a dictionary into either XML or JSON.
A dictionary with a single key can be passed and it can contain any
structure.
"""
if data is None:
return None
elif type(data) is dict:
return serializer.Serializer(
self.get_attr_metadata()).serialize(data, self.content_type())
else:
raise Exception(_("Unable to serialize object of type = '%s'") %
type(data))
def deserialize(self, data, status_code):
"""Deserializes an XML or JSON string into a dictionary."""
if status_code == 204:
return data
return serializer.Serializer(self.get_attr_metadata()).deserialize(
data, self.content_type())['body']
def get_attr_metadata(self): def get_attr_metadata(self):
if self.format == 'json': if self.format == 'json':
@@ -168,9 +242,107 @@ class Client(object):
ns = dict([(ext['alias'], ext['namespace']) for ext in exts]) ns = dict([(ext['alias'], ext['namespace']) for ext in exts])
self.EXTED_PLURALS.update(constants.PLURALS) self.EXTED_PLURALS.update(constants.PLURALS)
return {'plurals': self.EXTED_PLURALS, return {'plurals': self.EXTED_PLURALS,
'xmlns': constants.XML_NS_V10, 'xmlns': constants.XML_NS_V20,
constants.EXT_NS: ns} constants.EXT_NS: ns}
def content_type(self, _format=None):
"""Returns the mime-type for either 'xml' or 'json'.
Defaults to the currently set format.
"""
_format = _format or self.format
return "application/%s" % (_format)
def retry_request(self, method, action, body=None,
headers=None, params=None):
"""Call do_request with the default retry configuration.
Only idempotent requests should retry failed connection attempts.
:raises: ConnectionFailed if the maximum # of retries is exceeded
"""
max_attempts = self.retries + 1
for i in range(max_attempts):
try:
return self.do_request(method, action, body=body,
headers=headers, params=params)
except exceptions.ConnectionFailed:
# Exception has already been logged by do_request()
if i < self.retries:
_logger.debug('Retrying connection to Tacker service')
time.sleep(self.retry_interval)
elif self.raise_errors:
raise
if self.retries:
msg = (_("Failed to connect to Tacker server after %d attempts")
% max_attempts)
else:
msg = _("Failed to connect Tacker server")
raise exceptions.ConnectionFailed(reason=msg)
def delete(self, action, body=None, headers=None, params=None):
return self.retry_request("DELETE", action, body=body,
headers=headers, params=params)
def get(self, action, body=None, headers=None, params=None):
return self.retry_request("GET", action, body=body,
headers=headers, params=params)
def post(self, action, body=None, headers=None, params=None):
# Do not retry POST requests to avoid the orphan objects problem.
return self.do_request("POST", action, body=body,
headers=headers, params=params)
def put(self, action, body=None, headers=None, params=None):
return self.retry_request("PUT", action, body=body,
headers=headers, params=params)
def list(self, collection, path, retrieve_all=True, **params):
if retrieve_all:
res = []
for r in self._pagination(collection, path, **params):
res.extend(r[collection])
return {collection: res}
else:
return self._pagination(collection, path, **params)
def _pagination(self, collection, path, **params):
if params.get('page_reverse', False):
linkrel = 'previous'
else:
linkrel = 'next'
next = True
while next:
res = self.get(path, params=params)
yield res
next = False
try:
for link in res['%s_links' % collection]:
if link['rel'] == linkrel:
query_str = urlparse.urlparse(link['href']).query
params = urlparse.parse_qs(query_str)
next = True
break
except KeyError:
break
class Client(ClientBase):
extensions_path = "/extensions"
extension_path = "/extensions/%s"
device_templates_path = '/device-templates'
device_template_path = '/device-templates/%s'
devices_path = '/devices'
device_path = '/devices/%s'
interface_attach_path = '/devices/%s/attach_interface'
interface_detach_path = '/devices/%s/detach_interface'
# API has no way to report plurals, so we have to hard code them
# EXTED_PLURALS = {}
@APIParamsCall @APIParamsCall
def list_extensions(self, **_params): def list_extensions(self, **_params):
"""Fetch a list of all exts on server side.""" """Fetch a list of all exts on server side."""
@@ -229,168 +401,3 @@ class Client(object):
@APIParamsCall @APIParamsCall
def detach_interface(self, device, body=None): def detach_interface(self, device, body=None):
return self.put(self.detach_interface_path % device, body) return self.put(self.detach_interface_path % device, body)
def __init__(self, **kwargs):
"""Initialize a new client for the Tacker v1.0 API."""
super(Client, self).__init__()
self.httpclient = client.HTTPClient(**kwargs)
self.version = '1.0'
self.format = 'json'
self.action_prefix = "/v%s" % (self.version)
self.retries = 0
self.retry_interval = 1
def _handle_fault_response(self, status_code, response_body):
# Create exception with HTTP status code and message
_logger.debug(_("Error message: %s"), response_body)
# Add deserialized error message to exception arguments
try:
des_error_body = self.deserialize(response_body, status_code)
except Exception:
# If unable to deserialized body it is probably not a
# Tacker error
des_error_body = {'message': response_body}
# Raise the appropriate exception
exception_handler_v10(status_code, des_error_body)
def _check_uri_length(self, action):
uri_len = len(self.httpclient.endpoint_url) + len(action)
if uri_len > self.MAX_URI_LEN:
raise exceptions.RequestURITooLong(
excess=uri_len - self.MAX_URI_LEN)
def do_request(self, method, action, body=None, headers=None, params=None):
# Add format and tenant_id
action += ".%s" % self.format
action = self.action_prefix + action
if type(params) is dict and params:
params = utils.safe_encode_dict(params)
action += '?' + urllib.urlencode(params, doseq=1)
# Ensure client always has correct uri - do not guesstimate anything
self.httpclient.authenticate_and_fetch_endpoint_url()
self._check_uri_length(action)
if body:
body = self.serialize(body)
self.httpclient.content_type = self.content_type()
resp, replybody = self.httpclient.do_request(action, method, body=body)
status_code = self.get_status_code(resp)
if status_code in (requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
requests.codes.no_content):
return self.deserialize(replybody, status_code)
else:
if not replybody:
replybody = resp.reason
self._handle_fault_response(status_code, replybody)
def get_auth_info(self):
return self.httpclient.get_auth_info()
def get_status_code(self, response):
"""Returns the integer status code from the response.
Either a Webob.Response (used in testing) or requests.Response
is returned.
"""
if hasattr(response, 'status_int'):
return response.status_int
else:
return response.status_code
def serialize(self, data):
"""Serializes a dictionary into either xml or json.
A dictionary with a single key can be passed and
it can contain any structure.
"""
if data is None:
return None
elif type(data) is dict:
return serializer.Serializer(
self.get_attr_metadata()).serialize(data, self.content_type())
else:
raise Exception(_("Unable to serialize object of type = '%s'") %
type(data))
def deserialize(self, data, status_code):
"""Deserializes an xml or json string into a dictionary."""
if status_code == 204:
return data
return serializer.Serializer(self.get_attr_metadata()).deserialize(
data, self.content_type())['body']
def content_type(self, _format=None):
"""Returns the mime-type for either 'xml' or 'json'.
Defaults to the currently set format.
"""
_format = _format or self.format
return "application/%s" % (_format)
def retry_request(self, method, action, body=None,
headers=None, params=None):
"""Call do_request with the default retry configuration.
Only idempotent requests should retry failed connection attempts.
:raises: ConnectionFailed if the maximum # of retries is exceeded
"""
max_attempts = self.retries + 1
for i in range(max_attempts):
try:
return self.do_request(method, action, body=body,
headers=headers, params=params)
except exceptions.ConnectionFailed:
# Exception has already been logged by do_request()
if i < self.retries:
_logger.debug(_('Retrying connection to Tacker service'))
time.sleep(self.retry_interval)
raise exceptions.ConnectionFailed(reason=_("Maximum attempts reached"))
def delete(self, action, body=None, headers=None, params=None):
return self.retry_request("DELETE", action, body=body,
headers=headers, params=params)
def get(self, action, body=None, headers=None, params=None):
return self.retry_request("GET", action, body=body,
headers=headers, params=params)
def post(self, action, body=None, headers=None, params=None):
# Do not retry POST requests to avoid the orphan objects problem.
return self.do_request("POST", action, body=body,
headers=headers, params=params)
def put(self, action, body=None, headers=None, params=None):
return self.retry_request("PUT", action, body=body,
headers=headers, params=params)
def list(self, collection, path, retrieve_all=True, **params):
if retrieve_all:
res = []
for r in self._pagination(collection, path, **params):
res.extend(r[collection])
return {collection: res}
else:
return self._pagination(collection, path, **params)
def _pagination(self, collection, path, **params):
if params.get('page_reverse', False):
linkrel = 'previous'
else:
linkrel = 'next'
next = True
while next:
res = self.get(path, params=params)
yield res
next = False
try:
for link in res['%s_links' % collection]:
if link['rel'] == linkrel:
query_str = urlparse.urlparse(link['href']).query
params = urlparse.parse_qs(query_str)
next = True
break
except KeyError:
break