ac65c2bf07
Change-Id: I97daf416f829078d17d12139f509f9dbed8977cf Signed-off-by: Doug Hellmann <doug@doughellmann.com>
226 lines
7.2 KiB
Python
226 lines
7.2 KiB
Python
# Copyright (c) 2014 Mirantis Inc.
|
|
#
|
|
# 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 inspect
|
|
|
|
import six
|
|
|
|
from storyboardclient._apiclient import base
|
|
from storyboardclient._apiclient import client
|
|
from storyboardclient.auth import oauth
|
|
|
|
DEFAULT_API_URL = "https://storyboard-dev.openstack.org/api/v1"
|
|
|
|
|
|
class BaseClient(client.BaseClient):
|
|
|
|
def __init__(self, api_url=None, access_token=None, verify=True):
|
|
if not api_url:
|
|
api_url = DEFAULT_API_URL
|
|
|
|
self.auth_plugin = oauth.OAuthPlugin(api_url, access_token)
|
|
self.http_client = BaseHTTPClient(auth_plugin=self.auth_plugin,
|
|
verify=verify)
|
|
|
|
|
|
class BaseHTTPClient(client.HTTPClient):
|
|
"""Base class for setting up endpoint and token.
|
|
|
|
This HTTP client is overriding a client_request method to add
|
|
Authorization header if OAuth token is provided.
|
|
|
|
"""
|
|
|
|
def client_request(self, client, method, url, **kwargs):
|
|
"""Send an http request using `client`'s endpoint and specified `url`.
|
|
|
|
If request was rejected as unauthorized (possibly because the token is
|
|
expired), issue one authorization attempt and send the request once
|
|
again.
|
|
|
|
:param client: instance of BaseClient descendant
|
|
:param method: method of HTTP request
|
|
:param url: URL of HTTP request
|
|
:param kwargs: any other parameter that can be passed to
|
|
`HTTPClient.request`
|
|
"""
|
|
|
|
token, endpoint = (self.cached_token, client.cached_endpoint)
|
|
if not (token and endpoint):
|
|
token, endpoint = self.auth_plugin.token_and_endpoint()
|
|
self.cached_token = token
|
|
client.cached_endpoint = endpoint
|
|
|
|
if token:
|
|
kwargs.setdefault("headers", {})["Authorization"] = \
|
|
"Bearer %s" % token
|
|
|
|
return self.request(method, self.concat_url(endpoint, url), **kwargs)
|
|
|
|
|
|
class BaseManager(base.CrudManager):
|
|
|
|
def build_url(self, base_url=None, method=None, **kwargs):
|
|
# Overriding to use "url_key" instead of the "collection_key".
|
|
# "key_id" is replaced with just "id" when querying a specific object.
|
|
url = base_url if base_url is not None else ''
|
|
|
|
url += '/%s' % self.url_key
|
|
|
|
entity_id = kwargs.get('id')
|
|
if entity_id is not None:
|
|
url += '/%s' % entity_id
|
|
|
|
elif method == 'get':
|
|
first = True
|
|
for key, value in six.iteritems(kwargs):
|
|
if first:
|
|
url += '?'
|
|
first = False
|
|
else:
|
|
url += '&'
|
|
url += '%s=%s' % (key, value)
|
|
|
|
return url
|
|
|
|
def get(self, id):
|
|
"""Get a resource by id.
|
|
|
|
Get method is accepting id as a positional argument for simplicity.
|
|
|
|
:param id: The id of resource.
|
|
:return: The resource object.
|
|
"""
|
|
|
|
query_kwargs = {"id": id}
|
|
return self._get(self.build_url(**query_kwargs), self.key)
|
|
|
|
def get_all(self, **kwargs):
|
|
"""Get resources by properties other than ID."""
|
|
kwargs = self._filter_kwargs(kwargs)
|
|
return self._get_all(self.build_url(method='get', **kwargs), self.key)
|
|
|
|
def _get_all(self, url, response_key=None):
|
|
"""Get collection of stuff.
|
|
|
|
Put here because we can't modify base.py in the OpenStack
|
|
APIclient (probably)
|
|
|
|
:param url: a partial URL, e.g., '/servers'
|
|
:param response_key: the key to be looked up in response dictionary,
|
|
e.g., 'server'. If response_key is None - all response body
|
|
will be used.
|
|
"""
|
|
|
|
body = self.client.get(url).json()
|
|
data = body[response_key] if response_key is not None else body
|
|
return [self.resource_class(self, item, loaded=True) for item in data]
|
|
|
|
def create(self, **kwargs):
|
|
"""Create a resource.
|
|
|
|
The default implementation is overridden so that the dictionary is
|
|
passed 'as is' without any wrapping.
|
|
"""
|
|
|
|
kwargs = self._filter_kwargs(kwargs)
|
|
return self._post(self.build_url(**kwargs), kwargs)
|
|
|
|
def update(self, **kwargs):
|
|
"""Update a resource.
|
|
|
|
The default implementation is overridden so that the dictionary is
|
|
passed 'as is' without any wrapping. The id field is removed from the
|
|
request body.
|
|
"""
|
|
|
|
kwargs = self._filter_kwargs(kwargs)
|
|
params = kwargs.copy()
|
|
params.pop('id')
|
|
|
|
return self._put(self.build_url(**kwargs), params)
|
|
|
|
|
|
class BaseNestedManager(BaseManager):
|
|
|
|
def __init__(self, client, parent_id):
|
|
super(BaseNestedManager, self).__init__(client)
|
|
|
|
self.parent_id = parent_id
|
|
|
|
def build_url(self, base_url=None, **kwargs):
|
|
# Overriding to use "url_key" instead of the "collection_key".
|
|
# "key_id" is replaced with just "id" when querying a specific object.
|
|
url = base_url if base_url is not None else ''
|
|
|
|
url += '/%s/%s/%s' % (self.parent_url_key, self.parent_id,
|
|
self.url_key)
|
|
|
|
entity_id = kwargs.get('id')
|
|
if entity_id is not None:
|
|
url += '/%s' % entity_id
|
|
|
|
return url
|
|
|
|
|
|
class BaseObject(base.Resource):
|
|
|
|
id = None
|
|
created_at = None
|
|
updated_at = None
|
|
|
|
def __init__(self, manager, info, loaded=False, parent_id=None):
|
|
super(BaseObject, self).__init__(manager, info, loaded)
|
|
|
|
self._parent_id = parent_id
|
|
self._init_nested_managers()
|
|
|
|
def _add_details(self, info):
|
|
for field, value in six.iteritems(info):
|
|
|
|
# Skip the fields which are not declared in the object
|
|
if not hasattr(self, field):
|
|
continue
|
|
|
|
setattr(self, field, value)
|
|
|
|
def _init_nested_managers(self):
|
|
# If an object has nested resource managers, they will be initialized
|
|
# here.
|
|
|
|
manager_instances = {}
|
|
|
|
for manager_name, manager_class in self._managers():
|
|
manager_instance = manager_class(client=self.manager.client,
|
|
parent_id=self.id)
|
|
# Saving a manager to a dict as self.__dict__ should not be
|
|
# changed while iterating
|
|
manager_instances[manager_name] = manager_instance
|
|
|
|
for name, manager_instance in six.iteritems(manager_instances):
|
|
# replacing managers declarations with real managers
|
|
setattr(self, name, manager_instance)
|
|
|
|
def _managers(self):
|
|
# Iterator over nested managers
|
|
|
|
for attr in dir(self):
|
|
# Skip private fields
|
|
if attr.startswith("_"):
|
|
continue
|
|
val = getattr(self, attr)
|
|
if inspect.isclass(val) and issubclass(val, BaseNestedManager):
|
|
yield attr, val
|