Compare commits

..

71 Commits

Author SHA1 Message Date
OpenDev Sysadmins
7a8e80f62f OpenDev Migration Patch
This commit was bulk generated and pushed by the OpenDev sysadmins
as a part of the Git hosting and code review systems migration
detailed in these mailing list posts:

http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003603.html
http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004920.html

Attempts have been made to correct repository namespaces and
hostnames based on simple pattern matching, but it's possible some
were updated incorrectly or missed entirely. Please reach out to us
via the contact information listed at https://opendev.org/ with any
questions you may have.
2019-04-19 19:41:49 +00:00
Nguyen Hai
c138cec2ca import zuul job settings from project-config
This is a mechanically generated patch to complete step 1 of moving
the zuul job settings out of project-config and into each project
repository.

Because there will be a separate patch on each branch, the branch
specifiers for branch-specific jobs have been removed.

Because this patch is generated by a script, there may be some
cosmetic changes to the layout of the YAML file(s) as the contents are
normalized.

See the python3-first goal document for details:
https://governance.openstack.org/tc/goals/stein/python3-first.html

* This patch also fix docs failure.

Change-Id: I7e2b27289927f1b3d687dadf3e68c411269c8e08
Story: #2002586
Task: #24303
2018-08-22 23:17:17 +09:00
chenying
42066dcdf0 Fix the errors about parameter when creating a plan
Change-Id: Ibb6ab69c25cf232b15e2138bfc62f91159039a32
Closes-Bug:#1666186
(cherry picked from commit ee725acb94)
2017-02-22 09:51:48 +08:00
xiangxinyong
6702e0c62b Help info error
Modefied other command help error info.

Change-Id: I82fa9bd7524677dc84bab33f57477771198b65de
(cherry picked from commit a625f7a702)
2017-02-14 18:01:30 +08:00
xiangxinyong
3e9ace28ce 'karbor provider-list' help info error
Modified 'karbor provider-list'`s help error info.

Change-Id: I0db08456599c4db96b9effd47db568c460b7ca81
(cherry picked from commit 71b9c54d0a)
2017-02-14 16:07:05 +08:00
Yuval Brik
7426876280 Update defaultbranch for newton
Change-Id: I1a4033cb6c9a384ac6630ec071e4bbe2cfb4c63b
2017-01-29 16:17:21 +02:00
Jenkins
5217c987e3 Merge "readme: fix readme title" 2017-01-26 14:44:48 +00:00
Jenkins
345c0cbc36 Merge "Remove support for py33" 2017-01-24 01:33:07 +00:00
Yuval Brik
d4e21b53c0 readme: fix readme title
Change-Id: Ic4f2c34bab12f078bce7d5c34f44991e1d6fa40c
2017-01-23 16:01:27 +02:00
wujiajun
b1fe13ddd6 Uniform parameter split character
The paratmeter split character of some commands("karbor trigger-create",
"karbor trigger-update", "karbor scheduledoperation-create") is not
comma which used in other karbor commands.The split character colon
is duplicate with time spit character(etc. 12:12:00), and the will case
error, So I change it from colon to comma.

Change-Id: I4dd0b76419e14ddc71c666779b011e427ff18db1
2017-01-23 12:08:27 +08:00
Cao Xuan Hoang
f21f04e937 Remove support for py33
Python 3.3 is not supported from Mitaka, as per Infra.
This patch removes the support for the same.

Change-Id: Ica953eb59401f3ce177230945b169f151b0299a9
2017-01-23 10:46:31 +07:00
Jenkins
4881aa259a Merge "Make some command echo item print pretty" 2017-01-23 01:56:59 +00:00
Jenkins
733472aa51 Merge "Make command 'karbor plan-xxx' print pretty" 2017-01-22 08:11:48 +00:00
wujiajun
15763e9343 Make some command echo item print pretty
There is many command echo inforamtion which is json string,
and it is usually too long to view for user. I Modified it
to make it print pretty.

Change-Id: I0a50456480f2d5d2af74848926cee68fd0a9759a
2017-01-22 09:35:57 +08:00
wujiajun
dc459ba408 Make command 'karbor plan-xxx' print pretty
1. Format the items parameters and resources in command
   karbor plan-create and karobr-show.
2. Modidied print_dict and make it more general to format
   some special item.

Change-Id: Id55a6b5d10956ac3c9ae7976b1bb3714f463f8b2
2017-01-22 04:32:35 +08:00
Jenkins
629cc47e88 Merge "Make command 'karbor provider-show' print pretty" 2017-01-18 09:13:15 +00:00
wujiajun
5b8522b1f5 Make command 'karbor provider-show' print pretty
1. The item of 'extended_info_schema' printed by command
  'karbor provider-show' is too long to view, so I modified
  it so that it looks friendly for user.
2. 'extended_info_schema' in 'karbor provider-list' print info is too
   long and useless for user, so delete it. If user want to see the
   'extended_info_schema',he can use command 'karbor provider-show'

Change-Id: I94ff6458233c0b869fe37479a278a43b91aa04db
2017-01-18 05:06:06 +08:00
Jenkins
22665522da Merge "Check the resource when creating a plan" 2017-01-17 10:59:14 +00:00
Jenkins
3a77770597 Merge "Use the appropriate marker function for each message" 2017-01-16 09:57:10 +00:00
chenying
c94edfa3f2 Check the resource when creating a plan
Change-Id: I0e0a9277df2dd1a8132f34f5d9c47c781c925db1
Partial-Bug: #1574980
2017-01-15 20:59:41 +08:00
chenying
01eab7d347 The restore_target and restore_auth are optional when creating restore
Change-Id: Ia05cb5e0d052998cf0383f700db37a39ee5935ff
Closes-Bug: #1654473
2017-01-06 14:12:20 +08:00
shizhihui
d1577ed830 Use the appropriate marker function for each message
According to:
[1] http://docs.openstack.org/developer/oslo.i18n/usage.html

Change-Id: Ifa7bdf4f8054efab364f83913ab267a2dd9cdc0d
2016-11-20 19:00:03 +08:00
shizhihui
3d7c7cd0b1 Enable DeprecationWarning in test environments
Many deprecations are triggered early (on imports, for example).
To make sure all DeprecationWarning messages are emitted we enable
them via the PYTHONWARNINGS environment variable.

Note: https://review.openstack.org/#/c/379581/
https://review.openstack.org/#/c/353154/

Change-Id: Ibd28c98120db0513a3ef82dcee194f3c20ebc6d3
2016-11-17 11:05:55 +08:00
Jenkins
078f344adb Merge "Fix the end_date filter in the checkpoint list API" 2016-12-26 02:38:57 +00:00
Jenkins
dd463cce70 Merge "Fix the failure of get() method on a checkpoint object" 2016-12-25 11:53:24 +00:00
chenying
eeede3cc68 Fix the end_date filter in the checkpoint list API
Change-Id: I0f918f5d2579bda2e75190c7a70808054b8e2f80
Partial-Bug: #1569657
2016-12-23 16:48:17 +08:00
chenying
462284d817 Fix the failure of get() method on a checkpoint object
Change-Id: Idc5df35ab0a3cf503ae0b10d8d87a77f55a1452a
Closes-Bug: #1643315
2016-12-22 16:38:49 +08:00
Tony Breeds
6ec3904818 Add Constraints support
Adding constraints support to libraries is slightly more complex than
services as the libraries themselves are listed in upper-constraints.txt
which leads to errors that you can't install a specific version and a
constrained version.

This change adds constraints support by also adding a helper script to
edit the constraints to remove python-karborclient.

Change-Id: Ib7f5194b7cf04917d8cfd3e7e1dafc303e234d20
2016-12-21 14:10:05 +11:00
Jenkins
214ad23fbe Merge "Support plan and date filter in the checkpoint list API" 2016-12-19 15:50:25 +00:00
OpenStack Proposal Bot
fefecbcfe7 Updated from global requirements
Change-Id: I26db2ba3101d69a235ffc55720572a94ba8b3f99
2016-12-15 21:29:16 +00:00
chenying
ecffc10b16 Support plan and date filter in the checkpoint list API
Change-Id: Ice96a168bca9a26146c688147059c6b914f60b4c
Partial-Bug: #1569657
2016-12-14 22:34:21 +08:00
Hui Wang
041cb460f9 The inconsistency of delete method between restore API and client
In the restore module in karbor-client component,there exists
delete(restore_id) that calls delete(path), but I can't find any
delete method in restore API module.

Closes-Bug:#1624841

Change-Id: I507c677b141d72b57d52fd6b653cc8a652a103bf
2016-12-14 16:45:58 +08:00
Yuval Brik
6e40fea91e Skip docutils version 13.1
docutils version 13.1 is causing issues with remote images. At first,
we added :remote: for each image, however, that caused the images to not
appear.
We will be skipping version 13.1, until the docutils bug [1] will be
resolved.

[1] https://sourceforge.net/p/docutils/bugs/301/

Change-Id: I50f27978ec6748754e78bec5d1d35786b210cdeb
2016-12-12 14:07:05 +02:00
Yuval Brik
afa95cb8eb Remove resource's data method
Change-Id: Ie1247cbba0ac5dfbad54ad2dec0b739c0eb7570b
Closes-Bug: #1643329
2016-12-11 15:46:11 +02:00
Yuval Brik
1db46a2283 Replace dos newlines with unix newlines
Change-Id: I80c0ca3a8f5211ae8735790293c8bd7fd59b432b
2016-12-11 15:45:55 +02:00
Yuval Brik
c243de9232 Add remote tag to remote images
Change-Id: I04b195eadfd8a684a5e9a8f9472d145e56cb60f4
2016-12-11 15:42:21 +02:00
Jenkins
89f1e4eac7 Merge "Updated from global requirements" 2016-12-04 14:11:52 +00:00
Tin Lam
ced9795f05 Add specs target to README.rst
In README.rst, there is a missing link for the `specs` target
that cause a lint error when executing: rst-lint README.rst.
This patch set addresses this issue by adding the appropriate
spec link in the file.

Change-Id: Icbce11806b341f6b715432f4b8432989e46f3403
Closes-Bug: #1645280
2016-12-02 14:43:16 -06:00
OpenStack Proposal Bot
e834eb86bf Updated from global requirements
Change-Id: I366d60bea21b2ec0c4ebd1e5b9ad3d976d583a01
2016-12-02 17:17:37 +00:00
Jenkins
7340b04b80 Merge "Add a filter parameter 'description' for plan list API" 2016-12-02 02:20:28 +00:00
chenying
339086ce64 Add a filter parameter 'description' for plan list API
Change-Id: I7f84e41bfb765f2ad732492f8c700f8b2cc6ca14
Closes-Bug: #1646044
2016-12-02 09:43:29 +08:00
sloblee
3a469df201 Show Created_at field while checkpoint-list/checkpoint-show
Return the Created_at field when excuting the checkpoint-list command.

Change-Id: I50178d3e787032af72c7fe33b6de206b1ff5a1ac
Closes-Bug: #1578563
2016-12-01 10:38:48 +08:00
Flavio Percoco
3710127a5f Show team and repo badges on README
This patch adds the team's and repository's badges to the README file.
The motivation behind this is to communicate the project status and
features at first glance.

For more information about this effort, please read this email thread:

http://lists.openstack.org/pipermail/openstack-dev/2016-October/105562.html

To see an example of how this would look like check:

b'https://gist.github.com/86c7e945bcedfb376784ec3a4ecd6356\n'

Change-Id: Ie110ff1a82611e1c0efcade9cff243c094211804
2016-11-25 14:07:56 +01:00
Jenkins
faf5c656af Merge "Add metadata parameter to checkpoint API" 2016-11-23 02:48:00 +00:00
chenying
fc425e741d Add a filter argument operation_definition for scheduledoperations API
Change-Id: I16c79491c51c341d692066c78611b218d0731435
2016-11-14 19:22:51 +08:00
Jenkins
74a382ecb6 Merge "Updated from global requirements" 2016-11-09 12:05:16 +00:00
Jenkins
59ef5ebda0 Merge "Make method import_versioned_module work" 2016-11-09 08:49:15 +00:00
OpenStack Proposal Bot
e273aa5b55 Updated from global requirements
Change-Id: Ie04ae794555e6266141ff4cdcca0121028685e0e
2016-11-09 04:23:47 +00:00
pawnesh.kumar
fae4ef7a03 Make method import_versioned_module work
Update function import_versioned_module in Oslo.utils 3.17.
This patch update to meet new version. For more information:
http://docs.openstack.org/developer/oslo.utils/history.html

Change-Id: I8b856c4a6a017fdc668326cb18c0b14d6d09ddc2
Closes-Bug: #1627313
2016-11-02 16:51:11 +05:30
Tony Xu
f3ca2e6944 Add Python 3.5 classifier and venv
Now that there is a passing gate job, we can claim
support for Python 3.5 in the classifier.
This patch also adds the convenience py35 venv.

Change-Id: I17e31b7c5218be7ad0b9d3cde9536522d77af24f
2016-10-26 00:09:04 +08:00
Jenkins
ac61ea3499 Merge "Add parameters field for protectable instances API" 2016-10-20 02:31:00 +00:00
Jenkins
e689b3233e Merge "Add a description field for resource plan" 2016-10-06 13:07:28 +00:00
Jenkins
1f422f4c5c Merge "Add trigger update client" 2016-10-06 13:07:19 +00:00
Jenkins
fda6cdfb7b Merge "Remove copy of incubated Oslo code" 2016-10-06 12:57:15 +00:00
OpenStack Proposal Bot
448bb4468f Updated from global requirements
Change-Id: Ic3a9ecca756ec01c986f0028b5bf49dfbb11ca48
2016-09-30 20:05:43 +00:00
ChangBo Guo(gcb)
ce263ecc9e Remove copy of incubated Oslo code
The Oslo team has moved all previously incubated code from the
openstack/oslo-incubator repository into separate library repositories
and released those libraries to the Python Package Index. Many of our
big tent project teams are still using the old, unsupported, incubated
versions of the code. The Oslo team has been working to remove that
incubated code from projects, and the time has come to finish that work.

As one of community-wide goals in Ocata, please see:
https://github.com/openstack/governance/blob/master/goals/ocata/remove-incubated-oslo-code.rst

Note: This commit also fix pep8 violations.

Change-Id: Ic2d8079b85ebd302a27785772462378f13d593d0
2016-09-29 15:33:58 +00:00
Jenkins
d2d3a475e2 Merge "Update homepage with developer documentation page" 2016-09-29 11:05:51 +00:00
yizhihui
e0a2b0edcd Fix restore-create failed with "no attribute 'username'"
Change-Id: I5fa824a82d199fef6620318ccb93f156c69652bd
2016-09-29 12:29:05 +08:00
chenying
49aae96f22 Add parameters field for protectable instances API
Scenario #1
User need a parameter for the region name to query resource
instances from different region endpoint.

Scenario #2
User uses the Protectable Instances API to query database
instances from the verdor's backup software. User must provide
some parameters about authentication to the restfull API of the
verdor's backup software.

A dict type parameter is needed for Protectable Instances API.
And it is optional.

blueprint instances-parameters

Change-Id: I9b5d2dc581edda23543f4b264c334bc429bcd3c3
2016-09-27 15:50:18 +08:00
Tony Xu
6113f97d2f Update homepage with developer documentation page
Change-Id: I4d833e796da4ee02485e197c7ec5603f229e188f
2016-09-27 00:37:45 +08:00
Jenkins
b7ceed787e Merge "Add restore user and password to restore-create" 2016-09-25 08:53:32 +00:00
chenying
f18cf64f9d Add a description field for resource plan
Change-Id: I2191a557b2777a9c7e92b1c9bc0fb153cb7ec2af
2016-09-22 20:19:46 +08:00
Jenkins
c0bddb93d5 Merge "Shell: restore & plan CLI parameters" 2016-09-22 10:53:10 +00:00
OpenStack Proposal Bot
25beeb0819 Updated from global requirements
Change-Id: Ie1ac3466dfc6d820fdae4b0b596281a42aac6a47
2016-09-21 06:48:20 +00:00
zhangshuai
4c6a252b76 Add trigger update client
To keep consistency with api doc, add trigger update client,
and fix plan update client.

Change-Id: Ie2e6e01bde0d3c8a947685874dcb2d82a6a49e12
2016-09-21 11:21:00 +08:00
Yuval Brik
7c3ab24bf1 Add restore user and password to restore-create
Add restore_user and restore_password to restore-create command.
Will be sent in restore body as 'restore_auth'

Change-Id: Iac54ad345c4a43427b0353fde6ca9e159300d522
2016-09-15 16:40:39 +03:00
Yuval Brik
b304d5978d Shell: restore & plan CLI parameters
Current restore and plan parameter are passed in the key=value form,
which doesn't fix the requirement of key=value pairs for each
resource (i.e dictionary).
Change the restore and plan parameters to be passed in one of the
following formats:
1. JSON using the --parameter-json '{"OS::Cinder::Volume": { ... } }'
2. Multiple --parameter option for each resource:
   --parameter resource_type=OS::Cinder::Volume,resource_id=<uuid>,k=v

Change-Id: I416dc1f00060a5c994984ddfc04c30d1a04c803c
2016-09-15 16:35:41 +03:00
Jenkins
bfc9c63952 Merge "Updated from global requirements" 2016-09-05 09:30:20 +00:00
Yuval Brik
0fc2893234 Rename package python-karborclient
Rename package name to be python-karborclient following the project
and repository rename.

Change-Id: I7171e6ef12bc8de7fc6038534dac86a44094ad51
2016-09-04 12:17:33 +03:00
OpenStack Proposal Bot
51e14a5231 Updated from global requirements
Change-Id: I66bcdb9509ff266baf01129f56814836ab975eb6
2016-09-03 02:01:34 +00:00
chenying
7e81c138ec Add metadata parameter to checkpoint API
Change-Id: I86f6e6e8338e8256fec62a61275aeff8669b830c
2016-09-01 14:55:38 +08:00
37 changed files with 2294 additions and 2007 deletions

View File

@@ -1,4 +1,5 @@
[gerrit]
host=review.openstack.org
host=review.opendev.org
port=29418
project=openstack/python-karborclient.git
defaultbranch=stable/ocata

12
.zuul.yaml Normal file
View File

@@ -0,0 +1,12 @@
- project:
templates:
- openstack-python-jobs
- openstack-python35-jobs
- check-requirements
- publish-openstack-sphinx-docs
- openstackclient-plugin-jobs
check:
jobs:
- openstack-tox-cover:
voting: false

View File

@@ -1,3 +1,12 @@
========================
Team and repository tags
========================
.. image:: http://governance.openstack.org/badges/python-karborclient.svg
:target: http://governance.openstack.org/reference/tags/index.html
.. Change things from this point on
Karbor
======
@@ -30,6 +39,7 @@ Karbor Mission Statement
.. _Blueprints: https://blueprints.launchpad.net/python-karborclient
.. _Bugs: https://bugs.launchpad.net/python-karborclient
.. _Source: https://git.openstack.org/cgit/openstack/python-karborclient
.. _Specs: http://docs.openstack.org/developer/karbor/specs/index.html
.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html
@@ -38,6 +48,7 @@ Python Karborclient
python-karborclient is a client library for karbor built on the karbor API.
It provides a Python API (the ``karborclient`` module) and a command-line tool
(``karbor``).
Project Resources
-----------------

View File

@@ -16,4 +16,4 @@ import pbr.version
__version__ = pbr.version.VersionInfo(
'python-smaugclient').version_string()
'python-karborclient').version_string()

View File

@@ -1,18 +1,20 @@
# 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 karborclient.common import utils
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
return client_class(*args, **kwargs)
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_utils import importutils
def Client(version, *args, **kwargs):
module = importutils.import_versioned_module(
'karborclient', version, 'client'
)
client_class = getattr(module, 'Client')
return client_class(*args, **kwargs)

View File

@@ -1,221 +1,218 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import abc
import argparse
import os
import six
from stevedore import extension
from karborclient.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 = "karborclient.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
"""
# 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 karborclient.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 = "karborclient.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in six.iteritems(_discovered_plugins):
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)
def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)
def load_plugin_from_args(args):
"""Load required plugin and populate it with options.
Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.
:type args: argparse.Namespace
:raises: AuthPluginOptionsMissing
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]
def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)
@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.
:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin."""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins."""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.
:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))
def authenticate(self, http_client):
"""Authenticate using plugin defined method.
The method usually analyses `self.opts` and performs
a request to authentication server.
:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication."""
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
missing = [opt
for opt in self.opt_names
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)
@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.
:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

View File

@@ -1,365 +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
try:
import simplejson as json
except ImportError:
import json
import time
from oslo_log import log as logging
from oslo_utils import importutils
import requests
from karborclient.i18n import _
from karborclient.openstack.common.apiclient import exceptions
_logger = logging.getLogger(__name__)
class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "karborclient.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)
# 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
try:
import simplejson as json
except ImportError:
import json
import time
from oslo_log import log as logging
from oslo_utils import importutils
import requests
from karborclient.i18n import _
from karborclient.openstack.common.apiclient import exceptions
_logger = logging.getLogger(__name__)
class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "karborclient.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 karborclient.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
... openstack_client = client.HTTPClient(auth)
... # create nova client
... from novaclient.v1_1 import client
... client.Client(openstack_client)
... # create keystone client
... from keystoneclient.v2_0 import client
... client.Client(openstack_client)
... # use them
... openstack_client.identity.tenants.list()
... openstack_client.compute.servers.list()
"""
service_type = base_client_instance.service_type
if service_type and not hasattr(self, service_type):
setattr(self, service_type, base_client_instance)
def authenticate(self):
self.auth_plugin.authenticate(self)
# Store the authentication results in the keyring for later requests
if self.keyring_saver:
self.keyring_saver.save(self)
class BaseClient(object):
"""Top-level object to access the OpenStack API.
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
will handle a bunch of issues such as authentication.
"""
service_type = None
endpoint_type = None # "publicURL" will be used
cached_endpoint = None
def __init__(self, http_client, extensions=None):
self.http_client = http_client
http_client.add_client(self)
# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))
def client_request(self, method, url, **kwargs):
return self.http_client.client_request(
self, method, url, **kwargs)
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.client_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
@staticmethod
def get_class(api_name, version, version_map):
"""Returns the client class for the requested API version
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = _("Invalid %(api_name)s client version '%(version)s'. "
"Must be one of: %(version_map)s") % {
'api_name': api_name,
'version': version,
'version_map': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)

View File

@@ -1,465 +1,463 @@
# 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 karborclient.i18n 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
message = _("Expectation Failed")
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = _("Unprocessable Entity")
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = _("Internal Server Error")
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = _("Not Implemented")
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = _("Bad Gateway")
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = _("Service Unavailable")
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = _("Gateway Timeout")
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = _("HTTP Version Not Supported")
# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in six.iteritems(vars(sys.modules[__name__]))
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
req_id = response.headers.get("x-openstack-request-id")
#NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": req_id,
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if isinstance(body, dict):
error = list(body.values())[0]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
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)
# 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 karborclient.i18n 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
message = _("Expectation Failed")
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = _("Unprocessable Entity")
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = _("Internal Server Error")
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = _("Not Implemented")
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = _("Bad Gateway")
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = _("Service Unavailable")
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = _("Gateway Timeout")
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = _("HTTP Version Not Supported")
# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in six.iteritems(vars(sys.modules[__name__]))
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
req_id = response.headers.get("x-openstack-request-id")
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": req_id,
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if isinstance(body, dict):
error = list(body.values())[0]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
elif content_type.startswith("text/"):
kwargs["details"] = response.text
try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

View File

@@ -1,180 +1,177 @@
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import requests
import six
from six.moves.urllib import parse
from karborclient.openstack.common.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
if required is None:
required = []
if optional is None:
optional = []
for k in required:
try:
assert k in dct
except AssertionError:
extra_keys = set(dct.keys()).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization.
"""
def __init__(self, data):
super(TestResponse, self).__init__()
self._content_consumed = True
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
# Fake the text attribute to streamline Response creation
text = data.get('text', "")
if isinstance(text, (dict, list)):
self._content = json.dumps(text)
default_headers = {
"Content-Type": "application/json",
}
else:
self._content = text
default_headers = {}
if six.PY3 and isinstance(self._content, six.string_types):
self._content = self._content.encode('utf-8', 'strict')
self.headers = data.get('headers') or default_headers
else:
self.status_code = data
def __eq__(self, other):
return (self.status_code == other.status_code and
self.headers == other.headers and
self._content == other._content)
def __ne__(self, other):
return not self.__eq__(other)
class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and not "auth_plugin" in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called.
"""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import requests
import six
from six.moves.urllib import parse
from karborclient.common.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
if required is None:
required = []
if optional is None:
optional = []
for k in required:
try:
assert k in dct
except AssertionError:
extra_keys = set(dct.keys()).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization."""
def __init__(self, data):
super(TestResponse, self).__init__()
self._content_consumed = True
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
# Fake the text attribute to streamline Response creation
text = data.get('text', "")
if isinstance(text, (dict, list)):
self._content = json.dumps(text)
default_headers = {
"Content-Type": "application/json",
}
else:
self._content = text
default_headers = {}
if six.PY3 and isinstance(self._content, six.string_types):
self._content = self._content.encode('utf-8', 'strict')
self.headers = data.get('headers') or default_headers
else:
self.status_code = data
def __eq__(self, other):
return (self.status_code == other.status_code and
self.headers == other.headers and
self._content == other._content)
def __ne__(self, other):
return not self.__eq__(other)
class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and "auth_plugin" not in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called."""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test."""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@@ -20,8 +20,9 @@ import copy
import six
from six.moves.urllib import parse
from karborclient.common.apiclient import exceptions
from karborclient.common import http
from karborclient.openstack.common.apiclient import exceptions
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'name', 'created_at')

View File

@@ -26,7 +26,9 @@ import requests
import six
from six.moves import urllib
from karborclient.openstack.common.apiclient import exceptions as exc
from karborclient.common.apiclient import exceptions as exc
from karborclient.i18n import _LE
from karborclient.i18n import _LW
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-karborclient'
@@ -48,7 +50,7 @@ def get_system_ca_file():
if os.path.exists(ca):
LOG.debug("Using ca file %s", ca)
return ca
LOG.warning("System ca file could not be found.")
LOG.warning(_LW("System ca file could not be found."))
class HTTPClient(object):
@@ -246,7 +248,7 @@ class HTTPClient(object):
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
LOG.warning(_LW("Use of 'body' is deprecated; use 'data' instead"))
kwargs['data'] = kwargs.pop('body')
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
@@ -258,7 +260,7 @@ class HTTPClient(object):
try:
body = resp.json()
except ValueError:
LOG.error('Could not decode response body as JSON')
LOG.error(_LE('Could not decode response body as JSON'))
else:
body = None
@@ -269,7 +271,7 @@ class HTTPClient(object):
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
LOG.warning(_LW("Use of 'body' is deprecated; use 'data' instead"))
kwargs['data'] = kwargs.pop('body')
# Chunking happens automatically if 'body' is a
# file-like object
@@ -326,7 +328,7 @@ class SessionClient(keystone_adapter.Adapter):
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
LOG.warning(_LW("Use of 'body' is deprecated; use 'data' instead"))
kwargs['data'] = kwargs.pop('body')
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
@@ -351,7 +353,7 @@ class SessionClient(keystone_adapter.Adapter):
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
LOG.warning(_LW("Use of 'body' is deprecated; use 'data' instead"))
kwargs['data'] = kwargs.pop('body')
resp = keystone_adapter.Adapter.request(self,
url,

View File

@@ -12,6 +12,7 @@
from __future__ import print_function
import json
import os
import sys
@@ -19,11 +20,10 @@ import six
import uuid
from oslo_utils import encodeutils
from oslo_utils import importutils
import prettytable
from karborclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
# Decorator for cli-args
@@ -49,13 +49,6 @@ def env(*vars, **kwargs):
return kwargs.get('default', '')
def import_versioned_module(version, submodule=None):
module = 'karborclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
def _print(pt, order):
if sys.version_info >= (3, 0):
print(pt.get_string(sortby=order))
@@ -112,7 +105,7 @@ def print_list(objs, fields, exclude_unavailable=False, formatters=None,
fields.remove(f)
pt = prettytable.PrettyTable((f for f in fields), caching=False)
pt.aligns = ['l' for f in fields]
pt.align = 'l'
for row in rows:
pt.add_row(row)
@@ -123,17 +116,40 @@ def print_list(objs, fields, exclude_unavailable=False, formatters=None,
_print(pt, order_by)
def print_dict(d, property="Property"):
def print_dict(d, property="Property", dict_format_list=None,
json_format_list=None):
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
pt.aligns = ['l', 'l']
pt.align = 'l'
for r in six.iteritems(d):
r = list(r)
if isinstance(r[1], six.string_types) and "\r" in r[1]:
r[1] = r[1].replace("\r", " ")
if dict_format_list is not None and r[0] in dict_format_list:
r[1] = dict_prettyprint(r[1])
if json_format_list is not None and r[0] in json_format_list:
r[1] = json_prettyprint(r[1])
pt.add_row(r)
_print(pt, property)
def dict_prettyprint(val):
"""dict pretty print formatter.
:param val: dict.
:return: formatted json string.
"""
return json.dumps(val, indent=2, sort_keys=True)
def json_prettyprint(val):
"""json pretty print formatter.
:param val: json string.
:return: formatted json string.
"""
return val and json.dumps(json.loads(val), indent=2, sort_keys=True)
def find_resource(manager, name_or_id, *args, **kwargs):
"""Helper for the _find_* methods."""
# first try to get entity as integer id

View File

@@ -1,34 +1,38 @@
# 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
"""
import oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='karborclient')
# 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
# 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
"""
import oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='karborclient')
# 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
def get_available_languages():
return oslo_i18n.get_available_languages('karborclient')

View File

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

View File

@@ -28,13 +28,16 @@ from keystoneclient import session as ksession
from oslo_log import handlers
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import importutils
import six
import six.moves.urllib.parse as urlparse
import karborclient
from karborclient import client as karbor_client
from karborclient.common.apiclient import exceptions as exc
from karborclient.common import utils
from karborclient.openstack.common.apiclient import exceptions as exc
logger = logging.getLogger(__name__)
@@ -149,7 +152,9 @@ class KarborShell(object):
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = utils.import_versioned_module(version, 'shell')
submodule = importutils.import_versioned_module(
'karborclient', version, 'shell'
)
self._find_actions(subparsers, submodule)
self._find_actions(subparsers, self)

View File

@@ -16,8 +16,8 @@ import socket
import mock
import testtools
from karborclient.common.apiclient import exceptions as exc
from karborclient.common import http
from karborclient.openstack.common.apiclient import exceptions as exc
from karborclient.tests.unit import fakes

View File

@@ -23,7 +23,7 @@ from oslo_log import log
import six
from testtools import matchers
from karborclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
import karborclient.shell
from karborclient.tests.unit import base

View File

@@ -88,5 +88,5 @@ class CheckpointsTest(base.TestCaseShell):
'checkpoints'.format(
provider_id=FAKE_PROVIDER_ID),
data={
'checkpoint': {'plan_id': FAKE_PLAN_ID}},
'checkpoint': {'plan_id': FAKE_PLAN_ID, 'extra-info': None}},
headers={})

View File

@@ -48,7 +48,7 @@ class PlansTest(base.TestCaseShell):
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_plan(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.create('Plan name', 'provider_id', '', "")
cs.plans.create('Plan name', 'provider_id', '', "", '')
mock_request.assert_called_with(
'POST',
'/plans',
@@ -56,7 +56,8 @@ class PlansTest(base.TestCaseShell):
'plan': {'provider_id': 'provider_id',
'name': 'Plan name',
'resources': '',
'parameters': ''}},
'parameters': '',
'description': ''}},
headers={})
@mock.patch('karborclient.common.http.HTTPClient.raw_request')

View File

@@ -49,7 +49,9 @@ class RestoresTest(base.TestCaseShell):
cs.restores.create('586cc6ce-e286-40bd-b2b5-dd32694d9944',
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
'192.168.1.2:35357/v2.0',
'{"username": "admin"}')
'{}',
'{"type": "password", "username": "admin", '
'"password": "test"}')
mock_request.assert_called_with(
'POST',
'/restores',
@@ -57,9 +59,11 @@ class RestoresTest(base.TestCaseShell):
'restore':
{
'checkpoint_id': '2220f8b1-975d-4621-a872-fa9afb43cb6c',
'parameters': '{"username": "admin"}',
'parameters': '{}',
'provider_id': '586cc6ce-e286-40bd-b2b5-dd32694d9944',
'restore_target': '192.168.1.2:35357/v2.0'
'restore_target': '192.168.1.2:35357/v2.0',
'restore_auth': '{"type": "password", "username": '
'"admin", "password": "test"}'
}}, headers={})
@mock.patch('karborclient.common.http.HTTPClient.json_request')

View File

@@ -66,7 +66,7 @@ class TriggersTest(base.TestCaseShell):
headers={})
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_plan(self, mock_request):
def test_show_trigger(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.get('1')
mock_request.assert_called_with(
@@ -82,3 +82,18 @@ class TriggersTest(base.TestCaseShell):
'GET',
'/triggers/1',
headers={'X-Configuration-Session': 'fake_session_id'})
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_update_trigger(self, mock_request):
mock_request.return_value = mock_request_return
trigger_id = '123'
data = {"name": "My Trigger",
"properties": {"pattern": "0 10 * * *", "format": "crontab"}}
body = {"trigger_info": data}
cs.triggers.update(trigger_id, data)
mock_request.assert_called_with(
'PUT',
'/triggers/123',
data=body,
headers={}
)

View File

@@ -19,15 +19,26 @@ class Checkpoint(base.Resource):
def __repr__(self):
return "<Checkpoint %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
def get(self):
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
plan = self.protection_plan
if plan is not None:
provider_id = plan.get("provider_id", None)
new = self.manager.get(provider_id, self.id)
if new:
self._add_details(new._info)
else:
return
class CheckpointManager(base.ManagerWithFind):
resource_class = Checkpoint
def create(self, provider_id, plan_id):
body = {'checkpoint': {'plan_id': plan_id}}
def create(self, provider_id, plan_id, checkpoint_extra_info=None):
body = {'checkpoint': {'plan_id': plan_id,
'extra-info': checkpoint_extra_info}}
url = "/providers/{provider_id}/" \
"checkpoints" .format(provider_id=provider_id)
return self._create(url, body, 'checkpoint')

View File

@@ -17,15 +17,14 @@ class Plan(base.Resource):
def __repr__(self):
return "<Plan %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class PlanManager(base.ManagerWithFind):
resource_class = Plan
def create(self, name, provider_id, resources, parameters):
def create(self, name, provider_id, resources, parameters,
description=None):
body = {'plan': {'name': name,
'description': description,
'provider_id': provider_id,
'resources': resources,
'parameters': parameters

View File

@@ -19,17 +19,11 @@ class Protectable(base.Resource):
def __repr__(self):
return "<Protectable %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class Instances(base.Resource):
def __repr__(self):
return "<Instances %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class ProtectableManager(base.ManagerWithFind):
resource_class = Protectable
@@ -77,14 +71,27 @@ class ProtectableManager(base.ManagerWithFind):
sort_dir=sort_dir, sort=sort)
return self._list(url, response_key='instances', obj_class=Instances)
def get_instance(self, type, id, session_id=None):
def get_instance(self, type, id, search_opts=None, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/protectables/{protectable_type}/" \
"instances/{protectable_id}".format(protectable_type=type,
protectable_id=id)
if search_opts is None:
search_opts = {}
query_params = {}
for key, val in search_opts.items():
if val:
query_params[key] = val
query_string = ""
if query_params:
params = sorted(query_params.items(), key=lambda x: x[0])
query_string = "?%s" % parse.urlencode(params)
url = ("/protectables/{protectable_type}/instances/"
"{protectable_id}{query_string}").format(
protectable_type=type, protectable_id=id,
query_string=query_string)
return self._get(url, response_key="instance", headers=headers)
def _build_instances_list_url(self, protectable_type,

View File

@@ -17,9 +17,6 @@ class Provider(base.Resource):
def __repr__(self):
return "<Provider %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class ProviderManager(base.ManagerWithFind):
resource_class = Provider

View File

@@ -17,28 +17,24 @@ class Restore(base.Resource):
def __repr__(self):
return "<Restore %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class RestoreManager(base.ManagerWithFind):
resource_class = Restore
def create(self, provider_id, checkpoint_id, restore_target, parameters):
body = {'restore': {'provider_id': provider_id,
'checkpoint_id': checkpoint_id,
'restore_target': restore_target,
'parameters': parameters,
}
}
def create(self, provider_id, checkpoint_id, restore_target, parameters,
restore_auth):
body = {
'restore': {
'provider_id': provider_id,
'checkpoint_id': checkpoint_id,
'restore_target': restore_target,
'restore_auth': restore_auth,
'parameters': parameters,
}
}
url = "/restores"
return self._create(url, body, 'restore')
def delete(self, restore_id):
path = '/restores/{restore_id}'.format(
restore_id=restore_id)
return self._delete(path)
def get(self, restore_id, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}

View File

@@ -17,9 +17,6 @@ class ScheduledOperation(base.Resource):
def __repr__(self):
return "<ScheduledOperation %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class ScheduledOperationManager(base.ManagerWithFind):
resource_class = ScheduledOperation

View File

@@ -11,15 +11,17 @@
# under the License.
import argparse
import json
import os
from karborclient.common import base
from karborclient.common import utils
from karborclient.openstack.common.apiclient import exceptions
from datetime import datetime
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from karborclient.common.apiclient import exceptions
from karborclient.common import base
from karborclient.common import utils
@utils.arg('--all-tenants',
dest='all_tenants',
@@ -38,6 +40,10 @@ from oslo_utils import uuidutils
metavar='<name>',
default=None,
help='Filters results by a name. Default=None.')
@utils.arg('--description',
metavar='<description>',
default=None,
help='Filters results by a description. Default=None.')
@utils.arg('--status',
metavar='<status>',
default=None,
@@ -51,7 +57,7 @@ from oslo_utils import uuidutils
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
help='Maximum number of plans to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
@@ -82,6 +88,7 @@ def do_plan_list(cs, args):
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'description': args.description,
'status': args.status,
}
@@ -94,7 +101,7 @@ def do_plan_list(cs, args):
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'Provider id', 'Status']
key_list = ['Id', 'Name', 'Description', 'Provider id', 'Status']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
@@ -114,21 +121,33 @@ def do_plan_list(cs, args):
metavar='<id=type=name,id=type=name>',
help='Resource in list must be a dict when creating'
' a plan.The keys of resource are id and type.')
@utils.arg('--parameters',
@utils.arg('--parameters-json',
type=str,
dest='parameters_json',
metavar='<parameters>',
default=None,
help='The parameters of a plan.')
help='Plan parameters in json format.')
@utils.arg('--parameters',
action='append',
metavar='resource_type=<type>[,resource_id=<id>,key=val,...]',
default=[],
help='Plan parameters, may be specified multiple times. '
'resource_type: type of resource to apply parameters. '
'resource_id: limit the parameters to a specific resource. '
'Other keys and values: according to provider\'s protect schema.'
)
@utils.arg('--description',
metavar='<description>',
help='The description of a plan.')
def do_plan_create(cs, args):
"""Create a plan."""
plan_resources = _extract_resources(args)
if args.parameters is not None:
plan_parameters = jsonutils.loads(args.parameters)
else:
plan_parameters = {}
_check_resources(cs, plan_resources)
plan_parameters = _extract_parameters(args)
plan = cs.plans.create(args.name, args.provider_id, plan_resources,
plan_parameters)
utils.print_dict(plan.to_dict())
plan_parameters, description=args.description)
dict_format_list = {"resources", "parameters"}
utils.print_dict(plan.to_dict(), dict_format_list=dict_format_list)
@utils.arg('plan',
@@ -137,7 +156,8 @@ def do_plan_create(cs, args):
def do_plan_show(cs, args):
"""Shows plan details."""
plan = cs.plans.get(args.plan)
utils.print_dict(plan.to_dict())
dict_format_list = {"resources", "parameters"}
utils.print_dict(plan.to_dict(), dict_format_list=dict_format_list)
@utils.arg('plan',
@@ -164,7 +184,7 @@ def do_plan_delete(cs, args):
help="Id of plan to update.")
@utils.arg("--name", metavar="<name>",
help="A name to which the plan will be renamed.")
@utils.arg("--resources", metavar="<id=type,id=type>",
@utils.arg("--resources", metavar="<id=type=name,id=type=name>",
help="Resources to which the plan will be updated.")
@utils.arg("--status", metavar="<suspended|started>",
help="status to which the plan will be updated.")
@@ -205,21 +225,53 @@ def _extract_resources(args):
return resources
def _check_resources(cs, resources):
# check the resource whether it is available
for resource in resources:
try:
instance = cs.protectables.get_instance(
resource["type"], resource["id"])
except exceptions.NotFound:
raise exceptions.CommandError(
"The resource: %s can not be found." % resource["id"])
else:
if instance is None:
raise exceptions.CommandError(
"The resource: %s is invalid." % resource["id"])
@utils.arg('provider_id',
metavar='<provider_id>',
help='Provider id.')
@utils.arg('checkpoint_id',
metavar='<checkpoint_id>',
help='Checkpoint id.')
@utils.arg('restore_target',
@utils.arg('--restore_target',
metavar='<restore_target>',
help='Restore target.')
@utils.arg('--parameters',
type=str,
nargs='*',
metavar='<key=value>',
@utils.arg('--restore_username',
metavar='<restore_username>',
default=None,
help='The parameters of a restore target.')
help='Username to restore target.')
@utils.arg('--restore_password',
metavar='<restore_password>',
default=None,
help='Password to restore target.')
@utils.arg('--parameters-json',
type=str,
dest='parameters_json',
metavar='<parameters>',
default=None,
help='Restore parameters in json format.')
@utils.arg('--parameters',
action='append',
metavar='resource_type=<type>[,resource_id=<id>,key=val,...]',
default=[],
help='Restore parameters, may be specified multiple times. '
'resource_type: type of resource to apply parameters. '
'resource_id: limit the parameters to a specific resource. '
'Other keys and values: according to provider\'s restore schema.'
)
def do_restore_create(cs, args):
"""Create a restore."""
if not uuidutils.is_uuid_like(args.provider_id):
@@ -230,27 +282,66 @@ def do_restore_create(cs, args):
raise exceptions.CommandError(
"Invalid checkpoint id provided.")
if args.parameters is not None:
restore_parameters = _extract_parameters(args)
else:
raise exceptions.CommandError(
"parameters must be provided.")
restore_parameters = _extract_parameters(args)
restore_auth = None
if args.restore_target is not None:
if args.restore_username is None:
raise exceptions.CommandError(
"Must specify username for restore_target.")
if args.restore_password is None:
raise exceptions.CommandError(
"Must specify password for restore_target.")
restore_auth = {
'type': 'password',
'username': args.restore_username,
'password': args.restore_password,
}
restore = cs.restores.create(args.provider_id, args.checkpoint_id,
args.restore_target, restore_parameters)
utils.print_dict(restore.to_dict())
args.restore_target, restore_parameters,
restore_auth)
dict_format_list = {"parameters"}
utils.print_dict(restore.to_dict(), dict_format_list=dict_format_list)
def _extract_parameters(args):
parameters = {}
for data in args.parameters:
# unset doesn't require a val, so we have the if/else
if '=' in data:
(key, value) = data.split('=', 1)
else:
key = data
value = None
if all((args.parameters, args.parameters_json)):
raise exceptions.CommandError(
"Must provide parameters or parameters-json, not both")
if not any((args.parameters, args.parameters_json)):
return {}
if args.parameters_json:
return jsonutils.loads(args.parameters_json)
parameters = {}
for resource_params in args.parameters:
resource_type = None
resource_id = None
parameter = {}
for param_kv in resource_params.split(','):
try:
key, value = param_kv.split('=')
except Exception:
raise exceptions.CommandError(
'parameters must be in the form: key1=val1,key2=val2,...'
)
if key == "resource_type":
resource_type = value
elif key == "resource_id":
if not uuidutils.is_uuid_like(value):
raise exceptions.CommandError('resource_id must be a uuid')
resource_id = value
else:
parameter[key] = value
if resource_type is None:
raise exceptions.CommandError(
'Must specify resource_type for parameters'
)
if resource_id is None:
resource_key = resource_type
else:
resource_key = "%s#%s" % (resource_type, resource_id)
parameters[resource_key] = parameter
parameters[key] = value
return parameters
@@ -274,13 +365,13 @@ def _extract_parameters(args):
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning plans that appear later in the plan '
'list than that represented by this plan id. '
help='Begin returning restores that appear later in the restore '
'list than that represented by this restore id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
help='Maximum number of restores to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
@@ -329,8 +420,10 @@ def do_restore_list(cs, args):
sortby_index = None
else:
sortby_index = 0
formatters = {"Parameters": lambda obj: json.dumps(
obj.parameters, indent=2, sort_keys=True)}
utils.print_list(restores, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
sortby_index=sortby_index, formatters=formatters)
@utils.arg('restore',
@@ -339,7 +432,8 @@ def do_restore_list(cs, args):
def do_restore_show(cs, args):
"""Shows restore details."""
restore = cs.restores.get(args.restore)
utils.print_dict(restore.to_dict())
dict_format_list = {"parameters"}
utils.print_dict(restore.to_dict(), dict_format_list=dict_format_list)
def do_protectable_list(cs, args):
@@ -361,17 +455,30 @@ def do_protectable_show(cs, args):
utils.print_dict(protectable.to_dict())
@utils.arg('protectable_id',
metavar='<protectable_id>',
help='Protectable instance id.')
@utils.arg('protectable_type',
metavar='<protectable_type>',
help='Protectable type.')
@utils.arg('protectable_id',
metavar='<protectable_id>',
help='Protectable instance id.')
@utils.arg('--parameters',
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='Show a instance by parameters key and value pair. '
'Default=None.')
def do_protectable_show_instance(cs, args):
"""Shows instance details."""
search_opts = {
'parameters': (_extract_instances_parameters(args)
if args.parameters else None),
}
instance = cs.protectables.get_instance(args.protectable_type,
args.protectable_id)
utils.print_dict(instance.to_dict())
args.protectable_id,
search_opts=search_opts)
dict_format_list = {"dependent_resources"}
utils.print_dict(instance.to_dict(), dict_format_list=dict_format_list)
@utils.arg('protectable_type',
@@ -384,13 +491,13 @@ def do_protectable_show_instance(cs, args):
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning plans that appear later in the plan '
'list than that represented by this plan id. '
help='Begin returning instances that appear later in the instance '
'list than that represented by this instance id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
help='Maximum number of instances to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
@@ -406,11 +513,20 @@ def do_protectable_show_instance(cs, args):
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--parameters',
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='List instances by parameters key and value pair. '
'Default=None.')
def do_protectable_list_instances(cs, args):
"""Lists all protectable instances."""
search_opts = {
'type': args.type,
'parameters': (_extract_instances_parameters(args)
if args.parameters else None),
}
if args.sort and (args.sort_key or args.sort_dir):
@@ -430,8 +546,24 @@ def do_protectable_list_instances(cs, args):
sortby_index = None
else:
sortby_index = 0
formatters = {"Dependent resources": lambda obj: json.dumps(
obj.dependent_resources, indent=2, sort_keys=True)}
utils.print_list(instances, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
sortby_index=sortby_index, formatters=formatters)
def _extract_instances_parameters(args):
parameters = {}
for parameter in args.parameters:
if '=' in parameter:
(key, value) = parameter.split('=', 1)
else:
key = parameter
value = None
parameters[key] = value
return parameters
@utils.arg('provider_id',
@@ -440,7 +572,8 @@ def do_protectable_list_instances(cs, args):
def do_provider_show(cs, args):
"""Shows provider details."""
provider = cs.providers.get(args.provider_id)
utils.print_dict(provider.to_dict())
dict_format_list = {"extended_info_schema"}
utils.print_dict(provider.to_dict(), dict_format_list=dict_format_list)
@utils.arg('--name',
@@ -454,13 +587,13 @@ def do_provider_show(cs, args):
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning plans that appear later in the plan '
'list than that represented by this plan id. '
help='Begin returning providers that appear later in the provider '
'list than that represented by this provider id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
help='Maximum number of providers to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
@@ -493,7 +626,7 @@ def do_provider_list(cs, args):
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'Description', 'Extended_info_schema']
key_list = ['Id', 'Name', 'Description']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
@@ -509,19 +642,57 @@ def do_provider_list(cs, args):
@utils.arg('plan_id',
metavar='<plan_id>',
help='ID of plan.')
@utils.arg('--extra_info',
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='The extra info of a checkpoint.')
def do_checkpoint_create(cs, args):
"""Create a checkpoint."""
checkpoint = cs.checkpoints.create(args.provider_id, args.plan_id)
utils.print_dict(checkpoint.to_dict())
checkpoint_extra_info = None
if args.extra_info is not None:
checkpoint_extra_info = _extract_extra_info(args)
checkpoint = cs.checkpoints.create(args.provider_id, args.plan_id,
checkpoint_extra_info)
dict_format_list = {"protection_plan"}
json_format_list = {"resource_graph"}
utils.print_dict(checkpoint.to_dict(), dict_format_list=dict_format_list,
json_format_list=json_format_list)
def _extract_extra_info(args):
checkpoint_extra_info = {}
for data in args.extra_info:
# unset doesn't require a val, so we have the if/else
if '=' in data:
(key, value) = data.split('=', 1)
else:
key = data
value = None
checkpoint_extra_info[key] = value
return checkpoint_extra_info
@utils.arg('provider_id',
metavar='<provider_id>',
help='ID of provider.')
@utils.arg('--status',
metavar='<status>',
@utils.arg('--plan_id',
metavar='<plan_id>',
default=None,
help='Filters results by a status. Default=None.')
help='Filters results by a plan_id. Default=None.')
@utils.arg('--start_date',
type=str,
metavar='<start_date>',
default=None,
help='Filters results by a start_date("Y-m-d"). Default=None.')
@utils.arg('--end_date',
type=str,
metavar='<end_date>',
default=None,
help='Filters results by a end_date("Y-m-d"). Default=None.')
@utils.arg('--project_id',
metavar='<project_id>',
default=None,
@@ -529,13 +700,13 @@ def do_checkpoint_create(cs, args):
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning plans that appear later in the plan '
'list than that represented by this plan id. '
help='Begin returning checkpoints that appear later in the '
'checkpoint list than that represented by this checkpoint id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
help='Maximum number of checkpoints to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
@@ -553,9 +724,30 @@ def do_checkpoint_create(cs, args):
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
def do_checkpoint_list(cs, args):
"""Lists all checkpoints."""
if args.plan_id is not None:
if not uuidutils.is_uuid_like(args.plan_id):
raise exceptions.CommandError('The plan_id must be a uuid')
if args.start_date:
try:
datetime.strptime(
args.start_date, "%Y-%m-%d")
except (ValueError, SyntaxError):
raise exceptions.CommandError(
"The format of start_date should be %Y-%m-%d")
if args.end_date:
try:
datetime.strptime(
args.end_date, "%Y-%m-%d")
except (ValueError, SyntaxError):
raise exceptions.CommandError(
"The format of end_date should be %Y-%m-%d")
search_opts = {
'status': args.status,
'plan_id': args.plan_id,
'start_date': args.start_date,
'end_date': args.end_date,
'project_id': args.project_id,
}
@@ -569,14 +761,17 @@ def do_checkpoint_list(cs, args):
marker=args.marker, limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Project id', 'Status', 'Protection plan']
key_list = ['Id', 'Project id', 'Status', 'Protection plan', 'Metadata',
'Created at']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
formatters = {"Protection plan": lambda obj: json.dumps(
obj.protection_plan, indent=2, sort_keys=True)}
utils.print_list(checkpoints, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
sortby_index=sortby_index, formatters=formatters)
@utils.arg('provider_id',
@@ -588,7 +783,10 @@ def do_checkpoint_list(cs, args):
def do_checkpoint_show(cs, args):
"""Shows checkpoint details."""
checkpoint = cs.checkpoints.get(args.provider_id, args.checkpoint_id)
utils.print_dict(checkpoint.to_dict())
dict_format_list = {"protection_plan"}
json_format_list = {"resource_graph"}
utils.print_dict(checkpoint.to_dict(), dict_format_list=dict_format_list,
json_format_list=json_format_list)
@utils.arg('provider_id',
@@ -643,13 +841,13 @@ def do_checkpoint_delete(cs, args):
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning plans that appear later in the plan '
'list than that represented by this plan id. '
help='Begin returning triggers that appear later in the trigger '
'list than that represented by this trigger id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
help='Maximum number of triggers to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
@@ -699,8 +897,11 @@ def do_trigger_list(cs, args):
sortby_index = None
else:
sortby_index = 0
formatters = {"Properties": lambda obj: json.dumps(
obj.properties, indent=2, sort_keys=True)}
utils.print_list(triggers, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
sortby_index=sortby_index, formatters=formatters)
@utils.arg('name',
@@ -710,18 +911,21 @@ def do_trigger_list(cs, args):
metavar='<type>',
help='Type of trigger.')
@utils.arg('properties',
metavar='<key=value:key=value>',
metavar='<key=value,key=value>',
help='Properties of trigger.')
def do_trigger_create(cs, args):
"""Create a trigger."""
trigger_properties = _extract_properties(args)
trigger = cs.triggers.create(args.name, args.type, trigger_properties)
utils.print_dict(trigger.to_dict())
dict_format_list = {"properties"}
utils.print_dict(trigger.to_dict(), dict_format_list=dict_format_list)
def _extract_properties(args):
properties = {}
for data in args.properties.split(':'):
if args.properties is None:
return properties
for data in args.properties.split(','):
if '=' in data:
(resource_key, resource_value) = data.split('=', 1)
else:
@@ -732,13 +936,31 @@ def _extract_properties(args):
return properties
@utils.arg("trigger_id", metavar="<TRIGGER ID>",
help="Id of trigger to update.")
@utils.arg("--name", metavar="<name>",
help="A new name to which the trigger will be renamed.")
@utils.arg("--properties", metavar="<key=value,key=value>",
help="Properties of trigger which will be updated.")
def do_trigger_update(cs, args):
"""Update a trigger."""
trigger_info = {}
trigger_properties = _extract_properties(args)
trigger_info['name'] = args.name
trigger_info['properties'] = trigger_properties
trigger = cs.triggers.update(args.trigger_id, trigger_info)
dict_format_list = {"properties"}
utils.print_dict(trigger.to_dict(), dict_format_list=dict_format_list)
@utils.arg('trigger',
metavar='<trigger>',
help='ID of trigger.')
def do_trigger_show(cs, args):
"""Shows trigger details."""
trigger = cs.triggers.get(args.trigger)
utils.print_dict(trigger.to_dict())
dict_format_list = {"properties"}
utils.print_dict(trigger.to_dict(), dict_format_list=dict_format_list)
@utils.arg('trigger',
@@ -786,6 +1008,10 @@ def do_trigger_delete(cs, args):
metavar='<trigger_id>',
default=None,
help='Filters results by a trigger id. Default=None.')
@utils.arg('--operation_definition',
metavar='<operation_definition>',
default=None,
help='Filters results by the operation_definition. Default=None.')
@utils.arg('--marker',
metavar='<marker>',
default=None,
@@ -828,6 +1054,7 @@ def do_scheduledoperation_list(cs, args):
'name': args.name,
'operation_type': args.operation_type,
'trigger_id': args.trigger_id,
'operation_definition': args.operation_definition,
}
if args.sort and (args.sort_key or args.sort_dir):
@@ -860,7 +1087,7 @@ def do_scheduledoperation_list(cs, args):
metavar='<trigger_id>',
help='Trigger id of scheduled operation.')
@utils.arg('operation_definition',
metavar='<key=value:key=value>',
metavar='<key=value,key=value>',
help='Operation definition of scheduled operation.')
def do_scheduledoperation_create(cs, args):
"""Create a scheduled operation."""
@@ -869,12 +1096,14 @@ def do_scheduledoperation_create(cs, args):
args.operation_type,
args.trigger_id,
operation_definition)
utils.print_dict(scheduledoperation.to_dict())
dict_format_list = {"operation_definition"}
utils.print_dict(scheduledoperation.to_dict(),
dict_format_list=dict_format_list)
def _extract_operation_definition(args):
operation_definition = {}
for data in args.operation_definition.split(':'):
for data in args.operation_definition.split(','):
if '=' in data:
(resource_key, resource_value) = data.split('=', 1)
else:
@@ -891,7 +1120,9 @@ def _extract_operation_definition(args):
def do_scheduledoperation_show(cs, args):
"""Shows scheduledoperation details."""
scheduledoperation = cs.scheduled_operations.get(args.scheduledoperation)
utils.print_dict(scheduledoperation.to_dict())
dict_format_list = {"operation_definition"}
utils.print_dict(scheduledoperation.to_dict(),
dict_format_list=dict_format_list)
@utils.arg('scheduledoperation',

View File

@@ -17,9 +17,6 @@ class Trigger(base.Resource):
def __repr__(self):
return "<Trigger %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class TriggerManager(base.ManagerWithFind):
resource_class = Trigger
@@ -46,6 +43,14 @@ class TriggerManager(base.ManagerWithFind):
trigger_id=trigger_id)
return self._get(url, response_key="trigger_info", headers=headers)
def update(self, trigger_id, data):
body = {"trigger_info": data}
return self._update('/triggers/{trigger_id}'
.format(trigger_id=trigger_id),
body, "trigger_info")
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all triggers."""

View File

@@ -1,8 +0,0 @@
[DEFAULT]
# The list of modules to copy from openstack-common
module=apiclient.exceptions
module=apiclient
# The base module to hold the copy of openstack.common
base=karborclient

View File

@@ -1,13 +1,13 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=1.6 # Apache-2.0
PrettyTable<0.8,>=0.7 # BSD
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
requests>=2.10.0 # Apache-2.0
pbr>=1.8 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
python-keystoneclient>=3.8.0 # Apache-2.0
requests!=2.12.2,>=2.10.0 # Apache-2.0
simplejson>=2.2.0 # MIT
Babel>=2.3.4 # BSD
six>=1.9.0 # MIT
oslo.utils>=3.16.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.utils>=3.18.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0

View File

@@ -1,11 +1,11 @@
[metadata]
name = python-smaugclient
name = python-karborclient
summary = Python client library for Karbor API
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
home-page = http://docs.openstack.org/developer/karbor/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -16,8 +16,8 @@ classifier =
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
[global]
setup-hooks =

View File

@@ -4,10 +4,11 @@
hacking<0.11,>=0.10.2 # Apache-2.0
coverage>=3.6 # Apache-2.0
coverage>=4.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
docutils!=0.13.1,>=0.11 # OSI-Approved Open Source, Public Domain
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
oslosphinx>=4.7.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD

15
tox.ini
View File

@@ -1,14 +1,18 @@
[tox]
minversion = 1.6
envlist = py34,py27,pypy,pep8
minversion = 2.0
envlist = py35,py34,py27,pypy,pep8
skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
install_command = pip install {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
BRANCH_NAME=master
CLIENT_NAME=python-karborclient
PYTHONWARNINGS=default::DeprecationWarning
deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/ocata}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py test --slowest --testr-args='{posargs}'
@@ -20,6 +24,7 @@ commands = {posargs}
[testenv:functional]
setenv =
{[testenv]setenv}
OS_TEST_PATH = ./karborclient/tests/functional
passenv = OS_*
[testenv:cover]
@@ -37,4 +42,4 @@ commands = oslo_debug_helper {posargs}
show-source = True
ignore = E123,E125
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools