Browse Source

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>
tags/0.4.0
Guillaume Espanel 5 years ago
parent
commit
9ab382643a
67 changed files with 3368 additions and 2350 deletions
  1. +16
    -0
      CONTRIBUTING.rst
  2. +4
    -0
      HACKING.rst
  3. +1
    -0
      LICENSE
  4. +6
    -0
      MANIFEST.in
  5. +1
    -10
      README.rst
  6. +2
    -0
      babel.cfg
  7. +5
    -11
      cloudkittyclient/__init__.py
  8. +290
    -40
      cloudkittyclient/client.py
  9. +0
    -79
      cloudkittyclient/common/auth.py
  10. +173
    -0
      cloudkittyclient/common/base.py
  11. +0
    -76
      cloudkittyclient/common/client.py
  12. +0
    -84
      cloudkittyclient/common/exceptions.py
  13. +211
    -0
      cloudkittyclient/common/utils.py
  14. +121
    -0
      cloudkittyclient/exc.py
  15. +0
    -17
      cloudkittyclient/openstack/common/__init__.py
  16. +45
    -0
      cloudkittyclient/openstack/common/_i18n.py
  17. +13
    -0
      cloudkittyclient/openstack/common/apiclient/auth.py
  18. +37
    -14
      cloudkittyclient/openstack/common/apiclient/base.py
  19. +31
    -6
      cloudkittyclient/openstack/common/apiclient/client.py
  20. +29
    -16
      cloudkittyclient/openstack/common/apiclient/exceptions.py
  21. +18
    -1
      cloudkittyclient/openstack/common/apiclient/fake_client.py
  22. +100
    -0
      cloudkittyclient/openstack/common/apiclient/utils.py
  23. +271
    -0
      cloudkittyclient/openstack/common/cliutils.py
  24. +0
    -479
      cloudkittyclient/openstack/common/gettextutils.py
  25. +0
    -73
      cloudkittyclient/openstack/common/importutils.py
  26. +0
    -295
      cloudkittyclient/openstack/common/strutils.py
  27. +322
    -0
      cloudkittyclient/shell.py
  28. +6
    -9
      cloudkittyclient/tests/base.py
  29. +0
    -0
      cloudkittyclient/tests/common/__init__.py
  30. +0
    -94
      cloudkittyclient/tests/common/test_auth.py
  31. +64
    -0
      cloudkittyclient/tests/fakes.py
  32. +135
    -32
      cloudkittyclient/tests/test_client.py
  33. +12
    -16
      cloudkittyclient/tests/test_cloudkittyclient.py
  34. +24
    -0
      cloudkittyclient/tests/utils.py
  35. +0
    -0
      cloudkittyclient/tests/v1/billing/__init__.py
  36. +0
    -115
      cloudkittyclient/tests/v1/billing/test_modules.py
  37. +0
    -60
      cloudkittyclient/tests/v1/billing/test_quote.py
  38. +139
    -0
      cloudkittyclient/tests/v1/test_core.py
  39. +425
    -0
      cloudkittyclient/tests/v1/test_hashmap.py
  40. +0
    -48
      cloudkittyclient/tests/v1/test_report.py
  41. +12
    -14
      cloudkittyclient/v1/__init__.py
  42. +112
    -0
      cloudkittyclient/v1/billing/hashmap/__init__.py
  43. +31
    -0
      cloudkittyclient/v1/billing/hashmap/client.py
  44. +31
    -0
      cloudkittyclient/v1/billing/hashmap/extension.py
  45. +265
    -0
      cloudkittyclient/v1/billing/hashmap/shell.py
  46. +0
    -136
      cloudkittyclient/v1/billing/modules.py
  47. +0
    -64
      cloudkittyclient/v1/billing/quote.py
  48. +53
    -29
      cloudkittyclient/v1/client.py
  49. +30
    -0
      cloudkittyclient/v1/collector/__init__.py
  50. +53
    -0
      cloudkittyclient/v1/core.py
  51. +0
    -59
      cloudkittyclient/v1/report.py
  52. +40
    -0
      cloudkittyclient/v1/report/__init__.py
  53. +42
    -0
      cloudkittyclient/v1/report/shell.py
  54. +66
    -0
      cloudkittyclient/v1/shell.py
  55. +0
    -177
      doc/makefile
  56. +24
    -228
      doc/source/conf.py
  57. +4
    -0
      doc/source/contributing.rst
  58. +11
    -9
      doc/source/index.rst
  59. +12
    -0
      doc/source/installation.rst
  60. +1
    -0
      doc/source/readme.rst
  61. +7
    -0
      doc/source/usage.rst
  62. +2
    -2
      openstack-common.conf
  63. +6
    -3
      requirements.txt
  64. +30
    -11
      setup.cfg
  65. +10
    -17
      setup.py
  66. +10
    -7
      test-requirements.txt
  67. +15
    -19
      tox.ini

+ 16
- 0
CONTRIBUTING.rst View File

@@ -0,0 +1,16 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in this page:

http://docs.openstack.org/infra/manual/developers.html

Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:

http://docs.openstack.org/infra/manual/developers.html#development-workflow

Pull requests submitted through GitHub will be ignored.

Bugs should be filed on Launchpad, not GitHub:

https://bugs.launchpad.net/cloudkitty

+ 4
- 0
HACKING.rst View File

@@ -0,0 +1,4 @@
python-cloudkittyclient Style Commandments
===============================================

Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/

+ 1
- 0
LICENSE View File

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


+ 6
- 0
MANIFEST.in View File

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

global-exclude *.pyc

+ 1
- 10
README.rst View File

@@ -1,7 +1,7 @@
Python bindings to the CloudKitty API
=====================================

:version: 1.0
:version: 0.2
:Wiki: `CloudKitty Wiki`_
:IRC: #cloudkitty @ freenode

@@ -21,12 +21,3 @@ Status

This project is **highly** work in progress.


Roadmap
=======

* Add some tests.
* Add some doc.
* Move from importutils to stevedore.
* Add a command-line tool.
* Global code improvement.

+ 2
- 0
babel.cfg View File

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


+ 5
- 11
cloudkittyclient/__init__.py View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# Copyright 2015 Objectif Libre
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
@@ -12,15 +12,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: François Magimel (linkid)

__all__ = ['__version__']

import pbr.version

version_info = pbr.version.VersionInfo('python-cloudkittyclient')
try:
__version__ = version_info.version_string()
except AttributeError:
__version__ = None

__version__ = pbr.version.VersionInfo(
'cloudkittyclient').version_string()

+ 290
- 40
cloudkittyclient/client.py View File

@@ -1,66 +1,316 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: François Magimel (linkid)
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

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 utils
from cloudkittyclient import exc
from cloudkittyclient.openstack.common.apiclient import auth
from cloudkittyclient.openstack.common.apiclient import exceptions


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


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

"""
OpenStack Client interface. Handles the REST calls and responses.
"""

from cloudkittyclient.common import auth as ks_auth
from cloudkittyclient.common import client
from cloudkittyclient.openstack.common import importutils
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 get_client(api_version, **kwargs):
"""Get an authenticated client.
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'),
}

This is based on the credentials in the keyword args.
# 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

:param api_version: the API version to use
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

+ 0
- 79
cloudkittyclient/common/auth.py View File

@@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""
Keystone auth plugin.
"""

from keystoneclient.v2_0 import client as ksclient

from cloudkittyclient.openstack.common.apiclient import auth
from cloudkittyclient.openstack.common.apiclient import exceptions


class KeystoneAuthPlugin(auth.BaseAuthPlugin):

opt_names = [
"username",
"password",
"tenant_name",
"token",
"auth_url",
"endpoint"
]

def _do_authenticate(self, http_client):
if self.opts.get('token') is None:
ks_kwargs = {
'username': self.opts.get('username'),
'password': self.opts.get('password'),
'tenant_name': self.opts.get('tenant_name'),
'auth_url': self.opts.get('auth_url'),
}

self._ksclient = ksclient.Client(**ks_kwargs)

def token_and_endpoint(self, endpoint_type, service_type):
token = endpoint = None

if self.opts.get('token') and self.opts.get('endpoint'):
token = self.opts.get('token')
endpoint = self.opts.get('endpoint')

elif hasattr(self, '_ksclient'):
token = self._ksclient.auth_token
endpoint = (self.opts.get('endpoint') or
self._ksclient.service_catalog.url_for(
service_type=service_type,
endpoint_type=endpoint_type))

return (token, endpoint)

def sufficient_options(self):
"""Check if all required options are present.

:raises: AuthPluginOptionsMissing
"""

if self.opts.get('token'):
lookup_table = ["token", "endpoint"]
else:
lookup_table = ["username", "password", "tenant_name", "auth_url"]

missing = [opt
for opt in lookup_table
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)

+ 173
- 0
cloudkittyclient/common/base.py View File

@@ -0,0 +1,173 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""
Base utilities to build API operation managers and objects on top of.
"""

import copy

from six.moves.urllib import parse

from cloudkittyclient import exc
from cloudkittyclient.openstack.common.apiclient import base


def getid(obj):
"""Extracts object ID.

Abstracts the common pattern of allowing both an object or an
object's ID (UUID) as a parameter when dealing with relationships.
"""
try:
return obj.id
except AttributeError:
return obj


class Manager(object):
"""Managers interact with a particular type of API.

It works with samples, meters, alarms, etc. and provide CRUD operations for
them.
"""
resource_class = None

def __init__(self, api):
self.api = api

@property
def client(self):
"""Compatible with latest oslo-incubator.apiclient code."""
return self.api

def _create(self, url, body):
body = self.api.post(url, json=body).json()
if body:
return self.resource_class(self, body)

def _list(self, url, response_key=None, obj_class=None, body=None,
expect_single=False):
resp = self.api.get(url)
if not resp.content:
raise exc.HTTPNotFound
body = resp.json()

if obj_class is None:
obj_class = self.resource_class

if response_key:
try:
data = body[response_key]
except KeyError:
return []
else:
data = body
if expect_single:
data = [data]
return [obj_class(self, res, loaded=True) for res in data if res]

def _update(self, url, item, response_key=None):
if not item.dirty_fields:
return item
item = self.api.put(url, json=item.dirty_fields).json()
# PUT requests may not return a item
if item:
return self.resource_class(self, item)

def _delete(self, url):
self.api.delete(url)


class CrudManager(base.CrudManager):
"""A CrudManager that automatically gets its base URL."""

base_url = None

def build_url(self, base_url=None, **kwargs):
base_url = base_url or self.base_url
return super(CrudManager, self).build_url(base_url, **kwargs)

def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs))

def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs), kwargs)

def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()

return self._put(
self.build_url(**kwargs), params)

def findall(self, base_url=None, **kwargs):
"""Find multiple items with attributes matching ``**kwargs``.

:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)

rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)

if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exc.NotFound(404, msg)
return rl


class Resource(base.Resource):
"""A resource represents a particular instance of an object.

Resource might be tenant, user, etc.
This is pretty much just a bag for attributes.

:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""

key = None

def to_dict(self):
return copy.deepcopy(self._info)

@property
def dirty_fields(self):
out = self.to_dict()
for k, v in self._info.items():
if self.__dict__[k] != v:
out[k] = self.__dict__[k]
return out

def update(self):
try:
return self.manager.update(**self.dirty_fields)
except AttributeError:
raise exc.NotUpdatableError(self)

+ 0
- 76
cloudkittyclient/common/client.py View File

@@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: François Magimel (linkid)

"""
OpenStack Client interface. Handles the REST calls and responses.
Override the oslo-incubator one.
"""

import logging
import time

from cloudkittyclient.common import exceptions
from cloudkittyclient.openstack.common.apiclient import client


_logger = logging.getLogger(__name__)


class HTTPClient(client.HTTPClient):
"""This client handles sending HTTP requests to OpenStack servers.
[Overrider]
"""
def request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.

Wrapper around `requests.Session.request` to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.

:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", kwargs.get("headers", {}))
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
self.original_ip, self.user_agent)
if self.timeout is not None:
kwargs.setdefault("timeout", self.timeout)
kwargs.setdefault("verify", self.verify)
if self.cert is not None:
kwargs.setdefault("cert", self.cert)
self.serialize(kwargs)

self._http_log_req(method, url, kwargs)
if self.timings:
start_time = time.time()
resp = self.http.request(method, url, **kwargs)
if self.timings:
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
self._http_log_resp(resp)

if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
resp.status_code)
raise exceptions.from_response(resp, method, url)

return resp

+ 0
- 84
cloudkittyclient/common/exceptions.py View File

@@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: François Magimel (linkid)

"""
Exception definitions.
See cloudkittyclient.openstack.common.apiclient.exceptions.
"""

from cloudkittyclient.openstack.common.apiclient.exceptions import *


# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in six.iteritems(vars(sys.modules[__name__]))
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)


def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.

:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""

req_id = response.headers.get("x-openstack-request-id")
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": req_id,
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]

content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if isinstance(body, dict):
if isinstance(body.get("error"), dict):
error = body["error"]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
elif "faultstring" in body and "faultcode" in body:
# WSME
kwargs["message"] = "%(faultcode)s: %(faultstring)s" % body
kwargs["details"] = body.get("debuginfo", "")
elif content_type.startswith("text/"):
kwargs["details"] = response.text

try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

+ 211
- 0
cloudkittyclient/common/utils.py View File

@@ -0,0 +1,211 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from __future__ import print_function

import datetime
import sys
import textwrap
import uuid

from oslo.serialization import jsonutils
from oslo.utils import encodeutils
from oslo.utils import importutils
import prettytable
import six

from cloudkittyclient import exc
from cloudkittyclient.openstack.common import cliutils


def import_versioned_module(version, submodule=None):
module = 'cloudkittyclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)


# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
if 'help' in kwargs:
if 'default' in kwargs:
kwargs['help'] += " Defaults to %s." % kwargs['default']
required = kwargs.get('required', False)
if required:
kwargs['help'] += " Required."

# Because of the sematics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator


def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)


def print_list(objs, fields, field_labels, formatters={}, sortby=0):

def _make_default_formatter(field):
return lambda o: getattr(o, field, '')

new_formatters = {}
for field, field_label in six.moves.zip(fields, field_labels):
if field in formatters:
new_formatters[field_label] = formatters[field]
else:
new_formatters[field_label] = _make_default_formatter(field)

cliutils.print_list(objs, field_labels,
formatters=new_formatters,
sortby_index=sortby)


def nested_list_of_dict_formatter(field, column_names):
# (TMaddox) Because the formatting scheme actually drops the whole object
# into the formatter, rather than just the specified field, we have to
# extract it and then pass the value.
return lambda o: format_nested_list_of_dict(getattr(o, field),
column_names)


def format_nested_list_of_dict(l, column_names):
pt = prettytable.PrettyTable(caching=False, print_empty=False,
header=True, hrules=prettytable.FRAME,
field_names=column_names)
for d in l:
pt.add_row(list(map(lambda k: d[k], column_names)))
return pt.get_string()


def print_dict(d, dict_property="Property", wrap=0):
pt = prettytable.PrettyTable([dict_property, 'Value'], print_empty=False)
pt.align = 'l'
for k, v in sorted(six.iteritems(d)):
# convert dict to str to check length
if isinstance(v, dict):
v = jsonutils.dumps(v)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
if wrap > 0:
line = textwrap.fill(str(line), wrap)
pt.add_row([col1, line])
col1 = ''
else:
if wrap > 0:
v = textwrap.fill(str(v), wrap)
pt.add_row([k, v])
encoded = encodeutils.safe_encode(pt.get_string())
# FIXME(gordc): https://bugs.launchpad.net/oslo-incubator/+bug/1370710
if six.PY3:
encoded = encoded.decode()
print(encoded)


def find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
except exc.HTTPNotFound:
pass

# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id)
except (ValueError, exc.HTTPNotFound):
pass

# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exc.HTTPNotFound:
msg = ("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))
raise exc.CommandError(msg)


def args_array_to_dict(kwargs, key_to_convert):
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = dict(v.split("=", 1)
for v in values_to_convert)
except ValueError:
raise exc.CommandError(
'%s must be a list of key=value not "%s"' % (
key_to_convert, values_to_convert))
return kwargs


def args_array_to_list_of_dicts(kwargs, key_to_convert):
"""Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}]."""
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = []
for lst in values_to_convert:
pairs = lst.split(";")
dct = dict()
for pair in pairs:
kv = pair.split("=", 1)
dct[kv[0]] = kv[1].strip(" \"'") # strip spaces and quotes
kwargs[key_to_convert].append(dct)
except Exception:
raise exc.CommandError(
'%s must be a list of key1=value1;key2=value2;... not "%s"' % (
key_to_convert, values_to_convert))
return kwargs


def key_with_slash_to_nested_dict(kwargs):
nested_kwargs = {}
for k in list(kwargs):
keys = k.split('/', 1)
if len(keys) == 2:
nested_kwargs.setdefault(keys[0], {})[keys[1]] = kwargs[k]
del kwargs[k]
kwargs.update(nested_kwargs)
return kwargs


def merge_nested_dict(dest, source, depth=0):
for (key, value) in six.iteritems(source):
if isinstance(value, dict) and depth:
merge_nested_dict(dest[key], value,
depth=(depth - 1))
else:
dest[key] = value


def ts2dt(timestamp):
"""timestamp to datetime format."""
if not isinstance(timestamp, float):
timestamp = float(timestamp)
return datetime.datetime.utcfromtimestamp(timestamp)


def exit(msg=''):
if msg:
print(msg, file=sys.stderr)
sys.exit(1)

+ 121
- 0
cloudkittyclient/exc.py View File

@@ -0,0 +1,121 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import json
import sys


class BaseException(Exception):
"""An error occurred."""
def __init__(self, message=None):
self.message = message

def __str__(self):
return self.message or self.__class__.__doc__


class CommandError(BaseException):
"""Invalid usage of CLI."""


class InvalidEndpoint(BaseException):
"""The provided endpoint is invalid."""


class CommunicationError(BaseException):
"""Unable to communicate with server."""


class NotUpdatableError(BaseException):
"""This Resource is not updatable."""

def __init__(self, resource):
message = "%s is not updatable" % resource
super(BaseException, self).__init__(message)


class HTTPException(BaseException):
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'

def __init__(self, details=None):
self.details = details

def __str__(self):
try:
data = json.loads(self.details)
message = data.get("error_message", {}).get("faultstring")
if message:
return "%s (HTTP %s) ERROR %s" % (
self.__class__.__name__, self.code, message)
except (ValueError, TypeError, AttributeError):
pass
return "%s (HTTP %s)" % (self.__class__.__name__, self.code)


class HTTPBadRequest(HTTPException):
code = 400


class HTTPUnauthorized(HTTPException):
code = 401


class HTTPForbidden(HTTPException):
code = 403


class HTTPNotFound(HTTPException):
code = 404


class HTTPMethodNotAllowed(HTTPException):
code = 405


class HTTPConflict(HTTPException):
code = 409


class HTTPOverLimit(HTTPException):
code = 413


class HTTPInternalServerError(HTTPException):
code = 500


class HTTPNotImplemented(HTTPException):
code = 501


class HTTPBadGateway(HTTPException):
code = 502


class HTTPServiceUnavailable(HTTPException):
code = 503


# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
# classes
_code_map = {}
for obj_name in dir(sys.modules[__name__]):
if obj_name.startswith('HTTP'):
obj = getattr(sys.modules[__name__], obj_name)
_code_map[obj.code] = obj


def from_response(response, details=None):
"""Return an instance of an HTTPException based on httplib response."""
cls = _code_map.get(response.status, HTTPException)
return cls(details)

+ 0
- 17
cloudkittyclient/openstack/common/__init__.py View File

@@ -1,17 +0,0 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import six


six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))

+ 45
- 0
cloudkittyclient/openstack/common/_i18n.py View File

@@ -0,0 +1,45 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""oslo.i18n integration module.

See http://docs.openstack.org/developer/oslo.i18n/usage.html

"""

try:
import oslo.i18n

# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
# application name when this module is synced into the separate
# repository. It is OK to have more than one translation function
# using the same domain, since there will still only be one message
# catalog.
_translators = oslo.i18n.TranslatorFactory(domain='cloudkittyclient')

# The primary translation function using the well-known name "_"
_ = _translators.primary

# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
except ImportError:
# NOTE(dims): Support for cases where a project wants to use
# code from oslo-incubator, but is not ready to be internationalized
# (like tempest)
_ = _LI = _LW = _LE = _LC = lambda x: x

+ 13
- 0
cloudkittyclient/openstack/common/apiclient/auth.py View File

@@ -17,6 +17,19 @@
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202

########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################

import abc
import argparse
import os


+ 37
- 14
cloudkittyclient/openstack/common/apiclient/base.py View File

@@ -20,18 +20,32 @@
Base utilities to build API operation managers and objects on top of.
"""

########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################


# E1102: %s is not callable
# pylint: disable=E1102

import abc
import copy

from oslo.utils import strutils
import six
from six.moves.urllib import parse

from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
from cloudkittyclient.openstack.common.gettextutils import _
from cloudkittyclient.openstack.common import strutils


def getid(obj):
@@ -99,12 +113,13 @@ class BaseManager(HookableMixin):
super(BaseManager, self).__init__()
self.client = client

def _list(self, url, response_key, obj_class=None, json=None):
def _list(self, url, response_key=None, obj_class=None, json=None):
"""List the collection.

:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
e.g., 'servers'. If response_key is None - all response body
will be used.
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
@@ -118,7 +133,7 @@ class BaseManager(HookableMixin):
if obj_class is None:
obj_class = self.resource_class

data = body[response_key]
data = body[response_key] if response_key is not None else body
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
@@ -128,15 +143,17 @@ class BaseManager(HookableMixin):

return [obj_class(self, res, loaded=True) for res in data if res]

def _get(self, url, response_key):
def _get(self, url, response_key=None):
"""Get an object from collection.

:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
e.g., 'server'. If response_key is None - all response body
will be used.
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], loaded=True)
data = body[response_key] if response_key is not None else body
return self.resource_class(self, data, loaded=True)

def _head(self, url):
"""Retrieve request headers for an object.
@@ -146,21 +163,23 @@ class BaseManager(HookableMixin):
resp = self.client.head(url)
return resp.status_code == 204

def _post(self, url, json, response_key, return_raw=False):
def _post(self, url, json, response_key=None, return_raw=False):
"""Create an object.

:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
e.g., 'server'. If response_key is None - all response body
will be used.
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
data = body[response_key] if response_key is not None else body
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
return data
return self.resource_class(self, data)

def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
@@ -169,7 +188,8 @@ class BaseManager(HookableMixin):
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
@@ -187,7 +207,8 @@ class BaseManager(HookableMixin):
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
@@ -488,6 +509,8 @@ class Resource(object):
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
self._add_details(
{'x_request_id': self.manager.client.last_request_id})

def __eq__(self, other):
if not isinstance(other, Resource):


+ 31
- 6
cloudkittyclient/openstack/common/apiclient/client.py View File

@@ -25,6 +25,7 @@ OpenStack Client interface. Handles the REST calls and responses.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202

import hashlib
import logging
import time

@@ -33,14 +34,15 @@ try:
except ImportError:
import json

from oslo.utils import encodeutils
from oslo.utils import importutils
import requests

from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
from cloudkittyclient.openstack.common.gettextutils import _
from cloudkittyclient.openstack.common import importutils


_logger = logging.getLogger(__name__)
SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)


class HTTPClient(object):
@@ -98,19 +100,32 @@ class HTTPClient(object):
self.http = http or requests.Session()

self.cached_token = None
self.last_request_id = None

def _safe_header(self, name, value):
if name in SENSITIVE_HEADERS:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return encodeutils.safe_decode(name), "{SHA1}%s" % d
else:
return (encodeutils.safe_decode(name),
encodeutils.safe_decode(value))

def _http_log_req(self, method, url, kwargs):
if not self.debug:
return

string_parts = [
"curl -i",
"curl -g -i",
"-X '%s'" % method,
"'%s'" % url,
]

for element in kwargs['headers']:
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
header = ("-H '%s: %s'" %
self._safe_header(element, kwargs['headers'][element]))
string_parts.append(header)

_logger.debug("REQ: %s" % " ".join(string_parts))
@@ -156,7 +171,7 @@ class HTTPClient(object):
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", kwargs.get("headers", {}))
kwargs.setdefault("headers", {})
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
@@ -177,6 +192,8 @@ class HTTPClient(object):
start_time, time.time()))
self._http_log_resp(resp)

self.last_request_id = resp.headers.get('x-openstack-request-id')

if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
@@ -247,6 +264,10 @@ class HTTPClient(object):
raise
self.cached_token = None
client.cached_endpoint = None
if self.auth_plugin.opts.get('token'):
self.auth_plugin.opts['token'] = None
if self.auth_plugin.opts.get('endpoint'):
self.auth_plugin.opts['endpoint'] = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
@@ -323,6 +344,10 @@ class BaseClient(object):
return self.http_client.client_request(
self, method, url, **kwargs)

@property
def last_request_id(self):
return self.http_client.last_request_id

def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)



+ 29
- 16
cloudkittyclient/openstack/common/apiclient/exceptions.py View File

@@ -20,12 +20,25 @@
Exception definitions.
"""

########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################

import inspect
import sys

import six

from cloudkittyclient.openstack.common.gettextutils import _
from cloudkittyclient.openstack.common._i18n import _


class ClientException(Exception):
@@ -34,14 +47,6 @@ class ClientException(Exception):
pass


class MissingArgs(ClientException):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = _("Missing arguments: %s") % ", ".join(missing)
super(MissingArgs, self).__init__(msg)


class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
@@ -62,11 +67,16 @@ class AuthorizationFailure(ClientException):
pass


class ConnectionRefused(ClientException):
class ConnectionError(ClientException):
"""Cannot connect to API service."""
pass


class ConnectionRefused(ConnectionError):
"""Connection refused while trying to connect to API service."""
pass


class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
@@ -80,7 +90,7 @@ class AuthSystemNotFound(AuthorizationFailure):
"""User has specified an AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
_("AuthSystemNotFound: %s") % repr(auth_system))
_("AuthSystemNotFound: %r") % auth_system)
self.auth_system = auth_system


@@ -103,7 +113,7 @@ class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
_("AmbiguousEndpoints: %s") % repr(endpoints))
_("AmbiguousEndpoints: %r") % endpoints)
self.endpoints = endpoints


@@ -447,10 +457,13 @@ def from_response(response, method, url):
except ValueError:
pass
else:
if isinstance(body, dict) and isinstance(body.get("error"), dict):
error = body["error"]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
if isinstance(body, dict):
error = body.get(list(body)[0])
if isinstance(error, dict):
kwargs["message"] = (error.get("message") or
error.get("faultstring"))
kwargs["details"] = (error.get("details") or
six.text_type(body))
elif content_type.startswith("text/"):
kwargs["details"] = response.text



+ 18
- 1
cloudkittyclient/openstack/common/apiclient/fake_client.py View File

@@ -21,6 +21,19 @@ wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""

########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################

# W0102: Dangerous default value %s as argument
# pylint: disable=W0102

@@ -33,7 +46,9 @@ from six.moves.urllib import parse
from cloudkittyclient.openstack.common.apiclient import client


def assert_has_keys(dct, required=[], optional=[]):
def assert_has_keys(dct, required=None, optional=None):
required = required or []
optional = optional or []
for k in required:
try:
assert k in dct
@@ -166,6 +181,8 @@ class FakeHTTPClient(client.HTTPClient):
else:
status, body = resp
headers = {}
self.last_request_id = headers.get('x-openstack-request-id',
'req-test')
return TestResponse({
"status_code": status,
"text": body,


+ 100
- 0
cloudkittyclient/openstack/common/apiclient/utils.py View File

@@ -0,0 +1,100 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################

from oslo.utils import encodeutils
from oslo.utils import uuidutils
import six

from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions


def find_resource(manager, name_or_id, **find_args):
"""Look for resource in a given manager.

Used as a helper for the _find_* methods.
Example:

.. code-block:: python

def _find_hypervisor(cs, hypervisor):
#Get a hypervisor by name or ID.
return cliutils.find_resource(cs.hypervisors, hypervisor)
"""
# first try to get entity as integer id
try:
return manager.get(int(name_or_id))
except (TypeError, ValueError, exceptions.NotFound):
pass

# now try to get entity as uuid
try:
if six.PY2:
tmp_id = encodeutils.safe_encode(name_or_id)
else:
tmp_id = encodeutils.safe_decode(name_or_id)

if uuidutils.is_uuid_like(tmp_id):
return manager.get(tmp_id)
except (TypeError, ValueError, exceptions.NotFound):
pass

# for str id which is not uuid
if getattr(manager, 'is_alphanum_id_allowed', False):
try:
return manager.get(name_or_id)
except exceptions.NotFound:
pass

try:
try:
return manager.find(human_id=name_or_id, **find_args)
except exceptions.NotFound:
pass

# finally try to find entity by name
try:
resource = getattr(manager, 'resource_class', None)
name_attr = resource.NAME_ATTR if resource else 'name'
kwargs = {name_attr: name_or_id}
kwargs.update(find_args)
return manager.find(**kwargs)
except exceptions.NotFound:
msg = _("No %(name)s with a name or "
"ID of '%(name_or_id)s' exists.") % \
{
"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id
}
raise exceptions.CommandError(msg)
except exceptions.NoUniqueMatch:
msg = _("Multiple %(name)s matches found for "
"'%(name_or_id)s', use an ID to be more specific.") % \
{
"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id
}
raise exceptions.CommandError(msg)

+ 271
- 0
cloudkittyclient/openstack/common/cliutils.py View File

@@ -0,0 +1,271 @@
# Copyright 2012 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# W0603: Using the global statement
# W0621: Redefining name %s from outer scope
# pylint: disable=W0603,W0621

from __future__ import print_function

import getpass
import inspect
import os
import sys
import textwrap

from oslo.utils import encodeutils
from oslo.utils import strutils
import prettytable
import six
from six import moves

from cloudkittyclient.openstack.common._i18n import _


class MissingArgs(Exception):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = _("Missing arguments: %s") % ", ".join(missing)
super(MissingArgs, self).__init__(msg)


def validate_args(fn, *args, **kwargs):
"""Check that the supplied args are sufficient for calling a function.

>>> validate_args(lambda a: None)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): a
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): b, d

:param fn: the function to check
:param arg: the positional arguments supplied
:param kwargs: the keyword arguments supplied
"""
argspec = inspect.getargspec(fn)

num_defaults = len(argspec.defaults or [])
required_args = argspec.args[:len(argspec.args) - num_defaults]

def isbound(method):
return getattr(method, '__self__', None) is not None

if isbound(fn):
required_args.pop(0)

missing = [arg for arg in required_args if arg not in kwargs]
missing = missing[len(args):]
if missing:
raise MissingArgs(missing)


def arg(*args, **kwargs):
"""Decorator for CLI args.

Example:

>>> @arg("name", help="Name of the new entity")
... def entity_create(args):
... pass
"""
def _decorator(func):
add_arg(func, *args, **kwargs)
return func
return _decorator


def env(*args, **kwargs):
"""Returns the first environment variable set.

If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')


def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""

if not hasattr(func, 'arguments'):
func.arguments = []

# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))


def unauthenticated(func):
"""Adds 'unauthenticated' attribute to decorated function.

Usage:

>>> @unauthenticated
... def mymethod(f):
... pass
"""
func.unauthenticated = True
return func


def isunauthenticated(func):
"""Checks if the function does not require authentication.

Mark such functions with the `@unauthenticated` decorator.

:returns: bool
"""
return getattr(func, 'unauthenticated', False)


def print_list(objs, fields, formatters=None, sortby_index=0,
mixed_case_fields=None, field_labels=None):
"""Print a list or objects as a table, one row per object.

:param objs: iterable of :class:`Resource`
:param fields: attributes that correspond to columns, in order
:param formatters: `dict` of callables for field formatting
:param sortby_index: index of the field for sorting table rows
:param mixed_case_fields: fields corresponding to object attributes that
have mixed case names (e.g., 'serverId')
:param field_labels: Labels to use in the heading of the table, default to
fields.
"""
formatters = formatters or {}
mixed_case_fields = mixed_case_fields or []
field_labels = field_labels or fields
if len(field_labels) != len(fields):
raise ValueError(_("Field labels list %(labels)s has different number "
"of elements than fields list %(fields)s"),
{'labels': field_labels, 'fields': fields})

if sortby_index is None:
kwargs = {}
else:
kwargs = {'sortby': field_labels[sortby_index]}
pt = prettytable.PrettyTable(field_labels)
pt.align = 'l'

for o in objs:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](o))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)

if six.PY3:
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
else:
print(encodeutils.safe_encode(pt.get_string(**kwargs)))


def print_dict(dct, dict_property="Property", wrap=0):
"""Print a `dict` as a table of two columns.

:param dct: `dict` to print
:param dict_property: name of the first column
:param wrap: wrapping for the second column
"""
pt = prettytable.PrettyTable([dict_property, 'Value'])
pt.align = 'l'
for k, v in six.iteritems(dct):
# convert dict to str to check length
if isinstance(v, dict):
v = six.text_type(v)
if wrap > 0:
v = textwrap.fill(six.text_type(v), wrap)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
pt.add_row([col1, line])
col1 = ''
else:
pt.add_row([k, v])

if six.PY3:
print(encodeutils.safe_encode(pt.get_string()).decode())
else:
print(encodeutils.safe_encode(pt.get_string()))


def get_password(max_password_prompts=3):
"""Read password from TTY."""
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))