Compare commits

...

281 Commits

Author SHA1 Message Date
Kendall Nelson
e1087267bb Retire python-karborclient
As announced on the openstack-discuss ML[1], Karbor is retiring
this cycle (Wallaby).

This commit retires this repository as per the process defined in
the project-guide[2].

Thank you to all the contributors of Karbor for your hard work!

[1] http://lists.openstack.org/pipermail/openstack-discuss/2020-November/018643.html
[2] https://docs.openstack.org/project-team-guide/repository.html#retiring-a-repository

Depends-On: https://review.opendev.org/c/openstack/project-config/+/767030
Change-Id: Ic1b039239b8141097873b2f90c448d613c9c11df
2020-12-21 11:55:16 -08:00
Andreas Jaeger
c4b0bd2cdb Switch to newer openstackdocstheme and reno versions
Switch to openstackdocstheme 2.2.1 and reno 3.1.0 versions. Using
these versions will allow especially:
* Linking from HTML to PDF document
* Allow parallel building of documents
* Fix some rendering problems

Update Sphinx version as well.

Remove docs requirements from lower-constraints, they are not needed
during install or test but only for docs building.

openstackdocstheme renames some variables, so follow the renames
before the next release removes them. A couple of variables are also
not needed anymore, remove them.

Change pygments_style to 'native' since old theme version always used
'native' and the theme now respects the setting and using 'sphinx' can
lead to some strange rendering.

See also
http://lists.openstack.org/pipermail/openstack-discuss/2020-May/014971.html

Change-Id: I34f79cee33b7f5ad4a3adf0ad73f7e38052e921b
2020-05-22 17:18:38 +00:00
Zuul
44982f2456 Merge "Update hacking for Python3" 2020-04-21 07:35:42 +00:00
b475012330 Add Python3 victoria unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for victoria.

See also the PTI in governance [1].

[1]: https://governance.openstack.org/tc/reference/project-testing-interface.html

Change-Id: I01a66fa1b522c19392228d70c784ba8bb80603fe
2020-04-11 18:44:10 +00:00
Andreas Jaeger
9c8f2f29d4 Cleanup py27 support
Make a few cleanups:
- Remove python 2.7 stanza from setup.py
- Add requires on python >= 3.6 to setup.cfg so that pypi and pip
  know about the requirement
- Remove obsolete sections from setup.cfg:
  * Wheel is not needed for python 3 only repo
  * Some other sections are obsolete
- Update classifiers

Change-Id: I175c9e6078c352950933372e7ea89d5951fd6fc6
2020-04-04 16:52:27 +02:00
Andreas Jaeger
9af7683133 Update hacking for Python3
The repo is Python 3 now, so update hacking to version 3.0 which
supports Python 3.

Fix problems found.

Change-Id: I3dfc16a044db1eaffae909db5aa11496d4294a89
2020-03-31 12:10:29 +02:00
Ghanshyam Mann
6ded5189ba [ussuri][goal] Drop python 2.7 support and testing
OpenStack is dropping the py2.7 support in ussuri cycle.

python-karborclient is ready with python 3 and ok to drop the
python 2.7 support.

Complete discussion & schedule can be found in
- http://lists.openstack.org/pipermail/openstack-discuss/2019-October/010142.html
- https://etherpad.openstack.org/p/drop-python2-support

Ussuri Communtiy-wide goal:
https://governance.openstack.org/tc/goals/selected/ussuri/drop-py27.html

Change-Id: I777e4ecb3dbb1bea98a9a8c2e5bde1ee4129cc0f
2019-12-15 01:15:10 +00:00
Zuul
1419988ace Merge "Add unit test for quotas" 2019-11-09 07:28:19 +00:00
liushuai
21addfcca0 Add unit test for quotas
Change-Id: Id0d6ee5142687f64cbf448a8983683b19eb4b1f6
2019-11-07 23:44:37 +08:00
liushuai
56681c318e Add unit test for triggers
Change-Id: I3b6aaead255b51c742e8602d09b4b3f7f152ec6c
2019-11-07 17:34:58 +08:00
liushuai
5bd1b2feeb Add unit test for operation logs
Change-Id: I812ccf4ac1e5ab3f3d2402bcf9a9dd5b88781513
2019-11-05 22:44:18 +08:00
liushuai
05139e97e8 optional argument should have default values
Change-Id: I5ff5447f8ceec0a8ff25d46208a59eb2d6c8e307
Closes-Bug: #1844488
2019-09-23 17:29:28 +08:00
Zuul
c4e27f2e6d Merge "Add Python 3 Train unit tests" 2019-09-09 12:09:39 +00:00
jacky06
1a98ae3101 Replace git.openstack.org URLs with opendev.org URLs
Change-Id: Ia80a351665da5428d3c7c4cb518ecf0afc2ef8c1
2019-08-24 10:52:37 +08:00
chenke
4571dcb492 Switch to the new canonical constraints URL on master
Reference:
1. http://lists.openstack.org/pipermail/openstack-discuss/2019-May/006478.html
2. https://github.com/openstack/nova/blob/master/tox.ini#L17

Change-Id: Ie02a59eaee3807432c111e301163e87ab5afe2bd
2019-07-03 15:55:53 +08:00
Corey Bryant
9cd38596cd Add Python 3 Train unit tests
This is a mechanically generated patch to ensure unit testing is in place
for all of the Tested Runtimes for Train.

See the Train python3-updates goal document for details:
https://governance.openstack.org/tc/goals/train/python3-updates.html

Change-Id: Ia67bc92c85694a6be8eea65b9c4bb661ecc13b36
Story: #2005924
Task: #34214
2019-06-24 15:16:41 -04:00
Jiao Pengju
17f75a9c00 Fix listing with --all error
When executing command "karbor xxx-list --all", it will raise
error as 'error: ambiguous option: --all could match --all-tenants,
--all_tenants'. The reason is we have both '--all-tenants' and
'--all_tenants' in the specify operations, '--all' matches two
args, so it can not work, but when using '--all-' or '--all_',
it return the correct result. We should fix it, so we remove the
arg '--all_tenants' which is not in the help info.
Story: 2005874
Task: 33686

Change-Id: Iafa70c35594af732435122ebd50c114fd7f0b9df
2019-06-16 12:07:15 +08:00
OpenDev Sysadmins
6a5f46615c OpenDev Migration Patch
This commit was bulk generated and pushed by the OpenDev sysadmins
as a part of the Git hosting and code review systems migration
detailed in these mailing list posts:

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

Attempts have been made to correct repository namespaces and
hostnames based on simple pattern matching, but it's possible some
were updated incorrectly or missed entirely. Please reach out to us
via the contact information listed at https://opendev.org/ with any
questions you may have.
2019-04-19 19:41:49 +00:00
Zuul
036ec8746a Merge "Update json module to jsonutils" 2019-03-21 09:05:28 +00:00
cao.yuan
56474b54df Update json module to jsonutils
oslo project provide jsonutils, and karborclient use it in many place[1],
this PS to update the remained json module to oslo jsonutils for
consistency.

[1]: https://github.com/openstack/python-karborclient/search?utf8=%E2%9C%93&q=jsonutils&type=

Change-Id: I8c2c0383eac12a5562f205640d6c8c7d062266b1
2019-02-25 20:15:46 +08:00
ZhongShengping
998e72a03a add python 3.7 unit test job
This is a mechanically generated patch to add a unit test job running
under Python 3.7.

See ML discussion here [1] for context.

[1] http://lists.openstack.org/pipermail/openstack-dev/2018-October/135626.html

Change-Id: I207e95619a8f4e8948f0d71404738a32daa7b5ba
Story: #2004073
Task: #27421
2019-02-19 17:06:02 +08:00
98k
7d93f625d9 Add doc/requirements.txt to docs tox environment
Without these dependencies, the releasenotes build does not actually
work.

Change-Id: Ie38200dafb86dbf4cc604ae837fc04f42c47b399
2019-01-09 17:46:52 +00:00
Zuul
3216f64d14 Merge "Add Python 3.6 classifier to setup.cfg" 2018-12-21 01:27:52 +00:00
Zuul
e5440f809d Merge "Convert trigger window from string to integer" 2018-12-10 06:26:49 +00:00
YUHAN
d45538cf41 Convert trigger window from string to integer
Change-Id: Iccdaea4b4e5aedd2548e865576eb718643e41cf9
2018-12-10 12:56:18 +08:00
98k
ce7b872c26 Change openstack-dev to openstack-discuss
Mailinglists have been updated. Openstack-discuss replaces openstack-dev.

Change-Id: I7fd74af8edb78fd95f59162f1282f1434c522cac
2018-12-04 07:38:17 +00:00
Zuul
a0a7b4a24b Merge "Add osc support to update plan description" 2018-12-04 07:17:46 +00:00
liushuai
c2e7441444 Add osc support to update plan description
Change-Id: Ibc661594342a89cc5f89084972a4fb9844da1a84
2018-12-04 13:56:51 +08:00
Zuul
5cf1f0d246 Merge "Unsubmitted name field shoud be ignored" 2018-12-04 05:48:27 +00:00
Zuul
c992876f96 Merge "Add support to update plan description" 2018-12-04 05:45:40 +00:00
Zuul
bc9ac570bf Merge "Add osc support to reset checkpoint state" 2018-12-03 08:53:48 +00:00
Jiao Pengju
048b3a0bd9 Add support to reset checkpoint state
This patch added clinet support for doing
checkpoint state reset.
Implements: bp checkpoint-status-reset

Change-Id: Id34501bd4d43c6ae0e9d0d789be7e92581cbff8c
2018-12-03 14:14:50 +08:00
liushuai
58234ab51c Add support to update plan description
Change-Id: I048e970cd449e0e51bbfc3a97e325afd0fa73d5c
2018-12-02 23:19:04 +08:00
Jiao Pengju
1eb26df991 Add osc support to reset checkpoint state
This patch added osc clinet support for doing
checkpoint state reset.
Implements: bp checkpoint-status-reset

Change-Id: If7c2ae3563ff0959c4c59f2b23a8c7c9ea11e196
2018-12-02 22:35:47 +08:00
qingszhao
47d15a74a3 Add Python 3.6 classifier to setup.cfg
Change-Id: Ibc1b7be0b55756b8768b58b04b0898b76aba8427
2018-11-30 06:56:08 +00:00
liushuai
597e452dbc Unsubmitted name field shoud be ignored
Closes-Bug: #1805815

Change-Id: Ife1a13b7b145eff7a8e8e8471bac0ee43c195c68
2018-11-30 00:11:01 +08:00
liushuai
262799e3c0 Convert trigger window from string to integer
Closes-Bug: #1805755

Change-Id: Ib2da9cb008fad3f9d686a1409c83d98b7daebe68
2018-11-29 11:21:42 +08:00
Zuul
f3c117e17c Merge "Add osc all tenants support for checkpoint listing" 2018-11-19 03:36:42 +00:00
Jiao Pengju
e3ed8939b8 Add osc all tenants support for checkpoint listing
Change-Id: I19c5461f28425377918ebc3faeaf5a7340eaead8
Implements: bp checkpoint-all-tenants
2018-11-18 21:00:08 +08:00
Jiao Pengju
38b2b847c8 Add all tenants support for checkpoint listing
Change-Id: Iffcc68efad6a218faa9a6d6d53ff1f7b833ed13e
Implements: bp checkpoint-all-tenants
2018-11-18 20:05:52 +08:00
Jiao Pengju
2fe9422e04 Limit the operation type for scheduledoperation
Now karbor only support two types of scheduledoperation,
but the client do not show and limit the values. So the
end users can type any string to execute the command of
'scheduledoperation create', but the server returns error
, this will make users confused, and they still not know
the right value. This patch will limit the operation type
in 'protect' and 'retention_protect'.

Change-Id: Ic1110124472ac455f988bb25254feeb4417caf1a
2018-11-09 12:38:52 +08:00
Zuul
b0011487e2 Merge "Use templates for cover and lower-constraints" 2018-10-10 01:03:12 +00:00
Chen
db689c650f Remove PyPI downloads
According to official site,
https://packaging.python.org/guides/analyzing-pypi-package-downloads/
PyPI package download statistics is no longer maintained and thus
should be removed.

Change-Id: I0cdc7bdfa4a8c36d57a2c0f5391a7e8c53925fed
2018-10-06 02:45:59 +00:00
Andreas Jaeger
1c2079ef71 Use templates for cover and lower-constraints
Small cleanups:

* Use openstack-tox-cover template, this runs the cover job
  in the check queue only. Remove individual cover jobs.
* Use openstack-lower-constraints-jobs template, remove individual
  jobs.
* Sort list of templates

Change-Id: I1d4176ff6a3c53c447f9d118008d3d51ac455b88
2018-09-29 19:09:06 +02:00
Nguyen Hai
038e2f6984 add python 3.6 unit test job
This is a mechanically generated patch to add a unit test job running
under Python 3.6 as part of the python3-first goal.

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

Change-Id: Ie2624ea979e19abe3feae68d8315c54fd9a9f7ff
Story: #2002586
Task: #24303
2018-08-22 15:08:13 +09:00
Nguyen Hai
334ef1ec33 switch documentation job to new PTI
This is a mechanically generated patch to switch the documentation
jobs to use the new PTI versions of the jobs as part of the
python3-first goal.

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

Change-Id: I4e3d217ba4d67fd227d532ac2ce6a36f36d68815
Story: #2002586
Task: #24303
2018-08-22 15:08:12 +09:00
Nguyen Hai
4455105df5 import zuul job settings from project-config
This is a mechanically generated patch to complete step 1 of moving
the zuul job settings out of project-config and into each project
repository.

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

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

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

Change-Id: Ic6f174b987bd6a32af097926951bac75cb15f6ea
Story: #2002586
Task: #24303
2018-08-22 15:08:11 +09:00
Doug Hellmann
7aca76b32b fix tox python3 overrides
We want to default to running all tox environments under python 3, so
set the basepython value in each environment.

We do not want to specify a minor version number, because we do not
want to have to update the file every time we upgrade python.

We do not want to set the override once in testenv, because that
breaks the more specific versions used in default environments like
py35 and py36.

Change-Id: I8b2f1a62f8c9ca04d6c63f4a4ad22ec445bad88b
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2018-06-06 17:58:17 -04:00
Zuul
a614c845b2 Merge "Follow the new PTI for document build" 2018-04-21 07:32:13 +00:00
Zuul
8929cad166 Merge "add lower-constraints job" 2018-04-21 07:28:52 +00:00
melissaml
103d4dac05 Trivial: Update pypi url to new url
Pypi url changed from [1] to [2]

[1] https://pypi.python.org/pypi/<package>
[2] https://pypi.org/project/<package>

Change-Id: I7e141db7a05cdfb02dac362b14789c2bee51e184
2018-04-21 06:22:13 +08:00
Doug Hellmann
0fe7b7c087 add lower-constraints job
Create a tox environment for running the unit tests against the lower
bounds of the dependencies.

Create a lower-constraints.txt to be used to enforce the lower bounds
in those tests.

Add openstack-tox-lower-constraints job to the zuul configuration.

See http://lists.openstack.org/pipermail/openstack-dev/2018-March/128352.html
for more details.

Change-Id: I71c76a9173aaa7429b766225f19819715a9ccf64
Depends-On: https://review.openstack.org/555034
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2018-04-20 16:22:33 -04:00
Nguyen Hai
01d1aa0da9 Follow the new PTI for document build
- For compliance with the Project Testing Interface as described in:
https://governance.openstack.org/tc/reference/project-testing-interface.html
http://lists.openstack.org/pipermail/openstack-dev/2017-December/125710.html
- Remove the '[build_sphinx]' as described in:
http://lists.openstack.org/pipermail/openstack-dev/2018-March/128594.html
- This patch also changes minor mistakes in docs.

Change-Id: Iec58d57d59b98c6086910e532e16f1e417815c95
2018-04-12 12:33:29 +09:00
Zuul
71034945a3 Merge "Updated from global requirements" 2018-04-12 02:02:41 +00:00
Zuul
1658f71943 Merge "Update links in README" 2018-04-11 08:18:40 +00:00
OpenStack Proposal Bot
e7e720ca1d Updated from global requirements
Change-Id: I866a1aeb9dbd8c8e1ca502b23c8e4993b232b64e
2018-03-23 01:46:55 +00:00
melissaml
c3b5d573ad Update links in README
Change the outdated links to the latest links in README

Change-Id: If90de4139843539d890948d225eda64a249501b1
2018-03-11 03:04:46 +08:00
Zuul
2dd285549d Merge "Updated from global requirements" 2018-01-31 08:12:23 +00:00
Zuul
c14a1db371 Merge "Change home-page url for karborclient" 2018-01-30 02:06:31 +00:00
Zuul
26d570239d Merge "Add 'rm -f .testrepository/times.dbm' command in testenv" 2018-01-29 01:50:13 +00:00
OpenStack Proposal Bot
63c9890f21 Updated from global requirements
Change-Id: Ib644818e987386a6509ae4884b1dbc0d8418082c
2018-01-27 18:31:25 +00:00
Zuul
86fd679be3 Merge "Updated from global requirements" 2018-01-26 06:17:42 +00:00
OpenStack Proposal Bot
604007cbd3 Updated from global requirements
Change-Id: I21577f0c7cb5c622e13b13cd5e35781391d90319
2018-01-24 02:19:37 +00:00
Yuanbin.Chen
84d19ef13c Delete tox.ini functional when functional test is real.
This patch delete tox.ini functional, the
"/karborclient/tests/functional" is not exist.

Change-Id: I29b40bac7aff8e082e36a1575dc442370911088a
Signed-off-by: Yuanbin.Chen <cybing4@gmail.com>
2018-01-18 23:23:12 +08:00
chenpengzi
4c47bd020f Change home-page url for karborclient
Change-Id: I302b984078cb25b368b4aa5d47bd6f8ea7595d72
2018-01-10 13:03:10 +00:00
OpenStack Proposal Bot
4a5fe7dbc9 Updated from global requirements
Change-Id: I724d191cf50842c274a86bb53841abe28597bc13
2017-12-21 00:43:00 +00:00
Zuul
2d1ac6e744 Merge "Fixed non-ascii in README.txt" 2017-12-19 08:27:56 +00:00
Zuul
f93ac0b598 Merge "Avoid tox_install.sh for constraints support" 2017-12-15 07:32:54 +00:00
Thomas Goirand
194195a97a Fixed non-ascii in README.txt
Running "LANG=C python setup.py install" just fails because of a single
non-ascii char in README.txt. This patch fixes that.

Change-Id: I6840da97cf41891f75afebe06d00e0d3e3849a3d
2017-12-07 22:37:04 +01:00
Zuul
9cf5a90b5c Merge "Updated from global requirements" 2017-12-05 08:11:10 +00:00
chenying
a4cec6cb18 Add OSC commands for quota classes API
Change-Id: I9286d53d307d7b7b58340102967fc30bf87252a7
Implements: blueprint support-quotas-in-karbor
2017-12-05 15:43:53 +08:00
OpenStack Proposal Bot
b8726f4bf5 Updated from global requirements
Change-Id: I68301a8bd7c988d507740f4f306642cc4d462d6b
2017-12-05 03:32:01 +00:00
Zuul
7e084ac5c4 Merge "Add OSC commands for quota API" 2017-12-04 08:27:17 +00:00
chenying
8cf80d3041 Add OSC commands for quota API
Change-Id: I3b7afc0df984b015fb7a8b5488d447c470bd2a12
Implements: blueprint support-quotas-in-karbor
2017-12-04 14:54:12 +08:00
Andreas Jaeger
0b41772728 Avoid tox_install.sh for constraints support
We do not need tox_install.sh, pip can handle constraints itself
and install the project correctly. Thus update tox.ini and remove
the now obsolete tools/tox_install.sh file.

This follows https://review.openstack.org/#/c/508061 to remove
tools/tox_install.sh.

Change-Id: I28ba0904389699268d4b256f76fbfa3a6792eda0
2017-12-02 17:01:28 +00:00
Zuul
a09807472d Merge "Add commands for quota class API" 2017-11-29 12:15:32 +00:00
Zuul
f84d1c9b20 Merge "Add commands for quota API" 2017-11-29 12:12:01 +00:00
OpenStack Proposal Bot
9d4a1612c5 Updated from global requirements
Change-Id: I05e64b5f5542a722c9ede204b6c1f4e771ff1b88
2017-11-16 11:24:03 +00:00
chenying
3a22b3fcdd Add commands for quota class API
Change-Id: Id1710f986d7ff1943fe3841cfd9837d2316419e0
Implements: blueprint support-quotas-in-karbor
2017-11-14 19:38:42 +08:00
chenying
6c11094dc6 Add commands for quota API
Change-Id: Iac73809780012509ffa4883def68e37c7dc98bd4
Implements: blueprint support-quotas-in-karbor
2017-11-14 16:44:29 +08:00
Zuul
478ebe71ed Merge "Add OSC plugin for the service management API" 2017-10-27 10:17:33 +00:00
Zuul
a4a5088ff0 Merge "Add commands for service management API" 2017-10-26 07:51:42 +00:00
Jiao Pengju
87058d3e24 Add OSC plugin for the service management API
Depends-On: I3e9a62327653ea1dc9b5807f50f250c739c1566d
Change-Id: I49825e80935b357a06b0074c024390cd2ed3a9b9
Implements: blueprint karbor-service-management
2017-10-26 11:18:44 +08:00
Jiao Pengju
aba2875905 Add commands for service management API
Change-Id: I3e9a62327653ea1dc9b5807f50f250c739c1566d
Implements: blueprint karbor-service-management
2017-10-25 19:49:12 +08:00
chenying
50927b5c38 Add OSC plugin for the verification API
Change-Id: I2fbd064a058c7dcaff6e223a3e2c87bb16cdbaa0
Implements: blueprint support-verify-the-checkpoint-api
2017-10-25 15:00:18 +08:00
chenying
f13c3faef4 Add commands for verification API
Change-Id: I6b7075220bce16af9e162f2e42feb10ffd073169
Implements: blueprint support-verify-the-checkpoint-api
2017-10-25 11:15:08 +08:00
yushangbin
e11f2e8006 Add 'rm -f .testrepository/times.dbm' command in testenv
Running py2* post py3* tests results in error. Add
'rm -f .testrepository/times.dbm' command in testenv to
resolve this.

Closes-Bug: #1565928
Change-Id: I111d82a1509af03aa2e141ee3fd9b2ff9e223215
2017-10-13 08:50:31 +08:00
Jenkins
dd2e3ff3a3 Merge "Updated from global requirements" 2017-09-21 06:50:26 +00:00
OpenStack Proposal Bot
346c241876 Updated from global requirements
Change-Id: Ie9b8946e704e2ef85e73ef6d6c91a9537f2d4b83
2017-09-21 03:49:23 +00:00
lihaijing
1bfd6d0ee4 Delete bash_completion in subcommand
There are two "completion" in the subcommand table: bash-completion
and bash_completion. but "bash_completion" is not in help information
and it is repeated with "bash-completion", so delete it.

Change-Id: I18da874681ea00c18d72e164dc55aeea9d40731d
Closes-Bug: #1670123
2017-08-23 14:55:29 +08:00
Jenkins
7a7f708282 Merge "Updated from global requirements" 2017-08-20 08:48:26 +00:00
Jenkins
ea42b2ab3e Merge "Fix OSC scheduledoperations commands formatting" 2017-08-20 08:41:20 +00:00
OpenStack Proposal Bot
996cc1625f Updated from global requirements
Change-Id: I6a1540e5bd7966f887dbb0f968676f7d9bef2f10
2017-08-18 04:51:27 +00:00
chenying
a7de5b981e Fix OSC scheduledoperations commands formatting
Change-Id: I7eacb653ed8549c453fdca6598447bd9b5f93cc8
2017-08-17 09:47:58 +08:00
chenying
982a8c361d Fix OSC restore commands formatting
Change-Id: Ia85065631eef1f3d96df007e240df04362f18754
2017-08-16 21:10:35 +08:00
Yuval Brik
855cbe5cbb Fix OSC provider show formatting
Change-Id: I5c2c8283dd367ccdbf268b6abc38899a565fe94b
2017-08-15 10:16:19 +03:00
Yuval Brik
41d93cdca0 Fix OSC protectable show formatting
Change-Id: I353a9e2938335e1de3d1b2902a780039e03e5b7c
2017-08-15 10:15:58 +03:00
Yuval Brik
888008676c Convert DOS newlines to Unix newlines
Change-Id: If05dce63dadecd89de58350173fc000055e63ca5
2017-08-15 10:15:33 +03:00
Yuval Brik
c14aea8079 Fix OSC protectable instance formatting
Change-Id: Ib06abd01682c7194547332582c64b4c7ae7f5a05
2017-08-15 10:14:50 +03:00
Yuval Brik
e1f7540bac Use a copy of global literal for each test
If global is used in OSC tests, it might be altered, and then the test
result depends on concurrency and order.
Use a copy of the global literal for deterministic results.

Change-Id: I177cc4345c5ed194f2c36d80acc53db113f814e4
2017-08-15 10:13:10 +03:00
Jenkins
6a53851650 Merge "Fix OSC checkpoint show and create formatting" 2017-08-14 10:27:11 +00:00
Jenkins
84e4ff3733 Merge "Checkpoint list: show only plan name and id" 2017-08-14 10:25:50 +00:00
Yuval Brik
e774b2178c Fix OSC plan show, update, and create formatting
Change-Id: I0b7377506e38bf31f45e5bb5ad75f423a344af62
2017-08-14 11:22:05 +03:00
Yuval Brik
e3d987a2ce Fix OSC checkpoint show and create formatting
Change-Id: Iecbda83be307c30a432cc40a448429dc81a1245e
2017-08-14 11:11:03 +03:00
Yuval Brik
9b77c76ed0 Checkpoint list: show only plan name and id
No need to list the entire plan for each checkpoint in the checkpoint
list commands. Show only the name and id.

Change-Id: I50a7e65e8fe810e43e762a4355c7976c9c4eca96
2017-08-14 11:09:30 +03:00
OpenStack Proposal Bot
aa5d56495f Updated from global requirements
Change-Id: I8d9b3a48335b7eeb71464de54ed8adca7dbdc8c6
2017-07-27 20:32:31 +00:00
Yuval Brik
d5842f6196 Docs: arrange guides
Change-Id: I956d0137f3c1507d58aac0d9b22b6737473e55be
2017-07-27 12:10:38 +03:00
Jenkins
423e4d5b48 Merge "Add operation log API cmd to karborclient" 2017-07-27 06:23:16 +00:00
OpenStack Proposal Bot
30ee5f4829 Updated from global requirements
Change-Id: If26478dac87f9bc018e71c8ac8f39662aa37aeb2
2017-07-23 13:51:53 +00:00
OpenStack Proposal Bot
4839860b73 Updated from global requirements
Change-Id: Id0331e836c7530a6fce73a909675a0972b52236a
2017-07-22 16:38:24 +00:00
Hangdong Zhang
28ab39e6fb Update URLs in documentation
Update URLs according to OpenStack document migration.
BTW: Do some optimization as well (http -> https)

Change-Id: Ie0a5e08a0611715c6cb775b875b4fcae3b622780
2017-07-20 15:57:42 +08:00
Akihiro Motoki
a907bc2a34 Fix wrong entry points which breaks OSC gate
Change-Id: Ia7ed435b397a1ef82a0b8133c5009e1045ee3816
2017-07-19 12:42:56 +00:00
chenying
cb0b8b0515 Add operation log API cmd to karborclient
Change-Id: I097ec3424b47939ff00f417bedb186305de39a62
blueprint: operation-log-api
2017-07-18 15:42:05 +08:00
yushangbin
05c98f7a20 Fix warning in doc generating
Doc generating shows warning "Title overline too short", this patch
fixes that.

Change-Id: I7dff4f0e4425fcf99a3da690ede4c4659cb46222
2017-07-17 15:44:52 +08:00
rajat29
2d6a97e84a Update URLs in documents according to document migration
Change-Id: Ifd15ac7b3d1ef0b4ab450f797169b83991e8e250
2017-07-14 15:50:38 +05:30
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
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
66 changed files with 10 additions and 6895 deletions

View File

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

55
.gitignore vendored
View File

@@ -1,55 +0,0 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
.eggs
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
.testrepository
.venv
.log
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Complexity
output/*.html
output/*/index.html
# Sphinx
doc/build
# pbr generates these
AUTHORS
ChangeLog
# Editors
*~
.*.swp
.*sw?

View File

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

View File

@@ -1,3 +0,0 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>

View File

@@ -1,7 +0,0 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@@ -1,17 +0,0 @@
If you would like to contribute to the development of OpenStack, you must
follow the steps in this page:
http://docs.openstack.org/infra/manual/developers.html
If you already have a good understanding of how the system works and your
OpenStack accounts are set up, you can skip to the development workflow
section of this documentation to learn how changes to OpenStack should be
submitted for review via the Gerrit tool:
http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Launchpad, not GitHub:
https://launchpad.net/python-smaugclient

View File

@@ -1,4 +0,0 @@
Style Commandments
===============================================
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/

176
LICENSE
View File

@@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

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

View File

@@ -1,63 +1,10 @@
Smaug
======
This project is no longer maintained.
.. image:: https://img.shields.io/pypi/v/python-smaugclient.svg
:target: https://pypi.python.org/pypi/python-smaugclient/
:alt: Latest Version
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".
.. image:: https://img.shields.io/pypi/dm/python-smaugclient.svg
:target: https://pypi.python.org/pypi/python-smaugclient/
:alt: Downloads
Smaug Mission Statement
* Formalize Application Data Protection in OpenStack (APIs, Services, Plugins, …)
* Be able to protect Any Resource in OpenStack(as well as their dependencies)
* Allow Diversity of vendor solutions, capabilities and implementations
without compromising usability
* `PyPi`_ - package installation
* `Launchpad project`_ - release management
* `Blueprints`_ - feature specifications
* `Bugs`_ - issue tracking
* `Source`_
* `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
.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html
Python Smaugclient
-------------------
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``).
Project Resources
-----------------
Project status, bugs, and blueprints are tracked on Launchpad:
* Client bug tracker
* https://launchpad.net/python-smaugclient
* Smaug bug tracker
* https://launchpad.net/smaug
Developer documentation can be found here:
http://docs.openstack.org/developer/smaug
Additional resources are linked from the project wiki page:
https://wiki.openstack.org/wiki/Smaug
License
-------
Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
For any further questions, please email
openstack-discuss@lists.openstack.org or join #openstack-dev on
Freenode.

View File

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

View File

@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
# 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 os
import sys
sys.path.insert(0, os.path.abspath('../..'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
'oslosphinx'
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'python-smaugclient'
copyright = u'2013, OpenStack Foundation'
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'),
]
# Example configuration for intersphinx: refer to the Python standard library.
#intersphinx_mapping = {'http://docs.python.org/': None}

View File

@@ -1,4 +0,0 @@
============
Contributing
============
.. include:: ../../CONTRIBUTING.rst

View File

@@ -1,20 +0,0 @@
Welcome to smaugclient's documentation!
========================================================
Contents:
.. toctree::
:maxdepth: 2
readme
installation
usage
contributing
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -1,12 +0,0 @@
============
Installation
============
At the command line::
$ pip install python-smaugclient
Or, if you have virtualenvwrapper installed::
$ mkvirtualenv python-smaugclient
$ pip install python-smaugclient

View File

@@ -1 +0,0 @@
.. include:: ../../README.rst

View File

@@ -1,7 +0,0 @@
========
Usage
========
To use smaugclient in a project::
import smaugclient

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 +0,0 @@
# 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
simplejson>=2.2.0 # MIT
Babel>=1.3 # 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

View File

@@ -1,43 +0,0 @@
[metadata]
name = python-smaugclient
summary = Python client library for Smaug API
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
[global]
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
smaugclient
[entry_points]
console_scripts =
smaug = smaugclient.shell:main
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1
[upload_sphinx]
upload-dir = doc/build/html
[wheel]
universal = 1

View File

@@ -1,29 +0,0 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=1.8'],
pbr=True)

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# 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 pbr.version
__version__ = pbr.version.VersionInfo(
'python-smaugclient').version_string()

View File

@@ -1,18 +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.
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)

View File

@@ -1,341 +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.
"""
Base utilities to build API operation managers and objects on top of.
"""
import abc
import copy
import six
from six.moves.urllib import parse
from smaugclient.common import http
from smaugclient.openstack.common.apiclient import exceptions
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'name', 'created_at')
SORT_KEY_MAPPINGS = {}
def getid(obj):
"""Abstracts the common pattern of allowing both an object or
an object's ID (UUID) as a parameter when dealing with relationships.
"""
try:
return obj.id
except AttributeError:
return obj
class Manager(object):
"""Managers interact with a particular type of API (servers, flavors,
images, etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
if isinstance(self.api, http.SessionClient):
self.project_id = self.api.get_project_id()
else:
self.project_id = self.api.project_id
def _list(self, url, response_key=None, obj_class=None,
data=None, headers=None, return_raw=False,):
if headers is None:
headers = {}
resp, body = self.api.json_request('GET', url, headers=headers)
if obj_class is None:
obj_class = self.resource_class
if response_key:
if response_key not in body:
body[response_key] = []
data = body[response_key]
else:
data = body
if return_raw:
return data
return [obj_class(self, res, loaded=True) for res in data if res]
def _delete(self, url, headers=None):
if headers is None:
headers = {}
self.api.raw_request('DELETE', url, headers=headers)
def _update(self, url, data, response_key=None, headers=None):
if headers is None:
headers = {}
resp, body = self.api.json_request('PUT', url, data=data,
headers=headers)
# PUT requests may not return a body
if body:
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
def _create(self, url, data=None, response_key=None,
return_raw=False, headers=None):
if headers is None:
headers = {}
if data:
resp, body = self.api.json_request('POST', url,
data=data, headers=headers)
else:
resp, body = self.api.json_request('POST', url, headers=headers)
if return_raw:
if response_key:
return body[response_key]
return body
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
def _get(self, url, response_key=None, return_raw=False, headers=None):
if headers is None:
headers = {}
resp, body = self.api.json_request('GET', url, headers=headers)
if return_raw:
if response_key:
return body[response_key]
return body
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
def _build_list_url(self, resource_type, detailed=False,
search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
if search_opts is None:
search_opts = {}
query_params = {}
for key, val in search_opts.items():
if val:
query_params[key] = val
if marker:
query_params['marker'] = marker
if limit:
query_params['limit'] = limit
if sort:
query_params['sort'] = self._format_sort_param(sort)
else:
# sort_key and sort_dir deprecated in kilo, prefer sort
if sort_key:
query_params['sort_key'] = self._format_sort_key_param(
sort_key)
if sort_dir:
query_params['sort_dir'] = self._format_sort_dir_param(
sort_dir)
# Transform the dict to a sequence of two-element tuples in fixed
# order, then the encoded string will be consistent in Python 2&3.
query_string = ""
if query_params:
params = sorted(query_params.items(), key=lambda x: x[0])
query_string = "?%s" % parse.urlencode(params)
detail = ""
if detailed:
detail = "/detail"
return ("/v1/%(project_id)s/%(resource_type)s%(detail)s"
"%(query_string)s" %
{"project_id": self.project_id,
"resource_type": resource_type, "detail": detail,
"query_string": query_string})
def _format_sort_param(self, sort):
'''Formats the sort information into the sort query string parameter.
The input sort information can be any of the following:
- Comma-separated string in the form of <key[:dir]>
- List of strings in the form of <key[:dir]>
- List of either string keys, or tuples of (key, dir)
For example, the following import sort values are valid:
- 'key1:dir1,key2,key3:dir3'
- ['key1:dir1', 'key2', 'key3:dir3']
- [('key1', 'dir1'), 'key2', ('key3', dir3')]
:param sort: Input sort information
:returns: Formatted query string parameter or None
:raise ValueError: If an invalid sort direction or invalid sort key is
given
'''
if not sort:
return None
if isinstance(sort, six.string_types):
# Convert the string into a list for consistent validation
sort = [s for s in sort.split(',') if s]
sort_array = []
for sort_item in sort:
if isinstance(sort_item, tuple):
sort_key = sort_item[0]
sort_dir = sort_item[1]
else:
sort_key, _sep, sort_dir = sort_item.partition(':')
sort_key = sort_key.strip()
if sort_key in SORT_KEY_VALUES:
sort_key = SORT_KEY_MAPPINGS.get(sort_key, sort_key)
else:
raise ValueError('sort_key must be one of the following: %s.'
% ', '.join(SORT_KEY_VALUES))
if sort_dir:
sort_dir = sort_dir.strip()
if sort_dir not in SORT_DIR_VALUES:
msg = ('sort_dir must be one of the following: %s.'
% ', '.join(SORT_DIR_VALUES))
raise ValueError(msg)
sort_array.append('%s:%s' % (sort_key, sort_dir))
else:
sort_array.append(sort_key)
return ','.join(sort_array)
def _format_sort_key_param(self, sort_key):
if sort_key in SORT_KEY_VALUES:
return SORT_KEY_MAPPINGS.get(sort_key, sort_key)
msg = ('sort_key must be one of the following: %s.' %
', '.join(SORT_KEY_VALUES))
raise ValueError(msg)
def _format_sort_dir_param(self, sort_dir):
if sort_dir in SORT_DIR_VALUES:
return sort_dir
msg = ('sort_dir must be one of the following: %s.'
% ', '.join(SORT_DIR_VALUES))
raise ValueError(msg)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(Manager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
rl = self.findall(**kwargs)
num = len(rl)
if num == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return self.get(rl[0].id)
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class Resource(object):
"""A resource represents a particular instance of an object (tenant, user,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def _add_details(self, info):
for k, v in info.items():
setattr(self, k, v)
def __setstate__(self, d):
for k, v in d.items():
setattr(self, k, v)
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

@@ -1,394 +0,0 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import hashlib
import os
import socket
import keystoneclient.adapter as keystone_adapter
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import requests
import six
from six.moves import urllib
from smaugclient.openstack.common.apiclient import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-smaugclient'
CHUNKSIZE = 1024 * 64 # 64kB
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem',
'/System/Library/OpenSSL/certs/cacert.pem',
requests.certs.where()]
for ca in ca_path:
LOG.debug("Looking for ca file %s", ca)
if os.path.exists(ca):
LOG.debug("Using ca file %s", ca)
return ca
LOG.warning("System ca file could not be found.")
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.auth_url = kwargs.get('auth_url')
self.auth_token = kwargs.get('token')
self.username = kwargs.get('username')
self.password = kwargs.get('password')
self.region_name = kwargs.get('region_name')
self.include_pass = kwargs.get('include_pass')
self.project_id = kwargs.get('project_id')
self.endpoint_url = endpoint
self.cert_file = kwargs.get('cert_file')
self.key_file = kwargs.get('key_file')
self.timeout = kwargs.get('timeout')
self.ssl_connection_params = {
'cacert': kwargs.get('cacert'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
'insecure': kwargs.get('insecure'),
}
self.verify_cert = None
if urllib.parse.urlparse(endpoint).scheme == "https":
if kwargs.get('insecure'):
self.verify_cert = False
else:
self.verify_cert = kwargs.get('cacert', get_system_ca_file())
def _safe_header(self, name, value):
if name in ['X-Auth-Token', 'X-Subject-Token']:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return encodeutils.safe_decode(name), "{SHA1}%s" % d
else:
return (encodeutils.safe_decode(name),
encodeutils.safe_decode(value))
def log_curl_request(self, method, url, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in kwargs['headers'].items():
header = '-H \'%s: %s\'' % self._safe_header(key, value)
curl.append(header)
conn_params_fmt = [
('key_file', '--key %s'),
('cert_file', '--cert %s'),
('cacert', '--cacert %s'),
]
for (key, fmt) in conn_params_fmt:
value = self.ssl_connection_params.get(key)
if value:
curl.append(fmt % value)
if self.ssl_connection_params.get('insecure'):
curl.append('-k')
if 'data' in kwargs:
curl.append('-d \'%s\'' % kwargs['data'])
curl.append('%s%s' % (self.endpoint, url))
LOG.debug(' '.join(curl))
@staticmethod
def log_http_response(resp):
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
dump.append('')
if resp.content:
content = resp.content
if isinstance(content, six.binary_type):
try:
content = encodeutils.safe_decode(resp.content)
except UnicodeDecodeError:
pass
else:
dump.extend([content, ''])
LOG.debug('\n'.join(dump))
def _http_request(self, url, method, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around requests.request to handle tasks such
as setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
else:
kwargs['headers'].update(self.credentials_headers())
if self.auth_url:
kwargs['headers'].setdefault('X-Auth-Url', self.auth_url)
if self.region_name:
kwargs['headers'].setdefault('X-Region-Name', self.region_name)
self.log_curl_request(method, url, kwargs)
if self.cert_file and self.key_file:
kwargs['cert'] = (self.cert_file, self.key_file)
if self.verify_cert is not None:
kwargs['verify'] = self.verify_cert
if self.timeout is not None:
kwargs['timeout'] = float(self.timeout)
# Allow the option not to follow redirects
follow_redirects = kwargs.pop('follow_redirects', True)
# Since requests does not follow the RFC when doing redirection to sent
# back the same method on a redirect we are simply bypassing it. For
# example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says
# that we should follow that URL with the same method as before,
# requests doesn't follow that and send a GET instead for the method.
# Hopefully this could be fixed as they say in a comment in a future
# point version i.e.: 3.x
# See issue: https://github.com/kennethreitz/requests/issues/1704
allow_redirects = False
try:
resp = requests.request(
method,
self.endpoint_url + url,
allow_redirects=allow_redirects,
**kwargs)
except socket.gaierror as e:
message = ("Error finding address for %(url)s: %(e)s" %
{'url': self.endpoint_url + url, 'e': e})
raise exc.EndpointException(message)
except (socket.error,
socket.timeout,
requests.exceptions.ConnectionError) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s" %
{'endpoint': endpoint, 'e': e})
raise exc.ConnectionRefused(message)
self.log_http_response(resp)
if 'X-Auth-Key' not in kwargs['headers'] and \
(resp.status_code == 401 or
(resp.status_code == 500 and "(HTTP 401)" in resp.content)):
raise exc.AuthorizationFailure("Authentication failed. Please try"
" again.\n%s"
% resp.content)
elif 400 <= resp.status_code < 600:
raise exc.from_response(resp, method, url)
elif resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location,
# unless caller specified follow_redirects=False
if follow_redirects:
location = resp.headers.get('location')
path = self.strip_endpoint(location)
resp = self._http_request(path, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp, method, url)
return resp
def strip_endpoint(self, location):
if location is None:
message = "Location not returned with 302"
raise exc.EndpointException(message)
elif location.startswith(self.endpoint):
return location[len(self.endpoint):]
else:
message = "Prohibited endpoint redirect %s" % location
raise exc.EndpointException(message)
def credentials_headers(self):
creds = {}
if self.username:
creds['X-Auth-User'] = self.username
if self.password:
creds['X-Auth-Key'] = self.password
return creds
def json_request(self, method, url, content_type='application/json',
**kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', content_type)
# Don't set Accept because we aren't always dealing in JSON
if 'body' in kwargs:
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
kwargs['data'] = kwargs.pop('body')
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
resp = self._http_request(url, method, **kwargs)
body = resp.content
if body and 'application/json' in resp.headers['content-type']:
try:
body = resp.json()
except ValueError:
LOG.error('Could not decode response body as JSON')
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
if 'body' in kwargs:
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
kwargs['data'] = kwargs.pop('body')
# Chunking happens automatically if 'body' is a
# file-like object
return self._http_request(url, method, **kwargs)
def client_request(self, method, url, **kwargs):
resp, body = self.json_request(method, url, **kwargs)
return resp
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.raw_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
class SessionClient(keystone_adapter.Adapter):
"""Smaug specific keystoneclient Adapter.
"""
def request(self, url, method, **kwargs):
raise_exc = kwargs.pop('raise_exc', True)
resp = super(SessionClient, self).request(url,
method,
raise_exc=False,
**kwargs)
if raise_exc and resp.status_code >= 400:
LOG.trace("Error communicating with {url}: {exc}"
.format(url=url,
exc=exc.from_response(resp, method, url)))
raise exc.from_response(resp, method, url)
return resp, resp.text
def json_request(self, method, url, **kwargs):
headers = kwargs.setdefault('headers', {})
headers['Content-Type'] = kwargs.pop('content_type',
'application/json')
if 'body' in kwargs:
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
kwargs['data'] = kwargs.pop('body')
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
# NOTE(starodubcevna): We need to prove that json field is empty,
# or it will be modified by keystone adapter.
kwargs['json'] = None
resp, body = self.request(url, method, **kwargs)
if body:
try:
body = jsonutils.loads(body)
except ValueError:
pass
return resp, body
def raw_request(self, method, url, **kwargs):
# A non-json request; instead of calling
# super.request, need to call the grandparent
# adapter.request
raise_exc = kwargs.pop('raise_exc', True)
if 'body' in kwargs:
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
kwargs['data'] = kwargs.pop('body')
resp = keystone_adapter.Adapter.request(self,
url,
method,
raise_exc=False,
**kwargs)
if raise_exc and resp.status_code >= 400:
LOG.trace("Error communicating with {url}: {exc}"
.format(url=url,
exc=exc.from_response(resp, method, url)))
raise exc.from_response(resp, method, url)
return resp
def _construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
endpoint = next(iter(args), None)
if session:
service_type = kwargs.pop('service_type', None)
endpoint_type = kwargs.pop('endpoint_type', None)
region_name = kwargs.pop('region_name', None)
service_name = kwargs.pop('service_name', None)
parameters = {
'endpoint_override': endpoint,
'session': session,
'auth': auth,
'interface': endpoint_type,
'service_type': service_type,
'region_name': region_name,
'service_name': service_name,
'user_agent': 'python-smaugclient',
}
parameters.update(kwargs)
return SessionClient(**parameters)
else:
return HTTPClient(*args, **kwargs)

View File

@@ -1,162 +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.
from __future__ import print_function
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__)
# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
# Because of the sematics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
"""
for v in vars:
value = os.environ.get(v, None)
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))
else:
print(encodeutils.safe_encode(pt.get_string(sortby=order)))
def print_list(objs, fields, exclude_unavailable=False, formatters=None,
sortby_index=0):
'''Prints a list of objects.
@param objs: Objects to print
@param fields: Fields on each object to be printed
@param exclude_unavailable: Boolean to decide if unavailable fields are
removed
@param formatters: Custom field formatters
@param sortby_index: Results sorted against the key in the fields list at
this index; if None then the object order is not
altered
'''
formatters = formatters or {}
mixed_case_fields = ['serverId']
removed_fields = []
rows = []
for o in objs:
row = []
for field in fields:
if field in removed_fields:
continue
if field in formatters:
row.append(formatters[field](o))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
if type(o) == dict and field in o:
data = o[field]
else:
if not hasattr(o, field_name) and exclude_unavailable:
removed_fields.append(field)
continue
else:
data = getattr(o, field_name, '')
if data is None:
data = '-'
if isinstance(data, six.string_types) and "\r" in data:
data = data.replace("\r", " ")
row.append(data)
rows.append(row)
for f in removed_fields:
fields.remove(f)
pt = prettytable.PrettyTable((f for f in fields), caching=False)
pt.aligns = ['l' for f in fields]
for row in rows:
pt.add_row(row)
if sortby_index is None:
order_by = None
else:
order_by = fields[sortby_index]
_print(pt, order_by)
def print_dict(d, property="Property"):
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
pt.aligns = ['l', 'l']
for r in six.iteritems(d):
r = list(r)
if isinstance(r[1], six.string_types) and "\r" in r[1]:
r[1] = r[1].replace("\r", " ")
pt.add_row(r)
_print(pt, property)
def find_resource(manager, name_or_id, *args, **kwargs):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id), *args, **kwargs)
except exceptions.NotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id, *args, **kwargs)
except (ValueError, exceptions.NotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exceptions.NotFound:
msg = "No %s with a name or ID of '%s' exists." % \
(manager.resource_class.__name__.lower(), name_or_id)
raise exceptions.CommandError(msg)

View File

@@ -1,34 +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.
"""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

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

@@ -1,221 +0,0 @@
# 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
"""

View File

@@ -1,523 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
from oslo_utils import uuidutils
import six
from six.moves.urllib import parse
from smaugclient.i18n import _
from smaugclient.openstack.common.apiclient import exceptions
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param args: args to be passed to every hook function
:param kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in six.iteritems(kwargs.copy()):
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(404, msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
self._init_completion_cache()
def _init_completion_cache(self):
cache_write = getattr(self.manager, 'write_to_completion_cache', None)
if not cache_write:
return
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
# enter an infinite loop of __getattr__ -> get -> __init__ ->
# __getattr__ -> ...
if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id):
cache_write('uuid', self.id)
if self.human_id:
cache_write('human_id', self.human_id)
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
return strutils.to_slug(name)
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

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

View File

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

View File

@@ -1,177 +0,0 @@
# 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,
})

View File

@@ -1,458 +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.
"""
Command-line interface to the Smaug Project.
"""
from __future__ import print_function
import argparse
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 oslo_log import handlers
from oslo_log import log as logging
from oslo_utils import encodeutils
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
logger = logging.getLogger(__name__)
class SmaugShell(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)
identity.Password.register_argparse_arguments(parser)
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='smaug',
description=__doc__.strip(),
epilog='See "smaug help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=HelpFormatter,
)
# Global arguments
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS, )
parser.add_argument('--version',
action='version',
version=smaugclient.__version__,
help="Show program's version number and exit.")
parser.add_argument('-d', '--debug',
default=bool(utils.env('SMAUGCLIENT_DEBUG')),
action='store_true',
help='Defaults to env[SMAUGCLIENT_DEBUG].')
parser.add_argument('-v', '--verbose',
default=False, action="store_true",
help="Print more verbose output.")
# os-cert, os-key, insecure, ca-file are all added
# by keystone session register_cli_opts later
parser.add_argument('--cert-file',
dest='os_cert',
help='DEPRECATED! Use --os-cert.')
parser.add_argument('--key-file',
dest='os_key',
help='DEPRECATED! Use --os-key.')
parser.add_argument('--ca-file',
dest='os_cacert',
help='DEPRECATED! Use --os-cacert.')
parser.add_argument('--api-timeout',
help='Number of seconds to wait for an '
'API response, '
'defaults to system socket timeout.')
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',
default=utils.env('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME].')
parser.add_argument('--os-region-name',
default=utils.env('OS_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
parser.add_argument('--os-auth-token',
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN].')
parser.add_argument('--os-no-client-auth',
default=utils.env('OS_NO_CLIENT_AUTH'),
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('--smaug-api-version',
default=utils.env(
'SMAUG_API_VERSION', default='1'),
help='Defaults to env[SMAUG_API_VERSION] '
'or 1.')
parser.add_argument('--os-service-type',
default=utils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE].')
parser.add_argument('--os-endpoint-type',
default=utils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE].')
parser.add_argument('--include-password',
default=bool(utils.env('SMAUG_INCLUDE_PASSWORD')),
action='store_true',
help='Send os-username and os-password to smaug.')
self._append_global_identity_args(parser)
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = utils.import_versioned_module(version, 'shell')
self._find_actions(subparsers, submodule)
self._find_actions(subparsers, self)
self._add_bash_completion_subparser(subparsers)
return parser
def _add_bash_completion_subparser(self, subparsers):
subparser = subparsers.add_parser(
'bash_completion',
add_help=False,
formatter_class=HelpFormatter
)
self.subcommands['bash_completion'] = subparser
subparser.set_defaults(func=self.do_bash_completion)
def _find_actions(self, subparsers, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
subparser = subparsers.add_parser(command, help=help,
description=desc,
add_help=False,
formatter_class=HelpFormatter)
subparser.add_argument('-h', '--help', action='help',
help=argparse.SUPPRESS)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def _discover_auth_versions(self, session, auth_url):
# discover the API versions the server is supporting base on the
# given URL
v2_auth_url = None
v3_auth_url = None
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except ks_exc.ClientException as e:
# Identity service may not support discover API version.
# Lets trying to figure out the API version from the original URL.
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
v3_auth_url = auth_url
elif path.startswith('/v2'):
v2_auth_url = auth_url
else:
# not enough information to determine the auth version
msg = ('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url. Identity service may not support API '
'version discovery. Please provide a versioned '
'auth_url instead. error=%s') % (e)
raise exc.CommandError(msg)
return (v2_auth_url, v3_auth_url)
def _get_keystone_auth(self, session, auth_url, **kwargs):
auth_token = kwargs.pop('auth_token', None)
if auth_token:
return token.Token(
auth_url,
auth_token,
project_id=kwargs.pop('project_id'),
project_name=kwargs.pop('project_name'),
project_domain_id=kwargs.pop('project_domain_id'),
project_domain_name=kwargs.pop('project_domain_name'))
# NOTE(starodubcevna): this is a workaround for the bug:
# https://bugs.launchpad.net/python-openstackclient/+bug/1447704
# Change that fix this error in keystoneclient was abandoned,
# so we should use workaround until we move to keystoneauth.
# The idea of the code came from glanceclient.
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
session=session,
auth_url=auth_url)
if v3_auth_url:
# NOTE(starodubcevna): set user_domain_id and project_domain_id
# to default as it done in other projects.
return password.Password(auth_url,
username=kwargs.pop('username'),
user_id=kwargs.pop('user_id'),
password=kwargs.pop('password'),
user_domain_id=kwargs.pop(
'user_domain_id') or 'default',
user_domain_name=kwargs.pop(
'user_domain_name'),
project_id=kwargs.pop('project_id'),
project_name=kwargs.pop('project_name'),
project_domain_id=kwargs.pop(
'project_domain_id') or 'default')
elif v2_auth_url:
return password.Password(auth_url,
username=kwargs.pop('username'),
user_id=kwargs.pop('user_id'),
password=kwargs.pop('password'),
project_id=kwargs.pop('project_id'),
project_name=kwargs.pop('project_name'))
else:
# if we get here it means domain information is provided
# (caller meant to use Keystone V3) but the auth url is
# actually Keystone V2. Obviously we can't authenticate a V3
# user using V2.
exc.CommandError("Credential and auth_url mismatch. The given "
"auth_url is using Keystone V2 endpoint, which "
"may not able to handle Keystone V3 credentials. "
"Please provide a correct Keystone V3 auth_url.")
def _setup_logging(self, debug):
# Output the logs to command-line interface
color_handler = handlers.ColorHandler(sys.stdout)
logger_root = logging.getLogger(None).logger
logger_root.level = logging.DEBUG if debug else logging.WARNING
logger_root.addHandler(color_handler)
# Set the logger level of special library
logging.getLogger('iso8601') \
.logger.setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool') \
.logger.setLevel(logging.WARNING)
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(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)
self.parser = subcommand_parser
keystone_session = None
keystone_auth = None
# Handle top-level --help/-h before attempting to parse
# a command off the command line.
if (not args and options.help) or not argv:
self.do_help(options)
return 0
# Parse args again and call whatever callback was selected.
args = subcommand_parser.parse_args(argv)
# Short-circuit and deal with help command right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0
if not args.os_username and not args.os_auth_token:
raise exc.CommandError("You must provide a username via"
" either --os-username or env[OS_USERNAME]"
" or a token via --os-auth-token or"
" env[OS_AUTH_TOKEN]")
if args.os_no_client_auth:
if not args.smaug_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]")
else:
# Tenant name or ID is needed to make keystoneclient retrieve a
# service catalog, it's not required if os_no_client_auth is
# specified, neither is the auth URL.
if not any([args.os_tenant_name, args.os_tenant_id,
args.os_project_id, args.os_project_name]):
raise exc.CommandError("You must provide a project name or"
" project id via --os-project-name,"
" --os-project-id, env[OS_PROJECT_ID]"
" or env[OS_PROJECT_NAME]. You may"
" use os-project and os-tenant"
" interchangeably.")
if not args.os_auth_url:
raise exc.CommandError("You must provide an auth url via"
" either --os-auth-url or via"
" env[OS_AUTH_URL]")
endpoint = args.smaug_url
if args.os_no_client_auth:
# Authenticate through smaug, don't use session
kwargs = {
'username': args.os_username,
'password': args.os_password,
'auth_token': args.os_auth_token,
'auth_url': args.os_auth_url,
'token': args.os_auth_token,
'insecure': args.insecure,
'timeout': args.api_timeout
}
if args.os_region_name:
kwargs['region_name'] = args.os_region_name
else:
# Create a keystone session and keystone auth
keystone_session = ksession.Session.load_from_cli_options(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,
args.os_auth_url,
username=args.os_username,
user_id=args.os_user_id,
user_domain_id=args.os_user_domain_id,
user_domain_name=args.os_user_domain_name,
password=args.os_password,
auth_token=args.os_auth_token,
project_id=project_id,
project_name=project_name,
project_domain_id=args.os_project_domain_id,
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'
if not endpoint:
endpoint = keystone_auth.get_endpoint(
keystone_session,
service_type=service_type,
region_name=args.os_region_name)
kwargs = {
'session': keystone_session,
'auth': keystone_auth,
'service_type': service_type,
'endpoint_type': endpoint_type,
'region_name': args.os_region_name,
}
if args.api_timeout:
kwargs['timeout'] = args.api_timeout
client = smaug_client.Client(api_version, endpoint, **kwargs)
args.func(client, args)
def do_bash_completion(self, args):
"""Prints all of the commands and options to stdout."""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
commands.add(sc_str)
for option in list(sc._optionals._option_string_actions):
options.add(option)
commands.remove('bash-completion')
commands.remove('bash_completion')
print(' '.join(commands | options))
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""Display help about this program or one of its subcommands.
"""
if getattr(args, 'command', None):
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
msg = "'%s' is not a valid subcommand"
raise exc.CommandError(msg % args.command)
else:
self.parser.print_help()
class HelpFormatter(argparse.HelpFormatter):
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(HelpFormatter, self).start_section(heading)
def main(args=sys.argv[1:]):
try:
SmaugShell().main(args)
except KeyboardInterrupt:
print('... terminating smaug client', file=sys.stderr)
sys.exit(1)
except Exception as e:
if '--debug' in args or '-d' in args:
raise
else:
print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,60 +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 os
import fixtures
import requests
import testtools
class TestCaseShell(testtools.TestCase):
TEST_REQUEST_BASE = {
'verify': True,
}
def setUp(self):
super(TestCaseShell, self).setUp()
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
os.environ.get('OS_STDERR_CAPTURE') == '1'):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class TestResponse(requests.Response):
"""Class used to wrap requests.Response and provide some
convenience to initialize with a dict.
"""
def __init__(self, data):
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.reason = data.get('reason', '')
# Fake the text attribute to streamline Response creation
self._text = data.get('text', None)
else:
self.status_code = data
def __eq__(self, other):
return self.__dict__ == other.__dict__
@property
def text(self):
return self._text

View File

@@ -1,135 +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.
from oslo_serialization import jsonutils
class FakeHTTPResponse(object):
version = 1.1
def __init__(self, status_code, reason, headers, content):
self.headers = headers
self.content = content
self.status_code = status_code
self.reason = reason
self.raw = FakeRaw()
def getheader(self, name, default=None):
return self.headers.get(name, default)
def getheaders(self):
return self.headers.items()
def read(self, amt=None):
b = self.content
self.content = None
return b
def iter_content(self, chunksize):
return self.content
def json(self):
return jsonutils.loads(self.content)
class FakeRaw(object):
version = 110
class FakeClient(object):
def _dict_match(self, partial, real):
result = True
try:
for key, value in partial.items():
if type(value) is dict:
result = self._dict_match(value, real[key])
else:
assert real[key] == value
result = True
except (AssertionError, KeyError):
result = False
return result
def assert_called(self, method, url, body=None,
partial_body=None, pos=-1, **kwargs):
"""Assert than an API method was just called.
"""
expected = (method, url)
called = self.client.callstack[pos][0:2]
assert self.client.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:
assert self.client.callstack[pos][2] == body
if partial_body is not None:
try:
assert self._dict_match(partial_body,
self.client.callstack[pos][2])
except AssertionError:
print(self.client.callstack[pos][2])
print("does not contain")
print(partial_body)
raise
def assert_called_anytime(self, method, url, body=None, partial_body=None):
"""Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.client.callstack, ("Expected %s %s but no calls "
"were made." % expected)
found = False
for entry in self.client.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % (
expected + (self.client.callstack, ))
if body is not None:
try:
assert entry[2] == body
except AssertionError:
print(entry[2])
print("!=")
print(body)
raise
if partial_body is not None:
try:
assert self._dict_match(partial_body, entry[2])
except AssertionError:
print(entry[2])
print("does not contain")
print(partial_body)
raise
def clear_callstack(self):
self.client.callstack = []
def authenticate(self):
pass

View File

@@ -1,451 +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 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
@mock.patch('smaugclient.common.http.requests.request')
class HttpClientTest(testtools.TestCase):
# Patch os.environ to avoid required auth info.
def setUp(self):
super(HttpClientTest, self).setUp()
def test_http_raw_request(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
client = http.HTTPClient('http://example.com:8082')
resp = client.raw_request('GET', '')
self.assertEqual(200, resp.status_code)
self.assertEqual('', ''.join([x for x in resp.content]))
mock_request.assert_called_with('GET', 'http://example.com:8082',
allow_redirects=False,
headers=headers)
def test_token_or_credentials(self, mock_request):
# Record a 200
fake200 = fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
mock_request.side_effect = [fake200, fake200, fake200]
# Replay, create client, assert
client = http.HTTPClient('http://example.com:8082')
resp = client.raw_request('GET', '')
self.assertEqual(200, resp.status_code)
client.username = 'user'
client.password = 'pass'
resp = client.raw_request('GET', '')
self.assertEqual(200, resp.status_code)
client.auth_token = 'abcd1234'
resp = client.raw_request('GET', '')
self.assertEqual(200, resp.status_code)
# no token or credentials
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-smaugclient'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-smaugclient',
'X-Auth-Key': 'pass',
'X-Auth-User': 'user'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-smaugclient',
'X-Auth-Token': 'abcd1234'})
])
def test_region_name(self, mock_request):
# Record a 200
fake200 = fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
mock_request.return_value = fake200
client = http.HTTPClient('http://example.com:8082')
client.region_name = 'RegionOne'
resp = client.raw_request('GET', '')
self.assertEqual(200, resp.status_code)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'X-Region-Name': 'RegionOne',
'User-Agent': 'python-smaugclient'})
def test_http_json_request(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('GET', '')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_json_request_argument_passed_to_requests(self, mock_request):
"""Check that we have sent the proper arguments to requests."""
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
client.verify_cert = True
client.cert_file = 'RANDOM_CERT_FILE'
client.key_file = 'RANDOM_KEY_FILE'
client.auth_url = 'http://AUTH_URL'
resp, body = client.json_request('GET', '', data='text')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
verify=True,
data='"text"',
headers={'Content-Type': 'application/json',
'X-Auth-Url': 'http://AUTH_URL',
'User-Agent': 'python-smaugclient'})
def test_http_json_request_w_req_body(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('GET', '', data='test-body')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
data='"test-body"',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'not/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('GET', '', data='test-data')
self.assertEqual(200, resp.status_code)
self.assertIsNone(body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082', data='"test-data"',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_json_request_invalid_json(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'invalid-json')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('GET', '')
self.assertEqual(200, resp.status_code)
self.assertEqual('invalid-json', body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_manual_redirect_delete(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082/foo')
resp, body = client.json_request('DELETE', '')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
mock.call('DELETE', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
])
def test_http_manual_redirect_post(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082/foo')
resp, body = client.json_request('POST', '')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('POST', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
mock.call('POST', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
])
def test_http_manual_redirect_put(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082/foo')
resp, body = client.json_request('PUT', '')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('PUT', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
mock.call('PUT', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
])
def test_http_manual_redirect_prohibited(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/'},
'')
client = http.HTTPClient('http://example.com:8082/foo')
self.assertRaises(exc.EndpointException,
client.json_request, 'DELETE', '')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_manual_redirect_error_without_location(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
302, 'Found',
{},
'')
client = http.HTTPClient('http://example.com:8082/foo')
self.assertRaises(exc.EndpointException,
client.json_request, 'DELETE', '')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_json_request_redirect(self, mock_request):
# Record the 302
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('GET', '')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
])
def test_http_404_json_request(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
404, 'Not Found', {'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
e = self.assertRaises(exc.HTTPClientError,
client.json_request, 'GET', '')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 404
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_http_300_json_request(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
300, 'OK', {'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
e = self.assertRaises(
exc.MultipleChoices, client.json_request, 'GET', '')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 300
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'})
def test_fake_json_request(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('fake://example.com:8082')
self.assertRaises(exc.EndpointException,
client._http_request, "/", "GET")
mock_request.assert_called_once_with('GET', 'fake://example.com:8082/',
allow_redirects=False,
headers=headers)
def test_http_request_socket_error(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('http://example.com:8082')
self.assertRaises(exc.EndpointException,
client._http_request, "/", "GET")
mock_request.assert_called_once_with('GET', 'http://example.com:8082/',
allow_redirects=False,
headers=headers)
def test_http_request_socket_timeout(self, mock_request):
headers = {'User-Agent': 'python-smaugclient'}
mock_request.side_effect = [socket.timeout]
client = http.HTTPClient('http://example.com:8082')
self.assertRaises(exc.ConnectionRefused,
client._http_request, "/", "GET")
mock_request.assert_called_once_with('GET', 'http://example.com:8082/',
allow_redirects=False,
headers=headers)
def test_http_request_specify_timeout(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082', timeout='123')
resp, body = client.json_request('GET', '')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-smaugclient'},
timeout=float(123))
def test_get_system_ca_file(self, mock_request):
chosen = '/etc/ssl/certs/ca-certificates.crt'
with mock.patch('os.path.exists') as mock_os:
mock_os.return_value = chosen
ca = http.get_system_ca_file()
self.assertEqual(chosen, ca)
mock_os.assert_called_once_with(chosen)
def test_insecure_verify_cert_None(self, mock_request):
client = http.HTTPClient('https://foo', insecure=True)
self.assertFalse(client.verify_cert)
def test_passed_cert_to_verify_cert(self, mock_request):
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:
gsf.return_value = "SOMEWHERE"
client = http.HTTPClient('https://foo')
self.assertEqual("SOMEWHERE", client.verify_cert)

View File

@@ -1,207 +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 logging
import re
import sys
import fixtures
from keystoneclient import fixture
from keystoneclient.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
FAKE_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where/v2.0'}
FAKE_ENV2 = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_ID': 'tenant_id',
'OS_AUTH_URL': 'http://no.where/v2.0'}
FAKE_ENV_v3 = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_ID': 'tenant_id',
'OS_USER_DOMAIN_NAME': 'domain_name',
'OS_AUTH_URL': 'http://no.where/v3'}
def _create_ver_list(versions):
return {'versions': {'values': versions}}
class TestArgs(object):
package_version = ''
smaug_repo_url = 'http://127.0.0.1'
exists_action = ''
is_public = False
categories = []
class ShellTest(base.TestCaseShell):
def make_env(self, exclude=None, fake_env=FAKE_ENV):
env = dict((k, v) for k, v in fake_env.items() if k != exclude)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
class ShellCommandTest(ShellTest):
_msg_no_tenant_project = ('You must provide a project name or project'
' id via --os-project-name, --os-project-id,'
' env[OS_PROJECT_ID] or env[OS_PROJECT_NAME].'
' You may use os-project and os-tenant'
' interchangeably.',)
def setUp(self):
super(ShellCommandTest, self).setUp()
def get_auth_endpoint(bound_self, args):
return ('test', {})
self.useFixture(fixtures.MonkeyPatch(
'smaugclient.shell.SmaugShell._get_endpoint_and_kwargs',
get_auth_endpoint))
self.client = mock.MagicMock()
# To prevent log descriptors from being closed during
# shell tests set a custom StreamHandler
self.logger = log.getLogger(None).logger
self.logger.level = logging.DEBUG
self.color_handler = handlers.ColorHandler(sys.stdout)
self.logger.addHandler(self.color_handler)
def tearDown(self):
super(ShellTest, self).tearDown()
self.logger.removeHandler(self.color_handler)
def shell(self, argstr, exitcodes=(0,)):
orig = sys.stdout
orig_stderr = sys.stderr
try:
sys.stdout = six.StringIO()
sys.stderr = six.StringIO()
_shell = smaugclient.shell.SmaugShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertIn(exc_value.code, exitcodes)
finally:
stdout = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
stderr = sys.stderr.getvalue()
sys.stderr.close()
sys.stderr = orig_stderr
return (stdout, stderr)
def register_keystone_discovery_fixture(self, mreq):
v2_url = "http://no.where/v2.0"
v2_version = fixture.V2Discovery(v2_url)
mreq.register_uri('GET', v2_url, json=_create_ver_list([v2_version]),
status_code=200)
def register_keystone_token_fixture(self, mreq):
v2_token = ks_v2_fixture.Token(token_id='token')
service = v2_token.add_service('application-catalog')
service.add_endpoint('http://no.where', region='RegionOne')
mreq.register_uri('POST',
'http://no.where/v2.0/tokens',
json=v2_token,
status_code=200)
def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
def test_help(self):
required = [
'.*?^usage: smaug',
'.*?^\s+plan-create\s+Create a plan.',
'.*?^See "smaug help COMMAND" for help on a specific command',
]
stdout, stderr = self.shell('help')
for r in required:
self.assertThat((stdout + stderr),
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_help_on_subcommand(self):
required = [
'.*?^usage: smaug plan-create',
'.*?^Create a plan.',
]
stdout, stderr = self.shell('help plan-create')
for r in required:
self.assertThat((stdout + stderr),
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
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',
]
stdout, stderr = self.shell('')
for r in required:
self.assertThat((stdout + stderr),
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_no_username(self):
required = ('You must provide a username via either --os-username or '
'env[OS_USERNAME] or a token via --os-auth-token or '
'env[OS_AUTH_TOKEN]',)
self.make_env(exclude='OS_USERNAME')
try:
self.shell('plan-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_no_tenant_name(self):
required = self._msg_no_tenant_project
self.make_env(exclude='OS_TENANT_NAME')
try:
self.shell('plan-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_no_tenant_id(self):
required = self._msg_no_tenant_project
self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2)
try:
self.shell('plan-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_no_auth_url(self):
required = ('You must provide an auth url'
' via either --os-auth-url or via env[OS_AUTH_URL]',)
self.make_env(exclude='OS_AUTH_URL')
try:
self.shell('plan-list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')

View File

@@ -1,23 +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.
"""
Tests for `smaugclient` module.
"""
from smaugclient.tests.unit import base
class TestSmaugclient(base.TestCaseShell):
def test_something(self):
pass

View File

@@ -1,94 +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.
try:
import urlparse
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
REQUEST_ID = 'req-test-request-id'
PROJECT_ID = 'efc6a88b-9096-4bb6-8634-cda182a6e12a'
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
kwargs = {
'token': 'token',
'auth': 'auth_url',
'service_type': 'service_type',
'endpoint_type': 'endpoint_type',
'region_name': 'region_name',
'project_id': PROJECT_ID,
}
client.Client.__init__(self, 'http://endpoint', **kwargs)
self.client = FakeHTTPClient(**kwargs)
class FakeHTTPClient(base_client.HTTPClient):
def __init__(self, **kwargs):
self.username = 'username'
self.password = 'password'
self.auth_url = 'auth_url'
self.callstack = []
self.management_url = 'http://10.0.2.15:8776/v1/fake'
self.osapi_max_limit = 1000
self.marker = None
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert 'body' not in kwargs
elif method == 'PUT':
assert 'body' in kwargs
# Call the method
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
kwargs.update(args)
url_split = url.rsplit('?', 1)
munged_url = url_split[0]
if len(url_split) > 1:
parameters = url_split[1]
if 'marker' in parameters:
self.marker = int(parameters.rsplit('marker=', 1)[1])
else:
self.marker = None
else:
self.marker = None
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))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
status, headers, body = getattr(self, callback)(**kwargs)
# add fake request-id header
headers['x-openstack-request-id'] = REQUEST_ID
r = base.TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})
return r, body

View File

@@ -1,98 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'checkpoint': {}})
FAKE_PROVIDER_ID = "2220f8b1-975d-4621-a872-fa9afb43cb6c"
FAKE_PLAN_ID = "3330f8b1-975d-4621-a872-fa9afb43cb6c"
class CheckpointsTest(base.TestCaseShell):
@mock.patch('smaugclient.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,
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.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,
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.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,
provider_id=FAKE_PROVIDER_ID), headers={})
@mock.patch('smaugclient.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}/'
'checkpoints?limit=2&marker=1234'.format(
provider_id=FAKE_PROVIDER_ID,
project_id=fakes.PROJECT_ID), headers={})
@mock.patch('smaugclient.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}/'
'checkpoints?sort_dir=asc&sort_key=id'.format(
provider_id=FAKE_PROVIDER_ID,
project_id=fakes.PROJECT_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')
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}/'
'checkpoints'.format(
provider_id=FAKE_PROVIDER_ID,
project_id=fakes.PROJECT_ID),
data={
'checkpoint': {'plan_id': FAKE_PLAN_ID}},
headers={})

View File

@@ -1,95 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'plan': {'name': 'fake_name'}})
class PlansTest(base.TestCaseShell):
@mock.patch('smaugclient.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(
project_id=fakes.PROJECT_ID), headers={})
@mock.patch('smaugclient.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?'
'sort_dir=asc&sort_key=id'.format(
project_id=fakes.PROJECT_ID), headers={})
@mock.patch('smaugclient.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')
def test_create_plan(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.create('Plan name', 'provider_id', '')
mock_request.assert_called_with(
'POST',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/plans',
data={
'plan': {'provider_id': 'provider_id',
'name': 'Plan name',
'resources': ''}},
headers={})
@mock.patch('smaugclient.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',
headers={})
@mock.patch('smaugclient.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',
data={'plan': {'name': 'Test name.'}}, headers={})
@mock.patch('smaugclient.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',
headers={})
@mock.patch('smaugclient.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',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -1,77 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'protectable_type': {}})
mock_instances_request_return = ({}, {'instances': {}})
class ProtectablesTest(base.TestCaseShell):
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
def test_list_protectables_instances_with_invalid_sort_key(self):
self.assertRaises(ValueError,
cs.protectables.list_instances, 'OS::Cinder::Volume',
sort_key='invalid', sort_dir='asc')

View File

@@ -1,65 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'provider': {}})
class ProvidersTest(base.TestCaseShell):
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
def test_list_providers_with_invalid_sort_key(self):
self.assertRaises(ValueError,
cs.providers.list,
sort_key='invalid', sort_dir='asc')

View File

@@ -1,83 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'restore': {}})
class RestoresTest(base.TestCaseShell):
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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')
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"}')
mock_request.assert_called_with(
'POST',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/restores',
data={
'restore':
{
'checkpoint_id': '2220f8b1-975d-4621-a872-fa9afb43cb6c',
'parameters': '{"username": "admin"}',
'provider_id': '586cc6ce-e286-40bd-b2b5-dd32694d9944',
'restore_target': '192.168.1.2:35357/v2.0'
}}, headers={})
@mock.patch('smaugclient.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',
headers={})
@mock.patch('smaugclient.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',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -1,93 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'scheduled_operation': {'name': 'fake_name'}})
class ScheduledOperationsTest(base.TestCaseShell):
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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')
def test_create_scheduled_operation(self, mock_request):
mock_request.return_value = mock_request_return
cs.scheduled_operations.create(
'name', 'operation_type',
'efc6a88b-9096-4bb6-8634-cda182a6e12a',
'operation_definition')
mock_request.assert_called_with(
'POST',
'/v1/efc6a88b-9096-4bb6-8634-cda182a6e12a/scheduled_operations',
data={
'scheduled_operation': {
'name': 'name',
'operation_type': 'operation_type',
'trigger_id': 'efc6a88b-9096-4bb6-8634-cda182a6e12a',
'operation_definition': 'operation_definition'}},
headers={})
@mock.patch('smaugclient.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',
headers={})
@mock.patch('smaugclient.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',
headers={})
@mock.patch('smaugclient.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',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -1,86 +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 mock
from smaugclient.tests.unit import base
from smaugclient.tests.unit.v1 import fakes
cs = fakes.FakeClient()
mock_request_return = ({}, {'trigger_info': {'name': 'fake_name'}})
class TriggersTest(base.TestCaseShell):
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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={})
@mock.patch('smaugclient.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')
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',
data={
'trigger_info': {'name': 'name',
'type': 'time',
'properties': 'properties'}},
headers={})
@mock.patch('smaugclient.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',
headers={})
@mock.patch('smaugclient.common.http.HTTPClient.json_request')
def test_show_plan(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',
headers={})
@mock.patch('smaugclient.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',
headers={'X-Configuration-Session': 'fake_session_id'})

View File

@@ -1,118 +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.
from six.moves.urllib import parse
from smaugclient.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)
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 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,
checkpoint_id=checkpoint_id)
return self._delete(path)
def get(self, provider_id, checkpoint_id, session_id=None):
if session_id:
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,
checkpoint_id=checkpoint_id)
return self._get(url, response_key="checkpoint", headers=headers)
def list(self, provider_id=None, search_opts=None, marker=None,
limit=None, sort_key=None, sort_dir=None, sort=None):
"""Lists all checkpoints.
:param provider_id:
:param search_opts: Search options to filter out checkpoints.
:param marker: Begin returning checkpoints that appear later in the
checkpoints list.
:param limit: Maximum number of checkpoints to return.
:param sort_key: Key to be sorted; deprecated in kilo
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`checkpoint`
"""
url = self._build_checkpoints_list_url(
provider_id,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'checkpoints')
def _build_checkpoints_list_url(self, provider_id,
search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
if search_opts is None:
search_opts = {}
query_params = {}
for key, val in search_opts.items():
if val:
query_params[key] = val
if marker:
query_params['marker'] = marker
if limit:
query_params['limit'] = limit
if sort:
query_params['sort'] = self._format_sort_param(sort)
else:
# sort_key and sort_dir deprecated in kilo, prefer sort
if sort_key:
query_params['sort_key'] = self._format_sort_key_param(
sort_key)
if sort_dir:
query_params['sort_dir'] = self._format_sort_dir_param(
sort_dir)
# Transform the dict to a sequence of two-element tuples in fixed
# order, then the encoded string will be consistent in Python 2&3.
query_string = ""
if query_params:
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"
"/checkpoints%(query_string)s" %
{"project_id": self.project_id,
"provider_id": provider_id,
"query_string": query_string})

View File

@@ -1,44 +0,0 @@
# Copyright (c) 2013 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.
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
class Client(object):
"""Client for the Smaug v1 API.
:param string endpoint: A user-supplied endpoint URL for the service.
:param string token: Token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
"""
def __init__(self, *args, **kwargs):
"""Initialize a new client for the Smaug 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)
self.protectables = protectables.ProtectableManager(self.http_client)
self.providers = providers.ProviderManager(self.http_client)
self.checkpoints = checkpoints.CheckpointManager(self.http_client)
self.triggers = triggers.TriggerManager(self.http_client)
self.scheduled_operations = \
scheduled_operations.ScheduledOperationManager(self.http_client)

View File

@@ -1,83 +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.
from smaugclient.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):
body = {'plan': {'name': name,
'provider_id': provider_id,
'resources': resources,
}}
url = "/v1/{project_id}/plans" .format(
project_id=self.project_id)
return self._create(url, body, 'plan', return_raw=True)
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),
body, "plan")
def delete(self, plan_id):
path = '/v1/{project_id}/plans/{plan_id}'.format(
project_id=self.project_id,
plan_id=plan_id)
return self._delete(path)
def get(self, plan_id, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/plans/{plan_id}".format(
project_id=self.project_id,
plan_id=plan_id)
return self._get(url, response_key="plan", headers=headers)
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all plans.
:param detailed: Whether to return detailed volume info.
:param search_opts: Search options to filter out volumes.
:param marker: Begin returning volumes that appear later in the volume
list than that represented by this volume id.
:param limit: Maximum number of volumes to return.
:param sort_key: Key to be sorted; deprecated in kilo
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`Plan`
"""
resource_type = "plans"
url = self._build_list_url(
resource_type, detailed=detailed,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'plans')

View File

@@ -1,115 +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.
from six.moves.urllib import parse
from smaugclient.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 ProtectableManager(base.ManagerWithFind):
resource_class = Protectable
def get(self, protectable_type, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/protectables/{protectable_type}".format(
project_id=self.project_id,
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)
protectables = self._list(url, 'protectable_type', return_raw=True)
protectables_list = []
for protectable in protectables:
protectable_dict = {}
protectable_dict['protectable_type'] = protectable
protectables_list.append(Protectable(self, protectable_dict))
return protectables_list
def list_instances(self, protectable_type, search_opts=None, marker=None,
limit=None, sort_key=None, sort_dir=None, sort=None):
"""Lists all instances.
:param protectable_type:
:param search_opts: Search options to filter out instances.
:param marker: Begin returning volumes that appear later in the
instances list.
:param limit: Maximum number of instances to return.
:param sort_key: Key to be sorted; deprecated in kilo
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`Protectable`
"""
url = self._build_instances_list_url(
protectable_type,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'instances')
def _build_instances_list_url(self, protectable_type,
search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
if search_opts is None:
search_opts = {}
query_params = {}
for key, val in search_opts.items():
if val:
query_params[key] = val
if marker:
query_params['marker'] = marker
if limit:
query_params['limit'] = limit
if sort:
query_params['sort'] = self._format_sort_param(sort)
else:
# sort_key and sort_dir deprecated in kilo, prefer sort
if sort_key:
query_params['sort_key'] = self._format_sort_key_param(
sort_key)
if sort_dir:
query_params['sort_dir'] = self._format_sort_dir_param(
sort_dir)
# Transform the dict to a sequence of two-element tuples in fixed
# order, then the encoded string will be consistent in Python 2&3.
query_string = ""
if query_params:
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"
"/instances%(query_string)s" %
{"project_id": self.project_id,
"protectable_type": protectable_type,
"query_string": query_string})

View File

@@ -1,59 +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.
from smaugclient.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
def get(self, provider_id, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/providers/{provider_id}".format(
project_id=self.project_id,
provider_id=provider_id)
return self._get(url, response_key="provider", headers=headers)
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all providers.
:param detailed: Whether to return detailed provider info.
:param search_opts: Search options to filter out provider.
:param marker: Begin returning volumes that appear later in the
provider list than that represented by this provider id.
:param limit: Maximum number of providers to return.
:param sort_key: Key to be sorted; deprecated in kilo
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`Provider`
"""
resource_type = "providers"
url = self._build_list_url(
resource_type, detailed=detailed,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'providers')

View File

@@ -1,76 +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.
from smaugclient.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 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,
restore_id=restore_id)
return self._get(url, response_key="restore", headers=headers)
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all restores.
:param detailed: Whether to return detailed restore info.
:param search_opts: Search options to filter out restores.
:param marker: Begin returning volumes that appear later in the restore
list than that represented by this volume id.
:param limit: Maximum number of restores to return.
:param sort_key: Key to be sorted; deprecated in kilo
:param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated
in kilo
:param sort: Sort information
:rtype: list of :class:`Restore`
"""
resource_type = "restores"
url = self._build_list_url(
resource_type, detailed=detailed,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'restores')

View File

@@ -1,65 +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.
from smaugclient.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
def create(self, name, operation_type, trigger_id, operation_definition):
body = {'scheduled_operation': {'name': name,
'operation_type': operation_type,
'trigger_id': trigger_id,
'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)
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)
return self._delete(path)
def get(self, scheduled_operation_id, session_id=None):
if session_id:
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)
return self._get(url, response_key="scheduled_operation",
headers=headers)
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all scheduled_operations."""
resource_type = "scheduled_operations"
url = self._build_list_url(
resource_type, detailed=detailed,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'operations')

View File

@@ -1,889 +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 argparse
import os
from oslo_utils import uuidutils
from smaugclient.common import base
from smaugclient.common import utils
from smaugclient.openstack.common.apiclient import exceptions
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help='Filters results by a name. Default=None.')
@utils.arg('--status',
metavar='<status>',
default=None,
help='Filters results by a status. 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. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
def do_plan_list(cs, args):
"""Lists all plans."""
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'status': args.status,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
plans = cs.plans.list(search_opts=search_opts, marker=args.marker,
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'Provider id', 'Status']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(plans, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('name',
metavar='<name>',
help='Plan name.')
@utils.arg('provider_id',
metavar='<provider_id>',
help='ID of provider.')
@utils.arg('resources',
metavar='<id=type,id=type>',
help='Resource in list must be a dict when creating'
' a plan.The keys of resource are id and type.')
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)
@utils.arg('plan',
metavar='<plan>',
help='ID of plan.')
def do_plan_show(cs, args):
"""Shows plan details."""
plan = cs.plans.get(args.plan)
utils.print_dict(plan.to_dict())
@utils.arg('plan',
metavar='<plan>',
nargs="+",
help='ID of plan.')
def do_plan_delete(cs, args):
"""Delete plan."""
failure_count = 0
for plan_id in args.plan:
try:
plan = utils.find_resource(cs.plans, plan_id)
cs.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(args.plan):
raise exceptions.CommandError("Unable to find and delete any of the "
"specified plan.")
@utils.arg("plan_id", metavar="<PLAN ID>",
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>",
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."""
data = {}
if args.name is not None:
data['name'] = args.name
if args.resources is not None:
plan_resources = _extract_resources(args)
data['resources'] = plan_resources
if args.status is not None:
data['status'] = args.status
try:
plan = utils.find_resource(cs.plans, args.plan_id)
plan = cs.plans.update(plan.id, data)
except exceptions.NotFound:
raise exceptions.CommandError("Plan %s not found" % args.plan_id)
else:
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',
metavar='<restore_target>',
help='Restore target.')
@utils.arg('--parameters',
type=str,
nargs='*',
metavar='<key=value>',
default=None,
help='The parameters of a restore target.')
def do_restore_create(cs, args):
"""Create a restore."""
if not uuidutils.is_uuid_like(args.provider_id):
raise exceptions.CommandError(
"Invalid provider id provided.")
if not uuidutils.is_uuid_like(args.checkpoint_id):
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 = 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
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--status',
metavar='<status>',
default=None,
help='Filters results by a status. 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. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
def do_restore_list(cs, args):
"""Lists all restores."""
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'status': args.status,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
restores = cs.restores.list(search_opts=search_opts, marker=args.marker,
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Project id', 'Provider id', 'Checkpoint id',
'Restore target', 'Parameters', 'Status']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(restores, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('restore',
metavar='<restore>',
help='ID of restore.')
def do_restore_show(cs, args):
"""Shows restore details."""
restore = cs.restores.get(args.restore)
utils.print_dict(restore.to_dict())
def do_protectable_list(cs, args):
"""Lists all protectables type."""
protectables = cs.protectables.list()
key_list = ['Protectable type']
utils.print_list(protectables, key_list, exclude_unavailable=True)
@utils.arg('protectable_type',
metavar='<protectable_type>',
help='Protectable type.')
def do_protectable_show(cs, args):
"""Shows protectable type details."""
protectable = cs.protectables.get(args.protectable_type)
utils.print_dict(protectable.to_dict())
@utils.arg('protectable_type',
metavar='<protectable_type>',
help='Type of protectable.')
@utils.arg('--type',
metavar='<type>',
default=None,
help='Filters results by a status. 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. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
def do_protectable_list_instances(cs, args):
"""Lists all protectable instances."""
search_opts = {
'type': args.type,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
instances = cs.protectables.list_instances(
args.protectable_type, search_opts=search_opts,
marker=args.marker, limit=args.limit,
sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Type', 'Dependent resources']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(instances, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('provider_id',
metavar='<provider_id>',
help='Id of provider.')
def do_provider_show(cs, args):
"""Shows provider details."""
provider = cs.providers.get(args.provider_id)
utils.print_dict(provider.to_dict())
@utils.arg('--name',
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('--marker',
metavar='<marker>',
default=None,
help='Begin returning plans that appear later in the plan '
'list than that represented by this plan id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
def do_provider_list(cs, args):
"""Lists all providers."""
search_opts = {
'name': args.name,
'description': args.description,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
providers = cs.providers.list(search_opts=search_opts, marker=args.marker,
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'Description', 'Extended_info_schema']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(providers, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('provider_id',
metavar='<provider_id>',
help='ID of provider.')
@utils.arg('plan_id',
metavar='<plan_id>',
help='ID of plan.')
def do_checkpoint_create(cs, args):
"""Create a checkpoint."""
checkpoint = cs.checkpoints.create(args.provider_id, args.plan_id)
utils.print_dict(checkpoint)
@utils.arg('provider_id',
metavar='<provider_id>',
help='ID of provider.')
@utils.arg('--status',
metavar='<status>',
default=None,
help='Filters results by a status. Default=None.')
@utils.arg('--project_id',
metavar='<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. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
def do_checkpoint_list(cs, args):
"""Lists all checkpoints."""
search_opts = {
'status': args.status,
'project_id': args.project_id,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
checkpoints = cs.checkpoints.list(
provider_id=args.provider_id, search_opts=search_opts,
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']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(checkpoints, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('provider_id',
metavar='<provider_id>',
help='Id of provider.')
@utils.arg('checkpoint_id',
metavar='<checkpoint_id>',
help='Id of checkpoint.')
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())
@utils.arg('provider_id',
metavar='<provider_id>',
help='Id of provider.')
@utils.arg('checkpoint',
metavar='<checkpoint>',
nargs="+",
help='ID of checkpoint.')
def do_checkpoint_delete(cs, args):
"""Delete 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)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; checkpoint not found".
format(checkpoint_id))
if failure_count == len(args.checkpoint):
raise exceptions.CommandError("Unable to find and delete any of the "
"specified checkpoint.")
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help='Filters results by a name. Default=None.')
@utils.arg('--type',
metavar='<type>',
default=None,
help='Filters results by a type. Default=None.')
@utils.arg('--properties',
metavar='<properties>',
default=None,
help='Filters results by a properties. 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. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
def do_trigger_list(cs, args):
"""Lists all triggers."""
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'type': args.type,
'properties': args.properties,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
triggers = cs.triggers.list(search_opts=search_opts, marker=args.marker,
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'Type', 'Properties']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(triggers, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('name',
metavar='<name>',
help='Trigger name.')
@utils.arg('type',
metavar='<type>',
help='Type of trigger.')
@utils.arg('properties',
metavar='<key=value;key=value>',
help='Properties of trigger.')
def do_trigger_create(cs, args):
"""Create a trigger."""
trigger_properties = _extract_properties(args)
trigger = cs.triggers.create(args.name, args.type, trigger_properties)
utils.print_dict(trigger)
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',
metavar='<trigger>',
help='ID of trigger.')
def do_trigger_show(cs, args):
"""Shows trigger details."""
trigger = cs.triggers.get(args.trigger)
utils.print_dict(trigger.to_dict())
@utils.arg('trigger',
metavar='<trigger>',
nargs="+",
help='ID of trigger.')
def do_trigger_delete(cs, args):
"""Delete trigger."""
failure_count = 0
for trigger_id in args.trigger:
try:
trigger = utils.find_resource(cs.triggers, trigger_id)
cs.triggers.delete(trigger.id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; trigger not found".
format(trigger_id))
if failure_count == len(args.trigger):
raise exceptions.CommandError("Unable to find and delete any of the "
"specified trigger.")
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help='Filters results by a name. Default=None.')
@utils.arg('--operation_type',
metavar='<operation_type>',
default=None,
help='Filters results by a type. Default=None.')
@utils.arg('--trigger_id',
metavar='<trigger_id>',
default=None,
help='Filters results by a trigger id. Default=None.')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning resources that appear later in the '
'list than that represented by this id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number to return. Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
def do_scheduledoperation_list(cs, args):
"""Lists all scheduledoperations."""
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'operation_type': args.type,
'trigger_id': args.trigger_id,
}
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
scheduledoperations = cs.scheduled_operations.list(
search_opts=search_opts, marker=args.marker, limit=args.limit,
sort_key=args.sort_key, sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'OperationType', 'TriggerId',
'OperationDefinition']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(scheduledoperations, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('name',
metavar='<name>',
help='Trigger name.')
@utils.arg('operation_type',
metavar='<operation_type>',
help='Operation Type of scheduled operation.')
@utils.arg('trigger_id',
metavar='<trigger_id>',
help='Trigger id of scheduled operation.')
@utils.arg('operation_definition',
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
@utils.arg('scheduledoperation',
metavar='<scheduledoperation>',
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())
@utils.arg('scheduledoperation',
metavar='<scheduledoperation>',
nargs="+",
help='ID of scheduled operation.')
def do_scheduledoperation_delete(cs, args):
"""Delete a scheduled operation."""
failure_count = 0
for scheduledoperation_id in args.scheduledoperation:
try:
scheduledoperation = utils.find_resource(cs.scheduledoperations,
scheduledoperation_id)
cs.scheduledoperations.delete(scheduledoperation.id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; scheduledoperation not found".
format(scheduledoperation_id))
if failure_count == len(args.scheduledoperation):
raise exceptions.CommandError("Unable to find and delete any of the "
"specified scheduled operation.")

View File

@@ -1,62 +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.
from smaugclient.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
def create(self, name, type, properties):
body = {'trigger_info': {'name': name,
'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)
def delete(self, trigger_id):
path = '/v1/{project_id}/triggers/{trigger_id}'.format(
project_id=self.project_id,
trigger_id=trigger_id)
return self._delete(path)
def get(self, trigger_id, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
url = "/v1/{project_id}/triggers/{trigger_id}".format(
project_id=self.project_id,
trigger_id=trigger_id)
return self._get(url, response_key="trigger_info", headers=headers)
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all triggers."""
resource_type = "triggers"
url = self._build_list_url(
resource_type, detailed=detailed,
search_opts=search_opts, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir, sort=sort)
return self._list(url, 'triggers')

View File

@@ -1,16 +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.
from pbr import version
version_info = version.VersionInfo('python-smaugclient')

View File

@@ -1,15 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking<0.11,>=0.10.2 # Apache-2.0
coverage>=3.6 # Apache-2.0
discover # BSD
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
oslosphinx!=3.4.0,>=2.5.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

40
tox.ini
View File

@@ -1,40 +0,0 @@
[tox]
minversion = 1.6
envlist = py34,py27,pypy,pep8
skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py test --slowest --testr-args='{posargs}'
[testenv:pep8]
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:functional]
setenv =
OS_TEST_PATH = ./smaugclient/tests/functional
passenv = OS_*
[testenv:cover]
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools