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:
parent
3d5814ed2c
commit
9ab382643a
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
python-cloudkittyclient Style Commandments
|
||||
===============================================
|
||||
|
||||
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
|
1
LICENSE
1
LICENSE
|
@ -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.
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
include AUTHORS
|
||||
include ChangeLog
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
11
README.rst
11
README.rst
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'))
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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())
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'))
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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)
|
|
@ -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()
|
|
@ -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})
|
|
@ -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)
|
177
doc/makefile
177
doc/makefile
|
@ -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."
|
|
@ -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}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
============
|
||||
Contributing
|
||||
============
|
||||
.. include:: ../../CONTRIBUTING.rst
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
.. include:: ../../README.rst
|
|
@ -0,0 +1,7 @@
|
|||
========
|
||||
Usage
|
||||
========
|
||||
|
||||
To use python-cloudkittyclient in a project::
|
||||
|
||||
import cloudkittyclient
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
41
setup.cfg
41
setup.cfg
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
34
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue