catch up neutronclient change
Change-Id: I1354fe5378566dec66e7cac311a394cb5498c734
This commit is contained in:
parent
4291ce92df
commit
e843dbd1b6
|
@ -22,11 +22,12 @@ import logging
|
|||
import os
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient import adapter
|
||||
import requests
|
||||
|
||||
from tackerclient.common import exceptions
|
||||
from tackerclient.common import utils
|
||||
from tackerclient.openstack.common.gettextutils import _
|
||||
from tackerclient.i18n import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -39,12 +40,14 @@ else:
|
|||
_requests_log_level = logging.WARNING
|
||||
|
||||
logging.getLogger("requests").setLevel(_requests_log_level)
|
||||
MAX_URI_LEN = 8192
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
"""Handles the REST calls and responses, include authn."""
|
||||
|
||||
USER_AGENT = 'python-tackerclient'
|
||||
CONTENT_TYPE = 'application/json'
|
||||
|
||||
def __init__(self, username=None, user_id=None,
|
||||
tenant_name=None, tenant_id=None,
|
||||
|
@ -69,7 +72,6 @@ class HTTPClient(object):
|
|||
self.auth_token = token
|
||||
self.auth_tenant_id = None
|
||||
self.auth_user_id = None
|
||||
self.content_type = 'application/json'
|
||||
self.endpoint_url = endpoint_url
|
||||
self.auth_strategy = auth_strategy
|
||||
self.log_credentials = log_credentials
|
||||
|
@ -83,17 +85,8 @@ class HTTPClient(object):
|
|||
kargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
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:
|
||||
kargs['body'] = kwargs['body']
|
||||
args = utils.safe_encode_list(args)
|
||||
kargs = utils.safe_encode_dict(kargs)
|
||||
|
||||
if self.log_credentials:
|
||||
log_kargs = kargs
|
||||
|
@ -112,8 +105,7 @@ class HTTPClient(object):
|
|||
_logger.debug("throwing ConnectionFailed : %s", e)
|
||||
raise exceptions.ConnectionFailed(reason=e)
|
||||
utils.http_log_resp(_logger, resp, body)
|
||||
status_code = self.get_status_code(resp)
|
||||
if status_code == 401:
|
||||
if resp.status_code == 401:
|
||||
raise exceptions.Unauthorized(message=body)
|
||||
return resp, body
|
||||
|
||||
|
@ -132,24 +124,40 @@ class HTTPClient(object):
|
|||
elif not self.endpoint_url:
|
||||
self.endpoint_url = self._get_endpoint_url()
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = kwargs['body']
|
||||
del kwargs['body']
|
||||
def request(self, url, method, body=None, headers=None, **kwargs):
|
||||
"""Request without authentication."""
|
||||
|
||||
content_type = kwargs.pop('content_type', None) or 'application/json'
|
||||
headers = headers or {}
|
||||
headers.setdefault('Accept', content_type)
|
||||
|
||||
if body:
|
||||
headers.setdefault('Content-Type', content_type)
|
||||
|
||||
headers['User-Agent'] = self.USER_AGENT
|
||||
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
verify=self.verify_cert,
|
||||
timeout=self.timeout,
|
||||
**kwargs)
|
||||
|
||||
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):
|
||||
# Ensure client always has correct uri - do not guesstimate anything
|
||||
self.authenticate_and_fetch_endpoint_url()
|
||||
self._check_uri_length(url)
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
|
@ -206,8 +214,7 @@ class HTTPClient(object):
|
|||
body=json.dumps(body),
|
||||
content_type="application/json",
|
||||
allow_redirects=True)
|
||||
status_code = self.get_status_code(resp)
|
||||
if status_code != 200:
|
||||
if resp.status_code != 200:
|
||||
raise exceptions.Unauthorized(message=resp_body)
|
||||
if resp_body:
|
||||
try:
|
||||
|
@ -250,7 +257,7 @@ class HTTPClient(object):
|
|||
body = json.loads(body)
|
||||
for endpoint in body.get('endpoints', []):
|
||||
if (endpoint['type'] == 'servicevm' and
|
||||
endpoint.get('region') == self.region_name):
|
||||
endpoint.get('region') == self.region_name):
|
||||
if self.endpoint_type not in endpoint:
|
||||
raise exceptions.EndpointTypeNotFound(
|
||||
type_=self.endpoint_type)
|
||||
|
@ -264,13 +271,122 @@ class HTTPClient(object):
|
|||
'auth_user_id': self.auth_user_id,
|
||||
'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
|
||||
is returned.
|
||||
"""
|
||||
if hasattr(response, 'status_int'):
|
||||
return response.status_int
|
||||
class SessionClient(adapter.Adapter):
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('authenticated', False)
|
||||
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:
|
||||
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)
|
||||
|
|
|
@ -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)
|
|
@ -27,8 +27,7 @@ LOG = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class ClientCache(object):
|
||||
"""Descriptor class for caching created client handles.
|
||||
"""
|
||||
"""Descriptor class for caching created client handles."""
|
||||
|
||||
def __init__(self, factory):
|
||||
self.factory = factory
|
||||
|
@ -42,8 +41,7 @@ class ClientCache(object):
|
|||
|
||||
|
||||
class ClientManager(object):
|
||||
"""Manages access to API clients, including authentication.
|
||||
"""
|
||||
"""Manages access to API clients, including authentication."""
|
||||
tacker = ClientCache(tacker_client.make_client)
|
||||
|
||||
def __init__(self, token=None, url=None,
|
||||
|
@ -61,6 +59,11 @@ class ClientManager(object):
|
|||
ca_cert=None,
|
||||
log_credentials=False,
|
||||
service_type=None,
|
||||
timeout=None,
|
||||
retries=0,
|
||||
raise_errors=True,
|
||||
session=None,
|
||||
auth=None,
|
||||
):
|
||||
self._token = token
|
||||
self._url = url
|
||||
|
@ -79,11 +82,16 @@ class ClientManager(object):
|
|||
self._insecure = insecure
|
||||
self._ca_cert = ca_cert
|
||||
self._log_credentials = log_credentials
|
||||
self._timeout = timeout
|
||||
self._retries = retries
|
||||
self._raise_errors = raise_errors
|
||||
self._session = session
|
||||
self._auth = auth
|
||||
return
|
||||
|
||||
def initialize(self):
|
||||
if not self._url:
|
||||
httpclient = client.HTTPClient(
|
||||
httpclient = client.construct_http_client(
|
||||
username=self._username,
|
||||
user_id=self._user_id,
|
||||
tenant_name=self._tenant_name,
|
||||
|
@ -95,6 +103,9 @@ class ClientManager(object):
|
|||
endpoint_type=self._endpoint_type,
|
||||
insecure=self._insecure,
|
||||
ca_cert=self._ca_cert,
|
||||
timeout=self._timeout,
|
||||
session=self._session,
|
||||
auth=self._auth,
|
||||
log_credentials=self._log_credentials)
|
||||
httpclient.authenticate()
|
||||
# Populate other password flow attributes
|
||||
|
|
|
@ -14,16 +14,11 @@
|
|||
# under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
OpenStack base command
|
||||
"""
|
||||
|
||||
from cliff import command
|
||||
|
||||
|
||||
class OpenStackCommand(command.Command):
|
||||
"""Base class for OpenStack commands
|
||||
"""
|
||||
"""Base class for OpenStack commands."""
|
||||
|
||||
api = None
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
|
||||
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_ATTR = "xsi:nil"
|
||||
XSI_NIL_ATTR = "xmlns:xsi"
|
||||
|
@ -33,6 +33,7 @@ TYPE_FLOAT = "float"
|
|||
TYPE_LIST = "list"
|
||||
TYPE_DICT = "dict"
|
||||
|
||||
|
||||
PLURALS = {'templates': 'template',
|
||||
'devices': 'device',
|
||||
'services': 'service'}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tackerclient.common import _
|
||||
from tackerclient.i18n import _
|
||||
|
||||
"""
|
||||
Tacker base exception handling.
|
||||
|
@ -30,12 +30,11 @@ Exceptions are classified into three categories:
|
|||
|
||||
|
||||
class TackerException(Exception):
|
||||
"""Base Tacker Exception
|
||||
"""Base Tacker Exception.
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
status_code = 0
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
if 'status_code' in kwargs:
|
||||
self.status_code = kwargs['status_code']
|
||||
|
@ -139,6 +140,10 @@ class IpAddressInUseClient(Conflict):
|
|||
pass
|
||||
|
||||
|
||||
class InvalidIpForNetworkClient(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class OverQuotaClient(Conflict):
|
||||
pass
|
||||
|
||||
|
@ -153,6 +158,10 @@ class IpAddressGenerationFailureClient(Conflict):
|
|||
pass
|
||||
|
||||
|
||||
class MacAddressInUseClient(Conflict):
|
||||
pass
|
||||
|
||||
|
||||
class ExternalIpAddressExhaustedClient(BadRequest):
|
||||
pass
|
||||
|
||||
|
@ -212,8 +221,8 @@ class CommandError(TackerCLIError):
|
|||
|
||||
|
||||
class UnsupportedVersion(TackerCLIError):
|
||||
"""Indicates that the user is trying to use an unsupported
|
||||
version of the API
|
||||
"""Indicates that the user is trying to use an unsupported version of
|
||||
the API.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -12,23 +12,23 @@
|
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
###
|
||||
### Codes from tacker wsgi
|
||||
###
|
||||
|
||||
import logging
|
||||
|
||||
from xml.etree import ElementTree as etree
|
||||
from xml.parsers import expat
|
||||
|
||||
from oslo.serialization import jsonutils
|
||||
import six
|
||||
|
||||
from tackerclient.common import constants
|
||||
from tackerclient.common import exceptions as exception
|
||||
from tackerclient.openstack.common.gettextutils import _
|
||||
from tackerclient.openstack.common import jsonutils
|
||||
from tackerclient.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
if six.PY3:
|
||||
long = int
|
||||
|
||||
|
||||
class ActionDispatcher(object):
|
||||
"""Maps method name to local methods through action name."""
|
||||
|
@ -58,7 +58,7 @@ class JSONDictSerializer(DictSerializer):
|
|||
|
||||
def default(self, data):
|
||||
def sanitizer(obj):
|
||||
return unicode(obj)
|
||||
return six.text_type(obj)
|
||||
return jsonutils.dumps(data, default=sanitizer)
|
||||
|
||||
|
||||
|
@ -67,16 +67,16 @@ class XMLDictSerializer(DictSerializer):
|
|||
def __init__(self, metadata=None, xmlns=None):
|
||||
"""XMLDictSerializer constructor.
|
||||
|
||||
:param metadata: information needed to deserialize xml into
|
||||
:param metadata: information needed to deserialize XML into
|
||||
a dictionary.
|
||||
:param xmlns: XML namespace to include with serialized xml
|
||||
:param xmlns: XML namespace to include with serialized XML
|
||||
"""
|
||||
super(XMLDictSerializer, self).__init__()
|
||||
self.metadata = metadata or {}
|
||||
if not xmlns:
|
||||
xmlns = self.metadata.get('xmlns')
|
||||
if not xmlns:
|
||||
xmlns = constants.XML_NS_V10
|
||||
xmlns = constants.XML_NS_V20
|
||||
self.xmlns = xmlns
|
||||
|
||||
def default(self, data):
|
||||
|
@ -93,13 +93,13 @@ class XMLDictSerializer(DictSerializer):
|
|||
root_key = constants.VIRTUAL_ROOT_KEY
|
||||
root_value = None
|
||||
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 link_keys:
|
||||
links = data.pop(link_keys[0], None)
|
||||
has_atom = True
|
||||
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)
|
||||
doc = etree.Element("_temp_root")
|
||||
used_prefixes = []
|
||||
|
@ -122,8 +122,8 @@ class XMLDictSerializer(DictSerializer):
|
|||
self._add_xmlns(node, used_prefixes, has_atom)
|
||||
return etree.tostring(node, encoding='UTF-8')
|
||||
|
||||
#NOTE (ameade): the has_atom should be removed after all of the
|
||||
# xml serializers and view builders have been updated to the current
|
||||
# NOTE(ameade): the has_atom should be removed after all of the
|
||||
# XML serializers and view builders have been updated to the current
|
||||
# spec that required all responses include the xmlns:atom, the has_atom
|
||||
# flag is to prevent current tests from breaking
|
||||
def _add_xmlns(self, node, used_prefixes, has_atom=False):
|
||||
|
@ -142,7 +142,7 @@ class XMLDictSerializer(DictSerializer):
|
|||
result = etree.SubElement(parent, nodename)
|
||||
if ":" in nodename:
|
||||
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 not data:
|
||||
result.set(
|
||||
|
@ -158,7 +158,7 @@ class XMLDictSerializer(DictSerializer):
|
|||
for item in data:
|
||||
self._to_xml_node(result, metadata, singular, item,
|
||||
used_prefixes)
|
||||
#TODO(bcwaldon): accomplish this without a type-check
|
||||
# TODO(bcwaldon): accomplish this without a type-check
|
||||
elif isinstance(data, dict):
|
||||
if not data:
|
||||
result.set(
|
||||
|
@ -191,13 +191,10 @@ class XMLDictSerializer(DictSerializer):
|
|||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_FLOAT)
|
||||
LOG.debug(_("Data %(data)s type is %(type)s"),
|
||||
LOG.debug("Data %(data)s type is %(type)s",
|
||||
{'data': data,
|
||||
'type': type(data)})
|
||||
if isinstance(data, str):
|
||||
result.text = unicode(data, 'utf-8')
|
||||
else:
|
||||
result.text = unicode(data)
|
||||
result.text = six.text_type(data)
|
||||
return result
|
||||
|
||||
def _create_link_nodes(self, xml_doc, links):
|
||||
|
@ -235,14 +232,14 @@ class XMLDeserializer(TextDeserializer):
|
|||
def __init__(self, metadata=None):
|
||||
"""XMLDeserializer constructor.
|
||||
|
||||
:param metadata: information needed to deserialize xml into
|
||||
:param metadata: information needed to deserialize XML into
|
||||
a dictionary.
|
||||
"""
|
||||
super(XMLDeserializer, self).__init__()
|
||||
self.metadata = metadata or {}
|
||||
xmlns = self.metadata.get('xmlns')
|
||||
if not xmlns:
|
||||
xmlns = constants.XML_NS_V10
|
||||
xmlns = constants.XML_NS_V20
|
||||
self.xmlns = xmlns
|
||||
|
||||
def _get_key(self, tag):
|
||||
|
@ -290,7 +287,7 @@ class XMLDeserializer(TextDeserializer):
|
|||
parseError = False
|
||||
# Python2.7
|
||||
if (hasattr(etree, 'ParseError') and
|
||||
isinstance(e, getattr(etree, 'ParseError'))):
|
||||
isinstance(e, getattr(etree, 'ParseError'))):
|
||||
parseError = True
|
||||
# Python2.6
|
||||
elif isinstance(e, expat.ExpatError):
|
||||
|
@ -340,9 +337,9 @@ class XMLDeserializer(TextDeserializer):
|
|||
result = dict()
|
||||
for attr in node.keys():
|
||||
if (attr == 'xmlns' or
|
||||
attr.startswith('xmlns:') or
|
||||
attr == constants.XSI_ATTR or
|
||||
attr == constants.TYPE_ATTR):
|
||||
attr.startswith('xmlns:') or
|
||||
attr == constants.XSI_ATTR or
|
||||
attr == constants.TYPE_ATTR):
|
||||
continue
|
||||
result[self._get_key(attr)] = node.get(attr)
|
||||
children = list(node)
|
||||
|
@ -392,7 +389,6 @@ class Serializer(object):
|
|||
"""Deserialize a string to a dictionary.
|
||||
|
||||
The string must be in the format of a supported MIME type.
|
||||
|
||||
"""
|
||||
return self.get_deserialize_handler(content_type).deserialize(
|
||||
datastring)
|
||||
|
|
|
@ -17,21 +17,22 @@
|
|||
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import argparse
|
||||
import logging
|
||||
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.openstack.common import strutils
|
||||
from tackerclient.i18n import _
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""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:
|
||||
value = os.environ.get(v)
|
||||
|
@ -40,52 +41,8 @@ def env(*vars, **kwargs):
|
|||
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):
|
||||
"""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 version: the requested API version
|
||||
|
@ -101,10 +58,10 @@ def get_client_class(api_name, version, version_map):
|
|||
'map_keys': ', '.join(version_map.keys())}
|
||||
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.
|
||||
|
||||
: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
|
||||
to format the values
|
||||
"""
|
||||
if formatters is None:
|
||||
formatters = {}
|
||||
|
||||
row = []
|
||||
|
||||
for field in fields:
|
||||
|
@ -136,22 +96,17 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
|||
def str2bool(strbool):
|
||||
if strbool is None:
|
||||
return None
|
||||
else:
|
||||
return strbool.lower() == 'true'
|
||||
return strbool.lower() == 'true'
|
||||
|
||||
|
||||
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
|
||||
'''
|
||||
_info = {}
|
||||
if not strdict:
|
||||
return _info
|
||||
for kv_str in strdict.split(","):
|
||||
k, v = kv_str.split("=", 1)
|
||||
_info.update({k: v})
|
||||
return _info
|
||||
:param strdict: key1=value1,key2=value2
|
||||
"""
|
||||
if not strdict:
|
||||
return {}
|
||||
return dict([kv.split('=', 1) for kv in strdict.split(',')])
|
||||
|
||||
|
||||
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']:
|
||||
string_parts.append(" -d '%s'" % (kwargs['body']))
|
||||
string_parts = safe_encode_list(string_parts)
|
||||
_logger.debug(_("\nREQ: %s\n"), "".join(string_parts))
|
||||
req = encodeutils.safe_encode("".join(string_parts))
|
||||
_logger.debug("\nREQ: %s\n", req)
|
||||
|
||||
|
||||
def http_log_resp(_logger, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
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,
|
||||
'headers': resp.headers,
|
||||
'body': body})
|
||||
|
||||
|
||||
def _safe_encode_without_obj(data):
|
||||
if isinstance(data, basestring):
|
||||
return strutils.safe_encode(data)
|
||||
if isinstance(data, six.string_types):
|
||||
return encodeutils.safe_encode(data)
|
||||
return 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):
|
||||
|
@ -203,4 +158,16 @@ def safe_encode_dict(data):
|
|||
return (k, safe_encode_dict(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)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import netaddr
|
||||
|
||||
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):
|
||||
|
@ -29,7 +29,7 @@ def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
|
|||
else:
|
||||
int_val = val
|
||||
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
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
|
|
@ -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
|
|
@ -21,18 +21,30 @@ Command-line interface to the Tacker APIs
|
|||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
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 commandmanager
|
||||
|
||||
from tackerclient.common import clientmanager
|
||||
from tackerclient.common import command as openstack_command
|
||||
from tackerclient.common import exceptions as exc
|
||||
from tackerclient.common import extension as client_extension
|
||||
from tackerclient.common import utils
|
||||
from tackerclient.openstack.common.gettextutils import _
|
||||
from tackerclient.openstack.common import strutils
|
||||
from tackerclient.i18n import _
|
||||
from tackerclient.tacker.v1_0 import extension
|
||||
from tackerclient.tacker.v1_0.vm import device
|
||||
from tackerclient.tacker.v1_0.vm import device_template
|
||||
|
@ -70,7 +82,23 @@ def env(*_vars, **kwargs):
|
|||
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 = {
|
||||
'bash-completion': BashCompletionCommand,
|
||||
'ext-list': extension.ListExt,
|
||||
'ext-show': extension.ShowExt,
|
||||
'device-template-create': device_template.CreateDeviceTemplate,
|
||||
|
@ -134,6 +162,11 @@ class TackerShell(app.App):
|
|||
for k, v in self.commands[apiversion].items():
|
||||
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
|
||||
# password flow auth
|
||||
self.auth_client = None
|
||||
|
@ -169,20 +202,64 @@ class TackerShell(app.App):
|
|||
action='store_const',
|
||||
dest='verbose_level',
|
||||
const=0,
|
||||
help=_('Suppress output except warnings and errors'))
|
||||
help=_('Suppress output except warnings and errors.'))
|
||||
parser.add_argument(
|
||||
'-h', '--help',
|
||||
action=HelpAction,
|
||||
nargs=0,
|
||||
default=self, # tricky
|
||||
help=_("Show this help message and exit"))
|
||||
# Global arguments
|
||||
help=_("Show this help message and exit."))
|
||||
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(
|
||||
'--os-auth-strategy', metavar='<auth-strategy>',
|
||||
default=env('OS_AUTH_STRATEGY', default='keystone'),
|
||||
help=_('Authentication strategy (Env: OS_AUTH_STRATEGY'
|
||||
', default keystone). For now, any other value will'
|
||||
' disable the authentication'))
|
||||
help=_('DEPRECATED! Only keystone is supported.'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os_auth_strategy',
|
||||
help=argparse.SUPPRESS)
|
||||
|
@ -190,28 +267,49 @@ class TackerShell(app.App):
|
|||
parser.add_argument(
|
||||
'--os-auth-url', metavar='<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(
|
||||
'--os_auth_url',
|
||||
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>',
|
||||
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(
|
||||
'--os_tenant_name',
|
||||
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>',
|
||||
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(
|
||||
'--os-username', metavar='<auth-username>',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help=_('Authentication username (Env: OS_USERNAME)'))
|
||||
help=_('Authentication username, defaults to env[OS_USERNAME].'))
|
||||
parser.add_argument(
|
||||
'--os_username',
|
||||
help=argparse.SUPPRESS)
|
||||
|
@ -222,54 +320,115 @@ class TackerShell(app.App):
|
|||
help=_('Authentication user ID (Env: OS_USER_ID)'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-password', metavar='<auth-password>',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help=_('Authentication password (Env: OS_PASSWORD)'))
|
||||
parser.add_argument(
|
||||
'--os_password',
|
||||
'--os_user_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument(
|
||||
'--os-region-name', metavar='<auth-region-name>',
|
||||
default=env('OS_REGION_NAME'),
|
||||
help=_('Authentication region name (Env: OS_REGION_NAME)'))
|
||||
'--os-user-domain-id',
|
||||
metavar='<auth-user-domain-id>',
|
||||
default=utils.env('OS_USER_DOMAIN_ID'),
|
||||
help='OpenStack user domain ID. '
|
||||
'Defaults to env[OS_USER_DOMAIN_ID].')
|
||||
|
||||
parser.add_argument(
|
||||
'--os_region_name',
|
||||
'--os_user_domain_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument(
|
||||
'--os-token', metavar='<token>',
|
||||
default=env('OS_TOKEN'),
|
||||
help=_('Defaults to env[OS_TOKEN]'))
|
||||
'--os-user-domain-name',
|
||||
metavar='<auth-user-domain-name>',
|
||||
default=utils.env('OS_USER_DOMAIN_NAME'),
|
||||
help='OpenStack user domain name. '
|
||||
'Defaults to env[OS_USER_DOMAIN_NAME].')
|
||||
|
||||
parser.add_argument(
|
||||
'--os_token',
|
||||
'--os_user_domain_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument(
|
||||
'--service-type', metavar='<service-type>',
|
||||
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',
|
||||
'--os_project_id',
|
||||
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(
|
||||
'--os-cacert',
|
||||
metavar='<ca-certificate>',
|
||||
default=env('OS_CACERT', default=None),
|
||||
help=_("Specify a CA bundle file to use in "
|
||||
"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(
|
||||
'--insecure',
|
||||
|
@ -280,8 +439,6 @@ class TackerShell(app.App):
|
|||
"not be verified against any certificate authorities. "
|
||||
"This option should be used with caution."))
|
||||
|
||||
return parser
|
||||
|
||||
def _bash_completion(self):
|
||||
"""Prints all of the commands and options for bash-completion."""
|
||||
commands = set()
|
||||
|
@ -297,6 +454,26 @@ class TackerShell(app.App):
|
|||
options.add(option)
|
||||
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):
|
||||
"""Equivalent to the main program for the application.
|
||||
|
||||
|
@ -309,7 +486,7 @@ class TackerShell(app.App):
|
|||
help_pos = -1
|
||||
help_command_pos = -1
|
||||
for arg in argv:
|
||||
if arg == 'bash-completion':
|
||||
if arg == 'bash-completion' and help_command_pos == -1:
|
||||
self._bash_completion()
|
||||
return 0
|
||||
if arg in self.commands[self.api_version]:
|
||||
|
@ -331,27 +508,22 @@ class TackerShell(app.App):
|
|||
self.interactive_mode = not remainder
|
||||
self.initialize_app(remainder)
|
||||
except Exception as err:
|
||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
||||
self.log.exception(unicode(err))
|
||||
if self.options.verbose_level >= self.DEBUG_LEVEL:
|
||||
self.log.exception(err)
|
||||
raise
|
||||
else:
|
||||
self.log.error(unicode(err))
|
||||
self.log.error(err)
|
||||
return 1
|
||||
result = 1
|
||||
if self.interactive_mode:
|
||||
_argv = [sys.argv[0]]
|
||||
sys.argv = _argv
|
||||
result = self.interact()
|
||||
else:
|
||||
result = self.run_subcommand(remainder)
|
||||
return result
|
||||
return self.interact()
|
||||
return self.run_subcommand(remainder)
|
||||
|
||||
def run_subcommand(self, argv):
|
||||
subcommand = self.command_manager.find_command(argv)
|
||||
cmd_factory, cmd_name, sub_argv = subcommand
|
||||
cmd = cmd_factory(self, self.options)
|
||||
err = None
|
||||
result = 1
|
||||
try:
|
||||
self.prepare_to_run_command(cmd)
|
||||
full_name = (cmd_name
|
||||
|
@ -360,29 +532,12 @@ class TackerShell(app.App):
|
|||
)
|
||||
cmd_parser = cmd.get_parser(full_name)
|
||||
return run_command(cmd, cmd_parser, sub_argv)
|
||||
except Exception as err:
|
||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
||||
self.log.exception(unicode(err))
|
||||
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:
|
||||
except Exception as e:
|
||||
if self.options.verbose_level >= self.DEBUG_LEVEL:
|
||||
self.log.exception("%s", e)
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
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
|
||||
self.log.error("%s", e)
|
||||
return 1
|
||||
|
||||
def authenticate_user(self):
|
||||
"""Make sure the user has provided all of the authentication
|
||||
|
@ -394,43 +549,74 @@ class TackerShell(app.App):
|
|||
if not self.options.os_token:
|
||||
raise exc.CommandError(
|
||||
_("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:
|
||||
raise exc.CommandError(
|
||||
_("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:
|
||||
# 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
|
||||
and not self.options.os_user_id):
|
||||
and not self.options.os_user_id):
|
||||
raise exc.CommandError(
|
||||
_("You must provide a username or user ID via"
|
||||
" --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:
|
||||
raise exc.CommandError(
|
||||
_("You must provide a password via"
|
||||
" either --os-password or env[OS_PASSWORD]"))
|
||||
# No password, If we've got a tty, try prompting for it
|
||||
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||
# Check for Ctl-D
|
||||
try:
|
||||
self.options.os_password = getpass.getpass(
|
||||
'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
|
||||
and not self.options.os_tenant_id):
|
||||
if (not project_info):
|
||||
# tenent is deprecated in Keystone v3. Use the latest
|
||||
# terminology instead.
|
||||
raise exc.CommandError(
|
||||
_("You must provide a tenant_name or tenant_id via"
|
||||
" --os-tenant-name, env[OS_TENANT_NAME]"
|
||||
" --os-tenant-id, or via env[OS_TENANT_ID]"))
|
||||
_("You must provide a project_id or project_name ("
|
||||
"with project_domain_name or project_domain_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:
|
||||
raise exc.CommandError(
|
||||
_("You must provide an auth url via"
|
||||
" either --os-auth-url or via env[OS_AUTH_URL]"))
|
||||
auth_session = self._get_keystone_session()
|
||||
auth = auth_session.auth
|
||||
else: # not keystone
|
||||
if not self.options.os_url:
|
||||
raise exc.CommandError(
|
||||
_("You must provide a service URL via"
|
||||
" either --os-url or env[OS_URL]"))
|
||||
auth_session = None
|
||||
auth = None
|
||||
|
||||
self.client_manager = clientmanager.ClientManager(
|
||||
token=self.options.os_token,
|
||||
|
@ -444,10 +630,18 @@ class TackerShell(app.App):
|
|||
region_name=self.options.os_region_name,
|
||||
api_version=self.api_version,
|
||||
auth_strategy=self.options.os_auth_strategy,
|
||||
service_type=self.options.service_type,
|
||||
endpoint_type=self.options.endpoint_type,
|
||||
# FIXME (bklei) honor deprecated service_type and
|
||||
# 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,
|
||||
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)
|
||||
return
|
||||
|
||||
|
@ -471,11 +665,6 @@ class TackerShell(app.App):
|
|||
if self.interactive_mode or cmd_name != 'help':
|
||||
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):
|
||||
"""Create logging handlers for any log output."""
|
||||
root_logger = logging.getLogger('')
|
||||
|
@ -489,24 +678,118 @@ class TackerShell(app.App):
|
|||
self.INFO_LEVEL: logging.INFO,
|
||||
self.DEBUG_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:
|
||||
formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT)
|
||||
else:
|
||||
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
|
||||
logging.getLogger('iso8601.iso8601').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
||||
console.setFormatter(formatter)
|
||||
root_logger.addHandler(console)
|
||||
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:]):
|
||||
try:
|
||||
return TackerShell(TACKER_API_VERSION).run(map(strutils.safe_decode,
|
||||
argv))
|
||||
return TackerShell(TACKER_API_VERSION).run(
|
||||
list(map(encodeutils.safe_decode, argv)))
|
||||
except KeyboardInterrupt:
|
||||
print("... terminating tacker client", file=sys.stderr)
|
||||
return 130
|
||||
except exc.TackerClientException:
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(unicode(e))
|
||||
print(e)
|
||||
return 1
|
||||
|
||||
|
||||
|
|
|
@ -43,10 +43,15 @@ def make_client(instance):
|
|||
region_name=instance._region_name,
|
||||
auth_url=instance._auth_url,
|
||||
endpoint_url=url,
|
||||
endpoint_type=instance._endpoint_type,
|
||||
token=instance._token,
|
||||
auth_strategy=instance._auth_strategy,
|
||||
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
|
||||
else:
|
||||
raise exceptions.UnsupportedVersion(_("API version %s is not "
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright 2012 OpenStack Foundation.
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
|
@ -16,31 +17,29 @@
|
|||
|
||||
import logging
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import requests
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from tackerclient import client
|
||||
from tackerclient.common import _
|
||||
from tackerclient.common import constants
|
||||
from tackerclient.common import exceptions
|
||||
from tackerclient.common import serializer
|
||||
from tackerclient.common import utils
|
||||
from tackerclient.i18n import _
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
Tacker exception according to the contents of the
|
||||
response body
|
||||
This routine generates the appropriate Tacker exception according to
|
||||
the contents of the response body.
|
||||
|
||||
:param status_code: HTTP error status code
|
||||
:param error_content: deserialized body of error response
|
||||
:param status_code: HTTP error status code
|
||||
:param error_content: deserialized body of error response
|
||||
"""
|
||||
error_dict = None
|
||||
if isinstance(error_content, dict):
|
||||
|
@ -87,8 +86,7 @@ def exception_handler_v10(status_code, error_content):
|
|||
|
||||
|
||||
class APIParamsCall(object):
|
||||
"""A Decorator to add support for format and tenant overriding
|
||||
and filters
|
||||
"""A Decorator to add support for format and tenant overriding and filters.
|
||||
"""
|
||||
def __init__(self, function):
|
||||
self.function = function
|
||||
|
@ -104,7 +102,7 @@ class APIParamsCall(object):
|
|||
return with_params
|
||||
|
||||
|
||||
class Client(object):
|
||||
class ClientBase(object):
|
||||
"""Client for the OpenStack Tacker v1.0 API.
|
||||
|
||||
:param string username: Username for authentication. (optional)
|
||||
|
@ -113,6 +111,8 @@ class Client(object):
|
|||
:param string token: Token for authentication. (optional)
|
||||
:param string tenant_name: Tenant name. (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 service_type: Network service type to pull from the
|
||||
keystone catalog (e.g. 'network') (optional)
|
||||
|
@ -128,7 +128,17 @@ class Client(object):
|
|||
:param integer timeout: Allows customization of the timeout for client
|
||||
http requests. (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 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::
|
||||
|
||||
|
@ -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
|
||||
# This variable should be overridden by a child class.
|
||||
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):
|
||||
if self.format == 'json':
|
||||
|
@ -168,9 +242,107 @@ class Client(object):
|
|||
ns = dict([(ext['alias'], ext['namespace']) for ext in exts])
|
||||
self.EXTED_PLURALS.update(constants.PLURALS)
|
||||
return {'plurals': self.EXTED_PLURALS,
|
||||
'xmlns': constants.XML_NS_V10,
|
||||
'xmlns': constants.XML_NS_V20,
|
||||
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
|
||||
def list_extensions(self, **_params):
|
||||
"""Fetch a list of all exts on server side."""
|
||||
|
@ -229,168 +401,3 @@ class Client(object):
|
|||
@APIParamsCall
|
||||
def detach_interface(self, device, body=None):
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue