catch up neutronclient change
Change-Id: I1354fe5378566dec66e7cac311a394cb5498c734
This commit is contained in:
@@ -22,11 +22,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from keystoneclient import access
|
from keystoneclient import access
|
||||||
|
from keystoneclient import adapter
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tackerclient.common import exceptions
|
from tackerclient.common import exceptions
|
||||||
from tackerclient.common import utils
|
from tackerclient.common import utils
|
||||||
from tackerclient.openstack.common.gettextutils import _
|
from tackerclient.i18n import _
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -39,12 +40,14 @@ else:
|
|||||||
_requests_log_level = logging.WARNING
|
_requests_log_level = logging.WARNING
|
||||||
|
|
||||||
logging.getLogger("requests").setLevel(_requests_log_level)
|
logging.getLogger("requests").setLevel(_requests_log_level)
|
||||||
|
MAX_URI_LEN = 8192
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
class HTTPClient(object):
|
||||||
"""Handles the REST calls and responses, include authn."""
|
"""Handles the REST calls and responses, include authn."""
|
||||||
|
|
||||||
USER_AGENT = 'python-tackerclient'
|
USER_AGENT = 'python-tackerclient'
|
||||||
|
CONTENT_TYPE = 'application/json'
|
||||||
|
|
||||||
def __init__(self, username=None, user_id=None,
|
def __init__(self, username=None, user_id=None,
|
||||||
tenant_name=None, tenant_id=None,
|
tenant_name=None, tenant_id=None,
|
||||||
@@ -69,7 +72,6 @@ class HTTPClient(object):
|
|||||||
self.auth_token = token
|
self.auth_token = token
|
||||||
self.auth_tenant_id = None
|
self.auth_tenant_id = None
|
||||||
self.auth_user_id = None
|
self.auth_user_id = None
|
||||||
self.content_type = 'application/json'
|
|
||||||
self.endpoint_url = endpoint_url
|
self.endpoint_url = endpoint_url
|
||||||
self.auth_strategy = auth_strategy
|
self.auth_strategy = auth_strategy
|
||||||
self.log_credentials = log_credentials
|
self.log_credentials = log_credentials
|
||||||
@@ -83,17 +85,8 @@ class HTTPClient(object):
|
|||||||
kargs.setdefault('headers', kwargs.get('headers', {}))
|
kargs.setdefault('headers', kwargs.get('headers', {}))
|
||||||
kargs['headers']['User-Agent'] = self.USER_AGENT
|
kargs['headers']['User-Agent'] = self.USER_AGENT
|
||||||
|
|
||||||
if 'content_type' in kwargs:
|
|
||||||
kargs['headers']['Content-Type'] = kwargs['content_type']
|
|
||||||
kargs['headers']['Accept'] = kwargs['content_type']
|
|
||||||
else:
|
|
||||||
kargs['headers']['Content-Type'] = self.content_type
|
|
||||||
kargs['headers']['Accept'] = self.content_type
|
|
||||||
|
|
||||||
if 'body' in kwargs:
|
if 'body' in kwargs:
|
||||||
kargs['body'] = kwargs['body']
|
kargs['body'] = kwargs['body']
|
||||||
args = utils.safe_encode_list(args)
|
|
||||||
kargs = utils.safe_encode_dict(kargs)
|
|
||||||
|
|
||||||
if self.log_credentials:
|
if self.log_credentials:
|
||||||
log_kargs = kargs
|
log_kargs = kargs
|
||||||
@@ -112,8 +105,7 @@ class HTTPClient(object):
|
|||||||
_logger.debug("throwing ConnectionFailed : %s", e)
|
_logger.debug("throwing ConnectionFailed : %s", e)
|
||||||
raise exceptions.ConnectionFailed(reason=e)
|
raise exceptions.ConnectionFailed(reason=e)
|
||||||
utils.http_log_resp(_logger, resp, body)
|
utils.http_log_resp(_logger, resp, body)
|
||||||
status_code = self.get_status_code(resp)
|
if resp.status_code == 401:
|
||||||
if status_code == 401:
|
|
||||||
raise exceptions.Unauthorized(message=body)
|
raise exceptions.Unauthorized(message=body)
|
||||||
return resp, body
|
return resp, body
|
||||||
|
|
||||||
@@ -132,24 +124,40 @@ class HTTPClient(object):
|
|||||||
elif not self.endpoint_url:
|
elif not self.endpoint_url:
|
||||||
self.endpoint_url = self._get_endpoint_url()
|
self.endpoint_url = self._get_endpoint_url()
|
||||||
|
|
||||||
def request(self, url, method, **kwargs):
|
def request(self, url, method, body=None, headers=None, **kwargs):
|
||||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
"""Request without authentication."""
|
||||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
|
||||||
kwargs['headers']['Accept'] = 'application/json'
|
content_type = kwargs.pop('content_type', None) or 'application/json'
|
||||||
if 'body' in kwargs:
|
headers = headers or {}
|
||||||
kwargs['headers']['Content-Type'] = 'application/json'
|
headers.setdefault('Accept', content_type)
|
||||||
kwargs['data'] = kwargs['body']
|
|
||||||
del kwargs['body']
|
if body:
|
||||||
|
headers.setdefault('Content-Type', content_type)
|
||||||
|
|
||||||
|
headers['User-Agent'] = self.USER_AGENT
|
||||||
|
|
||||||
resp = requests.request(
|
resp = requests.request(
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
|
data=body,
|
||||||
|
headers=headers,
|
||||||
verify=self.verify_cert,
|
verify=self.verify_cert,
|
||||||
|
timeout=self.timeout,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
return resp, resp.text
|
return resp, resp.text
|
||||||
|
|
||||||
|
def _check_uri_length(self, action):
|
||||||
|
uri_len = len(self.endpoint_url) + len(action)
|
||||||
|
if uri_len > MAX_URI_LEN:
|
||||||
|
raise exceptions.RequestURITooLong(
|
||||||
|
excess=uri_len - MAX_URI_LEN)
|
||||||
|
|
||||||
def do_request(self, url, method, **kwargs):
|
def do_request(self, url, method, **kwargs):
|
||||||
|
# Ensure client always has correct uri - do not guesstimate anything
|
||||||
self.authenticate_and_fetch_endpoint_url()
|
self.authenticate_and_fetch_endpoint_url()
|
||||||
|
self._check_uri_length(url)
|
||||||
|
|
||||||
# Perform the request once. If we get a 401 back then it
|
# Perform the request once. If we get a 401 back then it
|
||||||
# might be because the auth token expired, so try to
|
# might be because the auth token expired, so try to
|
||||||
# re-authenticate and try again. If it still fails, bail.
|
# re-authenticate and try again. If it still fails, bail.
|
||||||
@@ -206,8 +214,7 @@ class HTTPClient(object):
|
|||||||
body=json.dumps(body),
|
body=json.dumps(body),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
allow_redirects=True)
|
allow_redirects=True)
|
||||||
status_code = self.get_status_code(resp)
|
if resp.status_code != 200:
|
||||||
if status_code != 200:
|
|
||||||
raise exceptions.Unauthorized(message=resp_body)
|
raise exceptions.Unauthorized(message=resp_body)
|
||||||
if resp_body:
|
if resp_body:
|
||||||
try:
|
try:
|
||||||
@@ -250,7 +257,7 @@ class HTTPClient(object):
|
|||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
for endpoint in body.get('endpoints', []):
|
for endpoint in body.get('endpoints', []):
|
||||||
if (endpoint['type'] == 'servicevm' and
|
if (endpoint['type'] == 'servicevm' and
|
||||||
endpoint.get('region') == self.region_name):
|
endpoint.get('region') == self.region_name):
|
||||||
if self.endpoint_type not in endpoint:
|
if self.endpoint_type not in endpoint:
|
||||||
raise exceptions.EndpointTypeNotFound(
|
raise exceptions.EndpointTypeNotFound(
|
||||||
type_=self.endpoint_type)
|
type_=self.endpoint_type)
|
||||||
@@ -264,13 +271,122 @@ class HTTPClient(object):
|
|||||||
'auth_user_id': self.auth_user_id,
|
'auth_user_id': self.auth_user_id,
|
||||||
'endpoint_url': self.endpoint_url}
|
'endpoint_url': self.endpoint_url}
|
||||||
|
|
||||||
def get_status_code(self, response):
|
|
||||||
"""Returns the integer status code from the response.
|
|
||||||
|
|
||||||
Either a Webob.Response (used in testing) or requests.Response
|
class SessionClient(adapter.Adapter):
|
||||||
is returned.
|
|
||||||
"""
|
def request(self, *args, **kwargs):
|
||||||
if hasattr(response, 'status_int'):
|
kwargs.setdefault('authenticated', False)
|
||||||
return response.status_int
|
kwargs.setdefault('raise_exc', False)
|
||||||
|
|
||||||
|
content_type = kwargs.pop('content_type', None) or 'application/json'
|
||||||
|
|
||||||
|
headers = kwargs.setdefault('headers', {})
|
||||||
|
headers.setdefault('Accept', content_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs.setdefault('data', kwargs.pop('body'))
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if kwargs.get('data'):
|
||||||
|
headers.setdefault('Content-Type', content_type)
|
||||||
|
|
||||||
|
resp = super(SessionClient, self).request(*args, **kwargs)
|
||||||
|
return resp, resp.text
|
||||||
|
|
||||||
|
def _check_uri_length(self, url):
|
||||||
|
uri_len = len(self.endpoint_url) + len(url)
|
||||||
|
if uri_len > MAX_URI_LEN:
|
||||||
|
raise exceptions.RequestURITooLong(
|
||||||
|
excess=uri_len - MAX_URI_LEN)
|
||||||
|
|
||||||
|
def do_request(self, url, method, **kwargs):
|
||||||
|
kwargs.setdefault('authenticated', True)
|
||||||
|
self._check_uri_length(url)
|
||||||
|
return self.request(url, method, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def endpoint_url(self):
|
||||||
|
# NOTE(jamielennox): This is used purely by the CLI and should be
|
||||||
|
# removed when the CLI gets smarter.
|
||||||
|
return self.get_endpoint()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token(self):
|
||||||
|
# NOTE(jamielennox): This is used purely by the CLI and should be
|
||||||
|
# removed when the CLI gets smarter.
|
||||||
|
return self.get_token()
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
# NOTE(jamielennox): This is used purely by the CLI and should be
|
||||||
|
# removed when the CLI gets smarter.
|
||||||
|
self.get_token()
|
||||||
|
|
||||||
|
def get_auth_info(self):
|
||||||
|
auth_info = {'auth_token': self.auth_token,
|
||||||
|
'endpoint_url': self.endpoint_url}
|
||||||
|
|
||||||
|
# NOTE(jamielennox): This is the best we can do here. It will work
|
||||||
|
# with identity plugins which is the primary case but we should
|
||||||
|
# deprecate it's usage as much as possible.
|
||||||
|
try:
|
||||||
|
get_access = (self.auth or self.session.auth).get_access
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
return response.status_code
|
auth_ref = get_access(self.session)
|
||||||
|
|
||||||
|
auth_info['auth_tenant_id'] = auth_ref.project_id
|
||||||
|
auth_info['auth_user_id'] = auth_ref.user_id
|
||||||
|
|
||||||
|
return auth_info
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME(bklei): Should refactor this to use kwargs and only
|
||||||
|
# explicitly list arguments that are not None.
|
||||||
|
def construct_http_client(username=None,
|
||||||
|
user_id=None,
|
||||||
|
tenant_name=None,
|
||||||
|
tenant_id=None,
|
||||||
|
password=None,
|
||||||
|
auth_url=None,
|
||||||
|
token=None,
|
||||||
|
region_name=None,
|
||||||
|
timeout=None,
|
||||||
|
endpoint_url=None,
|
||||||
|
insecure=False,
|
||||||
|
endpoint_type='publicURL',
|
||||||
|
log_credentials=None,
|
||||||
|
auth_strategy='keystone',
|
||||||
|
ca_cert=None,
|
||||||
|
service_type='servicevm',
|
||||||
|
session=None,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
if session:
|
||||||
|
kwargs.setdefault('user_agent', 'python-tackerclient')
|
||||||
|
kwargs.setdefault('interface', endpoint_type)
|
||||||
|
return SessionClient(session=session,
|
||||||
|
service_type=service_type,
|
||||||
|
region_name=region_name,
|
||||||
|
**kwargs)
|
||||||
|
else:
|
||||||
|
# FIXME(bklei): username and password are now optional. Need
|
||||||
|
# to test that they were provided in this mode. Should also
|
||||||
|
# refactor to use kwargs.
|
||||||
|
return HTTPClient(username=username,
|
||||||
|
password=password,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
tenant_name=tenant_name,
|
||||||
|
user_id=user_id,
|
||||||
|
auth_url=auth_url,
|
||||||
|
token=token,
|
||||||
|
endpoint_url=endpoint_url,
|
||||||
|
insecure=insecure,
|
||||||
|
timeout=timeout,
|
||||||
|
region_name=region_name,
|
||||||
|
endpoint_type=endpoint_type,
|
||||||
|
service_type=service_type,
|
||||||
|
ca_cert=ca_cert,
|
||||||
|
log_credentials=log_credentials,
|
||||||
|
auth_strategy=auth_strategy)
|
||||||
|
@@ -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):
|
class ClientCache(object):
|
||||||
"""Descriptor class for caching created client handles.
|
"""Descriptor class for caching created client handles."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, factory):
|
def __init__(self, factory):
|
||||||
self.factory = factory
|
self.factory = factory
|
||||||
@@ -42,8 +41,7 @@ class ClientCache(object):
|
|||||||
|
|
||||||
|
|
||||||
class ClientManager(object):
|
class ClientManager(object):
|
||||||
"""Manages access to API clients, including authentication.
|
"""Manages access to API clients, including authentication."""
|
||||||
"""
|
|
||||||
tacker = ClientCache(tacker_client.make_client)
|
tacker = ClientCache(tacker_client.make_client)
|
||||||
|
|
||||||
def __init__(self, token=None, url=None,
|
def __init__(self, token=None, url=None,
|
||||||
@@ -61,6 +59,11 @@ class ClientManager(object):
|
|||||||
ca_cert=None,
|
ca_cert=None,
|
||||||
log_credentials=False,
|
log_credentials=False,
|
||||||
service_type=None,
|
service_type=None,
|
||||||
|
timeout=None,
|
||||||
|
retries=0,
|
||||||
|
raise_errors=True,
|
||||||
|
session=None,
|
||||||
|
auth=None,
|
||||||
):
|
):
|
||||||
self._token = token
|
self._token = token
|
||||||
self._url = url
|
self._url = url
|
||||||
@@ -79,11 +82,16 @@ class ClientManager(object):
|
|||||||
self._insecure = insecure
|
self._insecure = insecure
|
||||||
self._ca_cert = ca_cert
|
self._ca_cert = ca_cert
|
||||||
self._log_credentials = log_credentials
|
self._log_credentials = log_credentials
|
||||||
|
self._timeout = timeout
|
||||||
|
self._retries = retries
|
||||||
|
self._raise_errors = raise_errors
|
||||||
|
self._session = session
|
||||||
|
self._auth = auth
|
||||||
return
|
return
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
if not self._url:
|
if not self._url:
|
||||||
httpclient = client.HTTPClient(
|
httpclient = client.construct_http_client(
|
||||||
username=self._username,
|
username=self._username,
|
||||||
user_id=self._user_id,
|
user_id=self._user_id,
|
||||||
tenant_name=self._tenant_name,
|
tenant_name=self._tenant_name,
|
||||||
@@ -95,6 +103,9 @@ class ClientManager(object):
|
|||||||
endpoint_type=self._endpoint_type,
|
endpoint_type=self._endpoint_type,
|
||||||
insecure=self._insecure,
|
insecure=self._insecure,
|
||||||
ca_cert=self._ca_cert,
|
ca_cert=self._ca_cert,
|
||||||
|
timeout=self._timeout,
|
||||||
|
session=self._session,
|
||||||
|
auth=self._auth,
|
||||||
log_credentials=self._log_credentials)
|
log_credentials=self._log_credentials)
|
||||||
httpclient.authenticate()
|
httpclient.authenticate()
|
||||||
# Populate other password flow attributes
|
# Populate other password flow attributes
|
||||||
|
@@ -14,16 +14,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
"""
|
|
||||||
OpenStack base command
|
|
||||||
"""
|
|
||||||
|
|
||||||
from cliff import command
|
from cliff import command
|
||||||
|
|
||||||
|
|
||||||
class OpenStackCommand(command.Command):
|
class OpenStackCommand(command.Command):
|
||||||
"""Base class for OpenStack commands
|
"""Base class for OpenStack commands."""
|
||||||
"""
|
|
||||||
|
|
||||||
api = None
|
api = None
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
|
|
||||||
EXT_NS = '_extension_ns'
|
EXT_NS = '_extension_ns'
|
||||||
XML_NS_V10 = 'http://openstack.org/tacker/api/v1.0'
|
XML_NS_V20 = 'http://openstack.org/tacker/api/v1.0'
|
||||||
XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
XSI_ATTR = "xsi:nil"
|
XSI_ATTR = "xsi:nil"
|
||||||
XSI_NIL_ATTR = "xmlns:xsi"
|
XSI_NIL_ATTR = "xmlns:xsi"
|
||||||
@@ -33,6 +33,7 @@ TYPE_FLOAT = "float"
|
|||||||
TYPE_LIST = "list"
|
TYPE_LIST = "list"
|
||||||
TYPE_DICT = "dict"
|
TYPE_DICT = "dict"
|
||||||
|
|
||||||
|
|
||||||
PLURALS = {'templates': 'template',
|
PLURALS = {'templates': 'template',
|
||||||
'devices': 'device',
|
'devices': 'device',
|
||||||
'services': 'service'}
|
'services': 'service'}
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from tackerclient.common import _
|
from tackerclient.i18n import _
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Tacker base exception handling.
|
Tacker base exception handling.
|
||||||
@@ -30,12 +30,11 @@ Exceptions are classified into three categories:
|
|||||||
|
|
||||||
|
|
||||||
class TackerException(Exception):
|
class TackerException(Exception):
|
||||||
"""Base Tacker Exception
|
"""Base Tacker Exception.
|
||||||
|
|
||||||
To correctly use this class, inherit from it and define
|
To correctly use this class, inherit from it and define
|
||||||
a 'message' property. That message will get printf'd
|
a 'message' property. That message will get printf'd
|
||||||
with the keyword arguments provided to the constructor.
|
with the keyword arguments provided to the constructor.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
message = _("An unknown exception occurred.")
|
message = _("An unknown exception occurred.")
|
||||||
|
|
||||||
@@ -60,6 +59,8 @@ class TackerClientException(TackerException):
|
|||||||
blocks. The actual error message is the one generated on the server side.
|
blocks. The actual error message is the one generated on the server side.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
status_code = 0
|
||||||
|
|
||||||
def __init__(self, message=None, **kwargs):
|
def __init__(self, message=None, **kwargs):
|
||||||
if 'status_code' in kwargs:
|
if 'status_code' in kwargs:
|
||||||
self.status_code = kwargs['status_code']
|
self.status_code = kwargs['status_code']
|
||||||
@@ -139,6 +140,10 @@ class IpAddressInUseClient(Conflict):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIpForNetworkClient(BadRequest):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OverQuotaClient(Conflict):
|
class OverQuotaClient(Conflict):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -153,6 +158,10 @@ class IpAddressGenerationFailureClient(Conflict):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MacAddressInUseClient(Conflict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExternalIpAddressExhaustedClient(BadRequest):
|
class ExternalIpAddressExhaustedClient(BadRequest):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -212,8 +221,8 @@ class CommandError(TackerCLIError):
|
|||||||
|
|
||||||
|
|
||||||
class UnsupportedVersion(TackerCLIError):
|
class UnsupportedVersion(TackerCLIError):
|
||||||
"""Indicates that the user is trying to use an unsupported
|
"""Indicates that the user is trying to use an unsupported version of
|
||||||
version of the API
|
the API.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
86
tackerclient/common/extension.py
Normal file
86
tackerclient/common/extension.py
Normal 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)
|
@@ -12,23 +12,23 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
#
|
|
||||||
###
|
|
||||||
### Codes from tacker wsgi
|
|
||||||
###
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from xml.etree import ElementTree as etree
|
from xml.etree import ElementTree as etree
|
||||||
from xml.parsers import expat
|
from xml.parsers import expat
|
||||||
|
|
||||||
|
from oslo.serialization import jsonutils
|
||||||
|
import six
|
||||||
|
|
||||||
from tackerclient.common import constants
|
from tackerclient.common import constants
|
||||||
from tackerclient.common import exceptions as exception
|
from tackerclient.common import exceptions as exception
|
||||||
from tackerclient.openstack.common.gettextutils import _
|
from tackerclient.i18n import _
|
||||||
from tackerclient.openstack.common import jsonutils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
long = int
|
||||||
|
|
||||||
|
|
||||||
class ActionDispatcher(object):
|
class ActionDispatcher(object):
|
||||||
"""Maps method name to local methods through action name."""
|
"""Maps method name to local methods through action name."""
|
||||||
@@ -58,7 +58,7 @@ class JSONDictSerializer(DictSerializer):
|
|||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
def sanitizer(obj):
|
def sanitizer(obj):
|
||||||
return unicode(obj)
|
return six.text_type(obj)
|
||||||
return jsonutils.dumps(data, default=sanitizer)
|
return jsonutils.dumps(data, default=sanitizer)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,16 +67,16 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
def __init__(self, metadata=None, xmlns=None):
|
def __init__(self, metadata=None, xmlns=None):
|
||||||
"""XMLDictSerializer constructor.
|
"""XMLDictSerializer constructor.
|
||||||
|
|
||||||
:param metadata: information needed to deserialize xml into
|
:param metadata: information needed to deserialize XML into
|
||||||
a dictionary.
|
a dictionary.
|
||||||
:param xmlns: XML namespace to include with serialized xml
|
:param xmlns: XML namespace to include with serialized XML
|
||||||
"""
|
"""
|
||||||
super(XMLDictSerializer, self).__init__()
|
super(XMLDictSerializer, self).__init__()
|
||||||
self.metadata = metadata or {}
|
self.metadata = metadata or {}
|
||||||
if not xmlns:
|
if not xmlns:
|
||||||
xmlns = self.metadata.get('xmlns')
|
xmlns = self.metadata.get('xmlns')
|
||||||
if not xmlns:
|
if not xmlns:
|
||||||
xmlns = constants.XML_NS_V10
|
xmlns = constants.XML_NS_V20
|
||||||
self.xmlns = xmlns
|
self.xmlns = xmlns
|
||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
@@ -93,13 +93,13 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
root_key = constants.VIRTUAL_ROOT_KEY
|
root_key = constants.VIRTUAL_ROOT_KEY
|
||||||
root_value = None
|
root_value = None
|
||||||
else:
|
else:
|
||||||
link_keys = [k for k in data.iterkeys() or []
|
link_keys = [k for k in six.iterkeys(data) or []
|
||||||
if k.endswith('_links')]
|
if k.endswith('_links')]
|
||||||
if link_keys:
|
if link_keys:
|
||||||
links = data.pop(link_keys[0], None)
|
links = data.pop(link_keys[0], None)
|
||||||
has_atom = True
|
has_atom = True
|
||||||
root_key = (len(data) == 1 and
|
root_key = (len(data) == 1 and
|
||||||
data.keys()[0] or constants.VIRTUAL_ROOT_KEY)
|
list(data.keys())[0] or constants.VIRTUAL_ROOT_KEY)
|
||||||
root_value = data.get(root_key, data)
|
root_value = data.get(root_key, data)
|
||||||
doc = etree.Element("_temp_root")
|
doc = etree.Element("_temp_root")
|
||||||
used_prefixes = []
|
used_prefixes = []
|
||||||
@@ -122,8 +122,8 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
self._add_xmlns(node, used_prefixes, has_atom)
|
self._add_xmlns(node, used_prefixes, has_atom)
|
||||||
return etree.tostring(node, encoding='UTF-8')
|
return etree.tostring(node, encoding='UTF-8')
|
||||||
|
|
||||||
#NOTE (ameade): the has_atom should be removed after all of the
|
# NOTE(ameade): the has_atom should be removed after all of the
|
||||||
# xml serializers and view builders have been updated to the current
|
# XML serializers and view builders have been updated to the current
|
||||||
# spec that required all responses include the xmlns:atom, the has_atom
|
# spec that required all responses include the xmlns:atom, the has_atom
|
||||||
# flag is to prevent current tests from breaking
|
# flag is to prevent current tests from breaking
|
||||||
def _add_xmlns(self, node, used_prefixes, has_atom=False):
|
def _add_xmlns(self, node, used_prefixes, has_atom=False):
|
||||||
@@ -142,7 +142,7 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
result = etree.SubElement(parent, nodename)
|
result = etree.SubElement(parent, nodename)
|
||||||
if ":" in nodename:
|
if ":" in nodename:
|
||||||
used_prefixes.append(nodename.split(":", 1)[0])
|
used_prefixes.append(nodename.split(":", 1)[0])
|
||||||
#TODO(bcwaldon): accomplish this without a type-check
|
# TODO(bcwaldon): accomplish this without a type-check
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
if not data:
|
if not data:
|
||||||
result.set(
|
result.set(
|
||||||
@@ -158,7 +158,7 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
for item in data:
|
for item in data:
|
||||||
self._to_xml_node(result, metadata, singular, item,
|
self._to_xml_node(result, metadata, singular, item,
|
||||||
used_prefixes)
|
used_prefixes)
|
||||||
#TODO(bcwaldon): accomplish this without a type-check
|
# TODO(bcwaldon): accomplish this without a type-check
|
||||||
elif isinstance(data, dict):
|
elif isinstance(data, dict):
|
||||||
if not data:
|
if not data:
|
||||||
result.set(
|
result.set(
|
||||||
@@ -191,13 +191,10 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
result.set(
|
result.set(
|
||||||
constants.TYPE_ATTR,
|
constants.TYPE_ATTR,
|
||||||
constants.TYPE_FLOAT)
|
constants.TYPE_FLOAT)
|
||||||
LOG.debug(_("Data %(data)s type is %(type)s"),
|
LOG.debug("Data %(data)s type is %(type)s",
|
||||||
{'data': data,
|
{'data': data,
|
||||||
'type': type(data)})
|
'type': type(data)})
|
||||||
if isinstance(data, str):
|
result.text = six.text_type(data)
|
||||||
result.text = unicode(data, 'utf-8')
|
|
||||||
else:
|
|
||||||
result.text = unicode(data)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _create_link_nodes(self, xml_doc, links):
|
def _create_link_nodes(self, xml_doc, links):
|
||||||
@@ -235,14 +232,14 @@ class XMLDeserializer(TextDeserializer):
|
|||||||
def __init__(self, metadata=None):
|
def __init__(self, metadata=None):
|
||||||
"""XMLDeserializer constructor.
|
"""XMLDeserializer constructor.
|
||||||
|
|
||||||
:param metadata: information needed to deserialize xml into
|
:param metadata: information needed to deserialize XML into
|
||||||
a dictionary.
|
a dictionary.
|
||||||
"""
|
"""
|
||||||
super(XMLDeserializer, self).__init__()
|
super(XMLDeserializer, self).__init__()
|
||||||
self.metadata = metadata or {}
|
self.metadata = metadata or {}
|
||||||
xmlns = self.metadata.get('xmlns')
|
xmlns = self.metadata.get('xmlns')
|
||||||
if not xmlns:
|
if not xmlns:
|
||||||
xmlns = constants.XML_NS_V10
|
xmlns = constants.XML_NS_V20
|
||||||
self.xmlns = xmlns
|
self.xmlns = xmlns
|
||||||
|
|
||||||
def _get_key(self, tag):
|
def _get_key(self, tag):
|
||||||
@@ -290,7 +287,7 @@ class XMLDeserializer(TextDeserializer):
|
|||||||
parseError = False
|
parseError = False
|
||||||
# Python2.7
|
# Python2.7
|
||||||
if (hasattr(etree, 'ParseError') and
|
if (hasattr(etree, 'ParseError') and
|
||||||
isinstance(e, getattr(etree, 'ParseError'))):
|
isinstance(e, getattr(etree, 'ParseError'))):
|
||||||
parseError = True
|
parseError = True
|
||||||
# Python2.6
|
# Python2.6
|
||||||
elif isinstance(e, expat.ExpatError):
|
elif isinstance(e, expat.ExpatError):
|
||||||
@@ -340,9 +337,9 @@ class XMLDeserializer(TextDeserializer):
|
|||||||
result = dict()
|
result = dict()
|
||||||
for attr in node.keys():
|
for attr in node.keys():
|
||||||
if (attr == 'xmlns' or
|
if (attr == 'xmlns' or
|
||||||
attr.startswith('xmlns:') or
|
attr.startswith('xmlns:') or
|
||||||
attr == constants.XSI_ATTR or
|
attr == constants.XSI_ATTR or
|
||||||
attr == constants.TYPE_ATTR):
|
attr == constants.TYPE_ATTR):
|
||||||
continue
|
continue
|
||||||
result[self._get_key(attr)] = node.get(attr)
|
result[self._get_key(attr)] = node.get(attr)
|
||||||
children = list(node)
|
children = list(node)
|
||||||
@@ -392,7 +389,6 @@ class Serializer(object):
|
|||||||
"""Deserialize a string to a dictionary.
|
"""Deserialize a string to a dictionary.
|
||||||
|
|
||||||
The string must be in the format of a supported MIME type.
|
The string must be in the format of a supported MIME type.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.get_deserialize_handler(content_type).deserialize(
|
return self.get_deserialize_handler(content_type).deserialize(
|
||||||
datastring)
|
datastring)
|
||||||
|
@@ -17,21 +17,22 @@
|
|||||||
|
|
||||||
"""Utilities and helper functions."""
|
"""Utilities and helper functions."""
|
||||||
|
|
||||||
import datetime
|
import argparse
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from tackerclient.common import _
|
from oslo.utils import encodeutils
|
||||||
|
from oslo.utils import importutils
|
||||||
|
import six
|
||||||
|
|
||||||
from tackerclient.common import exceptions
|
from tackerclient.common import exceptions
|
||||||
from tackerclient.openstack.common import strutils
|
from tackerclient.i18n import _
|
||||||
|
|
||||||
|
|
||||||
def env(*vars, **kwargs):
|
def env(*vars, **kwargs):
|
||||||
"""Returns the first environment variable set.
|
"""Returns the first environment variable set.
|
||||||
|
|
||||||
if none are non-empty, defaults to '' or keyword arg default.
|
If none are non-empty, defaults to '' or keyword arg default.
|
||||||
"""
|
"""
|
||||||
for v in vars:
|
for v in vars:
|
||||||
value = os.environ.get(v)
|
value = os.environ.get(v)
|
||||||
@@ -40,52 +41,8 @@ def env(*vars, **kwargs):
|
|||||||
return kwargs.get('default', '')
|
return kwargs.get('default', '')
|
||||||
|
|
||||||
|
|
||||||
def to_primitive(value):
|
|
||||||
if isinstance(value, list) or isinstance(value, tuple):
|
|
||||||
o = []
|
|
||||||
for v in value:
|
|
||||||
o.append(to_primitive(v))
|
|
||||||
return o
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
o = {}
|
|
||||||
for k, v in value.iteritems():
|
|
||||||
o[k] = to_primitive(v)
|
|
||||||
return o
|
|
||||||
elif isinstance(value, datetime.datetime):
|
|
||||||
return str(value)
|
|
||||||
elif hasattr(value, 'iteritems'):
|
|
||||||
return to_primitive(dict(value.iteritems()))
|
|
||||||
elif hasattr(value, '__iter__'):
|
|
||||||
return to_primitive(list(value))
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def dumps(value, indent=None):
|
|
||||||
try:
|
|
||||||
return json.dumps(value, indent=indent)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
return json.dumps(to_primitive(value))
|
|
||||||
|
|
||||||
|
|
||||||
def loads(s):
|
|
||||||
return json.loads(s)
|
|
||||||
|
|
||||||
|
|
||||||
def import_class(import_str):
|
|
||||||
"""Returns a class from a string including module and class.
|
|
||||||
|
|
||||||
:param import_str: a string representation of the class name
|
|
||||||
:rtype: the requested class
|
|
||||||
"""
|
|
||||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
|
||||||
__import__(mod_str)
|
|
||||||
return getattr(sys.modules[mod_str], class_str)
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_class(api_name, version, version_map):
|
def get_client_class(api_name, version, version_map):
|
||||||
"""Returns the client class for the requested API version
|
"""Returns the client class for the requested API version.
|
||||||
|
|
||||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
||||||
:param version: the requested API version
|
:param version: the requested API version
|
||||||
@@ -101,10 +58,10 @@ def get_client_class(api_name, version, version_map):
|
|||||||
'map_keys': ', '.join(version_map.keys())}
|
'map_keys': ', '.join(version_map.keys())}
|
||||||
raise exceptions.UnsupportedVersion(msg)
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
return import_class(client_path)
|
return importutils.import_class(client_path)
|
||||||
|
|
||||||
|
|
||||||
def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
def get_item_properties(item, fields, mixed_case_fields=(), formatters=None):
|
||||||
"""Return a tuple containing the item properties.
|
"""Return a tuple containing the item properties.
|
||||||
|
|
||||||
:param item: a single item resource (e.g. Server, Tenant, etc)
|
:param item: a single item resource (e.g. Server, Tenant, etc)
|
||||||
@@ -113,6 +70,9 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
|||||||
:param formatters: dictionary mapping field names to callables
|
:param formatters: dictionary mapping field names to callables
|
||||||
to format the values
|
to format the values
|
||||||
"""
|
"""
|
||||||
|
if formatters is None:
|
||||||
|
formatters = {}
|
||||||
|
|
||||||
row = []
|
row = []
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
@@ -136,22 +96,17 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
|||||||
def str2bool(strbool):
|
def str2bool(strbool):
|
||||||
if strbool is None:
|
if strbool is None:
|
||||||
return None
|
return None
|
||||||
else:
|
return strbool.lower() == 'true'
|
||||||
return strbool.lower() == 'true'
|
|
||||||
|
|
||||||
|
|
||||||
def str2dict(strdict):
|
def str2dict(strdict):
|
||||||
'''Convert key1=value1,key2=value2,... string into dictionary.
|
"""Convert key1=value1,key2=value2,... string into dictionary.
|
||||||
|
|
||||||
:param strdict: key1=value1,key2=value2
|
:param strdict: key1=value1,key2=value2
|
||||||
'''
|
"""
|
||||||
_info = {}
|
if not strdict:
|
||||||
if not strdict:
|
return {}
|
||||||
return _info
|
return dict([kv.split('=', 1) for kv in strdict.split(',')])
|
||||||
for kv_str in strdict.split(","):
|
|
||||||
k, v = kv_str.split("=", 1)
|
|
||||||
_info.update({k: v})
|
|
||||||
return _info
|
|
||||||
|
|
||||||
|
|
||||||
def http_log_req(_logger, args, kwargs):
|
def http_log_req(_logger, args, kwargs):
|
||||||
@@ -171,27 +126,27 @@ def http_log_req(_logger, args, kwargs):
|
|||||||
|
|
||||||
if 'body' in kwargs and kwargs['body']:
|
if 'body' in kwargs and kwargs['body']:
|
||||||
string_parts.append(" -d '%s'" % (kwargs['body']))
|
string_parts.append(" -d '%s'" % (kwargs['body']))
|
||||||
string_parts = safe_encode_list(string_parts)
|
req = encodeutils.safe_encode("".join(string_parts))
|
||||||
_logger.debug(_("\nREQ: %s\n"), "".join(string_parts))
|
_logger.debug("\nREQ: %s\n", req)
|
||||||
|
|
||||||
|
|
||||||
def http_log_resp(_logger, resp, body):
|
def http_log_resp(_logger, resp, body):
|
||||||
if not _logger.isEnabledFor(logging.DEBUG):
|
if not _logger.isEnabledFor(logging.DEBUG):
|
||||||
return
|
return
|
||||||
_logger.debug(_("RESP:%(code)s %(headers)s %(body)s\n"),
|
_logger.debug("RESP:%(code)s %(headers)s %(body)s\n",
|
||||||
{'code': resp.status_code,
|
{'code': resp.status_code,
|
||||||
'headers': resp.headers,
|
'headers': resp.headers,
|
||||||
'body': body})
|
'body': body})
|
||||||
|
|
||||||
|
|
||||||
def _safe_encode_without_obj(data):
|
def _safe_encode_without_obj(data):
|
||||||
if isinstance(data, basestring):
|
if isinstance(data, six.string_types):
|
||||||
return strutils.safe_encode(data)
|
return encodeutils.safe_encode(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def safe_encode_list(data):
|
def safe_encode_list(data):
|
||||||
return map(_safe_encode_without_obj, data)
|
return list(map(_safe_encode_without_obj, data))
|
||||||
|
|
||||||
|
|
||||||
def safe_encode_dict(data):
|
def safe_encode_dict(data):
|
||||||
@@ -203,4 +158,16 @@ def safe_encode_dict(data):
|
|||||||
return (k, safe_encode_dict(v))
|
return (k, safe_encode_dict(v))
|
||||||
return (k, _safe_encode_without_obj(v))
|
return (k, _safe_encode_without_obj(v))
|
||||||
|
|
||||||
return dict(map(_encode_item, data.items()))
|
return dict(list(map(_encode_item, data.items())))
|
||||||
|
|
||||||
|
|
||||||
|
def add_boolean_argument(parser, name, **kwargs):
|
||||||
|
for keyword in ('metavar', 'choices'):
|
||||||
|
kwargs.pop(keyword, None)
|
||||||
|
default = kwargs.pop('default', argparse.SUPPRESS)
|
||||||
|
parser.add_argument(
|
||||||
|
name,
|
||||||
|
metavar='{True,False}',
|
||||||
|
choices=['True', 'true', 'False', 'false'],
|
||||||
|
default=default,
|
||||||
|
**kwargs)
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
from tackerclient.common import exceptions
|
from tackerclient.common import exceptions
|
||||||
from tackerclient.openstack.common.gettextutils import _
|
from tackerclient.i18n import _
|
||||||
|
|
||||||
|
|
||||||
def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
|
def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
|
||||||
@@ -29,7 +29,7 @@ def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
|
|||||||
else:
|
else:
|
||||||
int_val = val
|
int_val = val
|
||||||
if ((min_value is None or min_value <= int_val) and
|
if ((min_value is None or min_value <= int_val) and
|
||||||
(max_value is None or int_val <= max_value)):
|
(max_value is None or int_val <= max_value)):
|
||||||
return
|
return
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
28
tackerclient/i18n.py
Normal file
28
tackerclient/i18n.py
Normal 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
|
@@ -21,18 +21,30 @@ Command-line interface to the Tacker APIs
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import inspect
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from keystoneclient.auth.identity import v2 as v2_auth
|
||||||
|
from keystoneclient.auth.identity import v3 as v3_auth
|
||||||
|
from keystoneclient import discover
|
||||||
|
from keystoneclient.openstack.common.apiclient import exceptions as ks_exc
|
||||||
|
from keystoneclient import session
|
||||||
|
from oslo.utils import encodeutils
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
from cliff import app
|
from cliff import app
|
||||||
from cliff import commandmanager
|
from cliff import commandmanager
|
||||||
|
|
||||||
from tackerclient.common import clientmanager
|
from tackerclient.common import clientmanager
|
||||||
|
from tackerclient.common import command as openstack_command
|
||||||
from tackerclient.common import exceptions as exc
|
from tackerclient.common import exceptions as exc
|
||||||
|
from tackerclient.common import extension as client_extension
|
||||||
from tackerclient.common import utils
|
from tackerclient.common import utils
|
||||||
from tackerclient.openstack.common.gettextutils import _
|
from tackerclient.i18n import _
|
||||||
from tackerclient.openstack.common import strutils
|
|
||||||
from tackerclient.tacker.v1_0 import extension
|
from tackerclient.tacker.v1_0 import extension
|
||||||
from tackerclient.tacker.v1_0.vm import device
|
from tackerclient.tacker.v1_0.vm import device
|
||||||
from tackerclient.tacker.v1_0.vm import device_template
|
from tackerclient.tacker.v1_0.vm import device_template
|
||||||
@@ -70,7 +82,23 @@ def env(*_vars, **kwargs):
|
|||||||
return kwargs.get('default', '')
|
return kwargs.get('default', '')
|
||||||
|
|
||||||
|
|
||||||
|
def check_non_negative_int(value):
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(_("invalid int value: %r") % value)
|
||||||
|
if value < 0:
|
||||||
|
raise argparse.ArgumentTypeError(_("input value %d is negative") %
|
||||||
|
value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class BashCompletionCommand(openstack_command.OpenStackCommand):
|
||||||
|
"""Prints all of the commands and options for bash-completion."""
|
||||||
|
resource = "bash_completion"
|
||||||
|
|
||||||
COMMAND_V1 = {
|
COMMAND_V1 = {
|
||||||
|
'bash-completion': BashCompletionCommand,
|
||||||
'ext-list': extension.ListExt,
|
'ext-list': extension.ListExt,
|
||||||
'ext-show': extension.ShowExt,
|
'ext-show': extension.ShowExt,
|
||||||
'device-template-create': device_template.CreateDeviceTemplate,
|
'device-template-create': device_template.CreateDeviceTemplate,
|
||||||
@@ -134,6 +162,11 @@ class TackerShell(app.App):
|
|||||||
for k, v in self.commands[apiversion].items():
|
for k, v in self.commands[apiversion].items():
|
||||||
self.command_manager.add_command(k, v)
|
self.command_manager.add_command(k, v)
|
||||||
|
|
||||||
|
self._register_extensions(VERSION)
|
||||||
|
|
||||||
|
# Pop the 'complete' to correct the outputs of 'tacker help'.
|
||||||
|
self.command_manager.commands.pop('complete')
|
||||||
|
|
||||||
# This is instantiated in initialize_app() only when using
|
# This is instantiated in initialize_app() only when using
|
||||||
# password flow auth
|
# password flow auth
|
||||||
self.auth_client = None
|
self.auth_client = None
|
||||||
@@ -169,20 +202,64 @@ class TackerShell(app.App):
|
|||||||
action='store_const',
|
action='store_const',
|
||||||
dest='verbose_level',
|
dest='verbose_level',
|
||||||
const=0,
|
const=0,
|
||||||
help=_('Suppress output except warnings and errors'))
|
help=_('Suppress output except warnings and errors.'))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-h', '--help',
|
'-h', '--help',
|
||||||
action=HelpAction,
|
action=HelpAction,
|
||||||
nargs=0,
|
nargs=0,
|
||||||
default=self, # tricky
|
default=self, # tricky
|
||||||
help=_("Show this help message and exit"))
|
help=_("Show this help message and exit."))
|
||||||
# Global arguments
|
parser.add_argument(
|
||||||
|
'-r', '--retries',
|
||||||
|
metavar="NUM",
|
||||||
|
type=check_non_negative_int,
|
||||||
|
default=0,
|
||||||
|
help=_("How many times the request to the Tacker server should "
|
||||||
|
"be retried if it fails."))
|
||||||
|
# FIXME(bklei): this method should come from python-keystoneclient
|
||||||
|
self._append_global_identity_args(parser)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def _append_global_identity_args(self, parser):
|
||||||
|
# FIXME(bklei): these are global identity (Keystone) arguments which
|
||||||
|
# should be consistent and shared by all service clients. Therefore,
|
||||||
|
# they should be provided by python-keystoneclient. We will need to
|
||||||
|
# refactor this code once this functionality is available in
|
||||||
|
# python-keystoneclient.
|
||||||
|
#
|
||||||
|
# Note: At that time we'll need to decide if we can just abandon
|
||||||
|
# the deprecated args (--service-type and --endpoint-type).
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-service-type', metavar='<os-service-type>',
|
||||||
|
default=env('OS_SERVICEVM_SERVICE_TYPE', default='servicevm'),
|
||||||
|
help=_('Defaults to env[OS_SERVICEVM_SERVICE_TYPE] or servicevm.'))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-endpoint-type', metavar='<os-endpoint-type>',
|
||||||
|
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
|
||||||
|
help=_('Defaults to env[OS_ENDPOINT_TYPE] or publicURL.'))
|
||||||
|
|
||||||
|
# FIXME(bklei): --service-type is deprecated but kept in for
|
||||||
|
# backward compatibility.
|
||||||
|
parser.add_argument(
|
||||||
|
'--service-type', metavar='<service-type>',
|
||||||
|
default=env('OS_SERVICEVM_SERVICE_TYPE', default='servicevm'),
|
||||||
|
help=_('DEPRECATED! Use --os-service-type.'))
|
||||||
|
|
||||||
|
# FIXME(bklei): --endpoint-type is deprecated but kept in for
|
||||||
|
# backward compatibility.
|
||||||
|
parser.add_argument(
|
||||||
|
'--endpoint-type', metavar='<endpoint-type>',
|
||||||
|
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
|
||||||
|
help=_('DEPRECATED! Use --os-endpoint-type.'))
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-auth-strategy', metavar='<auth-strategy>',
|
'--os-auth-strategy', metavar='<auth-strategy>',
|
||||||
default=env('OS_AUTH_STRATEGY', default='keystone'),
|
default=env('OS_AUTH_STRATEGY', default='keystone'),
|
||||||
help=_('Authentication strategy (Env: OS_AUTH_STRATEGY'
|
help=_('DEPRECATED! Only keystone is supported.'))
|
||||||
', default keystone). For now, any other value will'
|
|
||||||
' disable the authentication'))
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_auth_strategy',
|
'--os_auth_strategy',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
@@ -190,28 +267,49 @@ class TackerShell(app.App):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-auth-url', metavar='<auth-url>',
|
'--os-auth-url', metavar='<auth-url>',
|
||||||
default=env('OS_AUTH_URL'),
|
default=env('OS_AUTH_URL'),
|
||||||
help=_('Authentication URL (Env: OS_AUTH_URL)'))
|
help=_('Authentication URL, defaults to env[OS_AUTH_URL].'))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_auth_url',
|
'--os_auth_url',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument(
|
project_name_group = parser.add_mutually_exclusive_group()
|
||||||
|
project_name_group.add_argument(
|
||||||
'--os-tenant-name', metavar='<auth-tenant-name>',
|
'--os-tenant-name', metavar='<auth-tenant-name>',
|
||||||
default=env('OS_TENANT_NAME'),
|
default=env('OS_TENANT_NAME'),
|
||||||
help=_('Authentication tenant name (Env: OS_TENANT_NAME)'))
|
help=_('Authentication tenant name, defaults to '
|
||||||
|
'env[OS_TENANT_NAME].'))
|
||||||
|
project_name_group.add_argument(
|
||||||
|
'--os-project-name',
|
||||||
|
metavar='<auth-project-name>',
|
||||||
|
default=utils.env('OS_PROJECT_NAME'),
|
||||||
|
help='Another way to specify tenant name. '
|
||||||
|
'This option is mutually exclusive with '
|
||||||
|
' --os-tenant-name. '
|
||||||
|
'Defaults to env[OS_PROJECT_NAME].')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_tenant_name',
|
'--os_tenant_name',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument(
|
project_id_group = parser.add_mutually_exclusive_group()
|
||||||
|
project_id_group.add_argument(
|
||||||
'--os-tenant-id', metavar='<auth-tenant-id>',
|
'--os-tenant-id', metavar='<auth-tenant-id>',
|
||||||
default=env('OS_TENANT_ID'),
|
default=env('OS_TENANT_ID'),
|
||||||
help=_('Authentication tenant ID (Env: OS_TENANT_ID)'))
|
help=_('Authentication tenant ID, defaults to '
|
||||||
|
'env[OS_TENANT_ID].'))
|
||||||
|
project_id_group.add_argument(
|
||||||
|
'--os-project-id',
|
||||||
|
metavar='<auth-project-id>',
|
||||||
|
default=utils.env('OS_PROJECT_ID'),
|
||||||
|
help='Another way to specify tenant ID. '
|
||||||
|
'This option is mutually exclusive with '
|
||||||
|
' --os-tenant-id. '
|
||||||
|
'Defaults to env[OS_PROJECT_ID].')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-username', metavar='<auth-username>',
|
'--os-username', metavar='<auth-username>',
|
||||||
default=utils.env('OS_USERNAME'),
|
default=utils.env('OS_USERNAME'),
|
||||||
help=_('Authentication username (Env: OS_USERNAME)'))
|
help=_('Authentication username, defaults to env[OS_USERNAME].'))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_username',
|
'--os_username',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
@@ -222,54 +320,115 @@ class TackerShell(app.App):
|
|||||||
help=_('Authentication user ID (Env: OS_USER_ID)'))
|
help=_('Authentication user ID (Env: OS_USER_ID)'))
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-password', metavar='<auth-password>',
|
'--os_user_id',
|
||||||
default=utils.env('OS_PASSWORD'),
|
|
||||||
help=_('Authentication password (Env: OS_PASSWORD)'))
|
|
||||||
parser.add_argument(
|
|
||||||
'--os_password',
|
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-region-name', metavar='<auth-region-name>',
|
'--os-user-domain-id',
|
||||||
default=env('OS_REGION_NAME'),
|
metavar='<auth-user-domain-id>',
|
||||||
help=_('Authentication region name (Env: OS_REGION_NAME)'))
|
default=utils.env('OS_USER_DOMAIN_ID'),
|
||||||
|
help='OpenStack user domain ID. '
|
||||||
|
'Defaults to env[OS_USER_DOMAIN_ID].')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_region_name',
|
'--os_user_domain_id',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-token', metavar='<token>',
|
'--os-user-domain-name',
|
||||||
default=env('OS_TOKEN'),
|
metavar='<auth-user-domain-name>',
|
||||||
help=_('Defaults to env[OS_TOKEN]'))
|
default=utils.env('OS_USER_DOMAIN_NAME'),
|
||||||
|
help='OpenStack user domain name. '
|
||||||
|
'Defaults to env[OS_USER_DOMAIN_NAME].')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_token',
|
'--os_user_domain_name',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--service-type', metavar='<service-type>',
|
'--os_project_id',
|
||||||
default=env('OS_SERVICEVM_SERVICE_TYPE', default='servicevm'),
|
|
||||||
help=_('Defaults to env[OS_SERVICEVM_SERVICE_TYPE] or servicevm.'))
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--endpoint-type', metavar='<endpoint-type>',
|
|
||||||
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
|
|
||||||
help=_('Defaults to env[OS_ENDPOINT_TYPE] or publicURL.'))
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--os-url', metavar='<url>',
|
|
||||||
default=env('OS_URL'),
|
|
||||||
help=_('Defaults to env[OS_URL]'))
|
|
||||||
parser.add_argument(
|
|
||||||
'--os_url',
|
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_project_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-project-domain-id',
|
||||||
|
metavar='<auth-project-domain-id>',
|
||||||
|
default=utils.env('OS_PROJECT_DOMAIN_ID'),
|
||||||
|
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-project-domain-name',
|
||||||
|
metavar='<auth-project-domain-name>',
|
||||||
|
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||||
|
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-cert',
|
||||||
|
metavar='<certificate>',
|
||||||
|
default=utils.env('OS_CERT'),
|
||||||
|
help=_("Path of certificate file to use in SSL "
|
||||||
|
"connection. This file can optionally be "
|
||||||
|
"prepended with the private key. Defaults "
|
||||||
|
"to env[OS_CERT]."))
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-cacert',
|
'--os-cacert',
|
||||||
metavar='<ca-certificate>',
|
metavar='<ca-certificate>',
|
||||||
default=env('OS_CACERT', default=None),
|
default=env('OS_CACERT', default=None),
|
||||||
help=_("Specify a CA bundle file to use in "
|
help=_("Specify a CA bundle file to use in "
|
||||||
"verifying a TLS (https) server certificate. "
|
"verifying a TLS (https) server certificate. "
|
||||||
"Defaults to env[OS_CACERT]"))
|
"Defaults to env[OS_CACERT]."))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-key',
|
||||||
|
metavar='<key>',
|
||||||
|
default=utils.env('OS_KEY'),
|
||||||
|
help=_("Path of client key to use in SSL "
|
||||||
|
"connection. This option is not necessary "
|
||||||
|
"if your key is prepended to your certificate "
|
||||||
|
"file. Defaults to env[OS_KEY]."))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-password', metavar='<auth-password>',
|
||||||
|
default=utils.env('OS_PASSWORD'),
|
||||||
|
help=_('Authentication password, defaults to env[OS_PASSWORD].'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_password',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-region-name', metavar='<auth-region-name>',
|
||||||
|
default=env('OS_REGION_NAME'),
|
||||||
|
help=_('Authentication region name, defaults to '
|
||||||
|
'env[OS_REGION_NAME].'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_region_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-token', metavar='<token>',
|
||||||
|
default=env('OS_TOKEN'),
|
||||||
|
help=_('Authentication token, defaults to env[OS_TOKEN].'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_token',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--http-timeout', metavar='<seconds>',
|
||||||
|
default=env('OS_NETWORK_TIMEOUT', default=None), type=float,
|
||||||
|
help=_('Timeout in seconds to wait for an HTTP response. Defaults '
|
||||||
|
'to env[OS_NETWORK_TIMEOUT] or None if not specified.'))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-url', metavar='<url>',
|
||||||
|
default=env('OS_URL'),
|
||||||
|
help=_('Defaults to env[OS_URL].'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_url',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--insecure',
|
'--insecure',
|
||||||
@@ -280,8 +439,6 @@ class TackerShell(app.App):
|
|||||||
"not be verified against any certificate authorities. "
|
"not be verified against any certificate authorities. "
|
||||||
"This option should be used with caution."))
|
"This option should be used with caution."))
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
def _bash_completion(self):
|
def _bash_completion(self):
|
||||||
"""Prints all of the commands and options for bash-completion."""
|
"""Prints all of the commands and options for bash-completion."""
|
||||||
commands = set()
|
commands = set()
|
||||||
@@ -297,6 +454,26 @@ class TackerShell(app.App):
|
|||||||
options.add(option)
|
options.add(option)
|
||||||
print(' '.join(commands | options))
|
print(' '.join(commands | options))
|
||||||
|
|
||||||
|
def _register_extensions(self, version):
|
||||||
|
for name, module in itertools.chain(
|
||||||
|
client_extension._discover_via_entry_points()):
|
||||||
|
self._extend_shell_commands(module, version)
|
||||||
|
|
||||||
|
def _extend_shell_commands(self, module, version):
|
||||||
|
classes = inspect.getmembers(module, inspect.isclass)
|
||||||
|
for cls_name, cls in classes:
|
||||||
|
if (issubclass(cls, client_extension.TackerClientExtension) and
|
||||||
|
hasattr(cls, 'shell_command')):
|
||||||
|
cmd = cls.shell_command
|
||||||
|
if hasattr(cls, 'versions'):
|
||||||
|
if version not in cls.versions:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
self.command_manager.add_command(cmd, cls)
|
||||||
|
self.commands[version][cmd] = cls
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def run(self, argv):
|
def run(self, argv):
|
||||||
"""Equivalent to the main program for the application.
|
"""Equivalent to the main program for the application.
|
||||||
|
|
||||||
@@ -309,7 +486,7 @@ class TackerShell(app.App):
|
|||||||
help_pos = -1
|
help_pos = -1
|
||||||
help_command_pos = -1
|
help_command_pos = -1
|
||||||
for arg in argv:
|
for arg in argv:
|
||||||
if arg == 'bash-completion':
|
if arg == 'bash-completion' and help_command_pos == -1:
|
||||||
self._bash_completion()
|
self._bash_completion()
|
||||||
return 0
|
return 0
|
||||||
if arg in self.commands[self.api_version]:
|
if arg in self.commands[self.api_version]:
|
||||||
@@ -331,27 +508,22 @@ class TackerShell(app.App):
|
|||||||
self.interactive_mode = not remainder
|
self.interactive_mode = not remainder
|
||||||
self.initialize_app(remainder)
|
self.initialize_app(remainder)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
if self.options.verbose_level >= self.DEBUG_LEVEL:
|
||||||
self.log.exception(unicode(err))
|
self.log.exception(err)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
self.log.error(unicode(err))
|
self.log.error(err)
|
||||||
return 1
|
return 1
|
||||||
result = 1
|
|
||||||
if self.interactive_mode:
|
if self.interactive_mode:
|
||||||
_argv = [sys.argv[0]]
|
_argv = [sys.argv[0]]
|
||||||
sys.argv = _argv
|
sys.argv = _argv
|
||||||
result = self.interact()
|
return self.interact()
|
||||||
else:
|
return self.run_subcommand(remainder)
|
||||||
result = self.run_subcommand(remainder)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def run_subcommand(self, argv):
|
def run_subcommand(self, argv):
|
||||||
subcommand = self.command_manager.find_command(argv)
|
subcommand = self.command_manager.find_command(argv)
|
||||||
cmd_factory, cmd_name, sub_argv = subcommand
|
cmd_factory, cmd_name, sub_argv = subcommand
|
||||||
cmd = cmd_factory(self, self.options)
|
cmd = cmd_factory(self, self.options)
|
||||||
err = None
|
|
||||||
result = 1
|
|
||||||
try:
|
try:
|
||||||
self.prepare_to_run_command(cmd)
|
self.prepare_to_run_command(cmd)
|
||||||
full_name = (cmd_name
|
full_name = (cmd_name
|
||||||
@@ -360,29 +532,12 @@ class TackerShell(app.App):
|
|||||||
)
|
)
|
||||||
cmd_parser = cmd.get_parser(full_name)
|
cmd_parser = cmd.get_parser(full_name)
|
||||||
return run_command(cmd, cmd_parser, sub_argv)
|
return run_command(cmd, cmd_parser, sub_argv)
|
||||||
except Exception as err:
|
except Exception as e:
|
||||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
if self.options.verbose_level >= self.DEBUG_LEVEL:
|
||||||
self.log.exception(unicode(err))
|
self.log.exception("%s", e)
|
||||||
else:
|
|
||||||
self.log.error(unicode(err))
|
|
||||||
try:
|
|
||||||
self.clean_up(cmd, result, err)
|
|
||||||
except Exception as err2:
|
|
||||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
|
||||||
self.log.exception(unicode(err2))
|
|
||||||
else:
|
|
||||||
self.log.error(_('Could not clean up: %s'), unicode(err2))
|
|
||||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
|
||||||
raise
|
raise
|
||||||
else:
|
self.log.error("%s", e)
|
||||||
try:
|
return 1
|
||||||
self.clean_up(cmd, result, None)
|
|
||||||
except Exception as err3:
|
|
||||||
if self.options.verbose_level == self.DEBUG_LEVEL:
|
|
||||||
self.log.exception(unicode(err3))
|
|
||||||
else:
|
|
||||||
self.log.error(_('Could not clean up: %s'), unicode(err3))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def authenticate_user(self):
|
def authenticate_user(self):
|
||||||
"""Make sure the user has provided all of the authentication
|
"""Make sure the user has provided all of the authentication
|
||||||
@@ -394,43 +549,74 @@ class TackerShell(app.App):
|
|||||||
if not self.options.os_token:
|
if not self.options.os_token:
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide a token via"
|
_("You must provide a token via"
|
||||||
" either --os-token or env[OS_TOKEN]"))
|
" either --os-token or env[OS_TOKEN]"
|
||||||
|
" when providing a service URL"))
|
||||||
|
|
||||||
if not self.options.os_url:
|
if not self.options.os_url:
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide a service URL via"
|
_("You must provide a service URL via"
|
||||||
" either --os-url or env[OS_URL]"))
|
" either --os-url or env[OS_URL]"
|
||||||
|
" when providing a token"))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Validate password flow auth
|
# Validate password flow auth
|
||||||
|
project_info = (self.options.os_tenant_name or
|
||||||
|
self.options.os_tenant_id or
|
||||||
|
(self.options.os_project_name and
|
||||||
|
(self.options.os_project_domain_name or
|
||||||
|
self.options.os_project_domain_id)) or
|
||||||
|
self.options.os_project_id)
|
||||||
|
|
||||||
if (not self.options.os_username
|
if (not self.options.os_username
|
||||||
and not self.options.os_user_id):
|
and not self.options.os_user_id):
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide a username or user ID via"
|
_("You must provide a username or user ID via"
|
||||||
" --os-username, env[OS_USERNAME] or"
|
" --os-username, env[OS_USERNAME] or"
|
||||||
" --os-user_id, env[OS_USER_ID]"))
|
" --os-user-id, env[OS_USER_ID]"))
|
||||||
|
|
||||||
if not self.options.os_password:
|
if not self.options.os_password:
|
||||||
raise exc.CommandError(
|
# No password, If we've got a tty, try prompting for it
|
||||||
_("You must provide a password via"
|
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||||
" either --os-password or env[OS_PASSWORD]"))
|
# Check for Ctl-D
|
||||||
|
try:
|
||||||
|
self.options.os_password = getpass.getpass(
|
||||||
|
'OS Password: ')
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
# No password because we didn't have a tty or the
|
||||||
|
# user Ctl-D when prompted.
|
||||||
|
if not self.options.os_password:
|
||||||
|
raise exc.CommandError(
|
||||||
|
_("You must provide a password via"
|
||||||
|
" either --os-password or env[OS_PASSWORD]"))
|
||||||
|
|
||||||
if (not self.options.os_tenant_name
|
if (not project_info):
|
||||||
and not self.options.os_tenant_id):
|
# tenent is deprecated in Keystone v3. Use the latest
|
||||||
|
# terminology instead.
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide a tenant_name or tenant_id via"
|
_("You must provide a project_id or project_name ("
|
||||||
" --os-tenant-name, env[OS_TENANT_NAME]"
|
"with project_domain_name or project_domain_id) "
|
||||||
" --os-tenant-id, or via env[OS_TENANT_ID]"))
|
"via "
|
||||||
|
" --os-project-id (env[OS_PROJECT_ID])"
|
||||||
|
" --os-project-name (env[OS_PROJECT_NAME]),"
|
||||||
|
" --os-project-domain-id "
|
||||||
|
"(env[OS_PROJECT_DOMAIN_ID])"
|
||||||
|
" --os-project-domain-name "
|
||||||
|
"(env[OS_PROJECT_DOMAIN_NAME])"))
|
||||||
|
|
||||||
if not self.options.os_auth_url:
|
if not self.options.os_auth_url:
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide an auth url via"
|
_("You must provide an auth url via"
|
||||||
" either --os-auth-url or via env[OS_AUTH_URL]"))
|
" either --os-auth-url or via env[OS_AUTH_URL]"))
|
||||||
|
auth_session = self._get_keystone_session()
|
||||||
|
auth = auth_session.auth
|
||||||
else: # not keystone
|
else: # not keystone
|
||||||
if not self.options.os_url:
|
if not self.options.os_url:
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide a service URL via"
|
_("You must provide a service URL via"
|
||||||
" either --os-url or env[OS_URL]"))
|
" either --os-url or env[OS_URL]"))
|
||||||
|
auth_session = None
|
||||||
|
auth = None
|
||||||
|
|
||||||
self.client_manager = clientmanager.ClientManager(
|
self.client_manager = clientmanager.ClientManager(
|
||||||
token=self.options.os_token,
|
token=self.options.os_token,
|
||||||
@@ -444,10 +630,18 @@ class TackerShell(app.App):
|
|||||||
region_name=self.options.os_region_name,
|
region_name=self.options.os_region_name,
|
||||||
api_version=self.api_version,
|
api_version=self.api_version,
|
||||||
auth_strategy=self.options.os_auth_strategy,
|
auth_strategy=self.options.os_auth_strategy,
|
||||||
service_type=self.options.service_type,
|
# FIXME (bklei) honor deprecated service_type and
|
||||||
endpoint_type=self.options.endpoint_type,
|
# endpoint type until they are removed
|
||||||
|
service_type=self.options.os_service_type or
|
||||||
|
self.options.service_type,
|
||||||
|
endpoint_type=self.options.os_endpoint_type or self.endpoint_type,
|
||||||
insecure=self.options.insecure,
|
insecure=self.options.insecure,
|
||||||
ca_cert=self.options.os_cacert,
|
ca_cert=self.options.os_cacert,
|
||||||
|
timeout=self.options.http_timeout,
|
||||||
|
retries=self.options.retries,
|
||||||
|
raise_errors=False,
|
||||||
|
session=auth_session,
|
||||||
|
auth=auth,
|
||||||
log_credentials=True)
|
log_credentials=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -471,11 +665,6 @@ class TackerShell(app.App):
|
|||||||
if self.interactive_mode or cmd_name != 'help':
|
if self.interactive_mode or cmd_name != 'help':
|
||||||
self.authenticate_user()
|
self.authenticate_user()
|
||||||
|
|
||||||
def clean_up(self, cmd, result, err):
|
|
||||||
self.log.debug('clean_up %s', cmd.__class__.__name__)
|
|
||||||
if err:
|
|
||||||
self.log.debug(_('Got an error: %s'), unicode(err))
|
|
||||||
|
|
||||||
def configure_logging(self):
|
def configure_logging(self):
|
||||||
"""Create logging handlers for any log output."""
|
"""Create logging handlers for any log output."""
|
||||||
root_logger = logging.getLogger('')
|
root_logger = logging.getLogger('')
|
||||||
@@ -489,24 +678,118 @@ class TackerShell(app.App):
|
|||||||
self.INFO_LEVEL: logging.INFO,
|
self.INFO_LEVEL: logging.INFO,
|
||||||
self.DEBUG_LEVEL: logging.DEBUG,
|
self.DEBUG_LEVEL: logging.DEBUG,
|
||||||
}.get(self.options.verbose_level, logging.DEBUG)
|
}.get(self.options.verbose_level, logging.DEBUG)
|
||||||
console.setLevel(console_level)
|
# The default log level is INFO, in this situation, set the
|
||||||
|
# log level of the console to WARNING, to avoid displaying
|
||||||
|
# useless messages. This equals using "--quiet"
|
||||||
|
if console_level == logging.INFO:
|
||||||
|
console.setLevel(logging.WARNING)
|
||||||
|
else:
|
||||||
|
console.setLevel(console_level)
|
||||||
if logging.DEBUG == console_level:
|
if logging.DEBUG == console_level:
|
||||||
formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT)
|
formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT)
|
||||||
else:
|
else:
|
||||||
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
|
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
|
||||||
|
logging.getLogger('iso8601.iso8601').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
||||||
console.setFormatter(formatter)
|
console.setFormatter(formatter)
|
||||||
root_logger.addHandler(console)
|
root_logger.addHandler(console)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_v2_auth(self, v2_auth_url):
|
||||||
|
return v2_auth.Password(
|
||||||
|
v2_auth_url,
|
||||||
|
username=self.options.os_username,
|
||||||
|
password=self.options.os_password,
|
||||||
|
tenant_id=self.options.os_tenant_id,
|
||||||
|
tenant_name=self.options.os_tenant_name)
|
||||||
|
|
||||||
|
def get_v3_auth(self, v3_auth_url):
|
||||||
|
project_id = self.options.os_project_id or self.options.os_tenant_id
|
||||||
|
project_name = (self.options.os_project_name or
|
||||||
|
self.options.os_tenant_name)
|
||||||
|
|
||||||
|
return v3_auth.Password(
|
||||||
|
v3_auth_url,
|
||||||
|
username=self.options.os_username,
|
||||||
|
password=self.options.os_password,
|
||||||
|
user_id=self.options.os_user_id,
|
||||||
|
user_domain_name=self.options.os_user_domain_name,
|
||||||
|
user_domain_id=self.options.os_user_domain_id,
|
||||||
|
project_id=project_id,
|
||||||
|
project_name=project_name,
|
||||||
|
project_domain_name=self.options.os_project_domain_name,
|
||||||
|
project_domain_id=self.options.os_project_domain_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def _discover_auth_versions(self, session, auth_url):
|
||||||
|
# discover the API versions the server is supporting base on the
|
||||||
|
# given URL
|
||||||
|
try:
|
||||||
|
ks_discover = discover.Discover(session=session, auth_url=auth_url)
|
||||||
|
return (ks_discover.url_for('2.0'), ks_discover.url_for('3.0'))
|
||||||
|
except ks_exc.ClientException:
|
||||||
|
# Identity service may not support discover API version.
|
||||||
|
# Lets try to figure out the API version from the original URL.
|
||||||
|
url_parts = urlparse.urlparse(auth_url)
|
||||||
|
(scheme, netloc, path, params, query, fragment) = url_parts
|
||||||
|
path = path.lower()
|
||||||
|
if path.startswith('/v3'):
|
||||||
|
return (None, auth_url)
|
||||||
|
elif path.startswith('/v2'):
|
||||||
|
return (auth_url, None)
|
||||||
|
else:
|
||||||
|
# not enough information to determine the auth version
|
||||||
|
msg = _('Unable to determine the Keystone version '
|
||||||
|
'to authenticate with using the given '
|
||||||
|
'auth_url. Identity service may not support API '
|
||||||
|
'version discovery. Please provide a versioned '
|
||||||
|
'auth_url instead.')
|
||||||
|
raise exc.CommandError(msg)
|
||||||
|
|
||||||
|
def _get_keystone_session(self):
|
||||||
|
# first create a Keystone session
|
||||||
|
cacert = self.options.os_cacert or None
|
||||||
|
cert = self.options.os_cert or None
|
||||||
|
key = self.options.os_key or None
|
||||||
|
insecure = self.options.insecure or False
|
||||||
|
ks_session = session.Session.construct(dict(cacert=cacert,
|
||||||
|
cert=cert,
|
||||||
|
key=key,
|
||||||
|
insecure=insecure))
|
||||||
|
# discover the supported keystone versions using the given url
|
||||||
|
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
|
||||||
|
session=ks_session,
|
||||||
|
auth_url=self.options.os_auth_url)
|
||||||
|
|
||||||
|
# Determine which authentication plugin to use. First inspect the
|
||||||
|
# auth_url to see the supported version. If both v3 and v2 are
|
||||||
|
# supported, then use the highest version if possible.
|
||||||
|
user_domain_name = self.options.os_user_domain_name or None
|
||||||
|
user_domain_id = self.options.os_user_domain_id or None
|
||||||
|
project_domain_name = self.options.os_project_domain_name or None
|
||||||
|
project_domain_id = self.options.os_project_domain_id or None
|
||||||
|
domain_info = (user_domain_name or user_domain_id or
|
||||||
|
project_domain_name or project_domain_id)
|
||||||
|
|
||||||
|
if (v2_auth_url and not domain_info) or not v3_auth_url:
|
||||||
|
ks_session.auth = self.get_v2_auth(v2_auth_url)
|
||||||
|
else:
|
||||||
|
ks_session.auth = self.get_v3_auth(v3_auth_url)
|
||||||
|
|
||||||
|
return ks_session
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv[1:]):
|
def main(argv=sys.argv[1:]):
|
||||||
try:
|
try:
|
||||||
return TackerShell(TACKER_API_VERSION).run(map(strutils.safe_decode,
|
return TackerShell(TACKER_API_VERSION).run(
|
||||||
argv))
|
list(map(encodeutils.safe_decode, argv)))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("... terminating tacker client", file=sys.stderr)
|
||||||
|
return 130
|
||||||
except exc.TackerClientException:
|
except exc.TackerClientException:
|
||||||
return 1
|
return 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(unicode(e))
|
print(e)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
@@ -43,10 +43,15 @@ def make_client(instance):
|
|||||||
region_name=instance._region_name,
|
region_name=instance._region_name,
|
||||||
auth_url=instance._auth_url,
|
auth_url=instance._auth_url,
|
||||||
endpoint_url=url,
|
endpoint_url=url,
|
||||||
|
endpoint_type=instance._endpoint_type,
|
||||||
token=instance._token,
|
token=instance._token,
|
||||||
auth_strategy=instance._auth_strategy,
|
auth_strategy=instance._auth_strategy,
|
||||||
insecure=instance._insecure,
|
insecure=instance._insecure,
|
||||||
ca_cert=instance._ca_cert)
|
ca_cert=instance._ca_cert,
|
||||||
|
retries=instance._retries,
|
||||||
|
raise_errors=instance._raise_errors,
|
||||||
|
session=instance._session,
|
||||||
|
auth=instance._auth)
|
||||||
return client
|
return client
|
||||||
else:
|
else:
|
||||||
raise exceptions.UnsupportedVersion(_("API version %s is not "
|
raise exceptions.UnsupportedVersion(_("API version %s is not "
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
# Copyright 2012 OpenStack Foundation.
|
# Copyright 2012 OpenStack Foundation.
|
||||||
|
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
# All Rights Reserved
|
# All Rights Reserved
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@@ -16,31 +17,29 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import urllib
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import six.moves.urllib.parse as urlparse
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
from tackerclient import client
|
from tackerclient import client
|
||||||
from tackerclient.common import _
|
|
||||||
from tackerclient.common import constants
|
from tackerclient.common import constants
|
||||||
from tackerclient.common import exceptions
|
from tackerclient.common import exceptions
|
||||||
from tackerclient.common import serializer
|
from tackerclient.common import serializer
|
||||||
from tackerclient.common import utils
|
from tackerclient.common import utils
|
||||||
|
from tackerclient.i18n import _
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def exception_handler_v10(status_code, error_content):
|
def exception_handler_v10(status_code, error_content):
|
||||||
"""Exception handler for API v1.0 client
|
"""Exception handler for API v1.0 client.
|
||||||
|
|
||||||
This routine generates the appropriate
|
This routine generates the appropriate Tacker exception according to
|
||||||
Tacker exception according to the contents of the
|
the contents of the response body.
|
||||||
response body
|
|
||||||
|
|
||||||
:param status_code: HTTP error status code
|
:param status_code: HTTP error status code
|
||||||
:param error_content: deserialized body of error response
|
:param error_content: deserialized body of error response
|
||||||
"""
|
"""
|
||||||
error_dict = None
|
error_dict = None
|
||||||
if isinstance(error_content, dict):
|
if isinstance(error_content, dict):
|
||||||
@@ -87,8 +86,7 @@ def exception_handler_v10(status_code, error_content):
|
|||||||
|
|
||||||
|
|
||||||
class APIParamsCall(object):
|
class APIParamsCall(object):
|
||||||
"""A Decorator to add support for format and tenant overriding
|
"""A Decorator to add support for format and tenant overriding and filters.
|
||||||
and filters
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, function):
|
def __init__(self, function):
|
||||||
self.function = function
|
self.function = function
|
||||||
@@ -104,7 +102,7 @@ class APIParamsCall(object):
|
|||||||
return with_params
|
return with_params
|
||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class ClientBase(object):
|
||||||
"""Client for the OpenStack Tacker v1.0 API.
|
"""Client for the OpenStack Tacker v1.0 API.
|
||||||
|
|
||||||
:param string username: Username for authentication. (optional)
|
:param string username: Username for authentication. (optional)
|
||||||
@@ -113,6 +111,8 @@ class Client(object):
|
|||||||
:param string token: Token for authentication. (optional)
|
:param string token: Token for authentication. (optional)
|
||||||
:param string tenant_name: Tenant name. (optional)
|
:param string tenant_name: Tenant name. (optional)
|
||||||
:param string tenant_id: Tenant id. (optional)
|
:param string tenant_id: Tenant id. (optional)
|
||||||
|
:param string auth_strategy: 'keystone' by default, 'noauth' for no
|
||||||
|
authentication against keystone. (optional)
|
||||||
:param string auth_url: Keystone service endpoint for authorization.
|
:param string auth_url: Keystone service endpoint for authorization.
|
||||||
:param string service_type: Network service type to pull from the
|
:param string service_type: Network service type to pull from the
|
||||||
keystone catalog (e.g. 'network') (optional)
|
keystone catalog (e.g. 'network') (optional)
|
||||||
@@ -128,7 +128,17 @@ class Client(object):
|
|||||||
:param integer timeout: Allows customization of the timeout for client
|
:param integer timeout: Allows customization of the timeout for client
|
||||||
http requests. (optional)
|
http requests. (optional)
|
||||||
:param bool insecure: SSL certificate validation. (optional)
|
:param bool insecure: SSL certificate validation. (optional)
|
||||||
|
:param bool log_credentials: Allow for logging of passwords or not.
|
||||||
|
Defaults to False. (optional)
|
||||||
:param string ca_cert: SSL CA bundle file to use. (optional)
|
:param string ca_cert: SSL CA bundle file to use. (optional)
|
||||||
|
:param integer retries: How many times idempotent (GET, PUT, DELETE)
|
||||||
|
requests to Tacker server should be retried if
|
||||||
|
they fail (default: 0).
|
||||||
|
:param bool raise_errors: If True then exceptions caused by connection
|
||||||
|
failure are propagated to the caller.
|
||||||
|
(default: True)
|
||||||
|
:param session: Keystone client auth session to use. (optional)
|
||||||
|
:param auth: Keystone auth plugin to use. (optional)
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
@@ -143,20 +153,84 @@ class Client(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
extensions_path = "/extensions"
|
|
||||||
extension_path = "/extensions/%s"
|
|
||||||
|
|
||||||
device_templates_path = '/device-templates'
|
|
||||||
device_template_path = '/device-templates/%s'
|
|
||||||
devices_path = '/devices'
|
|
||||||
device_path = '/devices/%s'
|
|
||||||
interface_attach_path = '/devices/%s/attach_interface'
|
|
||||||
interface_detach_path = '/devices/%s/detach_interface'
|
|
||||||
|
|
||||||
# API has no way to report plurals, so we have to hard code them
|
# API has no way to report plurals, so we have to hard code them
|
||||||
|
# This variable should be overridden by a child class.
|
||||||
EXTED_PLURALS = {}
|
EXTED_PLURALS = {}
|
||||||
# 8192 Is the default max URI len for eventlet.wsgi.server
|
|
||||||
MAX_URI_LEN = 8192
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize a new client for the Tacker v1.0 API."""
|
||||||
|
super(ClientBase, self).__init__()
|
||||||
|
self.retries = kwargs.pop('retries', 0)
|
||||||
|
self.raise_errors = kwargs.pop('raise_errors', True)
|
||||||
|
self.httpclient = client.construct_http_client(**kwargs)
|
||||||
|
self.version = '1.0'
|
||||||
|
self.format = 'json'
|
||||||
|
self.action_prefix = "/v%s" % (self.version)
|
||||||
|
self.retry_interval = 1
|
||||||
|
|
||||||
|
def _handle_fault_response(self, status_code, response_body):
|
||||||
|
# Create exception with HTTP status code and message
|
||||||
|
_logger.debug("Error message: %s", response_body)
|
||||||
|
# Add deserialized error message to exception arguments
|
||||||
|
try:
|
||||||
|
des_error_body = self.deserialize(response_body, status_code)
|
||||||
|
except Exception:
|
||||||
|
# If unable to deserialized body it is probably not a
|
||||||
|
# Tacker error
|
||||||
|
des_error_body = {'message': response_body}
|
||||||
|
# Raise the appropriate exception
|
||||||
|
exception_handler_v10(status_code, des_error_body)
|
||||||
|
|
||||||
|
def do_request(self, method, action, body=None, headers=None, params=None):
|
||||||
|
# Add format and tenant_id
|
||||||
|
action += ".%s" % self.format
|
||||||
|
action = self.action_prefix + action
|
||||||
|
if type(params) is dict and params:
|
||||||
|
params = utils.safe_encode_dict(params)
|
||||||
|
action += '?' + urlparse.urlencode(params, doseq=1)
|
||||||
|
|
||||||
|
if body:
|
||||||
|
body = self.serialize(body)
|
||||||
|
|
||||||
|
resp, replybody = self.httpclient.do_request(
|
||||||
|
action, method, body=body,
|
||||||
|
content_type=self.content_type())
|
||||||
|
|
||||||
|
status_code = resp.status_code
|
||||||
|
if status_code in (requests.codes.ok,
|
||||||
|
requests.codes.created,
|
||||||
|
requests.codes.accepted,
|
||||||
|
requests.codes.no_content):
|
||||||
|
return self.deserialize(replybody, status_code)
|
||||||
|
else:
|
||||||
|
if not replybody:
|
||||||
|
replybody = resp.reason
|
||||||
|
self._handle_fault_response(status_code, replybody)
|
||||||
|
|
||||||
|
def get_auth_info(self):
|
||||||
|
return self.httpclient.get_auth_info()
|
||||||
|
|
||||||
|
def serialize(self, data):
|
||||||
|
"""Serializes a dictionary into either XML or JSON.
|
||||||
|
|
||||||
|
A dictionary with a single key can be passed and it can contain any
|
||||||
|
structure.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
elif type(data) is dict:
|
||||||
|
return serializer.Serializer(
|
||||||
|
self.get_attr_metadata()).serialize(data, self.content_type())
|
||||||
|
else:
|
||||||
|
raise Exception(_("Unable to serialize object of type = '%s'") %
|
||||||
|
type(data))
|
||||||
|
|
||||||
|
def deserialize(self, data, status_code):
|
||||||
|
"""Deserializes an XML or JSON string into a dictionary."""
|
||||||
|
if status_code == 204:
|
||||||
|
return data
|
||||||
|
return serializer.Serializer(self.get_attr_metadata()).deserialize(
|
||||||
|
data, self.content_type())['body']
|
||||||
|
|
||||||
def get_attr_metadata(self):
|
def get_attr_metadata(self):
|
||||||
if self.format == 'json':
|
if self.format == 'json':
|
||||||
@@ -168,9 +242,107 @@ class Client(object):
|
|||||||
ns = dict([(ext['alias'], ext['namespace']) for ext in exts])
|
ns = dict([(ext['alias'], ext['namespace']) for ext in exts])
|
||||||
self.EXTED_PLURALS.update(constants.PLURALS)
|
self.EXTED_PLURALS.update(constants.PLURALS)
|
||||||
return {'plurals': self.EXTED_PLURALS,
|
return {'plurals': self.EXTED_PLURALS,
|
||||||
'xmlns': constants.XML_NS_V10,
|
'xmlns': constants.XML_NS_V20,
|
||||||
constants.EXT_NS: ns}
|
constants.EXT_NS: ns}
|
||||||
|
|
||||||
|
def content_type(self, _format=None):
|
||||||
|
"""Returns the mime-type for either 'xml' or 'json'.
|
||||||
|
|
||||||
|
Defaults to the currently set format.
|
||||||
|
"""
|
||||||
|
_format = _format or self.format
|
||||||
|
return "application/%s" % (_format)
|
||||||
|
|
||||||
|
def retry_request(self, method, action, body=None,
|
||||||
|
headers=None, params=None):
|
||||||
|
"""Call do_request with the default retry configuration.
|
||||||
|
|
||||||
|
Only idempotent requests should retry failed connection attempts.
|
||||||
|
:raises: ConnectionFailed if the maximum # of retries is exceeded
|
||||||
|
"""
|
||||||
|
max_attempts = self.retries + 1
|
||||||
|
for i in range(max_attempts):
|
||||||
|
try:
|
||||||
|
return self.do_request(method, action, body=body,
|
||||||
|
headers=headers, params=params)
|
||||||
|
except exceptions.ConnectionFailed:
|
||||||
|
# Exception has already been logged by do_request()
|
||||||
|
if i < self.retries:
|
||||||
|
_logger.debug('Retrying connection to Tacker service')
|
||||||
|
time.sleep(self.retry_interval)
|
||||||
|
elif self.raise_errors:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if self.retries:
|
||||||
|
msg = (_("Failed to connect to Tacker server after %d attempts")
|
||||||
|
% max_attempts)
|
||||||
|
else:
|
||||||
|
msg = _("Failed to connect Tacker server")
|
||||||
|
|
||||||
|
raise exceptions.ConnectionFailed(reason=msg)
|
||||||
|
|
||||||
|
def delete(self, action, body=None, headers=None, params=None):
|
||||||
|
return self.retry_request("DELETE", action, body=body,
|
||||||
|
headers=headers, params=params)
|
||||||
|
|
||||||
|
def get(self, action, body=None, headers=None, params=None):
|
||||||
|
return self.retry_request("GET", action, body=body,
|
||||||
|
headers=headers, params=params)
|
||||||
|
|
||||||
|
def post(self, action, body=None, headers=None, params=None):
|
||||||
|
# Do not retry POST requests to avoid the orphan objects problem.
|
||||||
|
return self.do_request("POST", action, body=body,
|
||||||
|
headers=headers, params=params)
|
||||||
|
|
||||||
|
def put(self, action, body=None, headers=None, params=None):
|
||||||
|
return self.retry_request("PUT", action, body=body,
|
||||||
|
headers=headers, params=params)
|
||||||
|
|
||||||
|
def list(self, collection, path, retrieve_all=True, **params):
|
||||||
|
if retrieve_all:
|
||||||
|
res = []
|
||||||
|
for r in self._pagination(collection, path, **params):
|
||||||
|
res.extend(r[collection])
|
||||||
|
return {collection: res}
|
||||||
|
else:
|
||||||
|
return self._pagination(collection, path, **params)
|
||||||
|
|
||||||
|
def _pagination(self, collection, path, **params):
|
||||||
|
if params.get('page_reverse', False):
|
||||||
|
linkrel = 'previous'
|
||||||
|
else:
|
||||||
|
linkrel = 'next'
|
||||||
|
next = True
|
||||||
|
while next:
|
||||||
|
res = self.get(path, params=params)
|
||||||
|
yield res
|
||||||
|
next = False
|
||||||
|
try:
|
||||||
|
for link in res['%s_links' % collection]:
|
||||||
|
if link['rel'] == linkrel:
|
||||||
|
query_str = urlparse.urlparse(link['href']).query
|
||||||
|
params = urlparse.parse_qs(query_str)
|
||||||
|
next = True
|
||||||
|
break
|
||||||
|
except KeyError:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class Client(ClientBase):
|
||||||
|
|
||||||
|
extensions_path = "/extensions"
|
||||||
|
extension_path = "/extensions/%s"
|
||||||
|
|
||||||
|
device_templates_path = '/device-templates'
|
||||||
|
device_template_path = '/device-templates/%s'
|
||||||
|
devices_path = '/devices'
|
||||||
|
device_path = '/devices/%s'
|
||||||
|
interface_attach_path = '/devices/%s/attach_interface'
|
||||||
|
interface_detach_path = '/devices/%s/detach_interface'
|
||||||
|
|
||||||
|
# API has no way to report plurals, so we have to hard code them
|
||||||
|
# EXTED_PLURALS = {}
|
||||||
|
|
||||||
@APIParamsCall
|
@APIParamsCall
|
||||||
def list_extensions(self, **_params):
|
def list_extensions(self, **_params):
|
||||||
"""Fetch a list of all exts on server side."""
|
"""Fetch a list of all exts on server side."""
|
||||||
@@ -229,168 +401,3 @@ class Client(object):
|
|||||||
@APIParamsCall
|
@APIParamsCall
|
||||||
def detach_interface(self, device, body=None):
|
def detach_interface(self, device, body=None):
|
||||||
return self.put(self.detach_interface_path % device, body)
|
return self.put(self.detach_interface_path % device, body)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
"""Initialize a new client for the Tacker v1.0 API."""
|
|
||||||
super(Client, self).__init__()
|
|
||||||
self.httpclient = client.HTTPClient(**kwargs)
|
|
||||||
self.version = '1.0'
|
|
||||||
self.format = 'json'
|
|
||||||
self.action_prefix = "/v%s" % (self.version)
|
|
||||||
self.retries = 0
|
|
||||||
self.retry_interval = 1
|
|
||||||
|
|
||||||
def _handle_fault_response(self, status_code, response_body):
|
|
||||||
# Create exception with HTTP status code and message
|
|
||||||
_logger.debug(_("Error message: %s"), response_body)
|
|
||||||
# Add deserialized error message to exception arguments
|
|
||||||
try:
|
|
||||||
des_error_body = self.deserialize(response_body, status_code)
|
|
||||||
except Exception:
|
|
||||||
# If unable to deserialized body it is probably not a
|
|
||||||
# Tacker error
|
|
||||||
des_error_body = {'message': response_body}
|
|
||||||
# Raise the appropriate exception
|
|
||||||
exception_handler_v10(status_code, des_error_body)
|
|
||||||
|
|
||||||
def _check_uri_length(self, action):
|
|
||||||
uri_len = len(self.httpclient.endpoint_url) + len(action)
|
|
||||||
if uri_len > self.MAX_URI_LEN:
|
|
||||||
raise exceptions.RequestURITooLong(
|
|
||||||
excess=uri_len - self.MAX_URI_LEN)
|
|
||||||
|
|
||||||
def do_request(self, method, action, body=None, headers=None, params=None):
|
|
||||||
# Add format and tenant_id
|
|
||||||
action += ".%s" % self.format
|
|
||||||
action = self.action_prefix + action
|
|
||||||
if type(params) is dict and params:
|
|
||||||
params = utils.safe_encode_dict(params)
|
|
||||||
action += '?' + urllib.urlencode(params, doseq=1)
|
|
||||||
# Ensure client always has correct uri - do not guesstimate anything
|
|
||||||
self.httpclient.authenticate_and_fetch_endpoint_url()
|
|
||||||
self._check_uri_length(action)
|
|
||||||
|
|
||||||
if body:
|
|
||||||
body = self.serialize(body)
|
|
||||||
self.httpclient.content_type = self.content_type()
|
|
||||||
resp, replybody = self.httpclient.do_request(action, method, body=body)
|
|
||||||
status_code = self.get_status_code(resp)
|
|
||||||
if status_code in (requests.codes.ok,
|
|
||||||
requests.codes.created,
|
|
||||||
requests.codes.accepted,
|
|
||||||
requests.codes.no_content):
|
|
||||||
return self.deserialize(replybody, status_code)
|
|
||||||
else:
|
|
||||||
if not replybody:
|
|
||||||
replybody = resp.reason
|
|
||||||
self._handle_fault_response(status_code, replybody)
|
|
||||||
|
|
||||||
def get_auth_info(self):
|
|
||||||
return self.httpclient.get_auth_info()
|
|
||||||
|
|
||||||
def get_status_code(self, response):
|
|
||||||
"""Returns the integer status code from the response.
|
|
||||||
|
|
||||||
Either a Webob.Response (used in testing) or requests.Response
|
|
||||||
is returned.
|
|
||||||
"""
|
|
||||||
if hasattr(response, 'status_int'):
|
|
||||||
return response.status_int
|
|
||||||
else:
|
|
||||||
return response.status_code
|
|
||||||
|
|
||||||
def serialize(self, data):
|
|
||||||
"""Serializes a dictionary into either xml or json.
|
|
||||||
|
|
||||||
A dictionary with a single key can be passed and
|
|
||||||
it can contain any structure.
|
|
||||||
"""
|
|
||||||
if data is None:
|
|
||||||
return None
|
|
||||||
elif type(data) is dict:
|
|
||||||
return serializer.Serializer(
|
|
||||||
self.get_attr_metadata()).serialize(data, self.content_type())
|
|
||||||
else:
|
|
||||||
raise Exception(_("Unable to serialize object of type = '%s'") %
|
|
||||||
type(data))
|
|
||||||
|
|
||||||
def deserialize(self, data, status_code):
|
|
||||||
"""Deserializes an xml or json string into a dictionary."""
|
|
||||||
if status_code == 204:
|
|
||||||
return data
|
|
||||||
return serializer.Serializer(self.get_attr_metadata()).deserialize(
|
|
||||||
data, self.content_type())['body']
|
|
||||||
|
|
||||||
def content_type(self, _format=None):
|
|
||||||
"""Returns the mime-type for either 'xml' or 'json'.
|
|
||||||
|
|
||||||
Defaults to the currently set format.
|
|
||||||
"""
|
|
||||||
_format = _format or self.format
|
|
||||||
return "application/%s" % (_format)
|
|
||||||
|
|
||||||
def retry_request(self, method, action, body=None,
|
|
||||||
headers=None, params=None):
|
|
||||||
"""Call do_request with the default retry configuration.
|
|
||||||
|
|
||||||
Only idempotent requests should retry failed connection attempts.
|
|
||||||
:raises: ConnectionFailed if the maximum # of retries is exceeded
|
|
||||||
"""
|
|
||||||
max_attempts = self.retries + 1
|
|
||||||
for i in range(max_attempts):
|
|
||||||
try:
|
|
||||||
return self.do_request(method, action, body=body,
|
|
||||||
headers=headers, params=params)
|
|
||||||
except exceptions.ConnectionFailed:
|
|
||||||
# Exception has already been logged by do_request()
|
|
||||||
if i < self.retries:
|
|
||||||
_logger.debug(_('Retrying connection to Tacker service'))
|
|
||||||
time.sleep(self.retry_interval)
|
|
||||||
|
|
||||||
raise exceptions.ConnectionFailed(reason=_("Maximum attempts reached"))
|
|
||||||
|
|
||||||
def delete(self, action, body=None, headers=None, params=None):
|
|
||||||
return self.retry_request("DELETE", action, body=body,
|
|
||||||
headers=headers, params=params)
|
|
||||||
|
|
||||||
def get(self, action, body=None, headers=None, params=None):
|
|
||||||
return self.retry_request("GET", action, body=body,
|
|
||||||
headers=headers, params=params)
|
|
||||||
|
|
||||||
def post(self, action, body=None, headers=None, params=None):
|
|
||||||
# Do not retry POST requests to avoid the orphan objects problem.
|
|
||||||
return self.do_request("POST", action, body=body,
|
|
||||||
headers=headers, params=params)
|
|
||||||
|
|
||||||
def put(self, action, body=None, headers=None, params=None):
|
|
||||||
return self.retry_request("PUT", action, body=body,
|
|
||||||
headers=headers, params=params)
|
|
||||||
|
|
||||||
def list(self, collection, path, retrieve_all=True, **params):
|
|
||||||
if retrieve_all:
|
|
||||||
res = []
|
|
||||||
for r in self._pagination(collection, path, **params):
|
|
||||||
res.extend(r[collection])
|
|
||||||
return {collection: res}
|
|
||||||
else:
|
|
||||||
return self._pagination(collection, path, **params)
|
|
||||||
|
|
||||||
def _pagination(self, collection, path, **params):
|
|
||||||
if params.get('page_reverse', False):
|
|
||||||
linkrel = 'previous'
|
|
||||||
else:
|
|
||||||
linkrel = 'next'
|
|
||||||
next = True
|
|
||||||
while next:
|
|
||||||
res = self.get(path, params=params)
|
|
||||||
yield res
|
|
||||||
next = False
|
|
||||||
try:
|
|
||||||
for link in res['%s_links' % collection]:
|
|
||||||
if link['rel'] == linkrel:
|
|
||||||
query_str = urlparse.urlparse(link['href']).query
|
|
||||||
params = urlparse.parse_qs(query_str)
|
|
||||||
next = True
|
|
||||||
break
|
|
||||||
except KeyError:
|
|
||||||
break
|
|
||||||
|
Reference in New Issue
Block a user