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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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