Global rewrite of the client

- Dynamic import of cloudkitty modules
 - Support the new cloudkitty-api
 - Support the new hashmap API

Change-Id: I8e3067d3144ed9f78ffd6a89c97a2435939f0590
Co-Authored-By: Stéphane Albert <stephane.albert@objectif-libre.com>
This commit is contained in:
Guillaume Espanel 2015-03-05 13:38:50 +01:00
parent 3d5814ed2c
commit 9ab382643a
67 changed files with 3369 additions and 2351 deletions

16
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,16 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in this page:
http://docs.openstack.org/infra/manual/developers.html
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Launchpad, not GitHub:
https://bugs.launchpad.net/cloudkitty

4
HACKING.rst Normal file
View File

@ -0,0 +1,4 @@
python-cloudkittyclient Style Commandments
===============================================
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/

View File

@ -173,3 +173,4 @@
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview
global-exclude *.pyc

View File

@ -1,7 +1,7 @@
Python bindings to the CloudKitty API
=====================================
:version: 1.0
:version: 0.2
:Wiki: `CloudKitty Wiki`_
:IRC: #cloudkitty @ freenode
@ -21,12 +21,3 @@ Status
This project is **highly** work in progress.
Roadmap
=======
* Add some tests.
* Add some doc.
* Move from importutils to stevedore.
* Add a command-line tool.
* Global code improvement.

2
babel.cfg Normal file
View File

@ -0,0 +1,2 @@
[python: **.py]

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# Copyright 2015 Objectif Libre
# 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
@ -12,15 +12,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: François Magimel (linkid)
__all__ = ['__version__']
import pbr.version
version_info = pbr.version.VersionInfo('python-cloudkittyclient')
try:
__version__ = version_info.version_string()
except AttributeError:
__version__ = None
__version__ = pbr.version.VersionInfo(
'cloudkittyclient').version_string()

View File

@ -1,66 +1,316 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
# 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.
#
# @author: François Magimel (linkid)
# 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.
"""
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import discover
from keystoneclient import exceptions as ks_exc
from keystoneclient import session
from oslo.utils import strutils
import six.moves.urllib.parse as urlparse
from cloudkittyclient.common import auth as ks_auth
from cloudkittyclient.common import client
from cloudkittyclient.openstack.common import importutils
from cloudkittyclient.common import utils
from cloudkittyclient import exc
from cloudkittyclient.openstack.common.apiclient import auth
from cloudkittyclient.openstack.common.apiclient import exceptions
def get_client(api_version, **kwargs):
"""Get an authenticated client.
def _discover_auth_versions(session, auth_url):
# discover the API versions the server is supporting based on the
# given URL
v2_auth_url = None
v3_auth_url = None
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except ks_exc.DiscoveryFailure:
raise
except exceptions.ClientException:
# Identity service may not support discovery. In that case,
# try to determine version from auth_url
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
v3_auth_url = auth_url
elif path.startswith('/v2'):
v2_auth_url = auth_url
else:
raise exc.CommandError('Unable to determine the Keystone '
'version to authenticate with '
'using the given auth_url.')
return v2_auth_url, v3_auth_url
This is based on the credentials in the keyword args.
:param api_version: the API version to use
def _get_keystone_session(**kwargs):
# TODO(fabgia): the heavy lifting here should be really done by Keystone.
# Unfortunately Keystone does not support a richer method to perform
# discovery and return a single viable URL. A bug against Keystone has
# been filed: https://bugs.launchpad.net/python-keystoneclient/+bug/1330677
# first create a Keystone session
cacert = kwargs.pop('cacert', None)
cert = kwargs.pop('cert', None)
key = kwargs.pop('key', None)
insecure = kwargs.pop('insecure', False)
auth_url = kwargs.pop('auth_url', None)
project_id = kwargs.pop('project_id', None)
project_name = kwargs.pop('project_name', None)
if insecure:
verify = False
else:
verify = cacert or True
if cert and key:
# passing cert and key together is deprecated in favour of the
# requests lib form of having the cert and key as a tuple
cert = (cert, key)
# create the keystone client session
ks_session = session.Session(verify=verify, cert=cert)
v2_auth_url, v3_auth_url = _discover_auth_versions(ks_session, auth_url)
username = kwargs.pop('username', None)
user_id = kwargs.pop('user_id', None)
user_domain_name = kwargs.pop('user_domain_name', None)
user_domain_id = kwargs.pop('user_domain_id', None)
project_domain_name = kwargs.pop('project_domain_name', None)
project_domain_id = kwargs.pop('project_domain_id', None)
auth = None
use_domain = (user_domain_id or user_domain_name or
project_domain_id or project_domain_name)
use_v3 = v3_auth_url and (use_domain or (not v2_auth_url))
use_v2 = v2_auth_url and not use_domain
if use_v3:
# the auth_url as v3 specified
# e.g. http://no.where:5000/v3
# Keystone will return only v3 as viable option
auth = v3_auth.Password(
v3_auth_url,
username=username,
password=kwargs.pop('password', None),
user_id=user_id,
user_domain_name=user_domain_name,
user_domain_id=user_domain_id,
project_domain_name=project_domain_name,
project_domain_id=project_domain_id)
elif use_v2:
# the auth_url as v2 specified
# e.g. http://no.where:5000/v2.0
# Keystone will return only v2 as viable option
auth = v2_auth.Password(
v2_auth_url,
username,
kwargs.pop('password', None),
tenant_id=project_id,
tenant_name=project_name)
else:
raise exc.CommandError('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url.')
ks_session.auth = auth
return ks_session
def _get_endpoint(ks_session, **kwargs):
"""Get an endpoint using the provided keystone session."""
# set service specific endpoint types
endpoint_type = kwargs.get('endpoint_type') or 'publicURL'
service_type = kwargs.get('service_type') or 'rating'
endpoint = ks_session.get_endpoint(service_type=service_type,
interface=endpoint_type,
region_name=kwargs.get('region_name'))
return endpoint
class AuthPlugin(auth.BaseAuthPlugin):
opt_names = ['tenant_id', 'region_name', 'auth_token',
'service_type', 'endpoint_type', 'cacert',
'auth_url', 'insecure', 'cert_file', 'key_file',
'cert', 'key', 'tenant_name', 'project_name',
'project_id', 'user_domain_id', 'user_domain_name',
'password', 'username', 'endpoint']
def __init__(self, auth_system=None, **kwargs):
self.opt_names.extend(self.common_opt_names)
super(AuthPlugin, self).__init__(auth_system, **kwargs)
def _do_authenticate(self, http_client):
token = self.opts.get('token') or self.opts.get('auth_token')
endpoint = self.opts.get('endpoint')
if not (token and endpoint):
project_id = (self.opts.get('project_id') or
self.opts.get('tenant_id'))
project_name = (self.opts.get('project_name') or
self.opts.get('tenant_name'))
ks_kwargs = {
'username': self.opts.get('username'),
'password': self.opts.get('password'),
'user_id': self.opts.get('user_id'),
'user_domain_id': self.opts.get('user_domain_id'),
'user_domain_name': self.opts.get('user_domain_name'),
'project_id': project_id,
'project_name': project_name,
'project_domain_name': self.opts.get('project_domain_name'),
'project_domain_id': self.opts.get('project_domain_id'),
'auth_url': self.opts.get('auth_url'),
'cacert': self.opts.get('cacert'),
'cert': self.opts.get('cert'),
'key': self.opts.get('key'),
'insecure': strutils.bool_from_string(
self.opts.get('insecure')),
'endpoint_type': self.opts.get('endpoint_type'),
}
# retrieve session
ks_session = _get_keystone_session(**ks_kwargs)
token = lambda: ks_session.get_token()
endpoint = (self.opts.get('endpoint') or
_get_endpoint(ks_session, **ks_kwargs))
self.opts['token'] = token
self.opts['endpoint'] = endpoint
def token_and_endpoint(self, endpoint_type, service_type):
token = self.opts.get('token')
if callable(token):
token = token()
return token, self.opts.get('endpoint')
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
has_token = self.opts.get('token') or self.opts.get('auth_token')
no_auth = has_token and self.opts.get('endpoint')
has_tenant = self.opts.get('tenant_id') or self.opts.get('tenant_name')
has_credential = (self.opts.get('username') and has_tenant
and self.opts.get('password')
and self.opts.get('auth_url'))
missing = not (no_auth or has_credential)
if missing:
missing_opts = []
opts = ['token', 'endpoint', 'username', 'password', 'auth_url',
'tenant_id', 'tenant_name']
for opt in opts:
if not self.opts.get(opt):
missing_opts.append(opt)
raise exceptions.AuthPluginOptionsMissing(missing_opts)
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
kwargs['token'] = kwargs.get('token') or kwargs.get('auth_token')
return client_class(*args, **kwargs)
def _adjust_params(kwargs):
timeout = kwargs.get('timeout')
if timeout is not None:
timeout = int(timeout)
if timeout <= 0:
timeout = None
insecure = strutils.bool_from_string(kwargs.get('insecure'))
verify = kwargs.get('verify')
if verify is None:
if insecure:
verify = False
else:
verify = kwargs.get('cacert') or True
cert = kwargs.get('cert_file')
key = kwargs.get('key_file')
if cert and key:
cert = cert, key
return {'verify': verify, 'cert': cert, 'timeout': timeout}
def get_client(version, **kwargs):
"""Get an authenticated client, based on the credentials in the kwargs.
:param api_version: the API version to use ('1')
:param kwargs: keyword args containing credentials, either:
* os_auth_token: pre-existing token to re-use
* endpoint: CloudKitty API endpoint
* os_token: pre-existing token to re-use
* os_endpoint: Cloudkitty API endpoint
or:
* os_username: name of user
* os_password: user's password
* os_user_id: user's id
* os_user_domain_id: the domain id of the user
* os_user_domain_name: the domain name of the user
* os_project_id: the user project id
* os_tenant_id: V2 alternative to os_project_id
* os_project_name: the user project name
* os_tenant_name: V2 alternative to os_project_name
* os_project_domain_name: domain name for the user project
* os_project_domain_id: domain id for the user project
* os_auth_url: endpoint to authenticate against
* os_tenant_name: name of tenant
* os_cert|os_cacert: path of CA TLS certificate
* os_key: SSL private key
* insecure: allow insecure SSL (no cert verification)
"""
endpoint = kwargs.get('os_endpoint')
cli_kwargs = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'tenant_id': kwargs.get('os_tenant_id'),
'tenant_name': kwargs.get('os_tenant_name'),
'token': kwargs.get('os_auth_token'),
'auth_url': kwargs.get('os_auth_url'),
'endpoint': kwargs.get('cloudkitty_url')
'region_name': kwargs.get('os_region_name'),
'service_type': kwargs.get('os_service_type'),
'endpoint_type': kwargs.get('os_endpoint_type'),
'cacert': kwargs.get('os_cacert'),
'cert_file': kwargs.get('os_cert'),
'key_file': kwargs.get('os_key'),
'token': kwargs.get('os_token') or kwargs.get('os_auth_token'),
'user_domain_name': kwargs.get('os_user_domain_name'),
'user_domain_id': kwargs.get('os_user_domain_id'),
'project_domain_name': kwargs.get('os_project_domain_name'),
'project_domain_id': kwargs.get('os_project_domain_id'),
}
return Client(api_version, **cli_kwargs)
cli_kwargs.update(kwargs)
cli_kwargs.update(_adjust_params(cli_kwargs))
return Client(version, endpoint, **cli_kwargs)
def Client(version, **kwargs):
module = importutils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
keystone_auth = ks_auth.KeystoneAuthPlugin(
def get_auth_plugin(endpoint, **kwargs):
auth_plugin = AuthPlugin(
auth_url=kwargs.get('auth_url'),
service_type=kwargs.get('service_type'),
token=kwargs.get('token'),
endpoint_type=kwargs.get('endpoint_type'),
cacert=kwargs.get('cacert'),
tenant_id=kwargs.get('project_id') or kwargs.get('tenant_id'),
endpoint=endpoint,
username=kwargs.get('username'),
password=kwargs.get('password'),
tenant_name=kwargs.get('tenant_name'),
token=kwargs.get('token'),
auth_url=kwargs.get('auth_url'),
endpoint=kwargs.get('endpoint'))
http_client = client.HTTPClient(keystone_auth)
return client_class(http_client)
user_domain_name=kwargs.get('user_domain_name'),
user_domain_id=kwargs.get('user_domain_id'),
project_domain_name=kwargs.get('project_domain_name'),
project_domain_id=kwargs.get('project_domain_id')
)
return auth_plugin

View File

@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
"""
Keystone auth plugin.
"""
from keystoneclient.v2_0 import client as ksclient
from cloudkittyclient.openstack.common.apiclient import auth
from cloudkittyclient.openstack.common.apiclient import exceptions
class KeystoneAuthPlugin(auth.BaseAuthPlugin):
opt_names = [
"username",
"password",
"tenant_name",
"token",
"auth_url",
"endpoint"
]
def _do_authenticate(self, http_client):
if self.opts.get('token') is None:
ks_kwargs = {
'username': self.opts.get('username'),
'password': self.opts.get('password'),
'tenant_name': self.opts.get('tenant_name'),
'auth_url': self.opts.get('auth_url'),
}
self._ksclient = ksclient.Client(**ks_kwargs)
def token_and_endpoint(self, endpoint_type, service_type):
token = endpoint = None
if self.opts.get('token') and self.opts.get('endpoint'):
token = self.opts.get('token')
endpoint = self.opts.get('endpoint')
elif hasattr(self, '_ksclient'):
token = self._ksclient.auth_token
endpoint = (self.opts.get('endpoint') or
self._ksclient.service_catalog.url_for(
service_type=service_type,
endpoint_type=endpoint_type))
return (token, endpoint)
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
if self.opts.get('token'):
lookup_table = ["token", "endpoint"]
else:
lookup_table = ["username", "password", "tenant_name", "auth_url"]
missing = [opt
for opt in lookup_table
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)

View File

@ -0,0 +1,173 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2015 Objectif Libre
# 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 copy
from six.moves.urllib import parse
from cloudkittyclient import exc
from cloudkittyclient.openstack.common.apiclient import base
def getid(obj):
"""Extracts object ID.
Abstracts the common pattern of allowing both an object or an
object's ID (UUID) as a parameter when dealing with relationships.
"""
try:
return obj.id
except AttributeError:
return obj
class Manager(object):
"""Managers interact with a particular type of API.
It works with samples, meters, alarms, etc. and provide CRUD operations for
them.
"""
resource_class = None
def __init__(self, api):
self.api = api
@property
def client(self):
"""Compatible with latest oslo-incubator.apiclient code."""
return self.api
def _create(self, url, body):
body = self.api.post(url, json=body).json()
if body:
return self.resource_class(self, body)
def _list(self, url, response_key=None, obj_class=None, body=None,
expect_single=False):
resp = self.api.get(url)
if not resp.content:
raise exc.HTTPNotFound
body = resp.json()
if obj_class is None:
obj_class = self.resource_class
if response_key:
try:
data = body[response_key]
except KeyError:
return []
else:
data = body
if expect_single:
data = [data]
return [obj_class(self, res, loaded=True) for res in data if res]
def _update(self, url, item, response_key=None):
if not item.dirty_fields:
return item
item = self.api.put(url, json=item.dirty_fields).json()
# PUT requests may not return a item
if item:
return self.resource_class(self, item)
def _delete(self, url):
self.api.delete(url)
class CrudManager(base.CrudManager):
"""A CrudManager that automatically gets its base URL."""
base_url = None
def build_url(self, base_url=None, **kwargs):
base_url = base_url or self.base_url
return super(CrudManager, self).build_url(base_url, **kwargs)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs))
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs), kwargs)
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
return self._put(
self.build_url(**kwargs), params)
def findall(self, base_url=None, **kwargs):
"""Find multiple items 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 exc.NotFound(404, msg)
return rl
class Resource(base.Resource):
"""A resource represents a particular instance of an object.
Resource might be tenant, user, etc.
This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
key = None
def to_dict(self):
return copy.deepcopy(self._info)
@property
def dirty_fields(self):
out = self.to_dict()
for k, v in self._info.items():
if self.__dict__[k] != v:
out[k] = self.__dict__[k]
return out
def update(self):
try:
return self.manager.update(**self.dirty_fields)
except AttributeError:
raise exc.NotUpdatableError(self)

View File

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
OpenStack Client interface. Handles the REST calls and responses.
Override the oslo-incubator one.
"""
import logging
import time
from cloudkittyclient.common import exceptions
from cloudkittyclient.openstack.common.apiclient import client
_logger = logging.getLogger(__name__)
class HTTPClient(client.HTTPClient):
"""This client handles sending HTTP requests to OpenStack servers.
[Overrider]
"""
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.get("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

View File

@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Exception definitions.
See cloudkittyclient.openstack.common.apiclient.exceptions.
"""
from cloudkittyclient.openstack.common.apiclient.exceptions import *
# _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):
if isinstance(body.get("error"), dict):
error = body["error"]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
elif "faultstring" in body and "faultcode" in body:
# WSME
kwargs["message"] = "%(faultcode)s: %(faultstring)s" % body
kwargs["details"] = body.get("debuginfo", "")
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,211 @@
# Copyright 2012 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 __future__ import print_function
import datetime
import sys
import textwrap
import uuid
from oslo.serialization import jsonutils
from oslo.utils import encodeutils
from oslo.utils import importutils
import prettytable
import six
from cloudkittyclient import exc
from cloudkittyclient.openstack.common import cliutils
def import_versioned_module(version, submodule=None):
module = 'cloudkittyclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
if 'help' in kwargs:
if 'default' in kwargs:
kwargs['help'] += " Defaults to %s." % kwargs['default']
required = kwargs.get('required', False)
if required:
kwargs['help'] += " Required."
# Because of the sematics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
def _make_default_formatter(field):
return lambda o: getattr(o, field, '')
new_formatters = {}
for field, field_label in six.moves.zip(fields, field_labels):
if field in formatters:
new_formatters[field_label] = formatters[field]
else:
new_formatters[field_label] = _make_default_formatter(field)
cliutils.print_list(objs, field_labels,
formatters=new_formatters,
sortby_index=sortby)
def nested_list_of_dict_formatter(field, column_names):
# (TMaddox) Because the formatting scheme actually drops the whole object
# into the formatter, rather than just the specified field, we have to
# extract it and then pass the value.
return lambda o: format_nested_list_of_dict(getattr(o, field),
column_names)
def format_nested_list_of_dict(l, column_names):
pt = prettytable.PrettyTable(caching=False, print_empty=False,
header=True, hrules=prettytable.FRAME,
field_names=column_names)
for d in l:
pt.add_row(list(map(lambda k: d[k], column_names)))
return pt.get_string()
def print_dict(d, dict_property="Property", wrap=0):
pt = prettytable.PrettyTable([dict_property, 'Value'], print_empty=False)
pt.align = 'l'
for k, v in sorted(six.iteritems(d)):
# convert dict to str to check length
if isinstance(v, dict):
v = jsonutils.dumps(v)
# 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:
if wrap > 0:
line = textwrap.fill(str(line), wrap)
pt.add_row([col1, line])
col1 = ''
else:
if wrap > 0:
v = textwrap.fill(str(v), wrap)
pt.add_row([k, v])
encoded = encodeutils.safe_encode(pt.get_string())
# FIXME(gordc): https://bugs.launchpad.net/oslo-incubator/+bug/1370710
if six.PY3:
encoded = encoded.decode()
print(encoded)
def find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
except exc.HTTPNotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id)
except (ValueError, exc.HTTPNotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exc.HTTPNotFound:
msg = ("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))
raise exc.CommandError(msg)
def args_array_to_dict(kwargs, key_to_convert):
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = dict(v.split("=", 1)
for v in values_to_convert)
except ValueError:
raise exc.CommandError(
'%s must be a list of key=value not "%s"' % (
key_to_convert, values_to_convert))
return kwargs
def args_array_to_list_of_dicts(kwargs, key_to_convert):
"""Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}]."""
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = []
for lst in values_to_convert:
pairs = lst.split(";")
dct = dict()
for pair in pairs:
kv = pair.split("=", 1)
dct[kv[0]] = kv[1].strip(" \"'") # strip spaces and quotes
kwargs[key_to_convert].append(dct)
except Exception:
raise exc.CommandError(
'%s must be a list of key1=value1;key2=value2;... not "%s"' % (
key_to_convert, values_to_convert))
return kwargs
def key_with_slash_to_nested_dict(kwargs):
nested_kwargs = {}
for k in list(kwargs):
keys = k.split('/', 1)
if len(keys) == 2:
nested_kwargs.setdefault(keys[0], {})[keys[1]] = kwargs[k]
del kwargs[k]
kwargs.update(nested_kwargs)
return kwargs
def merge_nested_dict(dest, source, depth=0):
for (key, value) in six.iteritems(source):
if isinstance(value, dict) and depth:
merge_nested_dict(dest[key], value,
depth=(depth - 1))
else:
dest[key] = value
def ts2dt(timestamp):
"""timestamp to datetime format."""
if not isinstance(timestamp, float):
timestamp = float(timestamp)
return datetime.datetime.utcfromtimestamp(timestamp)
def exit(msg=''):
if msg:
print(msg, file=sys.stderr)
sys.exit(1)

121
cloudkittyclient/exc.py Normal file
View File

@ -0,0 +1,121 @@
# 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
import sys
class BaseException(Exception):
"""An error occurred."""
def __init__(self, message=None):
self.message = message
def __str__(self):
return self.message or self.__class__.__doc__
class CommandError(BaseException):
"""Invalid usage of CLI."""
class InvalidEndpoint(BaseException):
"""The provided endpoint is invalid."""
class CommunicationError(BaseException):
"""Unable to communicate with server."""
class NotUpdatableError(BaseException):
"""This Resource is not updatable."""
def __init__(self, resource):
message = "%s is not updatable" % resource
super(BaseException, self).__init__(message)
class HTTPException(BaseException):
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'
def __init__(self, details=None):
self.details = details
def __str__(self):
try:
data = json.loads(self.details)
message = data.get("error_message", {}).get("faultstring")
if message:
return "%s (HTTP %s) ERROR %s" % (
self.__class__.__name__, self.code, message)
except (ValueError, TypeError, AttributeError):
pass
return "%s (HTTP %s)" % (self.__class__.__name__, self.code)
class HTTPBadRequest(HTTPException):
code = 400
class HTTPUnauthorized(HTTPException):
code = 401
class HTTPForbidden(HTTPException):
code = 403
class HTTPNotFound(HTTPException):
code = 404
class HTTPMethodNotAllowed(HTTPException):
code = 405
class HTTPConflict(HTTPException):
code = 409
class HTTPOverLimit(HTTPException):
code = 413
class HTTPInternalServerError(HTTPException):
code = 500
class HTTPNotImplemented(HTTPException):
code = 501
class HTTPBadGateway(HTTPException):
code = 502
class HTTPServiceUnavailable(HTTPException):
code = 503
# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
# classes
_code_map = {}
for obj_name in dir(sys.modules[__name__]):
if obj_name.startswith('HTTP'):
obj = getattr(sys.modules[__name__], obj_name)
_code_map[obj.code] = obj
def from_response(response, details=None):
"""Return an instance of an HTTPException based on httplib response."""
cls = _code_map.get(response.status, HTTPException)
return cls(details)

View File

@ -1,17 +0,0 @@
#
# 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
six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))

View File

@ -0,0 +1,45 @@
# 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.
See http://docs.openstack.org/developer/oslo.i18n/usage.html
"""
try:
import oslo.i18n
# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
# application name when this module is synced into the separate
# repository. It is OK to have more than one translation function
# using the same domain, since there will still only be one message
# catalog.
_translators = oslo.i18n.TranslatorFactory(domain='cloudkittyclient')
# 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
except ImportError:
# NOTE(dims): Support for cases where a project wants to use
# code from oslo-incubator, but is not ready to be internationalized
# (like tempest)
_ = _LI = _LW = _LE = _LC = lambda x: x

View File

@ -17,6 +17,19 @@
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
import abc
import argparse
import os

View File

@ -20,18 +20,32 @@
Base utilities to build API operation managers and objects on top of.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# 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 cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
from cloudkittyclient.openstack.common.gettextutils import _
from cloudkittyclient.openstack.common import strutils
def getid(obj):
@ -99,12 +113,13 @@ class BaseManager(HookableMixin):
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key, obj_class=None, json=None):
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'
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
@ -118,7 +133,7 @@ class BaseManager(HookableMixin):
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
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:
@ -128,15 +143,17 @@ class BaseManager(HookableMixin):
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
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'
e.g., 'server'. If response_key is None - all response body
will be used.
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], loaded=True)
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.
@ -146,21 +163,23 @@ class BaseManager(HookableMixin):
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key, return_raw=False):
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., 'servers'
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 body[response_key]
return self.resource_class(self, body[response_key])
return data
return self.resource_class(self, data)
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
@ -169,7 +188,8 @@ class BaseManager(HookableMixin):
: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'
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
@ -187,7 +207,8 @@ class BaseManager(HookableMixin):
: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'
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:
@ -488,6 +509,8 @@ class Resource(object):
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
self._add_details(
{'x_request_id': self.manager.client.last_request_id})
def __eq__(self, other):
if not isinstance(other, Resource):

View File

@ -25,6 +25,7 @@ OpenStack Client interface. Handles the REST calls and responses.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import hashlib
import logging
import time
@ -33,14 +34,15 @@ try:
except ImportError:
import json
from oslo.utils import encodeutils
from oslo.utils import importutils
import requests
from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
from cloudkittyclient.openstack.common.gettextutils import _
from cloudkittyclient.openstack.common import importutils
_logger = logging.getLogger(__name__)
SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
class HTTPClient(object):
@ -98,19 +100,32 @@ class HTTPClient(object):
self.http = http or requests.Session()
self.cached_token = None
self.last_request_id = None
def _safe_header(self, name, value):
if name in SENSITIVE_HEADERS:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return encodeutils.safe_decode(name), "{SHA1}%s" % d
else:
return (encodeutils.safe_decode(name),
encodeutils.safe_decode(value))
def _http_log_req(self, method, url, kwargs):
if not self.debug:
return
string_parts = [
"curl -i",
"curl -g -i",
"-X '%s'" % method,
"'%s'" % url,
]
for element in kwargs['headers']:
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
header = ("-H '%s: %s'" %
self._safe_header(element, kwargs['headers'][element]))
string_parts.append(header)
_logger.debug("REQ: %s" % " ".join(string_parts))
@ -156,7 +171,7 @@ class HTTPClient(object):
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", kwargs.get("headers", {}))
kwargs.setdefault("headers", {})
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
@ -177,6 +192,8 @@ class HTTPClient(object):
start_time, time.time()))
self._http_log_resp(resp)
self.last_request_id = resp.headers.get('x-openstack-request-id')
if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
@ -247,6 +264,10 @@ class HTTPClient(object):
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(
@ -323,6 +344,10 @@ class BaseClient(object):
return self.http_client.client_request(
self, method, url, **kwargs)
@property
def last_request_id(self):
return self.http_client.last_request_id
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)

View File

@ -20,12 +20,25 @@
Exception definitions.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-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 cloudkittyclient.openstack.common.gettextutils import _
from cloudkittyclient.openstack.common._i18n import _
class ClientException(Exception):
@ -34,14 +47,6 @@ class ClientException(Exception):
pass
class MissingArgs(ClientException):
"""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)
class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
@ -62,11 +67,16 @@ class AuthorizationFailure(ClientException):
pass
class ConnectionRefused(ClientException):
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):
@ -80,7 +90,7 @@ class AuthSystemNotFound(AuthorizationFailure):
"""User has specified an AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
_("AuthSystemNotFound: %s") % repr(auth_system))
_("AuthSystemNotFound: %r") % auth_system)
self.auth_system = auth_system
@ -103,7 +113,7 @@ class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
_("AmbiguousEndpoints: %s") % repr(endpoints))
_("AmbiguousEndpoints: %r") % endpoints)
self.endpoints = endpoints
@ -447,10 +457,13 @@ def from_response(response, method, url):
except ValueError:
pass
else:
if isinstance(body, dict) and isinstance(body.get("error"), dict):
error = body["error"]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
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

View File

@ -21,6 +21,19 @@ wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
@ -33,7 +46,9 @@ from six.moves.urllib import parse
from cloudkittyclient.openstack.common.apiclient import client
def assert_has_keys(dct, required=[], optional=[]):
def assert_has_keys(dct, required=None, optional=None):
required = required or []
optional = optional or []
for k in required:
try:
assert k in dct
@ -166,6 +181,8 @@ class FakeHTTPClient(client.HTTPClient):
else:
status, body = resp
headers = {}
self.last_request_id = headers.get('x-openstack-request-id',
'req-test')
return TestResponse({
"status_code": status,
"text": body,

View File

@ -0,0 +1,100 @@
#
# 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.
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
from oslo.utils import encodeutils
from oslo.utils import uuidutils
import six
from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.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 cloudkittyclient.openstack.common._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

@ -1,479 +0,0 @@
# Copyright 2012 Red Hat, Inc.
# Copyright 2013 IBM Corp.
# 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.
"""
gettext for openstack-common modules.
Usual usage in an openstack.common module:
from cloudkittyclient.openstack.common.gettextutils import _
"""
import copy
import gettext
import locale
from logging import handlers
import os
from babel import localedata
import six
_AVAILABLE_LANGUAGES = {}
# FIXME(dhellmann): Remove this when moving to oslo.i18n.
USE_LAZY = False
class TranslatorFactory(object):
"""Create translator functions
"""
def __init__(self, domain, localedir=None):
"""Establish a set of translation functions for the domain.
:param domain: Name of translation domain,
specifying a message catalog.
:type domain: str
:param lazy: Delays translation until a message is emitted.
Defaults to False.
:type lazy: Boolean
:param localedir: Directory with translation catalogs.
:type localedir: str
"""
self.domain = domain
if localedir is None:
localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
self.localedir = localedir
def _make_translation_func(self, domain=None):
"""Return a new translation function ready for use.
Takes into account whether or not lazy translation is being
done.
The domain can be specified to override the default from the
factory, but the localedir from the factory is always used
because we assume the log-level translation catalogs are
installed in the same directory as the main application
catalog.
"""
if domain is None:
domain = self.domain
t = gettext.translation(domain,
localedir=self.localedir,
fallback=True)
# Use the appropriate method of the translation object based
# on the python version.
m = t.gettext if six.PY3 else t.ugettext
def f(msg):
"""oslo.i18n.gettextutils translation function."""
if USE_LAZY:
return Message(msg, domain=domain)
return m(msg)
return f
@property
def primary(self):
"The default translation function."
return self._make_translation_func()
def _make_log_translation_func(self, level):
return self._make_translation_func(self.domain + '-log-' + level)
@property
def log_info(self):
"Translate info-level log messages."
return self._make_log_translation_func('info')
@property
def log_warning(self):
"Translate warning-level log messages."
return self._make_log_translation_func('warning')
@property
def log_error(self):
"Translate error-level log messages."
return self._make_log_translation_func('error')
@property
def log_critical(self):
"Translate critical-level log messages."
return self._make_log_translation_func('critical')
# NOTE(dhellmann): When this module moves out of the incubator into
# oslo.i18n, these global variables can be moved to an integration
# module within each application.
# Create the global translation functions.
_translators = TranslatorFactory('cloudkittyclient')
# 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
# NOTE(dhellmann): End of globals that will move to the application's
# integration module.
def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
Call this at the start of execution to enable the gettextutils._
function to use lazy gettext functionality. This is useful if
your project is importing _ directly instead of using the
gettextutils.install() way of importing the _ function.
"""
global USE_LAZY
USE_LAZY = True
def install(domain):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
install() function.
The main difference from gettext.install() is that we allow
overriding the default localedir (e.g. /usr/share/locale) using
a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR).
Note that to enable lazy translation, enable_lazy must be
called.
:param domain: the translation domain
"""
from six import moves
tf = TranslatorFactory(domain)
moves.builtins.__dict__['_'] = tf.primary
class Message(six.text_type):
"""A Message object is a unicode object that can be translated.
Translation of Message is done explicitly using the translate() method.
For all non-translation intents and purposes, a Message is simply unicode,
and can be treated as such.
"""
def __new__(cls, msgid, msgtext=None, params=None,
domain='cloudkittyclient', *args):
"""Create a new Message object.
In order for translation to work gettext requires a message ID, this
msgid will be used as the base unicode text. It is also possible
for the msgid and the base unicode text to be different by passing
the msgtext parameter.
"""
# If the base msgtext is not given, we use the default translation
# of the msgid (which is in English) just in case the system locale is
# not English, so that the base text will be in that locale by default.
if not msgtext:
msgtext = Message._translate_msgid(msgid, domain)
# We want to initialize the parent unicode with the actual object that
# would have been plain unicode if 'Message' was not enabled.
msg = super(Message, cls).__new__(cls, msgtext)
msg.msgid = msgid
msg.domain = domain
msg.params = params
return msg
def translate(self, desired_locale=None):
"""Translate this message to the desired locale.
:param desired_locale: The desired locale to translate the message to,
if no locale is provided the message will be
translated to the system's default locale.
:returns: the translated message in unicode
"""
translated_message = Message._translate_msgid(self.msgid,
self.domain,
desired_locale)
if self.params is None:
# No need for more translation
return translated_message
# This Message object may have been formatted with one or more
# Message objects as substitution arguments, given either as a single
# argument, part of a tuple, or as one or more values in a dictionary.
# When translating this Message we need to translate those Messages too
translated_params = _translate_args(self.params, desired_locale)
translated_message = translated_message % translated_params
return translated_message
@staticmethod
def _translate_msgid(msgid, domain, desired_locale=None):
if not desired_locale:
system_locale = locale.getdefaultlocale()
# If the system locale is not available to the runtime use English
if not system_locale[0]:
desired_locale = 'en_US'
else:
desired_locale = system_locale[0]
locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
lang = gettext.translation(domain,
localedir=locale_dir,
languages=[desired_locale],
fallback=True)
if six.PY3:
translator = lang.gettext
else:
translator = lang.ugettext
translated_message = translator(msgid)
return translated_message
def __mod__(self, other):
# When we mod a Message we want the actual operation to be performed
# by the parent class (i.e. unicode()), the only thing we do here is
# save the original msgid and the parameters in case of a translation
params = self._sanitize_mod_params(other)
unicode_mod = super(Message, self).__mod__(params)
modded = Message(self.msgid,
msgtext=unicode_mod,
params=params,
domain=self.domain)
return modded
def _sanitize_mod_params(self, other):
"""Sanitize the object being modded with this Message.
- Add support for modding 'None' so translation supports it
- Trim the modded object, which can be a large dictionary, to only
those keys that would actually be used in a translation
- Snapshot the object being modded, in case the message is
translated, it will be used as it was when the Message was created
"""
if other is None:
params = (other,)
elif isinstance(other, dict):
# Merge the dictionaries
# Copy each item in case one does not support deep copy.
params = {}
if isinstance(self.params, dict):
for key, val in self.params.items():
params[key] = self._copy_param(val)
for key, val in other.items():
params[key] = self._copy_param(val)
else:
params = self._copy_param(other)
return params
def _copy_param(self, param):
try:
return copy.deepcopy(param)
except Exception:
# Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied
return six.text_type(param)
def __add__(self, other):
msg = _('Message objects do not support addition.')
raise TypeError(msg)
def __radd__(self, other):
return self.__add__(other)
if six.PY2:
def __str__(self):
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
# and it expects specifically a UnicodeError in order to proceed.
msg = _('Message objects do not support str() because they may '
'contain non-ascii characters. '
'Please use unicode() or translate() instead.')
raise UnicodeError(msg)
def get_available_languages(domain):
"""Lists the available languages for the given translation domain.
:param domain: the domain to get languages for
"""
if domain in _AVAILABLE_LANGUAGES:
return copy.copy(_AVAILABLE_LANGUAGES[domain])
localedir = '%s_LOCALEDIR' % domain.upper()
find = lambda x: gettext.find(domain,
localedir=os.environ.get(localedir),
languages=[x])
# NOTE(mrodden): en_US should always be available (and first in case
# order matters) since our in-line message strings are en_US
language_list = ['en_US']
# NOTE(luisg): Babel <1.0 used a function called list(), which was
# renamed to locale_identifiers() in >=1.0, the requirements master list
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
# this check when the master list updates to >=1.0, and update all projects
list_identifiers = (getattr(localedata, 'list', None) or
getattr(localedata, 'locale_identifiers'))
locale_identifiers = list_identifiers()
for i in locale_identifiers:
if find(i) is not None:
language_list.append(i)
# NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
# locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
# are perfectly legitimate locales:
# https://github.com/mitsuhiko/babel/issues/37
# In Babel 1.3 they fixed the bug and they support these locales, but
# they are still not explicitly "listed" by locale_identifiers().
# That is why we add the locales here explicitly if necessary so that
# they are listed as supported.
aliases = {'zh': 'zh_CN',
'zh_Hant_HK': 'zh_HK',
'zh_Hant': 'zh_TW',
'fil': 'tl_PH'}
for (locale_, alias) in six.iteritems(aliases):
if locale_ in language_list and alias not in language_list:
language_list.append(alias)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
def translate(obj, desired_locale=None):
"""Gets the translated unicode representation of the given object.
If the object is not translatable it is returned as-is.
If the locale is None the object is translated to the system locale.
:param obj: the object to translate
:param desired_locale: the locale to translate the message to, if None the
default system locale will be used
:returns: the translated object in unicode, or the original object if
it could not be translated
"""
message = obj
if not isinstance(message, Message):
# If the object to translate is not already translatable,
# let's first get its unicode representation
message = six.text_type(obj)
if isinstance(message, Message):
# Even after unicoding() we still need to check if we are
# running with translatable unicode before translating
return message.translate(desired_locale)
return obj
def _translate_args(args, desired_locale=None):
"""Translates all the translatable elements of the given arguments object.
This method is used for translating the translatable values in method
arguments which include values of tuples or dictionaries.
If the object is not a tuple or a dictionary the object itself is
translated if it is translatable.
If the locale is None the object is translated to the system locale.
:param args: the args to translate
:param desired_locale: the locale to translate the args to, if None the
default system locale will be used
:returns: a new args object with the translated contents of the original
"""
if isinstance(args, tuple):
return tuple(translate(v, desired_locale) for v in args)
if isinstance(args, dict):
translated_dict = {}
for (k, v) in six.iteritems(args):
translated_v = translate(v, desired_locale)
translated_dict[k] = translated_v
return translated_dict
return translate(args, desired_locale)
class TranslationHandler(handlers.MemoryHandler):
"""Handler that translates records before logging them.
The TranslationHandler takes a locale and a target logging.Handler object
to forward LogRecord objects to after translating them. This handler
depends on Message objects being logged, instead of regular strings.
The handler can be configured declaratively in the logging.conf as follows:
[handlers]
keys = translatedlog, translator
[handler_translatedlog]
class = handlers.WatchedFileHandler
args = ('/var/log/api-localized.log',)
formatter = context
[handler_translator]
class = openstack.common.log.TranslationHandler
target = translatedlog
args = ('zh_CN',)
If the specified locale is not available in the system, the handler will
log in the default locale.
"""
def __init__(self, locale=None, target=None):
"""Initialize a TranslationHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
# NOTE(luisg): In order to allow this handler to be a wrapper for
# other handlers, such as a FileHandler, and still be able to
# configure it using logging.conf, this handler has to extend
# MemoryHandler because only the MemoryHandlers' logging.conf
# parsing is implemented such that it accepts a target handler.
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
self.locale = locale
def setFormatter(self, fmt):
self.target.setFormatter(fmt)
def emit(self, record):
# We save the message from the original record to restore it
# after translation, so other handlers are not affected by this
original_msg = record.msg
original_args = record.args
try:
self._translate_and_log_record(record)
finally:
record.msg = original_msg
record.args = original_args
def _translate_and_log_record(self, record):
record.msg = translate(record.msg, self.locale)
# In addition to translating the message, we also need to translate
# arguments that were passed to the log method that were not part
# of the main message e.g., log.info(_('Some message %s'), this_one))
record.args = _translate_args(record.args, self.locale)
self.target.emit(record)

View File

@ -1,73 +0,0 @@
# 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.
"""
Import related utilities and helper functions.
"""
import sys
import traceback
def import_class(import_str):
"""Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_str)
try:
return getattr(sys.modules[mod_str], class_str)
except AttributeError:
raise ImportError('Class %s cannot be found (%s)' %
(class_str,
traceback.format_exception(*sys.exc_info())))
def import_object(import_str, *args, **kwargs):
"""Import a class and return an instance of it."""
return import_class(import_str)(*args, **kwargs)
def import_object_ns(name_space, import_str, *args, **kwargs):
"""Tries to import object from default namespace.
Imports a class and return an instance of it, first by trying
to find the class in a default namespace, then failing back to
a full path if not found in the default namespace.
"""
import_value = "%s.%s" % (name_space, import_str)
try:
return import_class(import_value)(*args, **kwargs)
except ImportError:
return import_class(import_str)(*args, **kwargs)
def import_module(import_str):
"""Import a module."""
__import__(import_str)
return sys.modules[import_str]
def import_versioned_module(version, submodule=None):
module = 'cloudkittyclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return import_module(module)
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try:
return import_module(import_str)
except ImportError:
return default

View File

@ -1,295 +0,0 @@
# 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.
"""
System-level utilities and helper functions.
"""
import math
import re
import sys
import unicodedata
import six
from cloudkittyclient.openstack.common.gettextutils import _
UNIT_PREFIX_EXPONENT = {
'k': 1,
'K': 1,
'Ki': 1,
'M': 2,
'Mi': 2,
'G': 3,
'Gi': 3,
'T': 4,
'Ti': 4,
}
UNIT_SYSTEM_INFO = {
'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')),
'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')),
}
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
# NOTE(flaper87): The following 3 globals are used by `mask_password`
_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
# NOTE(ldbragst): Let's build a list of regex objects using the list of
# _SANITIZE_KEYS we already have. This way, we only have to add the new key
# to the list of _SANITIZE_KEYS and we can generate regular expressions
# for XML and JSON automatically.
_SANITIZE_PATTERNS = []
_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
r'(<%(key)s>).*?(</%(key)s>)',
r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])',
r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?[\'"])'
'.*?([\'"])',
r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)']
for key in _SANITIZE_KEYS:
for pattern in _FORMAT_PATTERNS:
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
_SANITIZE_PATTERNS.append(reg_ex)
def int_from_bool_as_string(subject):
"""Interpret a string as a boolean and return either 1 or 0.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject, strict=False, default=False):
"""Interpret a string as a boolean.
A case-insensitive match is performed such that strings matching 't',
'true', 'on', 'y', 'yes', or '1' are considered True and, when
`strict=False`, anything else returns the value specified by 'default'.
Useful for JSON-decoded stuff and config file parsing.
If `strict=True`, unrecognized values, including None, will raise a
ValueError which is useful when parsing values passed in from an API call.
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
"""
if not isinstance(subject, six.string_types):
subject = six.text_type(subject)
lowered = subject.strip().lower()
if lowered in TRUE_STRINGS:
return True
elif lowered in FALSE_STRINGS:
return False
elif strict:
acceptable = ', '.join(
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
msg = _("Unrecognized value '%(val)s', acceptable values are:"
" %(acceptable)s") % {'val': subject,
'acceptable': acceptable}
raise ValueError(msg)
else:
return default
def safe_decode(text, incoming=None, errors='strict'):
"""Decodes incoming text/bytes string using `incoming` if they're not
already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, six.text_type):
return text
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
try:
return text.decode(incoming, errors)
except UnicodeDecodeError:
# Note(flaper87) If we get here, it means that
# sys.stdin.encoding / sys.getdefaultencoding
# didn't return a suitable encoding to decode
# text. This happens mostly when global LANG
# var is not set correctly and there's no
# default encoding. In this case, most likely
# python will use ASCII or ANSI encoders as
# default encodings but they won't be capable
# of decoding non-ASCII characters.
#
# Also, UTF-8 is being used since it's an ASCII
# extension.
return text.decode('utf-8', errors)
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""Encodes incoming text/bytes string using `encoding`.
If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`)
:param incoming: Text's current encoding
:param encoding: Expected encoding for text (Default UTF-8)
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a bytestring `encoding` encoded
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
if isinstance(text, six.text_type):
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
return text.encode(encoding, errors)
else:
return text
def string_to_bytes(text, unit_system='IEC', return_int=False):
"""Converts a string into an float representation of bytes.
The units supported for IEC ::
Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
KB, KiB, MB, MiB, GB, GiB, TB, TiB
The units supported for SI ::
kb(it), Mb(it), Gb(it), Tb(it)
kB, MB, GB, TB
Note that the SI unit system does not support capital letter 'K'
:param text: String input for bytes size conversion.
:param unit_system: Unit system for byte size conversion.
:param return_int: If True, returns integer representation of text
in bytes. (default: decimal)
:returns: Numerical representation of text in bytes.
:raises ValueError: If text has an invalid value.
"""
try:
base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
except KeyError:
msg = _('Invalid unit system: "%s"') % unit_system
raise ValueError(msg)
match = reg_ex.match(text)
if match:
magnitude = float(match.group(1))
unit_prefix = match.group(2)
if match.group(3) in ['b', 'bit']:
magnitude /= 8
else:
msg = _('Invalid string format: %s') % text
raise ValueError(msg)
if not unit_prefix:
res = magnitude
else:
res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
if return_int:
return int(math.ceil(res))
return res
def to_slug(value, incoming=None, errors="strict"):
"""Normalize string.
Convert to lowercase, remove non-word characters, and convert spaces
to hyphens.
Inspired by Django's `slugify` filter.
:param value: Text to slugify
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: slugified unicode representation of `value`
:raises TypeError: If text is not an instance of str
"""
value = safe_decode(value, incoming, errors)
# NOTE(aababilov): no need to use safe_(encode|decode) here:
# encodings are always "ascii", error handling is always "ignore"
# and types are always known (first: unicode; second: str)
value = unicodedata.normalize("NFKD", value).encode(
"ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value)
def mask_password(message, secret="***"):
"""Replace password with 'secret' in message.
:param message: The string which includes security information.
:param secret: value with which to replace passwords.
:returns: The unicode value of message with the password fields masked.
For example:
>>> mask_password("'adminPass' : 'aaaaa'")
"'adminPass' : '***'"
>>> mask_password("'admin_pass' : 'aaaaa'")
"'admin_pass' : '***'"
>>> mask_password('"password" : "aaaaa"')
'"password" : "***"'
>>> mask_password("'original_password' : 'aaaaa'")
"'original_password' : '***'"
>>> mask_password("u'original_password' : u'aaaaa'")
"u'original_password' : u'***'"
"""
message = six.text_type(message)
# NOTE(ldbragst): Check to see if anything in message contains any key
# specified in _SANITIZE_KEYS, if not then just return the message since
# we don't have to mask any passwords.
if not any(key in message for key in _SANITIZE_KEYS):
return message
secret = r'\g<1>' + secret + r'\g<2>'
for pattern in _SANITIZE_PATTERNS:
message = re.sub(pattern, secret, message)
return message

322
cloudkittyclient/shell.py Normal file
View File

@ -0,0 +1,322 @@
# Copyright 2015 Objectif Libre
# 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.
"""
Command-line interface to the OpenStack Cloudkitty API.
"""
from __future__ import print_function
import argparse
import logging
import sys
from oslo.utils import encodeutils
import six
from stevedore import extension
import cloudkittyclient
from cloudkittyclient import client as ckclient
from cloudkittyclient.common import utils
from cloudkittyclient import exc
from cloudkittyclient.openstack.common import cliutils
from cloudkittyclient.v1.report import shell as report_shell
SUBMODULES_NAMESPACE = 'cloudkitty.client.modules'
def _positive_non_zero_int(argument_value):
if argument_value is None:
return None
try:
value = int(argument_value)
except ValueError:
msg = "%s must be an integer" % argument_value
raise argparse.ArgumentTypeError(msg)
if value <= 0:
msg = "%s must be greater than 0" % argument_value
raise argparse.ArgumentTypeError(msg)
return value
class CloudkittyShell(object):
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='cloudkitty',
description=__doc__.strip(),
epilog='See "cloudkitty help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=HelpFormatter,
)
# Global arguments
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument('--version',
action='version',
version=cloudkittyclient.__version__)
parser.add_argument('-d', '--debug',
default=bool(cliutils.env('CLOUDKITTYCLIENT_DEBUG')
),
action='store_true',
help='Defaults to env[CLOUDKITTYCLIENT_DEBUG].')
parser.add_argument('-v', '--verbose',
default=False, action="store_true",
help="Print more verbose output.")
parser.add_argument('--timeout',
default=600,
type=_positive_non_zero_int,
help='Number of seconds to wait for a response.')
parser.add_argument('--cloudkitty-url', metavar='<CLOUDKITTY_URL>',
dest='os_endpoint',
default=cliutils.env('CLOUDKITTY_URL'),
help=("DEPRECATED, use --os-endpoint instead. "
"Defaults to env[CLOUDKITTY_URL]."))
parser.add_argument('--cloudkitty_url',
dest='os_endpoint',
help=argparse.SUPPRESS)
parser.add_argument('--cloudkitty-api-version',
default=cliutils.env(
'CLOUDKITTY_API_VERSION', default='1'),
help='Defaults to env[CLOUDKITTY_API_VERSION] '
'or 1.')
parser.add_argument('--cloudkitty_api_version',
help=argparse.SUPPRESS)
self.auth_plugin.add_opts(parser)
self.auth_plugin.add_common_opts(parser)
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = utils.import_versioned_module(version, 'shell')
self._find_actions(subparsers, submodule)
self._find_actions(subparsers, report_shell)
extensions = extension.ExtensionManager(
SUBMODULES_NAMESPACE,
)
for ext in extensions:
shell = ext.plugin.get_shell()
self._find_actions(subparsers, shell)
self._find_actions(subparsers, self)
self._add_bash_completion_subparser(subparsers)
return parser
def _add_bash_completion_subparser(self, subparsers):
subparser = subparsers.add_parser(
'bash_completion',
add_help=False,
formatter_class=HelpFormatter
)
self.subcommands['bash_completion'] = subparser
subparser.set_defaults(func=self.do_bash_completion)
def _find_actions(self, subparsers, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
subparser = subparsers.add_parser(command, help=help,
description=desc,
add_help=False,
formatter_class=HelpFormatter)
subparser.add_argument('-h', '--help', action='help',
help=argparse.SUPPRESS)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
@staticmethod
def _setup_logging(debug):
format = '%(levelname)s (%(module)s) %(message)s'
if debug:
logging.basicConfig(format=format, level=logging.DEBUG)
else:
logging.basicConfig(format=format, level=logging.WARN)
logging.getLogger('iso8601').setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
def parse_args(self, argv):
# Parse args once to find version
self.auth_plugin = ckclient.AuthPlugin()
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.auth_plugin.parse_opts(options)
self._setup_logging(options.debug)
# build available subcommands based on version
api_version = options.cloudkitty_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
self.parser = subcommand_parser
# Handle top-level --help/-h before attempting to parse
# a command off the command line
if options.help or not argv:
self.do_help(options)
return 0
# Return parsed args
return api_version, subcommand_parser.parse_args(argv)
@staticmethod
def no_project_and_domain_set(args):
if not (args.os_project_id or (args.os_project_name and
(args.os_user_domain_name or args.os_user_domain_id)) or
(args.os_tenant_id or args.os_tenant_name)):
return True
else:
return False
def main(self, argv):
parsed = self.parse_args(argv)
if parsed == 0:
return 0
api_version, args = parsed
# Short-circuit and deal with help command right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0
if not ((self.auth_plugin.opts.get('token')
or self.auth_plugin.opts.get('auth_token'))
and self.auth_plugin.opts['endpoint']):
if not self.auth_plugin.opts['username']:
raise exc.CommandError("You must provide a username via "
"either --os-username or via "
"env[OS_USERNAME]")
if not self.auth_plugin.opts['password']:
raise exc.CommandError("You must provide a password via "
"either --os-password or via "
"env[OS_PASSWORD]")
if self.no_project_and_domain_set(args):
# steer users towards Keystone V3 API
raise exc.CommandError("You must provide a project_id via "
"either --os-project-id or via "
"env[OS_PROJECT_ID] and "
"a domain_name via either "
"--os-user-domain-name or via "
"env[OS_USER_DOMAIN_NAME] or "
"a domain_id via either "
"--os-user-domain-id or via "
"env[OS_USER_DOMAIN_ID]")
if not (self.auth_plugin.opts['tenant_id']
or self.auth_plugin.opts['tenant_name']):
raise exc.CommandError("You must provide a tenant_id via "
"either --os-tenant-id or via "
"env[OS_TENANT_ID]")
if not self.auth_plugin.opts['auth_url']:
raise exc.CommandError("You must provide an auth url via "
"either --os-auth-url or via "
"env[OS_AUTH_URL]")
client_kwargs = vars(args)
client_kwargs.update(self.auth_plugin.opts)
client_kwargs['auth_plugin'] = self.auth_plugin
client = ckclient.get_client(api_version, **client_kwargs)
# call whatever callback was selected
try:
args.func(client, args)
except exc.HTTPUnauthorized:
raise exc.CommandError("Invalid OpenStack Identity credentials.")
def do_bash_completion(self, args):
"""Prints all of the commands and options to stdout.
The cloudkitty.bash_completion script doesn't have to hard code them.
"""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
commands.add(sc_str)
for option in list(sc._optionals._option_string_actions):
options.add(option)
commands.remove('bash-completion')
commands.remove('bash_completion')
print(' '.join(commands | options))
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""Display help about this program or one of its subcommands."""
if getattr(args, 'command', None):
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
self.parser.print_help()
class HelpFormatter(argparse.HelpFormatter):
def __init__(self, prog, indent_increment=2, max_help_position=32,
width=None):
super(HelpFormatter, self).__init__(prog, indent_increment,
max_help_position, width)
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(HelpFormatter, self).start_section(heading)
def main(args=None):
try:
if args is None:
args = sys.argv[1:]
CloudkittyShell().main(args)
except Exception as e:
if '--debug' in args or '-d' in args:
raise
else:
print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("Stopping Cloudkitty Client", file=sys.stderr)
sys.exit(130)
if __name__ == "__main__":
main()

View File

@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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
@ -12,15 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import fixtures
from oslotest import base as test
from oslotest import base
class TestCase(test.BaseTestCase):
class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests."""
def setUp(self):
"""Run before each test method to initialize test environment."""
super(TestCase, self).setUp()
self.log_fixture = self.useFixture(fixtures.FakeLogger())

View File

@ -1,94 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
"""
Tests for the Keystone auth plugin
`cloudkittyclient.common.KeystoneAuthPluginTest`.
"""
from keystoneclient.v2_0 import client as ksclient
import mock
from cloudkittyclient.common import auth
from cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.openstack.common.apiclient import exceptions
from cloudkittyclient.tests import base
@mock.patch.object(ksclient, 'Client')
class KeystoneAuthPluginTest(base.TestCase):
def setUp(self):
super(KeystoneAuthPluginTest, self).setUp()
plugin = auth.KeystoneAuthPlugin(
username="fake-username",
password="fake-password",
tenant_name="fake-tenant-name",
auth_url="http://auth:5000",
endpoint="http://cloudkitty:8888")
self.cs = client.HTTPClient(auth_plugin=plugin)
def test_authenticate(self, mock_ksclient):
self.cs.authenticate()
mock_ksclient.assert_called_with(
username="fake-username",
password="fake-password",
tenant_name="fake-tenant-name",
auth_url="http://auth:5000")
def test_token_and_endpoint(self, mock_ksclient):
self.cs.authenticate()
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
"fake-endpoint-type", "fake-service-type")
self.assertIsInstance(token, mock.MagicMock)
self.assertEqual("http://cloudkitty:8888", endpoint)
def test_token_and_endpoint_before_auth(self, mock_ksclient):
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
"fake-endpoint-type", "fake-service-type")
self.assertIsNone(token, None)
self.assertIsNone(endpoint, None)
def test_sufficient_options_missing_tenant_name(self, mock_ksclient):
plugin = auth.KeystoneAuthPlugin(
username="fake-username",
password="fake-password",
auth_url="http://auth:5000",
endpoint="http://cloudkitty:8888")
cs = client.HTTPClient(auth_plugin=plugin)
self.assertRaises(exceptions.AuthPluginOptionsMissing,
cs.authenticate)
@mock.patch.object(ksclient, 'Client')
class KeystoneAuthPluginTokenTest(base.TestCase):
def test_token_and_endpoint(self, mock_ksclient):
plugin = auth.KeystoneAuthPlugin(
token="fake-token",
endpoint="http://cloudkitty:8888")
cs = client.HTTPClient(auth_plugin=plugin)
cs.authenticate()
(token, endpoint) = cs.auth_plugin.token_and_endpoint(
"fake-endpoint-type", "fake-service-type")
self.assertEqual('fake-token', token)
self.assertEqual('http://cloudkitty:8888', endpoint)
def test_sufficient_options_missing_endpoint(self, mock_ksclient):
plugin = auth.KeystoneAuthPlugin(
token="fake-token")
cs = client.HTTPClient(auth_plugin=plugin)
self.assertRaises(exceptions.AuthPluginOptionsMissing,
cs.authenticate)

View File

@ -0,0 +1,64 @@
# 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 keystoneclient.v2_0 import client as ksclient
def script_keystone_client():
ksclient.Client(auth_url='http://no.where',
insecure=False,
password='password',
tenant_id='',
tenant_name='tenant_name',
username='username').AndReturn(FakeKeystone('abcd1234'))
def fake_headers():
return {'X-Auth-Token': 'abcd1234',
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'python-cloudkittyclient'}
class FakeServiceCatalog(object):
@staticmethod
def url_for(endpoint_type, service_type):
return 'http://192.168.1.5:8004/v1/f14b41234'
class FakeKeystone(object):
service_catalog = FakeServiceCatalog()
def __init__(self, auth_token):
self.auth_token = auth_token
class FakeHTTPResponse(object):
version = 1.1
def __init__(self, status, reason, headers, body):
self.headers = headers
self.body = body
self.status = status
self.reason = reason
def getheader(self, name, default=None):
return self.headers.get(name, default)
def getheaders(self):
return self.headers.items()
def read(self, amt=None):
b = self.body
self.body = None
return b

View File

@ -1,45 +1,148 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
# Copyright 2015 Objectif Libre
# 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.
#
# @author: François Magimel (linkid)
# 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.
"""
Tests for the main Client interface `cloudkittyclient.client`.
"""
import types
import mock
import six
from cloudkittyclient import client
from cloudkittyclient.common import auth
from cloudkittyclient.tests import base
from cloudkittyclient.tests import fakes
from cloudkittyclient.tests import utils
from cloudkittyclient.v1 import client as v1client
VERSIONS = {
'1': v1client,
FAKE_ENV = {
'username': 'username',
'password': 'password',
'tenant_name': 'tenant_name',
'auth_url': 'http://no.where',
'os_endpoint': 'http://no.where',
'auth_plugin': 'fake_auth',
'token': '1234',
'user_domain_name': 'default',
'project_domain_name': 'default',
}
class ClientTest(base.TestCase):
def test_client_unsupported_version(self):
self.assertRaises(ImportError, client.Client,
'111.11', **{})
class ClientTest(utils.BaseTestCase):
def test_client(self):
for (version, instance) in six.iteritems(VERSIONS):
with mock.patch.object(auth, 'KeystoneAuthPlugin'):
c = client.Client(version, **{})
self.assertIsInstance(c, instance.Client)
@staticmethod
def create_client(env, api_version=1, endpoint=None, exclude=[]):
env = dict((k, v) for k, v in env.items()
if k not in exclude)
return client.get_client(api_version, **env)
def setUp(self):
super(ClientTest, self).setUp()
def test_client_version(self):
c1 = self.create_client(env=FAKE_ENV, api_version=1)
self.assertIsInstance(c1, v1client.Client)
def test_client_auth_lambda(self):
env = FAKE_ENV.copy()
env['token'] = lambda: env['token']
self.assertIsInstance(env['token'],
types.FunctionType)
c1 = self.create_client(env)
self.assertIsInstance(c1, v1client.Client)
def test_client_auth_non_lambda(self):
env = FAKE_ENV.copy()
env['token'] = "1234"
self.assertIsInstance(env['token'], str)
c1 = self.create_client(env)
self.assertIsInstance(c1, v1client.Client)
@mock.patch('keystoneclient.v2_0.client', fakes.FakeKeystone)
def test_client_without_auth_plugin(self):
env = FAKE_ENV.copy()
del env['auth_plugin']
c = self.create_client(env, api_version=1, endpoint='fake_endpoint')
self.assertIsInstance(c.auth_plugin, client.AuthPlugin)
def test_client_without_auth_plugin_keystone_v3(self):
env = FAKE_ENV.copy()
del env['auth_plugin']
expected = {
'username': 'username',
'endpoint': 'http://no.where',
'tenant_name': 'tenant_name',
'service_type': None,
'token': '1234',
'endpoint_type': None,
'auth_url': 'http://no.where',
'tenant_id': None,
'cacert': None,
'password': 'password',
'user_domain_name': 'default',
'user_domain_id': None,
'project_domain_name': 'default',
'project_domain_id': None,
}
with mock.patch('cloudkittyclient.client.AuthPlugin') as auth_plugin:
self.create_client(env, api_version=1)
auth_plugin.assert_called_with(**expected)
def test_client_with_auth_plugin(self):
c = self.create_client(FAKE_ENV, api_version=1)
self.assertIsInstance(c.auth_plugin, str)
def test_v1_client_timeout_invalid_value(self):
env = FAKE_ENV.copy()
env['timeout'] = 'abc'
self.assertRaises(ValueError, self.create_client, env)
env['timeout'] = '1.5'
self.assertRaises(ValueError, self.create_client, env)
def _test_v1_client_timeout_integer(self, timeout, expected_value):
env = FAKE_ENV.copy()
env['timeout'] = timeout
expected = {
'auth_plugin': 'fake_auth',
'timeout': expected_value,
'original_ip': None,
'http': None,
'region_name': None,
'verify': True,
'timings': None,
'keyring_saver': None,
'cert': None,
'endpoint_type': None,
'user_agent': None,
'debug': None,
}
cls = 'cloudkittyclient.openstack.common.apiclient.client.HTTPClient'
with mock.patch(cls) as mocked:
self.create_client(env)
mocked.assert_called_with(**expected)
def test_v1_client_timeout_zero(self):
self._test_v1_client_timeout_integer(0, None)
def test_v1_client_timeout_valid_value(self):
self._test_v1_client_timeout_integer(30, 30)
def test_v1_client_cacert_in_verify(self):
env = FAKE_ENV.copy()
env['cacert'] = '/path/to/cacert'
client = self.create_client(env)
self.assertEqual('/path/to/cacert', client.client.verify)
def test_v1_client_certfile_and_keyfile(self):
env = FAKE_ENV.copy()
env['cert_file'] = '/path/to/cert'
env['key_file'] = '/path/to/keycert'
client = self.create_client(env)
self.assertEqual(('/path/to/cert', '/path/to/keycert'),
client.client.cert)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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
@ -13,20 +12,17 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo import i18n
"""
test_cloudkittyclient
----------------------------------
_translators = i18n.TranslatorFactory(domain='cloudkittyclient')
i18n.enable_lazy()
Tests for `cloudkittyclient` module.
"""
# The primary translation function using the well-known name "_"
_ = _translators.primary
from cloudkittyclient.tests import base
# 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
class TestCloudkittyclient(base.TestCase):
def test_something(self):
pass

View File

@ -0,0 +1,24 @@
# Copyright 2012 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.
import fixtures
import testtools
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
self.useFixture(fixtures.FakeLogger())

View File

@ -1,115 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Tests for the manager billing.modules `cloudkittyclient.v1.billing.modules`.
"""
import json
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import base
from cloudkittyclient.v1.billing import modules
from cloudkittyclient.v1 import client
modules_test = ["noop", "test"]
info_module = {
"enabled": True,
"name": "test",
"hot-config": True,
"description": "Test description"
}
fixtures_list_modules = {
'/v1/billing/modules': {
'GET': (
{},
json.dumps(modules_test)
),
}
}
fixtures_get_one_module = {
'/v1/billing/modules/test': {
'GET': (
{},
json.dumps(info_module)
),
}
}
fixtures_get_status_module = {
'/v1/billing/modules/test/enabled': {
'GET': (
{},
json.dumps(str(info_module['enabled']))
),
}
}
fixtures_put_status_module = {
'/v1/billing/modules/test/enabled': {
'PUT': (
{},
json.dumps(str(False))
),
}
}
class ModulesManagerTest(base.TestCase):
def connect_client(self, fixtures):
"""Returns the manager."""
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
api_client = client.Client(fake_http_client)
return modules.ModulesManager(api_client)
def test_list_modules(self):
mgr = self.connect_client(fixtures_list_modules)
modules_expected = [
modules.Module(modules.ModulesManager, module)
for module in modules_test
]
self.assertEqual(modules_expected, mgr.list())
def test_get_one_module(self):
mgr = self.connect_client(fixtures_get_one_module)
module_expected = modules.ExtensionSummary(
modules.ModulesManager, info_module)
module_get = mgr.get(module_id='test')
self.assertIn('ExtensionSummary', repr(module_get))
self.assertEqual(module_expected, module_get)
self.assertEqual(module_expected.enabled, module_get.enabled)
self.assertEqual('test', module_get.name)
self.assertEqual(getattr(module_expected, 'hot-config'),
getattr(module_get, 'hot-config'))
self.assertEqual(module_expected.description, module_get.description)
def test_get_status_module(self):
mgr = self.connect_client(fixtures_get_status_module)
module_status_expected = info_module['enabled']
module_status_get = mgr.get_status(module_id='test')
self.assertIn('Module', repr(module_status_get))
self.assertIn('test', repr(module_status_get))
self.assertEqual(str(module_status_expected), module_status_get.id)
def test_update_status_module(self):
mgr = self.connect_client(fixtures_put_status_module)
module_status_put = mgr.update(module_id='test', enabled=False)
self.assertEqual('False', module_status_put.id)

View File

@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Tests for the manager billing.quote `cloudkittyclient.v1.billing.quote`.
"""
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import base
from cloudkittyclient.v1.billing import quote
from cloudkittyclient.v1 import client
compute = {
'desc': {
'image_id': "a41fba37-2429-4f15-aa00-b5bc4bf557bf",
},
'service': "compute",
'volume': 1
}
fixtures = {
'/v1/billing/quote': {
'POST': (
{},
'4.2'
),
}
}
class QuoteManagerTest(base.TestCase):
def setUp(self):
super(QuoteManagerTest, self).setUp()
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
api_client = client.Client(fake_http_client)
self.mgr = quote.QuoteManager(api_client)
def test_post(self):
_quote = self.mgr.post(json=compute)
self.assertIn('Quote', repr(_quote))
self.assertEqual(4.2, _quote.price)
def test_post_raw(self):
_quote = self.mgr.post(json=compute, return_raw=True)
self.assertEqual(4.2, _quote)

View File

@ -0,0 +1,139 @@
# Copyright 2015 Objectif Libre
# 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 cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import utils
import cloudkittyclient.v1.core
fixtures = {
'/v1/billing/modules': {
'GET': (
{},
{'modules': [
{
'module_id': 'hashmap',
'enabled': True,
},
{
'module_id': 'noop',
'enabled': False,
},
]},
),
},
'/v1/billing/modules/hashmap': {
'GET': (
{},
{
'module_id': 'hashmap',
'enabled': True,
}
),
'PUT': (
{},
{
'module_id': 'hashmap',
'enabled': False,
}
),
},
'/v1/billing/modules/noop': {
'GET': (
{},
{
'module_id': 'noop',
'enabled': False,
}
),
'PUT': (
{},
{
'module_id': 'noop',
'enabled': True,
}
),
},
'/v1/collectors': {
'GET': (
{},
{'collectors': [
{
'module_id': 'ceilo',
'enabled': True,
},
]},
),
},
}
class CloudkittyModuleManagerTest(utils.BaseTestCase):
def setUp(self):
super(CloudkittyModuleManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = cloudkittyclient.v1.core.CloudkittyModuleManager(self.api)
def test_list_all(self):
resources = list(self.mgr.list())
expect = [
'GET', '/v1/billing/modules'
]
self.http_client.assert_called(*expect)
self.assertEqual(len(resources), 2)
self.assertEqual(resources[0].module_id, 'hashmap')
self.assertEqual(resources[1].module_id, 'noop')
def test_get_module_status(self):
resource = self.mgr.get(module_id='hashmap')
expect = [
'GET', '/v1/billing/modules/hashmap'
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.module_id, 'hashmap')
self.assertEqual(resource.enabled, True)
class CloudkittyModuleTest(utils.BaseTestCase):
def setUp(self):
super(CloudkittyModuleTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = cloudkittyclient.v1.core.CloudkittyModuleManager(self.api)
def test_enable(self):
self.ck_module = self.mgr.get(module_id='noop')
self.ck_module.enable()
# PUT /v1/billing/modules/noop
# body : {'enabled': True}
expect = [
'PUT', '/v1/billing/modules/noop', {'module_id': 'noop',
'enabled': True},
]
self.http_client.assert_called(*expect)
def test_disable(self):
self.ck_module = self.mgr.get(module_id='hashmap')
self.ck_module.disable()
# PUT /v1/billing/modules/hashmap
# body : {'enabled': False}
expect = [
'PUT', '/v1/billing/modules/hashmap', {'module_id': 'hashmap',
'enabled': False},
]
self.http_client.assert_called(*expect)

View File

@ -0,0 +1,425 @@
# Copyright 2015 Objectif Libre
# 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 cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import utils
from cloudkittyclient.v1.billing import hashmap
fixtures = {
# services
'/v1/billing/module_config/hashmap/services': {
'GET': (
{},
{'services':
[
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'compute'
},
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd6',
'name': 'volume'
},
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd7',
'name': 'network'
},
],
}
),
},
# a service
('/v1/billing/module_config/hashmap/services/'
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'compute',
}
),
'DELETE': (
{},
{},
),
},
# a field
('/v1/billing/module_config/hashmap/fields/'
'a53db546-bac0-472c-be4b-5bf9f6117581'): {
'GET': (
{},
{
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'flavor',
},
),
'PUT': (
{},
{},
),
},
('/v1/billing/module_config/hashmap/fields'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{'fields': [
{
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'flavor',
},
{
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117582',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'LOLOL',
},
]
},
),
'PUT': (
{},
{},
),
},
# a mapping
('/v1/billing/module_config/hashmap/mappings/'
'bff0d209-a8e4-46f8-8c1a-f231db375dcb'): {
'GET': (
{},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'group_id': None,
'value': 'm1.small',
'cost': 0.50,
'type': 'flat',
},
),
'PUT': (
{},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'group_id': None,
'value': 'm1.small',
'cost': 0.20,
'type': 'flat',
},
),
},
# some mappings
('/v1/billing/module_config/hashmap/mappings'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{'mappings':
[
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': None,
'group_id': None,
'value': 'm1.small',
'cost': 0.50,
'type': 'flat',
},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcc',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': None,
'group_id': None,
'value': 'm1.tiny',
'cost': 1.10,
'type': 'flat',
},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcd',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': None,
'group_id': None,
'value': 'm1.big',
'cost': 1.50,
'type': 'flat',
},
],
}
),
'PUT': (
{},
{},
),
},
'/v1/billing/module_config/hashmap/groups': {
'GET': (
{},
{'groups':
[
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'object_consumption'
},
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd6',
'name': 'compute_instance'
},
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd7',
'name': 'netowrking'
},
],
}
),
},
('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'object_consumption'
},
),
'DELETE': (
{},
{},
),
},
('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5?recursive=True'): {
'DELETE': (
{},
{},
),
},
}
class ServiceManagerTest(utils.BaseTestCase):
def setUp(self):
super(ServiceManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.ServiceManager(self.api)
def test_list_services(self):
resources = list(self.mgr.list())
expect = [
'GET', '/v1/billing/module_config/hashmap/services'
]
self.http_client.assert_called(*expect)
self.assertEqual(len(resources), 3)
self.assertEqual(
resources[0].service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.assertEqual(resources[0].name, 'compute')
self.assertEqual(resources[1].name, 'volume')
self.assertEqual(resources[2].name, 'network')
def test_get_a_service(self):
resource = self.mgr.get(
service_id='2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
expect = [
'GET', ('/v1/billing/module_config/hashmap/services/'
'2451c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5')
self.assertEqual(resource.name, 'compute')
class ServiceTest(utils.BaseTestCase):
def setUp(self):
super(ServiceTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.ServiceManager(self.api)
self.resource = self.mgr.get(
service_id='2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
def test_get_fields(self):
fields = self.resource.fields[:]
expect = [
'GET', ('/v1/billing/module_config/hashmap/fields'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'),
]
self.http_client.assert_called(*expect)
self.assertEqual(len(fields), 2)
def test_get_mappings(self):
mappings = self.resource.mappings[:]
expect = [
'GET', ('/v1/billing/module_config/hashmap/mappings'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'),
]
self.http_client.assert_called(*expect)
self.assertEqual(len(mappings), 3)
class FieldManagerTest(utils.BaseTestCase):
def setUp(self):
super(FieldManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.FieldManager(self.api)
def test_get_a_field(self):
resource = self.mgr.get(
field_id='a53db546-bac0-472c-be4b-5bf9f6117581'
)
expect = [
'GET', ('/v1/billing/module_config/hashmap/fields/'
'a53db546-bac0-472c-be4b-5bf9f6117581')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.field_id,
'a53db546-bac0-472c-be4b-5bf9f6117581')
self.assertEqual(
resource.service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.assertEqual(resource.name, 'flavor')
class MappingManagerTest(utils.BaseTestCase):
def setUp(self):
super(MappingManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.MappingManager(self.api)
def test_get_a_mapping(self):
resource = self.mgr.get(
mapping_id='bff0d209-a8e4-46f8-8c1a-f231db375dcb'
)
expect = [
'GET', ('/v1/billing/module_config/hashmap/mappings/'
'bff0d209-a8e4-46f8-8c1a-f231db375dcb')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.mapping_id,
'bff0d209-a8e4-46f8-8c1a-f231db375dcb')
self.assertEqual(
resource.service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.assertEqual(
resource.field_id,
'a53db546-bac0-472c-be4b-5bf9f6117581'
)
self.assertEqual(resource.value, 'm1.small')
self.assertEqual(resource.cost, 0.5)
def test_update_a_mapping(self):
resource = self.mgr.get(
mapping_id='bff0d209-a8e4-46f8-8c1a-f231db375dcb'
)
resource.cost = 0.2
self.mgr.update(**resource.dirty_fields)
expect = [
'PUT', ('/v1/billing/module_config/hashmap/mappings/'
'bff0d209-a8e4-46f8-8c1a-f231db375dcb'),
{u'mapping_id': u'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
u'cost': 0.2, u'type': u'flat',
u'service_id': u'2451c2e0-2c6b-4e75-987f-93661eef0fd5',
u'field_id': u'a53db546-bac0-472c-be4b-5bf9f6117581',
u'value': u'm1.small'}
]
self.http_client.assert_called(*expect)
class GroupManagerTest(utils.BaseTestCase):
def setUp(self):
super(GroupManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.GroupManager(self.api)
def test_get_a_group(self):
resource = self.mgr.get(
group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
)
expect = [
'GET', ('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.group_id,
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
self.assertEqual(resource.name, 'object_consumption')
def test_delete_a_group(self):
self.mgr.delete(group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
expect = [
'DELETE', ('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
def test_delete_a_group_recursively(self):
self.mgr.delete(group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5',
recursive=True)
expect = [
'DELETE', ('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5?recursive=True')
]
self.http_client.assert_called(*expect)
class GroupTest(utils.BaseTestCase):
def setUp(self):
super(GroupTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.GroupManager(self.api)
def test_delete(self):
self.group = self.mgr.get(
group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.group.delete()
# DELETE /v1/billing/groups/aaa1c2e0-2c6b-4e75-987f-93661eef0fd5
expect = [
'DELETE', ('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
def test_delete_recursive(self):
self.group = self.mgr.get(
group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.group.delete(recursive=True)
# DELETE
# /v1/billing/groups/aaa1c2e0-2c6b-4e75-987f-93661eef0fd5?recusrive=True
expect = [
'DELETE', ('/v1/billing/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
'?recursive=True')
]
self.http_client.assert_called(*expect)

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Tests for the manager report `cloudkittyclient.v1.report`.
"""
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import base
from cloudkittyclient.v1 import client
from cloudkittyclient.v1 import report
fixtures = {
'/v1/report/total': {
'GET': (
{},
'10.0'
),
}
}
class ReportManagerTest(base.TestCase):
def setUp(self):
super(ReportManagerTest, self).setUp()
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
api_client = client.Client(fake_http_client)
self.mgr = report.ReportManager(api_client)
def test_get(self):
_report = self.mgr.get()
self.assertIn('Report', repr(_report))
self.assertEqual(10.0, _report.total)

View File

@ -1,18 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
# Copyright 2015 Objectif Libre
# 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
# 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.
#
# @author: François Magimel (linkid)
# 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 cloudkittyclient.v1.client import Client # noqa
from cloudkittyclient.v1.client import Client # noqa

View File

@ -0,0 +1,112 @@
# Copyright 2015 Objectif Libre
#
# 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 cloudkittyclient.common import base
class Service(base.Resource):
key = 'service'
def __repr__(self):
return "<hashmap.Service %s>" % self._info
@property
def fields(self):
return FieldManager(client=self.manager.client).findall(
service_id=self.service_id
)
@property
def mappings(self):
return MappingManager(client=self.manager.client).findall(
service_id=self.service_id
)
class ServiceManager(base.CrudManager):
resource_class = Service
base_url = '/v1/billing/module_config/hashmap'
key = 'service'
collection_key = 'services'
class Field(base.Resource):
key = 'field'
def __repr__(self):
return "<hashmap.Field %s>" % self._info
@property
def service(self):
return ServiceManager(client=self.manager.client).get(
service_id=self.service_id
)
class FieldManager(base.CrudManager):
resource_class = Field
base_url = '/v1/billing/module_config/hashmap'
key = 'field'
collection_key = 'fields'
class Mapping(base.Resource):
key = 'mapping'
def __repr__(self):
return "<hashmap.Mapping %s>" % self._info
@property
def service(self):
return ServiceManager(client=self.manager.client).get(
service_id=self.service_id
)
@property
def field(self):
if self.field_id is None:
return None
return FieldManager(client=self.manager.client).get(
service_id=self.service_id
)
class MappingManager(base.CrudManager):
resource_class = Mapping
base_url = '/v1/billing/module_config/hashmap'
key = 'mapping'
collection_key = 'mappings'
class Group(base.Resource):
key = 'group'
def __repr__(self):
return "<hashmap.Group %s>" % self._info
def delete(self, recursive=False):
return self.manager.delete(group_id=self.group_id, recursive=recursive)
class GroupManager(base.CrudManager):
resource_class = Group
base_url = '/v1/billing/module_config/hashmap'
key = 'group'
collection_key = 'groups'
def delete(self, group_id, recursive=False):
url = self.build_url(group_id=group_id)
if recursive:
url += "?recursive=True"
return self._delete(url)

View File

@ -0,0 +1,31 @@
# Copyright 2015 Objectif Libre
# 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 cloudkittyclient.v1.billing import hashmap
class Client(object):
"""Client for the Hashmap v1 API.
:param http_client: A http client.
"""
def __init__(self, http_client):
"""Initialize a new client for the Hashmap v1 API."""
self.http_client = http_client
self.services = hashmap.ServiceManager(self.http_client)
self.fields = hashmap.FieldManager(self.http_client)
self.mappings = hashmap.MappingManager(self.http_client)
self.groups = hashmap.GroupManager(self.http_client)

View File

@ -0,0 +1,31 @@
# Copyright 2015 Objectif Libre
# 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 cloudkittyclient.v1.billing.hashmap import client
from cloudkittyclient.v1.billing.hashmap import shell
class Extension(object):
"""Hashmap extension.
"""
@staticmethod
def get_client(http_client):
return client.Client(http_client)
@staticmethod
def get_shell():
return shell

View File

@ -0,0 +1,265 @@
# Copyright 2015 Objectif Libre
# 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 cloudkittyclient.common import utils
from cloudkittyclient import exc
@utils.arg('-n', '--name',
help='Service name',
required=True)
def do_hashmap_service_create(cc, args={}):
"""Create a service."""
arg_to_field_mapping = {
'name': 'name'
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap_services.create(**fields)
utils.print_dict(out.to_dict())
def do_hashmap_service_list(cc, args={}):
"""List services."""
try:
services = cc.hashmap_services.list()
except exc.HTTPNotFound:
raise exc.CommandError('Services not found: %s' % args.counter_name)
else:
field_labels = ['Name', 'Service id']
fields = ['name', 'service_id']
utils.print_list(services, fields, field_labels,
sortby=0)
@utils.arg('-s', '--service-id',
help='Service uuid',
required=True)
def do_hashmap_service_delete(cc, args={}):
"""Delete a service."""
try:
cc.hashmap_services.delete(service_id=args.service_id)
except exc.HTTPNotFound:
raise exc.CommandError('Service not found: %s' % args.counter_name)
@utils.arg('-n', '--name',
help='Field name',
required=True)
@utils.arg('-s', '--service-id',
help='Service id',
required=True)
def do_hashmap_field_create(cc, args={}):
"""Create a field."""
arg_to_field_mapping = {
'name': 'name',
'service_id': 'service_id'
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap_fields.create(**fields)
utils.print_dict(out.to_dict())
@utils.arg('-s', '--service-id',
help='Service id',
required=True)
def do_hashmap_field_list(cc, args={}):
"""Create a field."""
try:
created_field = cc.hashmap_fields.list(service_id=args.service_id)
except exc.HTTPNotFound:
raise exc.CommandError('Fields not found: %s' % args.counter_name)
else:
field_labels = ['Name', 'Field id']
fields = ['name', 'field_id']
utils.print_list(created_field, fields, field_labels,
sortby=0)
@utils.arg('-f', '--field-id',
help='Field uuid',
required=True)
def do_hashmap_field_delete(cc, args={}):
"""Delete a field."""
try:
cc.hashmap_fields.delete(field_id=args.field_id)
except exc.HTTPNotFound:
raise exc.CommandError('Field not found: %s' % args.counter_name)
@utils.arg('-c', '--cost',
help='Mapping cost',
required=True)
@utils.arg('-v', '--value',
help='Mapping value',
required=False)
@utils.arg('-t', '--type',
help='Mapping type (flat, rate)',
required=False)
@utils.arg('-s', '--service-id',
help='Service id',
required=False)
@utils.arg('-f', '--field-id',
help='Field id',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_mapping_create(cc, args={}):
"""Create a ampping."""
arg_to_field_mapping = {
'cost': 'cost',
'value': 'value',
'type': 'type',
'service_id': 'service_id',
'field_id': 'field_id',
'group_id': 'group_id',
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap_mappings.create(**fields)
utils.print_dict(out)
@utils.arg('-m', '--mapping-id',
help='Mapping id',
required=True)
@utils.arg('-c', '--cost',
help='Mapping cost',
required=False)
@utils.arg('-v', '--value',
help='Mapping value',
required=False)
@utils.arg('-t', '--type',
help='Mapping type (flat, rate)',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_mapping_update(cc, args={}):
"""Update a mapping."""
arg_to_field_mapping = {
'mapping_id': 'mapping_id',
'cost': 'cost',
'value': 'value',
'type': 'type',
'group_id': 'group_id',
}
try:
mapping = cc.hashmap_mappings.get(mapping_id=args.mapping_id)
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
setattr(mapping, k, v)
cc.hashmap_mappings.update(**mapping.dirty_fields)
@utils.arg('-s', '--service-id',
help='Service id',
required=False)
@utils.arg('-f', '--field-id',
help='Field id',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_mapping_list(cc, args={}):
"""List mappings."""
if args.service_id is None and args.field_id is None:
raise exc.CommandError("Provide either service-id or field-id")
try:
mappings = cc.hashmap_mappings.list(service_id=args.service_id,
field_id=args.field_id,
group_id=args.group_id)
except exc.HTTPNotFound:
raise exc.CommandError('Mapping not found: %s' % args.counter_name)
else:
field_labels = ['Mapping id', 'Value', 'Cost',
'Type', 'Field id',
'Service id', 'Group id']
fields = ['mapping_id', 'value', 'cost',
'type', 'field_id',
'service_id', 'group_id']
utils.print_list(mappings, fields, field_labels,
sortby=0)
@utils.arg('-m', '--mapping-id',
help='Mapping uuid',
required=True)
def do_hashmap_mapping_delete(cc, args={}):
"""Delete a mapping."""
try:
cc.hashmap_mappings.delete(mapping_id=args.mapping_id)
except exc.HTTPNotFound:
raise exc.CommandError('Mapping not found: %s' % args.mapping_id)
@utils.arg('-n', '--name',
help='Group name',
required=True)
def do_hashmap_group_create(cc, args={}):
"""Create a group."""
arg_to_field_mapping = {
'name': 'name',
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
cc.hashmap_groups.create(**fields)
def do_hashmap_group_list(cc, args={}):
"""List groups."""
try:
groups = cc.hashmap_groups.list()
except exc.HTTPNotFound:
raise exc.CommandError('Mapping not found: %s' % args.counter_name)
else:
field_labels = ['Name',
'Group id']
fields = ['name', 'group_id']
utils.print_list(groups, fields, field_labels,
sortby=0)
@utils.arg('-g', '--group-id',
help='Group uuid',
required=True)
@utils.arg('-r', '--recursive',
help="""Delete the group's mappings""",
required=False,
default=False)
def do_hashmap_group_delete(cc, args={}):
"""Delete a group."""
try:
cc.hashmap_groups.delete(group_id=args.group_id,
recursive=args.recursive)
except exc.HTTPNotFound:
raise exc.CommandError('Group not found: %s' % args.group_id)

View File

@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Modules resource and manager.
"""
from cloudkittyclient.openstack.common.apiclient import base
class ExtensionSummary(base.Resource):
"""A billing extension summary."""
def __repr__(self):
return "<ExtensionSummary %s>" % self.name
@property
def id(self):
return self.name
class Module(base.Resource):
def __repr__(self):
name = self._info
if hasattr(self.manager, 'module_id'):
name = self.manager.module_id
return "<Module %s>" % name
def _add_details(self, info):
pass
@property
def id(self):
name = self._info
return name
class ModulesManager(base.CrudManager):
resource_class = Module
collection_key = 'billing/modules'
key = 'module'
def _list(self, url, response_key, obj_class=None, json=None):
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
# hack
if type(body) == dict:
data = body[response_key]
else:
data = 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 list(self, base_url=None, **kwargs):
"""Get module list in the billing pipeline.
/v1/billing/modules
"""
return super(ModulesManager, self).list(base_url='/v1', **kwargs)
def _get(self, url, response_key=None, obj_class=None):
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
# hack
if response_key is None:
return obj_class(self, body, loaded=True)
else:
return obj_class(self, body[response_key], loaded=True)
def get(self, **kwargs):
"""Get a module.
/v1/billing/module/<module>
"""
kwargs = self._filter_kwargs(kwargs)
self.module_id = kwargs.get('module_id')
return self._get(
url=self.build_url(base_url='/v1', **kwargs),
response_key=None,
obj_class=ExtensionSummary)
def get_status(self, **kwargs):
"""Get the status of a module.
/v1/billing/module/<module>/enabled
"""
kwargs = self._filter_kwargs(kwargs)
self.module_id = kwargs.get('module_id')
return self._get(
url='%(base_url)s/enabled' % {
'base_url': self.build_url(base_url='/v1', **kwargs),
},
response_key=None)
def update(self, **kwargs):
"""Update the status of a module.
/v1/billing/modules/<module>/enabled
"""
kwargs = self._filter_kwargs(kwargs)
self.module_id = kwargs.get('module_id')
return self._put(
url='%(base_url)s/enabled' % { # hack
'base_url': self.build_url(base_url='/v1', **kwargs),
},
json=kwargs.get('enabled'),
response_key=None)

View File

@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Quote resource and manager.
"""
from cloudkittyclient.openstack.common.apiclient import base
class Quote(base.Resource):
"""A resource represents a particular instance of an object (tenant, user,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
def _add_details(self, info):
try:
setattr(self, 'price', info)
except AttributeError:
# In this case we already defined the attribute on the class
pass
class QuoteManager(base.CrudManager):
"""Managers interact with a particular type of API and provide CRUD
operations for them.
"""
resource_class = Quote
collection_key = 'billing/quote'
key = 'quote'
def _post(self, url, json, response_key=None, return_raw=False):
"""Create an object."""
body = self.client.post(url, json=json).json()
if return_raw:
return body
return self.resource_class(self, body)
def post(self, **kwargs):
"""Get the price corresponding to resources attributes."""
kwargs = self._filter_kwargs(kwargs)
return self._post(
url=self.build_url(base_url='/v1', **kwargs),
json=kwargs.get('json'),
return_raw=kwargs.get('return_raw'))

View File

@ -1,42 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
# Copyright 2015 Objectif Libre
# 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
# 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.
#
# @author: François Magimel (linkid)
# 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.
"""
from stevedore import extension
from cloudkittyclient import client as ckclient
from cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.v1.billing import modules
from cloudkittyclient.v1.billing import quote
from cloudkittyclient.v1 import core
from cloudkittyclient.v1 import report
SUBMODULES_NAMESPACE = 'cloudkitty.client.modules'
class Client(client.BaseClient):
"""Client for the Cloudkitty v1 API."""
def __init__(self, http_client, extensions=None):
class Client(object):
"""Client for the Cloudkitty v1 API.
:param string endpoint: A user-supplied endpoint URL for the cloudkitty
service.
:param function token: Provides token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
"""
def __init__(self, *args, **kwargs):
"""Initialize a new client for the Cloudkitty v1 API."""
super(Client, self).__init__(http_client, extensions)
self.auth_plugin = (kwargs.get('auth_plugin')
or ckclient.get_auth_plugin(*args, **kwargs))
self.client = client.HTTPClient(
auth_plugin=self.auth_plugin,
region_name=kwargs.get('region_name'),
endpoint_type=kwargs.get('endpoint_type'),
original_ip=kwargs.get('original_ip'),
verify=kwargs.get('verify'),
cert=kwargs.get('cert'),
timeout=kwargs.get('timeout'),
timings=kwargs.get('timings'),
keyring_saver=kwargs.get('keyring_saver'),
debug=kwargs.get('debug'),
user_agent=kwargs.get('user_agent'),
http=kwargs.get('http')
)
self.billing = Billing(self)
self.report = report.ReportManager(self)
self.http_client = client.BaseClient(self.client)
self.modules = core.CloudkittyModuleManager(self.http_client)
self.reports = report.ReportManager(self.http_client)
self._expose_submodules()
class Billing(object):
def __init__(self, http_client):
self.modules = modules.ModulesManager(http_client)
self.quote = quote.QuoteManager(http_client)
def _expose_submodules(self):
extensions = extension.ExtensionManager(
SUBMODULES_NAMESPACE,
)
for ext in extensions:
client = ext.plugin.get_client(self.http_client)
setattr(self, ext.name, client)

View File

@ -0,0 +1,30 @@
# Copyright 2015 Objectif Libre
#
# 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 cloudkittyclient.common import base
class Collector(base.Resource):
key = 'collector'
def __repr__(self):
return "<Collector %s>" % self._info
class CollectorManager(base.Manager):
resource_class = Collector
base_url = "/v1/billing"
key = "collector"
collection_key = "collectors"

View File

@ -0,0 +1,53 @@
# Copyright 2015 Objectif Libre
#
# 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 cloudkittyclient.common import base
class CloudkittyModule(base.Resource):
key = 'module'
def __repr__(self):
return "<CloudkittyModule %s>" % self._info
def enable(self):
self.enabled = True
self.update()
def disable(self):
self.enabled = False
self.update()
class CloudkittyModuleManager(base.CrudManager):
resource_class = CloudkittyModule
base_url = "/v1/billing"
key = 'module'
collection_key = "modules"
class Collector(base.Resource):
key = 'collector'
def __repr__(self):
return "<Collector %s>" % self._info
class CollectorManager(base.Manager):
resource_class = Collector
base_url = "/v1/billing"
key = "collector"
collection_key = "collectors"

View File

@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# 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.
#
# @author: François Magimel (linkid)
"""
Report resource and manager.
"""
from cloudkittyclient.openstack.common.apiclient import base
class Report(base.Resource):
"""A resource represents a particular instance of an object (tenant, user,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
def _add_details(self, info):
try:
setattr(self, 'total', info)
except AttributeError:
# In this case we already defined the attribute on the class
pass
class ReportManager(base.CrudManager):
"""Managers interact with a particular type of API and provide CRUD
operations for them.
"""
resource_class = Report
collection_key = 'report/total'
key = 'report/total'
def _get(self, url, response_key=None):
body = self.client.get(url).json()
return self.resource_class(self, body, loaded=True)
def get(self, **kwargs):
"""Get the amount to pay for the current month.
/v1/report/total
"""
return super(ReportManager, self).get(base_url='/v1', **kwargs)

View File

@ -0,0 +1,40 @@
# Copyright 2015 Objectif Libre
#
# 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 cloudkittyclient.common import base
class ReportResult(base.Resource):
key = 'report'
def __repr__(self):
return "<Report %s>" % self._info
class ReportManager(base.Manager):
base_url = "/v1/report"
def list_tenants(self):
return self.client.get(self.base_url + "/tenants").json()
def get_total(self, tenant_id, begin=None, end=None):
url = self.base_url + "/total?tenant_id=%s" % tenant_id
filter = [url]
if begin:
filter.append("begin=%s" % begin.isoformat())
if end:
filter.append("end=%s" % end.isoformat())
return self.client.get("&".join(filter)).json()

View File

@ -0,0 +1,42 @@
# Copyright 2015 Objectif Libre
#
# 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 cloudkittyclient.common import utils
def do_report_tenant_list(cc, args):
tenants = cc.reports.list_tenants()
out_table = utils.prettytable.PrettyTable()
out_table.add_column("Tenant UUID", tenants)
print(out_table)
@utils.arg('-t', '--tenant-id',
help='Tenant id',
required=False, dest='total_tenant_id')
@utils.arg('-b', '--begin',
help='Begin timestamp',
required=False)
@utils.arg('-e', '--end',
help='End timestamp',
required=False)
def do_total_get(cc, args):
begin = utils.ts2dt(args.begin) if args.begin else None
end = utils.ts2dt(args.end) if args.end else None
total = cc.reports.get_total(args.total_tenant_id,
begin=begin,
end=end)
utils.print_dict({'Total': total or 0.0})

View File

@ -0,0 +1,66 @@
# Copyright 2015 Objectif Libre
# 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 cloudkittyclient.common import utils
from cloudkittyclient import exc
def do_module_list(cc, args):
'''List the samples for this meters.'''
try:
modules = cc.modules.list()
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
else:
field_labels = ['Module', 'Enabled']
fields = ['module_id', 'enabled']
utils.print_list(modules, fields, field_labels,
sortby=0)
@utils.arg('-n', '--name',
help='Module name',
required=True)
def do_module_enable(cc, args):
'''Enable a module.'''
try:
module = cc.modules.get(module_id=args.name)
module.enable()
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
else:
field_labels = ['Module', 'Enabled']
fields = ['module_id', 'enabled']
modules = [cc.modules.get(module_id=args.name)]
utils.print_list(modules, fields, field_labels,
sortby=0)
@utils.arg('-n', '--name',
help='Module name',
required=True)
def do_module_disable(cc, args):
'''Disable a module.'''
try:
module = cc.modules.get(module_id=args.name)
module.disable()
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
else:
field_labels = ['Module', 'Enabled']
fields = ['module_id', 'enabled']
modules = [cc.modules.get(module_id=args.name)]
utils.print_list(modules, fields, field_labels,
sortby=0)

View File

@ -1,177 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-cloudkittyclient.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-cloudkittyclient.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/python-cloudkittyclient"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-cloudkittyclient"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

252
doc/source/conf.py Normal file → Executable file
View File

@ -3,7 +3,7 @@
# 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,
@ -15,67 +15,30 @@
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
sys.path.insert(0, os.path.abspath('../..'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'oslosphinx',
#'sphinx.ext.intersphinx',
'oslosphinx'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'python-cloudkittyclient'
copyright = u'2014, Objectif Libre'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
copyright = u'2013, OpenStack Foundation'
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
@ -84,196 +47,29 @@ add_function_parentheses = True
# unit titles (such as .. function::).
add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#html_theme = 'default'
html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
(
'index',
'%s.tex' % project,
u'%s Documentation' % project,
u'Objectif Libre',
'manual'
),
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(
'index',
project,
u'%s Documentation' % project,
[u'Objectif Libre'],
1
),
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
'index',
project,
u'%s Documentation' % project,
u'Objectif Libre',
project,
'Python client library for CloudKitty API',
'Miscellaneous'
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
#intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -0,0 +1,4 @@
============
Contributing
============
.. include:: ../../CONTRIBUTING.rst

View File

@ -1,19 +1,20 @@
.. python-cloudkittyclient documentation master file, created by
sphinx-quickstart on Thu Jul 3 17:15:04 2014.
sphinx-quickstart on Tue Jul 9 22:26:36 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to CloudKitty Client's documentation!
=============================================
Welcome to python-cloudkittyclient's documentation!
========================================================
Introduction
============
Contents:
CloudKitty is a PricingAsAService project aimed at translating Ceilometer
metrics to prices.
python-cloudkitty is the Python client library for CloudKitty API.
.. toctree::
:maxdepth: 2
readme
installation
usage
contributing
Indices and tables
==================
@ -21,3 +22,4 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,12 @@
============
Installation
============
At the command line::
$ pip install python-cloudkittyclient
Or, if you have virtualenvwrapper installed::
$ mkvirtualenv python-cloudkittyclient
$ pip install python-cloudkittyclient

1
doc/source/readme.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../../README.rst

7
doc/source/usage.rst Normal file
View File

@ -0,0 +1,7 @@
========
Usage
========
To use python-cloudkittyclient in a project::
import cloudkittyclient

View File

@ -1,8 +1,8 @@
[DEFAULT]
# The list of modules to copy from openstack-common
# The list of modules to copy from oslo-incubator.git
module=apiclient
module=importutils
module=cliutils
# The base module to hold the copy of openstack.common
base=cloudkittyclient

View File

@ -1,5 +1,8 @@
argparse
oslo.i18n
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=0.6,!=0.7,<1.0
Babel>=1.3
python-keystoneclient
six>=1.7.0
stevedore

View File

@ -1,13 +1,12 @@
[metadata]
name = python-cloudkittyclient
summary = Python client library for CloudKitty API
summary = Cloudkittyclient is the api client for the cloudkitty rating project.
description-file =
README.rst
author = Objectif Libre
author-email = francois.magimel@objectif-libre.com
home-page = http://objectif-libre.com
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Environment :: Console
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
@ -15,20 +14,40 @@ classifier =
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
[files]
packages =
cloudkittyclient
[global]
setup-hooks =
pbr.hooks.setup_hook
[entry_points]
console_scripts =
cloudkitty = cloudkittyclient.shell:main
cloudkitty.client.modules =
hashmap = cloudkittyclient.v1.billing.hashmap.extension:Extension
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source
build-dir = doc/build
all_files = 1
[upload_sphinx]
upload-dir = doc/build/html
[compile_catalog]
directory = cloudkittyclient/locale
domain = python-cloudkittyclient
[update_catalog]
domain = python-cloudkittyclient
output_dir = cloudkittyclient/locale
input_file = cloudkittyclient/locale/python-cloudkittyclient.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
output_file = cloudkittyclient/locale/python-cloudkittyclient.pot

27
setup.py Normal file → Executable file
View File

@ -1,29 +1,22 @@
#!/usr/bin/env python
# Copyright 2014 Objectif Libre
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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.
# 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.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

View File

@ -1,12 +1,15 @@
# Hacking already pins down pep8, pyflakes and flake8
hacking>=0.9.1,<0.10
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=0.9.2,<0.10
coverage>=3.6
discover
doc8
fixtures>=0.3.14
mock>=1.0
python-subunit
sphinx>=1.1.2
oslosphinx
oslotest
sphinx>=1.1.2,!=1.2.0,<1.3
oslotest>=1.1.0.0a1
testrepository>=0.0.18
testscenarios>=0.4
testtools>=0.9.34

34
tox.ini
View File

@ -1,38 +1,34 @@
[tox]
envlist = py26,py27,py33,py34,pep8
minversion = 1.6
envlist = py33,py34,py27,pypy,pep8
skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
python setup.py testr --testr-args='{posargs}'
[tox:jenkins]
downloadcache = ~/cache/pip
commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:pep8]
commands = flake8 {posargs}
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:docs]
commands =
doc8 -e .rst README.rst doc/source
python setup.py build_sphinx
[testenv:venv]
commands = {posargs}
commands = python setup.py build_sphinx
[flake8]
# H405 multi line docstring summary not separated with an empty line
# H904 Wrap long lines in parentheses instead of a backslash
# H102 Apache 2.0 license header not found
ignore = H405,H904,H102
# H803 skipped on purpose per list discussion.
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools,./cloudkittyclient/common/exceptions.py
ignore = E123,E125,H803
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build