Browse Source

Initial work for the CloudKitty client

Change-Id: Icfcd82c156c433911230fbc98865ba7b662024ee
tags/0.4.0
François Magimel 6 years ago
parent
commit
40dfddecbe
40 changed files with 4200 additions and 0 deletions
  1. +7
    -0
      .coveragerc
  2. +13
    -0
      .gitignore
  3. +19
    -0
      .pylintrc
  4. +4
    -0
      .testr.conf
  5. +175
    -0
      LICENSE
  6. +34
    -0
      README.rst
  7. +26
    -0
      cloudkittyclient/__init__.py
  8. +66
    -0
      cloudkittyclient/client.py
  9. +0
    -0
      cloudkittyclient/common/__init__.py
  10. +79
    -0
      cloudkittyclient/common/auth.py
  11. +76
    -0
      cloudkittyclient/common/client.py
  12. +84
    -0
      cloudkittyclient/common/exceptions.py
  13. +32
    -0
      cloudkittyclient/i18n.py
  14. +0
    -0
      cloudkittyclient/openstack/__init__.py
  15. +17
    -0
      cloudkittyclient/openstack/common/__init__.py
  16. +0
    -0
      cloudkittyclient/openstack/common/apiclient/__init__.py
  17. +221
    -0
      cloudkittyclient/openstack/common/apiclient/auth.py
  18. +509
    -0
      cloudkittyclient/openstack/common/apiclient/base.py
  19. +363
    -0
      cloudkittyclient/openstack/common/apiclient/client.py
  20. +466
    -0
      cloudkittyclient/openstack/common/apiclient/exceptions.py
  21. +173
    -0
      cloudkittyclient/openstack/common/apiclient/fake_client.py
  22. +479
    -0
      cloudkittyclient/openstack/common/gettextutils.py
  23. +73
    -0
      cloudkittyclient/openstack/common/importutils.py
  24. +295
    -0
      cloudkittyclient/openstack/common/strutils.py
  25. +99
    -0
      cloudkittyclient/openstack/common/test.py
  26. +0
    -0
      cloudkittyclient/tests/__init__.py
  27. +6
    -0
      cloudkittyclient/tests/test_fake.py
  28. +18
    -0
      cloudkittyclient/v1/__init__.py
  29. +30
    -0
      cloudkittyclient/v1/client.py
  30. +56
    -0
      cloudkittyclient/v1/report.py
  31. +177
    -0
      doc/makefile
  32. +280
    -0
      doc/source/conf.py
  33. +23
    -0
      doc/source/index.rst
  34. +9
    -0
      openstack-common.conf
  35. +5
    -0
      requirements.txt
  36. +34
    -0
      setup.cfg
  37. +29
    -0
      setup.py
  38. +13
    -0
      test-requirements.txt
  39. +172
    -0
      tools/install_venv_common.py
  40. +38
    -0
      tox.ini

+ 7
- 0
.coveragerc View File

@@ -0,0 +1,7 @@
[run]
branch = True
source = cloudkittyclient
omit = cloudkittyclient/tests/*,cloudkittyclient/openstack/*,cloudkittyclient/i18n.py

[report]
ignore-errors = True

+ 13
- 0
.gitignore View File

@@ -0,0 +1,13 @@
*.pyc
*.egg-info
*.log
build
.coverage
.coverage.*
.tox
cover
.testrepository
.venv
dist
*.egg
*.sw?

+ 19
- 0
.pylintrc View File

@@ -0,0 +1,19 @@
[MASTER]
ignore=openstack,test

[MESSAGES CONTROL]
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622

[BASIC]
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$

[Variables]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
# _ is used by our localization
additional-builtins=_

+ 4
- 0
.testr.conf View File

@@ -0,0 +1,4 @@
[DEFAULT]
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./cloudkittyclient/tests $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

+ 175
- 0
LICENSE View File

@@ -0,0 +1,175 @@

Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.

"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:

(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and

(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and

(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and

(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.

You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
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.

+ 34
- 0
README.rst View File

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

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


.. _CloudKitty Wiki: https://wiki.openstack.org/wiki/CloudKitty


python-cloudkittyclient
=======================

This is a client library for CloudKitty built on the CloudKitty API. It
provides a Python API (the ``cloudkittyclient`` module) and a command-line
tool (``cloudkitty``).


Status
======

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


Roadmap
=======

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

+ 26
- 0
cloudkittyclient/__init__.py View File

@@ -0,0 +1,26 @@
# -*- 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)

__all__ = ['__version__']

import pbr.version

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

+ 66
- 0
cloudkittyclient/client.py View File

@@ -0,0 +1,66 @@
# -*- 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.
"""

from cloudkittyclient.common import auth as ks_auth
from cloudkittyclient.common import client
from cloudkittyclient.openstack.common import importutils


def get_client(api_version, **kwargs):
"""Get an authenticated client.

This is based on the credentials in the keyword args.

:param api_version: the API version to use
:param kwargs: keyword args containing credentials, either:
* os_auth_token: pre-existing token to re-use
* endpoint: CloudKitty API endpoint
or:
* os_username: name of user
* os_password: user's password
* os_auth_url: endpoint to authenticate against
* os_tenant_name: name of tenant
"""
cli_kwargs = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'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')
}
return Client(api_version, **cli_kwargs)


def Client(version, **kwargs):
module = importutils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')

keystone_auth = ks_auth.KeystoneAuthPlugin(
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)

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


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

@@ -0,0 +1,79 @@
# -*- 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)

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

@@ -0,0 +1,76 @@
# -*- 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

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

@@ -0,0 +1,84 @@
# -*- 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)

+ 32
- 0
cloudkittyclient/i18n.py View File

@@ -0,0 +1,32 @@
# -*- 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.

from oslo import i18n

_translators = i18n.TranslatorFactory(domain='cloudkittyclient')
i18n.enable_lazy()

# 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

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


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

@@ -0,0 +1,17 @@
#
# 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
cloudkittyclient/openstack/common/apiclient/__init__.py View File


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

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

# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202

import abc
import argparse
import os

import six
from stevedore import extension

from cloudkittyclient.openstack.common.apiclient import exceptions


_discovered_plugins = {}


def discover_auth_systems():
"""Discover the available auth-systems.

This won't take into account the old style auth-systems.
"""
global _discovered_plugins
_discovered_plugins = {}

def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin

ep_namespace = "cloudkittyclient.openstack.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)


def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.

This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in six.iteritems(_discovered_plugins):
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)


def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)


def load_plugin_from_args(args):
"""Load required plugin and populate it with options.

Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.

:type args: argparse.Namespace
:raises: AuthPluginOptionsMissing
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin

for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])


@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.

An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""

auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]

def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)

@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.

:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)

@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin.
"""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)

@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins.
"""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)

@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.

:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))

def parse_opts(self, args):
"""Parse the actual auth-system options if any.

This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))

def authenticate(self, http_client):
"""Authenticate using plugin defined method.

The method usually analyses `self.opts` and performs
a request to authentication server.

:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)

@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication.
"""

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

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

@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.

:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

+ 509
- 0
cloudkittyclient/openstack/common/apiclient/base.py View File

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

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

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

import abc
import copy

import six
from six.moves.urllib import parse

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


def getid(obj):
"""Return id if argument is a Resource.

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


# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}

@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.

:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []

cls._hooks_map[hook_type].append(hook_func)

@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.

:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param args: args to be passed to every hook function
:param kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)


class BaseManager(HookableMixin):
"""Basic manager type providing common operations.

Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None

def __init__(self, client):
"""Initializes BaseManager with `client`.

:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client

def _list(self, url, response_key, 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'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()

if obj_class is None:
obj_class = self.resource_class

data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass

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

def _get(self, url, response_key):
"""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'
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], loaded=True)

def _head(self, url):
"""Retrieve request headers for an object.

:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204

def _post(self, url, json, response_key, 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'
: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()
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])

def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.

:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)

def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.

:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)

def _delete(self, url):
"""Delete an object.

:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)


@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""

@abc.abstractmethod
def list(self):
pass

def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.

This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]

def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.

This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()

for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue

return found


class CrudManager(BaseManager):
"""Base manager class for manipulating entities.

Children of this class are expected to define a `collection_key` and `key`.

- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.

"""
collection_key = None
key = None

def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.

Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.

By default, the URL will represent a collection of entities, e.g.::

/entities

If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::

/entities/{entity_id}

:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''

url += '/%s' % self.collection_key

# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id

return url

def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in six.iteritems(kwargs.copy()):
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs

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

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

def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))

def list(self, base_url=None, **kwargs):
"""List the collection.

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

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

def put(self, base_url=None, **kwargs):
"""Update an element.

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

return self._put(self.build_url(base_url=base_url, **kwargs))

def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)

return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)

def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)

return self._delete(
self.build_url(**kwargs))

def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.

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

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

if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(404, msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]


class Extension(HookableMixin):
"""Extension descriptor."""

SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None

def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()

def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass

def __repr__(self):
return "<Extension '%s'>" % self.name


class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).

This is pretty much just a bag for attributes.
"""

HUMAN_ID = False
NAME_ATTR = 'name'

def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.

:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded

def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)

@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
return strutils.to_slug(name)
return None

def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass

def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)

raise AttributeError(k)
else:
return self.__dict__[k]

def get(self):
"""Support for lazy loading details.

Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return

new = self.manager.get(self.id)
if new:
self._add_details(new._info)

def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info

def is_loaded(self):
return self._loaded

def set_loaded(self, val):
self._loaded = val

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

+ 363
- 0
cloudkittyclient/openstack/common/apiclient/client.py View File

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

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

# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202

import logging
import time

try:
import simplejson as json
except ImportError:
import json

import requests

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


_logger = logging.getLogger(__name__)


class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.

Features:

- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""

user_agent = "cloudkittyclient.openstack.common.apiclient"

def __init__(self,
auth_plugin,
region_name=None,
endpoint_type="publicURL",
original_ip=None,
verify=True,
cert=None,
timeout=None,
timings=False,
keyring_saver=None,
debug=False,
user_agent=None,
http=None):
self.auth_plugin = auth_plugin

self.endpoint_type = endpoint_type
self.region_name = region_name

self.original_ip = original_ip
self.timeout = timeout
self.verify = verify
self.cert = cert

self.keyring_saver = keyring_saver
self.debug = debug
self.user_agent = user_agent or self.user_agent

self.times = [] # [("item", starttime, endtime), ...]
self.timings = timings

# requests within the same session can reuse TCP connections from pool
self.http = http or requests.Session()

self.cached_token = None

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

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

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

_logger.debug("REQ: %s" % " ".join(string_parts))
if 'data' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))

def _http_log_resp(self, resp):
if not self.debug:
return
_logger.debug(
"RESP: [%s] %s\n",
resp.status_code,
resp.headers)
if resp._content_consumed:
_logger.debug(
"RESP BODY: %s\n",
resp.text)

def serialize(self, kwargs):
if kwargs.get('json') is not None:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['json'])
try:
del kwargs['json']
except KeyError:
pass

def get_timings(self):
return self.times

def reset_timings(self):
self.times = []

def request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.

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

:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", kwargs.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

@staticmethod
def concat_url(endpoint, url):
"""Concatenate endpoint and final URL.

E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
"http://keystone/v2.0/tokens".

:param endpoint: the base URL
:param url: the final URL
"""
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))

def client_request(self, client, method, url, **kwargs):
"""Send an http request using `client`'s endpoint and specified `url`.

If request was rejected as unauthorized (possibly because the token is
expired), issue one authorization attempt and send the request once
again.

:param client: instance of BaseClient descendant
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
`HTTPClient.request`
"""

filter_args = {
"endpoint_type": client.endpoint_type or self.endpoint_type,
"service_type": client.service_type,
}
token, endpoint = (self.cached_token, client.cached_endpoint)
just_authenticated = False
if not (token and endpoint):
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
pass
if not (token and endpoint):
self.authenticate()
just_authenticated = True
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
if not (token and endpoint):
raise exceptions.AuthorizationFailure(
_("Cannot find endpoint or token for request"))

old_token_endpoint = (token, endpoint)
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
self.cached_token = token
client.cached_endpoint = endpoint
# Perform the request once. If we get Unauthorized, then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
except exceptions.Unauthorized as unauth_ex:
if just_authenticated:
raise
self.cached_token = None
client.cached_endpoint = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
raise unauth_ex
if (not (token and endpoint) or
old_token_endpoint == (token, endpoint)):
raise unauth_ex
self.cached_token = token
client.cached_endpoint = endpoint
kwargs["headers"]["X-Auth-Token"] = token
return self.request(
method, self.concat_url(endpoint, url), **kwargs)

def add_client(self, base_client_instance):
"""Add a new instance of :class:`BaseClient` descendant.

`self` will store a reference to `base_client_instance`.

Example:

>>> def test_clients():
... from keystoneclient.auth import keystone
... from openstack.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
... openstack_client = client.HTTPClient(auth)
... # create nova client
... from novaclient.v1_1 import client
... client.Client(openstack_client)
... # create keystone client
... from keystoneclient.v2_0 import client
... client.Client(openstack_client)
... # use them
... openstack_client.identity.tenants.list()
... openstack_client.compute.servers.list()
"""
service_type = base_client_instance.service_type
if service_type and not hasattr(self, service_type):
setattr(self, service_type, base_client_instance)

def authenticate(self):
self.auth_plugin.authenticate(self)
# Store the authentication results in the keyring for later requests
if self.keyring_saver:
self.keyring_saver.save(self)


class BaseClient(object):
"""Top-level object to access the OpenStack API.

This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
will handle a bunch of issues such as authentication.
"""

service_type = None
endpoint_type = None # "publicURL" will be used
cached_endpoint = None

def __init__(self, http_client, extensions=None):
self.http_client = http_client
http_client.add_client(self)

# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))

def client_request(self, method, url, **kwargs):
return self.http_client.client_request(
self, method, url, **kwargs)

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

def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)

def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)

def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)

def delete(self, url, **kwargs):
return self.client_request("DELETE", url, **kwargs)

def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)

@staticmethod
def get_class(api_name, version, version_map):
"""Returns the client class for the requested API version

:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = _("Invalid %(api_name)s client version '%(version)s'. "
"Must be one of: %(version_map)s") % {
'api_name': api_name,
'version': version,
'version_map': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg)

return importutils.import_class(client_path)

+ 466
- 0
cloudkittyclient/openstack/common/apiclient/exceptions.py View File

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

"""
Exception definitions.
"""

import inspect
import sys

import six

from cloudkittyclient.openstack.common.gettextutils import _


class ClientException(Exception):
"""The base exception class for all exceptions this library raises.
"""
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


class UnsupportedVersion(ClientException):
"""User is trying to use an unsupported version of the API."""
pass


class CommandError(ClientException):
"""Error in CLI tool."""
pass


class AuthorizationFailure(ClientException):
"""Cannot authorize API client."""
pass


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


class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
super(AuthPluginOptionsMissing, self).__init__(
_("Authentication failed. Missing options: %s") %
", ".join(opt_names))
self.opt_names = opt_names


class AuthSystemNotFound(AuthorizationFailure):
"""User has specified an AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
_("AuthSystemNotFound: %s") % repr(auth_system))
self.auth_system = auth_system


class NoUniqueMatch(ClientException):
"""Multiple entities found instead of one."""
pass


class EndpointException(ClientException):
"""Something is rotten in Service Catalog."""
pass


class EndpointNotFound(EndpointException):
"""Could not find requested endpoint in Service Catalog."""
pass


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


class HttpError(ClientException):
"""The base exception class for all HTTP exceptions.
"""
http_status = 0
message = _("HTTP Error")

def __init__(self, message=None, details=None,
response=None, request_id=None,
url=None, method=None, http_status=None):
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
self.request_id = request_id
self.response = response
self.url = url
self.method = method
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
if request_id:
formatted_string += " (Request-ID: %s)" % request_id
super(HttpError, self).__init__(formatted_string)


class HTTPRedirection(HttpError):
"""HTTP Redirection."""
message = _("HTTP Redirection")


class HTTPClientError(HttpError):
"""Client-side HTTP error.

Exception for cases in which the client seems to have erred.
"""
message = _("HTTP Client Error")


class HttpServerError(HttpError):
"""Server-side HTTP error.

Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
message = _("HTTP Server Error")


class MultipleChoices(HTTPRedirection):
"""HTTP 300 - Multiple Choices.

Indicates multiple options for the resource that the client may follow.
"""

http_status = 300
message = _("Multiple Choices")


class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.

The request cannot be fulfilled due to bad syntax.
"""
http_status = 400
message = _("Bad Request")


class Unauthorized(HTTPClientError):
"""HTTP 401 - Unauthorized.

Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
http_status = 401
message = _("Unauthorized")


class PaymentRequired(HTTPClientError):
"""HTTP 402 - Payment Required.

Reserved for future use.
"""
http_status = 402
message = _("Payment Required")


class Forbidden(HTTPClientError):
"""HTTP 403 - Forbidden.

The request was a valid request, but the server is refusing to respond
to it.
"""
http_status = 403
message = _("Forbidden")


class NotFound(HTTPClientError):
"""HTTP 404 - Not Found.

The requested resource could not be found but may be available again
in the future.
"""
http_status = 404
message = _("Not Found")


class MethodNotAllowed(HTTPClientError):
"""HTTP 405 - Method Not Allowed.

A request was made of a resource using a request method not supported
by that resource.
"""
http_status = 405
message = _("Method Not Allowed")


class NotAcceptable(HTTPClientError):
"""HTTP 406 - Not Acceptable.

The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
http_status = 406
message = _("Not Acceptable")


class ProxyAuthenticationRequired(HTTPClientError):
"""HTTP 407 - Proxy Authentication Required.

The client must first authenticate itself with the proxy.
"""
http_status = 407
message = _("Proxy Authentication Required")


class RequestTimeout(HTTPClientError):
"""HTTP 408 - Request Timeout.

The server timed out waiting for the request.
"""
http_status = 408
message = _("Request Timeout")


class Conflict(HTTPClientError):
"""HTTP 409 - Conflict.

Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
http_status = 409
message = _("Conflict")


class Gone(HTTPClientError):
"""HTTP 410 - Gone.

Indicates that the resource requested is no longer available and will
not be available again.
"""
http_status = 410
message = _("Gone")


class LengthRequired(HTTPClientError):
"""HTTP 411 - Length Required.

The request did not specify the length of its content, which is
required by the requested resource.
"""
http_status = 411
message = _("Length Required")


class PreconditionFailed(HTTPClientError):
"""HTTP 412 - Precondition Failed.

The server does not meet one of the preconditions that the requester
put on the request.
"""
http_status = 412
message = _("Precondition Failed")


class RequestEntityTooLarge(HTTPClientError):
"""HTTP 413 - Request Entity Too Large.

The request is larger than the server is willing or able to process.
"""
http_status = 413
message = _("Request Entity Too Large")

def __init__(self, *args, **kwargs):
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
self.retry_after = 0

super(RequestEntityTooLarge, self).__init__(*args, **kwargs)


class RequestUriTooLong(HTTPClientError):
"""HTTP 414 - Request-URI Too Long.

The URI provided was too long for the server to process.
"""
http_status = 414
message = _("Request-URI Too Long")


class UnsupportedMediaType(HTTPClientError):
"""HTTP 415 - Unsupported Media Type.

The request entity has a media type which the server or resource does
not support.
"""
http_status = 415
message = _("Unsupported Media Type")


class RequestedRangeNotSatisfiable(HTTPClientError):
"""HTTP 416 - Requested Range Not Satisfiable.

The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
http_status = 416
message = _("Requested Range Not Satisfiable")


class ExpectationFailed(HTTPClientError):
"""HTTP 417 - Expectation Failed.

The server cannot meet the requirements of the Expect request-header field.
"""
http_status = 417
</