Support distil V2 API

Change-Id: Id984fe7f0ee8f1f435d1870cbc6a49452e77876c
This commit is contained in:
Fei Long Wang 2017-02-02 16:02:52 +13:00
parent 8872e6c4d2
commit 1466c71d0e
35 changed files with 4307 additions and 210 deletions

View File

@ -0,0 +1,32 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2015 Chuck Fouts
# 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.
__all__ = ['__version__']
import pbr.version
version_info = pbr.version.VersionInfo('python-distilclient')
try:
__version__ = version_info.version_string()
except AttributeError:
__version__ = None
API_MAX_VERSION = '1'
API_MIN_VERSION = '2'
API_DEPRECATED_VERSION = []

225
distilclient/base.py Normal file
View File

@ -0,0 +1,225 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# 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.
"""
Base utilities to build API operation managers and objects on top of.
"""
import contextlib
import hashlib
import os
from six.moves.urllib import parse
from distilclient.common import cliutils
from distilclient import exceptions
from distilclient import utils
# Python 2.4 compat
try:
all
except NameError:
def all(iterable):
return True not in (not x for x in iterable)
class Manager(utils.HookableMixin):
"""Manager for CRUD operations.
Managers interact with a particular type of API (shares, snapshots,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
self.client = api.client
@property
def api_version(self):
return self.api.api_version
def _list(self, url, response_key, obj_class=None, body=None):
resp = None
if body:
resp, body = self.api.client.post(url, body=body)
else:
resp, body = self.api.client.get(url)
if obj_class is None:
if self.resource_class:
obj_class = self.resource_class
else:
return body
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
if isinstance(data, dict):
try:
data = data['values']
except KeyError:
pass
with self.completion_cache('human_id', obj_class, mode="w"):
with self.completion_cache('uuid', obj_class, mode="w"):
return [obj_class(self, res, loaded=True)
for res in data if res]
@contextlib.contextmanager
def completion_cache(self, cache_type, obj_class, mode):
"""Bash autocompletion items storage.
The completion cache store items that can be used for bash
autocompletion, like UUIDs or human-friendly IDs.
A resource listing will clear and repopulate the cache.
A resource create will append to the cache.
Delete is not handled because listings are assumed to be performed
often enough to keep the cache reasonably up-to-date.
"""
base_dir = cliutils.env('distilclient_UUID_CACHE_DIR',
'DISTILCLIENT_UUID_CACHE_DIR',
default="~/.distilclient")
# NOTE(sirp): Keep separate UUID caches for each username + endpoint
# pair
username = cliutils.env('OS_USERNAME', 'DISTIL_USERNAME')
url = cliutils.env('OS_URL', 'DISTIL_URL')
uniqifier = hashlib.md5(username.encode('utf-8') +
url.encode('utf-8')).hexdigest()
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
try:
os.makedirs(cache_dir, 0o755)
except OSError:
# NOTE(kiall): This is typically either permission denied while
# attempting to create the directory, or the directory
# already exists. Either way, don't fail.
pass
resource = obj_class.__name__.lower()
filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
path = os.path.join(cache_dir, filename)
cache_attr = "_%s_cache" % cache_type
try:
setattr(self, cache_attr, open(path, mode))
except IOError:
# NOTE(kiall): This is typically a permission denied while
# attempting to write the cache file.
pass
try:
yield
finally:
cache = getattr(self, cache_attr, None)
if cache:
cache.close()
delattr(self, cache_attr)
def write_to_completion_cache(self, cache_type, val):
cache = getattr(self, "_%s_cache" % cache_type, None)
if cache:
cache.write("%s\n" % val)
def _get(self, url, response_key=None):
resp, body = self.api.client.get(url)
if response_key:
return self.resource_class(self, body[response_key], loaded=True)
else:
return self.resource_class(self, body, loaded=True)
def _get_with_base_url(self, url, response_key=None):
resp, body = self.api.client.get_with_base_url(url)
if response_key:
return [self.resource_class(self, res, loaded=True)
for res in body[response_key] if res]
else:
return self.resource_class(self, body, loaded=True)
def _create(self, url, body, response_key, return_raw=False, **kwargs):
self.run_hooks('modify_body_for_create', body, **kwargs)
resp, body = self.api.client.post(url, body=body)
if return_raw:
return body[response_key]
with self.completion_cache('human_id', self.resource_class, mode="a"):
with self.completion_cache('uuid', self.resource_class, mode="a"):
return self.resource_class(self, body[response_key])
def _delete(self, url):
resp, body = self.api.client.delete(url)
def _update(self, url, body, response_key=None, **kwargs):
self.run_hooks('modify_body_for_update', body, **kwargs)
resp, body = self.api.client.put(url, body=body)
if body:
if response_key:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _build_query_string(self, search_opts):
q_string = parse.urlencode(
sorted([(k, v) for (k, v) in search_opts.items() if v]))
return "?%s" % q_string if q_string else q_string
class ManagerWithFind(Manager):
"""Like a `Manager`, but with additional `find()`/`findall()` methods."""
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = list(kwargs.items())
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
def list(self):
raise NotImplementedError

View File

@ -1,200 +1,73 @@
# Copyright (C) 2014 Catalyst IT Ltd
# Copyright (c) 2017 Catalyst IT Ltd.
#
# 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
# 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
# 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.
# 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.
"""
A `Client` is a high-level abstraction on top of Distil features. It
exposes the server features with an object-oriented interface, which
encourages dot notation and automatic, but lazy, resources
allocation. A `Client` allows you to control everything.
import requests
from requests.exceptions import ConnectionError
from urlparse import urljoin
To create a `Client` instance, you supply an url pointing to the
server and a version number::
from keystoneauth1 import adapter
from keystoneauth1.identity import generic
from keystoneauth1 import session
from distilclient import client
dc = client.Client(\'http://distil.example.com:9999/\', version='2')
which will load the appropriate client based on the specified
version. Optionally, you can also supply a config dictionary::
from distilclient import client
dc = client.Client(\'http://distil.example.com:9999/\',
version='2', session=session)
The arguments passed to this function will be passed to the client
instances as well.
It's recommended to use `Client` instances instead of accessing the
lower level API as it has been designed to ease the interaction with
the server and it gives enough control for the most common cases.
A simple example for accessing an existing queue through a client
instance - based on the API v2 - would look like::
from distilclient import client
dc = client.Client(\'http://distil.example.com:9999/\', version='2')
usages = dc.usages.list()
Through the queue instance will be then possible to access all the
features associated with the queue itself like posting messages,
getting message and deleting messages.
`Client` uses the lower-level API to access the server, which means
anything you can do with this client instance can be done by accessing
the underlying API, although not recommended.
"""
from distilclient import exceptions
from distilclient.v1 import client as cv1
from distilclient.v2 import client as cv2
_CLIENTS = {'1.0': cv1.Client,
'1': cv1.Client,
'2.0': cv2.Client,
'2': cv2.Client}
def Client(session=None, endpoint=None, username=None, password=None,
include_pass=None, endpoint_type=None,
auth_url=None, **kwargs):
if session:
kwargs['endpoint_override'] = endpoint
return SessionClient(session, **kwargs)
else:
return HTTPClient(**kwargs)
class SessionClient(object):
"""HTTP client based on Keystone client session."""
def __init__(self, session, service_type='rating',
interface='publicURL', **kwargs):
self.client = adapter.LegacyJsonAdapter(
session, service_type=service_type, interface=interface, **kwargs)
def collect_usage(self):
headers = {"Content-Type": "application/json"}
response, json = self.client.request(
"collect_usage", 'POST', headers=headers)
if response.status_code != 200:
raise AttributeError("Usage cycle failed: %s code: %s" %
(response.text, response.status_code))
else:
return json
def last_collected(self):
headers = {"Content-Type": "application/json"}
response, json = self.client.request(
"last_collected", 'GET', headers=headers)
if response.status_code != 200:
raise AttributeError("Get last collected failed: %s code: %s" %
(response.text, response.status_code))
else:
return json
def get_usage(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_usage")
def get_rated(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_rated")
def _query_usage(self, tenant, start, end, url):
params = {"tenant": tenant,
"start": start,
"end": end
}
response, json = self.client.request(
url, 'GET', params=params)
if response.status_code != 200:
raise AttributeError("Get usage failed: %s code: %s" %
(response.text, response.status_code))
else:
return json
class HTTPClient(object):
def __init__(self, distil_url=None, os_auth_token=None,
os_username=None, os_password=None,
os_project_id=None, os_project_name=None,
os_tenant_id=None, os_tenant_name=None,
os_project_domain_id='default',
os_project_domain_name='Default',
os_auth_url=None, os_region_name=None,
os_cacert=None, insecure=False,
os_service_type='rating', os_endpoint_type='publicURL'):
project_id = os_project_id or os_tenant_id
project_name = os_project_name or os_tenant_name
self.insecure = insecure
if os_auth_token and distil_url:
self.auth_token = os_auth_token
self.endpoint = distil_url
else:
if insecure:
verify = False
else:
verify = os_cacert or True
kwargs = {
'username': os_username,
'password': os_password,
'auth_url': os_auth_url,
'project_id': project_id,
'project_name': project_name,
'project_domain_id': os_project_domain_id,
'project_domain_name': os_project_domain_name,
}
auth = generic.Password(**kwargs)
sess = session.Session(auth=auth, verify=verify)
if os_auth_token:
self.auth_token = os_auth_token
else:
self.auth_token = auth.get_token(sess)
if distil_url:
self.endpoint = distil_url
else:
self.endpoint = auth.get_endpoint(
sess, service_type=os_service_type,
interface=os_endpoint_type,
region_name=os_region_name)
def collect_usage(self):
url = urljoin(self.endpoint, "collect_usage")
headers = {"Content-Type": "application/json",
"X-Auth-Token": self.auth_token}
try:
response = requests.post(url, headers=headers,
verify=not self.insecure)
if response.status_code != 200:
raise AttributeError("Usage cycle failed: %s code: %s" %
(response.text, response.status_code))
else:
return response.json()
except ConnectionError as e:
print e
def last_collected(self):
url = urljoin(self.endpoint, "last_collected")
headers = {"Content-Type": "application/json",
"X-Auth-Token": self.auth_token}
try:
response = requests.get(url, headers=headers,
verify=not self.insecure)
if response.status_code != 200:
raise AttributeError("Get last collected failed: %s code: %s" %
(response.text, response.status_code))
else:
return response.json()
except ConnectionError as e:
print e
def get_usage(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_usage")
def get_rated(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_rated")
def _query_usage(self, tenant, start, end, endpoint):
url = urljoin(self.endpoint, endpoint)
headers = {"X-Auth-Token": self.auth_token}
params = {"tenant": tenant,
"start": start,
"end": end
}
try:
response = requests.get(url, headers=headers,
params=params,
verify=not self.insecure)
if response.status_code != 200:
raise AttributeError("Get usage failed: %s code: %s" %
(response.text, response.status_code))
else:
return response.json()
except ConnectionError as e:
print e
def Client(version='1', *args, **kwargs):
try:
return _CLIENTS[version](*args, **kwargs)
except KeyError:
raise exceptions.VersionNotFoundForAPIMethod(version)

View File

View File

@ -0,0 +1,218 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# 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.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import abc
import argparse
import os
import six
from stevedore import extension
from distilclient.common.apiclient import exceptions
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
global _discovered_plugins
_discovered_plugins = {}
def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin
ep_namespace = "distilclient.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in six.iteritems(_discovered_plugins):
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)
def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)
def load_plugin_from_args(args):
"""Load required plugin and populate it with options.
Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.
:type args: argparse.Namespace
:raises: AuthPluginOptionsMissing
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]
def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)
@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.
:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin."""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins."""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.
:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))
def authenticate(self, http_client):
"""Authenticate using plugin defined method.
The method usually analyses `self.opts` and performs
a request to authentication server.
:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication."""
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
missing = [opt
for opt in self.opt_names
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)
@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.
:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

View File

@ -0,0 +1,518 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# 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.
"""
Base utilities to build API operation managers and objects on top of.
"""
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
import six
from six.moves.urllib import parse
from distilclient.i18n import _
from distilclient.common.apiclient import exceptions
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param args: args to be passed to every hook function
:param kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key=None, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key] if response_key is not None else body
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key=None):
"""Get an object from collection.
: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, data, loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key=None, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
: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.
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
data = body[response_key] if response_key is not None else body
if return_raw:
return data
return self.resource_class(self, data)
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in six.iteritems(kwargs.copy()):
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(404, msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion."""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
return strutils.to_slug(name)
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def __ne__(self, other):
return not self == other
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

@ -0,0 +1,367 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# 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.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import logging
import time
try:
import simplejson as json
except ImportError:
import json
from oslo_utils import importutils
import requests
from distilclient.i18n import _
from distilclient.common.apiclient import exceptions
_logger = logging.getLogger(__name__)
class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "distilclient.common.apiclient"
def __init__(self,
auth_plugin,
region_name=None,
endpoint_type="publicURL",
original_ip=None,
verify=True,
cert=None,
timeout=None,
timings=False,
keyring_saver=None,
debug=False,
user_agent=None,
http=None):
self.auth_plugin = auth_plugin
self.endpoint_type = endpoint_type
self.region_name = region_name
self.original_ip = original_ip
self.timeout = timeout
self.verify = verify
self.cert = cert
self.keyring_saver = keyring_saver
self.debug = debug
self.user_agent = user_agent or self.user_agent
self.times = [] # [("item", starttime, endtime), ...]
self.timings = timings
# requests within the same session can reuse TCP connections from pool
self.http = http or requests.Session()
self.cached_token = None
def _http_log_req(self, method, url, kwargs):
if not self.debug:
return
string_parts = [
"curl -i",
"-X '%s'" % method,
"'%s'" % url,
]
for element in kwargs['headers']:
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
string_parts.append(header)
_logger.debug("REQ: %s" % " ".join(string_parts))
if 'data' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
def _http_log_resp(self, resp):
if not self.debug:
return
_logger.debug(
"RESP: [%s] %s\n",
resp.status_code,
resp.headers)
if resp._content_consumed:
_logger.debug(
"RESP BODY: %s\n",
resp.text)
def serialize(self, kwargs):
if kwargs.get('json') is not None:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['json'])
try:
del kwargs['json']
except KeyError:
pass
def get_timings(self):
return self.times
def reset_timings(self):
self.times = []
def request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", {})
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
self.original_ip, self.user_agent)
if self.timeout is not None:
kwargs.setdefault("timeout", self.timeout)
kwargs.setdefault("verify", self.verify)
if self.cert is not None:
kwargs.setdefault("cert", self.cert)
self.serialize(kwargs)
self._http_log_req(method, url, kwargs)
if self.timings:
start_time = time.time()
resp = self.http.request(method, url, **kwargs)
if self.timings:
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
self._http_log_resp(resp)
if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
resp.status_code)
raise exceptions.from_response(resp, method, url)
return resp
@staticmethod
def concat_url(endpoint, url):
"""Concatenate endpoint and final URL.
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
"http://keystone/v2.0/tokens".
:param endpoint: the base URL
:param url: the final URL
"""
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
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`
"""
filter_args = {
"endpoint_type": client.endpoint_type or self.endpoint_type,
"service_type": client.service_type,
}
token, endpoint = (self.cached_token, client.cached_endpoint)
just_authenticated = False
if not (token and endpoint):
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
pass
if not (token and endpoint):
self.authenticate()
just_authenticated = True
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
if not (token and endpoint):
raise exceptions.AuthorizationFailure(
_("Cannot find endpoint or token for request"))
old_token_endpoint = (token, endpoint)
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
self.cached_token = token
client.cached_endpoint = endpoint
# Perform the request once. If we get Unauthorized, then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
except exceptions.Unauthorized as unauth_ex:
if just_authenticated:
raise
self.cached_token = None
client.cached_endpoint = None
if self.auth_plugin.opts.get('token'):
self.auth_plugin.opts['token'] = None
if self.auth_plugin.opts.get('endpoint'):
self.auth_plugin.opts['endpoint'] = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
raise unauth_ex
if (not (token and endpoint) or
old_token_endpoint == (token, endpoint)):
raise unauth_ex
self.cached_token = token
client.cached_endpoint = endpoint
kwargs["headers"]["X-Auth-Token"] = token
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
def add_client(self, base_client_instance):
"""Add a new instance of :class:`BaseClient` descendant.
`self` will store a reference to `base_client_instance`.
Example:
>>> def test_clients():
... from keystoneclient.auth import keystone
... from distilclient.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
... openstack_client = client.HTTPClient(auth)
... # create nova client
... from novaclient.v1_1 import client
... client.Client(openstack_client)
... # create keystone client
... from keystoneclient.v2_0 import client
... client.Client(openstack_client)
... # use them
... openstack_client.identity.tenants.list()
... openstack_client.compute.servers.list()
"""
service_type = base_client_instance.service_type
if service_type and not hasattr(self, service_type):
setattr(self, service_type, base_client_instance)
def authenticate(self):
self.auth_plugin.authenticate(self)
# Store the authentication results in the keyring for later requests
if self.keyring_saver:
self.keyring_saver.save(self)
class BaseClient(object):
"""Top-level object to access the OpenStack API.
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
will handle a bunch of issues such as authentication.
"""
service_type = None
endpoint_type = None # "publicURL" will be used
cached_endpoint = None
def __init__(self, http_client, extensions=None):
self.http_client = http_client
http_client.add_client(self)
# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))
def client_request(self, method, url, **kwargs):
return self.http_client.client_request(
self, method, url, **kwargs)
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.client_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
@staticmethod
def get_class(api_name, version, version_map):
"""Returns the client class for the requested API version
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = _("Invalid %(api_name)s client version '%(version)s'. "
"Must be one of: %(version_map)s") % {
'api_name': api_name,
'version': version,
'version_map': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)

View File

@ -0,0 +1,477 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation
# 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.
"""
Exception definitions.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
import inspect
import sys
import six
from distilclient.i18n import _
class ClientException(Exception):
"""The base exception class for all exceptions this library raises."""
pass
class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
class UnsupportedVersion(ClientException):
"""User is trying to use an unsupported version of the API."""
pass
class CommandError(ClientException):
"""Error in CLI tool."""
pass
class AuthorizationFailure(ClientException):
"""Cannot authorize API client."""
pass
class ConnectionError(ClientException):
"""Cannot connect to API service."""
pass
class ConnectionRefused(ConnectionError):
"""Connection refused while trying to connect to API service."""
pass
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
super(AuthPluginOptionsMissing, self).__init__(
_("Authentication failed. Missing options: %s") %
", ".join(opt_names))
self.opt_names = opt_names
class AuthSystemNotFound(AuthorizationFailure):
"""User has specified an AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
_("AuthSystemNotFound: %r") % auth_system)
self.auth_system = auth_system
class NoUniqueMatch(ClientException):
"""Multiple entities found instead of one."""
pass
class EndpointException(ClientException):
"""Something is rotten in Service Catalog."""
pass
class EndpointNotFound(EndpointException):
"""Could not find requested endpoint in Service Catalog."""
pass
class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
_("AmbiguousEndpoints: %r") % endpoints)
self.endpoints = endpoints
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions."""
http_status = 0
message = _("HTTP Error")
def __init__(self, message=None, details=None,
response=None, request_id=None,
url=None, method=None, http_status=None):
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
self.request_id = request_id
self.response = response
self.url = url
self.method = method
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
if request_id:
formatted_string += " (Request-ID: %s)" % request_id
super(HttpError, self).__init__(formatted_string)
class HTTPRedirection(HttpError):
"""HTTP Redirection."""
message = _("HTTP Redirection")
class HTTPClientError(HttpError):
"""Client-side HTTP error.
Exception for cases in which the client seems to have erred.
"""
message = _("HTTP Client Error")
class HttpServerError(HttpError):
"""Server-side HTTP error.
Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
message = _("HTTP Server Error")
class MultipleChoices(HTTPRedirection):
"""HTTP 300 - Multiple Choices.
Indicates multiple options for the resource that the client may follow.
"""
http_status = 300
message = _("Multiple Choices")
class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.
The request cannot be fulfilled due to bad syntax.
"""
http_status = 400
message = _("Bad Request")
class Unauthorized(HTTPClientError):
"""HTTP 401 - Unauthorized.
Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
http_status = 401
message = _("Unauthorized")
class PaymentRequired(HTTPClientError):
"""HTTP 402 - Payment Required.
Reserved for future use.
"""
http_status = 402
message = _("Payment Required")
class Forbidden(HTTPClientError):
"""HTTP 403 - Forbidden.
The request was a valid request, but the server is refusing to respond
to it.
"""
http_status = 403
message = _("Forbidden")
class NotFound(HTTPClientError):
"""HTTP 404 - Not Found.
The requested resource could not be found but may be available again
in the future.
"""
http_status = 404
message = _("Not Found")
class MethodNotAllowed(HTTPClientError):
"""HTTP 405 - Method Not Allowed.
A request was made of a resource using a request method not supported
by that resource.
"""
http_status = 405
message = _("Method Not Allowed")
class NotAcceptable(HTTPClientError):
"""HTTP 406 - Not Acceptable.
The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
http_status = 406
message = _("Not Acceptable")
class ProxyAuthenticationRequired(HTTPClientError):
"""HTTP 407 - Proxy Authentication Required.
The client must first authenticate itself with the proxy.
"""
http_status = 407
message = _("Proxy Authentication Required")
class RequestTimeout(HTTPClientError):
"""HTTP 408 - Request Timeout.
The server timed out waiting for the request.
"""
http_status = 408
message = _("Request Timeout")
class Conflict(HTTPClientError):
"""HTTP 409 - Conflict.
Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
http_status = 409
message = _("Conflict")
class Gone(HTTPClientError):
"""HTTP 410 - Gone.
Indicates that the resource requested is no longer available and will
not be available again.
"""
http_status = 410
message = _("Gone")
class LengthRequired(HTTPClientError):
"""HTTP 411 - Length Required.
The request did not specify the length of its content, which is
required by the requested resource.
"""
http_status = 411
message = _("Length Required")
class PreconditionFailed(HTTPClientError):
"""HTTP 412 - Precondition Failed.
The server does not meet one of the preconditions that the requester
put on the request.
"""
http_status = 412
message = _("Precondition Failed")
class RequestEntityTooLarge(HTTPClientError):
"""HTTP 413 - Request Entity Too Large.
The request is larger than the server is willing or able to process.
"""
http_status = 413
message = _("Request Entity Too Large")
def __init__(self, *args, **kwargs):
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
self.retry_after = 0
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
class RequestUriTooLong(HTTPClientError):
"""HTTP 414 - Request-URI Too Long.
The URI provided was too long for the server to process.
"""
http_status = 414
message = _("Request-URI Too Long")
class UnsupportedMediaType(HTTPClientError):
"""HTTP 415 - Unsupported Media Type.
The request entity has a media type which the server or resource does
not support.
"""
http_status = 415
message = _("Unsupported Media Type")
class RequestedRangeNotSatisfiable(HTTPClientError):
"""HTTP 416 - Requested Range Not Satisfiable.
The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
http_status = 416
message = _("Requested Range Not Satisfiable")
class ExpectationFailed(HTTPClientError):
"""HTTP 417 - Expectation Failed.
The server cannot meet the requirements of the Expect request-header field.
"""
http_status = 417
message = _("Expectation Failed")
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = _("Unprocessable Entity")
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = _("Internal Server Error")
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = _("Not Implemented")
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = _("Bad Gateway")
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = _("Service Unavailable")
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = _("Gateway Timeout")
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = _("HTTP Version Not Supported")
# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in six.iteritems(vars(sys.modules[__name__]))
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
req_id = response.headers.get("x-openstack-request-id")
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": req_id,
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if isinstance(body, dict):
error = body.get(list(body)[0])
if isinstance(error, dict):
kwargs["message"] = (error.get("message") or
error.get("faultstring"))
kwargs["details"] = (error.get("details") or
six.text_type(body))
elif content_type.startswith("text/"):
kwargs["details"] = response.text
try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

View File

@ -0,0 +1,174 @@
# Copyright 2013 OpenStack Foundation
# 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.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import requests
import six
from six.moves.urllib import parse
from distilclient.common.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
required = required or []
optional = optional or []
for k in required:
try:
assert k in dct
except AssertionError:
extra_keys = set(dct.keys()).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization."""
def __init__(self, data):
super(TestResponse, self).__init__()
self._content_consumed = True
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
# Fake the text attribute to streamline Response creation
text = data.get('text', "")
if isinstance(text, (dict, list)):
self._content = json.dumps(text)
default_headers = {
"Content-Type": "application/json",
}
else:
self._content = text
default_headers = {}
if six.PY3 and isinstance(self._content, six.string_types):
self._content = self._content.encode('utf-8', 'strict')
self.headers = data.get('headers') or default_headers
else:
self.status_code = data
def __eq__(self, other):
return (self.status_code == other.status_code and
self.headers == other.headers and
self._content == other._content)
class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and "auth_plugin" not in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called."""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None,
clear_callstack=True):
"""Assert than an API method was called anytime in the test."""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
if clear_callstack:
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@ -0,0 +1,83 @@
#
# 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_utils import encodeutils
from oslo_utils import uuidutils
import six
from distilclient.i18n import _
from distilclient.common.apiclient import exceptions
def find_resource(manager, name_or_id, **find_args):
"""Look for resource in a given manager.
Used as a helper for the _find_* methods.
Example:
.. code-block:: python
def _find_hypervisor(cs, hypervisor):
#Get a hypervisor by name or ID.
return cliutils.find_resource(cs.hypervisors, hypervisor)
"""
# first try to get entity as integer id
try:
return manager.get(int(name_or_id))
except (TypeError, ValueError, exceptions.NotFound):
pass
# now try to get entity as uuid
try:
if six.PY2:
tmp_id = encodeutils.safe_encode(name_or_id)
else:
tmp_id = encodeutils.safe_decode(name_or_id)
if uuidutils.is_uuid_like(tmp_id):
return manager.get(tmp_id)
except (TypeError, ValueError, exceptions.NotFound):
pass
# for str id which is not uuid
if getattr(manager, 'is_alphanum_id_allowed', False):
try:
return manager.get(name_or_id)
except exceptions.NotFound:
pass
try:
try:
return manager.find(human_id=name_or_id, **find_args)
except exceptions.NotFound:
pass
# finally try to find entity by name
try:
resource = getattr(manager, 'resource_class', None)
name_attr = resource.NAME_ATTR if resource else 'name'
kwargs = {name_attr: name_or_id}
kwargs.update(find_args)
return manager.find(**kwargs)
except exceptions.NotFound:
msg = _("No %(name)s with a name or "
"ID of '%(name_or_id)s' exists.") % \
{"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id}
raise exceptions.CommandError(msg)
except exceptions.NoUniqueMatch:
msg = _("Multiple %(name)s matches found for "
"'%(name_or_id)s', use an ID to be more specific.") % \
{"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id}
raise exceptions.CommandError(msg)

View File

@ -0,0 +1,271 @@
# Copyright 2012 Red Hat, 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.
# W0603: Using the global statement
# W0621: Redefining name %s from outer scope
# pylint: disable=W0603,W0621
from __future__ import print_function
import getpass
import inspect
import os
import sys
import textwrap
from oslo_utils import encodeutils
from oslo_utils import strutils
import prettytable
import six
from six import moves
from distilclient.i18n import _
class MissingArgs(Exception):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = _("Missing arguments: %s") % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
def validate_args(fn, *args, **kwargs):
"""Check that the supplied args are sufficient for calling a function.
>>> validate_args(lambda a: None)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): a
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): b, d
:param fn: the function to check
:param arg: the positional arguments supplied
:param kwargs: the keyword arguments supplied
"""
argspec = inspect.getargspec(fn)
num_defaults = len(argspec.defaults or [])
required_args = argspec.args[:len(argspec.args) - num_defaults]
def isbound(method):
return getattr(method, '__self__', None) is not None
if isbound(fn):
required_args.pop(0)
missing = [arg for arg in required_args if arg not in kwargs]
missing = missing[len(args):]
if missing:
raise MissingArgs(missing)
def arg(*args, **kwargs):
"""Decorator for CLI args.
Example:
>>> @arg("name", help="Name of the new entity")
... def entity_create(args):
... pass
"""
def _decorator(func):
add_arg(func, *args, **kwargs)
return func
return _decorator
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""
if not hasattr(func, 'arguments'):
func.arguments = []
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))
def unauthenticated(func):
"""Adds 'unauthenticated' attribute to decorated function.
Usage:
>>> @unauthenticated
... def mymethod(f):
... pass
"""
func.unauthenticated = True
return func
def isunauthenticated(func):
"""Checks if the function does not require authentication.
Mark such functions with the `@unauthenticated` decorator.
:returns: bool
"""
return getattr(func, 'unauthenticated', False)
def print_list(objs, fields, formatters=None, sortby_index=0,
mixed_case_fields=None, field_labels=None):
"""Print a list or objects as a table, one row per object.
:param objs: iterable of :class:`Resource`
:param fields: attributes that correspond to columns, in order
:param formatters: `dict` of callables for field formatting
:param sortby_index: index of the field for sorting table rows
:param mixed_case_fields: fields corresponding to object attributes that
have mixed case names (e.g., 'serverId')
:param field_labels: Labels to use in the heading of the table, default to
fields.
"""
formatters = formatters or {}
mixed_case_fields = mixed_case_fields or []
field_labels = field_labels or fields
if len(field_labels) != len(fields):
raise ValueError(_("Field labels list %(labels)s has different number "
"of elements than fields list %(fields)s"),
{'labels': field_labels, 'fields': fields})
if sortby_index is None:
kwargs = {}
else:
kwargs = {'sortby': field_labels[sortby_index]}
pt = prettytable.PrettyTable(field_labels)
pt.align = 'l'
for o in objs:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](o))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)
if six.PY3:
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
else:
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
def print_dict(dct, dict_property="Property", wrap=0):
"""Print a `dict` as a table of two columns.
:param dct: `dict` to print
:param dict_property: name of the first column
:param wrap: wrapping for the second column
"""
pt = prettytable.PrettyTable([dict_property, 'Value'])
pt.align = 'l'
for k, v in six.iteritems(dct):
# convert dict to str to check length
if isinstance(v, dict):
v = six.text_type(v)
if wrap > 0:
v = textwrap.fill(six.text_type(v), wrap)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
pt.add_row([col1, line])
col1 = ''
else:
pt.add_row([k, v])
if six.PY3:
print(encodeutils.safe_encode(pt.get_string()).decode())
else:
print(encodeutils.safe_encode(pt.get_string()))
def get_password(max_password_prompts=3):
"""Read password from TTY."""
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
pw = None
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
# Check for Ctrl-D
try:
for __ in moves.range(max_password_prompts):
pw1 = getpass.getpass("OS Password: ")
if verify:
pw2 = getpass.getpass("Please verify: ")
else:
pw2 = pw1
if pw1 == pw2 and pw1:
pw = pw1
break
except EOFError:
pass
return pw
def service_type(stype):
"""Adds 'service_type' attribute to decorated function.
Usage:
.. code-block:: python
@service_type('volume')
def mymethod(f):
...
"""
def inner(f):
f.service_type = stype
return f
return inner
def get_service_type(f):
"""Retrieves service type from function."""
return getattr(f, 'service_type', None)
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def exit(msg=''):
if msg:
print(msg, file=sys.stderr)
sys.exit(1)

View File

@ -0,0 +1,200 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2015 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 copy
import logging
from oslo_serialization import jsonutils
from oslo_utils import strutils
import requests
import six
from distilclient import exceptions
try:
from eventlet import sleep
except ImportError:
from time import sleep # noqa
class HTTPClient(object):
"""HTTP Client class used by multiple clients.
The imported Requests module caches and reuses objects with the same
destination. To avoid the problem of sending duplicate requests it is
necessary that the Requests module is only imported once during client
execution. This class is shared by multiple client versions so that the
client can be changed to another version during execution.
"""
def __init__(self, endpoint_url, token, user_agent, api_version,
insecure=False, cacert=None, timeout=None, retries=None,
http_log_debug=False):
self.endpoint_url = endpoint_url
self.base_url = self._get_base_url(self.endpoint_url)
self.retries = retries
self.http_log_debug = http_log_debug
self.request_options = self._set_request_options(
insecure, cacert, timeout)
self.default_headers = {
'X-Auth-Token': token,
'User-Agent': user_agent,
'Accept': 'application/json',
}
self._add_log_handlers(http_log_debug)
def _add_log_handlers(self, http_log_debug):
self._logger = logging.getLogger(__name__)
# check that handler hasn't already been added
if http_log_debug and not self._logger.handlers:
ch = logging.StreamHandler()
ch._name = 'http_client_handler'
self._logger.setLevel(logging.DEBUG)
self._logger.addHandler(ch)
if hasattr(requests, 'logging'):
rql = requests.logging.getLogger(requests.__name__)
rql.addHandler(ch)
def _get_base_url(self, url):
"""Truncates url and returns transport, address, and port number."""
base_url = '/'.join(url.split('/')[:3]) + '/'
return base_url
def _set_request_options(self, insecure, cacert, timeout=None):
options = {'verify': True}
if insecure:
options['verify'] = False
elif cacert:
options['verify'] = cacert
if timeout:
options['timeout'] = timeout
return options
def request(self, url, method, **kwargs):
headers = copy.deepcopy(self.default_headers)
headers.update(kwargs.get('headers', {}))
options = copy.deepcopy(self.request_options)
if 'body' in kwargs:
headers['Content-Type'] = 'application/json'
options['data'] = jsonutils.dumps(kwargs['body'])
self.log_request(method, url, headers, options.get('data', None))
resp = requests.request(method, url, headers=headers, **options)
self.log_response(resp)
body = None
if resp.text:
try:
body = jsonutils.loads(resp.text)
except ValueError:
pass
if resp.status_code >= 400:
raise exceptions.from_response(resp, method, url)
return resp, body
def _cs_request(self, url, method, **kwargs):
return self._cs_request_with_retries(
self.endpoint_url + url,
method,
**kwargs)
def _cs_request_base_url(self, url, method, **kwargs):
return self._cs_request_with_retries(
self.base_url + url,
method,
**kwargs)
def _cs_request_with_retries(self, url, method, **kwargs):
attempts = 0
timeout = 1
while True:
attempts += 1
try:
resp, body = self.request(url, method, **kwargs)
return resp, body
except (exceptions.BadRequest,
requests.exceptions.RequestException,
exceptions.ClientException) as e:
if attempts > self.retries:
raise
self._logger.debug("Request error: %s" % six.text_type(e))
self._logger.debug(
"Failed attempt(%(current)s of %(total)s), "
" retrying in %(sec)s seconds" % {
'current': attempts,
'total': self.retries,
'sec': timeout
})
sleep(timeout)
timeout *= 2
def get_with_base_url(self, url, **kwargs):
return self._cs_request_base_url(url, 'GET', **kwargs)
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self._cs_request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
def log_request(self, method, url, headers, data=None):
if not self.http_log_debug:
return
string_parts = ['curl -i', ' -X %s' % method, ' %s' % url]
for element in headers:
header = ' -H "%s: %s"' % (element, headers[element])
string_parts.append(header)
if data:
if "password" in data:
data = strutils.mask_password(data)
string_parts.append(" -d '%s'" % data)
self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
def log_response(self, resp):
if not self.http_log_debug:
return
self._logger.debug(
"RESP: [%(code)s] %(headers)s\nRESP BODY: %(body)s\n" % {
'code': resp.status_code,
'headers': resp.headers,
'body': resp.text
})

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# 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 json
from distilclient import exc
from distilclient.i18n import _
def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None):
"""Generate common filters for any list request.
:param marker: entity ID from which to start returning entities.
:param limit: maximum number of entities to return.
:param sort_key: field to use for sorting.
:param sort_dir: direction of sorting: 'asc' or 'desc'.
:returns: list of string filters.
"""
filters = []
if isinstance(limit, int):
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
if sort_key is not None:
filters.append('sort_key=%s' % sort_key)
if sort_dir is not None:
filters.append('sort_dir=%s' % sort_dir)
return filters
def split_and_deserialize(string):
"""Split and try to JSON deserialize a string.
Gets a string with the KEY=VALUE format, split it (using '=' as the
separator) and try to JSON deserialize the VALUE.
:returns: A tuple of (key, value).
"""
try:
key, value = string.split("=", 1)
except ValueError:
raise exc.CommandError(_('Attributes must be a list of '
'PATH=VALUE not "%s"') % string)
try:
value = json.loads(value)
except ValueError:
pass
return (key, value)
def args_array_to_patch(attributes):
patch = []
for attr in attributes:
path, value = split_and_deserialize(attr)
patch.append({path: value})
return patch
def format_args(args, parse_comma=True):
'''Reformat a list of key-value arguments into a dict.
Convert arguments into format expected by the API.
'''
if not args:
return {}
if parse_comma:
# expect multiple invocations of --label (or other arguments) but fall
# back to either , or ; delimited if only one --label is specified
if len(args) == 1:
args = args[0].replace(';', ',').split(',')
fmt_args = {}
for arg in args:
try:
(k, v) = arg.split(('='), 1)
except ValueError:
raise exc.CommandError(_('arguments must be a list of KEY=VALUE '
'not %s') % arg)
if k not in fmt_args:
fmt_args[k] = v
else:
if not isinstance(fmt_args[k], list):
fmt_args[k] = [fmt_args[k]]
fmt_args[k].append(v)
return fmt_args
def print_list_field(field):
return lambda obj: ', '.join(getattr(obj, field))

View File

@ -12,6 +12,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import debtcollector
@debtcollector.removals.removed_class("CommandError")
class CommandError(BaseException):
"""Invalid usage of CLI."""

View File

@ -0,0 +1,30 @@
# Copyright 2010 Jacob Kaplan-Moss
# 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.
"""
Exception definitions.
"""
from distilclient.common.apiclient.exceptions import * # noqa
class VersionNotFoundForAPIMethod(Exception):
msg_fmt = "API version '%(vers)s' is not supported."
def __init__(self, version):
self.version = version
def __str__(self):
return self.msg_fmt % {"vers": self.version}

35
distilclient/i18n.py Normal file
View File

@ -0,0 +1,35 @@
# 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.
"""oslo_i18n integration module for distilclient.
See http://docs.openstack.org/developer/oslo.i18n/usage.html .
"""
import oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='distilclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical

View File

@ -0,0 +1,94 @@
# 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.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
from __future__ import print_function
def assert_has_keys(dictonary, required=None, optional=None):
if required is None:
required = []
if optional is None:
optional = []
for k in required:
try:
assert k in dictonary
except AssertionError:
extra_keys = set(dictonary).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class FakeClient(object):
def assert_called(self, method, url, body=None, pos=-1, **kwargs):
"""Assert than an API method was just called."""
expected = (method, url)
called = self.client.callstack[pos][0:2]
assert self.client.callstack, ("Expected %s %s but no calls "
"were made." % expected)
assert expected == called, 'Expected %s %s; got %s %s' % (
expected + called)
if body is not None:
actual = self.client.callstack[pos][2]
if isinstance(actual, dict) and isinstance(body, dict):
assert sorted(list(actual)) == sorted(list(body))
else:
assert actual == body, "Expected %(b)s; got %(a)s" % {
'b': body,
'a': actual
}
def assert_called_anytime(self, method, url, body=None,
clear_callstack=True):
"""Assert than an API method was called anytime in the test."""
expected = (method, url)
assert self.client.callstack, ("Expected %s %s but no calls "
"were made." % expected)
found = False
for entry in self.client.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % (
expected, self.client.callstack)
if body is not None:
try:
assert entry[2] == body
except AssertionError:
print(entry[2])
print("!=")
print(body)
raise
if clear_callstack:
self.client.callstack = []
def clear_callstack(self):
self.client.callstack = []
def authenticate(self):
pass

View File

@ -0,0 +1,44 @@
# 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 distilclient.common.apiclient import base as common_base
from distilclient.tests.unit import utils
from distilclient.tests.unit.v2 import fakes
cs = fakes.FakeClient()
class BaseTest(utils.TestCase):
def test_resource_repr(self):
r = common_base.Resource(None, dict(foo="bar", baz="spam"))
self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
def test_eq(self):
# Two resources of the same type with the same id: equal
# The truth of r1==r2 does not imply that r1!=r2 is false in PY2.
# Test that inequality operator is defined and that comparing equal
# items returns False.
r1 = common_base.Resource(None, {'id': 1, 'name': 'hi'})
r2 = common_base.Resource(None, {'id': 1, 'name': 'hello'})
self.assertTrue(r1 == r2)
self.assertFalse(r1 != r2)
# Two resources with no ID: equal if their info is equal
# The truth of r1==r2 does not imply that r1!=r2 is false in PY2.
# Test that inequality operator is defined and that comparing equal
# items returns False.
r1 = common_base.Resource(None, {'name': 'joe', 'age': 12})
r2 = common_base.Resource(None, {'name': 'joe', 'age': 12})
self.assertTrue(r1 == r2)
self.assertFalse(r1 != r2)

View File

@ -1,21 +1,39 @@
# Copyright (C) 2016 Catalyst IT Ltd
# 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
#
# 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
#
# 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.
# 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 testtools
import ddt
import mock
from distilclient import client
from distilclient import exceptions
from distilclient.tests.unit import utils
import distilclient.v1.client
import distilclient.v2.client
class ClientTest(testtools.TestCase):
@ddt.ddt
class ClientTest(utils.TestCase):
def test_fake(self):
self.assertTrue(1 == 1)
@mock.patch.object(distilclient.v1.client, 'Client')
def test_init_client_with_string_v1_version(self, mock_client):
mock_client('1', 'foo', auth_url='quuz')
mock_client.assert_called_once_with('1', 'foo', auth_url='quuz')
@mock.patch.object(distilclient.v2.client, 'Client')
def test_init_client_with_string_v2_version(self, mock_client):
mock_client('2', 'foo', auth_url='quuz')
mock_client.assert_called_once_with('2', 'foo', auth_url='quuz')
@ddt.data(None, '', '3', 'v1', 'v2', 'v1.0', 'v2.0')
def test_init_client_with_unsupported_version(self, v):
self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
client.Client, v)

View File

@ -0,0 +1,78 @@
# 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 os
import fixtures
import mock
import requests
import testtools
class TestCase(testtools.TestCase):
TEST_REQUEST_BASE = {
'verify': True,
}
def setUp(self):
super(TestCase, self).setUp()
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
os.environ.get('OS_STDERR_CAPTURE') == '1'):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
def mock_object(self, obj, attr_name, new_attr=None, **kwargs):
"""Mock an object attribute.
Use python mock to mock an object attribute
Mocks the specified objects attribute with the given value.
Automatically performs 'addCleanup' for the mock.
"""
if not new_attr:
new_attr = mock.Mock()
patcher = mock.patch.object(obj, attr_name, new_attr, **kwargs)
patcher.start()
self.addCleanup(patcher.stop)
return new_attr
class TestResponse(requests.Response):
"""Class used to wrap requests.Response.
Class used to wrap requests.Response and provide some
convenience to initialize with a dict.
"""
def __init__(self, data):
self._text = None
super(TestResponse, self)
if isinstance(data, dict):
self.status_code = data.get('status_code', None)
self.headers = data.get('headers', {})
self.headers['x-openstack-request-id'] = data.get(
'x-openstack-request-id', 'fake-request-id')
# Fake the text attribute to streamline Response creation
self._text = data.get('text', None)
else:
self.status_code = data
self.headers = {'x-openstack-request-id': 'fake-request-id'}
def __eq__(self, other):
return self.__dict__ == other.__dict__
@property
def text(self):
return self._text

View File

View File

@ -0,0 +1,210 @@
# Copyright 2013 OpenStack Foundation
#
# 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 six.moves.urllib import parse
from distilclient.common import httpclient
from distilclient.tests.unit import fakes
from distilclient.tests.unit import utils
from distilclient.v2 import client
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
api_version = kwargs.get('version') or '2'
client.Client.__init__(self, 'username', 'password',
'project_id', 'auth_url',
extensions=kwargs.get('extensions'),
version=api_version)
self.client = FakeHTTPClient(**kwargs)
class FakeHTTPClient(httpclient.HTTPClient):
def __init__(self, **kwargs):
api_version = kwargs.get('version') or '2'
self.username = 'username'
self.password = 'password'
self.auth_url = 'auth_url'
self.callstack = []
self.base_url = 'localhost'
self.default_headers = {
'X-Auth-Token': 'xabc123',
'X-Openstack-Distil-Api-Version': api_version,
'Accept': 'application/json',
}
def _cs_request(self, url, method, **kwargs):
return self._cs_request_with_retries(url, method, **kwargs)
def _cs_request_base_url(self, url, method, **kwargs):
return self._cs_request_with_retries(url, method, **kwargs)
def _cs_request_with_retries(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert 'body' not in kwargs
elif method == 'PUT':
assert 'body' in kwargs
# Call the method
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
status, headers, body = getattr(self, callback)(**kwargs)
r = utils.TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})
return r, body
#
# Quotas
#
def get_os_quota_sets_test(self, **kw):
quota_set = {
'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'shares': 1,
'snapshots': 1,
'gigabytes': 1,
'snapshot_gigabytes': 1,
'share_networks': 1,
}
}
return (200, {}, quota_set)
get_quota_sets_test = get_os_quota_sets_test
def get_os_quota_sets_test_defaults(self):
quota_set = {
'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'shares': 1,
'snapshots': 1,
'gigabytes': 1,
'snapshot_gigabytes': 1,
'share_networks': 1,
}
}
return (200, {}, quota_set)
get_quota_sets_test_defaults = get_os_quota_sets_test_defaults
def put_os_quota_sets_test(self, body, **kw):
assert list(body) == ['quota_set']
fakes.assert_has_keys(body['quota_set'],
required=['tenant_id'])
quota_set = {
'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'shares': 2,
'snapshots': 2,
'gigabytes': 1,
'snapshot_gigabytes': 1,
'share_networks': 1,
}
}
return (200, {}, quota_set)
put_quota_sets_test = put_os_quota_sets_test
#
# Quota Classes
#
def get_os_quota_class_sets_test(self, **kw):
quota_class_set = {
'quota_class_set': {
'class_name': 'test',
'metadata_items': [],
'shares': 1,
'snapshots': 1,
'gigabytes': 1,
'snapshot_gigabytes': 1,
'share_networks': 1,
}
}
return (200, {}, quota_class_set)
get_quota_class_sets_test = get_os_quota_class_sets_test
def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set']
fakes.assert_has_keys(body['quota_class_set'],
required=['class_name'])
quota_class_set = {
'quota_class_set': {
'class_name': 'test',
'metadata_items': [],
'shares': 2,
'snapshots': 2,
'gigabytes': 1,
'snapshot_gigabytes': 1,
'share_networks': 1,
}
}
return (200, {}, quota_class_set)
put_quota_class_sets_test = put_os_quota_class_sets_test
def delete_os_quota_sets_test(self, **kw):
return (202, {}, {})
delete_quota_sets_test = delete_os_quota_sets_test
#
# List all extensions
#
def get_extensions(self, **kw):
exts = [
{
"alias": "FAKE-1",
"description": "Fake extension number 1",
"links": [],
"name": "Fake1",
"namespace": ("http://docs.openstack.org/"
"/ext/fake1/api/v1.1"),
"updated": "2011-06-09T00:00:00+00:00"
},
{
"alias": "FAKE-2",
"description": "Fake extension number 2",
"links": [],
"name": "Fake2",
"namespace": ("http://docs.openstack.org/"
"/ext/fake1/api/v1.1"),
"updated": "2011-06-09T00:00:00+00:00"
},
]
return (200, {}, {"extensions": exts, })

View File

@ -0,0 +1,167 @@
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
# Copyright 2011 OpenStack Foundation
#
# 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 __future__ import print_function
from distilclient.tests.unit.v2 import fake_clients as fakes
from distilclient.v2 import client
class FakeClient(fakes.FakeClient):
def __init__(self, *args, **kwargs):
client.Client.__init__(
self,
'2',
'username',
'password',
'project_id',
'auth_url',
input_auth_token='token',
extensions=kwargs.get('extensions'),
distil_url='http://localhost:9999',
api_version=kwargs.get("api_version", '2')
)
self.client = FakeHTTPClient(**kwargs)
def get_fake_export_location():
return {
'uuid': 'foo_el_uuid',
'path': '/foo/el/path',
'share_instance_id': 'foo_share_instance_id',
'is_admin_only': False,
'created_at': '2015-12-17T13:14:15Z',
'updated_at': '2015-12-17T14:15:16Z',
}
def get_fake_snapshot_export_location():
return {
'uuid': 'foo_el_uuid',
'path': '/foo/el/path',
'share_snapshot_instance_id': 'foo_share_instance_id',
'is_admin_only': False,
'created_at': '2017-01-17T13:14:15Z',
'updated_at': '2017-01-17T14:15:16Z',
}
class FakeHTTPClient(fakes.FakeHTTPClient):
def get_(self, **kw):
body = {
"versions": [
{
"status": "CURRENT",
"updated": "2015-07-30T11:33:21Z",
"links": [
{
"href": "http://docs.openstack.org/",
"type": "text/html",
"rel": "describedby",
},
{
"href": "http://localhost:8786/v2/",
"rel": "self",
}
],
"min_version": "2.0",
"version": self.default_headers[
"X-Openstack-Distil-Api-Version"],
"id": "v2.0",
}
]
}
return (200, {}, body)
def get_availability_zones(self):
availability_zones = {
"availability_zones": [
{"id": "368c5780-ad72-4bcf-a8b6-19e45f4fafoo",
"name": "foo",
"created_at": "2016-07-08T14:13:12.000000",
"updated_at": "2016-07-08T15:14:13.000000"},
{"id": "368c5780-ad72-4bcf-a8b6-19e45f4fabar",
"name": "bar",
"created_at": "2016-07-08T14:13:12.000000",
"updated_at": "2016-07-08T15:14:13.000000"},
]
}
return (200, {}, availability_zones)
def get_os_services(self, **kw):
services = {
"services": [
{"status": "enabled",
"binary": "distil-scheduler",
"zone": "foozone",
"state": "up",
"updated_at": "2015-10-09T13:54:09.000000",
"host": "lucky-star",
"id": 1},
{"status": "enabled",
"binary": "distil-share",
"zone": "foozone",
"state": "up",
"updated_at": "2015-10-09T13:54:05.000000",
"host": "lucky-star",
"id": 2},
]
}
return (200, {}, services)
get_services = get_os_services
def put_os_services_enable(self, **kw):
return (200, {}, {'host': 'foo', 'binary': 'distil-share',
'disabled': False})
put_services_enable = put_os_services_enable
def put_os_services_disable(self, **kw):
return (200, {}, {'host': 'foo', 'binary': 'distil-share',
'disabled': True})
put_services_disable = put_os_services_disable
def get_v2(self, **kw):
body = {
"versions": [
{
"status": "CURRENT",
"updated": "2015-07-30T11:33:21Z",
"links": [
{
"href": "http://docs.openstack.org/",
"type": "text/html",
"rel": "describedby",
},
{
"href": "http://localhost:8786/v2/",
"rel": "self",
}
],
"min_version": "2.0",
"version": "2.5",
"id": "v1.0",
}
]
}
return (200, {}, body)
def get_shares_1234(self, **kw):
share = {'share': {'id': 1234, 'name': 'sharename'}}
return (200, {}, share)

View File

@ -0,0 +1,301 @@
# 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 ddt
import uuid
import mock
import distilclient
from distilclient import exceptions
from distilclient.tests.unit import utils
from distilclient.v2 import client
@ddt.ddt
class ClientTest(utils.TestCase):
def setUp(self):
super(self.__class__, self).setUp()
self.catalog = {
'rating': [
{'region': 'TestRegion', 'publicURL': 'http://1.2.3.4'},
],
}
def test_adapter_properties(self):
# sample of properties, there are many more
retries = 3
base_url = uuid.uuid4().hex
s = client.session.Session()
c = client.Client(session=s,
api_version=distilclient.API_MAX_VERSION,
distil_url=base_url, retries=retries,
input_auth_token='token')
self.assertEqual(base_url, c.client.endpoint_url)
self.assertEqual(retries, c.client.retries)
def test_auth_via_token_invalid(self):
self.assertRaises(exceptions.ClientException, client.Client,
api_version=distilclient.API_MAX_VERSION,
input_auth_token="token")
def test_auth_via_token_and_session(self):
s = client.session.Session()
base_url = uuid.uuid4().hex
c = client.Client(input_auth_token='token',
distil_url=base_url, session=s,
api_version=distilclient.API_MAX_VERSION)
self.assertIsNotNone(c.client)
self.assertIsNone(c.keystone_client)
def test_auth_via_token(self):
base_url = uuid.uuid4().hex
c = client.Client(input_auth_token='token',
distil_url=base_url,
api_version=distilclient.API_MAX_VERSION)
self.assertIsNotNone(c.client)
self.assertIsNone(c.keystone_client)
@mock.patch.object(client.Client, '_get_keystone_client', mock.Mock())
def test_valid_region_name_v1(self):
self.mock_object(client.httpclient, 'HTTPClient')
kc = client.Client._get_keystone_client.return_value
kc.service_catalog = mock.Mock()
kc.service_catalog.get_endpoints = mock.Mock(return_value=self.catalog)
c = client.Client(api_version=distilclient.API_DEPRECATED_VERSION,
service_type="rating",
region_name='TestRegion')
self.assertTrue(client.Client._get_keystone_client.called)
kc.service_catalog.get_endpoints.assert_called_with('rating')
client.httpclient.HTTPClient.assert_called_with(
'http://1.2.3.4',
mock.ANY,
'python-distilclient',
insecure=False,
cacert=None,
timeout=None,
retries=None,
http_log_debug=False,
api_version=distilclient.API_DEPRECATED_VERSION)
self.assertIsNotNone(c.client)
@mock.patch.object(client.Client, '_get_keystone_client', mock.Mock())
def test_nonexistent_region_name(self):
kc = client.Client._get_keystone_client.return_value
kc.service_catalog = mock.Mock()
kc.service_catalog.get_endpoints = mock.Mock(return_value=self.catalog)
self.assertRaises(RuntimeError, client.Client,
api_version=distilclient.API_MAX_VERSION,
region_name='FakeRegion')
self.assertTrue(client.Client._get_keystone_client.called)
kc.service_catalog.get_endpoints.assert_called_with('rating')
@mock.patch.object(client.Client, '_get_keystone_client', mock.Mock())
def test_regions_with_same_name(self):
self.mock_object(client.httpclient, 'HTTPClient')
catalog = {
'ratingv2': [
{'region': 'FirstRegion', 'publicURL': 'http://1.2.3.4'},
{'region': 'secondregion', 'publicURL': 'http://1.1.1.1'},
{'region': 'SecondRegion', 'publicURL': 'http://2.2.2.2'},
],
}
kc = client.Client._get_keystone_client.return_value
kc.service_catalog = mock.Mock()
kc.service_catalog.get_endpoints = mock.Mock(return_value=catalog)
c = client.Client(api_version=distilclient.API_MIN_VERSION,
service_type='ratingv2',
region_name='SecondRegion')
self.assertTrue(client.Client._get_keystone_client.called)
kc.service_catalog.get_endpoints.assert_called_with('ratingv2')
client.httpclient.HTTPClient.assert_called_with(
'http://2.2.2.2',
mock.ANY,
'python-distilclient',
insecure=False,
cacert=None,
timeout=None,
retries=None,
http_log_debug=False,
api_version=distilclient.API_MIN_VERSION)
self.assertIsNotNone(c.client)
def _get_client_args(self, **kwargs):
client_args = {
'auth_url': 'both',
'api_version': distilclient.API_DEPRECATED_VERSION,
'username': 'fake_username',
'service_type': 'ratingv2',
'region_name': 'SecondRegion',
'input_auth_token': None,
'session': None,
'service_catalog_url': None,
'user_id': 'foo_user_id',
'user_domain_name': 'foo_user_domain_name',
'user_domain_id': 'foo_user_domain_id',
'project_name': 'foo_project_name',
'project_domain_name': 'foo_project_domain_name',
'project_domain_id': 'foo_project_domain_id',
'endpoint_type': 'publicUrl',
'cert': 'foo_cert',
}
client_args.update(kwargs)
return client_args
@ddt.data(
{'auth_url': 'only_v3', 'api_key': 'password_backward_compat',
'endpoint_type': 'publicURL', 'project_id': 'foo_tenant_project_id'},
{'password': 'renamed_api_key', 'endpoint_type': 'public',
'tenant_id': 'foo_tenant_project_id'},
)
def test_client_init_no_session_no_auth_token_v3(self, kwargs):
def fake_url_for(version):
if version == 'v3.0':
return 'url_v3.0'
elif version == 'v2.0' and self.auth_url == 'both':
return 'url_v2.0'
else:
return None
self.mock_object(client.httpclient, 'HTTPClient')
self.mock_object(client.ks_client, 'Client')
self.mock_object(client.discover, 'Discover')
self.mock_object(client.session, 'Session')
client_args = self._get_client_args(**kwargs)
client_args['api_version'] = distilclient.API_MIN_VERSION
self.auth_url = client_args['auth_url']
catalog = {
'rating': [
{'region': 'SecondRegion', 'region_id': 'SecondRegion',
'url': 'http://4.4.4.4', 'interface': 'public',
},
],
'ratingv2': [
{'region': 'FirstRegion', 'interface': 'public',
'region_id': 'SecondRegion', 'url': 'http://1.1.1.1'},
{'region': 'secondregion', 'interface': 'public',
'region_id': 'SecondRegion', 'url': 'http://2.2.2.2'},
{'region': 'SecondRegion', 'interface': 'internal',
'region_id': 'SecondRegion', 'url': 'http://3.3.3.1'},
{'region': 'SecondRegion', 'interface': 'public',
'region_id': 'SecondRegion', 'url': 'http://3.3.3.3'},
{'region': 'SecondRegion', 'interface': 'admin',
'region_id': 'SecondRegion', 'url': 'http://3.3.3.2'},
],
}
client.discover.Discover.return_value.url_for.side_effect = (
fake_url_for)
client.ks_client.Client.return_value.auth_token.return_value = (
'fake_token')
mocked_ks_client = client.ks_client.Client.return_value
mocked_ks_client.service_catalog.get_endpoints.return_value = catalog
client.Client(**client_args)
client.httpclient.HTTPClient.assert_called_with(
'http://3.3.3.3', mock.ANY, 'python-distilclient', insecure=False,
cacert=None, timeout=None, retries=None, http_log_debug=False,
api_version=distilclient.API_MIN_VERSION)
client.ks_client.Client.assert_called_with(
session=mock.ANY, version=(3, 0), auth_url='url_v3.0',
username=client_args['username'],
password=client_args.get('password', client_args.get('api_key')),
user_id=client_args['user_id'],
user_domain_name=client_args['user_domain_name'],
user_domain_id=client_args['user_domain_id'],
project_id=client_args.get('tenant_id',
client_args.get('project_id')),
project_name=client_args['project_name'],
project_domain_name=client_args['project_domain_name'],
project_domain_id=client_args['project_domain_id'],
region_name=client_args['region_name'],
)
mocked_ks_client.service_catalog.get_endpoints.assert_called_with(
client_args['service_type'])
mocked_ks_client.authenticate.assert_called_with()
@ddt.data(
{'auth_url': 'only_v2', 'api_key': 'foo', 'project_id': 'bar'},
{'password': 'foo', 'tenant_id': 'bar'},
)
def test_client_init_no_session_no_auth_token_v2(self, kwargs):
self.mock_object(client.httpclient, 'HTTPClient')
self.mock_object(client.ks_client, 'Client')
self.mock_object(client.discover, 'Discover')
self.mock_object(client.session, 'Session')
client_args = self._get_client_args(**kwargs)
client_args['api_version'] = distilclient.API_MIN_VERSION
self.auth_url = client_args['auth_url']
catalog = {
'rating': [
{'region': 'SecondRegion', 'publicUrl': 'http://4.4.4.4'},
],
'ratingv2': [
{'region': 'FirstRegion', 'publicUrl': 'http://1.1.1.1'},
{'region': 'secondregion', 'publicUrl': 'http://2.2.2.2'},
{'region': 'SecondRegion', 'internalUrl': 'http://3.3.3.1',
'publicUrl': 'http://3.3.3.3', 'adminUrl': 'http://3.3.3.2'},
],
}
client.discover.Discover.return_value.url_for.side_effect = (
lambda v: 'url_v2.0' if v == 'v2.0' else None)
client.ks_client.Client.return_value.auth_token.return_value = (
'fake_token')
mocked_ks_client = client.ks_client.Client.return_value
mocked_ks_client.service_catalog.get_endpoints.return_value = catalog
client.Client(**client_args)
client.httpclient.HTTPClient.assert_called_with(
'http://3.3.3.3', mock.ANY, 'python-distilclient', insecure=False,
cacert=None, timeout=None, retries=None, http_log_debug=False,
api_version=distilclient.API_MIN_VERSION)
client.ks_client.Client.assert_called_with(
session=mock.ANY, version=(2, 0), auth_url='url_v2.0',
username=client_args['username'],
password=client_args.get('password', client_args.get('api_key')),
tenant_id=client_args.get('tenant_id',
client_args.get('project_id')),
tenant_name=client_args['project_name'],
region_name=client_args['region_name'], cert=client_args['cert'],
use_keyring=False, force_new_token=False, stale_duration=300)
mocked_ks_client.service_catalog.get_endpoints.assert_called_with(
client_args['service_type'])
mocked_ks_client.authenticate.assert_called_with()
@mock.patch.object(client.ks_client, 'Client', mock.Mock())
@mock.patch.object(client.discover, 'Discover', mock.Mock())
@mock.patch.object(client.session, 'Session', mock.Mock())
def test_client_init_no_session_no_auth_token_endpoint_not_found(self):
self.mock_object(client.httpclient, 'HTTPClient')
client_args = self._get_client_args(
auth_urli='fake_url',
password='foo_password',
tenant_id='foo_tenant_id')
client.discover.Discover.return_value.url_for.return_value = None
mocked_ks_client = client.ks_client.Client.return_value
self.assertRaises(
exceptions.CommandError, client.Client, **client_args)
self.assertTrue(client.session.Session.called)
self.assertTrue(client.discover.Discover.called)
self.assertFalse(client.httpclient.HTTPClient.called)
self.assertFalse(client.ks_client.Client.called)
self.assertFalse(mocked_ks_client.service_catalog.get_endpoints.called)
self.assertFalse(mocked_ks_client.authenticate.called)

View File

@ -0,0 +1,25 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# 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 distilclient.tests.unit import utils
class UsageTest(utils.TestCase):
# Testcases for class Share
def setUp(self):
super(UsageTest, self).setUp()

53
distilclient/utils.py Normal file
View File

@ -0,0 +1,53 @@
# 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 six
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
def safe_issubclass(*args):
"""Like issubclass, but will just return False if not a class."""
try:
if issubclass(*args):
return True
except TypeError:
pass
return False
def get_function_name(func):
if six.PY2:
if hasattr(func, "im_class"):
return "%s.%s" % (func.im_class, func.__name__)
else:
return "%s.%s" % (func.__module__, func.__name__)
else:
return "%s.%s" % (func.__module__, func.__qualname__)

View File

200
distilclient/v1/client.py Normal file
View File

@ -0,0 +1,200 @@
# Copyright (C) 2014 Catalyst IT Ltd
#
# 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 requests
from requests.exceptions import ConnectionError
from urlparse import urljoin
from keystoneauth1 import adapter
from keystoneauth1.identity import generic
from keystoneauth1 import session
def Client(session=None, endpoint=None, username=None, password=None,
include_pass=None, endpoint_type=None,
auth_url=None, **kwargs):
if session:
kwargs['endpoint_override'] = endpoint
return SessionClient(session, **kwargs)
else:
return HTTPClient(**kwargs)
class SessionClient(object):
"""HTTP client based on Keystone client session."""
def __init__(self, session, service_type='rating',
interface='publicURL', **kwargs):
self.client = adapter.LegacyJsonAdapter(
session, service_type=service_type, interface=interface, **kwargs)
def collect_usage(self):
headers = {"Content-Type": "application/json"}
response, json = self.client.request(
"collect_usage", 'POST', headers=headers)
if response.status_code != 200:
raise AttributeError("Usage cycle failed: %s code: %s" %
(response.text, response.status_code))
else:
return json
def last_collected(self):
headers = {"Content-Type": "application/json"}
response, json = self.client.request(
"last_collected", 'GET', headers=headers)
if response.status_code != 200:
raise AttributeError("Get last collected failed: %s code: %s" %
(response.text, response.status_code))
else:
return json
def get_usage(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_usage")
def get_rated(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_rated")
def _query_usage(self, tenant, start, end, url):
params = {"tenant": tenant,
"start": start,
"end": end
}
response, json = self.client.request(
url, 'GET', params=params)
if response.status_code != 200:
raise AttributeError("Get usage failed: %s code: %s" %
(response.text, response.status_code))
else:
return json
class HTTPClient(object):
def __init__(self, distil_url=None, os_auth_token=None,
os_username=None, os_password=None,
os_project_id=None, os_project_name=None,
os_tenant_id=None, os_tenant_name=None,
os_project_domain_id='default',
os_project_domain_name='Default',
os_auth_url=None, os_region_name=None,
os_cacert=None, insecure=False,
os_service_type='rating', os_endpoint_type='publicURL'):
project_id = os_project_id or os_tenant_id
project_name = os_project_name or os_tenant_name
self.insecure = insecure
if os_auth_token and distil_url:
self.auth_token = os_auth_token
self.endpoint = distil_url
else:
if insecure:
verify = False
else:
verify = os_cacert or True
kwargs = {
'username': os_username,
'password': os_password,
'auth_url': os_auth_url,
'project_id': project_id,
'project_name': project_name,
'project_domain_id': os_project_domain_id,
'project_domain_name': os_project_domain_name,
}
auth = generic.Password(**kwargs)
sess = session.Session(auth=auth, verify=verify)
if os_auth_token:
self.auth_token = os_auth_token
else:
self.auth_token = auth.get_token(sess)
if distil_url:
self.endpoint = distil_url
else:
self.endpoint = auth.get_endpoint(
sess, service_type=os_service_type,
interface=os_endpoint_type,
region_name=os_region_name)
def collect_usage(self):
url = urljoin(self.endpoint, "collect_usage")
headers = {"Content-Type": "application/json",
"X-Auth-Token": self.auth_token}
try:
response = requests.post(url, headers=headers,
verify=not self.insecure)
if response.status_code != 200:
raise AttributeError("Usage cycle failed: %s code: %s" %
(response.text, response.status_code))
else:
return response.json()
except ConnectionError as e:
print e
def last_collected(self):
url = urljoin(self.endpoint, "last_collected")
headers = {"Content-Type": "application/json",
"X-Auth-Token": self.auth_token}
try:
response = requests.get(url, headers=headers,
verify=not self.insecure)
if response.status_code != 200:
raise AttributeError("Get last collected failed: %s code: %s" %
(response.text, response.status_code))
else:
return response.json()
except ConnectionError as e:
print e
def get_usage(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_usage")
def get_rated(self, tenant, start, end):
return self._query_usage(tenant, start, end, "get_rated")
def _query_usage(self, tenant, start, end, endpoint):
url = urljoin(self.endpoint, endpoint)
headers = {"X-Auth-Token": self.auth_token}
params = {"tenant": tenant,
"start": start,
"end": end
}
try:
response = requests.get(url, headers=headers,
params=params,
verify=not self.insecure)
if response.status_code != 200:
raise AttributeError("Get usage failed: %s code: %s" %
(response.text, response.status_code))
else:
return response.json()
except ConnectionError as e:
print e

View File

264
distilclient/v2/client.py Normal file
View File

@ -0,0 +1,264 @@
# 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 warnings
from keystoneclient import adapter
from keystoneclient import client as ks_client
from keystoneclient import discover
from keystoneclient import session
import six
from distilclient.common import httpclient
from distilclient import exceptions
from distilclient.v2 import usage
class Client(object):
"""Top-level object to access the OpenStack Distil API.
Create an instance with your creds::
>>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL)
Or, alternatively, you can create a client instance using the
keystoneclient.session API::
>>> from keystoneclient.auth.identity import v2
>>> from keystoneclient import session
>>> from distilclient import client
>>> auth = v2.Password(auth_url=AUTH_URL,
username=USERNAME,
password=PASSWORD,
tenant_name=PROJECT_ID)
>>> sess = session.Session(auth=auth)
>>> distil = client.Client(VERSION, session=sess)
Then call methods on its managers::
>>> client.cost.list()
...
"""
def __init__(self, username=None, api_key=None,
project_id=None, auth_url=None, insecure=False, timeout=None,
tenant_id=None, project_name=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='rating', service_name=None,
retries=None, http_log_debug=False, input_auth_token=None,
session=None, auth=None, cacert=None,
distil_url=None, user_agent='python-distilclient',
use_keyring=False, force_new_token=False,
cached_token_lifetime=300,
api_version='2',
user_id=None,
user_domain_id=None,
user_domain_name=None,
project_domain_id=None,
project_domain_name=None,
cert=None,
password=None,
**kwargs):
self.username = username
self.password = password or api_key
self.tenant_id = tenant_id or project_id
self.tenant_name = project_name
self.user_id = user_id
self.project_id = project_id or tenant_id
self.project_name = project_name
self.user_domain_id = user_domain_id
self.user_domain_name = user_domain_name
self.project_domain_id = project_domain_id
self.project_domain_name = project_domain_name
self.endpoint_type = endpoint_type
self.auth_url = auth_url
self.region_name = region_name
self.cacert = cacert
self.cert = cert
self.insecure = insecure
self.use_keyring = use_keyring
self.force_new_token = force_new_token
self.cached_token_lifetime = cached_token_lifetime
service_name = kwargs.get("share_service_name", service_name)
def check_deprecated_arguments():
deprecated = {
'share_service_name': 'service_name',
'proxy_tenant_id': None,
'proxy_token': None,
'os_cache': 'use_keyring',
'api_key': 'password',
}
for arg, replacement in six.iteritems(deprecated):
if kwargs.get(arg, None) is None:
continue
replacement_msg = ""
if replacement is not None:
replacement_msg = " Use %s instead." % replacement
msg = "Argument %(arg)s is deprecated.%(repl)s" % {
'arg': arg,
'repl': replacement_msg
}
warnings.warn(msg)
check_deprecated_arguments()
if input_auth_token and not distil_url:
msg = ("For token-based authentication you should "
"provide 'input_auth_token' and 'distil_url'.")
raise exceptions.ClientException(msg)
self.project_id = tenant_id if tenant_id is not None else project_id
self.keystone_client = None
self.session = session
# NOTE(u_glide): token authorization has highest priority.
# That's why session and/or password will be ignored
# if token is provided.
if not input_auth_token:
if session:
self.keystone_client = adapter.LegacyJsonAdapter(
session=session,
auth=auth,
interface=endpoint_type,
service_type=service_type,
service_name=service_name,
region_name=region_name)
input_auth_token = self.keystone_client.session.get_token(auth)
else:
self.keystone_client = self._get_keystone_client()
input_auth_token = self.keystone_client.auth_token
if not input_auth_token:
raise RuntimeError("Not Authorized")
if session and not distil_url:
distil_url = self.keystone_client.session.get_endpoint(
auth, interface=endpoint_type,
service_type=service_type)
elif not distil_url:
catalog = self.keystone_client.service_catalog.get_endpoints(
service_type)
for catalog_entry in catalog.get(service_type, []):
if (catalog_entry.get("interface") == (
endpoint_type.lower().split("url")[0]) or
catalog_entry.get(endpoint_type)):
if (region_name and not region_name == (
catalog_entry.get(
"region",
catalog_entry.get("region_id")))):
continue
distil_url = catalog_entry.get(
"url", catalog_entry.get(endpoint_type))
break
if not distil_url:
raise RuntimeError("Could not find Distil endpoint in catalog")
self.api_version = api_version
self.client = httpclient.HTTPClient(distil_url,
input_auth_token,
user_agent,
insecure=insecure,
cacert=cacert,
timeout=timeout,
retries=retries,
http_log_debug=http_log_debug,
api_version=self.api_version)
self.usage = usage.UsageManager(self)
self._load_extensions(extensions)
def _load_extensions(self, extensions):
if not extensions:
return
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name, extension.manager_class(self))
def authenticate(self):
"""Authenticate against the server.
Normally this is called automatically when you first access the API,
but you can call this method to force authentication right now.
Returns on success; raises :exc:`exceptions.Unauthorized` if the
credentials are wrong.
"""
warnings.warn("authenticate() method is deprecated. "
"Client automatically makes authentication call "
"in the constructor.")
def _get_keystone_client(self):
# First create a Keystone session
if self.insecure:
verify = False
else:
verify = self.cacert or True
ks_session = session.Session(verify=verify, cert=self.cert)
# Discover the supported keystone versions using the given url
ks_discover = discover.Discover(
session=ks_session, auth_url=self.auth_url)
# Inspect the auth_url to see the supported version. If both v3 and v2
# are supported, then use the highest version if possible.
v2_auth_url = ks_discover.url_for('v2.0')
v3_auth_url = ks_discover.url_for('v3.0')
if v3_auth_url:
keystone_client = ks_client.Client(
session=ks_session,
version=(3, 0),
auth_url=v3_auth_url,
username=self.username,
password=self.password,
user_id=self.user_id,
user_domain_name=self.user_domain_name,
user_domain_id=self.user_domain_id,
project_id=self.project_id or self.tenant_id,
project_name=self.project_name,
project_domain_name=self.project_domain_name,
project_domain_id=self.project_domain_id,
region_name=self.region_name)
elif v2_auth_url:
keystone_client = ks_client.Client(
session=ks_session,
version=(2, 0),
auth_url=v2_auth_url,
username=self.username,
password=self.password,
tenant_id=self.tenant_id,
tenant_name=self.tenant_name,
region_name=self.region_name,
cert=self.cert,
use_keyring=self.use_keyring,
force_new_token=self.force_new_token,
stale_duration=self.cached_token_lifetime)
else:
raise exceptions.CommandError(
'Unable to determine the Keystone version to authenticate '
'with using the given auth_url.')
keystone_client.authenticate()
return keystone_client

27
distilclient/v2/usage.py Normal file
View File

@ -0,0 +1,27 @@
# Copyright 2017 Catalyst IT Ltd.
#
# 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 distilclient import base
class UsageManager(base.Manager):
def list(self, project_id, start, end):
"""Retrieve a list of usages.
:returns: A list of usages.
"""
url = "/v2/usage?project_id={0}&start={1}&end={2}".format(project_id,
start,
end)
return self._list(url, "usage")

View File

@ -1,3 +1,5 @@
six>=1.9.0 # MIT
pbr>=1.6 # Apache-2.0
python-keystoneclient>=1.7.0,!=1.8.0,!=2.1.0 # Apache-2.0
python-keystoneclient>=1.7.0,!=1.8.0,!=2.1.0 # Apache-2.0
debtcollector>=1.2.0 # Apache-2.0

View File

@ -16,6 +16,9 @@ testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
tempest>=11.0.0 # Apache-2.0
os-testr>=0.8.0 # Apache-2.0
# releasenotes
reno>=1.6.2 # Apache2
ddt>=1.0.1 # MIT

View File

@ -16,7 +16,7 @@ setenv = VIRTUAL_ENV={envdir}
NOSE_OPENSTACK_STDOUT=1
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'
commands = ostestr {posargs}
whitelist_externals = find