Compare commits

...

285 Commits

Author SHA1 Message Date
d1213c3c5d Update .gitreview for stable/2026.1
Change-Id: I195b739ffce2c2615a85e0ecaf865275ae28409a
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/functions
2026-03-06 16:09:51 +00:00
Zuul
1c776112e9 Merge "Declare Python 3.13 support" 2026-02-04 10:19:16 +00:00
Zuul
8f26fed622 Merge "reno: Update master for unmaintained/2024.1" 2026-02-04 10:03:45 +00:00
Zuul
de6fcfa027 Merge "[tox] remove DEVSTACK_VENV variable" 2026-01-30 12:21:26 +00:00
Jaromir Wysoglad
f97ad45e67 [tox] remove DEVSTACK_VENV variable
This variable seems to be unused (grepping for it returns
only results from tox.ini, which are getting deleted here).

Looking at the PS where it got introduced
https://review.opendev.org/c/openstack/python-cloudkittyclient/+/893046
it seems like it was supposed to be used in the functional tests,
but there were issues and the path was hardcoded instead.

Change-Id: I8a75f5849894e5a0c327bd6e6b9ef87dd8f1f820
Signed-off-by: Jaromir Wysoglad <jwysogla@redhat.com>
2026-01-29 10:31:43 +01:00
Takashi Kajinami
d3c9d9e82e Declare Python 3.13 support
Python 3.13 is part of supported runtimes for 2026.1[1] and now is
tested.

[1] https://governance.openstack.org/tc/reference/runtimes/2026.1.html

Change-Id: I6b5bf46a8d3cf1c04269a387e37225ddce1eae85
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2026-01-22 00:48:54 +09:00
Zuul
dea9c3043a Merge "Use v2 API by default" 2025-12-09 14:42:16 +00:00
Juan Larriba
2d965f9459 Use v2 API by default
CloudKitty's v1 API has been deprecated for a while, but the CLI
continued to use that as a default, keeping v2 as optional. This patch
changes that behaviour, switching to use the more modern and currently
maintained v2 as default, while v1 is still available via the
--os-rating-api-version parameter.

Change-Id: I4ca8c4f69b022af53d9f7ec71f3a2efadfc9163e
Signed-off-by: Juan Larriba <jlarriba@redhat.com>
2025-12-09 12:57:43 +01:00
Zuul
5bd8a2f8fd Merge "CI: enable ceilometer plugin" 2025-12-08 16:27:21 +00:00
Zuul
a41e96b0e3 Merge "Replace os-client-config" 2025-12-08 16:24:58 +00:00
Jaromir Wysoglad
e1408eba2b Fix docs job
- Remove skipsdist which breaks automatic generation in CLI reference
  and API reference sections of the docs since tox 4

- repository_name is deprecated in favor of openstackdocs_repository_name

- bug_project and bug_tag are replaced by openstackdocs_use_storyboard

Change-Id: I28d9e87ebd59823ad0ef2b1893a711bfad700286
Signed-off-by: Jaromir Wysoglad <jwysogla@redhat.com>
2025-11-27 10:23:13 +01:00
606b47318c reno: Update master for unmaintained/2024.1
Update the 2024.1 release notes configuration to build from
unmaintained/2024.1.

Change-Id: I02a5088563ec81b900d95c2db1c6988f3b3c1372
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/change_reno_branch_to_unmaintained.sh
2025-10-31 11:37:29 +00:00
Zuul
59dc73588f Merge "Update supported python versions" 2025-09-30 12:37:23 +00:00
Takashi Kajinami
ab5b02d84c Update supported python versions
Python 3.9 was removed from the tested runtimes in this cycle[1].
Python 3.8 should have been removed in 2024.2 release.

Also declare support for Python 3.12 which has been tested for some
time and is mandatory now.

[1] https://governance.openstack.org/tc/reference/runtimes/2025.2.html

Change-Id: Ia7cf5ffe4e4093189fdee425afadd81df862be1e
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2025-09-30 19:38:30 +09:00
Zuul
ec1bf47a47 Merge "Add support to rating rules with start and end" 2025-09-15 13:48:31 +00:00
c26fc952ed Update master for stable/2025.2
Add file to the reno documentation build to show release notes for
stable/2025.2.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2025.2.

Sem-Ver: feature
Change-Id: I6b6264daba082f260aab197fce0a0a232e7cbb52
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh
2025-09-04 13:48:08 +00:00
Pedro Henrique
f8da9dab78 Add support to rating rules with start and end
It was introduced the concept of start and end periods in
Cloudkitty rating rules. Therefore to make Cloudkitty CLI
compatible with the Cloudkitty REST API, we need to add
those new available attributes in the CLI as well.

Change-Id: I0cd9b61fa81232d235c959da551a7840465fae88
Signed-off-by: Pedro Henrique <phpm13@gmail.com>
2025-09-02 12:22:05 -03:00
Takashi Kajinami
2240208aa5 Replace os-client-config
It was deprecated[1] after the code was merged into openstacksdk[2].

[1] https://review.opendev.org/c/openstack/os-client-config/+/549307
[2] https://review.opendev.org/c/openstack/openstacksdk/+/518128

Change-Id: I47f1ebf037d2e93bf8901d1c9e21b75532c93918
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2025-07-01 01:16:40 +09:00
Zuul
c4711dfd16 Merge "Use releases.openstack.org instead of opendev.org" 2025-03-25 09:38:15 +00:00
Pierre Riteau
4fec2c3a0b Remove Python 3 variables from devstack config
The USE_PYTHON3 variable was removed earlier this year [1].

[1]  https://review.opendev.org/c/openstack/devstack/+/920658

Change-Id: I50d8c94e00c38d6ec818a99727b2829bd330fba0
2025-03-19 13:58:56 +00:00
Pierre Riteau
4058c7f379 Use releases.openstack.org instead of opendev.org
This is a more stable URL that will redirect to the correct location.

Change-Id: If5ad860a65c1d4934ad47fb06f1f3386b4550008
2025-03-07 18:49:44 +01:00
5573390afa Update master for stable/2025.1
Add file to the reno documentation build to show release notes for
stable/2025.1.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2025.1.

Sem-Ver: feature
Change-Id: Ie0928aa18c8d69a2a01f2f594f42774b66279a91
2025-03-07 14:40:58 +00:00
Takashi Kajinami
4f375f8d23 tox: Drop envdir
tox now always recreates an env although the env is shared using envdir
options.
~~~
$ tox -e genpolicy
genpolicy: recreate env because env type changed from
{'name': 'genconfig', 'type': 'VirtualEnvRunner'} to
{'name': 'genpolicy', 'type': 'VirtualEnvRunner'}
~~~

According to the maintainer of tox, this functionality is not intended
to be supported.
https://github.com/tox-dev/tox/issues/425#issuecomment-1011944293

Change-Id: I3155eb73edb09184a29522708ef2f676bd06fb7f
2025-02-11 13:26:22 +09:00
Pierre Riteau
826297eb04 CI: enable ceilometer plugin
This should ensure Gnocchi is installed, otherwise we get the following
error in ck-proc:

    keystoneauth1.exceptions.catalog.EndpointNotFound: internalURL endpoint for metric service in RegionOne region not found

Change-Id: Ie0fe899caac481ea31675fcc17e6e849689bde77
2024-11-19 21:23:27 +01:00
9db6f56b9a reno: Update master for unmaintained/2023.1
Update the 2023.1 release notes configuration to build from
unmaintained/2023.1.

Change-Id: I2f94f7eb27f4672b9a9717f3cbd8d63e08a7d116
2024-11-14 10:01:19 +00:00
Zuul
8e6f9aee06 Merge "Replace distutils" 2024-10-14 15:18:32 +00:00
Zuul
9fda7927a1 Merge "Bump hacking" 2024-10-14 10:34:01 +00:00
Takashi Kajinami
ca7a0b1be2 Replace distutils
distutils was deprecated in Python 3.10 and was removed in Python
3.12 [1].

[1] https://docs.python.org//3.10/library/distutils.html

Change-Id: Iade3ecba97cb35f0afcb8e9fbde8dd1095b6fb47
2024-10-02 23:37:11 +09:00
Takashi Kajinami
33905a1913 Bump hacking
hacking 3.0.x is quite old. Bump it to the latest version available.

Change-Id: I31bf48ffaa8fce5d0ea4f5c1cd5031485ae729a1
2024-09-20 00:02:22 +00:00
Zuul
e2d507900a Merge "Update master for stable/2024.2" 2024-09-16 14:23:05 +00:00
acbd7ae72e Update master for stable/2024.2
Add file to the reno documentation build to show release notes for
stable/2024.2.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2024.2.

Sem-Ver: feature
Change-Id: Idf34ecff29925203f445a87244d24ca5823ca107
2024-09-05 15:54:51 +00:00
Takashi Kajinami
247b3ee51d Remove logic for Python < 3
... because Python 2 support was removed long ago.

Change-Id: I5ac3c2d3d41651274abc98435180efadad3dffd1
2024-09-03 16:34:53 +09:00
Zuul
79a4606ce2 Merge "add group by time help to CLI" 2024-06-26 12:16:31 +00:00
Zuul
e912feb85d Merge "Fix reprocessing POST request" 2024-04-29 11:41:36 +00:00
Zuul
9bb62e0338 Merge "reno: Update master for unmaintained/victoria" 2024-04-29 10:59:58 +00:00
8d992f59dd reno: Update master for unmaintained/zed
Update the zed release notes configuration to build from
unmaintained/zed.

Change-Id: I89f8f9f24499c2aaf663f3463143a688102e78e1
2024-04-26 18:15:28 +00:00
Matt Crees
6ee36ef0e3 Fix reprocessing POST request
The POST request for triggering reprocessing needs to be made to
``/v2/task/reprocesses``. This is the same as for other requests, so we
can drop ``url_for_post``.

Change-Id: If630d4f313c875733dbe1937ff7ca625821e04af
2024-04-23 17:01:09 +01:00
Rafael Weingärtner
98c13304bd add group by time help to CLI
Change-Id: I2176ab207f94b6f986ebc32371f85c47b097a116
2024-04-11 09:41:40 -03:00
Zuul
024399e10a Merge "reno: Update master for unmaintained/xena" 2024-03-30 11:45:09 +00:00
Zuul
0698fdde3a Merge "reno: Update master for unmaintained/wallaby" 2024-03-30 11:41:09 +00:00
bb26dbc381 Update master for stable/2024.1
Add file to the reno documentation build to show release notes for
stable/2024.1.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2024.1.

Sem-Ver: feature
Change-Id: I4bc29c5959626c2a2f55f943f6c25bb32a9dad99
2024-03-07 15:35:53 +00:00
d876b1b77f reno: Update master for unmaintained/xena
Update the xena release notes configuration to build from
unmaintained/xena.

Change-Id: Ia90b7810a48e5ecce59dcd9d86c556c2574016ad
2024-03-05 14:55:15 +00:00
2a3733e869 reno: Update master for unmaintained/wallaby
Update the wallaby release notes configuration to build from
unmaintained/wallaby.

Change-Id: If27a329a11330fa569d272224753a953c836f81f
2024-03-05 14:54:50 +00:00
00043cc23f reno: Update master for unmaintained/victoria
Update the victoria release notes configuration to build from
unmaintained/victoria.

Change-Id: I7c621c3eaca3ef115e6bbf13aef0044bbd1924b7
2024-03-05 14:54:26 +00:00
Zuul
d30991cefa Merge "reno: Update master for unmaintained/yoga" 2024-02-13 11:26:37 +00:00
36566c32fc reno: Update master for unmaintained/yoga
Update the yoga release notes configuration to build from
unmaintained/yoga.

Change-Id: I00904420fe9ef2cae1ea0d507e40f99b95bcae9e
2024-02-05 16:43:11 +00:00
Ghanshyam Mann
3e9fd5b540 Update python classifier in setup.cfg
As per the current release tested runtime, we test
till python 3.11 so updating the same in python
classifier in setup.cfg

Change-Id: I8695f7f097e4dd98850caab63ea914f0dbf5da11
2024-01-03 21:22:36 -08:00
255a871785 Update master for stable/2023.2
Add file to the reno documentation build to show release notes for
stable/2023.2.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2023.2.

Sem-Ver: feature
Change-Id: Iae9e9a83c2dcf5edf16d86ce1ec802c6310caa1a
2023-09-07 09:35:33 +00:00
Rafael Weingärtner
244f229af7 Fix passenv declaration in tox.ini and function tests python env
While running the tests with the latest tox I was getting the following error message:
```
failed with pass_env values cannot contain whitespace, use comma to have multiple values in a single line'

```

That error is happening because of the passenv declaration. This patch is proposing a fix for that.

Besides the `tox` issue, we also needed to create a patch for the use of virtual env inside DevStack.
This patches presents a solution to run tests using the virtual env of DevStack.

Change-Id: Id8249ebb15d4047dcc6181908eae66eb39722863
2023-08-31 20:18:31 -03:00
0835706738 Update master for stable/2023.1
Add file to the reno documentation build to show release notes for
stable/2023.1.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2023.1.

Sem-Ver: feature
Change-Id: I382ddca39bceac78b1fed648738edfaa5655117e
2023-02-21 14:50:01 +00:00
97dc4d64da Switch to 2023.1 Python3 unit tests and generic template name
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for antelope. Also,
updating the template name to generic one.

See also the PTI in governance [1].

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

Change-Id: Ibd133ec5cb0af7c0835a9014cc44b201050bd15c
2022-09-14 09:15:01 +00:00
0c97e40e2b Update master for stable/zed
Add file to the reno documentation build to show release notes for
stable/zed.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/zed.

Sem-Ver: feature
Change-Id: Iac2443a8053991e604fdab64bced60a7030d9772
2022-09-09 11:44:57 +00:00
niuke
0ee5e17ee5 remove unicode prefix from code
Change-Id: I716442a44cdb0cd651cfed8419713c6d76f2ba14
2022-08-24 19:47:52 +08:00
Zuul
07d4e86ca4 Merge "Add Python3 zed unit tests" 2022-08-08 15:20:12 +00:00
Zuul
6bd44e7413 Merge "Introduce reprocessing task API in the CLI" 2022-08-08 15:20:10 +00:00
Zuul
c30878e414 Merge "Introduce the patch scope API in the CLI" 2022-08-08 15:01:18 +00:00
Elod Illes
eb0cac7148 Add Python3 zed unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for zed.

See also the PTI in governance [1].

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

Change-Id: If9b21c3e9628a945d618c38b015196544a69ae8c
2022-08-08 15:51:48 +02:00
Rafael Weingärtner
187b0ce70c Introduce the patch scope API in the CLI
Change-Id: I8134020c409bc6c3e80bf996890e6609e1a763b9
2022-08-08 10:49:42 -03:00
Rafael Weingärtner
277b47779f Introduce reprocessing task API in the CLI
Change-Id: Ieab5df4deb9cbf5eddfc8eca3b028942f6303abd
2022-08-08 10:46:50 -03:00
Ghanshyam Mann
19c0ebad5b Drop lower-constraints.txt and its testing
As discussed in TC PTG[1] and TC resolution[2], we are
dropping the lower-constraints.txt file and its testing.
We will keep lower bounds in the requirements.txt file but
with a note that these are not tested lower bounds and we
try our best to keep them updated.

[1] https://etherpad.opendev.org/p/tc-zed-ptg#L326
[2] https://governance.openstack.org/tc/resolutions/20220414-drop-lower-constraints.html#proposal

Change-Id: I9261e86576e328b28a6c4442731e2ebbb414eb4d
2022-04-30 15:56:26 -05:00
de6c8b22c5 Update master for stable/yoga
Add file to the reno documentation build to show release notes for
stable/yoga.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/yoga.

Sem-Ver: feature
Change-Id: Ib59df27f2c771cb407b267fbcedee3588eb83049
2022-03-03 10:51:04 +00:00
Sam Morrison
783cc22662 Add response_format to list of available args for v2 summary API
This was added in cloudkitty with commit
6ba9d45ea6

Change-Id: Id091ee9f61c2863ed3ea1aaf5d967edf417df22a
2022-02-11 13:16:01 +11:00
Ghanshyam Mann
20003a58ce Re-add python 3.6/3.7 in classifier
We have updated the yoga testing runtime to keep the
py36 testing.

- https://review.opendev.org/c/openstack/governance/+/820195

Unit tests job template is also updated to keep python
3.6 as a voting job. So with the py3.6 and py3.9 testing as voting
job template, we are keeping python 3.6, 3.7, 3.8, and 3.8 as
tested versions in the Yoga cycle.

- https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/820286

This commit re-add the python 3.6/3.7 versions in setup.cfg classifier.

Change-Id: Iaf4b4a82b5a50b3ef2dc793c50ef1b0981f37856
2021-12-13 19:18:27 -06:00
Ghanshyam Mann
719f4cff6a Updating python testing as per Yoga testing runtime
Yoga testing runtime has been updated with py38 and py39
as voting and removed the py36 testing. Unit tests update are
handled by the job template change in openstack-zuul-job

- https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/818609

this commit makes other required changes in setup.cfg metadata.

[1] https://governance.openstack.org/tc/reference/runtimes/yoga.html

Change-Id: Idd8da33c954234b7c5d2d969c50da8aee4bf7904
2021-11-24 19:11:11 -06:00
wu.shiming
64ab6b412a Replace deprecated assertRaisesRegexp
The assertRaisesRegexp method has been deprecated since it was renamed
to assertRaisesRegex in Python 3.2.

https://docs.python.org/3/library/unittest.html#deprecated-aliases

Change-Id: Ibab4d14764372298cc0055f168ff44eabdc873f0
2021-11-09 09:04:34 +08:00
Pierre Riteau
85317b6773 Fix capitalisation of CloudKitty
This is really an excuse to force generating release notes which are
missing for Xena.

Change-Id: Ie8459f44142a981ee3475dd9b2688d155c852376
2021-09-24 09:48:01 +02:00
6e10961c49 Add Python3 yoga unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for yoga.

See also the PTI in governance [1].

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

Change-Id: I8e6532689aed280ce32a6b33dbb7b26bfb6c8044
2021-09-10 14:33:13 +00:00
2454c59cb2 Update master for stable/xena
Add file to the reno documentation build to show release notes for
stable/xena.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/xena.

Sem-Ver: feature
Change-Id: I408828a8e29a418dba134bed8cd7a94206b26132
2021-09-10 14:33:08 +00:00
wu.shiming
647f561532 Replace deprecated import of ABCs from collections
ABCs in collections should be imported from collections.abc and direct
import from collections is deprecated since Python 3.3.

Change-Id: I871810bbe95a3cd10f8a8df42c99e747a6dd463b
2021-08-13 17:15:35 +08:00
Pierre Riteau
61ef2c337e Fix creation of hashmap mapping with a zero cost
A cost of 0.0 was interpreted as a False value, making the check think
the cost argument was missing.

Change-Id: I5f86540221b80667fc63b8b54659092c637b7353
Story: 2009047
Task: 42814
2021-07-12 19:31:06 +02:00
Zuul
b1df2b5ccc Merge "Changed minversion in tox to 3.18.0" 2021-07-08 13:35:55 +00:00
Zuul
68fd0e9e33 Merge "setup.cfg: Replace dashes with underscores" 2021-07-08 13:06:59 +00:00
wu.shiming
eb7f5e46bb Changed minversion in tox to 3.18.0
The patch bumps min version of tox to 3.18.0 in order to
replace tox's whitelist_externals by allowlist_externals option:
https://github.com/tox-dev/tox/blob/master/docs/changelog.rst#v3180-2020-07-23

Change-Id: I1d22f2f0f452bc79ce79a339e6c304d5a0ac953f
2021-07-06 16:16:44 +08:00
Pierre Riteau
0608c0527b docs: Update Freenode to OFTC
Change-Id: I3224ca9b96a181de8c0251d0bd589135e098d557
2021-06-08 10:18:45 +02:00
wu.shiming
754e06c51c setup.cfg: Replace dashes with underscores
Setuptools v54.1.0 introduces a warning that the use of dash-separated
options in 'setup.cfg' will not be supported in a future version [1].
Get ahead of the issue by replacing the dashes with underscores. Without
this, we see 'UserWarning' messages like the following on new enough
versions of setuptools:

  UserWarning: Usage of dash-separated 'description-file' will not be
  supported in future versions. Please use the underscore name
  'description_file' instead

[1] https://github.com/pypa/setuptools/commit/a2e9ae4cb

Change-Id: I7a871d30bf935614ff4bc4a254cc4fc63c4218f4
2021-06-08 08:16:40 +00:00
Ghanshyam Mann
d12e5a821c [ussuri][goal] Update contributor documentation
This patch updates/adds the contributor documentation to follow
the guidelines of the Ussuri cycle community goal[1].

[1] https://governance.openstack.org/tc/goals/selected/ussuri/project-ptl-and-contrib-docs.html

Story: #2007236
Task: #38517
Change-Id: Ic973be624418d056195127bb6f621b93edc3a3ed
2021-05-19 15:09:51 +00:00
Pierre Riteau
d2c2323bf8 Fix PDF docs build
Update tox config to include upper-constraints as dependencies. This
resolves issues with building PDF docs.

Change-Id: I98f287e4654ecb6b92d4f3815f359b205b06559a
2021-05-18 11:09:15 +02:00
zhangboye
67fae0ff52 Use py3 as the default runtime for tox
Moving on py3 as the default runtime for tox to avoid to update
this at each new cycle.

Change-Id: I98097f7e5afe4dd1a69a9e661ccc5353dbd5e29c
2021-04-20 16:35:55 +08:00
Pierre Riteau
73668bebb0 Add release notes job template
Without this job template there are no release notes published [1].

[1] https://docs.openstack.org/releasenotes/python-cloudkittyclient

Change-Id: I8ab231a4c563ecc2f8db22bade12bf4afa9fafc8
2021-04-12 14:44:58 +02:00
dc606b1d3c Add Python3 xena unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for xena.

See also the PTI in governance [1].

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

Change-Id: Ie0255d5300b303e9392497d6494fdc5f6e08eb53
2021-03-19 13:01:51 +00:00
5141971122 Update master for stable/wallaby
Add file to the reno documentation build to show release notes for
stable/wallaby.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/wallaby.

Sem-Ver: feature
Change-Id: Ic56e3c816c1729fd66d6b5b8467bebb3fa2976b8
2021-03-19 13:01:32 +00:00
Zuul
1e8b06243e Merge "Fix create_threshold method when using cost as 0" 2020-12-22 16:20:55 +00:00
Rafael Weingärtner
a15f11a7d2 Fix create_threshold method when using cost as 0
When using 0 as the cost, the `create_threshold` method
throws an exception. That happens because 0 (zero) is evaluated
to False. Therefore, we need to change the validation method to
check if the values are None.

Change-Id: Iedd541c0ad16db0d11d6e6de332eddf880af1698
2020-12-11 10:25:10 -03:00
wuchunyang
c2b5ed9535 Replace deprecated UPPER_CONSTRAINTS_FILE variable
UPPER_CONSTRAINTS_FILE is old name and deprecated
-https://zuul-ci.org/docs/zuul-jobs/python-roles.html#rolevar-tox.tox_constraints_file
This allows to use lower-constraints file as more
readable way instead of UPPER_CONSTRAINTS_FILE=<lower-constraints file>.

Change-Id: I25d4154797da0453f59e310d2392d30ca35e5258
2020-11-27 12:41:36 +00:00
Zuul
bddf634143 Merge "Drop mock from lower-constraints.txt" 2020-09-25 11:55:49 +00:00
Zuul
5442134a5f Merge "bump py37 to py38 in tox.ini" 2020-09-25 11:55:48 +00:00
wangzihao
3ec8f86e5a bump py37 to py38 in tox.ini
in 'victoria' cycle, we should test py38 by default.

Change-Id: Ib01cf2aad3edf31a067a6ac4459529851f8d8ecb
2020-09-22 00:50:20 +00:00
wangzihao
5b35e817b2 Drop mock from lower-constraints.txt
The mock is not needed for py36 and later.

Change-Id: I24afe1f1255ac47dfba300946149732f70f5f399
2020-09-21 11:46:13 +08:00
wangzihao
953c6c9443 Bump hacking min version to 3.0.1
hacking 3.0.1 fix the pinning of flake8 to avoid bringing in a new
version with new checks.

bumping the min version for hacking so that any older hacking versions
which auto adopt the new checks are not used.

Change-Id: I5875f1c0261ff6039773d7daf412102f2c02651f
2020-09-18 10:36:40 +08:00
30e21ddf7b Add Python3 wallaby unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for wallaby.

See also the PTI in governance [1].

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

Change-Id: I4c238eefa02cece34ee8f77a0dee91a5bf698a65
2020-09-08 22:47:17 +00:00
c24de5fe54 Update master for stable/victoria
Add file to the reno documentation build to show release notes for
stable/victoria.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/victoria.

Change-Id: Ifdc6cb5adbb2139a2437d220b231c1c61526e648
Sem-Ver: feature
2020-09-08 22:47:15 +00:00
Justin Ferrieu
2a3dd279dc Add support for GET /v2/dataframes API endpoint to the client
Support for the ``GET /v2/dataframes`` endpoint has been added
to the client. A new ``dataframes get`` CLI command is also available.

Story: 2005890
Task: 36384
Depends-On: https://review.opendev.org/#/c/679636
Change-Id: Idfe93025e0f740906d0f53f33547c7746fc15169
2020-09-08 12:49:29 +00:00
Andreas Jaeger
def535752f 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: I8fed95ecb736e0b6d7c4b63a55553de1539139be
2020-09-07 11:29:21 -03:00
Zuul
d3408517d6 Merge "Remove translation sections from setup.cfg" 2020-09-01 13:43:38 +00:00
Zuul
37df7e1258 Merge "migrate testing to ubuntu focal" 2020-09-01 13:34:30 +00:00
Zuul
e9a6c22cef Merge "Remove six" 2020-08-31 17:58:31 +00:00
melissaml
db10c24e25 Remove translation sections from setup.cfg
These translation sections are not needed anymore, Babel can
generate translation files without them.

Change-Id: I6559348831b9277b70834556adbd9445c5982c79
2020-08-27 08:25:29 -05:00
Zuul
2bcd29dcbd Merge "Cleanup py27 support" 2020-08-24 15:57:36 +00:00
Zuul
d5a99b511f Merge "Fix pygments style" 2020-08-24 14:55:03 +00:00
Zuul
2a475bbb4a Merge "add py38 package metedata" 2020-08-21 16:41:25 +00:00
Zuul
8f9e104f84 Merge "Add Python3 victoria unit tests" 2020-08-21 16:07:40 +00:00
Zuul
a2f3de89b4 Merge "Update master for stable/ussuri" 2020-08-21 15:34:40 +00:00
jiasirui
608cd0262f add py38 package metedata
Change-Id: I08a5505237eabf5c440ed4e3f068b5a834d5917d
2020-08-18 15:43:38 +00:00
Ghanshyam Mann
de3c4924e9 migrate testing to ubuntu focal
As per victoria cycle testing runtime and community goal[1]
we need to migrate upstream CI/CD to Ubuntu Focal(20.04).

Fixing:
- bug#1886296
Bump the pyflakes to 2.1.1 as min version to run pep8 jobs
on py3.8 which is default python vesion in ubuntu focal.

Story: #2007865
Task: #40180

Closes-Bug: #1886296
[1] https://governance.openstack.org/tc/goals/selected/victoria/migrate-ci-cd-jobs-to-ubuntu-focal.html

Change-Id: I482ac98bc56f0e3cfb8b767f47649da11ed1afab
2020-08-13 17:24:09 +00:00
fuzihao
e88a3fa033 Fix pygments style
New theme of docs (Victoria+) respects pygments_style.
Since we starts using Victoria reqs while being on Ussuri,
this patch ensures proper rendering both in Ussuri and Victoria.

Change-Id: If42b2154a5a28f92d89dde9882afa31f01bc5ac3
2020-05-20 14:51:45 +08:00
jacky06
61dc82cb54 Remove six
We don't need this in a Python 3-only world.

Change-Id: Ibeb506281e88b44d454497d06f9187308859ac9c
2020-05-10 23:08:12 +08:00
Sean McGinnis
e69f9d5452 Use unittest.mock instead of third party mock
Now that we no longer support py27, we can use the standard library
unittest.mock module instead of the third party mock lib.

Change-Id: I9bf0a8fbb7b4f22aa2f5b5ed0836d11cac27552b
Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
2020-04-18 11:57:46 -05:00
73cb650ee4 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: I603335a1099880a00de6e4cfef6393a436ff990b
2020-04-11 18:49:23 +00:00
8bc96e21a9 Update master for stable/ussuri
Add file to the reno documentation build to show release notes for
stable/ussuri.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/ussuri.

Change-Id: I26f6e1f91faec1ce661ecfe2570c524b64687da9
Sem-Ver: feature
2020-04-11 18:49:18 +00:00
Andreas Jaeger
3f97e9844a Cleanup py27 support
Make a few cleanups:
- Remove python 2.7 stanza from setup.py
- Remove obsolete sections from setup.cfg
- Update classifiers

Change-Id: I79e3c540b56b024c7d01e4c916cdd79da9000331
2020-04-04 12:32:35 +02:00
Zuul
f4b1a3f224 Merge "Add support for v2/rating/modules endpoints" 2020-02-10 10:16:26 +00:00
Luka Peschke
9424e67f21 [ussuri][goal] Drop python 2.7 support and testing
This drops python2.7 support for cloudkittyclient. Even if this should be
done between milestone-1 and milestone-2, zuul jobs running on python2 are
currently broken since nova dropped python2.7 support.

Depends-On: https://review.opendev.org/#/c/693631/
Change-Id: I7615601540419e45259291a7bfce1cc038c27986
2020-01-23 13:42:35 +00:00
Luka Peschke
9c3bd770f2 Fix tox environments
Work items:

* Removed the globale-requirements constraint. Since python-cloudkittyclient
  is now part of the global requirements, the upstream requirements
  file can't be used anymore.

* Add cliff to docs requirements.

* Change cloudkittyclient namespace names: Having a dot in a namespace
  name causes the "autoprogram-cliff" to use the "application mode".

Change-Id: I8020d816b3397550fbcbd42cc14a9861bca7ae80
2019-12-17 15:10:17 +01:00
Luka Peschke
d28c5bc4dd Improve HTTP error formatting
This improves formatting for HTTP errors. keystoneauth's HttpErrors
are now caught an re-formatted to include the body of the http response.

Work items:

* Introduce the "http_error_formatter" function decorator, which catches
  an re-formats keystoneauths HttpErrors.

* Introduce the "format_http_errors" class decorator, which applies the
  "http_error_formatter" to all functions of a class.

* Add an "HttpDecoratorMeta" to the "BaseManager" class. This will decorate
  all functions of classes inheriting from "BaseManager" with the
  "http_error_formatter" decorator.

Change-Id: I6735f1fa8d876a87e2b7d4aaa533d5a32b085735
2019-11-28 11:52:40 +01:00
caoyuan
3e7f7a0f5d tox: Keeping going with docs
Sphinx 1.8 introduced [1] the '--keep-going' argument which, as its name
suggests, keeps the build running when it encounters non-fatal errors.
This is exceptionally useful in avoiding a continuous edit-build loop
when undertaking large doc reworks where multiple errors may be
introduced.

[1] https://github.com/sphinx-doc/sphinx/commit/e3483e9b045

Change-Id: Ie373018aafc05f7ea859a73c55163840dee60b56
2019-11-18 15:46:25 +00:00
Luka Peschke
584046761a Adapt functional tests to python3
A test looking for a regex in an argparse error message is failing when ran
on python3. Since this is now the default python runtime for devstack-based
jobs, this updates the message.

Change-Id: Id3372998715f13f7641d11a4a1d9d16629acfd18
Story: 2006896
Task: 37530
2019-11-18 10:28:23 +01:00
kangyufei
be0a9861d0 Switch to Ussuri jobs
Change-Id: I40dbc3c66164725bc4a8af4d0bf9ab481a7d72e7
2019-10-22 13:45:58 +08:00
Quentin Anglade
e599ac9f84 Add support for v2/rating/modules endpoints
This adds support for the new v2 API rating modules endpoints. Unit tests included.

Change-Id: Ie116c518f30128b49c3991f36101db7535af7db1
Story: 2006572
Task: 36680
2019-09-30 16:36:16 +02:00
55b056bc8a Update master for stable/train
Add file to the reno documentation build to show release notes for
stable/train.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/train.

Change-Id: Ia9a85597ccd963eb69f5e04eb1d28d9346f4c31d
Sem-Ver: feature
2019-09-20 16:44:57 +00:00
manchandavishal
296fd22640 Generate PDF documentation
This commit adds a new tox target to build PDF documentation.
It's a part of community goal, see storyboard for more
information.

Change-Id: I6e4f6372ff026a9eb379ee06c683451f58a99976
Story: 2006075
Task: 34812
2019-09-16 16:46:06 +00:00
Justin Ferrieu
c8d7a9e1c5 Add support for POST /v2/dataframes API endpoint to the client
Support for the ``/v2/dataframes`` endpoint has been added to the client.
A new ``dataframes add`` CLI command is also available.

Change-Id: I7fe9072d7280f251edc865a653a0b9ed2ab26c90
Story: 2005890
Task: 35970
2019-08-27 11:50:32 +00:00
Luka Peschke
dd4112acea Add functional test jobs for the v2 client
This adds test jobs for the v2 client, in python2 and python3.

Work items:

* Remove the "functional" tox environment and introduce the "functional-v1"
  and "functional-v2" environments.

* Add zuul base jobs for python2 and python 3 testing. Two jobs inherit from
  each of these new jobs: one for the v1 client and one for the v2 client.

* Add "OS_ENDPOINT" to the list of environment variables forwarded to the
  functional test environments in order to ease local testing.

Change-Id: I54a43a1e844e92730afbf87316b9efe73a08d850
2019-08-27 10:13:32 +02:00
pengyuesheng
3010383f10 Add lower-constraints job
create a tox environment for running the unit tests against the lower
bounds of the dependencies.

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: Iae676c4bbd00836cc6dce0f083f7aa308bbfc372
2019-08-20 13:07:13 +00:00
Justin Ferrieu
d660bef837 Add support for PUT /v2/scope API endpoint to the client
This allows to reset the state of one or several scopes through the API via
the client library and cli tool.

Change-Id: I69ce9a1c2ee0d8a6dd191a39e5c843e0baa1290f
Story: 2005395
Task: 30794
2019-07-22 11:42:15 +00:00
Luka Peschke
c138f409b1 Add support for /v2/summary to the client
This allows to get a summary through the v2 API endpoint via the client
library and cli tool.

Depends-On: https://review.opendev.org/#/c/660608/
Change-Id: Id63f2419fe3a1eb518a0ffa7ea5fa572b18df651
Story: 2005664
Task: 30960
2019-06-27 14:49:34 +00:00
pengyuesheng
a5a14a5883 Bump openstackdocstheme to 1.30.0
...to pick up many improvements, including the return of table borders.

Change-Id: I6af7c339e139f0980c64927fdd7cf3304b0715c5
2019-06-27 10:24:40 +08:00
Luka Peschke
7e7d25dd78 Add support for /v2/scope API endpoint to the client
This allows to retrieve the state of one or several scopes through the API via
the client library and cli tool.

Change-Id: I53995062fe76100f6dfcc672af482f653cc85bde
Story: 2005395
Task: 30795
Depends-On: https://review.opendev.org/#/c/658073/
2019-06-25 13:52:56 +00:00
pengyuesheng
b29fefaf8d Modify the url of upper_constraints_file
Depends-On: http://lists.openstack.org/pipermail/openstack-discuss/2019-May/006478.html

Change-Id: I6442a6423d3ab4df5dc699ad594eb2244e9ef994
2019-06-18 10:12:34 +08:00
pengyuesheng
301d8cb8ba Blacklist sphinx 2.1.0 (autodoc bug)
See https://github.com/sphinx-doc/sphinx/issues/6440 for upstream details
Depend-On: https://review.opendev.org/#/c/663060/

Change-Id: I3adad072b1acacecec47be076b5fca67fd1a9469
2019-06-17 14:27:44 +08:00
Zuul
b7018031a7 Merge "Add python 3.7 classifier to setup.cfg" 2019-06-13 09:01:26 +00:00
pengyuesheng
0c623ef90c Add python 3.7 classifier to setup.cfg
Change-Id: I9da335c8f7f186aa2bb8c7f4d30febafb0ec0649
2019-06-13 09:22:22 +08:00
pengyuesheng
6d27c9bc0a Use openstack-python3-train-jobs for python3 test runtime
Depends-On:https://review.opendev.org/#/c/641878/
Change-Id: I41112851450e23a05cd7ad4ccdf43bc2ad678386
2019-06-13 09:14:04 +08:00
98k
a3e18a120b Add upper-constraints.txt to releasenotes tox environment
Without these dependencies, the releasenotes build does not actually
work.

Change-Id: Iebb060857df6ec0b582e7635844e8505e09f9a4f
2019-05-20 17:12:50 +02:00
Luka Peschke
573908b345 Replaced openstack.org with opendev.org
Change-Id: I529172550f042eb291713661b6c97a8833db1872
2019-05-16 14:30:03 +02:00
Luka Peschke
d2fb83e64c Adapt the client for the v2 API
This adds a v2 client class allowing to add support for upcoming
v2 API endpoints. The v2 client class implements all v1 endpoints.
The cloudkitty API version can be specified with the
"--os-rating-api-version" option or the "OS_RATING_API_VERSION"
environment variable.

Change-Id: If38730da3baed59c93543a08f8a4989f919611db
2019-05-15 08:59:03 +00:00
Luka Peschke
d77526b42e Fix sphinx for global requirements
Change-Id: I0de0864dbc2e12efa1de6ad0da5f62a9a624b5d4
2019-05-15 10:15:57 +02:00
OpenDev Sysadmins
1f1f811f1d 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:42:57 +00:00
Ghanshyam Mann
40984c2593 Dropping the py35 testing
All the integration testing has been moved to
Bionic now[1] and py3.5 is not tested runtime for
Train or stable/stein[2].

As per below ML thread, we are good to drop the py35
testing now:
http://lists.openstack.org/pipermail/openstack-discuss/2019-April/005097.html

[1] http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004647.html
[2]
https://governance.openstack.org/tc/reference/runtimes/stein.html
https://governance.openstack.org/tc/reference/runtimes/train.html

Change-Id: Iaa29ad6616ca2cbdfeb492201aecb1aa51e728f3
2019-04-15 09:39:51 +02:00
Zuul
75687951e0 Merge "Adding a python3 functional job" 2019-04-04 08:19:06 +00:00
Luka Peschke
aeebd64928 Adding a python3 functional job
Change-Id: I3bb28edc9d62e8ea622e1061cbfb50168c217f24
2019-03-29 10:08:37 +01:00
Luka Peschke
de96c61985 Fix the rating.get_quotation method
This updates the rating.get_quotation method of the client. Tests on this
method have been added.

Depends-On: https://review.openstack.org/#/c/648062/
Change-Id: Ie2de0162311c2d162c1573042187ac4e628bd966
2019-03-28 14:57:44 +00:00
Luka Peschke
a7e687f740 Asserting 'summary get' returns nothing in functional tests
Since no data is available in devstack in our functional test environment,
we make the assertion that 'summary get' returns nothing. This is prone to
update if more complete test scenarios are implemented.

Change-Id: Ic80e39f0d2a75882762ebd6a0dba46033c9fd7f4
2019-03-28 13:41:45 +01:00
Luka Peschke
89475e2aac Fix releasenotes generation
Change-Id: Id72ea6407350bfccf7443bca9f4880d1324812c9
2019-03-27 10:43:55 +01:00
Zuul
658fd14155 Merge "Update master for stable/stein" 2019-03-21 09:15:43 +00:00
454584fc7e Update master for stable/stein
Add file to the reno documentation build to show release notes for
stable/stein.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/stein.

Change-Id: Ifba1f5deb95cc54e2b0d7a0e5782ad57e9842f62
Sem-Ver: feature
2019-03-18 14:49:55 +00:00
ZhongShengping
613e698de8 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: Ib366a82afa36152701947bcdfe9fad680264c858
Story: #2004073
Task: #27407
2019-02-19 17:05:53 +08:00
huang.zhiping
e7a9f20999 Update home-page
Change-Id: I81aebb4ca5f818fac4bd7077390b0283dce6ce18
2019-01-11 11:53:43 +00:00
ZhijunWei
dd1a421b7c Update the bugs link to storyboard
Change-Id: Ib089bdbc75d5a5008081ba0749eb944fa839f095
2018-12-31 12:28:13 +00:00
98k
419ee046f2 Change openstack-dev to openstack-discuss
Mailinglists have been updated. Openstack-discuss replaces openstack-dev.

Change-Id: Id0b5b599072f14824e29d2f896e9ea7c960545ff
2018-12-04 08:01:34 +00:00
taoguo
541d68239e Update http link to https link
Modify http to https.

Change-Id: I9cf0b476dd915725160b75affa54ef7bcbf6dc6c
2018-11-13 16:15:02 +08:00
Zuul
8e528c8e97 Merge "Fix oslo_debug_helper not running" 2018-10-26 13:40:56 +00:00
Zuul
3b49fbd4d0 Merge "Use global-requirements for requirements" 2018-10-26 11:13:37 +00:00
Jeremy Liu
378ee67117 Fix oslo_debug_helper not running
Specify test directory so that tox won't complain
`ImportError: Start directory is not importable`

Change-Id: I16fff5a8a43da50e4db8fbc5ef32cb59b0ba7f24
2018-10-23 13:50:50 +02:00
Luka Peschke
e9a92a2941 Fix "cloudkitty report tenant list" command
This fixes the "cloudkitty report tenant list": command, by transforming each
element of the list returned by CliTenantList's take_action method
into a tuple.

Change-Id: Iba1401b0cb4319a668d449139c8d20fc011cf178
Story: 2004149
Task: 27622
2018-10-23 11:27:13 +02:00
Luka Peschke
1ed287c92a Use global-requirements for requirements
This updates cloudkittyclient's requirement files in order to use
openstack/requirements for constraints. This will help to avoid dependency
conflicts when cloudkittyclient is deployed in an openstack context.

Work items:

* Updated requirements.txt, test-requirements.txt and doc/requirements.txt
  with the `update-requirements` tool provided by openstack/requirements.

* Added a lower-constraints.txt file.

* Added the "check-requirements" zuul job template to the CI.

Change-Id: I12a882ce4d24ade153a64b75852396377ac42ca6
2018-10-22 18:10:19 +02:00
huang.zhiping
63ac84b165 Update min tox version to 2.0
The commands used by constraints need at least tox 2.0.  Update to
reflect reality, which should help with local running of constraints
targets.

Change-Id: I0ff800d84949f1a02b083b4fcf16e9b82f0d9e57
2018-10-21 02:00:40 +00:00
Vieri
1cf5b3aca2 Don't quote {posargs} in tox.ini
Quotes around {posargs} cause the entire string to be combined into one
arg that gets passed to stestr. This prevents passing multiple args
(e.g. '--concurrency=16 some-regex')

Change-Id: Ie33e01abfd695c100033ec1ede8f14fec98d4b36
2018-10-09 13:46:50 +00:00
Luka Peschke
503dd3247a Add documentation jobs
Currently, the client's docs are neither built nor published. This
adds the required jobs.

Change-Id: I4199b6091502c22bf2a718d5baec29ab3f2ec400
2018-09-20 13:40:43 +02:00
Andreas Jaeger
7b9d447eb8 Use openstack-tox-cover template
Use openstack-tox-cover template, this runs the cover job as
non-voting in the check queue only.

Remove jobs and use template instead.

Add cover tox.ini environment - this job never worked before.

Change-Id: I9652aed41bc21bafc10e8c4a046d52bfb9681bdc
2018-09-07 14:32:43 +02:00
Zuul
a9eb87c436 Merge "Add `insecure and cacert` options to the client." 2018-09-07 10:00:10 +00:00
Luka Peschke
fff37a84fa Add `insecure and cacert` options to the client.
The client does support SSL authentication through keystoneauth right now. In
CLI mode, this is done through the "--os-cert" and "--os-cacert" options, or
through environment variables.

However, when the client is used as a python library,this is done through
requests' "verify" parameter, which is not very explicit.

This adds two parameters to the client

Change-Id: I68969c658724f53c85c47ab6098a3e2165f5925d
Story: 2003689
Task: 26224
2018-09-06 20:50:59 +02:00
Matthias Bastian
81cdcba4f3 Consider interface and region options with OSC
The --os-interface/OS_INTERFACE and --os-region-name/OS_REGION_NAME
options were considered by cloudkitty CLI but ignored when using the OSC
integration.

Change-Id: I36dc3616fba59c9b2e77da75abe0f76db7ddb7e4
2018-09-06 12:35:59 +02:00
Doug Hellmann
214083c695 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: I257a223e215cfb383f600954e612ffd92ab1b6f8
Story: #2002586
Task: #24289
2018-08-31 08:57:48 -04:00
Doug Hellmann
c78ae05005 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: I0fc887efcc4196dd3b371b2edae44075eb0b845e
Story: #2002586
Task: #24289
2018-08-31 08:57:42 -04:00
1348fd67e1 Update reno for stable/rocky
Change-Id: Ie70f71c1b702fd77039b4249c3ba562d0b813095
2018-08-08 22:09:57 +00:00
Zuul
7bf6f139cd Merge "Follow the new PTI for document build" 2018-08-08 13:52:36 +00:00
Zuul
9dfca5f854 Merge "fix error url" 2018-08-08 12:59:52 +00:00
François Magimel
b79833f67b 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#documentation
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

Update openstackdocstheme, sphinx and reno versions at the same time.

Change-Id: I8454b83f6ef200f8c5d34bee8568831b1e8fa15e
2018-07-27 23:06:10 +02:00
Doug Hellmann
f8d87cdb28 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.

Co-Authored-By: Nguyen Hai <nguyentrihai93@gmail.com>
Change-Id: I889acfe16c7175d14b8a15e55eb936a03e41e2fb
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2018-06-25 13:18:01 +09:00
Luka Peschke
d070f6a68c Rewrite of the client
The client has been completely rewritten in order to use cliff. The code
should be easier to maintain: authentication is now entirely handled by
keystoneauth, CloudKitty's client and CK's OSC plugin use the exact same
classes (no code duplication).

New features for users:

  * Client-side CSV report generation: It is possible for users to generate
    CSV reports with the new client. There is a default format, but reports
    may also be configured through a yaml config file. (see documentation)

  * The documentation has been improved. (A few examples on how to use the
    python library + complete API bindings and CLI reference).

  * It is now possible to use the client without Keystone authentication (this
    requires that CK's API is configured to use the noauth auth strategy).

  * Various features are brought by cliff: completion, command output formatting
    (table, shell, yaml, json...).

New features for developpers:

  * Python 2.7/3.5 compatible 'python-cloudkittyclient' module.

  * Integration tests (for 'openstack rating' and 'cloudkitty') have been
    added. These allow to create gate jobs running against a CK devstack

  * Tests are now ran with stestr instead of testr, which allows a better
    control over execution.

  * The dependency list has been reduced and upper constraints have been set.

Change-Id: I7c6afa46138d499b37b8be3d049b23ab5302a928
Task: 6589
Story: 2001614
2018-06-15 12:08:21 +02:00
melissaml
1b56089682 fix error url
Change-Id: Ia7125bc49449da29224021b615ffbc50e8ef37fd
2018-02-05 23:54:07 +08:00
f490bd0a84 Update reno for stable/queens
Change-Id: I597e1605c6c7e0320ed92cd9d6522e639ee0713b
2018-01-30 00:35:37 +00:00
Zuul
075cb4993d Merge "Delete outdated and unused directory" 2018-01-29 02:02:07 +00:00
Zuul
938697cb39 Merge "Remove log translations" 2018-01-26 13:55:16 +00:00
Zuul
7d3730cc77 Merge "Drop py34 target in tox.ini" 2018-01-26 13:23:13 +00:00
Zuul
8ba6712b74 Merge "Added release note for cloudkittyclient" 2018-01-26 13:22:08 +00:00
zhangguoqing
81bc4e3dce Added release note for cloudkittyclient
This adds the releasenotes directory to the python-cloudkittyclient repo.
It maintains the releasenotes for cloudkittyclient.

Change-Id: I31b310874c4cd0c26683c75c208edb607499d86b
2018-01-23 17:22:16 +00:00
lingyongxu
64129e43a1 Drop py34 target in tox.ini
We support py35 now.so it is no need to keep
the supoort for py34.

Change-Id: Ief8f6e8a383fd311c4bc4c552aabde4d80eb852b
2018-01-23 17:21:33 +00:00
Kiran_totad
bf30936048 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: Ic22cf715a70e9e1ff734944015d2f8a1ab2dbef8
2018-01-23 17:20:16 +00:00
Luka Peschke
b07b8c2360 Update the README
- Change the client version from 0.2 to 1.1.0

       - Add a link to PyPi

Change-Id: Iac050bea6b662e210493fc4af2dd0cda218ad777
2017-09-21 10:06:04 +02:00
Andreas Jaeger
87dbe6ea2e Use openstackdocstheme
Use the new theme for the docs.

This needs an update of requirements, I synced all requirements with
global requirements list.

Change-Id: I50c451501a8c428a174f477b89a2986f93adfcb1
2017-07-02 19:36:48 +02:00
Andreas Jaeger
f3451d8656 Import cli-reference from openstack-manuals
Change-Id: I32849dd61c0e5435000d78e01ec211bf9c47668e
2017-07-02 19:25:19 +02:00
Andreas Jaeger
34aef63639 Tread Sphinx warnings as errors
Set this to not introduce any warnings.

Change-Id: Ia17b24e612d155adb17df107135c3c44e457a9ae
2017-07-02 19:24:04 +02:00
Andreas Jaeger
7b5bb67733 rearrange existing docs to fit the new standard layout
Refer to
https://specs.openstack.org/openstack/docs-specs/specs/pike/os-manuals-migration.html
for details.

Change-Id: I10a07ae0b419730960d7d1013ace259dab7a2455
2017-07-02 19:23:58 +02:00
Jeremy Liu
cf2b4f31c7 [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.

Change-Id: I9177c69fd96ecacf164768b9e08f0e91d3a8690a
Partial-Bug: #1668848
2017-03-04 14:26:57 +00:00
HaiJieZhang
87dadb689c Delete outdated and unused directory
The file in "tools" directory is outdated and unused,
so delete it from repo.

Change-Id: I7faf4966f15b6f7757953eb758ad6dcec6c184e3
Closes-Bug: #1669024
2017-03-01 23:42:48 +08:00
Jenkins
2fe71f729a Merge "Add --all-tenants when get total/summary" 2017-01-26 16:42:43 +00:00
Jenkins
c6b90fc6e6 Merge "Update .gitignore" 2017-01-26 11:50:13 +00:00
Jenkins
d9aa46de34 Merge "Remove white space between print ()" 2017-01-26 11:46:07 +00:00
Aaron-DH
56e6f689bb Add --all-tenants when get total/summary
Use `cloudkitty total-get --all-tenants` to get total
rate of all_tenants. Same with summary-get

Depends-On: 8cf7332162ad30bcdb2c8dfd10a3d348601c2870
Change-Id: I1efcbb8eff77c5f8d358a02178b1f99204b6cba7
2017-01-24 11:05:54 +08:00
Jenkins
75755b2af3 Merge "Add oslo_debug_helper to tox.ini" 2017-01-23 08:03:37 +00:00
Jenkins
40ddf6a470 Merge "Delete unnecessary utf-8 coding" 2017-01-22 15:36:25 +00:00
Jenkins
287dd57185 Merge "Improve User experience" 2017-01-22 15:24:57 +00:00
Jenkins
416a717ebc Merge "Remove white space between print () in cliutils.py" 2017-01-19 09:57:32 +00:00
Maxime Cottret
efb2b0949a Improve User experience
This patch adds access to new REST API for config and service
metadata retrieval.

The following work has been done:

* Create new manager for config retrieval
* Create new Resource and CRUD manager for service info retrieval
* Add managers to client
* Add new CLI command and openstack client entries

Change-Id: I43f572202b1cd3832a820f46f7c7b44a0d998406
Depends-on: https://review.openstack.org/#/c/406180/
2017-01-18 14:48:17 +00:00
Jenkins
a567600b77 Merge "Add client for get summary report" 2017-01-18 14:32:18 +00:00
Jeremy Liu
d04e5ac776 Update .gitignore
These directories are generated by command `python setup.py install`,
better put them into .gitignore.

Change-Id: I63137da72f144556530677b43219aa0e10220b6e
2017-01-18 10:15:57 +08:00
Anh Tran
a1f45a0206 Remove white space between print ()
Change-Id: I464707dd23f006d790c618ef44e37bf6e6de9a22
2017-01-17 10:31:19 +07:00
Aaron-DH
2211d48f72 Add oslo_debug_helper to tox.ini
oslo_debug_helper is used to assist in debugging of python code,
i.e. by adding breakpoints via pdb.

To enable debugging, run tox with the debug environment:
tox -e debug <test_path>

Reference link:
http://docs.openstack.org/developer/oslotest/features.html#debugging-with-oslo-debug-helper

Change-Id: I01d66328a3529e542e503be2cd7bbca69a77c4aa
2017-01-16 19:19:52 +08:00
Aaron-DH
e402ce676c Add client for get summary report
Use commands as follows to get summary:
  -- cloudkitty summary-get
  -- openstack rating summary-get

Change-Id: I07da26cb31a03104493ab749efffd73ba8d17d62
Implements: blueprint price-groupby-fields
2017-01-16 18:59:38 +08:00
Jenkins
c423ca4470 Merge "Remove unused pylintrc" 2017-01-09 08:15:38 +00:00
Jenkins
f750d31169 Merge "H803 hacking have been deprecated" 2017-01-09 07:51:21 +00:00
zhangguoqing
cdbcafd142 Remove white space between print () in cliutils.py
There is a white space in line [print (*, then we remove it.

REF. https://review.openstack.org/#/c/387303/

Change-Id: I6762b7d1d9f573d62e7466da134addd51e0b4163
2017-01-04 02:33:38 +00:00
gecong1973
cffa8b8936 Delete unnecessary utf-8 coding
The file was added redundant utf-8 coding by some editor.
we can delete it .

Change-Id: I618e0c8f43717fd13db7b4d5213fa92a7b98a357
2016-12-27 10:08:37 +08:00
xhzhf
10f80c7d02 H803 hacking have been deprecated
H803 hacking have been removed.
https://github.com/openstack-dev/hacking/blob/master/setup.cfg
Closes-Bug: #1650741

Change-Id: Ibeeca8f56dd3fe872e1611bd4b61ec515670ec8d
2016-12-17 20:21:45 +08:00
Jenkins
87cdbb1340 Merge "Change the time args format from timestamp to date/time when total-get" 2016-12-15 12:19:18 +00:00
Jenkins
cede66dde6 Merge "Unify the date format "YYYY-MM-DDTHH:MM:SS"" 2016-12-15 12:18:18 +00:00
Jeremy Liu
addb3a8b33 Set upper-constraints for tox
Change-Id: Ia1dd9b902eae4d3ebdbd46802f7ba8043b3ee2a8
2016-12-14 11:00:05 +08:00
Luka Peschke
04bf3504ee Add support for OpenStack client
Cloudkittyclient now provides a plugin for the Openstack client.
setup.cfg was modified to provide entrypoints for the Openstack client. These
entrypoints can be found in the different shell_cli.py files.
Python-openstackclient was added to the requirements.

Implements: blueprint openstackclient-support
Change-Id: If0bbd919b1552b82cd77a52ded4f4ec32e6e14d8
2016-11-30 15:17:32 +01:00
Maxime Cottret
b6f7a7831f Add module priority management
This patch binds the python client to the module priority REST api.
It also adds a CLI setter command and updates the module list CLI
output to show current priority value.

Change-Id: I06ab6611452cdc6e875b5534cd955a0a3092ed0d
Implements: blueprint module-priority-cli-command
2016-11-29 14:59:35 +00:00
Jenkins
70cef21224 Merge "Use os_project name in get_client" 2016-11-29 14:10:12 +00:00
Luka Peschke
8fa848fb51 Use os_project name in get_client
The cloudkitty Client object cannot authenticate into keystone
if 'os_project_name' is passed as parameter instead of 'os_tenant_name'.
It is the same if 'os_project_id' is passed instead of 'os_tenant_id'.

This patch fixes both of these issues.

Change-Id: Ife248e87e1126d101be5e4550b933e66eccadbb9
2016-11-29 14:32:45 +01:00
zhangguoqing
d70f9c778c Unify the date format "YYYY-MM-DDTHH:MM:SS"
Change-Id: I729cef6f09d1e61f0c99faf87a87955e4bde3c13
2016-11-29 12:29:38 +00:00
Luka Peschke
ddf93becc1 Catches the right exceptions shell.py files
This is a fix for the bug #1645380. The right exceptions are caught and
'counter_name' is not used for the Exception messages anymore.

Change-Id: I242ee04783c5c8b2699ef3efe70f8e397eb794b8
Closes-Bug: 1645380
2016-11-29 12:33:13 +01:00
Jenkins
0024760bef Merge "Show team and repo badges on README" 2016-11-28 14:44:36 +00:00
Jenkins
5d43074cc4 Merge "Make begin and end optional when get dataframes" 2016-11-28 14:15:18 +00:00
Jenkins
d82273fcd4 Merge "Add __ne__ built-in function" 2016-11-26 05:21:25 +00:00
Flavio Percoco
241d90f541 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:

https://gist.github.com/168107e62fde067f3925d71a5a0f2d4d

Change-Id: I8caa8cdaa4efc2ea438b2ace632b62afd5d322e5
2016-11-25 17:25:39 +01:00
zhangguoqing
708aeff9b7 Change the time args format from timestamp to date/time when total-get
The begin and end args format is timestamp in total-get command, which
is less convenient than date/time format(YYYY-MM-DDTHH:MM:SS). So this
patch make it easy to use.

Change-Id: Id83e132e6b8a090d1abfa3002d53de1b678fc9f2
2016-11-24 14:53:06 +00:00
Jenkins
9a9399f284 Merge "py33 is no longer supported by Infra's CI" 2016-11-24 09:16:28 +00:00
gecong1973
a070f5b16c 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: Id2d32eedddbbbb91c6da6e36f12515972aa9e5a5
2016-11-23 17:09:15 +08:00
Jeremy Liu
9a1f531155 Remove unused pylintrc
We didn't use it in the gate and it's unmaintained. Clean that.

Change-Id: I5366927f2dd5771c2b8b0f9c68bf0201b8681776
2016-11-22 11:19:35 +08:00
Aaron-DH
d0f0aaf346 Make begin and end optional when get dataframes
when get dataframes with `cloudkitty storage-dataframe-list`,
begin and end is mandatory. make it optional like get total

Change-Id: I0a0f4073888621833b0bb6588a0452a642327797
Closes-Bug: #1640372
2016-11-09 14:51:37 +08:00
Steve Martinelli
a93f8b04b1 move old oslo-incubator code out of openstack/common
As part of the first community-wide goal, teams were asked
to remove the openstack/common package of their projects
if one existed. This was a byproduct of the old oslo-incubator
form of syncing common functionality.

The package, apiclient, was moved to a top level location
and cliutils was moved to the common module. There are no oslo
specific libraries, the recommended solution is to move it
in tree and maintain it there.

Change-Id: I0603d3c1419a5344bee8e43cfbe794c26641960a
2016-10-31 11:15:10 -04:00
Jenkins
e6daa3df81 Merge "Fix the logic of do_hashmap_mapping_list" 2016-09-01 16:25:53 +00:00
Jenkins
7a199820d0 Merge "Add short arg for storage command" 2016-09-01 16:14:36 +00:00
zhangguoqing
b24e261143 Fix the logic of do_hashmap_mapping_list
Only give the group_id should be allowed to do_hashmap_mapping_list,
and add the corresponding test case.

Depends-On: I4fe27a07e369728396d440b6b2f3462ee74d5f4d
Change-Id: Ia4272fff33b70db0dc24f7bf0a6d5971504cee7a
2016-09-01 16:01:11 +00:00
zhangguoqing
9acc36b4ca Add short arg for collector command
Change-Id: I92a9e03c6c8c517a78d03670ca19c3da54a4e3f2
2016-09-01 21:03:30 +08:00
Maxime Cottret
a6093c5a36 Fix CLI threshold command
- Threshold commands now use 'type' API field instead of 'map_type'
- change CLI option from "-m, --map-type" to "-t, --type" (same as mapping commands option)
- change short option for threshold-id to "-i"

Change-Id: I8c0f6b135bdc206ce1fc3ea14debd8d2cafc9ea7
Closes-Bug: #1619150
2016-09-01 11:15:56 +02:00
Jenkins
9554c9e440 Merge "Replaces client_kwargs by empty dict in ckclient/shell.py" 2016-08-29 10:20:59 +00:00
Luka Peschke
8318891835 Replaces client_kwargs by empty dict in ckclient/shell.py
ckclient.get_client() doesn't need the command-line args.

The client_kwargs.update() altered the args, and caused the project_id
field to be overwritten, leading to an invalid http request in
some cases.

Closes-Bug: #1616805
Change-Id: I09fe3bc3c71a399bdcfaaa178543a2516494399b
2016-08-25 15:54:57 +02:00
Maxime Cottret
b2a42f71fe Fix CLI auth user interface
This patch fixes how auth options are checked in CLI.

Use either:
- tenant-id or tenant-name
- project-id and user-domain (id or name)
- project-name and project-domain (id or name) and user-domain (id or name)

For consistency, the same checking is used in the client authentication plugin.

Change-Id: I2210d8bf21bba5d1faf72dfbe38756078d8bc0c1
Closes-Bug: #1616468
2016-08-25 10:11:09 +02:00
zhangguoqing
d4ae928048 Add short arg for storage command
Change-Id: I6c29171d66527fe4284c0ce7b2e9fb2e17a7d49f
2016-08-10 10:45:42 +00:00
Jenkins
1d8378cc6c Merge "Removes MANIFEST.in as it is not needed explicitely by PBR" 2016-08-08 12:48:45 +00:00
Jenkins
44a2bb0b26 Merge "Support getting client with keystone session" 2016-08-08 12:48:40 +00:00
Jenkins
0417e30f76 Merge "Remove discover from test-requirements" 2016-08-08 12:43:23 +00:00
zhangguoqing
a4dd6e1a61 [trivial] fix wrong typo
group_id --> tenant_id

Change-Id: Iaf3d3b4f83ae7007d3bda1f9adade881a298c7ec
2016-08-06 05:15:01 +00:00
Jenkins
d503c98cbe Merge "Fix client V3 unscope bug" 2016-08-02 13:16:16 +00:00
Swapnil Kulkarni (coolsvap)
0efe6a0606 Remove discover from test-requirements
It's only needed for python < 2.7 which is not supported

Change-Id: Ic92b43c09a0f8e4892b4fdd2b91c00e839e7adec
2016-07-22 03:52:26 +00:00
Jenkins
69399b5e5d Merge "Add client support for per tenant hashmap rules" 2016-06-30 16:01:19 +00:00
Jenkins
df5087a344 Merge "[Trivial] Remove executable privilege of doc/source/conf.py" 2016-06-23 15:34:04 +00:00
Stéphane Albert
70771bf2d7 Add client support for per tenant hashmap rules
Change-Id: Id85a0de7115439131cef4d1a98f884c2334fc474
2016-06-15 04:47:12 +00:00
Gauvain Pocentek
367608ceeb Remove spec file since cloudkittyclient is in RDO
Change-Id: I7cfa01daf2a6aa64bbf9afb89e817ff9d3942c71
2016-06-15 06:46:13 +02:00
Gauvain Pocentek
92981a7bb4 Add an explicit dependency on prettytable
Update the requirements for neutron

Change-Id: I0237bc725c6ab1948b40218a01434440dd173e8c
2016-06-14 16:24:59 +02:00
ZhiQiang Fan
5faa8b0de6 [Trivial] Remove executable privilege of doc/source/conf.py
It is a configuration file, rather than a script.

Change-Id: I133c05be7de743d2a89a69ef100ebe5d43422cd9
2016-04-29 20:19:29 +08:00
Stéphane Albert
55f3a3fa75 Updated requirements for mitaka
Preparing mitaka release

Change-Id: Ida7ddf7fcf70f5b9dba91b9c37b4d30581531f32
2016-03-04 15:14:46 +01:00
Jenkins
336f1466e0 Merge "cloudkittyclient with keystone v3 not working" 2016-03-04 14:08:52 +00:00
Jenkins
5b6fd6c529 Merge "Add support for query cost by service" 2016-03-04 11:04:01 +00:00
Jenkins
778782c3c9 Merge "Fix argument order for assertEqual to (expected, observed)" 2016-03-04 10:20:04 +00:00
Jenkins
985e3c2304 Merge "Update requirements" 2016-03-04 10:11:43 +00:00
Pierre-Alexandre Bardina
0cad4b0e99 Update requirements
Update requirements for liberty

Change-Id: I01489c2bc79428174a1005cf45a0803f6dcedfd6
2016-03-04 11:02:42 +01:00
reedip
1703d5538b Fix argument order for assertEqual to (expected, observed)
assertEqual expects that the arguments provided to it should be (expected, observed).
If a particluar order is kept as a convention, then it helps to provide a cleaner
message to the developer if Unit Tests fail.
The following patch fixes this issue

TrivialFix

Change-Id: Id417fb43ecd62563239d492bff3981277565525e
Closes-Bug: #1259292
2016-03-03 22:02:06 +00:00
Aaron-DH
c6e23ab770 Add support for query cost by service
Query cost of each service by using total-get -s servicetype

Change-Id: I7f579c70fe78cbd4031aa6ec20279d7661a2d67c
Closes-Bug: #1549687
2016-02-25 17:24:24 +08:00
Xiangjun Li
450aa61358 cloudkittyclient with keystone v3 not working
cloudkittyclient is failing to pass some domain/project related
information to keystoneclient, which caused "The service catalog
is empty" and "Expecting to find domain in project" error when
executing cloudkittyclient shell.

Change-Id: I386f4ecb38b947a1d8a0c8f1eee72e25ee12771a
Closes-Bug: #1547778
2016-02-20 15:10:44 +08:00
Chaozhe.Chen
066c9564fb Fix client V3 unscope bug
Client V3 will get unscope auth as no project provided. Unscope auth
will make client get empty catalog back from keystone.

This backport from ceilometer-client patch[1] and I verified it in my
devstack.

[1]https://review.openstack.org/#/c/169409/

Change-Id: I1fa5a5b1e9a40501dfbd563bf608d41eb4879bf8
Closes-Bug: #1522728
2016-02-16 17:02:11 +08:00
Jenkins
6abecf6348 Merge "Add helpinfo for collector commands." 2016-01-11 11:04:16 +00:00
Jenkins
a63b75c555 Merge "Drop py33 support" 2016-01-11 10:58:51 +00:00
Adam
f8c4caba43 Add helpinfo for collector commands.
Add some helpinfo for subcommand collector*, report*.

Change-Id: Ica1ad18fcaa4368a5d5a953839ab4499db034def
2016-01-10 14:33:21 +08:00
Jenkins
c6f2cb4643 Merge "Fix name not defined error" 2016-01-04 09:31:39 +00:00
Aaron-DH
5c188a2306 Fix name not defined error
Add the missing import packages and format the log messages
Move i18n to package(cloudkittyclient)

Change-Id: I77e7059e8eb91aef131713f0720f58d23ae7c11f
Closes-Bug: #1524680
2016-01-02 20:55:12 +08:00
Jenkins
9dd1a6fbea Merge "Set AuthPlugin in __init__()" 2015-12-31 12:14:02 +00:00
janonymous
0de831021d py33 is no longer supported by Infra's CI
Python 3.3 support would be dropped by
Infra team from mitaka,CI would no longer be testing it,
so projects should drop it also.

Change-Id: Ic03ded9bba499858a77debbcdc4fc9c9d963dd24
2015-12-26 15:47:45 +05:30
sonu.kumar
4dc3e65e44 Removes MANIFEST.in as it is not needed explicitely 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: Iddbd1a4b574223d840707351c5e8f025d56f2046
2015-12-17 14:45:11 +05:30
shu-mutou
8b69ecf237 Drop py33 support
"Python 3.3 support is being dropped since OpenStack Liberty."
written in following URL.
https://wiki.openstack.org/wiki/Python3

And already the infra team and the oslo team are dropping py33
support from their projects.

Since we rely on oslo for a lot of our work, and depend on infra
for our CI, we should drop py33 support too.

Change-Id: I3aa4c969425d885873be222c0ea4e32cb1060341
Closes-Bug: #1526170
2015-12-15 18:52:50 +09:00
Jenkins
ea21f9761b Merge "Fixed bug with report total" 2015-12-10 07:50:17 +00:00
Chaozhe.Chen
df4e8360e2 Support getting client with keystone session
This change will allow cloudkitty client to use keystoneclient/
keystoneauth session object.

Change-Id: Icc4bf5da12fb24d189fc38daf1b5cfb4a43228aa
2015-12-10 01:30:50 +08:00
Chaozhe.Chen
236bf8b307 Set AuthPlugin in __init__()
self.auth_plugin should be set in __init__()

Change-Id: Ib23fd14a697e4a03acd8c62cf1b09670d169a115
2015-12-03 14:47:39 +08:00
Atsushi SAKAI
6dbfc4502e Fix help message
Fix Required to small case(required)
Add period.

This fix is coming from below patch set 1 comment.
https://review.openstack.org/#/c/251331/

Change-Id: I614a8143ed6cba37dc726f3c85606daaf6a767be
2015-12-01 12:24:30 +09:00
Stéphane Albert
def167f77a Fixed bug with report total
The tenant filter was always sent even if not tenant filtering was used
for total retrieving.

Change-Id: I55565a30389b94f559e16d349d6aa3ef56053ea2
Closes-Bug: #1516484
2015-11-25 15:04:41 +01:00
Jenkins
4fe0255682 Merge "Add common arguments" 2015-11-25 12:24:05 +00:00
Chaozhe.Chen
e4623d6663 Fix a typo in command help
Change-Id: Ib0b00cae66907bffddbbd28f6d77ea952ec08508
2015-11-25 17:08:05 +08:00
chenchaozhe1988
382a2d9565 Add common arguments
Merge same arguments in common arguments to make it concise and convenient.

Change-Id: I75e246d36ed7d38858e9dfdedcc77dd19ea587d5
2015-11-18 16:03:05 +08:00
Gauvain Pocentek
9428ab38aa Do no set the version in setup.cfg
Change-Id: I670e61c94f6f58cd5b31caa220e94f6a30bfb66c
2015-10-30 11:33:41 +09:00
Jenkins
1e095e6da9 Merge "Improve HashMap client" 2015-10-22 12:08:17 +00:00
Jenkins
621c06f8af Merge "Add support for PyScripts rating module" 2015-10-22 04:29:02 +00:00
Stéphane Albert
e4df2e2105 Improve HashMap client
Modified tests to handle new functions.
Refactored tests to ease maintenance.

Change-Id: I24d74e0e9983091d4f81a3f72604fbae22476505
2015-10-21 12:20:51 +02:00
Jeremy Stanley
50f6cb3e54 Update .gitreview for new namespace
Change-Id: I1de00621c99bbaa2f785c50f849b635456167539
2015-10-17 22:36:22 +00:00
Stéphane Albert
d9d61d7727 Add support for PyScripts rating module
Change-Id: I06270893460f76fa73616197783b5e2d48702fe9
2015-10-05 17:43:24 +02:00
Monty Taylor
bf03f8fae6 Change ignore-errors to ignore_errors
Needed for coverage 4.0

Change-Id: I5b11e3f02107759d178c2d292a8eb03827924102
2015-09-23 07:55:26 +00:00
Stéphane Albert
b17283d585 Moving to Liberty cycle (0.5)
Change-Id: Ie18aea965350353624e59bfb769059f7af3c9278
2015-09-22 16:04:28 +02:00
Stéphane Albert
0696510d29 Preparing release 0.4.1
Change-Id: I549c749bcef38d997ec7a49da7478c90655066b4
2015-08-27 16:26:53 +02:00
DeliangFan
4ee7dcb3ee Fix syntax error of shell description
Correct the word mapping.

Change-Id: Ibc8c4afff846f9f88374d99452619522ccc080b0
Closes-Bug: #1481234
2015-08-04 17:07:02 +08:00
Jenkins
5efcd30fdd Merge "Transitioned collector client to new API" 2015-08-01 03:59:40 +00:00
Gauvain Pocentek
52955c5749 setup.cfg: set a version
Change-Id: Ib7b8722424a6e983c42417e00efbe6dba7754557
2015-07-31 15:19:39 +02:00
Stéphane Albert
2ccbed9139 Transitioned collector client to new API
Change-Id: I7f52c288f569c59381a3324714b3b1c6ac8be58a
2015-07-31 09:56:27 +02:00
176 changed files with 7148 additions and 5472 deletions

View File

@@ -3,10 +3,7 @@ branch = True
source = cloudkittyclient
omit =
cloudkittyclient/tests/*,
cloudkittyclient/openstack/*,
cloudkittyclient/i18n.py,
cloudkittyclient/common/client.py,
cloudkittyclient/common/exceptions.py
cloudkittyclient/i18n.py
[report]
ignore-errors = True
ignore_errors = True

6
.gitignore vendored
View File

@@ -7,7 +7,13 @@ build
.tox
cover
.testrepository
.stestr
.venv
dist
*.egg
*.sw?
.eggs
AUTHORS
ChangeLog
releasenotes/build
.idea/

View File

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

View File

@@ -1,19 +0,0 @@
[MASTER]
ignore=openstack,test
[MESSAGES CONTROL]
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622
[BASIC]
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
[Variables]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
# _ is used by our localization
additional-builtins=_

3
.stestr.conf Normal file
View File

@@ -0,0 +1,3 @@
[DEFAULT]
test_path=./cloudkittyclient/tests/unit
top_dir=./

View File

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

63
.zuul.yaml Normal file
View File

@@ -0,0 +1,63 @@
- job:
name: cloudkittyclient-devstack-functional-base
parent: devstack
description: |
Job for cloudkittyclient functional tests
pre-run: playbooks/cloudkittyclient-devstack-functional/pre.yaml
run: playbooks/cloudkittyclient-devstack-functional/run.yaml
post-run: playbooks/cloudkittyclient-devstack-functional/post.yaml
required-projects:
- name: openstack/ceilometer
- name: openstack/cloudkitty
- name: openstack/python-cloudkittyclient
roles:
- zuul: openstack-infra/devstack
timeout: 5400
irrelevant-files:
- ^.*\.rst$
- ^doc/.*$
- ^releasenotes/.*$
vars:
devstack_plugins:
ceilometer: https://opendev.org/openstack/ceilometer
cloudkitty: https://opendev.org/openstack/cloudkitty
devstack_localrc:
CLOUDKITTY_FETCHER: keystone
devstack_services:
ck-api: true
horizon: false
tox_install_siblings: false
zuul_work_dir: src/opendev.org/openstack/python-cloudkittyclient
- job:
name: cloudkittyclient-devstack-functional-v1-client
parent: cloudkittyclient-devstack-functional-base
vars:
tox_envlist: functional-v1
- job:
name: cloudkittyclient-devstack-functional-v2-client
parent: cloudkittyclient-devstack-functional-base
vars:
tox_envlist: functional-v2
- project:
templates:
- check-requirements
- openstack-cover-jobs
- openstack-python3-jobs
- openstackclient-plugin-jobs
- publish-openstack-docs-pti
- release-notes-jobs-python3
check:
jobs:
- cloudkittyclient-devstack-functional-v1-client:
voting: true
- cloudkittyclient-devstack-functional-v2-client:
voting: true
gate:
jobs:
- cloudkittyclient-devstack-functional-v1-client:
voting: true
- cloudkittyclient-devstack-functional-v2-client:
voting: true

View File

@@ -1,16 +1,19 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in this page:
The source repository for this project can be found at:
https://opendev.org/openstack/python-cloudkittyclient
http://docs.openstack.org/infra/manual/developers.html
Pull requests submitted through GitHub are not monitored.
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
To start contributing to OpenStack, follow the steps in the contribution guide
to set up and use Gerrit:
http://docs.openstack.org/infra/manual/developers.html#development-workflow
https://docs.openstack.org/contributors/code-and-documentation/quick-start.html
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Storyboard:
Bugs should be filed on Launchpad, not GitHub:
https://storyboard.openstack.org/#!/project/895
https://bugs.launchpad.net/cloudkitty
For more specific information about contributing to this repository, see the
python-cloudkittyclient contributor guide:
https://docs.openstack.org/python-cloudkittyclient/latest/contributor/contributing.html

View File

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

View File

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

View File

@@ -1,23 +1,16 @@
Python bindings to the CloudKitty API
=====================================
================
CloudKittyClient
================
:version: 0.2
:Wiki: `CloudKitty Wiki`_
:IRC: #cloudkitty @ freenode
.. image:: https://governance.openstack.org/badges/python-cloudkittyclient.svg
:target: https://governance.openstack.org/reference/tags/index.html
This is a client for CloudKitty_. It provides a Python api (the
``cloudkittyclient`` module), a command-line script (``cloudkitty``), and an
`OpenStack Client`_ extension (``openstack rating``).
.. _CloudKitty Wiki: https://wiki.openstack.org/wiki/CloudKitty
python-cloudkittyclient
=======================
This is a client library for CloudKitty built on the CloudKitty API. It
provides a Python API (the ``cloudkittyclient`` module).
Status
======
This project is **highly** work in progress.
The client is available on PyPi_.
.. _OpenStack Client: https://docs.openstack.org/python-openstackclient/latest/
.. _CloudKitty: https://github.com/openstack/cloudkitty
.. _PyPi: https://pypi.org/project/python-cloudkittyclient/

View File

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

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Objectif Libre
# Licensed under the Apache License, Version 2.0 (the "License"); you may

47
cloudkittyclient/auth.py Normal file
View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from keystoneauth1 import loading
from keystoneauth1 import plugin
class CloudKittyNoAuthPlugin(plugin.BaseAuthPlugin):
"""No authentication plugin for CloudKitty
"""
def __init__(self, endpoint='http://localhost:8889', *args, **kwargs):
super(CloudKittyNoAuthPlugin, self).__init__()
self._endpoint = endpoint
def get_auth_ref(self, session, **kwargs):
return None
def get_endpoint(self, session, **kwargs):
return self._endpoint
def get_headers(self, session, **kwargs):
return {}
class CloudKittyNoAuthLoader(loading.BaseLoader):
plugin_class = CloudKittyNoAuthPlugin
def get_options(self):
options = super(CloudKittyNoAuthLoader, self).get_options()
options.extend([
loading.Opt('endpoint', help='CloudKitty Endpoint',
required=True, default='http://localhost:8889'),
])
return options

View File

@@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
@@ -9,308 +12,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import discover
from keystoneclient import exceptions as ks_exc
from keystoneclient import session
from oslo_utils import strutils
import six.moves.urllib.parse as urlparse
from cloudkittyclient.common import utils
from cloudkittyclient import exc
from cloudkittyclient.openstack.common.apiclient import auth
from cloudkittyclient.openstack.common.apiclient import exceptions
def _discover_auth_versions(session, auth_url):
# discover the API versions the server is supporting based on the
# given URL
v2_auth_url = None
v3_auth_url = None
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except ks_exc.DiscoveryFailure:
raise
except exceptions.ClientException:
# Identity service may not support discovery. In that case,
# try to determine version from auth_url
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
v3_auth_url = auth_url
elif path.startswith('/v2'):
v2_auth_url = auth_url
else:
raise exc.CommandError('Unable to determine the Keystone '
'version to authenticate with '
'using the given auth_url.')
return v2_auth_url, v3_auth_url
def _get_keystone_session(**kwargs):
# TODO(fabgia): the heavy lifting here should be really done by Keystone.
# Unfortunately Keystone does not support a richer method to perform
# discovery and return a single viable URL. A bug against Keystone has
# been filed: https://bugs.launchpad.net/python-keystoneclient/+bug/1330677
# first create a Keystone session
cacert = kwargs.pop('cacert', None)
cert = kwargs.pop('cert', None)
key = kwargs.pop('key', None)
insecure = kwargs.pop('insecure', False)
auth_url = kwargs.pop('auth_url', None)
project_id = kwargs.pop('project_id', None)
project_name = kwargs.pop('project_name', None)
if insecure:
verify = False
else:
verify = cacert or True
if cert and key:
# passing cert and key together is deprecated in favour of the
# requests lib form of having the cert and key as a tuple
cert = (cert, key)
# create the keystone client session
ks_session = session.Session(verify=verify, cert=cert)
v2_auth_url, v3_auth_url = _discover_auth_versions(ks_session, auth_url)
username = kwargs.pop('username', None)
user_id = kwargs.pop('user_id', None)
user_domain_name = kwargs.pop('user_domain_name', None)
user_domain_id = kwargs.pop('user_domain_id', None)
project_domain_name = kwargs.pop('project_domain_name', None)
project_domain_id = kwargs.pop('project_domain_id', None)
auth = None
use_domain = (user_domain_id or user_domain_name or
project_domain_id or project_domain_name)
use_v3 = v3_auth_url and (use_domain or (not v2_auth_url))
use_v2 = v2_auth_url and not use_domain
if use_v3:
# the auth_url as v3 specified
# e.g. http://no.where:5000/v3
# Keystone will return only v3 as viable option
auth = v3_auth.Password(
v3_auth_url,
username=username,
password=kwargs.pop('password', None),
user_id=user_id,
user_domain_name=user_domain_name,
user_domain_id=user_domain_id,
project_domain_name=project_domain_name,
project_domain_id=project_domain_id)
elif use_v2:
# the auth_url as v2 specified
# e.g. http://no.where:5000/v2.0
# Keystone will return only v2 as viable option
auth = v2_auth.Password(
v2_auth_url,
username,
kwargs.pop('password', None),
tenant_id=project_id,
tenant_name=project_name)
else:
raise exc.CommandError('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url.')
ks_session.auth = auth
return ks_session
def _get_endpoint(ks_session, **kwargs):
"""Get an endpoint using the provided keystone session."""
# set service specific endpoint types
endpoint_type = kwargs.get('endpoint_type') or 'publicURL'
service_type = kwargs.get('service_type') or 'rating'
endpoint = ks_session.get_endpoint(service_type=service_type,
interface=endpoint_type,
region_name=kwargs.get('region_name'))
return endpoint
class AuthPlugin(auth.BaseAuthPlugin):
opt_names = ['tenant_id', 'region_name', 'auth_token',
'service_type', 'endpoint_type', 'cacert',
'auth_url', 'insecure', 'cert_file', 'key_file',
'cert', 'key', 'tenant_name', 'project_name',
'project_id', 'user_domain_id', 'user_domain_name',
'password', 'username', 'endpoint']
def __init__(self, auth_system=None, **kwargs):
self.opt_names.extend(self.common_opt_names)
super(AuthPlugin, self).__init__(auth_system, **kwargs)
def _do_authenticate(self, http_client):
token = self.opts.get('token') or self.opts.get('auth_token')
endpoint = self.opts.get('endpoint')
if not (token and endpoint):
project_id = (self.opts.get('project_id') or
self.opts.get('tenant_id'))
project_name = (self.opts.get('project_name') or
self.opts.get('tenant_name'))
ks_kwargs = {
'username': self.opts.get('username'),
'password': self.opts.get('password'),
'user_id': self.opts.get('user_id'),
'user_domain_id': self.opts.get('user_domain_id'),
'user_domain_name': self.opts.get('user_domain_name'),
'project_id': project_id,
'project_name': project_name,
'project_domain_name': self.opts.get('project_domain_name'),
'project_domain_id': self.opts.get('project_domain_id'),
'auth_url': self.opts.get('auth_url'),
'cacert': self.opts.get('cacert'),
'cert': self.opts.get('cert'),
'key': self.opts.get('key'),
'insecure': strutils.bool_from_string(
self.opts.get('insecure')),
'endpoint_type': self.opts.get('endpoint_type'),
}
# retrieve session
ks_session = _get_keystone_session(**ks_kwargs)
token = lambda: ks_session.get_token()
endpoint = (self.opts.get('endpoint') or
_get_endpoint(ks_session, **ks_kwargs))
self.opts['token'] = token
self.opts['endpoint'] = endpoint
def token_and_endpoint(self, endpoint_type, service_type):
token = self.opts.get('token')
if callable(token):
token = token()
return token, self.opts.get('endpoint')
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
has_token = self.opts.get('token') or self.opts.get('auth_token')
no_auth = has_token and self.opts.get('endpoint')
has_tenant = self.opts.get('tenant_id') or self.opts.get('tenant_name')
has_credential = (self.opts.get('username') and has_tenant
and self.opts.get('password')
and self.opts.get('auth_url'))
missing = not (no_auth or has_credential)
if missing:
missing_opts = []
opts = ['token', 'endpoint', 'username', 'password', 'auth_url',
'tenant_id', 'tenant_name']
for opt in opts:
if not self.opts.get(opt):
missing_opts.append(opt)
raise exceptions.AuthPluginOptionsMissing(missing_opts)
#
import sys
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
kwargs['token'] = kwargs.get('token') or kwargs.get('auth_token')
module = 'cloudkittyclient.v%s.client' % version
__import__(module)
client_class = getattr(sys.modules[module], 'Client')
return client_class(*args, **kwargs)
def _adjust_params(kwargs):
timeout = kwargs.get('timeout')
if timeout is not None:
timeout = int(timeout)
if timeout <= 0:
timeout = None
insecure = strutils.bool_from_string(kwargs.get('insecure'))
verify = kwargs.get('verify')
if verify is None:
if insecure:
verify = False
else:
verify = kwargs.get('cacert') or True
cert = kwargs.get('cert_file')
key = kwargs.get('key_file')
if cert and key:
cert = cert, key
return {'verify': verify, 'cert': cert, 'timeout': timeout}
def get_client(version, **kwargs):
"""Get an authenticated client, based on the credentials in the kwargs.
:param api_version: the API version to use ('1')
:param kwargs: keyword args containing credentials, either:
* os_token: pre-existing token to re-use
* os_endpoint: Cloudkitty API endpoint
or:
* os_username: name of user
* os_password: user's password
* os_user_id: user's id
* os_user_domain_id: the domain id of the user
* os_user_domain_name: the domain name of the user
* os_project_id: the user project id
* os_tenant_id: V2 alternative to os_project_id
* os_project_name: the user project name
* os_tenant_name: V2 alternative to os_project_name
* os_project_domain_name: domain name for the user project
* os_project_domain_id: domain id for the user project
* os_auth_url: endpoint to authenticate against
* os_cert|os_cacert: path of CA TLS certificate
* os_key: SSL private key
* insecure: allow insecure SSL (no cert verification)
"""
endpoint = kwargs.get('os_endpoint')
cli_kwargs = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'tenant_id': kwargs.get('os_tenant_id'),
'tenant_name': kwargs.get('os_tenant_name'),
'auth_url': kwargs.get('os_auth_url'),
'region_name': kwargs.get('os_region_name'),
'service_type': kwargs.get('os_service_type'),
'endpoint_type': kwargs.get('os_endpoint_type'),
'cacert': kwargs.get('os_cacert'),
'cert_file': kwargs.get('os_cert'),
'key_file': kwargs.get('os_key'),
'token': kwargs.get('os_token') or kwargs.get('os_auth_token'),
'user_domain_name': kwargs.get('os_user_domain_name'),
'user_domain_id': kwargs.get('os_user_domain_id'),
'project_domain_name': kwargs.get('os_project_domain_name'),
'project_domain_id': kwargs.get('os_project_domain_id'),
}
cli_kwargs.update(kwargs)
cli_kwargs.update(_adjust_params(cli_kwargs))
return Client(version, endpoint, **cli_kwargs)
def get_auth_plugin(endpoint, **kwargs):
auth_plugin = AuthPlugin(
auth_url=kwargs.get('auth_url'),
service_type=kwargs.get('service_type'),
token=kwargs.get('token'),
endpoint_type=kwargs.get('endpoint_type'),
cacert=kwargs.get('cacert'),
tenant_id=kwargs.get('project_id') or kwargs.get('tenant_id'),
endpoint=endpoint,
username=kwargs.get('username'),
password=kwargs.get('password'),
tenant_name=kwargs.get('tenant_name'),
user_domain_name=kwargs.get('user_domain_name'),
user_domain_id=kwargs.get('user_domain_id'),
project_domain_name=kwargs.get('project_domain_name'),
project_domain_id=kwargs.get('project_domain_id')
)
return auth_plugin

View File

@@ -1,6 +1,5 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2015 Objectif Libre
# All Rights Reserved.
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -14,160 +13,63 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
from string import Formatter as StringFormatter
from urllib.parse import urlencode
import copy
from six.moves.urllib import parse
from cloudkittyclient import exc
from cloudkittyclient.openstack.common.apiclient import base
from cloudkittyclient import utils
def getid(obj):
"""Extracts object ID.
class HttpDecoratorMeta(type):
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
ignore = ('get_url', )
def __new__(cls, *args, **kwargs):
return utils.format_http_errors(HttpDecoratorMeta.ignore)(
super(HttpDecoratorMeta, cls).__new__(cls, *args, **kwargs)
)
class Manager(object):
"""Managers interact with a particular type of API.
class BaseManager(object, metaclass=HttpDecoratorMeta):
"""Base class for Endpoint Manager objects."""
It works with samples, meters, alarms, etc. and provide CRUD operations for
them.
"""
resource_class = None
url = ''
def __init__(self, api):
self.api = api
def __init__(self, api_client):
self.api_client = api_client
self._formatter = StringFormatter()
@property
def client(self):
"""Compatible with latest oslo-incubator.apiclient code."""
return self.api
def _get_format_kwargs(self, **kwargs):
it = self._formatter.parse(self.url)
output = {i[1]: '' for i in it}
for key in output.keys():
if kwargs.get(key):
output[key] = kwargs[key]
if 'endpoint' in output.keys():
output.pop('endpoint')
return output
def _create(self, url, body):
body = self.api.post(url, json=body).json()
if body:
return self.resource_class(self, body)
def get_url(self,
endpoint,
kwargs,
authorized_args=[]):
"""Returns the required url for a request against CloudKitty's API.
def _list(self, url, response_key=None, obj_class=None, body=None,
expect_single=False):
resp = self.api.get(url)
if not resp.content:
raise exc.HTTPNotFound
body = resp.json()
if obj_class is None:
obj_class = self.resource_class
if response_key:
try:
data = body[response_key]
except KeyError:
return []
else:
data = body
if expect_single:
data = [data]
return [obj_class(self, res, loaded=True) for res in data if res]
def _update(self, url, item, response_key=None):
if not item.dirty_fields:
return item
item = self.api.put(url, json=item.dirty_fields).json()
# PUT requests may not return a item
if item:
return self.resource_class(self, item)
def _delete(self, url):
self.api.delete(url)
class CrudManager(base.CrudManager):
"""A CrudManager that automatically gets its base URL."""
base_url = None
def build_url(self, base_url=None, **kwargs):
base_url = base_url or self.base_url
return super(CrudManager, self).build_url(base_url, **kwargs)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs))
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs), kwargs)
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
return self._put(
self.build_url(**kwargs), params)
def findall(self, base_url=None, **kwargs):
"""Find multiple items with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
:param endpoint: The endpoint on which the request should be done
:type endpoint: str
:param kwargs: kwargs that will be used to build the query (part after
'?' in the url) and to format the url.
:type kwargs: dict
:param authorized_args: The arguments that are authorized in url
parameters
:type authorized_args: list
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exc.NotFound(404, msg)
return rl
class Resource(base.Resource):
"""A resource represents a particular instance of an object.
Resource might be tenant, user, etc.
This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
key = None
def to_dict(self):
return copy.deepcopy(self._info)
@property
def dirty_fields(self):
out = self.to_dict()
for k, v in self._info.items():
if self.__dict__[k] != v:
out[k] = self.__dict__[k]
return out
def update(self):
try:
return self.manager.update(**self.dirty_fields)
except AttributeError:
raise exc.NotUpdatableError(self)
query_kwargs = {
key: kwargs[key] for key in authorized_args
if kwargs.get(key, None)
}
kwargs = self._get_format_kwargs(**kwargs)
url = self.url.format(endpoint=endpoint, **kwargs)
query = urlencode(query_kwargs)
if query:
url += '?' + query
return url

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from keystoneauth1 import adapter
from keystoneauth1 import session as ks_session
class BaseClient(object):
def __init__(self,
session=None,
adapter_options={},
cacert=None,
insecure=False,
**kwargs):
adapter_options.setdefault('service_type', 'rating')
adapter_options.setdefault('additional_headers', {
'Content-Type': 'application/json',
})
if insecure:
verify_cert = False
else:
if cacert:
verify_cert = cacert
else:
verify_cert = True
self.session = session
if self.session is None:
self.session = ks_session.Session(
verify=verify_cert, **kwargs)
self.api_client = adapter.Adapter(
session=self.session, **adapter_options)

View File

@@ -1,211 +0,0 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import datetime
import sys
import textwrap
import uuid
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_utils import importutils
import prettytable
import six
from cloudkittyclient import exc
from cloudkittyclient.openstack.common import cliutils
def import_versioned_module(version, submodule=None):
module = 'cloudkittyclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
if 'help' in kwargs:
if 'default' in kwargs:
kwargs['help'] += " Defaults to %s." % kwargs['default']
required = kwargs.get('required', False)
if required:
kwargs['help'] += " Required."
# Because of the sematics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
def _make_default_formatter(field):
return lambda o: getattr(o, field, '')
new_formatters = {}
for field, field_label in six.moves.zip(fields, field_labels):
if field in formatters:
new_formatters[field_label] = formatters[field]
else:
new_formatters[field_label] = _make_default_formatter(field)
cliutils.print_list(objs, field_labels,
formatters=new_formatters,
sortby_index=sortby)
def nested_list_of_dict_formatter(field, column_names):
# (TMaddox) Because the formatting scheme actually drops the whole object
# into the formatter, rather than just the specified field, we have to
# extract it and then pass the value.
return lambda o: format_nested_list_of_dict(getattr(o, field),
column_names)
def format_nested_list_of_dict(l, column_names):
pt = prettytable.PrettyTable(caching=False, print_empty=False,
header=True, hrules=prettytable.FRAME,
field_names=column_names)
for d in l:
pt.add_row(list(map(lambda k: d[k], column_names)))
return pt.get_string()
def print_dict(d, dict_property="Property", wrap=0):
pt = prettytable.PrettyTable([dict_property, 'Value'], print_empty=False)
pt.align = 'l'
for k, v in sorted(six.iteritems(d)):
# convert dict to str to check length
if isinstance(v, dict):
v = jsonutils.dumps(v)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
if wrap > 0:
line = textwrap.fill(str(line), wrap)
pt.add_row([col1, line])
col1 = ''
else:
if wrap > 0:
v = textwrap.fill(str(v), wrap)
pt.add_row([k, v])
encoded = encodeutils.safe_encode(pt.get_string())
# FIXME(gordc): https://bugs.launchpad.net/oslo-incubator/+bug/1370710
if six.PY3:
encoded = encoded.decode()
print(encoded)
def find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
except exc.HTTPNotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id)
except (ValueError, exc.HTTPNotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exc.HTTPNotFound:
msg = ("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))
raise exc.CommandError(msg)
def args_array_to_dict(kwargs, key_to_convert):
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = dict(v.split("=", 1)
for v in values_to_convert)
except ValueError:
raise exc.CommandError(
'%s must be a list of key=value not "%s"' % (
key_to_convert, values_to_convert))
return kwargs
def args_array_to_list_of_dicts(kwargs, key_to_convert):
"""Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}]."""
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = []
for lst in values_to_convert:
pairs = lst.split(";")
dct = dict()
for pair in pairs:
kv = pair.split("=", 1)
dct[kv[0]] = kv[1].strip(" \"'") # strip spaces and quotes
kwargs[key_to_convert].append(dct)
except Exception:
raise exc.CommandError(
'%s must be a list of key1=value1;key2=value2;... not "%s"' % (
key_to_convert, values_to_convert))
return kwargs
def key_with_slash_to_nested_dict(kwargs):
nested_kwargs = {}
for k in list(kwargs):
keys = k.split('/', 1)
if len(keys) == 2:
nested_kwargs.setdefault(keys[0], {})[keys[1]] = kwargs[k]
del kwargs[k]
kwargs.update(nested_kwargs)
return kwargs
def merge_nested_dict(dest, source, depth=0):
for (key, value) in six.iteritems(source):
if isinstance(value, dict) and depth:
merge_nested_dict(dest[key], value,
depth=(depth - 1))
else:
dest[key] = value
def ts2dt(timestamp):
"""timestamp to datetime format."""
if not isinstance(timestamp, float):
timestamp = float(timestamp)
return datetime.datetime.utcfromtimestamp(timestamp)
def exit(msg=''):
if msg:
print(msg, file=sys.stderr)
sys.exit(1)

View File

@@ -31,10 +31,18 @@ class InvalidEndpoint(BaseException):
"""The provided endpoint is invalid."""
class ArgumentRequired(BaseException):
"""A required argument was not provided."""
class CommunicationError(BaseException):
"""Unable to communicate with server."""
class InvalidArgumentError(BaseException):
"""Exception raised when a provided argument is invalid"""
class NotUpdatableError(BaseException):
"""This Resource is not updatable."""

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import csv
from cliff.formatters import base
import jsonpath_rw_ext as jp
from oslo_log import log
import yaml
LOG = log.getLogger(__name__)
class DataframeToCsvFormatter(base.ListFormatter):
"""Cliff formatter allowing to customize CSV report content."""
default_config = [
('Begin', '$.begin'),
('End', '$.end'),
('Metric Type', '$.service'),
('Qty', '$.volume'),
('Cost', '$.rating'),
('Project ID', '$.desc.project_id'),
('Resource ID', '$.desc.resource_id'),
('User ID', '$.desc.user_id'),
]
def _load_config(self, filename):
config = self.default_config
if filename:
try:
with open(filename, 'r') as fd:
yml_config = yaml.safe_load(fd.read())
if len(yml_config):
config = [(list(item.keys())[0], list(item.values())[0])
for item in yml_config]
else:
LOG.warning('Invalid config file {file}. Using default '
'configuration'.format(file=filename))
except (IOError, yaml.scanner.ScannerError) as err:
LOG.warning('Error: {err}. Using default '
'configuration'.format(err=err))
self.parsers = {}
for col, path in config:
self.parsers[col] = jp.parse(path)
return config
def add_argument_group(self, parser):
group = parser.add_argument_group('dataframe-to-csv formatter')
group.add_argument('--format-config-file',
type=str, dest='format_config',
help='Config file for the dict-to-csv formatter')
def _get_csv_row(self, config, json_item):
row = {}
for col, parser in self.parsers.items():
items = parser.find(json_item)
row[col] = items[0].value if items else ''
return row
def emit_list(self, column_names, data, stdout, parsed_args):
config = self._load_config(vars(parsed_args).get('format_config'))
self.writer = csv.DictWriter(stdout,
fieldnames=[elem[0] for elem in config])
self.writer.writeheader()
for dataframe in data:
rating_data = dataframe[3]
for item in rating_data:
item['begin'] = dataframe[0]
item['end'] = dataframe[1]
row = self._get_csv_row(config, item)
self.writer.writerow(row)

View File

@@ -1,45 +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
"""
try:
import oslo_i18n
# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
# application name when this module is synced into the separate
# repository. It is OK to have more than one translation function
# using the same domain, since there will still only be one message
# catalog.
_translators = oslo_i18n.TranslatorFactory(domain='cloudkittyclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
except ImportError:
# NOTE(dims): Support for cases where a project wants to use
# code from oslo-incubator, but is not ready to be internationalized
# (like tempest)
_ = _LI = _LW = _LE = _LC = lambda x: x

View File

@@ -1,234 +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
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
import abc
import argparse
import os
import six
from stevedore import extension
from cloudkittyclient.openstack.common.apiclient import exceptions
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
global _discovered_plugins
_discovered_plugins = {}
def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin
ep_namespace = "cloudkittyclient.openstack.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in six.iteritems(_discovered_plugins):
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)
def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)
def load_plugin_from_args(args):
"""Load required plugin and populate it with options.
Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.
:type args: argparse.Namespace
:raises: AuthPluginOptionsMissing
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]
def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)
@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.
:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin.
"""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins.
"""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.
:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))
def authenticate(self, http_client):
"""Authenticate using plugin defined method.
The method usually analyses `self.opts` and performs
a request to authentication server.
:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication.
"""
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
missing = [opt
for opt in self.opt_names
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)
@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.
:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

View File

@@ -1,533 +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.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
import six
from six.moves.urllib import parse
from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
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=None, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
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] if response_key is not None else body
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
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=None):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'. If response_key is None - all response body
will be used.
"""
body = self.client.get(url).json()
data = body[response_key] if response_key is not None else body
return self.resource_class(self, data, loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
: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=None, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'. If response_key is None - all response body
will be used.
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
data = body[response_key] if response_key is not None else body
if return_raw:
return data
return self.resource_class(self, data)
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'. If response_key is None - all response body
will be used.
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
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'. If response_key is None - all response body
will be used.
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
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(msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
return strutils.to_slug(name)
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
if self.manager.client.last_request_id:
self._add_details(
{'x_request_id': self.manager.client.last_request_id})
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

@@ -1,391 +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
import hashlib
import logging
import time
try:
import simplejson as json
except ImportError:
import json
from oslo_utils import encodeutils
from oslo_utils import importutils
import requests
from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
_logger = logging.getLogger(__name__)
SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "cloudkittyclient.openstack.common.apiclient"
def __init__(self,
auth_plugin,
region_name=None,
endpoint_type="publicURL",
original_ip=None,
verify=True,
cert=None,
timeout=None,
timings=False,
keyring_saver=None,
debug=False,
user_agent=None,
http=None):
self.auth_plugin = auth_plugin
self.endpoint_type = endpoint_type
self.region_name = region_name
self.original_ip = original_ip
self.timeout = timeout
self.verify = verify
self.cert = cert
self.keyring_saver = keyring_saver
self.debug = debug
self.user_agent = user_agent or self.user_agent
self.times = [] # [("item", starttime, endtime), ...]
self.timings = timings
# requests within the same session can reuse TCP connections from pool
self.http = http or requests.Session()
self.cached_token = None
self.last_request_id = None
def _safe_header(self, name, value):
if name in SENSITIVE_HEADERS:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return encodeutils.safe_decode(name), "{SHA1}%s" % d
else:
return (encodeutils.safe_decode(name),
encodeutils.safe_decode(value))
def _http_log_req(self, method, url, kwargs):
if not self.debug:
return
string_parts = [
"curl -g -i",
"-X '%s'" % method,
"'%s'" % url,
]
if not kwargs.get('verify', self.verify):
string_parts.insert(1, '--insecure')
for element in kwargs['headers']:
header = ("-H '%s: %s'" %
self._safe_header(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["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)
self.last_request_id = resp.headers.get('x-openstack-request-id')
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
if self.auth_plugin.opts.get('token'):
self.auth_plugin.opts['token'] = None
if self.auth_plugin.opts.get('endpoint'):
self.auth_plugin.opts['endpoint'] = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**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)
@property
def last_request_id(self):
return self.http_client.last_request_id
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
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,479 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Exception definitions.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
import inspect
import sys
import six
from cloudkittyclient.openstack.common._i18n import _
class ClientException(Exception):
"""The base exception class for all exceptions this library raises.
"""
pass
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 ConnectionError(ClientException):
"""Cannot connect to API service."""
pass
class ConnectionRefused(ConnectionError):
"""Connection refused while trying to connect to API service."""
pass
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
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: %r") % 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: %r") % 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 = body.get(list(body)[0])
if isinstance(error, dict):
kwargs["message"] = (error.get("message") or
error.get("faultstring"))
kwargs["details"] = (error.get("details") or
six.text_type(body))
elif content_type.startswith("text/"):
kwargs["details"] = getattr(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,189 +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.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import requests
import six
from six.moves.urllib import parse
from cloudkittyclient.openstack.common.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
required = required or []
optional = optional or []
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 "auth_plugin" not in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called.
"""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
self.last_request_id = headers.get('x-openstack-request-id')
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@@ -1,100 +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.
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
from oslo_utils import encodeutils
from oslo_utils import uuidutils
import six
from cloudkittyclient.openstack.common._i18n import _
from cloudkittyclient.openstack.common.apiclient import exceptions
def find_resource(manager, name_or_id, **find_args):
"""Look for resource in a given manager.
Used as a helper for the _find_* methods.
Example:
.. code-block:: python
def _find_hypervisor(cs, hypervisor):
#Get a hypervisor by name or ID.
return cliutils.find_resource(cs.hypervisors, hypervisor)
"""
# first try to get entity as integer id
try:
return manager.get(int(name_or_id))
except (TypeError, ValueError, exceptions.NotFound):
pass
# now try to get entity as uuid
try:
if six.PY2:
tmp_id = encodeutils.safe_encode(name_or_id)
else:
tmp_id = encodeutils.safe_decode(name_or_id)
if uuidutils.is_uuid_like(tmp_id):
return manager.get(tmp_id)
except (TypeError, ValueError, exceptions.NotFound):
pass
# for str id which is not uuid
if getattr(manager, 'is_alphanum_id_allowed', False):
try:
return manager.get(name_or_id)
except exceptions.NotFound:
pass
try:
try:
return manager.find(human_id=name_or_id, **find_args)
except exceptions.NotFound:
pass
# finally try to find entity by name
try:
resource = getattr(manager, 'resource_class', None)
name_attr = resource.NAME_ATTR if resource else 'name'
kwargs = {name_attr: name_or_id}
kwargs.update(find_args)
return manager.find(**kwargs)
except exceptions.NotFound:
msg = _("No %(name)s with a name or "
"ID of '%(name_or_id)s' exists.") % \
{
"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id
}
raise exceptions.CommandError(msg)
except exceptions.NoUniqueMatch:
msg = _("Multiple %(name)s matches found for "
"'%(name_or_id)s', use an ID to be more specific.") % \
{
"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id
}
raise exceptions.CommandError(msg)

View File

@@ -1,271 +0,0 @@
# Copyright 2012 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# W0603: Using the global statement
# W0621: Redefining name %s from outer scope
# pylint: disable=W0603,W0621
from __future__ import print_function
import getpass
import inspect
import os
import sys
import textwrap
from oslo_utils import encodeutils
from oslo_utils import strutils
import prettytable
import six
from six import moves
from cloudkittyclient.openstack.common._i18n import _
class MissingArgs(Exception):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = _("Missing arguments: %s") % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
def validate_args(fn, *args, **kwargs):
"""Check that the supplied args are sufficient for calling a function.
>>> validate_args(lambda a: None)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): a
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): b, d
:param fn: the function to check
:param arg: the positional arguments supplied
:param kwargs: the keyword arguments supplied
"""
argspec = inspect.getargspec(fn)
num_defaults = len(argspec.defaults or [])
required_args = argspec.args[:len(argspec.args) - num_defaults]
def isbound(method):
return getattr(method, '__self__', None) is not None
if isbound(fn):
required_args.pop(0)
missing = [arg for arg in required_args if arg not in kwargs]
missing = missing[len(args):]
if missing:
raise MissingArgs(missing)
def arg(*args, **kwargs):
"""Decorator for CLI args.
Example:
>>> @arg("name", help="Name of the new entity")
... def entity_create(args):
... pass
"""
def _decorator(func):
add_arg(func, *args, **kwargs)
return func
return _decorator
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""
if not hasattr(func, 'arguments'):
func.arguments = []
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))
def unauthenticated(func):
"""Adds 'unauthenticated' attribute to decorated function.
Usage:
>>> @unauthenticated
... def mymethod(f):
... pass
"""
func.unauthenticated = True
return func
def isunauthenticated(func):
"""Checks if the function does not require authentication.
Mark such functions with the `@unauthenticated` decorator.
:returns: bool
"""
return getattr(func, 'unauthenticated', False)
def print_list(objs, fields, formatters=None, sortby_index=0,
mixed_case_fields=None, field_labels=None):
"""Print a list or objects as a table, one row per object.
:param objs: iterable of :class:`Resource`
:param fields: attributes that correspond to columns, in order
:param formatters: `dict` of callables for field formatting
:param sortby_index: index of the field for sorting table rows
:param mixed_case_fields: fields corresponding to object attributes that
have mixed case names (e.g., 'serverId')
:param field_labels: Labels to use in the heading of the table, default to
fields.
"""
formatters = formatters or {}
mixed_case_fields = mixed_case_fields or []
field_labels = field_labels or fields
if len(field_labels) != len(fields):
raise ValueError(_("Field labels list %(labels)s has different number "
"of elements than fields list %(fields)s"),
{'labels': field_labels, 'fields': fields})
if sortby_index is None:
kwargs = {}
else:
kwargs = {'sortby': field_labels[sortby_index]}
pt = prettytable.PrettyTable(field_labels)
pt.align = 'l'
for o in objs:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](o))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)
if six.PY3:
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
else:
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
def print_dict(dct, dict_property="Property", wrap=0):
"""Print a `dict` as a table of two columns.
:param dct: `dict` to print
:param dict_property: name of the first column
:param wrap: wrapping for the second column
"""
pt = prettytable.PrettyTable([dict_property, 'Value'])
pt.align = 'l'
for k, v in six.iteritems(dct):
# convert dict to str to check length
if isinstance(v, dict):
v = six.text_type(v)
if wrap > 0:
v = textwrap.fill(six.text_type(v), wrap)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
pt.add_row([col1, line])
col1 = ''
else:
pt.add_row([k, v])
if six.PY3:
print(encodeutils.safe_encode(pt.get_string()).decode())
else:
print(encodeutils.safe_encode(pt.get_string()))
def get_password(max_password_prompts=3):
"""Read password from TTY."""
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
pw = None
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
# Check for Ctrl-D
try:
for __ in moves.range(max_password_prompts):
pw1 = getpass.getpass("OS Password: ")
if verify:
pw2 = getpass.getpass("Please verify: ")
else:
pw2 = pw1
if pw1 == pw2 and pw1:
pw = pw1
break
except EOFError:
pass
return pw
def service_type(stype):
"""Adds 'service_type' attribute to decorated function.
Usage:
.. code-block:: python
@service_type('volume')
def mymethod(f):
...
"""
def inner(f):
f.service_type = stype
return f
return inner
def get_service_type(f):
"""Retrieves service type from function."""
return getattr(f, 'service_type', None)
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def exit(msg=''):
if msg:
print (msg, file=sys.stderr)
sys.exit(1)

49
cloudkittyclient/osc.py Normal file
View File

@@ -0,0 +1,49 @@
# Copyright 2014 OpenStack Foundation
#
# 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 osc_lib import utils
DEFAULT_API_VERSION = '2'
API_VERSION_OPTION = 'os_rating_api_version'
API_NAME = "rating"
API_VERSIONS = {
"1": "cloudkittyclient.v1.client.Client",
"2": "cloudkittyclient.v2.client.Client",
}
def make_client(instance):
"""Returns a rating service client."""
version = instance._api_version[API_NAME]
ck_client = utils.get_client_class(
API_NAME,
version,
API_VERSIONS)
instance.setup_auth()
adapter_options = dict(
interface=instance.interface,
region_name=instance.region_name,
)
return ck_client(session=instance.session,
adapter_options=adapter_options)
def build_option_parser(parser):
"""Hook to add global options."""
parser.add_argument(
'--rating-api-version', type=int, default=utils.env(
'OS_RATING_API_VERSION',
default=DEFAULT_API_VERSION)
)
return parser

View File

@@ -1,5 +1,6 @@
# Copyright 2015 Objectif Libre
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
@@ -11,314 +12,146 @@
# 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
from sys import argv
"""
Command-line interface to the OpenStack Cloudkitty API.
"""
import cliff.app
from cliff.commandmanager import CommandManager
from openstack import config as occ
from oslo_log import log
from __future__ import print_function
import argparse
import logging
import sys
from oslo_utils import encodeutils
import six
from stevedore import extension
import cloudkittyclient
from cloudkittyclient import client as ckclient
from cloudkittyclient.common import utils
from cloudkittyclient import exc
from cloudkittyclient.openstack.common import cliutils
from cloudkittyclient.v1.report import shell as report_shell
from cloudkittyclient.v1.storage import shell as storage_shell
SUBMODULES_NAMESPACE = 'cloudkitty.client.modules'
from cloudkittyclient import client
from cloudkittyclient.osc import DEFAULT_API_VERSION
from cloudkittyclient import utils
def _positive_non_zero_int(argument_value):
if argument_value is None:
return None
try:
value = int(argument_value)
except ValueError:
msg = "%s must be an integer" % argument_value
raise argparse.ArgumentTypeError(msg)
if value <= 0:
msg = "%s must be greater than 0" % argument_value
raise argparse.ArgumentTypeError(msg)
return value
LOG = log.getLogger(__name__)
class CloudkittyShell(object):
class CloudKittyShell(cliff.app.App):
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='cloudkitty',
description=__doc__.strip(),
epilog='See "cloudkitty help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=HelpFormatter,
legacy_commands = [
'module-list',
'module-enable',
'module-list',
'module-enable',
'module-disable',
'module-set-priority',
'info-config-get',
'info-service-get',
'total-get',
'summary-get',
'report-tenant-list',
'collector-mapping-list',
'collector-mapping-get',
'collector-mapping-create',
'collector-mapping-delete',
'collector-state-get',
'collector-state-enable',
'collector-state-disable',
'storage-dataframe-list',
'hashmap-service-create',
'hashmap-service-list',
'hashmap-service-delete',
'hashmap-field-create',
'hashmap-field-list',
'hashmap-field-delete',
'hashmap-mapping-create',
'hashmap-mapping-update',
'hashmap-mapping-list',
'hashmap-mapping-delete',
'hashmap-group-create',
'hashmap-group-list',
'hashmap-group-delete',
'hashmap-threshold-create'
'hashmap-threshold-update'
'hashmap-threshold-list',
'hashmap-threshold-delete',
'hashmap-threshold-get',
'hashmap-threshold-group',
'pyscripts-script-create',
'pyscripts-script-list',
'pyscripts-script-get',
'pyscripts-script-get-data',
'pyscripts-script-delete',
'pyscripts-script-update',
]
def _get_api_version(self, args):
# FIXME(peschk_l): This is a hacky way to figure out the client version
# to load. If anybody has a better idea, please fix this.
self.deferred_help = True
parser = self.build_option_parser('CloudKitty CLI client',
utils.get_version())
del self.deferred_help
parsed_args = parser.parse_known_args(args)
return str(parsed_args[0].os_rating_api_version or DEFAULT_API_VERSION)
def __init__(self, args):
self._args = args
self.cloud_config = occ.OpenStackConfig()
super(CloudKittyShell, self).__init__(
description='CloudKitty CLI client',
version=utils.get_version(),
command_manager=CommandManager('cloudkittyclient_v{}'.format(
self._get_api_version(args[:]),
)),
deferred_help=True,
)
self._client = None
# Global arguments
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument('--version',
action='version',
version=cloudkittyclient.__version__)
parser.add_argument('-d', '--debug',
default=bool(cliutils.env('CLOUDKITTYCLIENT_DEBUG')
),
action='store_true',
help='Defaults to env[CLOUDKITTYCLIENT_DEBUG].')
parser.add_argument('-v', '--verbose',
default=False, action="store_true",
help="Print more verbose output.")
parser.add_argument('--timeout',
default=600,
type=_positive_non_zero_int,
help='Number of seconds to wait for a response.')
parser.add_argument('--cloudkitty-url', metavar='<CLOUDKITTY_URL>',
dest='os_endpoint',
default=cliutils.env('CLOUDKITTY_URL'),
help=("DEPRECATED, use --os-endpoint instead. "
"Defaults to env[CLOUDKITTY_URL]."))
parser.add_argument('--cloudkitty_url',
dest='os_endpoint',
help=argparse.SUPPRESS)
parser.add_argument('--cloudkitty-api-version',
default=cliutils.env(
'CLOUDKITTY_API_VERSION', default='1'),
help='Defaults to env[CLOUDKITTY_API_VERSION] '
'or 1.')
parser.add_argument('--cloudkitty_api_version',
help=argparse.SUPPRESS)
self.auth_plugin.add_opts(parser)
self.auth_plugin.add_common_opts(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, report_shell)
self._find_actions(subparsers, storage_shell)
extensions = extension.ExtensionManager(
SUBMODULES_NAMESPACE,
)
for ext in extensions:
shell = ext.plugin.get_shell()
self._find_actions(subparsers, shell)
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)
@staticmethod
def _setup_logging(debug):
format = '%(levelname)s (%(module)s) %(message)s'
if debug:
logging.basicConfig(format=format, level=logging.DEBUG)
else:
logging.basicConfig(format=format, level=logging.WARN)
logging.getLogger('iso8601').setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
def parse_args(self, argv):
# Parse args once to find version
self.auth_plugin = ckclient.AuthPlugin()
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.auth_plugin.parse_opts(options)
self._setup_logging(options.debug)
# build available subcommands based on version
api_version = options.cloudkitty_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
self.parser = subcommand_parser
# Handle top-level --help/-h before attempting to parse
# a command off the command line
if options.help or not argv:
self.do_help(options)
return 0
# Return parsed args
return api_version, subcommand_parser.parse_args(argv)
@staticmethod
def no_project_and_domain_set(args):
if not (args.os_project_id or (args.os_project_name and
(args.os_user_domain_name or args.os_user_domain_id)) or
(args.os_tenant_id or args.os_tenant_name)):
return True
else:
return False
def main(self, argv):
parsed = self.parse_args(argv)
if parsed == 0:
return 0
api_version, args = parsed
# 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 ((self.auth_plugin.opts.get('token')
or self.auth_plugin.opts.get('auth_token'))
and self.auth_plugin.opts['endpoint']):
if not self.auth_plugin.opts['username']:
raise exc.CommandError("You must provide a username via "
"either --os-username or via "
"env[OS_USERNAME]")
if not self.auth_plugin.opts['password']:
raise exc.CommandError("You must provide a password via "
"either --os-password or via "
"env[OS_PASSWORD]")
if self.no_project_and_domain_set(args):
# steer users towards Keystone V3 API
raise exc.CommandError("You must provide a project_id via "
"either --os-project-id or via "
"env[OS_PROJECT_ID] and "
"a domain_name via either "
"--os-user-domain-name or via "
"env[OS_USER_DOMAIN_NAME] or "
"a domain_id via either "
"--os-user-domain-id or via "
"env[OS_USER_DOMAIN_ID]")
if not (self.auth_plugin.opts['tenant_id']
or self.auth_plugin.opts['tenant_name']):
raise exc.CommandError("You must provide a tenant_id via "
"either --os-tenant-id or via "
"env[OS_TENANT_ID]")
if not self.auth_plugin.opts['auth_url']:
raise exc.CommandError("You must provide an auth url via "
"either --os-auth-url or via "
"env[OS_AUTH_URL]")
client_kwargs = vars(args)
client_kwargs.update(self.auth_plugin.opts)
client_kwargs['auth_plugin'] = self.auth_plugin
client = ckclient.get_client(api_version, **client_kwargs)
# call whatever callback was selected
# NOTE(peschk_l): Used to warn users about command syntax change in Rocky.
# To be deleted in S.
def run_subcommand(self, argv):
try:
args.func(client, args)
except exc.HTTPUnauthorized:
raise exc.CommandError("Invalid OpenStack Identity credentials.")
self.command_manager.find_command(argv)
except ValueError:
if argv[0] in self.legacy_commands:
LOG.warning('WARNING: This command is deprecated, please see'
' the reference for the new commands\n')
exit(1)
return super(CloudKittyShell, self).run_subcommand(argv)
def do_bash_completion(self, args):
"""Prints all of the commands and options to stdout.
def build_option_parser(self, description, version):
parser = super(CloudKittyShell, self).build_option_parser(
description,
version,
argparse_kwargs={'allow_abbrev': False})
if 'OS_AUTH_TYPE' not in os.environ.keys() \
and 'OS_PASSWORD' in os.environ.keys():
os.environ['OS_AUTH_TYPE'] = 'password'
self.cloud_config.register_argparse_arguments(
parser, self._args, service_keys=['rating'])
return parser
The cloudkitty.bash_completion script doesn't have to hard code them.
"""
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:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
self.parser.print_help()
class HelpFormatter(argparse.HelpFormatter):
def __init__(self, prog, indent_increment=2, max_help_position=32,
width=None):
super(HelpFormatter, self).__init__(prog, indent_increment,
max_help_position, width)
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(HelpFormatter, self).start_section(heading)
@property
def client(self):
if self._client is None:
self.cloud = self.cloud_config.get_one(
argparse=self.options)
session = self.cloud.get_session()
adapter_options = dict(
service_type=(self.options.os_rating_service_type or
self.options.os_service_type),
service_name=(self.options.os_rating_service_name or
self.options.os_service_name),
interface=(self.options.os_rating_interface or
self.options.os_interface),
region_name=self.options.os_region_name,
endpoint_override=(
self.options.os_rating_endpoint_override or
self.options.os_endpoint_override),
)
self._client = client.Client(
str(self.options.os_rating_api_version or DEFAULT_API_VERSION),
session=session,
adapter_options=adapter_options)
return self._client
def main(args=None):
try:
if args is None:
args = sys.argv[1:]
CloudkittyShell().main(args)
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)
except KeyboardInterrupt:
print("Stopping Cloudkitty Client", file=sys.stderr)
sys.exit(130)
if __name__ == "__main__":
main()
if args is None:
args = argv[1:]
client_app = CloudKittyShell(args)
return client_app.run(args)

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2010-2011 OpenStack Foundation
# 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.
from oslotest import base
class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests."""

View File

@@ -1,64 +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 keystoneclient.v2_0 import client as ksclient
def script_keystone_client():
ksclient.Client(auth_url='http://no.where',
insecure=False,
password='password',
tenant_id='',
tenant_name='tenant_name',
username='username').AndReturn(FakeKeystone('abcd1234'))
def fake_headers():
return {'X-Auth-Token': 'abcd1234',
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'python-cloudkittyclient'}
class FakeServiceCatalog(object):
@staticmethod
def url_for(endpoint_type, service_type):
return 'http://192.168.1.5:8004/v1/f14b41234'
class FakeKeystone(object):
service_catalog = FakeServiceCatalog()
def __init__(self, auth_token):
self.auth_token = auth_token
class FakeHTTPResponse(object):
version = 1.1
def __init__(self, status, reason, headers, body):
self.headers = headers
self.body = body
self.status = status
self.reason = reason
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.body
self.body = None
return b

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import json
import os
import subprocess
from cloudkittyclient.tests import utils
from oslo_log import log
LOG = log.getLogger(__name__)
class BaseFunctionalTest(utils.BaseTestCase):
# DevStack is using VENV by default. Therefore, to execute the commands,
# we need to activate the VENV. And, to do that, we need the VENV path.
# This path is hardcoded here because we could not find a variable in this
# Python code to retrieve the VENV variable from the test machine.
# It seems that because of the stack TOX -> stestr -> this python code, and
# so on, we are not able to access the DevStack variables here.
#
# If somebody finds a solution, we can remove the hardcoded path here.
DEV_STACK_VENV_BASE_PATH = "/opt/stack/data/venv"
BASE_COMMAND_WITH_VENV = "source %s/bin/activate && %s "
def _run(self, executable, action,
flags='', params='', fmt='-f json', stdin=None, has_output=True):
if not has_output:
fmt = ''
does_venv_exist = not os.system("ls -lah /opt/stack/data/venv")
LOG.info("Test to check if the VENV file exist returned: [%s].",
does_venv_exist)
system_variables = os.environ.copy()
LOG.info("System variables [%s] found when executing the tests.",
system_variables)
cmd = ' '.join([executable, flags, action, params, fmt])
actual_command_with_venv = self.BASE_COMMAND_WITH_VENV % (
self.DEV_STACK_VENV_BASE_PATH, cmd)
LOG.info("Command being executed: [%s].", actual_command_with_venv)
p = subprocess.Popen(
["bash", "-c", actual_command_with_venv],
env=os.environ.copy(), shell=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.PIPE if stdin else None
)
stdout, stderr = p.communicate(input=stdin)
LOG.info("Standard output [%s] and error output [%s] for command "
"[%s]. ", stdout, stderr, actual_command_with_venv)
if p.returncode != 0:
raise RuntimeError('"{cmd}" returned {val}: {msg}'.format(
cmd=' '.join(cmd), val=p.returncode, msg=stderr))
return json.loads(stdout) if has_output else None
def openstack(self, action,
flags='', params='', fmt='-f json',
stdin=None, has_output=True):
return self._run('openstack rating', action,
flags, params, fmt, stdin, has_output)
def cloudkitty(self, action,
flags='', params='', fmt='-f json',
stdin=None, has_output=True):
return self._run('cloudkitty', action, flags, params, fmt,
stdin, has_output)

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.functional import base
class CkCollectorTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkCollectorTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_create_get_delete_collector_mapping(self):
# Create Mapping
resp = self.runner(
'collector-mapping create', params='compute gnocchi')[0]
self.assertEqual(resp['Collector'], 'gnocchi')
self.assertEqual(resp['Service'], 'compute')
# Check that mapping is queryable
resp = self.runner('collector-mapping list')
self.assertEqual(len(resp), 1)
resp = resp[0]
self.assertEqual(resp['Collector'], 'gnocchi')
self.assertEqual(resp['Service'], 'compute')
# Delete mapping
self.runner('collector-mapping delete',
params='compute', has_output=False)
# Check that mapping was deleted
resp = self.runner('collector-mapping list')
self.assertEqual(len(resp), 0)
def test_collector_enable_disable(self):
# Enable collector
resp = self.runner('collector enable gnocchi')
self.assertEqual(len(resp), 1)
resp = resp[0]
self.assertEqual(resp['Collector'], 'gnocchi')
self.assertEqual(resp['State'], True)
# Disable collector
resp = self.runner('collector disable gnocchi')
self.assertEqual(len(resp), 1)
resp = resp[0]
self.assertEqual(resp['Collector'], 'gnocchi')
self.assertEqual(resp['State'], False)
class OSCCollectorTest(CkCollectorTest):
def __init__(self, *args, **kwargs):
super(OSCCollectorTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,366 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from datetime import datetime
from datetime import timedelta
from cloudkittyclient.tests.functional import base
class CkHashmapTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkHashmapTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def setUp(self):
super(CkHashmapTest, self).setUp()
self._fields = list()
self._services = list()
self._mappings = list()
self._groups = list()
self._thresholds = list()
def tearDown(self):
super(CkHashmapTest, self).tearDown()
for field in self._fields:
try:
self.runner(
'hashmap field delete', params=field, has_output=False)
except RuntimeError:
pass
for service in self._services:
try:
self.runner(
'hashmap service delete', params=service, has_output=False)
except RuntimeError:
pass
for group in self._groups:
try:
self.runner(
'hashmap group delete', params=group, has_output=False)
except RuntimeError:
pass
for mapping in self._mappings:
try:
self.runner(
'hashmap mapping delete', params=mapping, has_output=False)
except RuntimeError:
pass
for threshold in self._thresholds:
try:
self.runner('hashmap threshold delete',
params=threshold, has_output=False)
except RuntimeError:
pass
def test_list_mapping_types(self):
resp = self.runner('hashmap mapping-types list')
found_types = [elem['Mapping types'] for elem in resp]
self.assertIn('flat', found_types)
self.assertIn('rate', found_types)
def test_create_get_delete_service(self):
# Create service
resp = self.runner('hashmap service create', params='testservice')[0]
self.assertEqual(resp['Name'], 'testservice')
service_id = resp['Service ID']
self._services.append(service_id)
# Check that resp is the same with service get and list
resp_with_sid = self.runner(
'hashmap service get', params=service_id)
resp_without_sid = self.runner('hashmap service list')
self.assertEqual(resp_with_sid, resp_without_sid)
self.assertEqual(len(resp_with_sid), 1)
# Check that deletion works
self.runner('hashmap service delete',
params=resp['Service ID'],
has_output=False)
resp = self.runner('hashmap service list')
self.assertEqual(len(resp), 0)
def test_group_get_create_delete(self):
# Create group
resp = self.runner('hashmap group create', params='testgroup')[0]
self.assertEqual(resp['Name'], 'testgroup')
group_id = resp['Group ID']
self._groups.append(group_id)
resp = self.runner('hashmap group list')
self.assertEqual(len(resp), 1)
# Check that deletion works
self.runner('hashmap group delete',
params=group_id, has_output=False)
resp = self.runner('hashmap group list')
self.assertEqual(len(resp), 0)
def test_create_get_delete_field(self):
# Create service
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
# Create field
resp = self.runner('hashmap field create',
params='{} testfield'.format(service_id))[0]
self.assertEqual(resp['Name'], 'testfield')
self.assertEqual(resp['Service ID'], service_id)
field_id = resp['Field ID']
self._fields.append(field_id)
# Check that resp is the same with field get and list
resp_with_fid = self.runner('hashmap field get', params=field_id)
resp_with_sid = self.runner('hashmap field list', params=service_id)
self.assertEqual(resp_with_fid, resp_with_sid)
self.assertEqual(len(resp_with_fid), 1)
# Check that deletion works
self.runner(
'hashmap field delete', params=field_id, has_output=False)
# resp = self.runner(
# 'hashmap field list', params='-s {}'.format(service_id))
resp = self.runner(
'hashmap field list', params=service_id)
self.assertEqual(len(resp), 0)
def test_create_get_update_delete_mapping_service(self):
future_date = datetime.now() + timedelta(days=1)
date_iso = future_date.isoformat()
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
# Create mapping
resp = self.runner('hashmap mapping create',
params=f'-s {service_id} 12 --start {date_iso}')[0]
mapping_id = resp['Mapping ID']
self._mappings.append(mapping_id)
self.assertEqual(resp['Service ID'], service_id)
self.assertEqual(float(resp['Cost']), float(12))
# Get mapping
resp_with_sid = self.runner(
'hashmap mapping list', params='-s {}'.format(service_id))[0]
resp_with_mid = self.runner(
'hashmap mapping get', params=mapping_id)[0]
self.assertEqual(resp_with_sid, resp_with_mid)
self.assertEqual(resp_with_sid['Mapping ID'], mapping_id)
self.assertEqual(resp_with_sid['Service ID'], service_id)
self.assertEqual(float(resp_with_sid['Cost']), float(12))
# Update mapping
resp = self.runner('hashmap mapping update',
params='--cost 10 {}'.format(mapping_id))[0]
self.assertEqual(float(resp['Cost']), float(10))
# Check that deletion works
self.runner(
'hashmap mapping delete', params=mapping_id, has_output=False)
resp = self.runner(
'hashmap mapping list', params='-s {}'.format(service_id))
self.assertEqual(len(resp), 0)
self.runner(
'hashmap service delete', params=service_id, has_output=False)
def test_create_get_update_delete_mapping_field(self):
future_date = datetime.now() + timedelta(days=1)
date_iso = future_date.isoformat()
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
resp = self.runner('hashmap field create',
params='{} testfield'.format(service_id))[0]
field_id = resp['Field ID']
self._fields.append(field_id)
# Create mapping
resp = self.runner(
'hashmap mapping create',
params=f'--field-id {field_id} 12 --value '
f'testvalue --start {date_iso}')[0]
mapping_id = resp['Mapping ID']
self._mappings.append(service_id)
self.assertEqual(resp['Field ID'], field_id)
self.assertEqual(float(resp['Cost']), float(12))
self.assertEqual(resp['Value'], 'testvalue')
# Get mapping
resp = self.runner(
'hashmap mapping get', params=mapping_id)[0]
self.assertEqual(resp['Mapping ID'], mapping_id)
self.assertEqual(float(resp['Cost']), float(12))
# Update mapping
resp = self.runner('hashmap mapping update',
params='--cost 10 {}'.format(mapping_id))[0]
self.assertEqual(float(resp['Cost']), float(10))
def test_create_get_update_delete_mapping_field_started(self):
resp = self.runner('hashmap service create',
params='testservice_date_started')[0]
service_id = resp['Service ID']
self._services.append(service_id)
resp = self.runner(
'hashmap field create',
params='{} testfield_date_started'.format(service_id))[0]
field_id = resp['Field ID']
self._fields.append(field_id)
# Create mapping
resp = self.runner(
'hashmap mapping create',
params=f'--field-id {field_id} 12 --value '
f'testvalue')[0]
mapping_id = resp['Mapping ID']
self._mappings.append(service_id)
self.assertEqual(resp['Field ID'], field_id)
self.assertEqual(float(resp['Cost']), float(12))
self.assertEqual(resp['Value'], 'testvalue')
# Get mapping
resp = self.runner(
'hashmap mapping get', params=mapping_id)[0]
self.assertEqual(resp['Mapping ID'], mapping_id)
self.assertEqual(float(resp['Cost']), float(12))
# Should not be able to update a rule that is running (start < now)
try:
self.runner('hashmap mapping update',
params='--cost 10 {}'.format(mapping_id))[0]
except RuntimeError as e:
expected_error = ("You are allowed to update only the attribute "
"[end] as this rule is already running as it "
"started on ")
self.assertIn(expected_error, str(e))
def test_group_mappings_get(self):
# Service and group
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
resp = self.runner('hashmap group create', params='testgroup')[0]
group_id = resp['Group ID']
self._groups.append(group_id)
# Create service mapping bleonging to testgroup
resp = self.runner(
'hashmap mapping create',
params='-s {} -g {} 12'.format(service_id, group_id))[0]
mapping_id = resp['Mapping ID']
self._mappings.append(mapping_id)
resp = self.runner('hashmap group mappings get', params=group_id)[0]
self.assertEqual(resp['Group ID'], group_id)
self.assertEqual(float(resp['Cost']), float(12))
def test_create_get_update_delete_threshold_service(self):
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
# Create threshold
resp = self.runner('hashmap threshold create',
params='-s {} 12 0.9'.format(service_id))[0]
threshold_id = resp['Threshold ID']
self._thresholds.append(threshold_id)
self.assertEqual(resp['Service ID'], service_id)
self.assertEqual(float(resp['Level']), float(12))
self.assertEqual(float(resp['Cost']), float(0.9))
# Get threshold
resp_with_sid = self.runner(
'hashmap threshold list', params='-s {}'.format(service_id))[0]
resp_with_tid = self.runner(
'hashmap threshold get', params=threshold_id)[0]
self.assertEqual(resp_with_sid, resp_with_tid)
self.assertEqual(resp_with_sid['Threshold ID'], threshold_id)
self.assertEqual(resp_with_sid['Service ID'], service_id)
self.assertEqual(float(resp_with_sid['Level']), float(12))
self.assertEqual(float(resp_with_sid['Cost']), float(0.9))
# Update threshold
resp = self.runner('hashmap threshold update',
params='--cost 10 {}'.format(threshold_id))[0]
self.assertEqual(float(resp['Cost']), float(10))
# Check that deletion works
self.runner(
'hashmap threshold delete', params=threshold_id, has_output=False)
resp = self.runner(
'hashmap threshold list', params='-s {}'.format(service_id))
self.assertEqual(len(resp), 0)
def test_create_get_update_delete_threshold_field(self):
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
resp = self.runner('hashmap field create',
params='{} testfield'.format(service_id))[0]
field_id = resp['Field ID']
self._fields.append(field_id)
# Create threshold
resp = self.runner(
'hashmap threshold create',
params='--field-id {} 12 0.9'.format(field_id))[0]
threshold_id = resp['Threshold ID']
self._thresholds.append(service_id)
self.assertEqual(resp['Field ID'], field_id)
self.assertEqual(float(resp['Level']), float(12))
self.assertEqual(float(resp['Cost']), float(0.9))
# Get threshold
resp = self.runner('hashmap threshold get', params=threshold_id)[0]
self.assertEqual(resp['Threshold ID'], threshold_id)
self.assertEqual(float(resp['Level']), float(12))
self.assertEqual(float(resp['Cost']), float(0.9))
# Update threshold
resp = self.runner('hashmap threshold update',
params='--cost 10 {}'.format(threshold_id))[0]
self.assertEqual(float(resp['Cost']), float(10))
def test_group_thresholds_get(self):
# Service and group
resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID']
self._services.append(service_id)
resp = self.runner('hashmap group create', params='testgroup')[0]
group_id = resp['Group ID']
self._groups.append(group_id)
# Create service threshold bleonging to testgroup
resp = self.runner(
'hashmap threshold create',
params='-s {} -g {} 12 0.9'.format(service_id, group_id))[0]
threshold_id = resp['Threshold ID']
self._thresholds.append(threshold_id)
resp = self.runner('hashmap group thresholds get', params=group_id)[0]
self.assertEqual(resp['Group ID'], group_id)
self.assertEqual(float(resp['Level']), float(12))
self.assertEqual(float(resp['Cost']), float(0.9))
class OSCHashmapTest(CkHashmapTest):
def __init__(self, *args, **kwargs):
super(OSCHashmapTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import jsonpath_rw_ext as jp
from cloudkittyclient.tests.functional import base
class CkInfoTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkInfoTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_info_config_get(self):
resp = self.runner('info config get')
for elem in resp:
if elem.get('Section') == 'name':
self.assertEqual(elem['Value'], 'OpenStack')
def test_info_metric_list(self):
resp = self.runner('info metric list')
res = jp.match1('$.[*].Metric', resp)
self.assertIsNotNone(res)
def test_info_service_get_image_size(self):
resp = self.runner('info metric get', params='image.size')[0]
self.assertEqual(resp['Metric'], 'image.size')
class OSCInfoTest(CkInfoTest):
def __init__(self, *args, **kwargs):
super(OSCInfoTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from datetime import datetime
from datetime import timedelta
from cloudkittyclient.tests.functional import base
class CkPyscriptTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkPyscriptTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_create_get_update_list_delete(self):
future_date = datetime.now() + timedelta(days=1)
date_iso = future_date.isoformat()
# Create
resp = self.runner(
'pyscript create', params=f"testscript "
f"'return 0' --start {date_iso}")[0]
script_id = resp['Script ID']
self.assertEqual(resp['Name'], 'testscript')
# Get
resp = self.runner('pyscript get', params=script_id)[0]
self.assertEqual(resp['Name'], 'testscript')
self.assertEqual(resp['Script ID'], script_id)
# Update
resp = self.runner(
'pyscript update',
params="-d 'return 1' {} --description "
"desc".format(script_id))[0]
self.assertEqual(resp['Script Description'], 'desc')
self.assertEqual(resp['Script ID'], script_id)
self.assertEqual(resp['Data'], 'return 1')
# List
resp = self.runner('pyscript list')
self.assertEqual(len(resp), 1)
resp = resp[0]
self.assertEqual(resp['Script Description'], 'desc')
self.assertEqual(resp['Script ID'], script_id)
self.assertEqual(resp['Data'], 'return 1')
# Delete
self.runner('pyscript delete', params=script_id, has_output=False)
def test_create_get_update_list_delete_started(self):
# Create
resp = self.runner(
'pyscript create', params="testscript_started "
"'return 0'")[0]
script_id = resp['Script ID']
self.assertEqual(resp['Name'], 'testscript_started')
# Get
resp = self.runner('pyscript get', params=script_id)[0]
self.assertEqual(resp['Name'], 'testscript_started')
self.assertEqual(resp['Script ID'], script_id)
# Should not be able to update a rule that is running (start < now)
try:
self.runner(
'pyscript update',
params="-d 'return 1' {} --description "
"desc".format(script_id))[0]
except RuntimeError as e:
expected_error = ("You are allowed to update only the attribute "
"[end] as this rule is already running as it "
"started on ")
self.assertIn(expected_error, str(e))
# List
resp = self.runner('pyscript list')
self.assertEqual(len(resp), 1)
resp = resp[0]
self.assertEqual(resp['Script Description'], None)
self.assertEqual(resp['Script ID'], script_id)
self.assertEqual(resp['Data'], 'return 0')
# Delete
self.runner('pyscript delete', params=script_id, has_output=False)
class OSCPyscriptTest(CkPyscriptTest):
def __init__(self, *args, **kwargs):
super(CkPyscriptTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.functional import base
class CkRatingTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkRatingTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_module_enable_get_disable(self):
# enable
resp = self.runner('module enable', params='hashmap')[0]
self.assertTrue(resp['Enabled'])
# get
resp = self.runner('module get', params='hashmap')[0]
self.assertTrue(resp['Enabled'])
self.assertEqual(resp['Module'], 'hashmap')
# disable
resp = self.runner('module disable', params='hashmap')[0]
self.assertFalse(resp['Enabled'])
def test_module_set_priority(self):
resp = self.runner('module set priority', params='hashmap 100')[0]
self.assertEqual(resp['Priority'], 100)
class OSCRatingTest(CkRatingTest):
def __init__(self, *args, **kwargs):
super(CkRatingTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.functional import base
class CkReportTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkReportTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_get_summary(self):
resp = self.runner('summary get')
self.assertEqual(len(resp), 0)
def test_get_summary_with_groupby(self):
resp = self.runner('summary get', params='-g res_type tenant_id')
self.assertEqual(len(resp), 0)
def test_get_total(self):
resp = self.runner('total get')
self.assertIn('Total', resp.keys())
def test_get_tenants(self):
self.runner('report tenant list')
class OSCReportTest(CkReportTest):
def __init__(self, *args, **kwargs):
super(OSCReportTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -1,4 +1,5 @@
# Copyright 2015 Objectif Libre
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -11,20 +12,22 @@
# 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 cloudkittyclient.common import base
#
from cloudkittyclient.tests.functional import base
class Collector(base.Resource):
class CkStorageTest(base.BaseFunctionalTest):
key = 'collector'
def __init__(self, *args, **kwargs):
super(CkStorageTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def __repr__(self):
return "<Collector %s>" % self._info
def test_dataframes_get(self):
self.runner('dataframes get')
class CollectorManager(base.Manager):
resource_class = Collector
base_url = "/v1/rating"
key = "collector"
collection_key = "collectors"
class OSCStorageTest(CkStorageTest):
def __init__(self, *args, **kwargs):
super(CkStorageTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,174 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import os
import uuid
from cloudkittyclient.tests.functional import base
class CkDataframesTest(base.BaseFunctionalTest):
dataframes_data = """
{
"dataframes": [
{
"period": {
"begin": "20190723T122810Z",
"end": "20190723T132810Z"
},
"usage": {
"metric_one": [
{
"vol": {
"unit": "GiB",
"qty": 1.2
},
"rating": {
"price": 0.04
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
],
"metric_two": [
{
"vol": {
"unit": "MB",
"qty": 200.4
},
"rating": {
"price": 0.06
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
]
}
},
{
"period": {
"begin": "20190823T122810Z",
"end": "20190823T132810Z"
},
"usage": {
"metric_one": [
{
"vol": {
"unit": "GiB",
"qty": 2.4
},
"rating": {
"price": 0.08
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
],
"metric_two": [
{
"vol": {
"unit": "MB",
"qty": 400.8
},
"rating": {
"price": 0.12
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
]
}
}
]
}
"""
def __init__(self, *args, **kwargs):
super(CkDataframesTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def setUp(self):
super(CkDataframesTest, self).setUp()
self.fixture_file_name = '{}.json'.format(uuid.uuid4())
with open(self.fixture_file_name, 'w') as f:
f.write(self.dataframes_data)
def tearDown(self):
files = os.listdir('.')
if self.fixture_file_name in files:
os.remove(self.fixture_file_name)
super(CkDataframesTest, self).tearDown()
def test_dataframes_add_with_no_args(self):
self.assertRaisesRegex(
RuntimeError,
'error: the following arguments are required: datafile',
self.runner,
'dataframes add',
fmt='',
has_output=False,
)
def test_dataframes_add(self):
self.runner(
'dataframes add {}'.format(self.fixture_file_name),
fmt='',
has_output=False,
)
def test_dataframes_add_with_hyphen_stdin(self):
with open(self.fixture_file_name, 'r') as f:
self.runner(
'dataframes add -',
fmt='',
stdin=f.read().encode(),
has_output=False,
)
def test_dataframes_get(self):
# TODO(jferrieu): functional tests will be added in another
# patch for `dataframes get`
pass
class OSCDataframesTest(CkDataframesTest):
def __init__(self, *args, **kwargs):
super(OSCDataframesTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,41 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.functional import base
class CkScopeTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkScopeTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_scope_state_get(self):
return True
# FIXME(peschk_l): Uncomment and update this once there is a way to set
# the state of a scope through the client
# resp = self.runner('scope state get')
def test_scope_state_reset(self):
return True
# FIXME(jferrieu): Uncomment and update this once there is a way to set
# the state of a scope through the client
# resp = self.runner('scope state reset')
class OSCScopeTest(CkScopeTest):
def __init__(self, *args, **kwargs):
super(OSCScopeTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -0,0 +1,35 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.functional import base
class CkSummaryTest(base.BaseFunctionalTest):
def __init__(self, *args, **kwargs):
super(CkSummaryTest, self).__init__(*args, **kwargs)
self.runner = self.cloudkitty
def test_summary_get(self):
return True
# FIXME(peschk_l): Uncomment and update this once there is a way to set
# the state of a summary through the client
# resp = self.runner('summary get')
class OSCSummaryTest(CkSummaryTest):
def __init__(self, *args, **kwargs):
super(OSCSummaryTest, self).__init__(*args, **kwargs)
self.runner = self.openstack

View File

@@ -1,148 +0,0 @@
# Copyright 2015 Objectif Libre
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# 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 types
import mock
from cloudkittyclient import client
from cloudkittyclient.tests import fakes
from cloudkittyclient.tests import utils
from cloudkittyclient.v1 import client as v1client
FAKE_ENV = {
'username': 'username',
'password': 'password',
'tenant_name': 'tenant_name',
'auth_url': 'http://no.where',
'os_endpoint': 'http://no.where',
'auth_plugin': 'fake_auth',
'token': '1234',
'user_domain_name': 'default',
'project_domain_name': 'default',
}
class ClientTest(utils.BaseTestCase):
@staticmethod
def create_client(env, api_version=1, endpoint=None, exclude=[]):
env = dict((k, v) for k, v in env.items()
if k not in exclude)
return client.get_client(api_version, **env)
def setUp(self):
super(ClientTest, self).setUp()
def test_client_version(self):
c1 = self.create_client(env=FAKE_ENV, api_version=1)
self.assertIsInstance(c1, v1client.Client)
def test_client_auth_lambda(self):
env = FAKE_ENV.copy()
env['token'] = lambda: env['token']
self.assertIsInstance(env['token'],
types.FunctionType)
c1 = self.create_client(env)
self.assertIsInstance(c1, v1client.Client)
def test_client_auth_non_lambda(self):
env = FAKE_ENV.copy()
env['token'] = "1234"
self.assertIsInstance(env['token'], str)
c1 = self.create_client(env)
self.assertIsInstance(c1, v1client.Client)
@mock.patch('keystoneclient.v2_0.client', fakes.FakeKeystone)
def test_client_without_auth_plugin(self):
env = FAKE_ENV.copy()
del env['auth_plugin']
c = self.create_client(env, api_version=1, endpoint='fake_endpoint')
self.assertIsInstance(c.auth_plugin, client.AuthPlugin)
def test_client_without_auth_plugin_keystone_v3(self):
env = FAKE_ENV.copy()
del env['auth_plugin']
expected = {
'username': 'username',
'endpoint': 'http://no.where',
'tenant_name': 'tenant_name',
'service_type': None,
'token': '1234',
'endpoint_type': None,
'auth_url': 'http://no.where',
'tenant_id': None,
'cacert': None,
'password': 'password',
'user_domain_name': 'default',
'user_domain_id': None,
'project_domain_name': 'default',
'project_domain_id': None,
}
with mock.patch('cloudkittyclient.client.AuthPlugin') as auth_plugin:
self.create_client(env, api_version=1)
auth_plugin.assert_called_with(**expected)
def test_client_with_auth_plugin(self):
c = self.create_client(FAKE_ENV, api_version=1)
self.assertIsInstance(c.auth_plugin, str)
def test_v1_client_timeout_invalid_value(self):
env = FAKE_ENV.copy()
env['timeout'] = 'abc'
self.assertRaises(ValueError, self.create_client, env)
env['timeout'] = '1.5'
self.assertRaises(ValueError, self.create_client, env)
def _test_v1_client_timeout_integer(self, timeout, expected_value):
env = FAKE_ENV.copy()
env['timeout'] = timeout
expected = {
'auth_plugin': 'fake_auth',
'timeout': expected_value,
'original_ip': None,
'http': None,
'region_name': None,
'verify': True,
'timings': None,
'keyring_saver': None,
'cert': None,
'endpoint_type': None,
'user_agent': None,
'debug': None,
}
cls = 'cloudkittyclient.openstack.common.apiclient.client.HTTPClient'
with mock.patch(cls) as mocked:
self.create_client(env)
mocked.assert_called_with(**expected)
def test_v1_client_timeout_zero(self):
self._test_v1_client_timeout_integer(0, None)
def test_v1_client_timeout_valid_value(self):
self._test_v1_client_timeout_integer(30, 30)
def test_v1_client_cacert_in_verify(self):
env = FAKE_ENV.copy()
env['cacert'] = '/path/to/cacert'
client = self.create_client(env)
self.assertEqual('/path/to/cacert', client.client.verify)
def test_v1_client_certfile_and_keyfile(self):
env = FAKE_ENV.copy()
env['cert_file'] = '/path/to/cert'
env['key_file'] = '/path/to/keycert'
client = self.create_client(env)
self.assertEqual(('/path/to/cert', '/path/to/keycert'),
client.client.cert)

View File

@@ -1,28 +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.
"""
test_cloudkittyclient
----------------------------------
Tests for `cloudkittyclient` module.
"""
from cloudkittyclient.tests import base
class TestCloudkittyclient(base.TestCase):
def test_something(self):
pass

View File

@@ -0,0 +1,37 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.tests import utils
from cloudkittyclient.v1 import collector
from cloudkittyclient.v1 import info
from cloudkittyclient.v1 import rating
from cloudkittyclient.v1.rating import hashmap
from cloudkittyclient.v1.rating import pyscripts
from cloudkittyclient.v1 import report
from cloudkittyclient.v1 import storage
class BaseAPIEndpointTestCase(utils.BaseTestCase):
def setUp(self):
super(BaseAPIEndpointTestCase, self).setUp()
self.api_client = utils.FakeHTTPClient()
self.storage = storage.StorageManager(self.api_client)
self.rating = rating.RatingManager(self.api_client)
self.collector = collector.CollectorManager(self.api_client)
self.info = info.InfoManager(self.api_client)
self.report = report.ReportManager(self.api_client)
self.pyscripts = pyscripts.PyscriptManager(self.api_client)
self.hashmap = hashmap.HashmapManager(self.api_client)

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v1 import base
class TestCollector(base.BaseAPIEndpointTestCase):
def test_get_mapping_no_args(self):
self.collector.get_mapping()
self.api_client.get.assert_called_once_with('/v1/collector/mappings/')
def test_get_mapping_service_id(self):
self.collector.get_mapping(service='testservice')
self.api_client.get.assert_called_once_with(
'/v1/collector/mappings/testservice')
def test_get_mapping_collector(self):
self.collector.get_mapping(collector='testcollector')
self.api_client.get.assert_called_once_with(
'/v1/collector/mappings/?collector=testcollector')
def test_get_mapping_collector_service_id(self):
self.collector.get_mapping(
service='testservice', collector='testcollector')
self.api_client.get.assert_called_once_with(
'/v1/collector/mappings/testservice?collector=testcollector')
def test_create_mapping(self):
kwargs = dict(service='testservice', collector='testcollector')
self.collector.create_mapping(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/collector/mappings/', json=kwargs)
def test_create_mapping_no_name(self):
self.assertRaises(exc.ArgumentRequired,
self.collector.create_mapping,
collector='testcollector')
def test_delete_mapping(self):
kwargs = dict(service='testservice')
self.collector.delete_mapping(**kwargs)
self.api_client.delete.assert_called_once_with(
'/v1/collector/mappings/', json=kwargs)
def test_delete_mapping_no_service(self):
self.assertRaises(exc.ArgumentRequired,
self.collector.create_mapping)
def test_get_state(self):
self.collector.get_state(name='testcollector')
self.api_client.get.assert_called_once_with(
'/v1/collector/states/?name=testcollector')
def test_set_state(self):
kwargs = dict(name='testcollector', enabled=True)
self.collector.set_state(**kwargs)
self.api_client.put.assert_called_once_with(
'/v1/collector/states/', json=kwargs)

View File

@@ -0,0 +1,345 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from unittest import mock
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v1 import base
from cloudkittyclient.tests import utils
class TestHashmap(base.BaseAPIEndpointTestCase):
def test_get_mapping_types(self):
self.hashmap.get_mapping_types()
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/types/')
def test_get_service(self):
self.hashmap.get_service()
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/services/')
def test_get_service_service_id(self):
self.hashmap.get_service(service_id='service_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/services/service_id')
def test_create_service(self):
kwargs = dict(name='service')
self.hashmap.create_service(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/hashmap/services/', json=kwargs)
def test_create_service_no_name(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_service)
def test_delete_service(self):
self.hashmap.delete_service(service_id='service_id')
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/hashmap/services/',
json={'service_id': 'service_id'})
def test_delete_service_no_id(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_service)
def test_get_fields_of_service(self):
self.hashmap.get_field(service_id='service_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/fields/?service_id=service_id')
def test_get_field(self):
self.hashmap.get_field(field_id='field_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/fields/field_id')
def test_get_field_no_args(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.get_field)
def test_get_field_with_service_id_and_field_id(self):
self.assertRaises(exc.InvalidArgumentError, self.hashmap.get_field,
service_id='service_id', field_id='field_id')
def test_create_field(self):
kwargs = dict(name='name', service_id='service_id')
self.hashmap.create_field(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/hashmap/fields/', json=kwargs)
def test_create_field_no_name(self):
self.assertRaises(exc.ArgumentRequired,
self.hashmap.create_field,
service_id='service_id')
def test_create_field_no_service_id(self):
self.assertRaises(
exc.ArgumentRequired, self.hashmap.create_field, name='name')
def test_delete_field(self):
kwargs = dict(field_id='field_id')
self.hashmap.delete_field(**kwargs)
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/hashmap/fields/', json=kwargs)
def test_delete_field_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_field)
def test_get_mapping_with_id(self):
self.hashmap.get_mapping(mapping_id='mapping_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/mappings/mapping_id')
def test_get_mapping_service_id(self):
self.hashmap.get_mapping(service_id='service_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/mappings/?service_id=service_id')
def test_get_mapping_no_args(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping)
def test_create_mapping(self):
kwargs = dict(cost=2, value='value', field_id='field_id',
name='name', start="2024-01-01",
end="2024-01-01",
description="description")
body = dict(
cost=kwargs.get('cost'),
value=kwargs.get('value'),
service_id=kwargs.get('service_id'),
field_id=kwargs.get('field_id'),
group_id=kwargs.get('group_id'),
tenant_id=kwargs.get('tenant_id'),
type=kwargs.get('type') or 'flat',
start="2024-01-01",
end="2024-01-01",
description="description",
name='name'
)
self.hashmap.create_mapping(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/hashmap/mappings/', json=body)
def test_create_mapping_no_cost(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_mapping,
value='value', field_id='field_id')
def test_create_mapping_no_id(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_mapping,
value='value', cost=12)
def test_create_mapping_field_and_service_id(self):
self.assertRaises(
exc.InvalidArgumentError, self.hashmap.create_mapping, cost=12,
field_id='field_id', service_id='service_id')
def test_create_mapping_value_and_service_id(self):
self.assertRaises(
exc.InvalidArgumentError, self.hashmap.create_mapping,
value='value', service_id='service_id', cost=0.8)
def test_update_mapping(self):
kwargs = dict(
cost=12,
value='value',
service_id='service_id',
field_id='field_id',
tenant_id='tenant_id',
type='type',
mapping_id='mapping_id',
)
fake_get = mock.Mock(return_value=utils.FakeRequest(
cost='Bad value',
value='Bad value',
service_id='Bad value',
field_id='Bad value',
tenant_id='Bad value',
type='Bad value',
mapping_id='mapping_id',
))
self.api_client.get = fake_get
self.hashmap.update_mapping(**kwargs)
self.api_client.get.assert_called_with(
'/v1/rating/module_config/hashmap/mappings/mapping_id')
self.api_client.put.assert_called_once_with(
'/v1/rating/module_config/hashmap/mappings/', json=kwargs)
def test_update_mapping_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.update_mapping)
def test_get_mapping_group(self):
self.hashmap.get_mapping_group(mapping_id='mapping_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/'
'hashmap/mappings/group?mapping_id=mapping_id')
def test_delete_mapping(self):
kwargs = dict(mapping_id='mapping_id')
self.hashmap.delete_mapping(**kwargs)
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/hashmap/mappings/', json=kwargs)
def test_delete_mapping_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_mapping)
def test_get_mapping_group_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping_group)
def test_get_group_no_arg(self):
self.hashmap.get_group()
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/')
def test_get_group(self):
self.hashmap.get_group(group_id='group_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/group_id')
def test_create_group(self):
kwargs = dict(name='group')
self.hashmap.create_group(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/',
json=kwargs)
def test_create_group_no_name(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_group)
def test_delete_group(self):
kwargs = dict(group_id='group_id')
self.hashmap.delete_group(**kwargs)
kwargs['recursive'] = False
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/',
json=kwargs)
def test_delete_group_recursive(self):
kwargs = dict(group_id='group_id', recursive=True)
self.hashmap.delete_group(**kwargs)
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/',
json=kwargs)
def test_delete_group_no_id(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_group)
def test_get_group_mappings(self):
self.hashmap.get_group_mappings(group_id='group_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/mappings'
'?group_id=group_id')
def test_get_group_mappings_no_args(self):
self.assertRaises(
exc.ArgumentRequired, self.hashmap.get_group_mappings)
def test_get_group_thresholds(self):
self.hashmap.get_group_thresholds(group_id='group_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/groups/thresholds'
'?group_id=group_id')
def test_get_group_thresholds_no_args(self):
self.assertRaises(
exc.ArgumentRequired, self.hashmap.get_group_thresholds)
def test_get_threshold_with_id(self):
self.hashmap.get_threshold(threshold_id='threshold_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/thresholds/threshold_id')
def test_get_threshold_service_id(self):
self.hashmap.get_threshold(service_id='service_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/hashmap/thresholds/'
'?service_id=service_id')
def test_get_threshold_no_args(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.get_threshold)
def test_create_threshold(self):
kwargs = dict(cost=2, level=123, field_id='field_id')
body = dict(
cost=kwargs.get('cost'),
level=kwargs.get('level'),
service_id=kwargs.get('service_id'),
field_id=kwargs.get('field_id'),
group_id=kwargs.get('group_id'),
tenant_id=kwargs.get('tenant_id'),
type=kwargs.get('type') or 'flat',
)
self.hashmap.create_threshold(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/hashmap/thresholds/', json=body)
def test_create_threshold_no_cost(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_threshold,
level=123, field_id='field_id')
def test_create_threshold_no_id(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.create_threshold,
level=123, cost=12)
def test_create_threshold_field_and_service_id(self):
self.assertRaises(
exc.ArgumentRequired, self.hashmap.create_threshold, cost=12,
field_id='field_id', service_id='service_id')
def test_delete_threshold(self):
kwargs = dict(threshold_id='threshold_id')
self.hashmap.delete_threshold(**kwargs)
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/hashmap/thresholds/', json=kwargs)
def test_delete_threshold_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_threshold)
def test_update_threshold(self):
kwargs = dict(
cost=12,
level=123,
service_id='service_id',
field_id='field_id',
tenant_id='tenant_id',
type='type',
threshold_id='threshold_id'
)
fake_get = mock.Mock(return_value=utils.FakeRequest(
cost='Bad value',
level='Bad value',
service_id='Bad value',
field_id='Bad value',
tenant_id='Bad value',
type='Bad value',
threshold_id='threshold_id'
))
self.api_client.get = fake_get
self.hashmap.update_threshold(**kwargs)
self.api_client.get.assert_called_with(
'/v1/rating/module_config/hashmap/thresholds/threshold_id')
self.api_client.put.assert_called_once_with(
'/v1/rating/module_config/hashmap/thresholds/', json=kwargs)
def test_update_threshold_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.hashmap.update_threshold)
def test_get_threshold_group(self):
self.hashmap.get_threshold_group(threshold_id='threshold_id')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/'
'hashmap/thresholds/group?threshold_id=threshold_id')
def test_get_threshold_group_no_arg(self):
self.assertRaises(
exc.ArgumentRequired, self.hashmap.get_threshold_group)

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.unit.v1 import base
class TestInfo(base.BaseAPIEndpointTestCase):
def test_get_metric(self):
self.info.get_metric()
self.api_client.get.assert_called_once_with('/v1/info/metrics/')
def test_get_metric_with_arg(self):
self.info.get_metric(metric_name='testmetric')
self.api_client.get.assert_called_once_with(
'/v1/info/metrics/testmetric')
def test_get_config(self):
self.info.get_config()
self.api_client.get.assert_called_once_with('/v1/info/config/')

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v1 import base
class TestPyscripts(base.BaseAPIEndpointTestCase):
def test_list_scripts(self):
self.pyscripts.list_scripts()
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/')
def test_list_scripts_no_data(self):
self.pyscripts.list_scripts(no_data=True)
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/?no_data=True')
def test_get_script(self):
self.pyscripts.get_script(script_id='testscript')
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/testscript')
def test_get_script_no_arg(self):
self.assertRaises(exc.ArgumentRequired, self.pyscripts.get_script)
def test_create_script(self):
kwargs = dict(name='name', data='data', start=None,
end=None, description=None)
self.pyscripts.create_script(**kwargs)
self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/', json=kwargs)
def test_create_script_no_data(self):
self.assertRaises(
exc.ArgumentRequired, self.pyscripts.create_script, name='name')
def test_create_script_no_name(self):
self.assertRaises(
exc.ArgumentRequired, self.pyscripts.create_script, data='data')
def test_update_script(self):
args = dict(script_id='script_id', name='name', data='data')
self.pyscripts.update_script(**args)
self.api_client.get.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/script_id')
args.pop('script_id', None)
self.api_client.put.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/script_id', json=args)
def test_update_script_no_script_id(self):
self.assertRaises(
exc.ArgumentRequired, self.pyscripts.update_script, name='name')
def test_delete_script(self):
kwargs = dict(script_id='script_id')
self.pyscripts.delete_script(**kwargs)
self.api_client.delete.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/script_id')

View File

@@ -0,0 +1,48 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import decimal
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v1 import base
class TestRating(base.BaseAPIEndpointTestCase):
def test_quote_request(self):
res_data = [{'usage': {
'instance': [{
'vol': {'unit': 'undef', 'qty': '1'},
'rating': {'price': decimal.Decimal(1)},
'desc': {
'disk_total_display': 1,
'image_id': 'c43a3e7d-c4e6-45d6-8c8d-e2832a45bc0a',
'ram': 64,
'ephemeral': 0,
'vcpus': 1,
'source_type': 'image',
'disk_total': 1,
'flavor_id': '42',
'flavor': 'm1.nano',
'disk': 1,
'source_val': 'c43a3e7d-c4e6-45d6-8c8d-e2832a45bc0a'}
}]
}}]
self.rating.get_quotation(res_data=res_data)
self.api_client.post.assert_called_once_with(
'/v1/rating/quote/', json={'resources': res_data})
def test_get_quotation_no_res_data(self):
self.assertRaises(exc.ArgumentRequired, self.rating.get_quotation)

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.unit.v1 import base
class TestReport(base.BaseAPIEndpointTestCase):
def test_get_summary(self):
self.report.get_summary()
self.api_client.get.assert_called_once_with('/v1/report/summary')
def test_get_summary_with_groupby(self):
self.report.get_summary(groupby=['res_type', 'tenant_id'])
self.api_client.get.assert_called_once_with(
'/v1/report/summary?groupby=res_type%2Ctenant_id')
def test_get_summary_with_begin_end(self):
self.report.get_summary(begin='begin', end='end')
try:
self.api_client.get.assert_called_once_with(
'/v1/report/summary?begin=begin&end=end')
# Passing a dict to urlencode can change arg order
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v1/report/summary?end=end&begin=begin')
def test_get_total(self):
self.report.get_total()
self.api_client.get.assert_called_once_with('/v1/report/total')
def test_get_total_with_begin_end(self):
self.report.get_total(begin='begin', end='end')
try:
self.api_client.get.assert_called_once_with(
'/v1/report/total?begin=begin&end=end')
# Passing a dict to urlencode can change arg order
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v1/report/total?end=end&begin=begin')
def test_get_tenants(self):
self.report.get_tenants()
self.api_client.get.assert_called_once_with('/v1/report/tenants')
def test_get_tenants_with_begin_end(self):
self.report.get_tenants(begin='begin', end='end')
try:
self.api_client.get.assert_called_once_with(
'/v1/report/tenants?begin=begin&end=end')
# Passing a dict to urlencode can change arg order
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v1/report/tenants?end=end&begin=begin')

View File

@@ -0,0 +1,63 @@
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from collections import abc
from unittest import mock
from cloudkittyclient.tests.unit.v1 import base
from cloudkittyclient.v1 import report_cli
class TestReportCli(base.BaseAPIEndpointTestCase):
def test_report_tenant_list(self):
class DummyAPIClient(object):
def get_tenants(*args, **kwargs):
return ['ee530dfc-319a-438f-9d43-346cfef501d6',
'91743a9a-688b-4526-b568-7b501531176c',
'4468704c-972e-4cfd-a342-9b71c493b79b']
class ClientWrap(object):
report = DummyAPIClient()
class DummyParsedArgs(object):
def __init__(self):
self.begin = '2042-01-01T00:00:00'
self.end = '2042-12-01T00:00:00'
class DummyCliTenantList(report_cli.CliTenantList):
def __init__(self):
pass
def __get_client_from_osc(*args):
return ClientWrap()
parsed_args = DummyParsedArgs()
cli_class_instance = DummyCliTenantList()
with mock.patch('cloudkittyclient.utils.get_client_from_osc',
new=__get_client_from_osc):
# NOTE(peschk_l): self is only used used to get a client so just we
# just override __init__ in order to skip class instanciation. In
# python3 we could just have passed None
result = report_cli.CliTenantList.take_action(
cli_class_instance, parsed_args)
assert len(result) == 2
assert result[0] == ('Tenant ID', )
assert isinstance(result[1], abc.Iterable)
for res in result[1]:
assert isinstance(res, abc.Iterable)

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.unit.v1 import base
class TestStorage(base.BaseAPIEndpointTestCase):
def test_get_dataframes(self):
self.storage.get_dataframes()
self.api_client.get.assert_called_once_with('/v1/storage/dataframes')
def test_get_dataframes_with_begin_end(self):
self.storage.get_dataframes(begin='begin', end='end')
try:
self.api_client.get.assert_called_once_with(
'/v1/storage/dataframes?begin=begin&end=end')
# Passing a dict to urlencode can change arg order
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v1/storage/dataframes?end=end&begin=begin')

View File

@@ -0,0 +1,31 @@
# Copyright 2019 objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.tests import utils
from cloudkittyclient.v2 import dataframes
from cloudkittyclient.v2.rating import modules
from cloudkittyclient.v2 import scope
from cloudkittyclient.v2 import summary
class BaseAPIEndpointTestCase(utils.BaseTestCase):
def setUp(self):
super(BaseAPIEndpointTestCase, self).setUp()
self.api_client = utils.FakeHTTPClient()
self.dataframes = dataframes.DataframesManager(self.api_client)
self.scope = scope.ScopeManager(self.api_client)
self.summary = summary.SummaryManager(self.api_client)
self.rating = modules.RatingManager(self.api_client)

View File

@@ -0,0 +1,172 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import json
from collections import OrderedDict
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v2 import base
class TestDataframes(base.BaseAPIEndpointTestCase):
dataframes_data = """
{
"dataframes": [
{
"period": {
"begin": "20190723T122810Z",
"end": "20190723T132810Z"
},
"usage": {
"metric_one": [
{
"vol": {
"unit": "GiB",
"qty": 1.2
},
"rating": {
"price": 0.04
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
],
"metric_two": [
{
"vol": {
"unit": "MB",
"qty": 200.4
},
"rating": {
"price": 0.06
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
]
}
},
{
"period": {
"begin": "20190823T122810Z",
"end": "20190823T132810Z"
},
"usage": {
"metric_one": [
{
"vol": {
"unit": "GiB",
"qty": 2.4
},
"rating": {
"price": 0.08
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
],
"metric_two": [
{
"vol": {
"unit": "MB",
"qty": 400.8
},
"rating": {
"price": 0.12
},
"groupby": {
"group_one": "one",
"group_two": "two"
},
"metadata": {
"attr_one": "one",
"attr_two": "two"
}
}
]
}
}
]
}
"""
def test_add_dataframes_with_string(self):
self.dataframes.add_dataframes(
dataframes=self.dataframes_data,
)
self.api_client.post.assert_called_once_with(
'/v2/dataframes',
data=self.dataframes_data,
)
def test_add_dataframes_with_json_object(self):
json_data = json.loads(self.dataframes_data)
self.dataframes.add_dataframes(
dataframes=json_data,
)
self.api_client.post.assert_called_once_with(
'/v2/dataframes',
data=json.dumps(json_data),
)
def test_add_dataframes_with_neither_string_nor_object_raises_exc(self):
self.assertRaises(
exc.InvalidArgumentError,
self.dataframes.add_dataframes,
dataframes=[open],
)
def test_add_dataframes_with_no_args_raises_exc(self):
self.assertRaises(
exc.ArgumentRequired,
self.dataframes.add_dataframes)
def test_get_dataframes(self):
self.dataframes.get_dataframes()
self.api_client.get.assert_called_once_with('/v2/dataframes')
def test_get_dataframes_with_pagination_args(self):
self.dataframes.get_dataframes(offset=10, limit=10)
try:
self.api_client.get.assert_called_once_with(
'/v2/dataframes?limit=10&offset=10')
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v2/dataframes?offset=10&limit=10')
def test_get_dataframes_filters(self):
self.dataframes.get_dataframes(
filters=OrderedDict([('one', 'two'), ('three', 'four')]))
self.api_client.get.assert_called_once_with(
'/v2/dataframes?filters=one%3Atwo%2Cthree%3Afour')

View File

@@ -0,0 +1,38 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.tests.unit.v2 import base
class TestRating(base.BaseAPIEndpointTestCase):
def test_get_modules(self):
self.rating.get_module()
self.api_client.get.assert_called_once_with('/v2/rating/modules')
def test_get_one_module(self):
self.rating.get_module(module_id="moduleidtest")
self.api_client.get.assert_called_once_with(
'/v2/rating/modules/moduleidtest')
def test_update_one_module(self):
self.rating.update_module(module_id="moduleidtest",
enabled=False, priority=42)
self.api_client.put.assert_called_once_with(
'/v2/rating/modules/moduleidtest',
json={
'enabled': False,
'priority': 42,
})

View File

@@ -0,0 +1,88 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v2 import base
import datetime
class TestScope(base.BaseAPIEndpointTestCase):
def test_get_scope(self):
self.scope.get_scope_state()
self.api_client.get.assert_called_once_with('/v2/scope')
def test_get_scope_with_args(self):
self.scope.get_scope_state(offset=10, limit=10)
try:
self.api_client.get.assert_called_once_with(
'/v2/scope?limit=10&offset=10')
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v2/scope?offset=10&limit=10')
def test_reset_scope_with_args(self):
self.scope.reset_scope_state(
state=datetime.datetime(2019, 5, 7),
all_scopes=True)
self.api_client.put.assert_called_once_with(
'/v2/scope',
json={
'state': datetime.datetime(2019, 5, 7),
'all_scopes': True,
})
def test_reset_scope_with_list_args(self):
self.scope.reset_scope_state(
state=datetime.datetime(2019, 5, 7),
scope_id=['id1', 'id2'],
all_scopes=False)
self.api_client.put.assert_called_once_with(
'/v2/scope',
json={
'state': datetime.datetime(2019, 5, 7),
'scope_id': 'id1,id2',
})
def test_reset_scope_strips_none_and_false_args(self):
self.scope.reset_scope_state(
state=datetime.datetime(2019, 5, 7),
all_scopes=False,
scope_key=None,
scope_id=['id1', 'id2'])
self.api_client.put.assert_called_once_with(
'/v2/scope',
json={
'state': datetime.datetime(2019, 5, 7),
'scope_id': 'id1,id2',
})
def test_reset_scope_with_no_args_raises_exc(self):
self.assertRaises(
exc.ArgumentRequired,
self.scope.reset_scope_state)
def test_reset_scope_with_lacking_args_raises_exc(self):
self.assertRaises(
exc.ArgumentRequired,
self.scope.reset_scope_state,
state=datetime.datetime(2019, 5, 7))
def test_reset_scope_with_both_args_raises_exc(self):
self.assertRaises(
exc.InvalidArgumentError,
self.scope.reset_scope_state,
state=datetime.datetime(2019, 5, 7),
scope_id=['id1', 'id2'],
all_scopes=True)

View File

@@ -0,0 +1,39 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from collections import OrderedDict
from cloudkittyclient.tests.unit.v2 import base
class TestSummary(base.BaseAPIEndpointTestCase):
def test_get_summary(self):
self.summary.get_summary()
self.api_client.get.assert_called_once_with('/v2/summary')
def test_get_summary_with_pagination_args(self):
self.summary.get_summary(offset=10, limit=10)
try:
self.api_client.get.assert_called_once_with(
'/v2/summary?limit=10&offset=10')
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v2/summary?offset=10&limit=10')
def test_get_summary_filters(self):
self.summary.get_summary(
filters=OrderedDict([('one', 'two'), ('three', 'four')]))
self.api_client.get.assert_called_once_with(
'/v2/summary?filters=one%3Atwo%2Cthree%3Afour')

View File

@@ -12,13 +12,33 @@
# 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 unittest import mock
import fixtures
import testtools
from keystoneauth1 import adapter
from keystoneauth1 import session
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
self.useFixture(fixtures.FakeLogger())
class FakeRequest(dict):
"""Fake requests.Request object."""
def json(self):
return self
class FakeHTTPClient(adapter.Adapter):
"""Keystone HTTP adapter with request methods being mocks"""
def __init__(self):
super(FakeHTTPClient, self).__init__(session=session.Session())
for attr in ('get', 'put', 'post', 'delete'):
setattr(self, attr, mock.Mock(return_value=FakeRequest()))

View File

@@ -1,139 +0,0 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import utils
import cloudkittyclient.v1.core
fixtures = {
'/v1/rating/modules': {
'GET': (
{},
{'modules': [
{
'module_id': 'hashmap',
'enabled': True,
},
{
'module_id': 'noop',
'enabled': False,
},
]},
),
},
'/v1/rating/modules/hashmap': {
'GET': (
{},
{
'module_id': 'hashmap',
'enabled': True,
}
),
'PUT': (
{},
{
'module_id': 'hashmap',
'enabled': False,
}
),
},
'/v1/rating/modules/noop': {
'GET': (
{},
{
'module_id': 'noop',
'enabled': False,
}
),
'PUT': (
{},
{
'module_id': 'noop',
'enabled': True,
}
),
},
'/v1/collectors': {
'GET': (
{},
{'collectors': [
{
'module_id': 'ceilo',
'enabled': True,
},
]},
),
},
}
class CloudkittyModuleManagerTest(utils.BaseTestCase):
def setUp(self):
super(CloudkittyModuleManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = cloudkittyclient.v1.core.CloudkittyModuleManager(self.api)
def test_list_all(self):
resources = list(self.mgr.list())
expect = [
'GET', '/v1/rating/modules'
]
self.http_client.assert_called(*expect)
self.assertEqual(len(resources), 2)
self.assertEqual(resources[0].module_id, 'hashmap')
self.assertEqual(resources[1].module_id, 'noop')
def test_get_module_status(self):
resource = self.mgr.get(module_id='hashmap')
expect = [
'GET', '/v1/rating/modules/hashmap'
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.module_id, 'hashmap')
self.assertEqual(resource.enabled, True)
class CloudkittyModuleTest(utils.BaseTestCase):
def setUp(self):
super(CloudkittyModuleTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = cloudkittyclient.v1.core.CloudkittyModuleManager(self.api)
def test_enable(self):
self.ck_module = self.mgr.get(module_id='noop')
self.ck_module.enable()
# PUT /v1/rating/modules/noop
# body : {'enabled': True}
expect = [
'PUT', '/v1/rating/modules/noop', {'module_id': 'noop',
'enabled': True},
]
self.http_client.assert_called(*expect)
def test_disable(self):
self.ck_module = self.mgr.get(module_id='hashmap')
self.ck_module.disable()
# PUT /v1/rating/modules/hashmap
# body : {'enabled': False}
expect = [
'PUT', '/v1/rating/modules/hashmap', {'module_id': 'hashmap',
'enabled': False},
]
self.http_client.assert_called(*expect)

View File

@@ -1,513 +0,0 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.openstack.common.apiclient import fake_client
from cloudkittyclient.tests import utils
from cloudkittyclient.v1.rating import hashmap
fixtures = {
# services
'/v1/rating/module_config/hashmap/services': {
'GET': (
{},
{'services':
[
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'compute'
},
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd6',
'name': 'volume'
},
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd7',
'name': 'network'
},
],
}
),
},
# a service
('/v1/rating/module_config/hashmap/services/'
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'compute',
}
),
'DELETE': (
{},
{},
),
},
# a field
('/v1/rating/module_config/hashmap/fields/'
'a53db546-bac0-472c-be4b-5bf9f6117581'): {
'GET': (
{},
{
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'flavor',
},
),
'PUT': (
{},
{},
),
},
('/v1/rating/module_config/hashmap/fields'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{'fields': [
{
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'flavor',
},
{
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117582',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'LOLOL',
},
]
},
),
'PUT': (
{},
{},
),
},
# a mapping
('/v1/rating/module_config/hashmap/mappings/'
'bff0d209-a8e4-46f8-8c1a-f231db375dcb'): {
'GET': (
{},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'group_id': None,
'value': 'm1.small',
'cost': 0.50,
'type': 'flat',
},
),
'PUT': (
{},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581',
'group_id': None,
'value': 'm1.small',
'cost': 0.20,
'type': 'flat',
},
),
},
# some mappings
('/v1/rating/module_config/hashmap/mappings'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{'mappings':
[
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': None,
'group_id': None,
'value': 'm1.small',
'cost': 0.50,
'type': 'flat',
},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcc',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': None,
'group_id': None,
'value': 'm1.tiny',
'cost': 1.10,
'type': 'flat',
},
{
'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcd',
'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5',
'field_id': None,
'group_id': None,
'value': 'm1.big',
'cost': 1.50,
'type': 'flat',
},
],
}
),
'PUT': (
{},
{},
),
},
'/v1/rating/module_config/hashmap/groups': {
'GET': (
{},
{'groups':
[
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'object_consumption'
},
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd6',
'name': 'compute_instance'
},
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd7',
'name': 'netowrking'
},
],
}
),
},
('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'): {
'GET': (
{},
{
'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5',
'name': 'object_consumption'
},
),
'DELETE': (
{},
{},
),
},
('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5?recursive=True'): {
'DELETE': (
{},
{},
),
},
# a threshold
('/v1/rating/module_config/hashmap/thresholds/'
'1f136864-be73-481f-b9be-4fbda2496f72'): {
'GET': (
{},
{
'threshold_id': '1f136864-be73-481f-b9be-4fbda2496f72',
'service_id': '1329d62f-bd1c-4a88-a75a-07545e41e8d7',
'field_id': 'c7c28d87-5103-4a05-af7f-e4d0891cb7fc',
'group_id': None,
'level': 30,
'cost': 5.98,
'map_type': 'flat',
},
),
'PUT': (
{},
{
'threshold_id': '1f136864-be73-481f-b9be-4fbda2496f72',
'service_id': '1329d62f-bd1c-4a88-a75a-07545e41e8d7',
'field_id': 'c7c28d87-5103-4a05-af7f-e4d0891cb7fc',
'group_id': None,
'level': 30,
'cost': 5.99,
'type': 'flat',
},
),
'DELETE': (
{},
{},
),
},
}
class ServiceManagerTest(utils.BaseTestCase):
def setUp(self):
super(ServiceManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.ServiceManager(self.api)
def test_list_services(self):
resources = list(self.mgr.list())
expect = [
'GET', '/v1/rating/module_config/hashmap/services'
]
self.http_client.assert_called(*expect)
self.assertEqual(len(resources), 3)
self.assertEqual(
resources[0].service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.assertEqual(resources[0].name, 'compute')
self.assertEqual(resources[1].name, 'volume')
self.assertEqual(resources[2].name, 'network')
def test_get_a_service(self):
resource = self.mgr.get(
service_id='2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
expect = [
'GET', ('/v1/rating/module_config/hashmap/services/'
'2451c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5')
self.assertEqual(resource.name, 'compute')
class ServiceTest(utils.BaseTestCase):
def setUp(self):
super(ServiceTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.ServiceManager(self.api)
self.resource = self.mgr.get(
service_id='2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
def test_get_fields(self):
fields = self.resource.fields[:]
expect = [
'GET', ('/v1/rating/module_config/hashmap/fields'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'),
]
self.http_client.assert_called(*expect)
self.assertEqual(len(fields), 2)
def test_get_mappings(self):
mappings = self.resource.mappings[:]
expect = [
'GET', ('/v1/rating/module_config/hashmap/mappings'
'?service_id=2451c2e0-2c6b-4e75-987f-93661eef0fd5'),
]
self.http_client.assert_called(*expect)
self.assertEqual(len(mappings), 3)
class FieldManagerTest(utils.BaseTestCase):
def setUp(self):
super(FieldManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.FieldManager(self.api)
def test_get_a_field(self):
resource = self.mgr.get(
field_id='a53db546-bac0-472c-be4b-5bf9f6117581'
)
expect = [
'GET', ('/v1/rating/module_config/hashmap/fields/'
'a53db546-bac0-472c-be4b-5bf9f6117581')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.field_id,
'a53db546-bac0-472c-be4b-5bf9f6117581')
self.assertEqual(
resource.service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.assertEqual(resource.name, 'flavor')
class MappingManagerTest(utils.BaseTestCase):
def setUp(self):
super(MappingManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.MappingManager(self.api)
def test_get_a_mapping(self):
resource = self.mgr.get(
mapping_id='bff0d209-a8e4-46f8-8c1a-f231db375dcb'
)
expect = [
'GET', ('/v1/rating/module_config/hashmap/mappings/'
'bff0d209-a8e4-46f8-8c1a-f231db375dcb')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.mapping_id,
'bff0d209-a8e4-46f8-8c1a-f231db375dcb')
self.assertEqual(
resource.service_id,
'2451c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.assertEqual(
resource.field_id,
'a53db546-bac0-472c-be4b-5bf9f6117581'
)
self.assertEqual(resource.value, 'm1.small')
self.assertEqual(resource.cost, 0.5)
def test_update_a_mapping(self):
resource = self.mgr.get(
mapping_id='bff0d209-a8e4-46f8-8c1a-f231db375dcb'
)
resource.cost = 0.2
self.mgr.update(**resource.dirty_fields)
expect = [
'PUT', ('/v1/rating/module_config/hashmap/mappings/'
'bff0d209-a8e4-46f8-8c1a-f231db375dcb'),
{u'mapping_id': u'bff0d209-a8e4-46f8-8c1a-f231db375dcb',
u'cost': 0.2, u'type': u'flat',
u'service_id': u'2451c2e0-2c6b-4e75-987f-93661eef0fd5',
u'field_id': u'a53db546-bac0-472c-be4b-5bf9f6117581',
u'value': u'm1.small'}
]
self.http_client.assert_called(*expect)
class GroupManagerTest(utils.BaseTestCase):
def setUp(self):
super(GroupManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.GroupManager(self.api)
def test_get_a_group(self):
resource = self.mgr.get(
group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
)
expect = [
'GET', ('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.group_id,
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
self.assertEqual(resource.name, 'object_consumption')
def test_delete_a_group(self):
self.mgr.delete(group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
expect = [
'DELETE', ('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
def test_delete_a_group_recursively(self):
self.mgr.delete(group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5',
recursive=True)
expect = [
'DELETE', ('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5?recursive=True')
]
self.http_client.assert_called(*expect)
class GroupTest(utils.BaseTestCase):
def setUp(self):
super(GroupTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.GroupManager(self.api)
def test_delete(self):
self.group = self.mgr.get(
group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.group.delete()
# DELETE /v1/rating/groups/aaa1c2e0-2c6b-4e75-987f-93661eef0fd5
expect = [
'DELETE', ('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5')
]
self.http_client.assert_called(*expect)
def test_delete_recursive(self):
self.group = self.mgr.get(
group_id='aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
)
self.group.delete(recursive=True)
# DELETE
# /v1/rating/groups/aaa1c2e0-2c6b-4e75-987f-93661eef0fd5?recusrive=True
expect = [
'DELETE', ('/v1/rating/module_config/hashmap/groups/'
'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5'
'?recursive=True')
]
self.http_client.assert_called(*expect)
class ThresholdManagerTest(utils.BaseTestCase):
def setUp(self):
super(ThresholdManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
self.api = client.BaseClient(self.http_client)
self.mgr = hashmap.ThresholdManager(self.api)
def test_get_a_threshold(self):
resource = self.mgr.get(
threshold_id='1f136864-be73-481f-b9be-4fbda2496f72'
)
expect = [
'GET', ('/v1/rating/module_config/hashmap/thresholds/'
'1f136864-be73-481f-b9be-4fbda2496f72')
]
self.http_client.assert_called(*expect)
self.assertEqual(resource.threshold_id,
'1f136864-be73-481f-b9be-4fbda2496f72')
self.assertEqual(
resource.service_id,
'1329d62f-bd1c-4a88-a75a-07545e41e8d7'
)
self.assertEqual(
resource.field_id,
'c7c28d87-5103-4a05-af7f-e4d0891cb7fc'
)
self.assertEqual(resource.level, 30)
self.assertEqual(resource.cost, 5.98)
def test_update_a_threshold(self):
resource = self.mgr.get(
threshold_id='1f136864-be73-481f-b9be-4fbda2496f72'
)
resource.cost = 5.99
self.mgr.update(**resource.dirty_fields)
expect = [
'PUT', ('/v1/rating/module_config/hashmap/thresholds/'
'1f136864-be73-481f-b9be-4fbda2496f72'),
{u'threshold_id': u'1f136864-be73-481f-b9be-4fbda2496f72',
u'cost': 5.99, u'map_type': u'flat',
u'service_id': u'1329d62f-bd1c-4a88-a75a-07545e41e8d7',
u'field_id': u'c7c28d87-5103-4a05-af7f-e4d0891cb7fc',
u'level': 30}
]
self.http_client.assert_called(*expect)
def test_delete_a_threshold(self):
self.mgr.delete(threshold_id='1f136864-be73-481f-b9be-4fbda2496f72')
expect = [
'DELETE', ('/v1/rating/module_config/hashmap/thresholds/'
'1f136864-be73-481f-b9be-4fbda2496f72')
]
self.http_client.assert_called(*expect)

96
cloudkittyclient/utils.py Normal file
View File

@@ -0,0 +1,96 @@
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import inspect
import pbr.version
from keystoneauth1.exceptions import http
from oslo_utils import timeutils
def get_version():
"""Returns cloudkittyclient's version."""
return pbr.version.VersionInfo('python-cloudkittyclient').version_string()
def iso2dt(iso_date):
"""iso8601 format to datetime."""
iso_dt = timeutils.parse_isotime(iso_date)
trans_dt = timeutils.normalize_time(iso_dt)
return trans_dt
def get_client_from_osc(obj):
if hasattr(obj.app, 'client_manager'):
return obj.app.client_manager.rating
return obj.app.client
def dict_to_cols(dict_obj, cols):
"""Converts a dict to a cliff-compatible value list.
For cliff lister.Lister objects, you should use list_to_cols() instead
of this function.
'cols' shouls be a list of (key, Name) tuples.
"""
values = []
for col in cols:
values.append(dict_obj.get(col[0]))
return values
def list_to_cols(list_obj, cols):
if not isinstance(list_obj, list):
list_obj = [list_obj]
values = []
for item in list_obj:
values.append(dict_to_cols(item, cols))
return values
def http_error_formatter(func):
"""This decorator catches Http Errors and re-formats them"""
def wrap(*args, **kwargs):
try:
return func(*args, **kwargs)
except http.HttpError as e:
raise http.HttpError(message=e.response.text,
http_status=e.http_status)
return wrap
def format_http_errors(ignore):
"""Applies ``http_error_formatter`` to all methods of a class.
:param ignore: List of function names to ignore
:type ignore: iterable
"""
def wrap(cls):
def predicate(item):
# This avoids decorating functions of parent classes
return (inspect.isfunction(item)
and item.__name__ not in ignore
and not item.__name__.startswith('_')
and cls.__name__ in item.__qualname__)
for name, func in inspect.getmembers(cls, predicate):
setattr(cls, name, http_error_formatter(func))
return cls
return wrap

View File

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

View File

@@ -1,5 +1,5 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -12,58 +12,33 @@
# 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 stevedore import extension
from cloudkittyclient import client as ckclient
from cloudkittyclient.openstack.common.apiclient import client
from cloudkittyclient.v1 import core
#
from cloudkittyclient.common import client
from cloudkittyclient.v1 import collector
from cloudkittyclient.v1 import info
from cloudkittyclient.v1 import rating
from cloudkittyclient.v1 import report
from cloudkittyclient.v1 import storage
SUBMODULES_NAMESPACE = 'cloudkitty.client.modules'
class Client(client.BaseClient):
class Client(object):
"""Client for the Cloudkitty v1 API.
:param string endpoint: A user-supplied endpoint URL for the cloudkitty
service.
:param function token: Provides 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 Cloudkitty v1 API."""
self.auth_plugin = (kwargs.get('auth_plugin')
or ckclient.get_auth_plugin(*args, **kwargs))
self.client = client.HTTPClient(
auth_plugin=self.auth_plugin,
region_name=kwargs.get('region_name'),
endpoint_type=kwargs.get('endpoint_type'),
original_ip=kwargs.get('original_ip'),
verify=kwargs.get('verify'),
cert=kwargs.get('cert'),
timeout=kwargs.get('timeout'),
timings=kwargs.get('timings'),
keyring_saver=kwargs.get('keyring_saver'),
debug=kwargs.get('debug'),
user_agent=kwargs.get('user_agent'),
http=kwargs.get('http')
def __init__(self,
session=None,
adapter_options={},
cacert=None,
insecure=False,
**kwargs):
super(Client, self).__init__(
session=session,
adapter_options=adapter_options,
cacert=cacert,
insecure=insecure,
**kwargs
)
self.http_client = client.BaseClient(self.client)
self.modules = core.CloudkittyModuleManager(self.http_client)
self.reports = report.ReportManager(self.http_client)
self.quotations = core.QuotationManager(self.http_client)
self.storage = storage.StorageManager(self.http_client)
self._expose_submodules()
def _expose_submodules(self):
extensions = extension.ExtensionManager(
SUBMODULES_NAMESPACE,
)
for ext in extensions:
client = ext.plugin.get_client(self.http_client)
setattr(self, ext.name, client)
self.info = info.InfoManager(self.api_client)
self.collector = collector.CollectorManager(self.api_client)
self.rating = rating.RatingManager(self.api_client)
self.report = report.ReportManager(self.api_client)
self.storage = storage.StorageManager(self.api_client)

View File

@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from oslo_log import log
from cloudkittyclient.common import base
from cloudkittyclient import exc
LOG = log.getLogger(__name__)
class CollectorManager(base.BaseManager):
"""Class used to handle /v1/collector/mappings endpoint"""
url = '/v1/collector/{endpoint}/{service_id}'
def get_mapping(self, **kwargs):
"""Returns a service to collector mapping.
If the service is not specified, returns a list of mappings for the
given collector.
:param service: Name of the service to filter on.
:type service: str
:param collector: Name of the collector to filter on.
:type collector: str
"""
LOG.warning('WARNING: Collector mappings are deprecated and will '
'be removed in a future release')
kwargs['service_id'] = kwargs.get('service') or ''
authorized_args = ['collector']
url = self.get_url('mappings', kwargs, authorized_args)
return self.api_client.get(url).json()
def create_mapping(self, **kwargs):
"""Creates a service to collector mapping.
:param service: Name of the service to filter on.
:type service: str
:param collector: Name of the collector to filter on.
:type collector: str
"""
LOG.warning('WARNING: Collector mappings are deprecated and will '
'be removed in a future release')
for arg in ('collector', 'service'):
if not kwargs.get(arg):
raise exc.ArgumentRequired(
"'{arg}' argument is required.".format(arg=arg))
url = self.get_url('mappings', kwargs)
body = dict(
collector=kwargs['collector'],
service=kwargs['service'])
return self.api_client.post(url, json=body).json()
def delete_mapping(self, **kwargs):
"""Deletes a service to collector mapping.
:param service: Name of the service of which the mapping
should be deleted.
:type service: str
"""
LOG.warning('WARNING: Collector mappings are deprecated and will '
'be removed in a future release')
if not kwargs.get('service'):
raise exc.ArgumentRequired("'service' argument is required.")
body = dict(service=kwargs['service'])
url = self.get_url('mappings', kwargs)
self.api_client.delete(url, json=body)
def get_state(self, **kwargs):
"""Returns the state of a collector.
:param name: Name of the collector.
:type name: str
"""
LOG.warning('WARNING: Collector mappings are deprecated and will '
'be removed in a future release')
if not kwargs.get('name'):
raise exc.ArgumentRequired("'name' argument is required.")
authorized_args = ['name']
url = self.get_url('states', kwargs, authorized_args)
return self.api_client.get(url).json()
def set_state(self, **kwargs):
"""Sets the state of the collector.
:param name: Name of the collector
:type name: str
:param enabled: State of the collector
:type name: bool
"""
LOG.warning('WARNING: Collector mappings are deprecated and will '
'be removed in a future release')
if not kwargs.get('name'):
raise exc.ArgumentRequired("'name' argument is required.")
kwargs['enabled'] = kwargs.get('enabled') or False
url = self.get_url('states', kwargs)
body = dict(
name=kwargs['name'],
enabled=kwargs['enabled'],
)
self.api_client.put(url, json=body)
return self.get_state(**kwargs)

View File

@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import command
from cliff import lister
from cloudkittyclient import utils
class CliCollectorMappingGet(lister.Lister):
"""(DEPRECATED) Get a service to collector mapping."""
columns = [
('service', 'Service'),
('collector', 'Collector'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).collector.get_mapping(
service=parsed_args.service,
)
resp = [resp] if resp.get('mappings') is None else resp['mappings']
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCollectorMappingGet, self).get_parser(prog_name)
parser.add_argument('service', type=str,
help='Name of the service to filter on')
return parser
class CliCollectorMappingList(lister.Lister):
"""(DEPRECATED) List service to collector mappings."""
columns = [
('service', 'Service'),
('collector', 'Collector'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).collector.get_mapping(
collector=parsed_args.collector)
resp = [resp] if resp.get('mappings') is None else resp['mappings']
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCollectorMappingList, self).get_parser(prog_name)
parser.add_argument('--collector', type=str,
help='Name of the collector to filter on')
return parser
class CliCollectorMappingCreate(lister.Lister):
"""(DEPRECATED) Create a service to collector mapping."""
columns = [
('service', 'Service'),
('collector', 'Collector'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).collector.create_mapping(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCollectorMappingCreate, self).get_parser(prog_name)
parser.add_argument('service', type=str, help='Name of the service')
parser.add_argument('collector', type=str,
help='Name of the collector')
return parser
class CliCollectorMappingDelete(command.Command):
"""(DEPRECATED) Delete a service to collector mapping."""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).collector.delete_mapping(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliCollectorMappingDelete, self).get_parser(prog_name)
parser.add_argument('service', type=str, help='Name of the service')
return parser
class CliCollectorGetState(lister.Lister):
"""(DEPRECATED) Get the state of a collector."""
columns = [
('name', 'Collector'),
('enabled', 'State'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).collector.get_state(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCollectorGetState, self).get_parser(prog_name)
parser.add_argument('name', type=str, help='Name of the collector')
return parser
class CliCollectorEnable(lister.Lister):
"""(DEPRECATED) Enable a collector."""
columns = [
('name', 'Collector'),
('enabled', 'State'),
]
def take_action(self, parsed_args):
parsed_args.enabled = True
resp = utils.get_client_from_osc(self).collector.set_state(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCollectorEnable, self).get_parser(prog_name)
parser.add_argument('name', type=str, help='Name of the collector')
return parser
class CliCollectorDisable(CliCollectorEnable):
"""(DEPRECATED) Disable a collector."""
def take_action(self, parsed_args):
parsed_args.disabled = True
resp = utils.get_client_from_osc(self).collector.set_state(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values

View File

@@ -1,62 +0,0 @@
# Copyright 2015 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# 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 cloudkittyclient.common import base
class CloudkittyModule(base.Resource):
key = 'module'
def __repr__(self):
return "<CloudkittyModule %s>" % self._info
def enable(self):
self.enabled = True
self.update()
def disable(self):
self.enabled = False
self.update()
class CloudkittyModuleManager(base.CrudManager):
resource_class = CloudkittyModule
base_url = "/v1/rating"
key = 'module'
collection_key = "modules"
class Collector(base.Resource):
key = 'collector'
def __repr__(self):
return "<Collector %s>" % self._info
class CollectorManager(base.Manager):
resource_class = Collector
base_url = "/v1/rating"
key = "collector"
collection_key = "collectors"
class QuotationManager(base.Manager):
base_url = "/v1/rating/quote"
def quote(self, resources):
out = self.api.post(self.base_url,
json={'resources': resources}).json()
return out

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.common import base
class InfoManager(base.BaseManager):
"""Class used to handle /v1/info endpoint"""
url = '/v1/info/{endpoint}/{metric_name}'
def get_metric(self, **kwargs):
"""Returns info for the given service.
If metric_name is not specified, returns info for all services.
:param metric_name: Name of the service on which you want information
:type metric_name: str
"""
url = self.get_url('metrics', kwargs)
return self.api_client.get(url).json()
def get_config(self, **kwargs):
"""Returns the current configuration."""
url = self.get_url('config', kwargs)
return self.api_client.get(url).json()

View File

@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import lister
from cloudkittyclient import utils
class CliInfoMetricGet(lister.Lister):
"""Get information about current metrics."""
info_columns = [
('metric_id', 'Metric'),
('unit', 'Unit'),
('metadata', 'Metadata'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).info.get_metric(
metric_name=parsed_args.metric_name,
)
values = utils.list_to_cols([resp], self.info_columns)
return [col[1] for col in self.info_columns], values
def get_parser(self, prog_name):
parser = super(CliInfoMetricGet, self).get_parser(prog_name)
parser.add_argument('metric_name',
type=str, default='', help='Metric name')
return parser
class CliInfoMetricList(lister.Lister):
"""Get information about a single metric."""
info_columns = [
('metric_id', 'Metric'),
('unit', 'Unit'),
('metadata', 'Metadata'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).info.get_metric()
values = utils.list_to_cols(resp['metrics'], self.info_columns)
return [col[1] for col in self.info_columns], values
class CliInfoConfigGet(lister.Lister):
"""Get information about the current configuration."""
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).info.get_config()
values = [(key, value) for key, value in resp.items()]
return ('Section', 'Value'), values

View File

@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import lister
from cloudkittyclient.common import base
from cloudkittyclient import exc
from cloudkittyclient import utils
from cloudkittyclient.v1.rating import hashmap
from cloudkittyclient.v1.rating import pyscripts
class RatingManager(base.BaseManager):
"""Class used to handle /v1/rating endpoint"""
url = '/v1/rating/{endpoint}/{module_id}'
def __init__(self, api_client):
super(RatingManager, self).__init__(api_client)
self.hashmap = hashmap.HashmapManager(api_client)
self.pyscripts = pyscripts.PyscriptManager(api_client)
def get_module(self, **kwargs):
"""Returns the given module.
If module_id is not specified, returns the list of loaded modules.
:param module_id: ID of the module on which you want information.
:type module_id: str
"""
authorized_args = ['module_id']
url = self.get_url('modules', kwargs, authorized_args)
return self.api_client.get(url).json()
def update_module(self, **kwargs):
"""Update the given module.
:param module_id: Id of the module to update.
:type module_id: str
:param enabled: Set to True to enable the module, False to disable it.
:type enabled: bool
:param priority: New priority of the module.
:type priority: int
"""
if not kwargs.get('module_id', None):
raise exc.ArgumentRequired("'module_id' argument is required.")
url = self.get_url('modules', kwargs)
module = self.get_module(**kwargs)
for key in module.keys():
value = kwargs.get(key, None)
if value is not None and module[key] != value:
module[key] = value
self.api_client.put(url, json=module)
return self.get_module(**kwargs)
def reload_modules(self, **kwargs):
"""Triggers a reload of all rating modules."""
url = self.get_url('reload_modules', kwargs)
self.api_client.get(url)
def get_quotation(self, **kwargs):
"""Returns a quote base on multiple resource descriptions.
:param res_data: A list of resource descriptions.
:type res_data: list
"""
if not kwargs.get('res_data', None):
raise exc.ArgumentRequired("'res_data' argument is required.")
url = self.get_url('quote', {})
body = {'resources': kwargs['res_data']}
return self.api_client.post(url, json=body).json()
class CliModuleGet(lister.Lister):
"""Get a rating module or list loaded rating modules.
If module_id is not specified, returns a list of all loaded
rating modules.
"""
columns = [
('module_id', 'Module'),
('enabled', 'Enabled'),
('priority', 'Priority'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.get_module(
module_id=parsed_args.module_id,
)
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliModuleGet, self).get_parser(prog_name)
parser.add_argument('module_id', type=str, help='Module name')
return parser
class CliModuleList(lister.Lister):
"""List loaded rating modules."""
columns = [
('module_id', 'Module'),
('enabled', 'Enabled'),
('priority', 'Priority'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.get_module()
values = utils.list_to_cols(resp['modules'], self.columns)
return [col[1] for col in self.columns], values
class CliModuleSet(lister.Lister):
columns = [
('module_id', 'Module'),
('enabled', 'Enabled'),
('priority', 'Priority'),
]
def _take_action(self, **kwargs):
resp = utils.get_client_from_osc(self).rating.update_module(**kwargs)
values = [resp.get(col[0]) for col in self.columns]
return [col[1] for col in self.columns], [values]
def get_parser(self, prog_name):
parser = super(CliModuleSet, self).get_parser(prog_name)
parser.add_argument('module_id', type=str, help='Module name')
return parser
class CliModuleEnable(CliModuleSet):
"""Enable a rating module."""
def take_action(self, parsed_args):
kwargs = vars(parsed_args)
kwargs['enabled'] = True
return self._take_action(**kwargs)
class CliModuleDisable(CliModuleEnable):
"""Disable a rating module."""
def take_action(self, parsed_args):
kwargs = vars(parsed_args)
kwargs['enabled'] = False
return self._take_action(**kwargs)
class CliModuleSetPriority(CliModuleSet):
"""Set the priority of a rating module."""
def get_parser(self, prog_name):
parser = super(CliModuleSetPriority, self).get_parser(prog_name)
parser.add_argument('priority', type=int, help='Priority (int)')
return parser
def take_action(self, parsed_args):
return self._take_action(**vars(parsed_args))

View File

@@ -0,0 +1,466 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import uuid
from cloudkittyclient.common import base
from cloudkittyclient import exc
class HashmapManager(base.BaseManager):
"""Class used to manage the Hashmap rating module"""
url = '/v1/rating/module_config/hashmap/{endpoint}/{resource_id}'
def get_mapping_types(self, **kwargs):
"""Returns a list of all available mapping types."""
url = self.get_url('types', kwargs)
return self.api_client.get(url).json()
def get_service(self, **kwargs):
"""Returns the service corresponding to the provided ID.
If no ID is provided, returns a list of all hashmap services.
:param service_id: ID of the service
:type service_id: str
"""
if kwargs.get('service_id'):
kwargs['resource_id'] = kwargs['service_id']
url = self.get_url('services', kwargs)
return self.api_client.get(url).json()
def create_service(self, **kwargs):
"""Creates a hashmap service.
:param name: Name of the service
:type name: str
"""
if not kwargs.get('name'):
raise exc.ArgumentRequired("Argument 'service_name' is mandatory.")
url = self.get_url('services', kwargs)
body = dict(name=kwargs['name'])
return self.api_client.post(url, json=body).json()
def delete_service(self, **kwargs):
"""Deletes a hashmap service
:param service_id: ID of the service to delete
:type service_id: uuid
"""
if not kwargs.get('service_id'):
raise exc.ArgumentRequired("Argument 'service_id' is mandatory.")
url = self.get_url('services', kwargs)
body = dict(service_id=kwargs['service_id'])
self.api_client.delete(url, json=body)
def get_field(self, **kwargs):
"""Returns a hashmap field.
Either service_id or field_id must be specified. If service_id is
provided, all fields of the given service are returned. If field_id
is specified, only this field is returned.
:param service_id: ID of the service of which you want fields
:type service_id: str
:param field_id: ID of the field you want
:type field_id: str
"""
if not kwargs.get('service_id') and not kwargs.get('field_id'):
raise exc.ArgumentRequired("Either 'service_id' or 'field_id' "
"must be specified.")
elif kwargs.get('service_id') and kwargs.get('field_id'):
raise exc.InvalidArgumentError(
"You can't specify both 'service_id' and 'field_id'")
elif kwargs.get('field_id'):
kwargs['resource_id'] = kwargs['field_id']
kwargs.pop('service_id', None)
else:
kwargs.pop('resource_id', None)
authorized_args = ['service_id']
url = self.get_url('fields', kwargs, authorized_args)
return self.api_client.get(url).json()
def create_field(self, **kwargs):
"""Creates a hashmap field.
:param name: Field name
:type name: str
:param service_id: ID of the service the field belongs to
:type service_id: uuid
"""
if not kwargs.get('name'):
raise exc.ArgumentRequired("'name' argument is required")
if not kwargs.get('service_id'):
raise exc.ArgumentRequired("'service_id' argument is required")
body = dict(name=kwargs['name'], service_id=kwargs['service_id'])
url = self.get_url('fields', kwargs)
return self.api_client.post(url, json=body).json()
def delete_field(self, **kwargs):
"""Deletes the given field.
:param field_id: ID of the field to delete.
:type field_id: uuid
"""
if not kwargs.get('field_id'):
raise exc.ArgumentRequired("'field_id' argument is required")
url = self.get_url('fields', kwargs)
body = dict(field_id=kwargs['field_id'])
self.api_client.delete(url, json=body)
def get_mapping(self, **kwargs):
"""Get hashmap mappings.
If mapping_id is not provided, you need to specify either service_id,
field_id or group_id.
:param mapping_id: ID of the mapping
:type mapping_id: uuid
:param service_id: ID of the service to filter on
:type service_id: uuid
:param group_id: ID of the group to filter on
:type group_id: uuid
:param field_id: ID of the field to filter on
:type field_id: uuid
:param tenant_id: ID of the tenant to filter on
:type tenant_id: uuid
:param filter_tenant: Explicitly filter on given tenant (allows to
filter on tenant being None). Defaults to false.
:type filter_tenant: bool
:param no_group: Filter on orphaned mappings.
:type no_group: bool
"""
if not kwargs.get('mapping_id'):
if not kwargs.get('service_id') and not kwargs.get('field_id') \
and not kwargs.get('group_id'):
raise exc.ArgumentRequired("You must provide either 'field_id'"
", 'service_id' or 'group_id'.")
allowed_args = ['service_id', 'group_id', 'field_id', 'tenant_id',
'filter_tenant', 'no_group']
else:
allowed_args = []
kwargs['resource_id'] = kwargs['mapping_id']
url = self.get_url('mappings', kwargs, allowed_args)
return self.api_client.get(url).json()
def create_mapping(self, **kwargs):
"""Create a hashmap mapping.
:param cost: Cost of the mapping
:type cost: decimal.Decimal
:param field_id: ID of the field the mapping belongs to
:type field_id: uuid
:param service_id: ID of the service the mapping belongs to
:type service_id: uuid
:param tenant_id: ID of the tenant the mapping belongs to
:type tenant_id: uuid
:param group_id: ID of the group the mapping belongs to
:type group_id: uuid
:param type: Type of the mapping (flat or rate)
:type type: str
:param value: Value of the mapping
:type value: str
:param name: Name of the mapping
:type name: str
:param start: Date the mapping starts being valid
:type start: str
:param end: Date the mapping stops being valid
:type end: str
:param description: Description of the mapping
:type description: str
"""
if kwargs.get('cost') is None:
raise exc.ArgumentRequired("'cost' argument is required")
if not kwargs.get('value'):
if not kwargs.get('service_id'):
raise exc.ArgumentRequired(
"'service_id' must be specified if no value is provided")
if kwargs.get('value') and kwargs.get('service_id'):
raise exc.InvalidArgumentError(
"You can't specify a value when 'service_id' is specified.")
if not kwargs.get('service_id') and not kwargs.get('field_id'):
raise exc.ArgumentRequired("You must specify either 'service_id'"
" or 'field_id'")
elif kwargs.get('service_id') and kwargs.get('field_id'):
raise exc.InvalidArgumentError(
"You can't specify both 'service_id'and 'field_id'")
body = dict(
cost=kwargs.get('cost'),
value=kwargs.get('value'),
service_id=kwargs.get('service_id'),
group_id=kwargs.get('group_id'),
field_id=kwargs.get('field_id'),
tenant_id=kwargs.get('tenant_id'),
type=kwargs.get('type') or 'flat',
)
if kwargs.get('description'):
body['description'] = kwargs.get('description')
if kwargs.get('start'):
body['start'] = kwargs.get('start')
if kwargs.get('end'):
body['end'] = kwargs.get('end')
if kwargs.get('name'):
body['name'] = kwargs.get('name')
else:
body['name'] = uuid.uuid4().hex[:24]
url = self.get_url('mappings', kwargs)
return self.api_client.post(url, json=body).json()
def delete_mapping(self, **kwargs):
"""Delete a hashmap mapping.
:param mapping_id: ID of the mapping to delete.
:type mapping_id: uuid
"""
if not kwargs.get('mapping_id'):
raise exc.ArgumentRequired("'mapping_id' argument is required")
url = self.get_url('mappings', kwargs)
body = dict(mapping_id=kwargs['mapping_id'])
self.api_client.delete(url, json=body)
def update_mapping(self, **kwargs):
"""Update a hashmap mapping.
:param mapping_id: ID of the mapping to update
:type mapping_id: uuid
:param cost: Cost of the mapping
:type cost: decimal.Decimal
:param field_id: ID of the field the mapping belongs to
:type field_id: uuid
:param service_id: ID of the field the mapping belongs to
:type service_id: uuid
:param tenant_id: ID of the field the mapping belongs to
:type tenant_id: uuid
:param type: Type of the mapping (flat or rate)
:type type: str
:param value: Value of the mapping
:type value: str
"""
if not kwargs.get('mapping_id'):
raise exc.ArgumentRequired("'mapping_id' argument is required")
mapping = self.get_mapping(**kwargs)
for key in mapping.keys():
value = kwargs.get(key, None)
if value is not None and mapping[key] != value:
mapping[key] = value
url = self.get_url('mappings', kwargs)
self.api_client.put(url, json=mapping)
return self.get_mapping(**kwargs)
def get_mapping_group(self, **kwargs):
"""Get the group attached to a mapping.
:param mapping_id: ID of the mapping to update
:type mapping_id: uuid
"""
if not kwargs.get('mapping_id'):
raise exc.ArgumentRequired("'mapping_id' argument is required")
kwargs['resource_id'] = 'group'
allowed_args = ['mapping_id']
url = self.get_url('mappings', kwargs, allowed_args)
return self.api_client.get(url).json()
def get_group(self, **kwargs):
"""Get the hashmap group corresponding to the given ID.
If group_id is not specified, returns a list of all hashmap groups.
:param group_id: Group ID
:type group_id: uuid
"""
kwargs['resource_id'] = kwargs.get('group_id') or ''
url = self.get_url('groups', kwargs)
return self.api_client.get(url).json()
def create_group(self, **kwargs):
"""Create a hashmap group.
:param name: Name of the group
:type name: str
"""
if not kwargs.get('name'):
raise exc.ArgumentRequired("'name' argument is required")
body = dict(name=kwargs['name'])
url = self.get_url('groups', kwargs)
return self.api_client.post(url, json=body).json()
def delete_group(self, **kwargs):
"""Delete a hashmap group.
:param group_id: ID of the group to delete
:type group_id: uuid
:param recursive: Delete mappings recursively
:type recursive: bool
"""
if not kwargs.get('group_id'):
raise exc.ArgumentRequired("'group_id' argument is required")
body = dict(
group_id=kwargs['group_id'],
recursive=kwargs.get('recursive', False))
url = self.get_url('groups', kwargs)
self.api_client.delete(url, json=body)
def get_group_mappings(self, **kwargs):
"""Get the mappings attached to the given group.
:param group_id: ID of the group
:type group_id: uuid
"""
if not kwargs.get('group_id'):
raise exc.ArgumentRequired("'group_id' argument is required")
authorized_args = ['group_id']
kwargs['resource_id'] = 'mappings'
url = self.get_url('groups', kwargs, authorized_args)
return self.api_client.get(url).json()
def get_group_thresholds(self, **kwargs):
"""Get the thresholds attached to the given group.
:param group_id: ID of the group
:type group_id: uuid
"""
if not kwargs.get('group_id'):
raise exc.ArgumentRequired("'group_id' argument is required")
authorized_args = ['group_id']
kwargs['resource_id'] = 'thresholds'
url = self.get_url('groups', kwargs, authorized_args)
return self.api_client.get(url).json()
def get_threshold(self, **kwargs):
"""Get hashmap thresholds.
If threshold_id is not provided, you need to specify either service_id,
field_id or group_id.
:param threshold_id: ID of the threshold
:type threshold_id: uuid
:param service_id: ID of the service to filter on
:type service_id: uuid
:param group_id: ID of the group to filter on
:type group_id: uuid
:param field_id: ID of the field to filter on
:type field_id: uuid
:param tenant_id: ID of the tenant to filter on
:type tenant_id: uuid
:param filter_tenant: Explicitly filter on given tenant (allows to
filter on tenant being None). Defaults to false.
:type filter_tenant: bool
:param no_group: Filter on orphaned thresholds.
:type no_group: bool
"""
if not kwargs.get('threshold_id'):
if not kwargs.get('service_id') and not kwargs.get('field_id') \
and not kwargs.get('group_id'):
raise exc.ArgumentRequired("You must provide either 'field_id'"
", 'service_id' or 'group_id'.")
allowed_args = ['service_id', 'group_id', 'field_id', 'tenant_id',
'filter_tenant', 'no_group']
else:
allowed_args = []
kwargs['resource_id'] = kwargs['threshold_id']
url = self.get_url('thresholds', kwargs, allowed_args)
return self.api_client.get(url).json()
def create_threshold(self, **kwargs):
"""Create a hashmap threshold.
:param cost: Cost of the threshold
:type cost: decimal.Decimal
:param field_id: ID of the field the threshold belongs to
:type field_id: uuid
:param service_id: ID of the service the threshold belongs to
:type service_id: uuid
:param tenant_id: ID of the tenant the threshold belongs to
:type tenant_id: uuid
:param group_id: ID of the group the threshold belongs to
:type group_id: uuid
:param type: Type of the threshold (flat or rate)
:type type: str
:param level: Level of the threshold
:type level: str
"""
for arg in ['cost', 'level']:
if kwargs.get(arg) is None:
raise exc.ArgumentRequired(
"'{}' argument is required".format(arg))
if not kwargs.get('service_id') and not kwargs.get('field_id'):
raise exc.ArgumentRequired("You must specify either 'service_id'"
" or 'field_id'")
body = dict(
cost=kwargs.get('cost'),
level=kwargs.get('level'),
service_id=kwargs.get('service_id'),
field_id=kwargs.get('field_id'),
group_id=kwargs.get('group_id'),
tenant_id=kwargs.get('tenant_id'),
type=kwargs.get('type') or 'flat',
)
url = self.get_url('thresholds', kwargs)
return self.api_client.post(url, json=body).json()
def delete_threshold(self, **kwargs):
"""Delete a hashmap threshold.
:param threshold_id: ID of the threshold to delete.
:type threshold_id: uuid
"""
if not kwargs.get('threshold_id'):
raise exc.ArgumentRequired("'threshold_id' argument is required")
url = self.get_url('thresholds', kwargs)
body = dict(threshold_id=kwargs['threshold_id'])
self.api_client.delete(url, json=body)
def update_threshold(self, **kwargs):
"""Update a hashmap threshold.
:param threshold_id: ID of the threshold to update
:type threshold_id: uuid
:param cost: Cost of the threshold
:type cost: decimal.Decimal
:param field_id: ID of the field the threshold belongs to
:type field_id: uuid
:param service_id: ID of the field the threshold belongs to
:type service_id: uuid
:param tenant_id: ID of the field the threshold belongs to
:type tenant_id: uuid
:param type: Type of the threshold (flat or rate)
:type type: str
:param level: Level of the threshold
:type level: str
"""
if not kwargs.get('threshold_id'):
raise exc.ArgumentRequired("'threshold_id' argument is required")
threshold = self.get_threshold(**kwargs)
for key in threshold.keys():
value = kwargs.get(key, None)
if value is not None and threshold[key] != value:
threshold[key] = value
url = self.get_url('thresholds', kwargs)
self.api_client.put(url, json=threshold)
return self.get_threshold(**kwargs)
def get_threshold_group(self, **kwargs):
"""Get the group attached to a threshold.
:param threshold_id: ID of the threshold to update
:type threshold_id: uuid
"""
if not kwargs.get('threshold_id'):
raise exc.ArgumentRequired("'threshold_id' argument is required")
kwargs['resource_id'] = 'group'
allowed_args = ['threshold_id']
url = self.get_url('thresholds', kwargs, allowed_args)
return self.api_client.get(url).json()

View File

@@ -1,132 +0,0 @@
# Copyright 2015 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# 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 cloudkittyclient.common import base
class Service(base.Resource):
key = 'service'
def __repr__(self):
return "<hashmap.Service %s>" % self._info
@property
def fields(self):
return FieldManager(client=self.manager.client).findall(
service_id=self.service_id
)
@property
def mappings(self):
return MappingManager(client=self.manager.client).findall(
service_id=self.service_id
)
class ServiceManager(base.CrudManager):
resource_class = Service
base_url = '/v1/rating/module_config/hashmap'
key = 'service'
collection_key = 'services'
class Field(base.Resource):
key = 'field'
def __repr__(self):
return "<hashmap.Field %s>" % self._info
@property
def service(self):
return ServiceManager(client=self.manager.client).get(
service_id=self.service_id
)
class FieldManager(base.CrudManager):
resource_class = Field
base_url = '/v1/rating/module_config/hashmap'
key = 'field'
collection_key = 'fields'
class Mapping(base.Resource):
key = 'mapping'
def __repr__(self):
return "<hashmap.Mapping %s>" % self._info
@property
def service(self):
return ServiceManager(client=self.manager.client).get(
service_id=self.service_id
)
@property
def field(self):
if self.field_id is None:
return None
return FieldManager(client=self.manager.client).get(
service_id=self.service_id
)
class MappingManager(base.CrudManager):
resource_class = Mapping
base_url = '/v1/rating/module_config/hashmap'
key = 'mapping'
collection_key = 'mappings'
class Group(base.Resource):
key = 'group'
def __repr__(self):
return "<hashmap.Group %s>" % self._info
def delete(self, recursive=False):
return self.manager.delete(group_id=self.group_id, recursive=recursive)
class GroupManager(base.CrudManager):
resource_class = Group
base_url = '/v1/rating/module_config/hashmap'
key = 'group'
collection_key = 'groups'
def delete(self, group_id, recursive=False):
url = self.build_url(group_id=group_id)
if recursive:
url += "?recursive=True"
return self._delete(url)
class Threshold(base.Resource):
key = 'threshold'
def __repr__(self):
return "<hashmap.Threshold %s>" % self._info
class ThresholdManager(base.CrudManager):
resource_class = Threshold
base_url = '/v1/rating/module_config/hashmap'
key = 'threshold'
collection_key = 'thresholds'
def group(self, threshold_id):
url = ('%(base_url)s/thresholds/%(threshold_id)s/group' %
{'base_url': self.base_url, 'threshold_id': threshold_id})
out = self._get(url)
return out

View File

@@ -1,32 +0,0 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.v1.rating import hashmap
class Client(object):
"""Client for the Hashmap v1 API.
:param http_client: A http client.
"""
def __init__(self, http_client):
"""Initialize a new client for the Hashmap v1 API."""
self.http_client = http_client
self.services = hashmap.ServiceManager(self.http_client)
self.fields = hashmap.FieldManager(self.http_client)
self.mappings = hashmap.MappingManager(self.http_client)
self.groups = hashmap.GroupManager(self.http_client)
self.thresholds = hashmap.ThresholdManager(self.http_client)

View File

@@ -1,31 +0,0 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.v1.rating.hashmap import client
from cloudkittyclient.v1.rating.hashmap import shell
class Extension(object):
"""Hashmap extension.
"""
@staticmethod
def get_client(http_client):
return client.Client(http_client)
@staticmethod
def get_shell():
return shell

View File

@@ -1,415 +0,0 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
from oslo_utils import strutils
from cloudkittyclient.common import utils
from cloudkittyclient import exc
_bool_strict = functools.partial(strutils.bool_from_string, strict=True)
@utils.arg('-n', '--name',
help='Service name',
required=True)
def do_hashmap_service_create(cc, args={}):
"""Create a service."""
arg_to_field_mapping = {
'name': 'name'
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap.services.create(**fields)
utils.print_dict(out.to_dict())
def do_hashmap_service_list(cc, args={}):
"""List services."""
try:
services = cc.hashmap.services.list()
except exc.HTTPNotFound:
raise exc.CommandError('Services not found: %s' % args.counter_name)
else:
field_labels = ['Name', 'Service id']
fields = ['name', 'service_id']
utils.print_list(services, fields, field_labels,
sortby=0)
@utils.arg('-s', '--service-id',
help='Service uuid',
required=True)
def do_hashmap_service_delete(cc, args={}):
"""Delete a service."""
try:
cc.hashmap.services.delete(service_id=args.service_id)
except exc.HTTPNotFound:
raise exc.CommandError('Service not found: %s' % args.counter_name)
@utils.arg('-n', '--name',
help='Field name',
required=True)
@utils.arg('-s', '--service-id',
help='Service id',
required=True)
def do_hashmap_field_create(cc, args={}):
"""Create a field."""
arg_to_field_mapping = {
'name': 'name',
'service_id': 'service_id'
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap.fields.create(**fields)
utils.print_dict(out.to_dict())
@utils.arg('-s', '--service-id',
help='Service id',
required=True)
def do_hashmap_field_list(cc, args={}):
"""Create a field."""
try:
created_field = cc.hashmap.fields.list(service_id=args.service_id)
except exc.HTTPNotFound:
raise exc.CommandError('Fields not found: %s' % args.counter_name)
else:
field_labels = ['Name', 'Field id']
fields = ['name', 'field_id']
utils.print_list(created_field, fields, field_labels,
sortby=0)
@utils.arg('-f', '--field-id',
help='Field uuid',
required=True)
def do_hashmap_field_delete(cc, args={}):
"""Delete a field."""
try:
cc.hashmap.fields.delete(field_id=args.field_id)
except exc.HTTPNotFound:
raise exc.CommandError('Field not found: %s' % args.counter_name)
@utils.arg('-c', '--cost',
help='Mapping cost',
required=True)
@utils.arg('-v', '--value',
help='Mapping value',
required=False)
@utils.arg('-t', '--type',
help='Mapping type (flat, rate)',
required=False)
@utils.arg('-s', '--service-id',
help='Service id',
required=False)
@utils.arg('-f', '--field-id',
help='Field id',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_mapping_create(cc, args={}):
"""Create a ampping."""
arg_to_field_mapping = {
'cost': 'cost',
'value': 'value',
'type': 'type',
'service_id': 'service_id',
'field_id': 'field_id',
'group_id': 'group_id',
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap.mappings.create(**fields)
utils.print_dict(out.to_dict())
@utils.arg('-m', '--mapping-id',
help='Mapping id',
required=True)
@utils.arg('-c', '--cost',
help='Mapping cost',
required=False)
@utils.arg('-v', '--value',
help='Mapping value',
required=False)
@utils.arg('-t', '--type',
help='Mapping type (flat, rate)',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_mapping_update(cc, args={}):
"""Update a mapping."""
arg_to_field_mapping = {
'mapping_id': 'mapping_id',
'cost': 'cost',
'value': 'value',
'type': 'type',
'group_id': 'group_id',
}
try:
mapping = cc.hashmap.mappings.get(mapping_id=args.mapping_id)
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
setattr(mapping, k, v)
cc.hashmap.mappings.update(**mapping.dirty_fields)
@utils.arg('-s', '--service-id',
help='Service id',
required=False)
@utils.arg('-f', '--field-id',
help='Field id',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_mapping_list(cc, args={}):
"""List mappings."""
if args.service_id is None and args.field_id is None:
raise exc.CommandError("Provide either service-id or field-id")
try:
mappings = cc.hashmap.mappings.list(service_id=args.service_id,
field_id=args.field_id,
group_id=args.group_id)
except exc.HTTPNotFound:
raise exc.CommandError('Mapping not found: %s' % args.counter_name)
else:
field_labels = ['Mapping id', 'Value', 'Cost',
'Type', 'Field id',
'Service id', 'Group id']
fields = ['mapping_id', 'value', 'cost',
'type', 'field_id',
'service_id', 'group_id']
utils.print_list(mappings, fields, field_labels,
sortby=0)
@utils.arg('-m', '--mapping-id',
help='Mapping uuid',
required=True)
def do_hashmap_mapping_delete(cc, args={}):
"""Delete a mapping."""
try:
cc.hashmap.mappings.delete(mapping_id=args.mapping_id)
except exc.HTTPNotFound:
raise exc.CommandError('Mapping not found: %s' % args.mapping_id)
@utils.arg('-n', '--name',
help='Group name',
required=True)
def do_hashmap_group_create(cc, args={}):
"""Create a group."""
arg_to_field_mapping = {
'name': 'name',
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
group = cc.hashmap.groups.create(**fields)
utils.print_dict(group.to_dict())
def do_hashmap_group_list(cc, args={}):
"""List groups."""
try:
groups = cc.hashmap.groups.list()
except exc.HTTPNotFound:
raise exc.CommandError('Mapping not found: %s' % args.counter_name)
else:
field_labels = ['Name',
'Group id']
fields = ['name', 'group_id']
utils.print_list(groups, fields, field_labels,
sortby=0)
@utils.arg('-g', '--group-id',
help='Group uuid',
required=True)
@utils.arg('-r', '--recursive',
help="""Delete the group's mappings""",
required=False,
default=False)
def do_hashmap_group_delete(cc, args={}):
"""Delete a group."""
try:
cc.hashmap.groups.delete(group_id=args.group_id,
recursive=args.recursive)
except exc.HTTPNotFound:
raise exc.CommandError('Group not found: %s' % args.group_id)
@utils.arg('-l', '--level',
help='Threshold level',
required=True)
@utils.arg('-c', '--cost',
help='Threshold cost',
required=True)
@utils.arg('-m', '--map-type',
help='Threshold type (flat, rate)',
required=False)
@utils.arg('-s', '--service-id',
help='Service id',
required=False)
@utils.arg('-f', '--field-id',
help='Field id',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_threshold_create(cc, args={}):
"""Create a ampping."""
arg_to_field_mapping = {
'level': 'level',
'cost': 'cost',
'map_type': 'map_type',
'service_id': 'service_id',
'field_id': 'field_id',
'group_id': 'group_id',
}
fields = {}
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
fields[arg_to_field_mapping.get(k, k)] = v
out = cc.hashmap.thresholds.create(**fields)
utils.print_dict(out.to_dict())
@utils.arg('-t', '--threshold-id',
help='Threshold id',
required=True)
@utils.arg('-l', '--level',
help='Threshold level',
required=False)
@utils.arg('-c', '--cost',
help='Threshold cost',
required=False)
@utils.arg('-m', '--map-type',
help='Threshold type (flat, rate)',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
def do_hashmap_threshold_update(cc, args={}):
"""Update a threshold."""
arg_to_field_mapping = {
'threshold_id': 'threshold_id',
'cost': 'cost',
'level': 'level',
'map_type': 'map_type',
'group_id': 'group_id',
}
try:
threshold = cc.hashmap.thresholds.get(threshold_id=args.threshold_id)
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
for k, v in vars(args).items():
if k in arg_to_field_mapping:
if v is not None:
setattr(threshold, k, v)
cc.hashmap.thresholds.update(**threshold.dirty_fields)
@utils.arg('-s', '--service-id',
help='Service id',
required=False)
@utils.arg('-f', '--field-id',
help='Field id',
required=False)
@utils.arg('-g', '--group-id',
help='Group id',
required=False)
@utils.arg('--no-group',
type=_bool_strict, metavar='{True,False}',
help='If True, list only orhpaned thresholds',
required=False)
def do_hashmap_threshold_list(cc, args={}):
"""List thresholds."""
if (args.group_id is None and
args.service_id is None and args.field_id is None):
raise exc.CommandError("Provide either group-id, service-id or "
"field-id")
try:
thresholds = cc.hashmap.thresholds.list(service_id=args.service_id,
field_id=args.field_id,
group_id=args.group_id,
no_group=args.no_group)
except exc.HTTPNotFound:
raise exc.CommandError('Threshold not found: %s' % args.counter_name)
else:
field_labels = ['Threshold id', 'Level', 'Cost',
'Type', 'Field id',
'Service id', 'Group id']
fields = ['threshold_id', 'level', 'cost',
'map_type', 'field_id',
'service_id', 'group_id']
utils.print_list(thresholds, fields, field_labels, sortby=0)
@utils.arg('-t', '--threshold-id',
help='Threshold uuid',
required=True)
def do_hashmap_threshold_delete(cc, args={}):
"""Delete a threshold."""
try:
cc.hashmap.thresholds.delete(threshold_id=args.threshold_id)
except exc.HTTPNotFound:
raise exc.CommandError('Threshold not found: %s' % args.threshold_id)
@utils.arg('-t', '--threshold-id',
help='Threshold uuid',
required=True)
def do_hashmap_threshold_get(cc, args={}):
"""Get a threshold."""
try:
threshold = cc.hashmap.thresholds.get(threshold_id=args.threshold_id)
except exc.HTTPNotFound:
raise exc.CommandError('Threshold not found: %s' % args.threshold_id)
utils.print_dict(threshold.to_dict())
@utils.arg('-t', '--threshold-id',
help='Threshold uuid',
required=True)
def do_hashmap_threshold_group(cc, args={}):
"""Get a threshold group."""
try:
threshold = cc.hashmap.thresholds.group(threshold_id=args.threshold_id)
except exc.HTTPNotFound:
raise exc.CommandError('Threshold not found: %s' % args.threshold_id)
utils.print_dict(threshold.to_dict())

View File

@@ -0,0 +1,581 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import command
from cliff import lister
from cloudkittyclient import utils
class CliGetMappingTypes(lister.Lister):
"""Get hashmap mapping types/"""
def take_action(self, parsed_args):
client = utils.get_client_from_osc(self)
resp = client.rating.hashmap.get_mapping_types()
return ['Mapping types'], [[item] for item in resp]
class CliGetService(lister.Lister):
"""Get a hashmap service"""
columns = [
('name', 'Name'),
('service_id', 'Service ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_service(
service_id=parsed_args.service_id,
)
# NOTE(lukapeschke): This can't be done with 'or', because it would
# lead to resp being [[]] if resp['services'] is an empty list. Having
# a list in a list causes cliff to display a row of 'None' instead of
# nothing
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliGetService, self).get_parser(prog_name)
parser.add_argument('service_id', type=str, help='Service ID')
return parser
class CliListService(lister.Lister):
"""List hashmap services."""
columns = [
('name', 'Name'),
('service_id', 'Service ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_service()
# NOTE(lukapeschke): This can't be done with 'or', because it would
# lead to resp being [[]] if resp['services'] is an empty list. Having
# a list in a list causes cliff to display a row of 'None' instead of
# nothing
values = utils.list_to_cols(resp['services'], self.columns)
return [col[1] for col in self.columns], values
class CliCreateService(lister.Lister):
"""Create a hashmap service."""
columns = [
('name', 'Name'),
('service_id', 'Service ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.create_service(
**vars(parsed_args))
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCreateService, self).get_parser(prog_name)
parser.add_argument('name', type=str, help='Service Name')
return parser
class CliDeleteService(command.Command):
"""Delete a hashmap service"""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).rating.hashmap.delete_service(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliDeleteService, self).get_parser(prog_name)
parser.add_argument('service_id', type=str, help='Service ID')
return parser
class CliGetField(lister.Lister):
"""Get a Hashmap field."""
columns = [
('name', 'Name'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_field(
field_id=parsed_args.field_id,
)
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliGetField, self).get_parser(prog_name)
parser.add_argument('field_id', type=str, help='Field ID')
return parser
class CliListField(lister.Lister):
"""List hashmap fields for the given service."""
columns = [
('name', 'Name'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_field(
service_id=parsed_args.service_id,
)
values = utils.list_to_cols(resp['fields'], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliListField, self).get_parser(prog_name)
parser.add_argument('service_id', type=str, help='Service ID')
return parser
class CliCreateField(lister.Lister):
"""Create a hashmap field."""
columns = [
('name', 'Name'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.create_field(
**vars(parsed_args))
resp = [resp] if resp.get('fields') is None else resp['fields']
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCreateField, self).get_parser(prog_name)
parser.add_argument('service_id', type=str, help='Service ID')
parser.add_argument('name', type=str, help='Field name')
return parser
class CliDeleteField(command.Command):
"""Delete a hashmap field."""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).rating.hashmap.delete_field(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliDeleteField, self).get_parser(prog_name)
parser.add_argument('field_id', type=str, help='Field ID')
return parser
class CliGetMapping(lister.Lister):
"""Get a hashmap mapping."""
columns = [
('mapping_id', 'Mapping ID'),
('value', 'Value'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_mapping(
mapping_id=parsed_args.mapping_id)
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliGetMapping, self).get_parser(prog_name)
parser.add_argument('mapping_id', type=str,
help='Mapping ID to filter on')
return parser
class CliListMapping(lister.Lister):
"""List hashmap mappings."""
columns = [
('mapping_id', 'Mapping ID'),
('value', 'Value'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_mapping(
**vars(parsed_args))
values = utils.list_to_cols(resp['mappings'], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliListMapping, self).get_parser(prog_name)
parser.add_argument('-s', '--service-id', type=str,
help='Service ID to filter on')
parser.add_argument('-g', '--group-id', type=str,
help='Group ID to filter on')
parser.add_argument('--field-id', type=str,
help='Field ID to filter on')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Project ID to filter on')
parser.add_argument('--filter-tenant', action='store_true',
help='Explicitly filter on given tenant (allows '
'to filter on tenant being None)')
parser.add_argument('--no-group', action='store_true',
help='Filter on orphaned mappings')
return parser
class CliCreateMapping(lister.Lister):
"""Create a Hashmap mapping."""
columns = [
('mapping_id', 'Mapping ID'),
('value', 'Value'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
('name', 'Mapping Name'),
('start', 'Mapping Start Date'),
('end', 'Mapping End Date'),
('Description', 'Mapping Description')
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.create_mapping(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCreateMapping, self).get_parser(prog_name)
parser.add_argument('-s', '--service-id', type=str, help='Service ID')
parser.add_argument('-g', '--group-id', type=str, help='Group ID')
parser.add_argument('--field-id', type=str, help='Field ID')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Project ID')
parser.add_argument('-t', '--type', type=str, help='Mapping type')
parser.add_argument('--value', type=str, help='Value')
parser.add_argument('cost', type=float, help='Cost')
parser.add_argument('--name', type=str, help='Mapping Name')
parser.add_argument('--start', type=str, help='Mapping Start')
parser.add_argument('--end', type=str, help='Mapping End')
parser.add_argument('--description', type=str,
help='Mapping Description')
return parser
class CliDeleteMapping(command.Command):
"""Delete a Hashmap mapping."""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).rating.hashmap.delete_mapping(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliDeleteMapping, self).get_parser(prog_name)
parser.add_argument('mapping_id', type=str, help='Mapping ID')
return parser
class CliUpdateMapping(lister.Lister):
"""Update a Hashmap mapping."""
columns = [
('mapping_id', 'Mapping ID'),
('value', 'Value'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.update_mapping(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliUpdateMapping, self).get_parser(prog_name)
parser.add_argument('-s', '--service-id', type=str, help='Service ID')
parser.add_argument('-g', '--group-id', type=str, help='Group ID')
parser.add_argument('--field-id', type=str, help='Field ID')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Project ID')
parser.add_argument('--value', type=str, help='Value')
parser.add_argument('--cost', type=str, help='Cost')
parser.add_argument('mapping_id', type=str, help='Mapping ID')
parser.add_argument('--name', type=str, help='Mapping Name')
parser.add_argument('--start', type=str, help='Mapping Start')
parser.add_argument('--end', type=str, help='Mapping End')
parser.add_argument('--description', type=str,
help='Mapping Description')
return parser
class CliListGroup(lister.Lister):
"""List existing hashmap groups."""
columns = [
('name', 'Name'),
('group_id', 'Group ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_group()
values = utils.list_to_cols(resp['groups'], self.columns)
return [col[1] for col in self.columns], values
class CliCreateGroup(lister.Lister):
"""Create a Hashmap group."""
columns = [
('name', 'Name'),
('group_id', 'Group ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.create_group(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCreateGroup, self).get_parser(prog_name)
parser.add_argument('name', type=str, help='Group Name')
return parser
class CliDeleteGroup(command.Command):
"""Create a Hashmap group."""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).rating.hashmap.delete_group(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliDeleteGroup, self).get_parser(prog_name)
parser.add_argument('--recursive', action='store_true',
help='Delete mappings recursively')
parser.add_argument('group_id', type=str, help='Group ID')
return parser
class CliGetGroupMappings(lister.Lister):
"""Get all Hashmap mappings for the given group."""
columns = [
('mapping_id', 'Mapping ID'),
('value', 'Value'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
client = utils.get_client_from_osc(self)
resp = client.rating.hashmap.get_group_mappings(**vars(parsed_args))
return ([col[1] for col in self.columns],
utils.list_to_cols(resp.get('mappings', []), self.columns))
def get_parser(self, prog_name):
parser = super(CliGetGroupMappings, self).get_parser(prog_name)
parser.add_argument('group_id', type=str, help='Group ID')
return parser
class CliGetGroupThresholds(lister.Lister):
"""Get all thresholds for the given group."""
columns = [
('threshold_id', 'Threshold ID'),
('level', 'Level'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
client = utils.get_client_from_osc(self)
resp = client.rating.hashmap.get_group_thresholds(**vars(parsed_args))
return ([col[1] for col in self.columns],
utils.list_to_cols(resp.get('thresholds', []), self.columns))
def get_parser(self, prog_name):
parser = super(CliGetGroupThresholds, self).get_parser(prog_name)
parser.add_argument('group_id', type=str, help='Group ID')
return parser
class CliGetThreshold(lister.Lister):
"""Get a Hashmap threshold."""
columns = [
('threshold_id', 'Threshold ID'),
('level', 'Level'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_threshold(
threshold_id=parsed_args.threshold_id,
)
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliGetThreshold, self).get_parser(prog_name)
parser.add_argument('threshold_id', type=str,
help='Threshold ID to filter on')
return parser
class CliListThreshold(lister.Lister):
"""List Hashmap thresholds"""
columns = [
('threshold_id', 'Threshold ID'),
('level', 'Level'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.get_threshold(
**vars(parsed_args))
values = utils.list_to_cols(resp['thresholds'], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliListThreshold, self).get_parser(prog_name)
parser.add_argument('-s', '--service-id', type=str,
help='Service ID to filter on')
parser.add_argument('-g', '--group-id', type=str,
help='Group ID to filter on')
parser.add_argument('--field-id', type=str,
help='Field ID to filter on')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Project ID to filter on')
parser.add_argument('--filter-tenant', action='store_true',
help='Explicitly filter on given tenant (allows '
'to filter on tenant being None)')
parser.add_argument('--no-group', action='store_true',
help='Filter on orphaned thresholds')
return parser
class CliCreateThreshold(lister.Lister):
"""Create a Hashmap threshold."""
columns = [
('threshold_id', 'Threshold ID'),
('level', 'Level'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.create_threshold(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCreateThreshold, self).get_parser(prog_name)
parser.add_argument('-s', '--service-id', type=str, help='Service ID')
parser.add_argument('-g', '--group-id', type=str, help='Group ID')
parser.add_argument('--field-id', type=str, help='Field ID')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Project ID')
parser.add_argument('-t', '--type', type=str, help='Threshold type')
parser.add_argument('level', type=str, help='Threshold level')
parser.add_argument('cost', type=float, help='Cost')
return parser
class CliDeleteThreshold(command.Command):
"""Delete a Hashmap threshold."""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).rating.hashmap.delete_threshold(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliDeleteThreshold, self).get_parser(prog_name)
parser.add_argument('threshold_id', type=str, help='Threshold ID')
return parser
class CliUpdateThreshold(lister.Lister):
"""Update a Hashmap threshold."""
columns = [
('threshold_id', 'Threshold ID'),
('level', 'Level'),
('cost', 'Cost'),
('type', 'Type'),
('field_id', 'Field ID'),
('service_id', 'Service ID'),
('group_id', 'Group ID'),
('tenant_id', 'Project ID'),
]
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.hashmap.update_threshold(
**vars(parsed_args))
values = utils.list_to_cols([resp], self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliUpdateThreshold, self).get_parser(prog_name)
parser.add_argument('-s', '--service-id', type=str, help='Service ID')
parser.add_argument('-g', '--group-id', type=str, help='Group ID')
parser.add_argument('--field-id', type=str, help='Field ID')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Project ID')
parser.add_argument('-l', '--level', type=str, help='Threshold level')
parser.add_argument('--cost', type=str, help='Cost')
parser.add_argument('threshold_id', type=str, help='Threshold ID')
return parser

View File

@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.common import base
from cloudkittyclient import exc
class PyscriptManager(base.BaseManager):
"""Class used to manage the Pyscript rating module"""
url = '/v1/rating/module_config/pyscripts/{endpoint}/{script_id}'
def list_scripts(self, **kwargs):
"""Get a list of all pyscripts.
:param no_data: Set to True to remove script data from output.
:type no_data: bool
"""
authorized_args = ['no_data']
url = self.get_url('scripts', kwargs, authorized_args)
return self.api_client.get(url).json()
def get_script(self, **kwargs):
"""Get the script corresponding to the given ID.
:param script_id: ID of the script.
:type script_id: str
"""
if not kwargs.get('script_id'):
raise exc.ArgumentRequired("Argument 'script_id' is required.")
url = self.get_url('scripts', kwargs)
return self.api_client.get(url).json()
def create_script(self, **kwargs):
"""Create a new script.
:param name: Name of the script to create
:type name: str
:param data: Content of the script
:type data: str
:param start: Date the script starts being valid
:type start: str
:param end: Date the script stops being valid
:type end: str
:param description: Description of the script
:type description: str
"""
for arg in ('name', 'data'):
if not kwargs.get(arg):
raise exc.ArgumentRequired(
"'Argument {} is required.'".format(arg))
url = self.get_url('scripts', kwargs)
body = dict(name=kwargs['name'], data=kwargs['data'],
start=kwargs.get('start'),
end=kwargs.get('end'),
description=kwargs.get('description'))
return self.api_client.post(url, json=body).json()
def update_script(self, **kwargs):
"""Update an existing script.
:param script_id: ID of the script to update
:type script_id: str
:param name: Name of the script to create
:type name: str
:param data: Content of the script
:type data: str
:param start: Date the script starts being valid
:type start: str
:param end: Date the script stops being valid
:type end: str
:param description: Description of the script
:type description: str
"""
if not kwargs.get('script_id'):
raise exc.ArgumentRequired("Argument 'script_id' is required.")
script = self.get_script(script_id=kwargs['script_id'])
for key in ('name', 'data', 'start', 'end', 'description'):
if kwargs.get(key):
script[key] = kwargs[key]
script.pop('checksum', None)
url = self.get_url('scripts', kwargs)
return self.api_client.put(url, json=script).json()
def delete_script(self, **kwargs):
"""Delete a script.
:param script_id: ID of the script to update
:type script_id: str
"""
if not kwargs.get('script_id'):
raise exc.ArgumentRequired("Argument 'script_id' is required.")
url = self.get_url('scripts', kwargs)
self.api_client.delete(url)

View File

@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import command
from cliff import lister
from cloudkittyclient import utils
class BaseScriptCli(lister.Lister):
columns = [
('name', 'Name'),
('script_id', 'Script ID'),
('checksum', 'Checksum'),
('data', 'Data'),
('start', 'Script Start Date'),
('end', 'Script End Date'),
('description', 'Script Description')
]
class CliGetScript(BaseScriptCli):
"""Get a PyScript."""
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.pyscripts.get_script(
**vars(parsed_args))
resp = [resp]
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliGetScript, self).get_parser(prog_name)
parser.add_argument('script_id', type=str, help='Script ID')
return parser
class CliListScripts(BaseScriptCli):
"""List existing PyScripts."""
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(self).rating.pyscripts.list_scripts(
**vars(parsed_args))
resp = resp.get('scripts') or []
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliListScripts, self).get_parser(prog_name)
parser.add_argument(
'-n', '--no-data', action='store_true',
help='Set to true to remove script data from output')
return parser
class CliCreateScript(BaseScriptCli):
"""Create a PyScript."""
def take_action(self, parsed_args):
try:
with open(parsed_args.data, 'r') as fd:
parsed_args.data = fd.read()
except IOError:
pass
resp = utils.get_client_from_osc(self).rating.pyscripts.create_script(
**vars(parsed_args))
resp = [resp]
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliCreateScript, self).get_parser(prog_name)
parser.add_argument('name', type=str, help='Script Name')
parser.add_argument('data', type=str, help='Script Data or data file')
parser.add_argument('--start', type=str, help='Script Start')
parser.add_argument('--end', type=str, help='Script End')
parser.add_argument('--description', type=str,
help='Script Description')
return parser
class CliUpdateScript(BaseScriptCli):
"""Update a PyScript."""
def take_action(self, parsed_args):
if parsed_args.data:
try:
with open(parsed_args.data, 'r') as fd:
parsed_args.data = fd.read()
except IOError:
pass
resp = utils.get_client_from_osc(self).rating.pyscripts.update_script(
**vars(parsed_args))
resp = [resp]
values = utils.list_to_cols(resp, self.columns)
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliUpdateScript, self).get_parser(prog_name)
parser.add_argument('script_id', type=str, help='Script ID')
parser.add_argument('-n', '--name', type=str, help='Script Name')
parser.add_argument('-d', '--data', type=str,
help='Script Data or data file')
parser.add_argument('--start', type=str, help='Script Start')
parser.add_argument('--end', type=str, help='Script End')
parser.add_argument('--description', type=str,
help='Script Description')
return parser
class CliDeleteScript(command.Command):
"""Delete a PyScript."""
def take_action(self, parsed_args):
utils.get_client_from_osc(self).rating.pyscripts.delete_script(
**vars(parsed_args))
def get_parser(self, prog_name):
parser = super(CliDeleteScript, self).get_parser(prog_name)
parser.add_argument('script_id', type=str, help='Script ID')
return parser

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from oslo_log import log
from cloudkittyclient.common import base
LOG = log.getLogger(__name__)
class ReportManager(base.BaseManager):
"""Class used to handle /v1/report endpoint."""
url = '/v1/report/{endpoint}'
def get_summary(self, **kwargs):
"""Returns a list of summaries.
:param begin: Begin timestamp
:type begin: datetime.datetime
:param end: End timestamp
:type end: datetime.datetime
:param tenant_id: Tenant ID
:type tenant_id: str
:param groupby: Fields to group by.
:type groupby: list
:param all_tenants: Get summary from all tenants (admin only). Defaults
to False.
:type all_tenants: bool
"""
authorized_args = [
'begin', 'end', 'tenant_id', 'service', 'groupby', 'all_tenants']
if kwargs.get('groupby', None):
kwargs['groupby'] = ','.join(kwargs['groupby'])
url = self.get_url('summary', kwargs, authorized_args)
return self.api_client.get(url).json()
def get_total(self, **kwargs):
"""Returns the total for the given tenant.
:param begin: Begin timestamp
:type begin: datetime.datetime
:param end: End timestamp
:type end: datetime.datetime
:param tenant_id: Tenant ID
:type tenant_id: str
:param all_tenants: Get total from all tenants (admin only). Defaults
to False.
:type all_tenants: bool
"""
LOG.warning('WARNING: /v1/report/total/ endpoint is deprecated, '
'please use /v1/report/summary instead.')
authorized_args = [
'begin', 'end', 'tenant_id', 'service', 'all_tenants']
url = self.get_url('total', kwargs, authorized_args)
return self.api_client.get(url).json()
def get_tenants(self, **kwargs):
"""Returns a list of tenants.
:param begin: Begin timestamp
:type begin: datetime.datetime
:param end: End timestamp
:type end: datetime.datetime
"""
url = self.get_url('tenants', kwargs, ['begin', 'end'])
return self.api_client.get(url).json()

View File

@@ -1,40 +0,0 @@
# Copyright 2015 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# 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 cloudkittyclient.common import base
class ReportResult(base.Resource):
key = 'report'
def __repr__(self):
return "<Report %s>" % self._info
class ReportManager(base.Manager):
base_url = "/v1/report"
def list_tenants(self):
return self.client.get(self.base_url + "/tenants").json()
def get_total(self, tenant_id, begin=None, end=None):
url = self.base_url + "/total?tenant_id=%s" % tenant_id
filter = [url]
if begin:
filter.append("begin=%s" % begin.isoformat())
if end:
filter.append("end=%s" % end.isoformat())
return self.client.get("&".join(filter)).json()

View File

@@ -1,42 +0,0 @@
# Copyright 2015 Objectif Libre
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.common import utils
def do_report_tenant_list(cc, args):
tenants = cc.reports.list_tenants()
out_table = utils.prettytable.PrettyTable()
out_table.add_column("Tenant UUID", tenants)
print(out_table)
@utils.arg('-t', '--tenant-id',
help='Tenant id',
required=False, dest='total_tenant_id')
@utils.arg('-b', '--begin',
help='Begin timestamp',
required=False)
@utils.arg('-e', '--end',
help='End timestamp',
required=False)
def do_total_get(cc, args):
begin = utils.ts2dt(args.begin) if args.begin else None
end = utils.ts2dt(args.end) if args.end else None
total = cc.reports.get_total(args.total_tenant_id,
begin=begin,
end=end)
utils.print_dict({'Total': total or 0.0})

View File

@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import lister
from cliff import show
from cloudkittyclient import exc
from cloudkittyclient import utils
class CliSummaryGet(lister.Lister):
"""Get a summary report."""
summary_columns = [
('tenant_id', 'Tenant ID'),
('res_type', 'Resource Type'),
('rate', 'Rate'),
('begin', 'Begin Time'),
('end', 'End Time'),
]
def take_action(self, parsed_args):
for arg in ['begin', 'end']:
value = getattr(parsed_args, arg)
if value is not None:
try:
setattr(parsed_args, arg, utils.iso2dt(value))
except ValueError:
raise exc.InvalidArgumentError(
'Invalid timestamp "{}"'.format(value))
resp = utils.get_client_from_osc(self).report.get_summary(
**vars(parsed_args))
values = utils.list_to_cols(
resp.get('summary', []), self.summary_columns)
return [col[1] for col in self.summary_columns], values
def get_parser(self, prog_name):
parser = super(CliSummaryGet, self).get_parser(prog_name)
parser.add_argument('-t', '--tenant-id', type=str,
help='Tenant id.')
parser.add_argument('-b', '--begin', type=str,
help='Begin timestamp.')
parser.add_argument('-e', '--end', type=str,
help='End timestamp.')
parser.add_argument('-s', '--service', type=str,
help='Service Type.')
parser.add_argument('-g', '--groupby', nargs='+',
help='Fields to group by, space-separated. '
'(res_type and tenant_id are supported for now)')
parser.add_argument('-a', '--all-tenants', action='store_true',
help='Allows to get summary from all tenants '
'(admin only). Defaults to False.')
return parser
class CliTotalGet(show.ShowOne):
"""(DEPRECATED) Get total reports."""
def take_action(self, parsed_args):
for arg in ['begin', 'end']:
value = getattr(parsed_args, arg)
if value is not None:
try:
setattr(parsed_args, arg, utils.iso2dt(value))
except ValueError:
raise exc.InvalidArgumentError(
'Invalid timestamp "{}"'.format(value))
resp = utils.get_client_from_osc(self).report.get_total(
**vars(parsed_args))
return ('Total', ), (float(resp), )
def get_parser(self, prog_name):
parser = super(CliTotalGet, self).get_parser(prog_name)
parser.add_argument('-t', '--tenant-id',
help='Tenant id.')
parser.add_argument('-b', '--begin', type=str,
help='Begin timestamp.')
parser.add_argument('-e', '--end', type=str,
help='End timestamp.')
parser.add_argument('-s', '--service',
help='Service Type.')
parser.add_argument('-g', '--groupby', nargs='+',
help='Fields to group by, space-separated. '
'(res_type and tenant_id are supported for now)')
parser.add_argument('-a', '--all-tenants', action='store_true',
help='Allows to get summary from all tenants '
'(admin only). Defaults to False.')
return parser
class CliTenantList(lister.Lister):
"""Get rated tenants for the given period.
Begin defaults to the beginning of the current month and end defaults to
the beginning of the next month.
"""
def take_action(self, parsed_args):
for arg in ['begin', 'end']:
value = getattr(parsed_args, arg)
if value is not None:
try:
setattr(parsed_args, arg, utils.iso2dt(value))
except ValueError:
raise exc.InvalidArgumentError(
'Invalid timestamp "{}"'.format(value))
client = utils.get_client_from_osc(self)
tenants = client.report.get_tenants(**vars(parsed_args))
output = []
for tenant in tenants:
output.append((tenant, ))
return (('Tenant ID', ), output)
def get_parser(self, prog_name):
parser = super(CliTenantList, self).get_parser(prog_name)
parser.add_argument('-b', '--begin', type=str,
help='Begin timestamp.')
parser.add_argument('-e', '--end', type=str,
help='End timestamp.')
return parser

View File

@@ -1,66 +0,0 @@
# Copyright 2015 Objectif Libre
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.common import utils
from cloudkittyclient import exc
def do_module_list(cc, args):
'''List the samples for this meters.'''
try:
modules = cc.modules.list()
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
else:
field_labels = ['Module', 'Enabled']
fields = ['module_id', 'enabled']
utils.print_list(modules, fields, field_labels,
sortby=0)
@utils.arg('-n', '--name',
help='Module name',
required=True)
def do_module_enable(cc, args):
'''Enable a module.'''
try:
module = cc.modules.get(module_id=args.name)
module.enable()
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
else:
field_labels = ['Module', 'Enabled']
fields = ['module_id', 'enabled']
modules = [cc.modules.get(module_id=args.name)]
utils.print_list(modules, fields, field_labels,
sortby=0)
@utils.arg('-n', '--name',
help='Module name',
required=True)
def do_module_disable(cc, args):
'''Disable a module.'''
try:
module = cc.modules.get(module_id=args.name)
module.disable()
except exc.HTTPNotFound:
raise exc.CommandError('Modules not found: %s' % args.counter_name)
else:
field_labels = ['Module', 'Enabled']
fields = ['module_id', 'enabled']
modules = [cc.modules.get(module_id=args.name)]
utils.print_list(modules, fields, field_labels,
sortby=0)

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.common import base
class StorageManager(base.BaseManager):
"""Class used to handle /v1/storage endpoint"""
url = '/v1/storage/dataframes'
def get_dataframes(self, **kwargs):
"""Returns a list of rated dataframes.
:param begin: Begin timestamp
:type begin: datetime
:param end: End timestamp
:type end: datetime
:param tenant_id: ID of the tenant to filter on
:type tenant_id: str
:param resource_type: Resource type to filter on
:type resource_type: str
"""
authorized_args = ['begin', 'end', 'tenant_id', 'resource_type']
url = self.get_url('', kwargs, authorized_args)
return self.api_client.get(url).json()

View File

@@ -1,29 +0,0 @@
# Copyright 2015 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# 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 cloudkittyclient.common import base
class DataFrameResource(base.Resource):
key = 'dataframe'
def __repr__(self):
return "<DataFrameResource %s>" % self._info
class DataFrameManager(base.CrudManager):
resource_class = DataFrameResource
base_url = '/v1/storage'
key = 'dataframe'
collection_key = 'dataframes'

View File

@@ -1,40 +0,0 @@
# Copyright 2015 Objectif Libre
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudkittyclient.common import utils
@utils.arg('--begin',
help='Starting date/time (YYYY-MM-ddThh:mm:ss)',
required=True)
@utils.arg('--end',
help='Ending date/time (YYYY-MM-ddThh:mm:ss)',
required=True)
@utils.arg('--tenant',
help='Tenant ID',
required=False,
default=None)
@utils.arg('--resource-type',
help='Resource type (compute, image, ...)',
required=False,
default=None)
def do_storage_dataframe_list(cc, args):
data = cc.storage.dataframes.list(begin=args.begin, end=args.end,
tenant_id=args.tenant,
resource_type=args.resource_type)
fields = ['begin', 'end', 'tenant_id', 'resources']
fields_labels = ['Begin', 'End', 'Tenant ID', 'Resources']
utils.print_list(data, fields, fields_labels, sortby=0)

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import lister
from cloudkittyclient import exc
from cloudkittyclient import utils
class CliGetDataframes(lister.Lister):
"""List stored dataframes or generate CSV reports.
Dataframes can be filtered on resource_type and project_id.
CSV reports can be generated with the 'df-to-csv' formatter.
A config file may be provided to configure the output of that formatter.
See documentation for more details.
"""
columns = [
('begin', 'Begin'),
('end', 'End'),
('tenant_id', 'Project ID'),
('resources', 'Resources'),
]
def take_action(self, parsed_args):
for arg in ['begin', 'end']:
value = getattr(parsed_args, arg)
if value is not None:
try:
setattr(parsed_args, arg, utils.iso2dt(value))
except ValueError:
raise exc.InvalidArgumentError(
'Invalid timestamp "{}"'.format(value))
resp = utils.get_client_from_osc(self).storage.get_dataframes(
**vars(parsed_args)).get('dataframes', [])
values = utils.list_to_cols(resp, self.columns)
for value in values:
for resource in value[3]:
rating = float(resource['rating'])
volume = float(resource['volume'])
if volume > 0:
resource['rate_value'] = '{:.4f}'.format(rating / volume)
else:
resource['rate_value'] = ''
return [col[1] for col in self.columns], values
def get_parser(self, prog_name):
parser = super(CliGetDataframes, self).get_parser(prog_name)
parser.add_argument('-b', '--begin', type=str, help='Begin timestamp')
parser.add_argument('-e', '--end', type=str, help='End timestamp')
parser.add_argument('-p', '--project-id', type=str, dest='tenant_id',
help='Id of the tenant to filter on')
parser.add_argument('-r', '--resource_type', type=str,
help='Resource type to filter on')
return parser

View File

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient.v1 import client
from cloudkittyclient.v2 import dataframes
from cloudkittyclient.v2.rating import modules
from cloudkittyclient.v2 import reprocessing
from cloudkittyclient.v2 import scope
from cloudkittyclient.v2 import summary
# NOTE(peschk_l) v2 client needs to implement v1 until the v1 API has been
# completely ported to v2
class Client(client.Client):
def __init__(self,
session=None,
adapter_options={},
cacert=None,
insecure=False,
**kwargs):
super(Client, self).__init__(
session=session,
adapter_options=adapter_options,
cacert=cacert,
insecure=insecure,
**kwargs
)
self.dataframes = dataframes.DataframesManager(self.api_client)
self.scope = scope.ScopeManager(self.api_client)
self.summary = summary.SummaryManager(self.api_client)
self.rating = modules.RatingManager(self.api_client)
self.reprocessing = reprocessing.ReprocessingManager(self.api_client)

View File

@@ -0,0 +1,77 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import json
from cloudkittyclient.common import base
from cloudkittyclient import exc
class DataframesManager(base.BaseManager):
"""Class used to handle /v2/dataframes endpoint"""
url = '/v2/dataframes'
def add_dataframes(self, **kwargs):
"""Add DataFrames to the storage backend. Returns nothing.
:param dataframes: List of dataframes to add to the storage backend.
:type dataframes: list of dataframes
"""
dataframes = kwargs.get('dataframes')
if not dataframes:
raise exc.ArgumentRequired("'dataframes' argument is required")
if not isinstance(dataframes, str):
try:
dataframes = json.dumps(dataframes)
except TypeError:
raise exc.InvalidArgumentError(
"'dataframes' must be either a string"
"or a JSON serializable object.")
url = self.get_url(None, kwargs)
return self.api_client.post(
url,
data=dataframes,
)
def get_dataframes(self, **kwargs):
"""Returns a paginated list of DataFrames.
This support filters and datetime framing.
:param offset: Index of the first dataframe that should be returned.
:type offset: int
:param limit: Maximal number of dataframes to return.
:type limit: int
:param filters: Optional dict of filters to select data on.
:type filters: dict
:param begin: Start of the period to gather data from
:type begin: datetime.datetime
:param end: End of the period to gather data from
:type end: datetime.datetime
"""
kwargs['filters'] = ','.join(
'{}:{}'.format(k, v) for k, v in
(kwargs.get('filters', None) or {}).items()
)
authorized_args = [
'offset', 'limit', 'filters', 'begin', 'end']
url = self.get_url(None, kwargs, authorized_args=authorized_args)
return self.api_client.get(url).json()

View File

@@ -0,0 +1,116 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import argparse
from cliff import command
from cliff import lister
from oslo_utils import timeutils
from cloudkittyclient import utils
class CliDataframesAdd(command.Command):
"""Add one or several DataFrame objects to the storage backend."""
def get_parser(self, prog_name):
parser = super(CliDataframesAdd, self).get_parser(prog_name)
parser.add_argument(
'datafile',
type=argparse.FileType('r'),
help="File formatted as a JSON object having a DataFrame list"
"under a 'dataframes' key."
"'-' (hyphen) can be specified for using stdin.",
)
return parser
def take_action(self, parsed_args):
with parsed_args.datafile as dfile:
dataframes = dfile.read()
utils.get_client_from_osc(self).dataframes.add_dataframes(
dataframes=dataframes,
)
class CliDataframesGet(lister.Lister):
"""Get dataframes from the storage backend."""
columns = [
('begin', 'Begin'),
('end', 'End'),
('metric', 'Metric Type'),
('unit', 'Unit'),
('qty', 'Quantity'),
('price', 'Price'),
('groupby', 'Group By'),
('metadata', 'Metadata'),
]
def get_parser(self, prog_name):
parser = super(CliDataframesGet, self).get_parser(prog_name)
def filter_(elem):
if len(elem.split(':')) != 2:
raise TypeError
return str(elem)
parser.add_argument('--offset', type=int, default=0,
help='Index of the first dataframe')
parser.add_argument('--limit', type=int, default=100,
help='Maximal number of dataframes')
parser.add_argument('--filter', type=filter_, action='append',
help="Optional filter, in 'key:value' format. Can "
"be specified several times.")
parser.add_argument('-b', '--begin', type=timeutils.parse_isotime,
help="Start of the period to query, in iso8601 "
"format. Example: 2019-05-01T00:00:00Z.")
parser.add_argument('-e', '--end', type=timeutils.parse_isotime,
help="End of the period to query, in iso8601 "
"format. Example: 2019-06-01T00:00:00Z.")
return parser
def take_action(self, parsed_args):
filters = dict(elem.split(':') for elem in (parsed_args.filter or []))
dataframes = utils.get_client_from_osc(self).dataframes.get_dataframes(
offset=parsed_args.offset,
limit=parsed_args.limit,
begin=parsed_args.begin,
end=parsed_args.end,
filters=filters,
).get('dataframes', [])
def format_(d):
return ' '.join([
'{}="{}"'.format(k, v) for k, v in (d or {}).items()])
values = []
for df in dataframes:
period = df['period']
usage = df['usage']
for metric_type, points in usage.items():
for point in points:
values.append([
period['begin'],
period['end'],
metric_type,
point['vol']['unit'],
point['vol']['qty'],
point['rating']['price'],
format_(point.get('groupby', {})),
format_(point.get('metadata', {})),
])
return [col[1] for col in self.columns], values

View File

View File

@@ -0,0 +1,59 @@
# Copyright 2019 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cloudkittyclient import exc
from cloudkittyclient.v1.client import rating
class RatingManager(rating.RatingManager):
"""Class used to handle /v2/rating/modules endpoint"""
url = '/v2/rating/modules'
def get_module(self, **kwargs):
"""Returns the given module.
If module_id is not specified, returns the list of loaded modules.
:param module_id: ID of the module on which you want information.
:type module_id: str
"""
module_id = kwargs.get('module_id', None)
if module_id is not None:
url = "{}/{}".format(self.url, module_id)
else:
url = self.url
return self.api_client.get(url).json()
def update_module(self, **kwargs):
"""Update the given module.
:param module_id: Id of the module to update.
:type module_id: str
:param enabled: Set to True to enable the module, False to disable it.
:type enabled: bool
:param priority: New priority of the module.
:type priority: int
"""
if not kwargs.get('module_id', None):
raise exc.ArgumentRequired("'module_id' argument is required.")
mutable_fields = ['enabled', 'priority']
changes = {}
for key, value in kwargs.items():
if value is not None and key in mutable_fields:
changes[key] = value
self.api_client.put("{}/{}".format(self.url, kwargs['module_id']),
json=changes)
return self.get_module(**kwargs)

Some files were not shown because too many files have changed in this diff Show More