Compare commits

..

168 Commits
0.0.2 ... 0.4.0

Author SHA1 Message Date
Jenkins
7bb1cc2c7a Merge "Remove unused None from dict.get()" 2017-07-13 17:53:54 +00:00
Jenkins
b56f70e7fd Merge "Replace six.iteritems() with .items()" 2017-07-13 12:13:25 +00:00
sudhir_agarwal
cd7d456491 Remove unused None from dict.get()
Since the default value is None when can't get a key from a dict,
So there is no need to use dict.get('key', None).

Change-Id: I9f2ea40e13a63992170149936ae4e80b8969c023
2017-07-05 19:09:03 +05:30
sudhir_agarwal
eb70a9d460 Replace six.iteritems() with .items()
1.As mentioned in [1], we should avoid using six.iteritems to achieve
iterators. We can use dict.items instead, as it will return iterators
in PY3 as well. And dict.items/keys will more readable.
2.In py2, the performance about list should be negligible, see the
link [2].
[1] https://wiki.openstack.org/wiki/Python3
[2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html

Change-Id: Ib752ad4c3aed525c4bea9dbd5710172ddbaf193b
2017-07-05 18:26:01 +05:30
Jeremy Liu
5eab398150 Add OpenStackClient plugin for scheduledoperations
This patch adds data protection support to the python-openstackclient
through a plugin for scheduledoperations.

Co-Authored-By: Spencer Yu <yushb@gohighsec.com>
Change-Id: Id2712e3a081e0fd2b69cad093d1a6b26b644e967
Partially-Implements: blueprint karbor-support-python-openstackclient
2017-07-05 17:29:00 +08:00
Jenkins
1a7243354e Merge "Add OpenStackClient plugin for checkpoint" 2017-07-05 09:20:48 +00:00
Jenkins
5ded9c1f19 Merge "Drop MANIFEST.in - it's not needed by pbr" 2017-07-05 09:14:23 +00:00
yushangbin
2fc5904648 Add OpenStackClient plugin for checkpoint
This patch adds dataprotection support to the python-openstackclient
through a plugin for checkpoint.

Partially-Implements: blueprint karbor-support-python-openstackclient
Change-Id: I5c4c927c63ac39538d5565a4167f5836e7127170
2017-07-05 16:52:41 +08:00
Jenkins
71e029c629 Merge "Add OpenStackClient plugin for triggers" 2017-07-05 08:43:06 +00:00
yushangbin
289370ab38 Add OpenStackClient plugin for triggers
This patch adds data protection support to the python-openstackclient
through a plugin for trigger.

Change-Id: I1a19340f47cbb1713ee8da1b238ea18ea5f3bc92
Partially-Implements: blueprint karbor-support-python-openstackclient
2017-07-05 16:02:49 +08:00
Jeremy Liu
795fa730a8 Fix bug in plan deletion when using osc plugin for karbor
Change-Id: Idb5620506dd800428407031eddd525e68b93f414
2017-07-05 15:58:43 +08:00
liyanhang
3f91adc2d8 Drop MANIFEST.in - it's not needed by pbr
This patch removes `MANIFEST.in` file as pbr generates a
sensible manifest from git files and some standard files
and it removes the need for an explicit `MANIFEST.in` file.

Change-Id: I9b3d5b6ca56766389b0a8c69325bbd4f391112b5
2017-07-05 14:14:17 +08:00
yushangbin
1b3b860565 Add OpenStackClient plugin for protectable
This patch adds dataprotection support to the python-openstackclient
through a plugin for protectable.

Partially-Implements: blueprint karbor-support-python-openstackclient
Change-Id: I088919edd7287602baaa9176b3af9d2995e5c116
2017-07-04 21:47:19 +08:00
Jenkins
d358465dcf Merge "Add OpenStackClient plugin for provider" 2017-07-04 11:01:57 +00:00
Jeremy Liu
b11e847b52 Add OpenStackClient plugin for provider
This patch adds data protection support to the python-openstackclient
through a plugin for provider.

Co-Authored-By: Spencer Yu <yushb@gohighsec.com>
Change-Id: I54d731f29c7f27c3b020a0de741d6efe0b062c8b
Partially-Implements: blueprint karbor-support-python-openstackclient
2017-07-04 07:10:30 +00:00
Jeremy Liu
a82f43ae23 Add unit tests for showing a plan/restore
Change-Id: Ibc1e578c9de6bec026411cfc34ea4a6dc83b43f7
2017-07-04 15:03:58 +08:00
Jeremy Liu
9936200bf8 Update spec to match what we've registered in openstackclient
Change-Id: Idacccaa8d61b47311691456a6f2fdf152b244e66
2017-07-04 10:09:40 +08:00
Jenkins
75a7e6276f Merge "Enable coverage report in console output" 2017-07-02 08:37:55 +00:00
OpenStack Proposal Bot
7b2a2e8c92 Updated from global requirements
Change-Id: I502d11c6165b197aace610d192f2028d3005cec9
2017-06-29 21:03:48 +00:00
Kiran_totad
22d5a7ec38 Remove old pep8 ignores that are no longer necessary
Change-Id: Ie2595cd59dd48c58b6afb8852f5743212ca61848
2017-06-29 13:34:35 +05:30
Kiran_totad
6f2feab60d switch to openstackdocstheme
Change-Id: Ie5a86285db70f6b5be237e9c4c84a26b60bd13be
2017-06-29 06:23:52 +00:00
Jenkins
3233c39c52 Merge "Fix help message for plan command." 2017-06-29 03:20:33 +00:00
yushangbin
7eb818c0ee Add OpenStackClient plugin for restore
This patch adds dataprotection support to the python-openstackclient
through a plugin for restore.

Partially-Implements: blueprint karbor-support-python-openstackclient
Change-Id: I48aa68af1dbf199b6de3a648abdf977b6201bf48
2017-06-28 15:05:48 +08:00
Jenkins
b658068fa6 Merge "Updated from global requirements" 2017-06-28 06:34:59 +00:00
Jenkins
378a7a38f9 Merge "Fix a bug for plan delete test." 2017-06-28 06:34:30 +00:00
OpenStack Proposal Bot
1f907c95b8 Updated from global requirements
Change-Id: I49feca2eb4cf9ddd5455003aa3f5d2de22d098df
2017-06-27 12:21:33 +00:00
yushangbin
1bf9f74604 Fix help message for plan command.
Change-Id: I54d96690b45f4bed630892f1f4181540fa48849b
2017-06-27 18:04:18 +08:00
Jeremy Liu
f898a169cb Use 'project' instead of 'tenant' when switching to openstackclient command
We tend to use 'project' rather than 'tenant' when switching to openstackclient
command, such as:

  openstack role add --user <user> --project <project> <role>

Change-Id: I661f1d7ca3ea229ce03376b43867af260943de23
2017-06-27 17:50:36 +08:00
yushangbin
2800c5f1c2 Fix a bug for plan delete test.
Change-Id: Ie8527c2f7d70d54e5ea9ee09e6eeb7515c625437
2017-06-27 14:24:57 +08:00
Yuval Brik
2d1d7624e8 Enable translation of python-karborclient
Change-Id: I39bed8fbf905ba5cdbfdaadc4b7db863aa38924f
2017-06-23 10:25:36 +03:00
chenying
995fe11dbe Add plan commands for OpenStackClinet plugin in karbor
Change-Id: I8e10fc1775cd07271ff5651e104b6b8b26743be3
Partially-Implements: karbor-support-python-openstackclient
2017-06-22 15:12:29 +08:00
OpenStack Proposal Bot
956f0207e7 Updated from global requirements
Change-Id: I124d8200bc0d57416395ff14a1b4ffeeaee90c0f
2017-06-14 16:43:21 +00:00
Jenkins
61a46a6752 Merge "Add OpenStackClient plugin and plan list" 2017-06-14 13:11:13 +00:00
chenying
0d9611ba9b Add OpenStackClient plugin and plan list
This patch adds dataprotection support to the python-openstackclient
through a plugin.

The support can be demonstrated through the implementation of
the karbor command plan-list which is now:
    openstack dataprotection plan list

Partially-Implements: karbor-support-python-openstackclient
Change-Id: I4dfac08fd2b04f9ac254d3aa8fdadc3a1691de0a
2017-06-13 18:58:42 +08:00
chenying
e3f50485fd Spec for openstack client support
This spec is intent on implementing karbor commands in the
python-karborclient repository as python-openstackclient plugins.

Change-Id: I06eea28a922997586e670fe33bf82a6da72cb4a2
Partially-Implements: karbor-support-python-openstackclient
2017-06-05 23:21:23 +08:00
Jenkins
cd75dda7bb Merge "Optimize the link address" 2017-06-01 02:18:21 +00:00
Jeremy Liu
1924aba63a Enable coverage report in console output
This will output coverage rate of every module in console.

Change-Id: If9e24293ae8b4d7293721c2fa49562462d84ce8a
2017-05-18 21:23:00 +08:00
Jenkins
e55840dd82 Merge "Add a verification about the provider_id of plan" 2017-05-18 03:40:34 +00:00
Jenkins
0b630c1de8 Merge "Replace six.iteritems() with .items()" 2017-05-17 07:42:35 +00:00
OpenStack Proposal Bot
3edac93182 Updated from global requirements
Change-Id: I3c6b25063aeb9bf9166a1c515fe268fc9cfa408d
2017-05-17 03:57:47 +00:00
chenying
74f828c876 Add a verification about the provider_id of plan
Change-Id: Ideb96b4d96fec673ffc0815d1f9840911c8afe70
Closes-Bug: #1686765
2017-05-16 22:20:08 +08:00
Jenkins
641dacabc3 Merge "Switch from keystoneclient to keystoneauth" 2017-05-09 08:15:07 +00:00
Jeremy Liu
776ab9fabe Switch from keystoneclient to keystoneauth
keystoneauth was extracted from keystoneclient, this CR replaces usage
of keystoneclient in favor of keystoneauth.

Change-Id: Ia310aa4d72590290cc50e0617842d1f79af3089e
Implements: blueprint use-keystoneauth-instead-of-keystoneclient
2017-05-09 11:03:52 +03:00
Jenkins
df7c98a7ae Merge "Replace http with https" 2017-05-08 09:19:48 +00:00
chenying
e4980fbb1e Delete py34 in setup.cfg and tox.ini
We support py35 now. We do not need python 3.4 in setup.cfg which
declares the explicit supported versions. So it is no need to keep
the supoort for py34.

Change-Id: I6c0faf2642ec9e487173be0afce1d27064c56d4d
2017-04-28 15:27:17 +08:00
yfzhao
6821a3352c Replace http with https
Use https instead of http to ensure the safety without containing our
account/password information

Change-Id: I22d4738f54762a0461d5019f7bac9d855e1aba08
2017-04-26 10:37:42 +08:00
OpenStack Proposal Bot
b16aa98e8a Updated from global requirements
Change-Id: Ic51eb6459bcf84a10b6887c3783a233b5fcdea0b
2017-04-18 16:55:40 +00:00
chenying
1110520952 Add extra_info field to Plans API
User can get the extra_info of resource instances from
the response of this API using karborclient.
User can add a resource instance with the extra information
to a plan.

blueprint instances-extra-info
Change-Id: I436fc7f1aa7a98c7b1809bbf97a1f36ef5f3516d
2017-04-14 16:34:39 +08:00
chenying
b5e952d573 Add extra_info field to protectables API
User can get the extra_info of resource instances from
the response of this API using karborclient.
User can add a resource instance with the extra information
to a plan.

blueprint instances-extra-info
Depends-On: Id97b8e5b3c29283320f5d4aa81d3947505b35671

Change-Id: I132c66d7cced3a105e5357043fe05cd67f69a35e
2017-04-14 10:41:15 +08:00
M V P Nitesh
86b15d38b8 Optimize the link address
Use https instead of http to ensure the safety

Change-Id: Iebd272cac1691bbb89a72cc5f02b4667e85fbc8d
2017-04-11 12:41:46 +05:30
M V P Nitesh
68d0a7ece4 Replace six.iteritems() with .items()
1.As mentioned in [1], we should avoid using six.iteritems to achieve
iterators. We can use dict.items instead, as it will return iterators
in PY3 as well. And dict.items/keys will more readable.
2.In py2, the performance about list should be negligible, see the
link [2].
[1] https://wiki.openstack.org/wiki/Python3
[2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html

Change-Id: I3bdceff37eb43058b9a4c7a1f55c2875eb61c9a4
2017-04-03 12:07:03 +05:30
gaozx
3c92869735 Remove log translations
Log messages are no longer being translated. This removes all use of
the _LE, _LI, and _LW translation markers to simplify logging and to
avoid confusion with new contributions.

See:
http://lists.openstack.org/pipermail/openstack-i18n/2016-November/002574.html
http://lists.openstack.org/pipermail/openstack-dev/2017-March/113365.html

Change-Id: I21f94b34e511ca9fd43332219d15d6d57fcf5ecf
2017-03-21 02:59:26 -04:00
OpenStack Proposal Bot
26f966f91f Updated from global requirements
Change-Id: I0b448725caf6560000bec1230f1b73112c4b61b1
2017-03-07 12:26:58 +00:00
liyanhang
5b122a89bc Fix oslo_debug_helper not running
Specify test directory so that tox won't complain
`ImportError: Start directory is not importable

Closes-Bug: #1666560
Change-Id: Idcf207c321641da5f00b9cee1a50c8626a4d424e
2017-03-05 21:22:14 +08:00
ricolin
8a4995a4fc [Fix gate]Update test requirement
Since pbr already landed and the old version of hacking seems not
work very well with pbr>=2, we should update it to match global
requirement.
Partial-Bug: #1668848

Change-Id: I90910b785795cd07889511495400b1155f1a320f
2017-03-02 20:30:27 +08:00
Jenkins
68c676c46f Merge "Unified help information style" 2017-02-27 06:43:22 +00:00
Jenkins
16fee61954 Merge "protectable list help info error" 2017-02-27 06:43:15 +00:00
Jenkins
c67b1b490f Merge "Command karbor help info error" 2017-02-27 06:43:08 +00:00
chenying
ee725acb94 Fix the errors about parameter when creating a plan
Change-Id: Ibb6ab69c25cf232b15e2138bfc62f91159039a32
Closes-Bug:#1666186
2017-02-20 19:43:01 +08:00
xiangxinyong
1a64452662 protectable list help info error
Modified protectable list help infomation.

Change-Id: I0388992c3494ad4581475cf83a5512a68835576d
2017-02-16 05:42:41 +08:00
xiangxinyong
6b6332bd99 Unified help information style
Change-Id: I08e573ab67c20af065722d79eb7b4e0169b5ae23
2017-02-16 04:11:49 +08:00
xiangxinyong
c68869270e Command karbor help info error
Modeified karbor help info error

Change-Id: I35b919848cc94267cd0b34e0541a0c0e44169137
2017-02-16 04:07:53 +08:00
Jenkins
0eb328ed07 Merge "Help info error" 2017-02-14 09:55:39 +00:00
Jenkins
e2e1e67db2 Merge "'karbor provider-list' help info error" 2017-02-14 07:48:40 +00:00
xiangxinyong
a625f7a702 Help info error
Modefied other command help error info.

Change-Id: I82fa9bd7524677dc84bab33f57477771198b65de
2017-02-14 09:38:50 +08:00
xiangxinyong
71b9c54d0a 'karbor provider-list' help info error
Modified 'karbor provider-list'`s help error info.

Change-Id: I0db08456599c4db96b9effd47db568c460b7ca81
2017-02-14 09:19:58 +08:00
OpenStack Proposal Bot
72a42ed2e7 Updated from global requirements
Change-Id: I6017a7396a657fbe8a58a729814ac7043998b4dd
2017-02-11 17:51:26 +00: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
Jenkins
d8da515092 Merge "Fix package to be 'karborclient'" 2016-08-29 07:54:52 +00:00
Yuval Brik
271998a5e8 Fix package to be 'karborclient'
Package name will be 'python-smaugclient' for pypi, and
'karborclient' for python

Change-Id: I62290f48e2cc5d4b7f837abaaf44d6b21251f57e
2016-08-28 15:02:50 +03:00
zhangshuai
ef4f89da06 fix .coveragerc
Replace karbor as karborclient in .coveragerc,

Change-Id: I1761237c252707411f5e7b4548ce7d5e89d57079
2016-08-26 17:50:37 +08:00
Yuval Brik
6b3ba9f4af Change package name back to smaugclient
Change the package name to smaugclient to allow a sane release

Change-Id: I39843f7c7c46b4fe16f9cb03cfff1647265f83f0
2016-08-23 15:29:33 +03:00
chenying
44f20e3019 Change Smaug to Karbor
There was a decision in the community to change the project name.

Change-Id: I552759662151f424d752ced8f1df2e6664f434e4
2016-08-18 22:59:45 +08:00
OpenStack Proposal Bot
321b6f640a Updated from global requirements
Change-Id: Idc45cd53c60b5f391f2547d56e7e12ca2094dcce
2016-08-09 10:31:15 +00:00
Jenkins
86b59bd334 Merge "Remove discover from test-requirements" 2016-08-09 08:13:04 +00:00
Swapnil Kulkarni (coolsvap)
c25f584fa0 Remove discover from test-requirements
It's only needed for python < 2.7 which is not supported

Change-Id: Ib26c5f2fa5c540cfa1cf3cb6b05a106bb864af64
2016-07-22 04:12:45 +00:00
chenying
2879e0df18 Fix the parameter field not being successfully passed with plan create command
Change-Id: Ie17be4f8db459e66ab0d4e755282a62c471d38ce
Closes-Bug:#1597689
2016-07-21 17:59:14 +08:00
Jenkins
811a4f0ad0 Merge "Fix checkpoint delete error because of provider api update" 2016-07-21 09:25:27 +00:00
Jenkins
9e8380d8d5 Merge "Add __ne__ built-in function" 2016-07-21 09:08:45 +00:00
Jenkins
6d10a66474 Merge "Remove unused LOG" 2016-07-21 09:08:12 +00:00
chenying
6f30c77f41 Fix checkpoint delete error because of provider api update
Change-Id: I455134bc5bdfbfa113ffa303ed86f144903b6504
Closes-Bug:#1597670
2016-07-21 09:02:37 +00:00
zhangshuai
368eaf9aed fix scheduledoperation show and delete
Change-Id: Ie7e3e9b0010d4b42018deafe649a8ed652528830
Closes-Bug:#1603277
2016-07-15 10:13:15 +08:00
zhangshuai
ea3e6e775a fix scheduledoperation list, object has no attribute 'type'
Change-Id: Icb7380cc84684298e5480ddc8a9e0bb0f8160ac5
Closes-Bugs:#1603069
2016-07-14 20:28:19 +08:00
Daiki Kato
4599b87f1c Fixed argment in trigger-create and scheduledoperation-create
Colon was replaced with a semi-colon.

Change-Id: Ia48815a059ae3cb0bd0aab4a787159a733e309e9
2016-07-14 20:26:42 +08:00
liangjingtao
a0a38d5e68 Remove unused LOG
This is to remove unused LOG to keep code clean.

Change-Id: I037a1db59a7297bdcf64fa576652819daf4c6e48
2016-07-09 11:28:27 +08:00
yuyafei
7b16de4908 Add __ne__ built-in function
In Python 3 __ne__ by default delegates to __eq__ and inverts the
result, but in Python 2 they urge you to define __ne__ when you
define __eq__ for it to work properly [1].There are no implied
relationships among the comparison operators. The truth of x==y
does not imply that x!=y is false. Accordingly, when defining
__eq__(), one should also define __ne__() so that the operators
will behave as expected.
[1]https://docs.python.org/2/reference/datamodel.html#object.__ne__

Change-Id: Iae22095146e64081f362601665284b43d0056f48
Closes-Bug: #1586268
2016-07-05 15:38:37 +08:00
OpenStack Proposal Bot
9292e6fb2e Updated from global requirements
Change-Id: I55ba6361c75d6d3622f529624c8fe778d55cd231
2016-06-30 18:49:57 +00:00
Jenkins
25971fd867 Merge "Return actual object, not raw, when creating" 2016-06-28 09:27:19 +00:00
Yuval Brik
0644b6e175 Return actual object, not raw, when creating
When creating objects (checkpoint, plan, restore, scheduled operation,
trigger, etc) return the actual object, and not a raw dictionary.
Because a raw dictionary was returned on create and an actual object was
returned on get, this lead to the following confusion:

  checkpoint = smaug_client.checkpoint.create(...)
  checkpoint_id = checkpoint['id']
  checkpoint = smaug_client.checkpoint.get(...)
  checkpoint_id = checkpoint.id

Change-Id: I30c8eb4468d8fba830a64f336dd6cfa7793f0b48
2016-06-22 10:49:06 +03:00
OpenStack Proposal Bot
20139d066f Updated from global requirements
Change-Id: I78e0fa4c64e4a0916f25a7016c564d2d41861483
2016-06-21 18:05:49 +00:00
Jenkins
4225036154 Merge "The parameters of plan could be an empty dict without being configured" 2016-06-20 14:22:33 +00:00
chenying
db20090283 The parameters of plan could be an empty dict without being configured
Change-Id: Ife1796048a5830df707627aa7fcceae499699bff
2016-06-20 15:41:16 +08:00
OpenStack Proposal Bot
537ce1ed60 Updated from global requirements
Change-Id: I955acc77803e5f42f7f8d4d3a9af262df3e1c493
2016-06-01 13:54:35 +00:00
chenying
18ae2eb061 Fix creating scheduled_operations error
Change-Id: I5f150a6065424adbce12e2dc08a3e1cc784ef410
Closes-Bug:#1577607
2016-05-03 14:54:07 +08:00
Jenkins
34a9e75754 Merge "Add a field parameters to resource plans" 2016-04-22 09:05:20 +00:00
Jenkins
fb37a4ad52 Merge "Remove the project_id in the url of smaugclient resource" 2016-04-22 09:02:39 +00:00
Jenkins
17342e3d01 Merge "Add show protectables instance endpoint" 2016-04-21 09:26:01 +00:00
chenying
66c6ff898c Add a field parameters to resource plans
Change-Id: I89fa8861535c32262f5c538b80000116bfab488c
Closes-Bug:#1571993
2016-04-19 19:55:45 +08:00
chenying
a1f6310356 Remove the project_id in the url of smaugclient resource
The publicURL of smaug endpoint contains the api version
and project_id. So the project_id in the url of smaugclient
resource can be removed.

Change-Id: I972e584d637781fdca4fbd03d5e3cfa81b2d76cc
Closes-Bug: #1570365
2016-04-14 23:36:22 +08:00
Jenkins
77df0bfe89 Merge "Fix missing a resource class Instances" 2016-04-10 06:45:13 +00:00
Tony Breeds
f70d2500ee Manual update from global-requirements
In Ib88adcf0a6ef09349aefd6fba981a399a73ed8dd smaug is added to
projects.txt.

This change manually runs the update code to make sure that
python-smaugclient is ready for inclusion in projects.txt

Change-Id: Id2b7eeb01dcf9a85f3c3c86b414d99b12548485d
2016-04-08 13:16:56 +10:00
Tony Breeds
923dd9b83b Switch to post-versioning
Change-Id: I90c46c1e6b89043dbccaddb493085a3de2014555
2016-04-08 13:16:56 +10:00
chenying
a54697d7bc Add show protectables instance endpoint
Change-Id: I635b1993ef40d4c29e260d28ff11714493bc37a8
Closes-Bug: #1567264
2016-04-07 17:39:45 +08:00
chenying
b8cbc418e4 Fix missing a resource class Instances
Change-Id: I9d45035c0c684bf0e645f36c3aa623b7a079fd30
Closes-Bug: #1564323
2016-03-31 17:45:11 +08:00
81 changed files with 5578 additions and 2431 deletions

View File

@@ -1,7 +1,7 @@
[run]
branch = True
source = smaugclient
omit = smaugclient/tests/*,smaug/openstack/*
source = karborclient
omit = karborclient/tests/*,karborclient/openstack/*
[report]
ignore_errors = True

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ pip-log.txt
# Unit test / coverage reports
.coverage
cover
.tox
nosetests.xml
.testrepository

View File

@@ -1,4 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/python-smaugclient.git
project=openstack/python-karborclient.git

View File

@@ -14,4 +14,4 @@ Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Launchpad, not GitHub:
https://launchpad.net/python-smaugclient
https://launchpad.net/python-karborclient

View File

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

View File

@@ -1,16 +1,25 @@
Smaug
========================
Team and repository tags
========================
.. image:: https://governance.openstack.org/badges/python-karborclient.svg
:target: https://governance.openstack.org/reference/tags/index.html
.. Change things from this point on
Karbor
======
.. image:: https://img.shields.io/pypi/v/python-smaugclient.svg
:target: https://pypi.python.org/pypi/python-smaugclient/
.. image:: https://img.shields.io/pypi/v/python-karborclient.svg
:target: https://pypi.python.org/pypi/python-karborclient/
:alt: Latest Version
.. image:: https://img.shields.io/pypi/dm/python-smaugclient.svg
:target: https://pypi.python.org/pypi/python-smaugclient/
.. image:: https://img.shields.io/pypi/dm/python-karborclient.svg
:target: https://pypi.python.org/pypi/python-karborclient/
:alt: Downloads
Smaug Mission Statement
Karbor Mission Statement
* Formalize Application Data Protection in OpenStack (APIs, Services, Plugins, …)
* Be able to protect Any Resource in OpenStack(as well as their dependencies)
@@ -25,37 +34,39 @@ Smaug Mission Statement
* `Specs`_
* `How to Contribute`_
.. _PyPi: https://pypi.python.org/pypi/python-smaugclient
.. _Launchpad project: https://launchpad.net/python-smaugclient
.. _Blueprints: https://blueprints.launchpad.net/python-smaugclient
.. _Bugs: https://bugs.launchpad.net/python-smaugclient
.. _Source: https://git.openstack.org/cgit/openstack/python-smaugclient
.. _PyPi: https://pypi.python.org/pypi/python-karborclient
.. _Launchpad project: https://launchpad.net/python-karborclient
.. _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
Python Smaugclient
Python Karborclient
-------------------
python-smaugclient is a client library for Smaug built on the Smaug API.
It provides a Python API (the ``smaugclient`` module) and a command-line tool
(``smaug``).
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
-----------------
Project status, bugs, and blueprints are tracked on Launchpad:
* Client bug tracker
* https://launchpad.net/python-smaugclient
* https://launchpad.net/python-karborclient
* Smaug bug tracker
* https://launchpad.net/smaug
* Karbor bug tracker
* https://launchpad.net/karbor
Developer documentation can be found here:
http://docs.openstack.org/developer/smaug
http://docs.openstack.org/developer/karbor
Additional resources are linked from the project wiki page:
https://wiki.openstack.org/wiki/Smaug
https://wiki.openstack.org/wiki/karbor
License
-------

View File

@@ -23,7 +23,7 @@ sys.path.insert(0, os.path.abspath('../..'))
extensions = [
'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
'oslosphinx'
'openstackdocstheme'
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
@@ -37,7 +37,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'python-smaugclient'
project = u'python-karborclient'
copyright = u'2013, OpenStack Foundation'
# If true, '()' will be appended to :func: etc. cross-reference text.
@@ -55,9 +55,15 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]
# html_theme = '_theme'
html_theme = 'openstackdocs'
# html_static_path = ['static']
# openstackdocstheme options
repository_name = 'openstack/python-karborclient'
bug_project = 'python-karborclient'
bug_tag = ''
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project

View File

@@ -1,4 +1,4 @@
Welcome to smaugclient's documentation!
Welcome to karborclient's documentation!
========================================================
Contents:

View File

@@ -4,9 +4,9 @@ Installation
At the command line::
$ pip install python-smaugclient
$ pip install python-karborclient
Or, if you have virtualenvwrapper installed::
$ mkvirtualenv python-smaugclient
$ pip install python-smaugclient
$ mkvirtualenv python-karborclient
$ pip install python-karborclient

View File

@@ -0,0 +1,166 @@
..
This work is licensed under a Creative Commons Attribution 3.0 Unported
License.
http://creativecommons.org/licenses/by/3.0/legalcode
========================================
Karbor support in python-openstackclient
========================================
Implement a new set of karbor commands as python-openstackclient plugins.
Launchpad Blueprint:
https://blueprints.launchpad.net/python-karborclient/+spec/karbor-support-python-openstackclient
Problem Description
===================
python-openstackclient is becoming the default command line client for many
OpenStack projects. Karbor would benefit from implementing all of its client
commands as a single python-openstackclient plugin implemented in the
python-karborclient repository.
Proposed Change
===============
The intent of this spec is to identify the commands to be implemented and
establish conventions for command and argument names. This spec is not
intended to be a full and correct specification of command and argument names.
The details can be left to the code reviews for the commands themselves.
The following conventions will be adopted for command names:
* As the ``OpenStackClient`` convention, the command name shall always take
the following form:
.. code-block:: bash
openstack [<global-options>] <object-1> <action> [<object-2>] \
[command-arguments]
As a example:
The following ``karbor`` commands about plan will be implemented for ``openstack``
initially suggesting these command names:
.. code-block:: bash
karbor plan-create <name> <provider_id> <resources>
openstack data protection plan create <name> <provider_id> <resources>
karbor plan-delete <plan>
openstack data protection plan delete <plan>
karbor plan-list
openstack data protection plan list
karbor plan-show <plan>
openstack data protection plan show <plan>
karbor plan-update <name> <resources> <status>
openstack data protection plan update <name> <resources> <status>
Configuration
-------------
None
Database
--------
None
Public API
----------
None
Public API Security
-------------------
None
Python API
----------
None
CLI (python-karborclient)
------------------------
A new directory named osc will be created under /karborclient/osc
for the ``OpenStackClient`` plugin and the commands mentioned above.
Internal API
------------
None
Guest Agent
-----------
None
Alternatives
------------
None
Dashboard Impact (UX)
=====================
None
Implementation
==============
Assignee(s)
-----------
Primary assignee:
chenying
Milestones
----------
Work Items
----------
CLI commands as stated above.
Unit tests
Upgrade Implications
====================
None
Dependencies
============
python-openstackclient
osc-lib
Testing
=======
Unit tests will be located in: /karborclient/tests/unit/osc/
Documentation Impact
====================
OpenStack Client adoption list will be updated to include python-karborclient.
References
==========
http://docs.openstack.org/developer/python-openstackclient/commands.html
Appendix
========
None

View File

@@ -2,6 +2,6 @@
Usage
========
To use smaugclient in a project::
To use karborclient in a project::
import smaugclient
import karborclient

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 smaugclient.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 smaugclient.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 = "smaugclient.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 _discovered_plugins.items():
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 smaugclient.i18n import _
from smaugclient.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 = "smaugclient.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,462 @@
# 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 smaugclient.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
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 vars(sys.modules[__name__]).items()
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,177 +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 smaugclient.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)
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 smaugclient.common import http
from smaugclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
from karborclient.common import http
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'name', 'created_at')
@@ -160,10 +161,9 @@ class Manager(object):
if detailed:
detail = "/detail"
return ("/v1/%(project_id)s/%(resource_type)s%(detail)s"
return ("/%(resource_type)s%(detail)s"
"%(query_string)s" %
{"project_id": self.project_id,
"resource_type": resource_type, "detail": detail,
{"resource_type": resource_type, "detail": detail,
"query_string": query_string})
def _format_sort_param(self, sort):
@@ -331,6 +331,9 @@ class Resource(object):
return False
return self._info == other._info
def __ne__(self, other):
return not self.__eq__(other)
def is_loaded(self):
return self._loaded

View File

@@ -18,7 +18,7 @@ import hashlib
import os
import socket
import keystoneclient.adapter as keystone_adapter
import keystoneauth1.adapter as keystone_adapter
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
@@ -26,10 +26,10 @@ import requests
import six
from six.moves import urllib
from smaugclient.openstack.common.apiclient import exceptions as exc
from karborclient.common.apiclient import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-smaugclient'
USER_AGENT = 'python-karborclient'
CHUNKSIZE = 1024 * 64 # 64kB
@@ -299,7 +299,7 @@ class HTTPClient(object):
class SessionClient(keystone_adapter.Adapter):
"""Smaug specific keystoneclient Adapter.
"""karbor specific keystoneauth Adapter.
"""
@@ -386,7 +386,7 @@ def _construct_http_client(*args, **kwargs):
'service_type': service_type,
'region_name': region_name,
'service_name': service_name,
'user_agent': 'python-smaugclient',
'user_agent': 'python-karborclient',
}
parameters.update(kwargs)
return SessionClient(**parameters)

View File

@@ -12,21 +12,18 @@
from __future__ import print_function
import json
import os
import sys
import six
import uuid
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import importutils
import prettytable
from smaugclient.openstack.common.apiclient import exceptions
LOG = logging.getLogger(__name__)
from karborclient.common.apiclient import exceptions
# Decorator for cli-args
@@ -46,19 +43,12 @@ def env(*vars, **kwargs):
returns the default defined in kwargs.
"""
for v in vars:
value = os.environ.get(v, None)
value = os.environ.get(v)
if value:
return value
return kwargs.get('default', '')
def import_versioned_module(version, submodule=None):
module = 'smaugclient.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))
@@ -115,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)
@@ -126,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']
for r in six.iteritems(d):
pt.align = 'l'
for r in d.items():
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,28 @@
# 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='smaugclient')
# 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
def get_available_languages():
return oslo_i18n.get_available_languages('karborclient')

View File

@@ -0,0 +1,56 @@
# 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 logging
from osc_lib import utils
LOG = logging.getLogger(__name__)
DEFAULT_DATA_PROTECTION_API_VERSION = '1'
API_VERSION_OPTION = 'os_data_protection_api_version'
API_NAME = 'data_protection'
API_VERSIONS = {
'1': 'karborclient.v1.client.Client',
}
def make_client(instance):
"""Returns a data protection service client"""
data_protection_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
LOG.debug('Instantiating data protection client: %s',
data_protection_client)
client = data_protection_client(
auth=instance.auth,
session=instance.session,
service_type="data-protect"
)
return client
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-data-protection-api-version',
metavar='<data-protection-api-version>',
default=utils.env(
'OS_DATA_PROTECTION_API_VERSION',
default=DEFAULT_DATA_PROTECTION_API_VERSION),
help='Data protection API version, default=' +
DEFAULT_DATA_PROTECTION_API_VERSION +
' (Env: OS_DATA_PROTECTION_API_VERSION)')
return parser

View File

@@ -0,0 +1,205 @@
# 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.
"""Data protection V1 checkpoint action implementations"""
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.common.apiclient import exceptions
from karborclient.i18n import _
from karborclient import utils
class ListCheckpoints(command.Lister):
_description = _("List checkpoints.")
log = logging.getLogger(__name__ + ".ListCheckpoints")
def get_parser(self, prog_name):
parser = super(ListCheckpoints, self).get_parser(prog_name)
parser.add_argument(
'provider_id',
metavar='<provider_id>',
help=_('ID of provider.'),
)
parser.add_argument(
'--plan_id',
metavar='<plan_id>',
default=None,
help=_('Filters results by a plan ID. Default=None.'),
)
parser.add_argument(
'--start_date',
type=str,
metavar='<start_date>',
default=None,
help=_('Filters results by a start date("Y-m-d"). Default=None.'),
)
parser.add_argument(
'--end_date',
type=str,
metavar='<end_date>',
default=None,
help=_('Filters results by a end date("Y-m-d"). Default=None.'),
)
parser.add_argument(
'--project_id',
metavar='<project_id>',
default=None,
help=_('Filters results by a project ID. Default=None.'),
)
parser.add_argument(
'--marker',
metavar='<checkpoint>',
help=_('The last checkpoint ID of the previous page.'),
)
parser.add_argument(
'--limit',
type=int,
metavar='<num-checkpoints>',
help=_('Maximum number of checkpoints to display.'),
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc), "
"multiple keys and directions can be "
"specified separated by comma"),
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
search_opts = {
'plan_id': parsed_args.plan_id,
'start_date': parsed_args.start_date,
'end_date': parsed_args.end_date,
'project_id': parsed_args.project_id,
}
data = data_protection_client.checkpoints.list(
provider_id=parsed_args.provider_id, search_opts=search_opts,
marker=parsed_args.marker, limit=parsed_args.limit,
sort=parsed_args.sort)
column_headers = ['Id', 'Project id', 'Status', 'Protection plan',
'Metadata', 'Created at']
return (column_headers,
(osc_utils.get_item_properties(
s, column_headers
) for s in data))
class ShowCheckpoint(command.ShowOne):
_description = "Shows checkpoint details"
def get_parser(self, prog_name):
parser = super(ShowCheckpoint, self).get_parser(prog_name)
parser.add_argument(
'provider_id',
metavar="<provider_id>",
help=_('Id of provider.')
)
parser.add_argument(
'checkpoint_id',
metavar="<checkpoint_id>",
help=_('Id of checkpoint.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
checkpoint = client.checkpoints.get(parsed_args.provider_id,
parsed_args.checkpoint_id)
checkpoint._info.pop("links", None)
return zip(*sorted(checkpoint._info.items()))
class CreateCheckpoint(command.ShowOne):
_description = "Creates a checkpoint"
def get_parser(self, prog_name):
parser = super(CreateCheckpoint, self).get_parser(prog_name)
parser.add_argument(
'provider_id',
metavar='<provider_id>',
help=_('ID of provider.')
)
parser.add_argument(
'plan_id',
metavar='<plan_id>',
help=_('ID of plan.')
)
parser.add_argument(
'--extra_info',
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help=_('The extra info of a checkpoint.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
checkpoint_extra_info = None
if parsed_args.extra_info is not None:
checkpoint_extra_info = utils.extract_extra_info(parsed_args)
checkpoint = client.checkpoints.create(parsed_args.provider_id,
parsed_args.plan_id,
checkpoint_extra_info)
checkpoint._info.pop("links", None)
return zip(*sorted(checkpoint._info.items()))
class DeleteCheckpoint(command.Command):
_description = "Delete checkpoint"
log = logging.getLogger(__name__ + ".DeleteCheckpoint")
def get_parser(self, prog_name):
parser = super(DeleteCheckpoint, self).get_parser(prog_name)
parser.add_argument(
'provider_id',
metavar='<provider_id>',
help=_('Id of provider.')
)
parser.add_argument(
'checkpoint',
metavar='<checkpoint>',
nargs="+",
help=_('Id of checkpoint.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
failure_count = 0
for checkpoint_id in parsed_args.checkpoint:
try:
client.checkpoints.delete(parsed_args.provider_id,
checkpoint_id)
except exceptions.NotFound:
failure_count += 1
self.log.error(
"Failed to delete '{0}'; checkpoint not found".
format(checkpoint_id))
if failure_count == len(parsed_args.checkpoint):
raise exceptions.CommandError(
"Unable to find and delete any of the "
"specified checkpoint.")

View File

@@ -0,0 +1,265 @@
# 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.
"""Data protection V1 plan action implementations"""
from oslo_utils import uuidutils
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.common.apiclient import exceptions
from karborclient.i18n import _
from karborclient import utils
class ListPlans(command.Lister):
_description = _("List plans.")
log = logging.getLogger(__name__ + ".ListPlans")
def get_parser(self, prog_name):
parser = super(ListPlans, self).get_parser(prog_name)
parser.add_argument(
'--all-projects',
action='store_true',
default=False,
help=_('Include all projects (admin only)'),
)
parser.add_argument(
'--name',
metavar='<name>',
help=_('Filter results by plan name'),
)
parser.add_argument(
'--description',
metavar='<description>',
help=_('Filter results by plan description'),
)
parser.add_argument(
'--status',
metavar='<status>',
help=_('Filter results by status'),
)
parser.add_argument(
'--marker',
metavar='<plan>',
help=_('The last plan ID of the previous page'),
)
parser.add_argument(
'--limit',
type=int,
metavar='<num-plans>',
help=_('Maximum number of plans to display'),
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc) "
"(default: name:asc), multiple keys and directions can be "
"specified separated by comma"),
)
parser.add_argument(
'--project',
metavar='<project>',
help=_('Filter results by a project(admin only)')
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
all_projects = bool(parsed_args.project) or parsed_args.all_projects
search_opts = {
'all_tenants': all_projects,
'project_id': parsed_args.project,
'name': parsed_args.name,
'description': parsed_args.description,
'status': parsed_args.status,
}
data = data_protection_client.plans.list(
search_opts=search_opts, marker=parsed_args.marker,
limit=parsed_args.limit, sort=parsed_args.sort)
column_headers = ['Id', 'Name', 'Description', 'Provider id', 'Status']
return (column_headers,
(osc_utils.get_item_properties(
s, column_headers
) for s in data))
class ShowPlan(command.ShowOne):
_description = "Shows plan details"
def get_parser(self, prog_name):
parser = super(ShowPlan, self).get_parser(prog_name)
parser.add_argument(
'plan',
metavar="<plan>",
help=_('The UUID of the plan.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
plan = osc_utils.find_resource(client.plans, parsed_args.plan)
plan._info.pop("links", None)
return zip(*sorted(plan._info.items()))
class CreatePlan(command.ShowOne):
_description = "Creates a plan"
def get_parser(self, prog_name):
parser = super(CreatePlan, self).get_parser(prog_name)
parser.add_argument(
'name',
metavar='<name>',
help=_('The name of the plan.')
)
parser.add_argument(
'provider_id',
metavar='<provider_id>',
help=_('The UUID of the provider.')
)
parser.add_argument(
'resources',
metavar='<id=type=name=extra_info,id=type=name=extra_info>',
help=_('Resource in list must be a dict when creating'
' a plan. The keys of resource are id ,type, name and '
'extra_info. The extra_info field is optional.')
)
parser.add_argument(
'--parameters-json',
type=str,
dest='parameters_json',
metavar='<parameters>',
default=None,
help=_('Plan parameters in json format.')
)
parser.add_argument(
'--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.')
)
parser.add_argument(
'--description',
metavar='<description>',
help=_('The description of the plan.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
if not uuidutils.is_uuid_like(parsed_args.provider_id):
raise exceptions.CommandError(
"Invalid provider id provided.")
plan_resources = utils.extract_resources(parsed_args)
utils.check_resources(client, plan_resources)
plan_parameters = utils.extract_parameters(parsed_args)
plan = client.plans.create(parsed_args.name, parsed_args.provider_id,
plan_resources, plan_parameters,
description=parsed_args.description)
plan._info.pop("links", None)
return zip(*sorted(plan._info.items()))
class UpdatePlan(command.ShowOne):
_description = "Update a plan"
def get_parser(self, prog_name):
parser = super(UpdatePlan, self).get_parser(prog_name)
parser.add_argument(
"plan_id",
metavar="<PLAN ID>",
help=_("Id of plan to update.")
)
parser.add_argument(
"--name",
metavar="<name>",
help=_("A name to which the plan will be renamed.")
)
parser.add_argument(
"--resources",
metavar="<id=type=name,id=type=name>",
help=_("Resources to which the plan will be updated.")
)
parser.add_argument(
"--status",
metavar="<suspended|started>",
help=_("status to which the plan will be updated.")
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
data = {}
if parsed_args.name is not None:
data['name'] = parsed_args.name
if parsed_args.resources is not None:
plan_resources = utils.extract_resources(parsed_args)
data['resources'] = plan_resources
if parsed_args.status is not None:
data['status'] = parsed_args.status
try:
plan = osc_utils.find_resource(client.plans,
parsed_args.plan_id)
plan = client.plans.update(plan.id, data)
except exceptions.NotFound:
raise exceptions.CommandError(
"Plan %s not found" % parsed_args.plan_id)
else:
plan._info.pop("links", None)
return zip(*sorted(plan._info.items()))
class DeletePlan(command.Command):
_description = "Delete plan"
def get_parser(self, prog_name):
parser = super(DeletePlan, self).get_parser(prog_name)
parser.add_argument(
'plan',
metavar='<plan>',
nargs="+",
help=_('ID of plan.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
failure_count = 0
for plan_id in parsed_args.plan:
try:
plan = osc_utils.find_resource(client.plans, plan_id)
client.plans.delete(plan.id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; plan not "
"found".format(plan_id))
if failure_count == len(parsed_args.plan):
raise exceptions.CommandError(
"Unable to find and delete any of the "
"specified plan.")

View File

@@ -0,0 +1,177 @@
# 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.
"""Data protection V1 protectables action implementations"""
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.i18n import _
from karborclient import utils
class ListProtectables(command.Lister):
_description = _("List protectable types.")
log = logging.getLogger(__name__ + ".ListProtectables")
def get_parser(self, prog_name):
parser = super(ListProtectables, self).get_parser(prog_name)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
data = data_protection_client.protectables.list()
column_headers = ['Protectable type']
return (column_headers,
(osc_utils.get_item_properties(
s, column_headers
) for s in data))
class ShowProtectable(command.ShowOne):
_description = "Shows protectable type details"
def get_parser(self, prog_name):
parser = super(ShowProtectable, self).get_parser(prog_name)
parser.add_argument(
'protectable_type',
metavar="<protectable_type>",
help=_('Protectable type.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
protectable = osc_utils.find_resource(client.protectables,
parsed_args.protectable_type)
protectable._info.pop("links", None)
return zip(*sorted(protectable._info.items()))
class ListProtectableInstances(command.Lister):
_description = _("List protectable instances.")
log = logging.getLogger(__name__ + ".ListProtectableInstances")
def get_parser(self, prog_name):
parser = super(ListProtectableInstances, self).get_parser(prog_name)
parser.add_argument(
'protectable_type',
metavar="<protectable_type>",
help=_('Type of protectable.')
)
parser.add_argument(
'--type',
metavar="<type>",
default=None,
help=_('Filters results by protectable type. Default=None.')
)
parser.add_argument(
'--marker',
metavar="<protectable_instance>",
default=None,
help=_('The last protectable instance ID of the previous page.')
)
parser.add_argument(
'--limit',
metavar="<num-protectable_instances>",
default=None,
help=_('Maximum number of protectable instances to display.')
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc), "
"multiple keys and directions can be "
"specified separated by comma"),
)
parser.add_argument(
'--parameters',
type=str,
nargs='*',
metavar="<key=value>",
default=None,
help=_('List instances by parameters key and value pair. '
'Default=None.')
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
search_opts = {
'type': parsed_args.type,
'parameters': (utils.extract_instances_parameters(parsed_args)
if parsed_args.parameters else None),
}
data = data_protection_client.protectables.list_instances(
parsed_args.protectable_type, search_opts=search_opts,
marker=parsed_args.marker, limit=parsed_args.limit,
sort=parsed_args.sort)
column_headers = ['Id', 'Type', 'Name', 'Dependent resources',
'Extra info']
return (column_headers, data)
class ShowProtectableInstance(command.ShowOne):
_description = "Shows protectable instance details"
def get_parser(self, prog_name):
parser = super(ShowProtectableInstance, self).get_parser(prog_name)
parser.add_argument(
'protectable_type',
metavar="<protectable_type>",
help=_('Protectable type.')
)
parser.add_argument(
'protectable_id',
metavar="<protectable_id>",
help=_('Protectable instance id.')
)
parser.add_argument(
'--parameters',
type=str,
nargs='*',
metavar="<key=value>",
default=None,
help=_('Show a instance by parameters key and value pair. '
'Default=None.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
search_opts = {
'parameters': (utils.extract_instances_parameters(parsed_args)
if parsed_args.parameters else None),
}
instance = client.protectables.get_instance(
parsed_args.protectable_type,
parsed_args.protectable_id,
search_opts=search_opts)
instance._info.pop("links", None)
return zip(*sorted(instance._info.items()))

View File

@@ -0,0 +1,99 @@
# 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.
"""Data protection V1 provider action implementations"""
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.i18n import _
class ListProviders(command.Lister):
_description = _("List providers.")
log = logging.getLogger(__name__ + ".ListProviders")
def get_parser(self, prog_name):
parser = super(ListProviders, self).get_parser(prog_name)
parser.add_argument(
'--name',
metavar='<name>',
help=_('Filters results by a name. Default=None.'),
)
parser.add_argument(
'--description',
metavar='<description>',
help=_('Filters results by a description. Default=None.'),
)
parser.add_argument(
'--marker',
metavar='<provider>',
help=_('The last provider ID of the previous page'),
)
parser.add_argument(
'--limit',
type=int,
metavar='<num-providers>',
help=_('Maximum number of providers to display'),
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc) "
"(default: name:asc), multiple keys and directions can be "
"specified separated by comma"),
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
search_opts = {
'name': parsed_args.name,
'description': parsed_args.description,
}
data = data_protection_client.providers.list(
search_opts=search_opts, marker=parsed_args.marker,
limit=parsed_args.limit, sort=parsed_args.sort)
column_headers = ['Id', 'Name', 'Description']
return (column_headers,
(osc_utils.get_item_properties(
s, column_headers
) for s in data))
class ShowProvider(command.ShowOne):
_description = "Shows provider details"
def get_parser(self, prog_name):
parser = super(ShowProvider, self).get_parser(prog_name)
parser.add_argument(
'provider',
metavar="<provider>",
help=_('The UUID of the provider.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
provider = osc_utils.find_resource(client.providers,
parsed_args.provider)
provider._info.pop("links", None)
return zip(*sorted(provider._info.items()))

View File

@@ -0,0 +1,195 @@
# 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.
"""Data protection V1 restore action implementations"""
from oslo_utils import uuidutils
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.common.apiclient import exceptions
from karborclient.i18n import _
from karborclient import utils
class ListRestores(command.Lister):
_description = _("List restores.")
log = logging.getLogger(__name__ + ".ListRestores")
def get_parser(self, prog_name):
parser = super(ListRestores, self).get_parser(prog_name)
parser.add_argument(
'--all-projects',
action='store_true',
default=False,
help=_('Include all projects (admin only)'),
)
parser.add_argument(
'--status',
metavar='<status>',
help=_('Filter results by status'),
)
parser.add_argument(
'--marker',
metavar='<restore>',
help=_('The last restore ID of the previous page'),
)
parser.add_argument(
'--limit',
type=int,
metavar='<num-restores>',
help=_('Maximum number of restores to display'),
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc), "
"multiple keys and directions can be "
"specified separated by comma"),
)
parser.add_argument(
'--project',
metavar='<project>',
help=_('Filter results by a project(admin only)')
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
all_projects = bool(parsed_args.project) or parsed_args.all_projects
search_opts = {
'all_tenants': all_projects,
'project_id': parsed_args.project,
'status': parsed_args.status,
}
data = data_protection_client.restores.list(
search_opts=search_opts, marker=parsed_args.marker,
limit=parsed_args.limit, sort=parsed_args.sort)
column_headers = ['Id', 'Project id', 'Provider id', 'Checkpoint id',
'Restore target', 'Parameters', 'Status']
return (column_headers,
(osc_utils.get_item_properties(
s, column_headers
) for s in data))
class ShowRestore(command.ShowOne):
_description = "Shows restore details"
def get_parser(self, prog_name):
parser = super(ShowRestore, self).get_parser(prog_name)
parser.add_argument(
'restore',
metavar="<restore>",
help=_('The UUID of the restore.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
restore = osc_utils.find_resource(client.restores, parsed_args.restore)
restore._info.pop("links", None)
return zip(*sorted(restore._info.items()))
class CreateRestore(command.ShowOne):
_description = "Creates a restore"
def get_parser(self, prog_name):
parser = super(CreateRestore, self).get_parser(prog_name)
parser.add_argument(
'provider_id',
metavar='<provider_id>',
help=_('The UUID of the provider.')
)
parser.add_argument(
'checkpoint_id',
metavar='<checkpoint_id>',
help=_('The UUID of the checkpoint.')
)
parser.add_argument(
'--restore_target',
metavar='<restore_target>',
help=_('The target of the restore operation.')
)
parser.add_argument(
'--restore_username',
metavar='<restore_username>',
default=None,
help=_('Username to restore target.')
)
parser.add_argument(
'--restore_password',
metavar='<restore_password>',
default=None,
help=_('Password to restore target.')
)
parser.add_argument(
'--parameters-json',
type=str,
dest='parameters_json',
metavar='<parameters>',
default=None,
help=_('Restore parameters in json format.')
)
parser.add_argument(
'--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.")
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
if not uuidutils.is_uuid_like(parsed_args.provider_id):
raise exceptions.CommandError(
"Invalid provider id provided.")
if not uuidutils.is_uuid_like(parsed_args.checkpoint_id):
raise exceptions.CommandError(
"Invalid checkpoint id provided.")
restore_parameters = utils.extract_parameters(parsed_args)
restore_auth = None
if parsed_args.restore_target is not None:
if parsed_args.restore_username is None:
raise exceptions.CommandError(
"Must specify username for restore_target.")
if parsed_args.restore_password is None:
raise exceptions.CommandError(
"Must specify password for restore_target.")
restore_auth = {
'type': 'password',
'username': parsed_args.restore_username,
'password': parsed_args.restore_password,
}
restore = client.restores.create(parsed_args.provider_id,
parsed_args.checkpoint_id,
parsed_args.restore_target,
restore_parameters, restore_auth)
restore._info.pop("links", None)
return zip(*sorted(restore._info.items()))

View File

@@ -0,0 +1,208 @@
# 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.
"""Data protection V1 scheduled_operations action implementations"""
import six
from oslo_utils import uuidutils
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.common.apiclient import exceptions
from karborclient.i18n import _
class ListScheduledOperations(command.Lister):
_description = _("List scheduled_operations.")
log = logging.getLogger(__name__ + ".ListScheduledOperations")
def get_parser(self, prog_name):
parser = super(ListScheduledOperations, self).get_parser(prog_name)
parser.add_argument(
'--all-projects',
action='store_true',
default=False,
help=_('Shows details for all tenants. Admin only.'),
)
parser.add_argument(
'--name',
metavar='<name>',
help=_('Filters results by a name. Default=None.'),
)
parser.add_argument(
'--operation_type',
metavar='<operation_type>',
default=None,
help=_('Filters results by a type. Default=None.'),
)
parser.add_argument(
'--trigger_id',
metavar='<trigger_id>',
default=None,
help=_('Filters results by a trigger id. Default=None.'),
)
parser.add_argument(
'--operation_definition',
metavar='<operation_definition>',
default=None,
help=_('Filters results by a operation definition. Default=None.'),
)
parser.add_argument(
'--marker',
metavar='<scheduled_operations>',
help=_('The last scheduled_operations ID of the previous page'),
)
parser.add_argument(
'--limit',
type=int,
metavar='<num-scheduled_operations>',
help=_('Maximum number of scheduled_operations to display'),
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc) "
"(default: name:asc), multiple keys and directions can be "
"specified separated by comma"),
)
parser.add_argument(
'--project',
metavar='<project>',
help=_('Filter results by a project(admin only)')
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
all_projects = bool(parsed_args.project) or parsed_args.all_projects
search_opts = {
'all_tenants': all_projects,
'project_id': parsed_args.project,
'name': parsed_args.name,
'operation_type': parsed_args.operation_type,
'trigger_id': parsed_args.trigger_id,
'operation_definition': parsed_args.operation_definition,
}
data = data_protection_client.scheduled_operations.list(
search_opts=search_opts, marker=parsed_args.marker,
limit=parsed_args.limit, sort=parsed_args.sort)
column_headers = ['Id', 'Name', 'OperationType', 'TriggerId',
'OperationDefinition']
scheduled_operations = []
for s in data:
scheduled_operation = (s.id, s.name, s.operation_type,
s.trigger_id, s.operation_definition)
scheduled_operations.append(scheduled_operation)
return (column_headers, scheduled_operations)
class ShowScheduledOperation(command.ShowOne):
_description = "Shows scheduled_operation details"
def get_parser(self, prog_name):
parser = super(ShowScheduledOperation, self).get_parser(prog_name)
parser.add_argument(
'scheduledoperation',
metavar="<scheduledoperation>",
help=_('The UUID of the scheduledoperation.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
so = osc_utils.find_resource(client.scheduled_operations,
parsed_args.scheduledoperation)
so._info.pop("links", None)
return zip(*sorted(six.iteritems(so._info)))
class CreateScheduledOperation(command.ShowOne):
_description = "Creates a scheduled operation"
def get_parser(self, prog_name):
parser = super(CreateScheduledOperation, self).get_parser(prog_name)
parser.add_argument(
'name',
metavar='<name>',
help=_('The name of the scheduled operation.')
)
parser.add_argument(
'operation_type',
metavar='<operation_type>',
help=_('Operation Type of scheduled operation.')
)
parser.add_argument(
'trigger_id',
metavar='<trigger_id>',
help=_('Trigger id of scheduled operation.')
)
parser.add_argument(
'operation_definition',
metavar='<key=value,key=value>',
help=_('Operation definition of scheduled operation.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
if not uuidutils.is_uuid_like(parsed_args.trigger_id):
raise exceptions.CommandError(
"Invalid trigger id provided.")
so = client.scheduled_operations.create(
parsed_args.name, parsed_args.operation_type,
parsed_args.trigger_id, parsed_args.operation_definition)
so._info.pop("links", None)
return zip(*sorted(six.iteritems(so._info)))
class DeleteScheduledOperation(command.Command):
_description = "Delete scheduled operation"
def get_parser(self, prog_name):
parser = super(DeleteScheduledOperation, self).get_parser(prog_name)
parser.add_argument(
'scheduledoperation',
metavar='<scheduledoperation>',
nargs="+",
help=_('ID of scheduled operation.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
failure_count = 0
for so_id in parsed_args.scheduledoperation:
try:
so = osc_utils.find_resource(client.scheduled_operations,
so_id)
client.scheduled_operations.delete(so.id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '%s'; scheduled operation "
"not found" % so_id)
if failure_count == len(parsed_args.scheduledoperation):
raise exceptions.CommandError(
"Unable to find and delete any of the "
"specified scheduled operation.")

View File

@@ -0,0 +1,229 @@
# 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.
"""Data protection V1 triggers action implementations"""
from osc_lib.command import command
from osc_lib import utils as osc_utils
from oslo_log import log as logging
from karborclient.common.apiclient import exceptions
from karborclient.i18n import _
from karborclient import utils
class ListTriggers(command.Lister):
_description = _("List triggers.")
log = logging.getLogger(__name__ + ".ListTriggers")
def get_parser(self, prog_name):
parser = super(ListTriggers, self).get_parser(prog_name)
parser.add_argument(
'--all-projects',
action='store_true',
default=False,
help=_('Shows details for all tenants. Admin only.'),
)
parser.add_argument(
'--name',
metavar='<name>',
default=None,
help=_('Filters results by a name. Default=None.'),
)
parser.add_argument(
'--type',
metavar='<type>',
default=None,
help=_('Filters results by a type. Default=None.'),
)
parser.add_argument(
'--properties',
metavar='<properties>',
default=None,
help=_('Filters results by a properties. Default=None.'),
)
parser.add_argument(
'--marker',
metavar='<trigger>',
help=_('The last trigger ID of the previous page'),
)
parser.add_argument(
'--limit',
type=int,
metavar='<num-triggers>',
help=_('Maximum number of triggers to display'),
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
default=None,
help=_("Sort output by selected keys and directions(asc or desc) "
"(default: name:asc), multiple keys and directions can be "
"specified separated by comma"),
)
parser.add_argument(
'--project',
metavar='<project>',
help=_('Display information from single tenant (Admin only).')
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
data_protection_client = self.app.client_manager.data_protection
all_projects = bool(parsed_args.project) or parsed_args.all_projects
search_opts = {
'all_tenants': all_projects,
'project_id': parsed_args.project,
'name': parsed_args.name,
'type': parsed_args.type,
'properties': parsed_args.properties,
}
data = data_protection_client.triggers.list(
search_opts=search_opts, marker=parsed_args.marker,
limit=parsed_args.limit, sort=parsed_args.sort)
column_headers = ['Id', 'Name', 'Type', 'Properties']
return (column_headers,
(osc_utils.get_item_properties(
s, column_headers
) for s in data))
class ShowTrigger(command.ShowOne):
_description = "Shows trigger details"
def get_parser(self, prog_name):
parser = super(ShowTrigger, self).get_parser(prog_name)
parser.add_argument(
'trigger',
metavar="<trigger>",
help=_('The UUID of the trigger.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
trigger = osc_utils.find_resource(client.triggers, parsed_args.trigger)
trigger._info.pop("links", None)
return zip(*sorted(trigger._info.items()))
class CreateTrigger(command.ShowOne):
_description = "Creates a trigger"
def get_parser(self, prog_name):
parser = super(CreateTrigger, self).get_parser(prog_name)
parser.add_argument(
'name',
metavar='<name>',
help=_('The name of the trigger.')
)
parser.add_argument(
'type',
metavar='<type>',
help=_('Type of trigger.')
)
parser.add_argument(
'properties',
metavar='<key=value,key=value>',
help=_('Properties of trigger.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
trigger = client.triggers.create(parsed_args.name, parsed_args.type,
parsed_args.properties)
trigger._info.pop("links", None)
return zip(*sorted(trigger._info.items()))
class UpdateTrigger(command.ShowOne):
_description = "Update a trigger"
def get_parser(self, prog_name):
parser = super(UpdateTrigger, self).get_parser(prog_name)
parser.add_argument(
"trigger_id",
metavar="<TRIGGER ID>",
help=_("Id of trigger to update.")
)
parser.add_argument(
"--name",
metavar="<name>",
help=_("A name to which the trigger will be renamed.")
)
parser.add_argument(
"--properties",
metavar="<key=value,key=value>",
help=_("Properties of trigger which will be updated.")
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
data = {}
if parsed_args.name is not None:
data['name'] = parsed_args.name
if parsed_args.properties is not None:
trigger_properties = utils.extract_properties(parsed_args)
data['properties'] = trigger_properties
try:
trigger = osc_utils.find_resource(client.triggers,
parsed_args.trigger_id)
trigger = client.triggers.update(trigger.id, data)
except exceptions.NotFound:
raise exceptions.CommandError(
"Trigger %s not found" % parsed_args.trigger_id)
else:
trigger._info.pop("links", None)
return zip(*sorted(trigger._info.items()))
class DeleteTrigger(command.Command):
_description = "Delete trigger"
log = logging.getLogger(__name__ + ".DeleteTrigger")
def get_parser(self, prog_name):
parser = super(DeleteTrigger, self).get_parser(prog_name)
parser.add_argument(
'trigger',
metavar='<trigger>',
nargs="+",
help=_('ID of trigger.')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.data_protection
failure_count = 0
for trigger_id in parsed_args.trigger:
try:
trigger = osc_utils.find_resource(client.triggers, trigger_id)
client.triggers.delete(trigger.id)
except exceptions.NotFound:
failure_count += 1
self.log.error(
"Failed to delete '{0}'; trigger not found".
format(trigger_id))
if failure_count == len(parsed_args.trigger):
raise exceptions.CommandError(
"Unable to find and delete any of the "
"specified trigger.")

View File

@@ -11,48 +11,58 @@
# under the License.
"""
Command-line interface to the Smaug Project.
Command-line interface to the karbor Project.
"""
from __future__ import print_function
import argparse
import copy
import sys
from keystoneclient.auth.identity.generic import password
from keystoneclient.auth.identity.generic import token
from keystoneclient.auth.identity import v3 as identity
from keystoneclient import discover
from keystoneclient import exceptions as ks_exc
from keystoneclient import session as ksession
from keystoneauth1 import discover
from keystoneauth1 import exceptions as ks_exc
from keystoneauth1.identity.generic import password
from keystoneauth1.identity.generic import token
from keystoneauth1 import loading
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 smaugclient
from smaugclient import client as smaug_client
from smaugclient.common import utils
from smaugclient.openstack.common.apiclient import exceptions as exc
import karborclient
from karborclient import client as karbor_client
from karborclient.common.apiclient import exceptions as exc
from karborclient.common import utils
logger = logging.getLogger(__name__)
class SmaugShell(object):
class KarborShell(object):
def _append_global_identity_args(self, parser):
# Register the CLI arguments that have moved to the session object.
ksession.Session.register_cli_options(parser)
def _append_global_identity_args(self, parser, argv):
loading.register_session_argparse_arguments(parser)
# Peek into argv to see if os-auth-token (or the deprecated
# os_auth_token) or the new os-token or the environment variable
# OS_AUTH_TOKEN were given. In which case, the token auth plugin is
# what the user wants. Else, we'll default to password.
default_auth_plugin = 'password'
token_opts = ['os-token', 'os-auth-token', 'os_auth-token']
if argv and any(i in token_opts for i in argv):
default_auth_plugin = 'token'
loading.register_auth_argparse_arguments(
parser, argv, default=default_auth_plugin)
identity.Password.register_argparse_arguments(parser)
def get_base_parser(self):
def get_base_parser(self, argv):
parser = argparse.ArgumentParser(
prog='smaug',
prog='karbor',
description=__doc__.strip(),
epilog='See "smaug help COMMAND" '
epilog='See "karbor help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=HelpFormatter,
@@ -65,13 +75,13 @@ class SmaugShell(object):
parser.add_argument('--version',
action='version',
version=smaugclient.__version__,
version=karborclient.__version__,
help="Show program's version number and exit.")
parser.add_argument('-d', '--debug',
default=bool(utils.env('SMAUGCLIENT_DEBUG')),
default=bool(utils.env('KARBORCLIENT_DEBUG')),
action='store_true',
help='Defaults to env[SMAUGCLIENT_DEBUG].')
help='Defaults to env[KARBORCLIENT_DEBUG].')
parser.add_argument('-v', '--verbose',
default=False, action="store_true",
@@ -96,11 +106,11 @@ class SmaugShell(object):
'API response, '
'defaults to system socket timeout.')
parser.add_argument('--os-tenant-id',
parser.add_argument('--os_tenant_id',
default=utils.env('OS_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID].')
parser.add_argument('--os-tenant-name',
parser.add_argument('--os_tenant_name',
default=utils.env('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME].')
@@ -117,14 +127,14 @@ class SmaugShell(object):
action='store_true',
help="Do not contact keystone for a token. "
"Defaults to env[OS_NO_CLIENT_AUTH].")
parser.add_argument('--smaug-url',
default=utils.env('SMAUG_URL'),
help='Defaults to env[SMAUG_URL].')
parser.add_argument('--karbor-url',
default=utils.env('KARBOR_URL'),
help='Defaults to env[KARBOR_URL].')
parser.add_argument('--smaug-api-version',
parser.add_argument('--karbor-api-version',
default=utils.env(
'SMAUG_API_VERSION', default='1'),
help='Defaults to env[SMAUG_API_VERSION] '
'KARBOR_API_VERSION', default='1'),
help='Defaults to env[KARBOR_API_VERSION] '
'or 1.')
parser.add_argument('--os-service-type',
@@ -136,20 +146,22 @@ class SmaugShell(object):
help='Defaults to env[OS_ENDPOINT_TYPE].')
parser.add_argument('--include-password',
default=bool(utils.env('SMAUG_INCLUDE_PASSWORD')),
default=bool(utils.env('KARBOR_INCLUDE_PASSWORD')),
action='store_true',
help='Send os-username and os-password to smaug.')
help='Send os-username and os-password to karbor.')
self._append_global_identity_args(parser)
self._append_global_identity_args(parser, argv)
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
def get_subcommand_parser(self, version, argv=None):
parser = self.get_base_parser(argv)
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)
@@ -192,7 +204,7 @@ class SmaugShell(object):
v2_auth_url = None
v3_auth_url = None
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
ks_discover = discover.Discover(session=session, url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except ks_exc.ClientException as e:
@@ -284,16 +296,17 @@ class SmaugShell(object):
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
base_argv = copy.deepcopy(argv)
parser = self.get_base_parser(argv)
(options, args) = parser.parse_known_args(base_argv)
self._setup_logging(options.debug)
# build available subcommands based on version
api_version = options.smaug_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
api_version = options.karbor_api_version
subcommand_parser = self.get_subcommand_parser(api_version, argv)
self.parser = subcommand_parser
keystone_session = None
ks_session = None
keystone_auth = None
# Handle top-level --help/-h before attempting to parse
@@ -320,11 +333,11 @@ class SmaugShell(object):
" env[OS_AUTH_TOKEN]")
if args.os_no_client_auth:
if not args.smaug_url:
if not args.karbor_url:
raise exc.CommandError(
"If you specify --os-no-client-auth"
" you must also specify a Smaug API URL"
" via either --smaug-url or env[SMAUG_URL]")
" you must also specify a Karbor API URL"
" via either --karbor-url or env[KARBOR_URL]")
else:
# Tenant name or ID is needed to make keystoneclient retrieve a
@@ -343,10 +356,10 @@ class SmaugShell(object):
" either --os-auth-url or via"
" env[OS_AUTH_URL]")
endpoint = args.smaug_url
endpoint = args.karbor_url
if args.os_no_client_auth:
# Authenticate through smaug, don't use session
# Authenticate through karbor, don't use session
kwargs = {
'username': args.os_username,
'password': args.os_password,
@@ -361,12 +374,12 @@ class SmaugShell(object):
kwargs['region_name'] = args.os_region_name
else:
# Create a keystone session and keystone auth
keystone_session = ksession.Session.load_from_cli_options(args)
ks_session = loading.load_session_from_argparse_arguments(args)
project_id = args.os_project_id or args.os_tenant_id
project_name = args.os_project_name or args.os_tenant_name
keystone_auth = self._get_keystone_auth(
keystone_session,
ks_session,
args.os_auth_url,
username=args.os_username,
user_id=args.os_user_id,
@@ -380,16 +393,15 @@ class SmaugShell(object):
project_domain_name=args.os_project_domain_name)
endpoint_type = args.os_endpoint_type or 'publicURL'
service_type = args.os_service_type or 'application-catalog'
service_type = args.os_service_type or 'data-protect'
if not endpoint:
endpoint = keystone_auth.get_endpoint(
keystone_session,
service_type=service_type,
region_name=args.os_region_name)
endpoint = keystone_auth.get_endpoint(
ks_session,
service_type=service_type,
region_name=args.os_region_name)
kwargs = {
'session': keystone_session,
'session': ks_session,
'auth': keystone_auth,
'service_type': service_type,
'endpoint_type': endpoint_type,
@@ -399,7 +411,7 @@ class SmaugShell(object):
if args.api_timeout:
kwargs['timeout'] = args.api_timeout
client = smaug_client.Client(api_version, endpoint, **kwargs)
client = karbor_client.Client(api_version, endpoint, **kwargs)
args.func(client, args)
@@ -441,10 +453,10 @@ class HelpFormatter(argparse.HelpFormatter):
def main(args=sys.argv[1:]):
try:
SmaugShell().main(args)
KarborShell().main(args)
except KeyboardInterrupt:
print('... terminating smaug client', file=sys.stderr)
print('... terminating karbor client', file=sys.stderr)
sys.exit(1)
except Exception as e:
if '--debug' in args or '-d' in args:

View File

@@ -44,17 +44,20 @@ class TestResponse(requests.Response):
self._text = None
super(TestResponse, self)
if isinstance(data, dict):
self.status_code = data.get('status_code', None)
self.headers = data.get('headers', None)
self.status_code = data.get('status_code')
self.headers = data.get('headers')
self.reason = data.get('reason', '')
# Fake the text attribute to streamline Response creation
self._text = data.get('text', None)
self._text = data.get('text')
else:
self.status_code = data
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __ne__(self, other):
return not self.__eq__(other)
@property
def text(self):
return self._text

View File

@@ -0,0 +1 @@
__author__ = 'c00179918'

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2015 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
from osc_lib.tests import utils
class TestDataProtection(utils.TestCommand):
def setUp(self):
super(TestDataProtection, self).setUp()
self.app.client_manager.data_protection = mock.Mock()
self.app.client_manager.network = mock.Mock()
self.app.client_manager.compute = mock.Mock()
self.app.client_manager.volume = mock.Mock()

View File

@@ -0,0 +1,160 @@
# 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.osc.v1 import checkpoints as osc_checkpoints
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import checkpoints
CHECKPOINT_INFO = {
"id": "dcb20606-ad71-40a3-80e4-ef0fafdad0c3",
"project_id": "e486a2f49695423ca9c47e589b948108",
"status": "available",
"protection_plan": {
"id": "3523a271-68aa-42f5-b9ba-56e5200a2ebb",
"name": "My application",
"provider_id": "cf56bd3e-97a7-4078-b6d5-f36246333fd9",
"resources": [{
"id": "99777fdd-8a5b-45ab-ba2c-52420008103f",
"type": "OS::Glance::Image",
"name": "cirros-0.3.4-x86_64-uec"}]
},
"resource_graph": "[{'0x0': ['OS::Glance::Image', "
"'99777fdd-8a5b-45ab-ba2c-52420008103f', "
"'cirros-0.3.4-x86_64-uec']}, [[['0x0']]]]"
}
class TestCheckpoints(fakes.TestDataProtection):
def setUp(self):
super(TestCheckpoints, self).setUp()
cm = self.app.client_manager
self.checkpoints_mock = cm.data_protection.checkpoints
self.checkpoints_mock.reset_mock()
class TestListCheckpoints(TestCheckpoints):
def setUp(self):
super(TestListCheckpoints, self).setUp()
self.checkpoints_mock.list.return_value = [checkpoints.Checkpoint(
None, CHECKPOINT_INFO)]
# Command to test
self.cmd = osc_checkpoints.ListCheckpoints(self.app, None)
def test_checkpoints_list(self):
arglist = ['cf56bd3e-97a7-4078-b6d5-f36246333fd9']
verifylist = [('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Id', 'Project id', 'Status', 'Protection plan', 'Metadata',
'Created at'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [(
"dcb20606-ad71-40a3-80e4-ef0fafdad0c3",
"e486a2f49695423ca9c47e589b948108",
"available",
{
"id": "3523a271-68aa-42f5-b9ba-56e5200a2ebb",
"name": "My application",
"provider_id": "cf56bd3e-97a7-4078-b6d5-f36246333fd9",
"resources": [{
"id": "99777fdd-8a5b-45ab-ba2c-52420008103f",
"type": "OS::Glance::Image",
"name": "cirros-0.3.4-x86_64-uec"}]
},
'',
'')]
self.assertEqual(expected_data, list(data))
class TestCreateCheckpoint(TestCheckpoints):
def setUp(self):
super(TestCreateCheckpoint, self).setUp()
self.checkpoints_mock.create.return_value = checkpoints.Checkpoint(
None, CHECKPOINT_INFO)
# Command to test
self.cmd = osc_checkpoints.CreateCheckpoint(self.app, None)
def test_checkpoint_create(self):
arglist = ['cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'3523a271-68aa-42f5-b9ba-56e5200a2ebb']
verifylist = [('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9'),
('plan_id', '3523a271-68aa-42f5-b9ba-56e5200a2ebb')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.checkpoints_mock.create.assert_called_once_with(
'cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'3523a271-68aa-42f5-b9ba-56e5200a2ebb',
None)
class TestShowCheckpoint(TestCheckpoints):
def setUp(self):
super(TestShowCheckpoint, self).setUp()
self.checkpoints_mock.get.return_value = checkpoints.Checkpoint(
None, CHECKPOINT_INFO)
# Command to test
self.cmd = osc_checkpoints.ShowCheckpoint(self.app, None)
def test_checkpoint_show(self):
arglist = ['cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3']
verifylist = [('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9'),
('checkpoint_id',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.checkpoints_mock.get.assert_called_once_with(
'cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3')
class TestDeleteCheckpoint(TestCheckpoints):
def setUp(self):
super(TestDeleteCheckpoint, self).setUp()
self.checkpoints_mock.get.return_value = checkpoints.Checkpoint(
None, CHECKPOINT_INFO)
# Command to test
self.cmd = osc_checkpoints.DeleteCheckpoint(self.app, None)
def test_checkpoint_delete(self):
arglist = ['cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3']
verifylist = [('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9'),
('checkpoint',
['dcb20606-ad71-40a3-80e4-ef0fafdad0c3'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.checkpoints_mock.delete.assert_called_once_with(
'cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3')

View File

@@ -0,0 +1,180 @@
# 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.osc.v1 import plans as osc_plans
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import plans
PLAN_INFO = {
"status": "suspended",
"provider_id": "cf56bd3e-97a7-4078-b6d5-f36246333fd9",
"description": "",
"parameters": {},
"id": "204c825e-eb2f-4609-95ab-70b3caa43ac8",
"resources": [{
'type': 'OS::Cinder::Volume',
'id': '71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14',
'name': 'testsinglevolume'}],
"name": "OS Volume protection plan."
}
class TestPlans(fakes.TestDataProtection):
def setUp(self):
super(TestPlans, self).setUp()
self.plans_mock = self.app.client_manager.data_protection.plans
self.plans_mock.reset_mock()
class TestListPlans(TestPlans):
def setUp(self):
super(TestListPlans, self).setUp()
self.plans_mock.list.return_value = [plans.Plan(
None, PLAN_INFO)]
# Command to test
self.cmd = osc_plans.ListPlans(self.app, None)
def test_plans_list(self):
arglist = ['--status', 'suspended']
verifylist = [('status', 'suspended')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Id', 'Name', 'Description', 'Provider id', 'Status'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [("204c825e-eb2f-4609-95ab-70b3caa43ac8",
"OS Volume protection plan.",
"",
"cf56bd3e-97a7-4078-b6d5-f36246333fd9",
"suspended")]
self.assertEqual(expected_data, list(data))
class TestCreatePlan(TestPlans):
def setUp(self):
super(TestCreatePlan, self).setUp()
self.plans_mock.create.return_value = plans.Plan(
None, PLAN_INFO)
# Command to test
self.cmd = osc_plans.CreatePlan(self.app, None)
def test_plan_create(self):
arglist = ['OS Volume protection plan.',
'cf56bd3e-97a7-4078-b6d5-f36246333fd9',
"'71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14'="
"'OS::Cinder::Volume'='testsinglevolume'"]
verifylist = [('name', 'OS Volume protection plan.'),
('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9'),
('resources', "'71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14'="
"'OS::Cinder::Volume'='testsinglevolume'")]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.plans_mock.create.assert_called_once_with(
'OS Volume protection plan.',
'cf56bd3e-97a7-4078-b6d5-f36246333fd9',
[{'id': "'71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14'",
'type': "'OS::Cinder::Volume'",
'name': "'testsinglevolume'"}],
{}, description=None)
class TestUpdatePlan(TestPlans):
def setUp(self):
super(TestUpdatePlan, self).setUp()
self.plans_mock.get.return_value = plans.Plan(
None, PLAN_INFO)
self.plans_mock.update.return_value = plans.Plan(
None, PLAN_INFO)
# Command to test
self.cmd = osc_plans.UpdatePlan(self.app, None)
def test_plan_update(self):
arglist = ['204c825e-eb2f-4609-95ab-70b3caa43ac8',
'--status', 'started']
verifylist = [('plan_id', '204c825e-eb2f-4609-95ab-70b3caa43ac8'),
('status', 'started')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.plans_mock.update.assert_called_once_with(
'204c825e-eb2f-4609-95ab-70b3caa43ac8',
{'status': 'started'})
class TestDeletePlan(TestPlans):
def setUp(self):
super(TestDeletePlan, self).setUp()
self.plans_mock.get.return_value = plans.Plan(
None, PLAN_INFO)
# Command to test
self.cmd = osc_plans.DeletePlan(self.app, None)
def test_plan_delete(self):
arglist = ['204c825e-eb2f-4609-95ab-70b3caa43ac8']
verifylist = [('plan', ['204c825e-eb2f-4609-95ab-70b3caa43ac8'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.plans_mock.delete.assert_called_once_with(
'204c825e-eb2f-4609-95ab-70b3caa43ac8')
class TestShowPlan(TestPlans):
def setUp(self):
super(TestShowPlan, self).setUp()
self.plans_mock.get.return_value = plans.Plan(
None, PLAN_INFO)
# Command to test
self.cmd = osc_plans.ShowPlan(self.app, None)
def test_plan_show(self):
arglist = ['204c825e-eb2f-4609-95ab-70b3caa43ac8']
verifylist = [('plan', '204c825e-eb2f-4609-95ab-70b3caa43ac8')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
'description', 'id', 'name', 'parameters', 'provider_id',
'resources', 'status')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertEqual(PLAN_INFO['description'], data[0])
self.assertEqual(PLAN_INFO['id'], data[1])
self.assertEqual(PLAN_INFO['name'], data[2])
self.assertEqual(PLAN_INFO['parameters'], data[3])
self.assertEqual(PLAN_INFO['provider_id'], data[4])
self.assertEqual(PLAN_INFO['resources'], data[5])
self.assertEqual(PLAN_INFO['status'], data[6])

View File

@@ -0,0 +1,161 @@
# 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.osc.v1 import protectables as osc_protectables
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import protectables
PROTECTABLE_LIST_INFO = {
"protectable_type": [
"OS::Keystone::Project",
"OS::Cinder::Volume",
"OS::Glance::Image",
"OS::Nova::Server"
]
}
PROTECTABLE_SHOW_INFO = {
"name": "OS::Nova::Server",
"dependent_types": [
"OS::Cinder::Volume",
"OS::Glance::Image"
]
}
PROTECTABLE_INSTANCE_LIST_INFO = {
"id": "25336116-f38e-4c22-81ad-e9b7bd71ba51",
"type": "OS::Cinder::Volume",
"name": "System volume",
"extra_info": {
"availability_zone": "az1"
}
}
PROTECTABLE_INSTANCE_SHOW_INFO = {
"id": "cb4ef2ff-10f5-46c9-bce4-cf7a49c65a01",
"type": "OS::Nova::Server",
"name": "My VM",
"dependent_resources": [{
"id": "99777fdd-8a5b-45ab-ba2c-52420008103f",
"type": "OS::Glance::Image",
"name": "cirros-0.3.4-x86_64-uec"}]
}
class TestProtectables(fakes.TestDataProtection):
def setUp(self):
super(TestProtectables, self).setUp()
cm = self.app.client_manager
self.protectables_mock = cm.data_protection.protectables
self.protectables_mock.reset_mock()
class TestListProtectables(TestProtectables):
def setUp(self):
super(TestListProtectables, self).setUp()
self.protectables_mock.list.return_value = [protectables.Protectable(
None, PROTECTABLE_LIST_INFO)]
# Command to test
self.cmd = osc_protectables.ListProtectables(self.app, None)
def test_protectables_list(self):
arglist = []
verifylist = []
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Protectable type'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [(['OS::Keystone::Project',
'OS::Cinder::Volume',
'OS::Glance::Image',
'OS::Nova::Server'],)]
self.assertEqual(expected_data, list(data))
class TestShowProtectable(TestProtectables):
def setUp(self):
super(TestShowProtectable, self).setUp()
self.protectables_mock.get.return_value = protectables.Protectable(
None, PROTECTABLE_SHOW_INFO)
# Command to test
self.cmd = osc_protectables.ShowProtectable(self.app, None)
def test_protectable_show(self):
arglist = ['OS::Nova::Server']
verifylist = [('protectable_type', 'OS::Nova::Server')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.protectables_mock.get.assert_called_once_with(
'OS::Nova::Server')
class TestListProtectableInstances(TestProtectables):
def setUp(self):
super(TestListProtectableInstances, self).setUp()
pm = self.protectables_mock
pm.list_instances.return_value = protectables.Instances(
None, PROTECTABLE_INSTANCE_LIST_INFO)
# Command to test
self.cmd = osc_protectables.ListProtectableInstances(self.app, None)
def test_protectable_instances_list(self):
arglist = ['OS::Cinder::Volume']
verifylist = [('protectable_type', 'OS::Cinder::Volume')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.protectables_mock.list_instances.assert_called_once_with(
'OS::Cinder::Volume', limit=None, marker=None,
search_opts={'type': None, 'parameters': None},
sort=None)
class TestShowProtectableInstance(TestProtectables):
def setUp(self):
super(TestShowProtectableInstance, self).setUp()
pm = self.protectables_mock
pm.get_instance.return_value = protectables.Instances(
None, PROTECTABLE_INSTANCE_SHOW_INFO)
# Command to test
self.cmd = osc_protectables.ShowProtectableInstance(self.app, None)
def test_protectable_instance_show(self):
arglist = ['OS::Nova::Server', 'cb4ef2ff-10f5-46c9-bce4-cf7a49c65a01']
verifylist = [('protectable_type', 'OS::Nova::Server'),
('protectable_id',
'cb4ef2ff-10f5-46c9-bce4-cf7a49c65a01')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.protectables_mock.get_instance.assert_called_once_with(
'OS::Nova::Server', 'cb4ef2ff-10f5-46c9-bce4-cf7a49c65a01',
search_opts={'parameters': None})

View File

@@ -0,0 +1,141 @@
# 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.osc.v1 import providers as osc_providers
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import providers
PROVIDER_INFO = {
"id": "2220f8b1-975d-4621-a872-fa9afb43cb6c",
"name": "OS Infra Provider",
"description": "provider description",
"extended_info_schema": {
"options_schema": {
"OS::Cinder::Volume": {
"required": [
"backup_mode"
],
"type": "object",
"properties": {
"backup_mode": {
"default": "auto",
"enum": [
"full",
"incremental",
"auto"
],
"type": "string",
"description": "The backup mode.",
"title": "Backup Mode"
}
},
"title": "Cinder Protection Options"
}
},
"saved_info_schema": {
"OS::Cinder::Volume": {
"required": [
"name"
],
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name for this backup.",
"title": "Name"
}
},
"title": "Cinder Protection Saved Info"
}
},
"restore_schema": {
"OS::Cinder::Volume": {
"type": "object",
"properties": {
"restore_name": {
"type": "string",
"description": "The name of the restored volume.",
"title": "Restore Name"
}
},
"title": "Cinder Protection Restore"
}
}
}
}
class TestProviders(fakes.TestDataProtection):
def setUp(self):
super(TestProviders, self).setUp()
self.providers_mock = self.app.client_manager.data_protection.providers
self.providers_mock.reset_mock()
class TestListProviders(TestProviders):
def setUp(self):
super(TestListProviders, self).setUp()
self.providers_mock.list.return_value = [providers.Provider(
None, PROVIDER_INFO)]
# Command to test
self.cmd = osc_providers.ListProviders(self.app, None)
def test_providers_list(self):
arglist = ['--name', 'OS Infra Provider']
verifylist = [('name', 'OS Infra Provider')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Id', 'Name', 'Description'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [("2220f8b1-975d-4621-a872-fa9afb43cb6c",
"OS Infra Provider",
"provider description")]
self.assertEqual(expected_data, list(data))
class TestShowProvider(TestProviders):
def setUp(self):
super(TestShowProvider, self).setUp()
self.providers_mock.get.return_value = providers.Provider(
None, PROVIDER_INFO)
# Command to test
self.cmd = osc_providers.ShowProvider(self.app, None)
def test_provider_show(self):
arglist = ['2220f8b1-975d-4621-a872-fa9afb43cb6c']
verifylist = [('provider', '2220f8b1-975d-4621-a872-fa9afb43cb6c')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
'description', 'extended_info_schema', 'id', 'name')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertEqual(PROVIDER_INFO['description'], data[0])
self.assertEqual(PROVIDER_INFO['extended_info_schema'], data[1])
self.assertEqual(PROVIDER_INFO['id'], data[2])
self.assertEqual(PROVIDER_INFO['name'], data[3])

View File

@@ -0,0 +1,134 @@
# 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.osc.v1 import restores as osc_restores
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import restores
RESTORE_INFO = {
"id": "22b82aa7-9179-4c71-bba2-caf5c0e68db7",
"project_id": "e486a2f49695423ca9c47e589b948108",
"provider_id": "cf56bd3e-97a7-4078-b6d5-f36246333fd9",
"checkpoint_id": "dcb20606-ad71-40a3-80e4-ef0fafdad0c3",
"restore_target": "",
"parameters": {},
"restore_auth": {},
"resources_status": {},
"resources_reason": {},
"status": "success"
}
class TestRestores(fakes.TestDataProtection):
def setUp(self):
super(TestRestores, self).setUp()
self.restores_mock = self.app.client_manager.data_protection.restores
self.restores_mock.reset_mock()
class TestListRestores(TestRestores):
def setUp(self):
super(TestListRestores, self).setUp()
self.restores_mock.list.return_value = [restores.Restore(
None, RESTORE_INFO)]
# Command to test
self.cmd = osc_restores.ListRestores(self.app, None)
def test_restores_list(self):
arglist = ['--status', 'success']
verifylist = [('status', 'success')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Id', 'Project id', 'Provider id', 'Checkpoint id',
'Restore target', 'Parameters', 'Status'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [("22b82aa7-9179-4c71-bba2-caf5c0e68db7",
"e486a2f49695423ca9c47e589b948108",
"cf56bd3e-97a7-4078-b6d5-f36246333fd9",
"dcb20606-ad71-40a3-80e4-ef0fafdad0c3",
"",
{},
"success")]
self.assertEqual(expected_data, list(data))
class TestCreateRestore(TestRestores):
def setUp(self):
super(TestCreateRestore, self).setUp()
self.restores_mock.create.return_value = restores.Restore(
None, RESTORE_INFO)
# Command to test
self.cmd = osc_restores.CreateRestore(self.app, None)
def test_restore_create(self):
arglist = ['cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3']
verifylist = [('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9'),
('checkpoint_id',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.restores_mock.create.assert_called_once_with(
'cf56bd3e-97a7-4078-b6d5-f36246333fd9',
'dcb20606-ad71-40a3-80e4-ef0fafdad0c3',
None, {}, None)
class TestShowRestore(TestRestores):
def setUp(self):
super(TestShowRestore, self).setUp()
self.restores_mock.get.return_value = restores.Restore(
None, RESTORE_INFO)
# Command to test
self.cmd = osc_restores.ShowRestore(self.app, None)
def test_restore_show(self):
arglist = ['22b82aa7-9179-4c71-bba2-caf5c0e68db7']
verifylist = [('restore', '22b82aa7-9179-4c71-bba2-caf5c0e68db7')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
'checkpoint_id', 'id', 'parameters', 'project_id',
'provider_id', 'resources_reason', 'resources_status',
'restore_auth', 'restore_target', 'status')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertEqual(RESTORE_INFO['checkpoint_id'], data[0])
self.assertEqual(RESTORE_INFO['id'], data[1])
self.assertEqual(RESTORE_INFO['parameters'], data[2])
self.assertEqual(RESTORE_INFO['project_id'], data[3])
self.assertEqual(RESTORE_INFO['provider_id'], data[4])
self.assertEqual(RESTORE_INFO['resources_reason'], data[5])
self.assertEqual(RESTORE_INFO['resources_status'], data[6])
self.assertEqual(RESTORE_INFO['restore_auth'], data[7])
self.assertEqual(RESTORE_INFO['restore_target'], data[8])
self.assertEqual(RESTORE_INFO['status'], data[9])

View File

@@ -0,0 +1,167 @@
# 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.osc.v1 import scheduled_operations as osc_so
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import scheduled_operations
SCHEDULEDOPERATION_INFO = {
"id": "1a2c0c3d-f402-4cd8-b5db-82e85cb51fad",
"name": "My scheduled operation",
"description": "It will run everyday",
"operation_type": "protect",
"trigger_id": "23902b02-5666-4ee6-8dfe-962ac09c3995",
"operation_definition": {
"provider_id": "2a9ce1f3-cc1a-4516-9435-0ebb13caa399",
"plan_id": "2a9ce1f3-cc1a-4516-9435-0ebb13caa398"
},
"enabled": 1
}
class TestScheduledOperations(fakes.TestDataProtection):
def setUp(self):
super(TestScheduledOperations, self).setUp()
self.so_mock = self.app.client_manager.data_protection.\
scheduled_operations
self.so_mock.reset_mock()
class TestListScheduledOperations(TestScheduledOperations):
def setUp(self):
super(TestListScheduledOperations, self).setUp()
self.so_mock.list.return_value = \
[scheduled_operations.ScheduledOperation(None,
SCHEDULEDOPERATION_INFO)]
# Command to test
self.cmd = osc_so.ListScheduledOperations(self.app, None)
def test_scheduled_operations_list(self):
arglist = ['--name', 'My scheduled operation']
verifylist = [('name', 'My scheduled operation')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Id', 'Name', 'OperationType', 'TriggerId',
'OperationDefinition'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [("1a2c0c3d-f402-4cd8-b5db-82e85cb51fad",
"My scheduled operation",
"protect",
"23902b02-5666-4ee6-8dfe-962ac09c3995",
{
"provider_id":
"2a9ce1f3-cc1a-4516-9435-0ebb13caa399", # noqa
"plan_id":
"2a9ce1f3-cc1a-4516-9435-0ebb13caa398"
})]
self.assertEqual(expected_data, data)
class TestCreateScheduledOperation(TestScheduledOperations):
def setUp(self):
super(TestCreateScheduledOperation, self).setUp()
self.so_mock.create.return_value = scheduled_operations.\
ScheduledOperation(None, SCHEDULEDOPERATION_INFO)
# Command to test
self.cmd = osc_so.CreateScheduledOperation(self.app, None)
def test_scheduled_operation_create(self):
arglist = ['My scheduled operation',
'protect',
"23902b02-5666-4ee6-8dfe-962ac09c3995",
"'provider_id=2a9ce1f3-cc1a-4516-9435-0ebb13caa399,"
"plan_id=2a9ce1f3-cc1a-4516-9435-0ebb13caa398'"]
verifylist = [('name', 'My scheduled operation'),
('operation_type', 'protect'),
('trigger_id', "23902b02-5666-4ee6-8dfe-962ac09c3995"),
('operation_definition',
"'provider_id=2a9ce1f3-cc1a-4516-9435-0ebb13caa399,"
"plan_id=2a9ce1f3-cc1a-4516-9435-0ebb13caa398'")]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.so_mock.create.assert_called_once_with(
'My scheduled operation',
'protect',
'23902b02-5666-4ee6-8dfe-962ac09c3995',
"'provider_id=2a9ce1f3-cc1a-4516-9435-0ebb13caa399,"
"plan_id=2a9ce1f3-cc1a-4516-9435-0ebb13caa398'")
class TestDeleteScheduledOperation(TestScheduledOperations):
def setUp(self):
super(TestDeleteScheduledOperation, self).setUp()
self.so_mock.get.return_value = scheduled_operations.\
ScheduledOperation(None, SCHEDULEDOPERATION_INFO)
# Command to test
self.cmd = osc_so.DeleteScheduledOperation(self.app, None)
def test_scheduled_operation_delete(self):
arglist = ['1a2c0c3d-f402-4cd8-b5db-82e85cb51fad']
verifylist = [('scheduledoperation',
['1a2c0c3d-f402-4cd8-b5db-82e85cb51fad'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.so_mock.delete.assert_called_once_with(
'1a2c0c3d-f402-4cd8-b5db-82e85cb51fad')
class TestShowScheduledOperation(TestScheduledOperations):
def setUp(self):
super(TestShowScheduledOperation, self).setUp()
self.so_mock.get.return_value = scheduled_operations.\
ScheduledOperation(None, SCHEDULEDOPERATION_INFO)
# Command to test
self.cmd = osc_so.ShowScheduledOperation(self.app, None)
def test_scheduled_operation_show(self):
arglist = ['1a2c0c3d-f402-4cd8-b5db-82e85cb51fad']
verifylist = [('scheduledoperation',
'1a2c0c3d-f402-4cd8-b5db-82e85cb51fad')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
'description', 'enabled', 'id', 'name', 'operation_definition',
'operation_type', 'trigger_id')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertEqual(SCHEDULEDOPERATION_INFO['description'], data[0])
self.assertEqual(SCHEDULEDOPERATION_INFO['enabled'], data[1])
self.assertEqual(SCHEDULEDOPERATION_INFO['id'], data[2])
self.assertEqual(SCHEDULEDOPERATION_INFO['name'], data[3])
self.assertEqual(SCHEDULEDOPERATION_INFO['operation_definition'],
data[4])
self.assertEqual(SCHEDULEDOPERATION_INFO['operation_type'], data[5])
self.assertEqual(SCHEDULEDOPERATION_INFO['trigger_id'], data[6])

View File

@@ -0,0 +1,186 @@
# 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.osc.v1 import triggers as osc_triggers
from karborclient.tests.unit.osc.v1 import fakes
from karborclient.v1 import triggers
TRIGGER_INFO = {
"id": "2a9ce1f3-cc1a-4516-9435-0ebb13caa398",
"name": "My backup trigger",
"type": "time",
"properties": {
"format": "calendar",
"pattern": "BEGIN:VEVENT\\nRRULE:FREQ=HOURLY;INTERVAL=1;\\nEND:VEVENT",
"start_time": "2015-12-17T08:30:00",
"end_time": "2016-03-17T08:30:00",
"window": "3600"
}
}
class TestTriggers(fakes.TestDataProtection):
def setUp(self):
super(TestTriggers, self).setUp()
self.triggers_mock = self.app.client_manager.data_protection.triggers
self.triggers_mock.reset_mock()
class TestListTriggers(TestTriggers):
def setUp(self):
super(TestListTriggers, self).setUp()
self.triggers_mock.list.return_value = [triggers.Trigger(
None, TRIGGER_INFO)]
# Command to test
self.cmd = osc_triggers.ListTriggers(self.app, None)
def test_triggers_list(self):
arglist = ['--name', 'My backup trigger']
verifylist = [('name', 'My backup trigger')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
['Id', 'Name', 'Type', 'Properties'])
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [("2a9ce1f3-cc1a-4516-9435-0ebb13caa398",
"My backup trigger",
"time",
{"format": "calendar",
"pattern": "BEGIN:VEVENT\\nRRULE:FREQ=HOURLY;INTERVAL=1;\\nEND:VEVENT", # noqa
"start_time": "2015-12-17T08:30:00",
"end_time": "2016-03-17T08:30:00",
"window": "3600"})]
self.assertEqual(expected_data, list(data))
class TestCreateTrigger(TestTriggers):
def setUp(self):
super(TestCreateTrigger, self).setUp()
self.triggers_mock.create.return_value = triggers.Trigger(
None, TRIGGER_INFO)
# Command to test
self.cmd = osc_triggers.CreateTrigger(self.app, None)
def test_trigger_create(self):
arglist = ['My backup trigger',
'time',
"'format'='calendar',"
"'pattern'='BEGIN:VEVENT\\nRRULE:FREQ=HOURLY;INTERVAL=1;\\nEND:VEVENT'," # noqa
"'start_time'='2015-12-17T08:30:00',"
"'end_time'='2016-03-17T08:30:00',"
"'window'='3600'"]
verifylist = [('name', 'My backup trigger'),
('type', 'time'),
('properties', "'format'='calendar',"
"'pattern'='BEGIN:VEVENT\\nRRULE:FREQ=HOURLY;INTERVAL=1;\\nEND:VEVENT'," # noqa
"'start_time'='2015-12-17T08:30:00',"
"'end_time'='2016-03-17T08:30:00',"
"'window'='3600'")]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.triggers_mock.create.assert_called_once_with(
'My backup trigger',
'time',
"'format'='calendar',"
"'pattern'='BEGIN:VEVENT\\nRRULE:FREQ=HOURLY;INTERVAL=1;\\nEND:VEVENT'," # noqa
"'start_time'='2015-12-17T08:30:00',"
"'end_time'='2016-03-17T08:30:00',"
"'window'='3600'")
class TestUpdateTrigger(TestTriggers):
def setUp(self):
super(TestUpdateTrigger, self).setUp()
self.triggers_mock.get.return_value = triggers.Trigger(
None, TRIGGER_INFO)
self.triggers_mock.update.return_value = triggers.Trigger(
None, TRIGGER_INFO)
# Command to test
self.cmd = osc_triggers.UpdateTrigger(self.app, None)
def test_trigger_update(self):
arglist = ['2a9ce1f3-cc1a-4516-9435-0ebb13caa398',
'--name', 'My backup trigger']
verifylist = [('trigger_id', '2a9ce1f3-cc1a-4516-9435-0ebb13caa398'),
('name', 'My backup trigger')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.triggers_mock.update.assert_called_once_with(
'2a9ce1f3-cc1a-4516-9435-0ebb13caa398',
{'name': 'My backup trigger'})
class TestDeleteTrigger(TestTriggers):
def setUp(self):
super(TestDeleteTrigger, self).setUp()
self.triggers_mock.get.return_value = triggers.Trigger(
None, TRIGGER_INFO)
# Command to test
self.cmd = osc_triggers.DeleteTrigger(self.app, None)
def test_trigger_delete(self):
arglist = ['2a9ce1f3-cc1a-4516-9435-0ebb13caa398']
verifylist = [('trigger', ['2a9ce1f3-cc1a-4516-9435-0ebb13caa398'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Check that correct arguments were passed
self.triggers_mock.delete.assert_called_once_with(
'2a9ce1f3-cc1a-4516-9435-0ebb13caa398')
class TestShowTrigger(TestTriggers):
def setUp(self):
super(TestShowTrigger, self).setUp()
self.triggers_mock.get.return_value = triggers.Trigger(
None, TRIGGER_INFO)
# Command to test
self.cmd = osc_triggers.ShowTrigger(self.app, None)
def test_trigger_show(self):
arglist = ['2a9ce1f3-cc1a-4516-9435-0ebb13caa398']
verifylist = [('trigger', '2a9ce1f3-cc1a-4516-9435-0ebb13caa398')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = (
'id', 'name', 'properties', 'type')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertEqual(TRIGGER_INFO['id'], data[0])
self.assertEqual(TRIGGER_INFO['name'], data[1])
self.assertEqual(TRIGGER_INFO['properties'], data[2])
self.assertEqual(TRIGGER_INFO['type'], data[3])

View File

@@ -16,12 +16,12 @@ import socket
import mock
import testtools
from smaugclient.common import http
from smaugclient.openstack.common.apiclient import exceptions as exc
from smaugclient.tests.unit import fakes
from karborclient.common.apiclient import exceptions as exc
from karborclient.common import http
from karborclient.tests.unit import fakes
@mock.patch('smaugclient.common.http.requests.request')
@mock.patch('karborclient.common.http.requests.request')
class HttpClientTest(testtools.TestCase):
# Patch os.environ to avoid required auth info.
@@ -29,7 +29,7 @@ class HttpClientTest(testtools.TestCase):
super(HttpClientTest, self).setUp()
def test_http_raw_request(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
headers = {'User-Agent': 'python-karborclient'}
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
@@ -71,15 +71,15 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-smaugclient'}),
headers={'User-Agent': 'python-karborclient'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-smaugclient',
headers={'User-Agent': 'python-karborclient',
'X-Auth-Key': 'pass',
'X-Auth-User': 'user'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-smaugclient',
headers={'User-Agent': 'python-karborclient',
'X-Auth-Token': 'abcd1234'})
])
@@ -101,7 +101,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'X-Region-Name': 'RegionOne',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_json_request(self, mock_request):
# Record a 200
@@ -119,7 +119,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_json_request_argument_passed_to_requests(self, mock_request):
"""Check that we have sent the proper arguments to requests."""
@@ -147,7 +147,7 @@ class HttpClientTest(testtools.TestCase):
data='"text"',
headers={'Content-Type': 'application/json',
'X-Auth-Url': 'http://AUTH_URL',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_json_request_w_req_body(self, mock_request):
# Record a 200
@@ -166,7 +166,7 @@ class HttpClientTest(testtools.TestCase):
data='"test-body"',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
# Record a 200
@@ -184,7 +184,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082', data='"test-data"',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_json_request_invalid_json(self, mock_request):
# Record a 200
@@ -202,7 +202,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_manual_redirect_delete(self, mock_request):
mock_request.side_effect = [
@@ -223,11 +223,11 @@ class HttpClientTest(testtools.TestCase):
mock.call('DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
'User-Agent': 'python-karborclient'}),
mock.call('DELETE', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
])
def test_http_manual_redirect_post(self, mock_request):
@@ -249,11 +249,11 @@ class HttpClientTest(testtools.TestCase):
mock.call('POST', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
'User-Agent': 'python-karborclient'}),
mock.call('POST', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
])
def test_http_manual_redirect_put(self, mock_request):
@@ -275,11 +275,11 @@ class HttpClientTest(testtools.TestCase):
mock.call('PUT', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
'User-Agent': 'python-karborclient'}),
mock.call('PUT', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
])
def test_http_manual_redirect_prohibited(self, mock_request):
@@ -295,7 +295,7 @@ class HttpClientTest(testtools.TestCase):
'DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_manual_redirect_error_without_location(self, mock_request):
mock_request.return_value = \
@@ -310,7 +310,7 @@ class HttpClientTest(testtools.TestCase):
'DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_json_request_redirect(self, mock_request):
# Record the 302
@@ -333,11 +333,11 @@ class HttpClientTest(testtools.TestCase):
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
'User-Agent': 'python-karborclient'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
])
def test_http_404_json_request(self, mock_request):
@@ -356,7 +356,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_http_300_json_request(self, mock_request):
mock_request.return_value = \
@@ -374,10 +374,10 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
'User-Agent': 'python-karborclient'})
def test_fake_json_request(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
headers = {'User-Agent': 'python-karborclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('fake://example.com:8082')
@@ -388,7 +388,7 @@ class HttpClientTest(testtools.TestCase):
headers=headers)
def test_http_request_socket_error(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
headers = {'User-Agent': 'python-karborclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('http://example.com:8082')
@@ -399,7 +399,7 @@ class HttpClientTest(testtools.TestCase):
headers=headers)
def test_http_request_socket_timeout(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
headers = {'User-Agent': 'python-karborclient'}
mock_request.side_effect = [socket.timeout]
client = http.HTTPClient('http://example.com:8082')
@@ -424,7 +424,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'},
'User-Agent': 'python-karborclient'},
timeout=float(123))
def test_get_system_ca_file(self, mock_request):
@@ -445,7 +445,7 @@ class HttpClientTest(testtools.TestCase):
client = http.HTTPClient('https://foo', cacert="NOWHERE")
self.assertEqual("NOWHERE", client.verify_cert)
with mock.patch('smaugclient.common.http.get_system_ca_file') as gsf:
with mock.patch('karborclient.common.http.get_system_ca_file') as gsf:
gsf.return_value = "SOMEWHERE"
client = http.HTTPClient('https://foo')
self.assertEqual("SOMEWHERE", client.verify_cert)

View File

@@ -11,13 +11,13 @@
# under the License.
"""
Tests for `smaugclient` module.
Tests for `karborclient` module.
"""
from smaugclient.tests.unit import base
from karborclient.tests.unit import base
class TestSmaugclient(base.TestCaseShell):
class TestKarborclient(base.TestCaseShell):
def test_something(self):
pass

View File

@@ -15,17 +15,17 @@ import re
import sys
import fixtures
from keystoneclient import fixture
from keystoneclient.fixture import v2 as ks_v2_fixture
from keystoneauth1 import fixture
from keystoneauth1.fixture import v2 as ks_v2_fixture
import mock
from oslo_log import handlers
from oslo_log import log
import six
from testtools import matchers
from smaugclient.openstack.common.apiclient import exceptions
import smaugclient.shell
from smaugclient.tests.unit import base
from karborclient.common.apiclient import exceptions
import karborclient.shell
from karborclient.tests.unit import base
FAKE_ENV = {'OS_USERNAME': 'username',
@@ -51,7 +51,7 @@ def _create_ver_list(versions):
class TestArgs(object):
package_version = ''
smaug_repo_url = 'http://127.0.0.1'
karbor_repo_url = 'http://127.0.0.1'
exists_action = ''
is_public = False
categories = []
@@ -78,7 +78,7 @@ class ShellCommandTest(ShellTest):
def get_auth_endpoint(bound_self, args):
return ('test', {})
self.useFixture(fixtures.MonkeyPatch(
'smaugclient.shell.SmaugShell._get_endpoint_and_kwargs',
'karborclient.shell.KarborShell._get_endpoint_and_kwargs',
get_auth_endpoint))
self.client = mock.MagicMock()
@@ -99,7 +99,7 @@ class ShellCommandTest(ShellTest):
try:
sys.stdout = six.StringIO()
sys.stderr = six.StringIO()
_shell = smaugclient.shell.SmaugShell()
_shell = karborclient.shell.KarborShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
@@ -133,9 +133,9 @@ class ShellCommandTest(ShellTest):
def test_help(self):
required = [
'.*?^usage: smaug',
'.*?^\s+plan-create\s+Create a plan.',
'.*?^See "smaug help COMMAND" for help on a specific command',
'.*?^usage: karbor',
'.*?^\s+plan-create\s+Creates a plan.',
'.*?^See "karbor help COMMAND" for help on a specific command',
]
stdout, stderr = self.shell('help')
for r in required:
@@ -144,8 +144,8 @@ class ShellCommandTest(ShellTest):
def test_help_on_subcommand(self):
required = [
'.*?^usage: smaug plan-create',
'.*?^Create a plan.',
'.*?^usage: karbor plan-create',
'.*?^Creates a plan.',
]
stdout, stderr = self.shell('help plan-create')
for r in required:
@@ -154,9 +154,9 @@ class ShellCommandTest(ShellTest):
def test_help_no_options(self):
required = [
'.*?^usage: smaug',
'.*?^\s+plan-create\s+Create a plan',
'.*?^See "smaug help COMMAND" for help on a specific command',
'.*?^usage: karbor',
'.*?^\s+plan-create\s+Creates a plan',
'.*?^See "karbor help COMMAND" for help on a specific command',
]
stdout, stderr = self.shell('')
for r in required:

View File

View File

@@ -15,10 +15,10 @@ try:
except ImportError:
import urllib.parse as urlparse
from smaugclient.common import http as base_client
from smaugclient.tests.unit import base
from smaugclient.tests.unit import fakes
from smaugclient.v1 import client
from karborclient.common import http as base_client
from karborclient.tests.unit import base
from karborclient.tests.unit import fakes
from karborclient.v1 import client
REQUEST_ID = 'req-test-request-id'
@@ -82,7 +82,7 @@ class FakeHTTPClient(base_client.HTTPClient):
(method, url, callback))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
self.callstack.append((method, url, kwargs.get('body')))
status, headers, body = getattr(self, callback)(**kwargs)
# add fake request-id header
headers['x-openstack-request-id'] = REQUEST_ID

View File

@@ -12,8 +12,8 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'checkpoint': {}})
@@ -24,75 +24,69 @@ FAKE_PLAN_ID = "3330f8b1-975d-4621-a872-fa9afb43cb6c"
class CheckpointsTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_checkpoints(self, mock_request):
mock_request.return_value = mock_request_return
cs.checkpoints.list(provider_id=FAKE_PROVIDER_ID)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers/{provider_id}/checkpoints'.format(
project_id=fakes.PROJECT_ID,
'/providers/{provider_id}/checkpoints'.format(
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_get_checkpoint(self, mock_request):
mock_request.return_value = mock_request_return
cs.checkpoints.get(FAKE_PROVIDER_ID, '1')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers/{provider_id}/checkpoints/1'.format(
project_id=fakes.PROJECT_ID,
'/providers/{provider_id}/checkpoints/1'.format(
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.common.http.HTTPClient.raw_request')
@mock.patch('karborclient.common.http.HTTPClient.raw_request')
def test_delete_checkpoint(self, mock_request):
mock_request.return_value = mock_request_return
cs.checkpoints.delete(FAKE_PROVIDER_ID, '1')
mock_request.assert_called_with(
'DELETE',
'/v1/{project_id}/providers/{provider_id}/checkpoints/1'.format(
project_id=fakes.PROJECT_ID,
'/providers/{provider_id}/checkpoints/1'.format(
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_checkpoints_with_marker_limit(self, mock_request):
mock_request.return_value = mock_request_return
cs.checkpoints.list(provider_id=FAKE_PROVIDER_ID,
marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers/{provider_id}/'
'/providers/{provider_id}/'
'checkpoints?limit=2&marker=1234'.format(
provider_id=FAKE_PROVIDER_ID,
project_id=fakes.PROJECT_ID), headers={})
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_checkpoints_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_request_return
cs.checkpoints.list(provider_id=FAKE_PROVIDER_ID,
sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers/{provider_id}/'
'/providers/{provider_id}/'
'checkpoints?sort_dir=asc&sort_key=id'.format(
provider_id=FAKE_PROVIDER_ID,
project_id=fakes.PROJECT_ID), headers={})
provider_id=FAKE_PROVIDER_ID), headers={})
def test_list_checkpoints_with_invalid_sort_key(self):
self.assertRaises(ValueError,
cs.checkpoints.list, FAKE_PROVIDER_ID,
sort_key='invalid', sort_dir='asc')
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_checkpoint(self, mock_request):
mock_request.return_value = mock_request_return
cs.checkpoints.create(FAKE_PROVIDER_ID, FAKE_PLAN_ID)
mock_request.assert_called_with(
'POST',
'/v1/{project_id}/providers/{provider_id}/'
'/providers/{provider_id}/'
'checkpoints'.format(
provider_id=FAKE_PROVIDER_ID,
project_id=fakes.PROJECT_ID),
provider_id=FAKE_PROVIDER_ID),
data={
'checkpoint': {'plan_id': FAKE_PLAN_ID}},
'checkpoint': {'plan_id': FAKE_PLAN_ID, 'extra-info': None}},
headers={})

View File

@@ -12,8 +12,8 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'plan': {'name': 'fake_name'}})
@@ -21,75 +21,77 @@ mock_request_return = ({}, {'plan': {'name': 'fake_name'}})
class PlansTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_plans_with_marker_limit(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.list(marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/plans?limit=2&marker=1234'.format(
'/plans?limit=2&marker=1234'.format(
project_id=fakes.PROJECT_ID), headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_plans_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.list(sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/plans?'
'/plans?'
'sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_plans_with_invalid_sort_key(self, mock_request):
self.assertRaises(ValueError,
cs.plans.list, sort_key='invalid', sort_dir='asc')
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@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',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/plans',
'/plans',
data={
'plan': {'provider_id': 'provider_id',
'name': 'Plan name',
'resources': ''}},
'resources': '',
'parameters': '',
'description': ''}},
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.raw_request')
@mock.patch('karborclient.common.http.HTTPClient.raw_request')
def test_delete_plan(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.delete('1')
mock_request.assert_called_with(
'DELETE',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/plans/1',
'/plans/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_update(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.update('1', {'name': 'Test name.'})
mock_request.assert_called_with(
'PUT',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/plans/1',
'/plans/1',
data={'plan': {'name': 'Test name.'}}, headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_plan(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.get('1')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/plans/1',
'/plans/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_plan_with_headers(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.get('1', session_id='fake_session_id')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/plans/1',
'/plans/1',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -12,64 +12,70 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'protectable_type': {}})
mock_instances_request_return = ({}, {'instances': {}})
mock_instance_request_return = ({}, {'instance': {}})
class ProtectablesTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_protectables(self, mock_request):
mock_request.return_value = mock_request_return
cs.protectables.list()
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/protectables'.format(
project_id=fakes.PROJECT_ID), headers={})
'/protectables', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_get_protectables(self, mock_request):
mock_request.return_value = mock_request_return
cs.protectables.get('OS::Cinder::Volume')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/protectables/OS::Cinder::Volume'.format(
project_id=fakes.PROJECT_ID), headers={})
'/protectables/OS::Cinder::Volume', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_get_protectables_instance(self, mock_request):
mock_request.return_value = mock_instance_request_return
cs.protectables.get_instance('OS::Cinder::Volume', '1')
mock_request.assert_called_with(
'GET',
'/protectables/OS::Cinder::Volume/'
'instances/1', headers={})
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_protectables_instances(self, mock_request):
mock_request.return_value = mock_instances_request_return
cs.protectables.list_instances('OS::Cinder::Volume')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/protectables/OS::Cinder::Volume/'
'instances'.format(project_id=fakes.PROJECT_ID), headers={})
'/protectables/OS::Cinder::Volume/'
'instances', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_protectables_instances_with_marker_limit(self, mock_request):
mock_request.return_value = mock_instances_request_return
cs.protectables.list_instances('OS::Cinder::Volume',
marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/protectables/OS::Cinder::Volume/'
'instances?limit=2&marker=1234'.format(
project_id=fakes.PROJECT_ID), headers={})
'/protectables/OS::Cinder::Volume/'
'instances?limit=2&marker=1234', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_protectables_instances_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_instances_request_return
cs.protectables.list_instances('OS::Cinder::Volume',
sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/protectables/OS::Cinder::Volume/'
'instances?sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
'/protectables/OS::Cinder::Volume/'
'instances?sort_dir=asc&sort_key=id', headers={})
def test_list_protectables_instances_with_invalid_sort_key(self):
self.assertRaises(ValueError,

View File

@@ -12,8 +12,8 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'provider': {}})
@@ -21,43 +21,39 @@ mock_request_return = ({}, {'provider': {}})
class ProvidersTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_get_providers(self, mock_request):
mock_request.return_value = mock_request_return
cs.providers.get('2220f8b1-975d-4621-a872-fa9afb43cb6c')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers/'
'2220f8b1-975d-4621-a872-fa9afb43cb6c'.format(
project_id=fakes.PROJECT_ID), headers={})
'/providers/'
'2220f8b1-975d-4621-a872-fa9afb43cb6c', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_providers(self, mock_request):
mock_request.return_value = mock_request_return
cs.providers.list()
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers'.format(
project_id=fakes.PROJECT_ID), headers={})
'/providers', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_providers_with_marker_limit(self, mock_request):
mock_request.return_value = mock_request_return
cs.providers.list(marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers?limit=2&marker=1234'.format(
project_id=fakes.PROJECT_ID), headers={})
'/providers?limit=2&marker=1234', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_providers_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_request_return
cs.providers.list(sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/providers?'
'sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
'/providers?'
'sort_dir=asc&sort_key=id', headers={})
def test_list_providers_with_invalid_sort_key(self):
self.assertRaises(ValueError,

View File

@@ -12,8 +12,8 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'restore': {}})
@@ -21,63 +21,65 @@ mock_request_return = ({}, {'restore': {}})
class RestoresTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_restores_with_marker_limit(self, mock_request):
mock_request.return_value = mock_request_return
cs.restores.list(marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/restores?limit=2&marker=1234'.format(
project_id=fakes.PROJECT_ID), headers={})
'/restores?limit=2&marker=1234', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_restores_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_request_return
cs.restores.list(sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/restores?'
'sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
'/restores?'
'sort_dir=asc&sort_key=id', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_plans_with_invalid_sort_key(self, mock_request):
self.assertRaises(ValueError,
cs.restores.list, sort_key='invalid', sort_dir='asc')
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_restore(self, mock_request):
mock_request.return_value = mock_request_return
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',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/restores',
'/restores',
data={
'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('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_restore(self, mock_request):
mock_request.return_value = mock_request_return
cs.restores.get('1')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/restores/1',
'/restores/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_restore_with_headers(self, mock_request):
mock_request.return_value = mock_request_return
cs.restores.get('1', session_id='fake_session_id')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/restores/1',
'/restores/1',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -12,8 +12,8 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'scheduled_operation': {'name': 'fake_name'}})
@@ -21,33 +21,31 @@ mock_request_return = ({}, {'scheduled_operation': {'name': 'fake_name'}})
class ScheduledOperationsTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_scheduled_operations_with_marker_limit(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.list(marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/scheduled_operations?limit=2&marker=1234'.format(
project_id=fakes.PROJECT_ID), headers={})
'/scheduled_operations?limit=2&marker=1234', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list__scheduled_operations_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.list(sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/scheduled_operations?'
'sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
'/scheduled_operations?'
'sort_dir=asc&sort_key=id', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_scheduled_operations_with_invalid_sort_key(self,
mock_request):
self.assertRaises(ValueError,
cs.scheduled_operations.list, sort_key='invalid',
sort_dir='asc')
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_scheduled_operation(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.create(
@@ -56,7 +54,7 @@ class ScheduledOperationsTest(base.TestCaseShell):
'operation_definition')
mock_request.assert_called_with(
'POST',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/scheduled_operations',
'/scheduled_operations',
data={
'scheduled_operation': {
'name': 'name',
@@ -65,29 +63,29 @@ class ScheduledOperationsTest(base.TestCaseShell):
'operation_definition': 'operation_definition'}},
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.raw_request')
@mock.patch('karborclient.common.http.HTTPClient.raw_request')
def test_delete_scheduled_operation(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.delete('1')
mock_request.assert_called_with(
'DELETE',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/scheduled_operations/1',
'/scheduled_operations/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_scheduled_operation(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.get('1')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/scheduled_operations/1',
'/scheduled_operations/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_scheduled_operation_with_headers(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.get('1', session_id='fake_session_id')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/scheduled_operations/1',
'/scheduled_operations/1',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -12,8 +12,8 @@
import mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
from karborclient.tests.unit import base
from karborclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'trigger_info': {'name': 'fake_name'}})
@@ -21,66 +21,79 @@ mock_request_return = ({}, {'trigger_info': {'name': 'fake_name'}})
class TriggersTest(base.TestCaseShell):
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_triggers_with_marker_limit(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.list(marker=1234, limit=2)
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/triggers?limit=2&marker=1234'.format(
project_id=fakes.PROJECT_ID), headers={})
'/triggers?limit=2&marker=1234', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_triggers_with_sort_key_dir(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.list(sort_key='id', sort_dir='asc')
mock_request.assert_called_with(
'GET',
'/v1/{project_id}/triggers?'
'sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
'/triggers?'
'sort_dir=asc&sort_key=id', headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_list_triggers_with_invalid_sort_key(self, mock_request):
self.assertRaises(ValueError,
cs.triggers.list, sort_key='invalid', sort_dir='asc')
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_trigger(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.create('name', 'time', 'properties')
mock_request.assert_called_with(
'POST',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/triggers',
'/triggers',
data={
'trigger_info': {'name': 'name',
'type': 'time',
'properties': 'properties'}},
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.raw_request')
@mock.patch('karborclient.common.http.HTTPClient.raw_request')
def test_delete_trigger(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.delete('1')
mock_request.assert_called_with(
'DELETE',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/triggers/1',
'/triggers/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
def test_show_plan(self, mock_request):
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_trigger(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.get('1')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/triggers/1',
'/triggers/1',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_trigger_with_headers(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.get('1', session_id='fake_session_id')
mock_request.assert_called_with(
'GET',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/triggers/1',
'/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={}
)

146
karborclient/utils.py Normal file
View File

@@ -0,0 +1,146 @@
# 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_serialization import jsonutils
from oslo_utils import uuidutils
from karborclient.common.apiclient import exceptions
def extract_resources(args):
resources = []
for data in args.resources.split(','):
if '=' in data and len(data.split('=')) in [3, 4]:
resource = dict(zip(['id', 'type', 'name', 'extra_info'],
data.split('=')))
if resource.get('extra_info'):
resource['extra_info'] = jsonutils.loads(
resource.get('extra_info'))
else:
raise exceptions.CommandError(
"Unable to parse parameter resources. "
"The keys of resource are id , type, name and "
"extra_info. The extra_info field is optional.")
resources.append(resource)
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"])
def extract_parameters(args):
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
return parameters
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
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
def extract_properties(args):
properties = {}
if args.properties is None:
return properties
for data in args.properties.split(','):
if '=' in data:
(resource_key, resource_value) = data.split('=', 1)
else:
raise exceptions.CommandError(
"Unable to parse parameter properties.")
properties[resource_key] = resource_value
return properties
def extract_operation_definition(args):
operation_definition = {}
for data in args.operation_definition.split(','):
if '=' in data:
(resource_key, resource_value) = data.split('=', 1)
else:
raise exceptions.CommandError(
"Unable to parse parameter operation_definition.")
operation_definition[resource_key] = resource_value
return operation_definition

View File

View File

@@ -12,31 +12,40 @@
from six.moves.urllib import parse
from smaugclient.common import base
from karborclient.common import base
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")
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}}
url = "/v1/{project_id}/providers/{provider_id}/" \
"checkpoints" .format(project_id=self.project_id,
provider_id=provider_id)
return self._create(url, body, 'checkpoint', return_raw=True)
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')
def delete(self, provider_id, checkpoint_id):
path = '/v1/{project_id}/providers/{provider_id}/checkpoints/' \
'{checkpoint_id}'.format(project_id=self.project_id,
provider_id=provider_id,
path = '/providers/{provider_id}/checkpoints/' \
'{checkpoint_id}'.format(provider_id=provider_id,
checkpoint_id=checkpoint_id)
return self._delete(path)
@@ -45,9 +54,8 @@ class CheckpointManager(base.ManagerWithFind):
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = '/v1/{project_id}/providers/{provider_id}/checkpoints/' \
'{checkpoint_id}'.format(project_id=self.project_id,
provider_id=provider_id,
url = '/providers/{provider_id}/checkpoints/' \
'{checkpoint_id}'.format(provider_id=provider_id,
checkpoint_id=checkpoint_id)
return self._get(url, response_key="checkpoint", headers=headers)
@@ -111,8 +119,7 @@ class CheckpointManager(base.ManagerWithFind):
params = sorted(query_params.items(), key=lambda x: x[0])
query_string = "?%s" % parse.urlencode(params)
return ("/v1/%(project_id)s/providers/%(provider_id)s"
return ("/providers/%(provider_id)s"
"/checkpoints%(query_string)s" %
{"project_id": self.project_id,
"provider_id": provider_id,
{"provider_id": provider_id,
"query_string": query_string})

View File

@@ -12,18 +12,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from smaugclient.common import http
from smaugclient.v1 import checkpoints
from smaugclient.v1 import plans
from smaugclient.v1 import protectables
from smaugclient.v1 import providers
from smaugclient.v1 import restores
from smaugclient.v1 import scheduled_operations
from smaugclient.v1 import triggers
from karborclient.common import http
from karborclient.v1 import checkpoints
from karborclient.v1 import plans
from karborclient.v1 import protectables
from karborclient.v1 import providers
from karborclient.v1 import restores
from karborclient.v1 import scheduled_operations
from karborclient.v1 import triggers
class Client(object):
"""Client for the Smaug v1 API.
"""Client for the karbor v1 API.
:param string endpoint: A user-supplied endpoint URL for the service.
:param string token: Token for authentication.
@@ -32,7 +32,7 @@ class Client(object):
"""
def __init__(self, *args, **kwargs):
"""Initialize a new client for the Smaug v1 API."""
"""Initialize a new client for the karbor v1 API."""
self.http_client = http._construct_http_client(*args, **kwargs)
self.plans = plans.PlanManager(self.http_client)
self.restores = restores.RestoreManager(self.http_client)

View File

@@ -10,41 +10,38 @@
# License for the specific language governing permissions and limitations
# under the License.
from smaugclient.common import base
from karborclient.common import base
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):
def create(self, name, provider_id, resources, parameters,
description=None):
body = {'plan': {'name': name,
'description': description,
'provider_id': provider_id,
'resources': resources,
'parameters': parameters
}}
url = "/v1/{project_id}/plans" .format(
project_id=self.project_id)
return self._create(url, body, 'plan', return_raw=True)
url = "/plans"
return self._create(url, body, 'plan')
def update(self, plan_id, data):
body = {"plan": data}
return self._update('/v1/{project_id}/plans/{plan_id}'
.format(project_id=self.project_id,
plan_id=plan_id),
return self._update('/plans/{plan_id}'
.format(plan_id=plan_id),
body, "plan")
def delete(self, plan_id):
path = '/v1/{project_id}/plans/{plan_id}'.format(
project_id=self.project_id,
path = '/plans/{plan_id}'.format(
plan_id=plan_id)
return self._delete(path)
@@ -53,8 +50,7 @@ class PlanManager(base.ManagerWithFind):
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/plans/{plan_id}".format(
project_id=self.project_id,
url = "/plans/{plan_id}".format(
plan_id=plan_id)
return self._get(url, response_key="plan", headers=headers)

View File

@@ -12,15 +12,17 @@
from six.moves.urllib import parse
from smaugclient.common import base
from karborclient.common import base
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
class ProtectableManager(base.ManagerWithFind):
@@ -31,14 +33,12 @@ class ProtectableManager(base.ManagerWithFind):
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/protectables/{protectable_type}".format(
project_id=self.project_id,
url = "/protectables/{protectable_type}".format(
protectable_type=protectable_type)
return self._get(url, response_key="protectable_type", headers=headers)
def list(self):
url = "/v1/{project_id}/protectables".format(
project_id=self.project_id)
url = "/protectables"
protectables = self._list(url, 'protectable_type', return_raw=True)
protectables_list = []
@@ -61,7 +61,7 @@ class ProtectableManager(base.ManagerWithFind):
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`Protectable`
:rtype: list of :class:`Instances`
"""
url = self._build_instances_list_url(
@@ -69,7 +69,30 @@ class ProtectableManager(base.ManagerWithFind):
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'instances')
return self._list(url, response_key='instances', obj_class=Instances)
def get_instance(self, type, id, search_opts=None, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
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,
search_opts=None, marker=None, limit=None,
@@ -108,8 +131,7 @@ class ProtectableManager(base.ManagerWithFind):
params = sorted(query_params.items(), key=lambda x: x[0])
query_string = "?%s" % parse.urlencode(params)
return ("/v1/%(project_id)s/protectables/%(protectable_type)s"
return ("/protectables/%(protectable_type)s"
"/instances%(query_string)s" %
{"project_id": self.project_id,
"protectable_type": protectable_type,
{"protectable_type": protectable_type,
"query_string": query_string})

View File

@@ -10,16 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from smaugclient.common import base
from karborclient.common import base
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
@@ -29,8 +26,7 @@ class ProviderManager(base.ManagerWithFind):
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/providers/{provider_id}".format(
project_id=self.project_id,
url = "/providers/{provider_id}".format(
provider_id=provider_id)
return self._get(url, response_key="provider", headers=headers)

View File

@@ -10,44 +10,37 @@
# License for the specific language governing permissions and limitations
# under the License.
from smaugclient.common import base
from karborclient.common import base
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,
}
}
url = "/v1/{project_id}/restores" .format(
project_id=self.project_id)
return self._create(url, body, 'restore', return_raw=True)
def delete(self, restore_id):
path = '/v1/{project_id}/restores/{restore_id}'.format(
project_id=self.project_id,
restore_id=restore_id)
return self._delete(path)
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 get(self, restore_id, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/restores/{restore_id}".format(
project_id=self.project_id,
url = "/restores/{restore_id}".format(
restore_id=restore_id)
return self._get(url, response_key="restore", headers=headers)

View File

@@ -10,16 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from smaugclient.common import base
from karborclient.common import base
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
@@ -31,14 +28,12 @@ class ScheduledOperationManager(base.ManagerWithFind):
'operation_definition':
operation_definition,
}}
url = "/v1/{project_id}/scheduled_operations" .format(
project_id=self.project_id)
return self._create(url, body, 'scheduled_operation', return_raw=True)
url = "/scheduled_operations"
return self._create(url, body, 'scheduled_operation')
def delete(self, scheduled_operation_id):
path = '/v1/{project_id}/scheduled_operations/{scheduled_operation_id}'.\
format(project_id=self.project_id,
scheduled_operation_id=scheduled_operation_id)
path = '/scheduled_operations/{scheduled_operation_id}'.\
format(scheduled_operation_id=scheduled_operation_id)
return self._delete(path)
def get(self, scheduled_operation_id, session_id=None):
@@ -46,9 +41,8 @@ class ScheduledOperationManager(base.ManagerWithFind):
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/scheduled_operations/{scheduled_operation_id}".\
format(project_id=self.project_id,
scheduled_operation_id=scheduled_operation_id)
url = "/scheduled_operations/{scheduled_operation_id}".\
format(scheduled_operation_id=scheduled_operation_id)
return self._get(url, response_key="scheduled_operation",
headers=headers)

View File

@@ -11,13 +11,16 @@
# under the License.
import argparse
import json
import os
from datetime import datetime
from oslo_utils import uuidutils
from smaugclient.common import base
from smaugclient.common import utils
from smaugclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
from karborclient.common import base
from karborclient.common import utils
from karborclient import utils as arg_utils
@utils.arg('--all-tenants',
@@ -37,6 +40,10 @@ from smaugclient.openstack.common.apiclient import exceptions
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,
@@ -50,7 +57,7 @@ from smaugclient.openstack.common.apiclient import exceptions
@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,
@@ -81,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,
}
@@ -93,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
@@ -110,14 +118,40 @@ def do_plan_list(cs, args):
metavar='<provider_id>',
help='ID of provider.')
@utils.arg('resources',
metavar='<id=type,id=type>',
metavar='<id=type=name=extra_info,id=type=name=extra_info>',
help='Resource in list must be a dict when creating'
' a plan.The keys of resource are id and type.')
' a plan. The keys of resource are id ,type, name and '
'extra_info. The extra_info field is optional.')
@utils.arg('--parameters-json',
type=str,
dest='parameters_json',
metavar='<parameters>',
default=None,
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)
plan = cs.plans.create(args.name, args.provider_id, plan_resources)
utils.print_dict(plan)
"""Creates a plan."""
if not uuidutils.is_uuid_like(args.provider_id):
raise exceptions.CommandError(
"Invalid provider id provided.")
plan_resources = arg_utils.extract_resources(args)
arg_utils.check_resources(cs, plan_resources)
plan_parameters = arg_utils.extract_parameters(args)
plan = cs.plans.create(args.name, args.provider_id, plan_resources,
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',
@@ -126,7 +160,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',
@@ -134,7 +169,7 @@ def do_plan_show(cs, args):
nargs="+",
help='ID of plan.')
def do_plan_delete(cs, args):
"""Delete plan."""
"""Deletes plan."""
failure_count = 0
for plan_id in args.plan:
try:
@@ -153,17 +188,17 @@ 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.")
def do_plan_update(cs, args):
"""Updata a plan."""
"""Updatas a plan."""
data = {}
if args.name is not None:
data['name'] = args.name
if args.resources is not None:
plan_resources = _extract_resources(args)
plan_resources = arg_utils.extract_resources(args)
data['resources'] = plan_resources
if args.status is not None:
data['status'] = args.status
@@ -176,39 +211,40 @@ def do_plan_update(cs, args):
utils.print_dict(plan.to_dict())
def _extract_resources(args):
resources = []
for data in args.resources.split(','):
resource = {}
if '=' in data:
(resource_id, resource_type) = data.split('=', 1)
else:
raise exceptions.CommandError(
"Unable to parse parameter resources.")
resource["id"] = resource_id
resource["type"] = resource_type
resources.append(resource)
return resources
@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."""
"""Creates a restore."""
if not uuidutils.is_uuid_like(args.provider_id):
raise exceptions.CommandError(
"Invalid provider id provided.")
@@ -217,28 +253,25 @@ 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(
"checkpoint_id must be provided.")
restore_parameters = arg_utils.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)
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
parameters[key] = value
return parameters
args.restore_target, restore_parameters,
restore_auth)
dict_format_list = {"parameters"}
utils.print_dict(restore.to_dict(), dict_format_list=dict_format_list)
@utils.arg('--all-tenants',
@@ -261,13 +294,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,
@@ -316,8 +349,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',
@@ -326,11 +361,12 @@ 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):
"""Lists all protectables type."""
"""Lists all protectable types."""
protectables = cs.protectables.list()
@@ -348,6 +384,32 @@ def do_protectable_show(cs, args):
utils.print_dict(protectable.to_dict())
@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': (arg_utils.extract_instances_parameters(args)
if args.parameters else None),
}
instance = cs.protectables.get_instance(args.protectable_type,
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',
metavar='<protectable_type>',
help='Type of protectable.')
@@ -358,13 +420,13 @@ def do_protectable_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 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,
@@ -380,11 +442,20 @@ def do_protectable_show(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': (arg_utils.extract_instances_parameters(args)
if args.parameters else None),
}
if args.sort and (args.sort_key or args.sort_dir):
@@ -398,14 +469,17 @@ def do_protectable_list_instances(cs, args):
sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Type', 'Dependent resources']
key_list = ['Id', 'Type', 'Name', 'Dependent resources', 'Extra info']
if args.sort_key or args.sort_dir or args.sort:
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)
@utils.arg('provider_id',
@@ -414,7 +488,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',
@@ -428,13 +503,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,
@@ -467,7 +542,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
@@ -483,33 +558,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)
"""Creates a checkpoint."""
checkpoint_extra_info = None
if args.extra_info is not None:
checkpoint_extra_info = arg_utils.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)
@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,
help='Filters results by a project id. Default=None.')
help='Filters results by a project ID. Default=None.')
@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,
@@ -527,9 +626,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,
}
@@ -543,14 +663,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',
@@ -562,7 +685,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',
@@ -573,13 +699,13 @@ def do_checkpoint_show(cs, args):
nargs="+",
help='ID of checkpoint.')
def do_checkpoint_delete(cs, args):
"""Delete checkpoints."""
"""Deletes checkpoints."""
failure_count = 0
for checkpoint_id in args.checkpoint:
try:
checkpoint = cs.checkpoints.get(args.provider_id,
checkpoint_id)
cs.checkpoints.delete(checkpoint.provider_id, checkpoint.id)
cs.checkpoints.delete(args.provider_id, checkpoint.id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; checkpoint not found".
@@ -617,13 +743,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,
@@ -673,8 +799,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',
@@ -684,26 +813,31 @@ 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)
"""Creates a trigger."""
trigger_properties = arg_utils.extract_properties(args)
trigger = cs.triggers.create(args.name, args.type, trigger_properties)
utils.print_dict(trigger)
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 '=' in data:
(resource_key, resource_value) = data.split('=', 1)
else:
raise exceptions.CommandError(
"Unable to parse parameter properties.")
properties[resource_key] = resource_value
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 = arg_utils.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',
@@ -712,7 +846,8 @@ def _extract_properties(args):
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',
@@ -720,7 +855,7 @@ def do_trigger_show(cs, args):
nargs="+",
help='ID of trigger.')
def do_trigger_delete(cs, args):
"""Delete trigger."""
"""Deletes trigger."""
failure_count = 0
for trigger_id in args.trigger:
try:
@@ -760,6 +895,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 a operation definition. Default=None.')
@utils.arg('--marker',
metavar='<marker>',
default=None,
@@ -800,8 +939,9 @@ def do_scheduledoperation_list(cs, args):
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'operation_type': args.type,
'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):
@@ -834,29 +974,18 @@ 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."""
operation_definition = _extract_operation_definition(args)
scheduledoperation = cs.scheduledoperations.create(args.name,
args.operation_type,
args.trigger_id,
operation_definition)
utils.print_dict(scheduledoperation)
def _extract_operation_definition(args):
operation_definition = {}
for data in args.operation_definition.split(':'):
if '=' in data:
(resource_key, resource_value) = data.split('=', 1)
else:
raise exceptions.CommandError(
"Unable to parse parameter operation_definition.")
operation_definition[resource_key] = resource_value
return operation_definition
"""Creates a scheduled operation."""
operation_definition = arg_utils.extract_operation_definition(args)
scheduledoperation = cs.scheduled_operations.create(args.name,
args.operation_type,
args.trigger_id,
operation_definition)
dict_format_list = {"operation_definition"}
utils.print_dict(scheduledoperation.to_dict(),
dict_format_list=dict_format_list)
@utils.arg('scheduledoperation',
@@ -864,8 +993,10 @@ def _extract_operation_definition(args):
help='ID of scheduled operation.')
def do_scheduledoperation_show(cs, args):
"""Shows scheduledoperation details."""
scheduledoperation = cs.scheduledoperations.get(args.scheduledoperation)
utils.print_dict(scheduledoperation.to_dict())
scheduledoperation = cs.scheduled_operations.get(args.scheduledoperation)
dict_format_list = {"operation_definition"}
utils.print_dict(scheduledoperation.to_dict(),
dict_format_list=dict_format_list)
@utils.arg('scheduledoperation',
@@ -873,13 +1004,13 @@ def do_scheduledoperation_show(cs, args):
nargs="+",
help='ID of scheduled operation.')
def do_scheduledoperation_delete(cs, args):
"""Delete a scheduled operation."""
"""Deletes a scheduled operation."""
failure_count = 0
for scheduledoperation_id in args.scheduledoperation:
try:
scheduledoperation = utils.find_resource(cs.scheduledoperations,
scheduledoperation = utils.find_resource(cs.scheduled_operations,
scheduledoperation_id)
cs.scheduledoperations.delete(scheduledoperation.id)
cs.scheduled_operations.delete(scheduledoperation.id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; scheduledoperation not found".

View File

@@ -10,16 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from smaugclient.common import base
from karborclient.common import base
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
@@ -29,13 +26,11 @@ class TriggerManager(base.ManagerWithFind):
'type': type,
'properties': properties,
}}
url = "/v1/{project_id}/triggers" .format(
project_id=self.project_id)
return self._create(url, body, 'trigger_info', return_raw=True)
url = "/triggers"
return self._create(url, body, 'trigger_info')
def delete(self, trigger_id):
path = '/v1/{project_id}/triggers/{trigger_id}'.format(
project_id=self.project_id,
path = '/triggers/{trigger_id}'.format(
trigger_id=trigger_id)
return self._delete(path)
@@ -44,11 +39,18 @@ class TriggerManager(base.ManagerWithFind):
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/triggers/{trigger_id}".format(
project_id=self.project_id,
url = "/triggers/{trigger_id}".format(
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

@@ -13,4 +13,4 @@
from pbr import version
version_info = version.VersionInfo('python-smaugclient')
version_info = version.VersionInfo('python-karborclient')

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=smaugclient

View File

@@ -1,13 +1,14 @@
# 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.6.0 # Apache-2.0
requests!=2.9.0,>=2.8.1 # Apache-2.0
pbr!=2.1.0,>=2.0.0 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
keystoneauth1>=2.21.0 # Apache-2.0
requests>=2.14.2 # Apache-2.0
simplejson>=2.2.0 # MIT
Babel>=1.3 # BSD
Babel!=2.4.0,>=2.3.4 # BSD
six>=1.9.0 # MIT
oslo.utils>=3.5.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
osc-lib>=1.5.1 # Apache-2.0
oslo.utils>=3.20.0 # Apache-2.0
oslo.log>=3.22.0 # Apache-2.0
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0

View File

@@ -1,12 +1,11 @@
[metadata]
name = python-smaugclient
version = 0.0.1
summary = Python client library for Smaug API
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 = https://docs.openstack.org/developer/karbor/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -17,8 +16,7 @@ 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 =
@@ -26,11 +24,57 @@ setup-hooks =
[files]
packages =
smaugclient
karborclient
[entry_points]
console_scripts =
smaug = smaugclient.shell:main
karbor = karborclient.shell:main
openstack.cli.extension =
data_protection = karborclient.osc.plugin
openstack.data_protection.v1 =
data_protection_plan_list = karborclient.osc.v1.plans:ListPlans
data_protection_plan_show = karborclient.osc.v1.plans:ShowPlan
data_protection_plan_create = karborclient.osc.v1.plans:CreatePlan
data_protection_plan_update = karborclient.osc.v1.plans:UpdatePlan
data_protection_plan_delete = karborclient.osc.v1.plans:DeletePlan
data_protection_restore_list = karborclient.osc.v1.restores:ListRestores
data_protection_restore_show = karborclient.osc.v1.restores:ShowRestore
data_protection_restore_create = karborclient.osc.v1.restores:CreateRestore
data_protection_provider_list = karborclient.osc.v1.providers:ListProviders
data_protection_provider_show = karborclient.osc.v1.providers:ShowProvider
data_protection_protectable_list = karborclient.osc.v1.protectables.ListProtectables
data_protection_protectable_show = karborclient.osc.v1.protectables.ShowProtectable
data_protection_protectable_instance_list = karborclient.osc.v1.protectables.ListProtectableInstances
data_protection_protectable_instance_show = karborclient.osc.v1.protectables.ShowProtectableInstance
data_protection_trigger_list = karborclient.osc.v1.triggers:ListTriggers
data_protection_trigger_show = karborclient.osc.v1.triggers:ShowTrigger
data_protection_trigger_create = karborclient.osc.v1.triggers:CreateTrigger
data_protection_trigger_update = karborclient.osc.v1.triggers:UpdateTrigger
data_protection_trigger_delete = karborclient.osc.v1.triggers:DeleteTrigger
data_protection_checkpoint_list = karborclient.osc.v1.checkpoints:ListCheckpoints
data_protection_checkpoint_show = karborclient.osc.v1.checkpoints:ShowCheckpoint
data_protection_checkpoint_create = karborclient.osc.v1.checkpoints:CreateCheckpoint
data_protection_checkpoint_delete = karborclient.osc.v1.checkpoints:DeleteCheckpoint
data_protection_scheduledoperation_list = karborclient.osc.v1.scheduled_operations:ListScheduledOperations
data_protection_scheduledoperation_show = karborclient.osc.v1.scheduled_operations:ShowScheduledOperation
data_protection_scheduledoperation_create = karborclient.osc.v1.scheduled_operations:CreateScheduledOperation
data_protection_scheduledoperation_delete = karborclient.osc.v1.scheduled_operations:DeleteScheduledOperation
[compile_catalog]
directory = karborclient/locale
domain = karborclient
[update_catalog]
domain = karborclient
output_dir = karborclient/locale
input_file = karborclient/locale/karborclient.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
output_file = karborclient/locale/karborclient.pot
[build_sphinx]
source-dir = doc/source

View File

@@ -25,5 +25,5 @@ except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=1.8'],
setup_requires=['pbr>=2.0.0'],
pbr=True)

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

@@ -2,14 +2,16 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=0.10.2,<0.11 # Apache-2.0
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
coverage>=3.6 # Apache-2.0
discover # BSD
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 # BSD
oslosphinx>=2.5.0,!=3.4.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
docutils>=0.11 # OSI-Approved Open Source, Public Domain
sphinx>=1.6.2 # BSD
openstackdocstheme>=1.11.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
testtools>=1.4.0 # MIT
python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0
requests-mock>=1.1 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT

30
tools/tox_install.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Client constraint file contains this client version pin that is in conflict
# with installing the client from source. We should remove the version pin in
# the constraints file before applying it for from-source installation.
CONSTRAINTS_FILE="$1"
shift 1
set -e
# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get
# published to logs.openstack.org for easy debugging.
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
if [[ "$CONSTRAINTS_FILE" != http* ]]; then
CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE"
fi
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile"
pip install -c"$localfile" openstack-requirements
# This is the main purpose of the script: Allow local installation of
# the current repo. It is listed in constraints file and thus any
# install will be constrained and we need to unconstrain it.
edit-constraints "$localfile" -- "$CLIENT_NAME"
pip install -c"$localfile" -U "$@"
exit $?

23
tox.ini
View File

@@ -1,13 +1,16 @@
[tox]
minversion = 1.6
envlist = py34,py27,pypy,pep8
minversion = 2.0
envlist = py35,py27,pypy,pep8
skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
BRANCH_NAME=master
CLIENT_NAME=python-karborclient
PYTHONWARNINGS=default::DeprecationWarning
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py test --slowest --testr-args='{posargs}'
@@ -20,21 +23,23 @@ commands = {posargs}
[testenv:functional]
setenv =
OS_TEST_PATH = ./smaugclient/tests/functional
{[testenv]setenv}
OS_TEST_PATH = ./karborclient/tests/functional
passenv = OS_*
[testenv:cover]
commands = python setup.py test --coverage --testr-args='{posargs}'
commands =
python setup.py test --coverage --testr-args='{posargs}'
coverage report
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
commands = oslo_debug_helper -t karborclient/tests {posargs}
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
ignore =
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools