Compare commits

...

93 Commits

Author SHA1 Message Date
OpenStack Proposal Bot
a9dc3794a6 Imported Translations from Zanata
For more information about this automatic import see:
https://docs.openstack.org/i18n/latest/reviewing-translation-import.html

Change-Id: I2b2afb0c0e590b737871bf4c43293df2ed88e534
2024-06-01 02:47:52 +00:00
Takashi Kajinami
d6f169197e SQLAlchemy 2.0: Omnibus fixes patch
This was originally five patches, but they are all needed to pass
any of the test jobs now, so they have been squashed into one:

Co-Authored-By: Dan Smith (dms@danplanet.com)

First:

The autoload argument was removed[1] in SQLAlchemy and only
the autoload_with argument should be passed.

The autoload argument is set according to the autoload_with argument
automatically even in SQLAlchemy 1.x[2] so is not at all needed.

[1] c932123bac
[2] ad8f921e96

Second:

Remove _warn_on_bytestring for newer SA, AFAICT, this flag has been
removed from SQLAlchemy and that is why watcher-db-manage fails to
initialize the DB for me on jammy. This migration was passing the
default value (=False) anyway, so I assume this is the right "fix".

Third:

Fix joinedload passing string attribute names

Fourth:

Fix engine.select pattern to use begin() per the migration guide.

Fifth:

Override the apscheduler get_next_run_time() which appears to be
trivially not compatible with SQLAlchemy 2.0 because of a return type
from scalar().

Change-Id: I000e5e78f97f82ed4ea64d42f1c38354c3252e08
2024-05-29 06:49:32 -07:00
James Page
bc5922c684 Fix oslo.db >= 15.0.0 compatibility
Minimal refactor of SQLAlchemy api module to be compatible with
oslo.db >= 15.0.0 where autocommit behaviour was dropped.

Closes-Bug: #2056181
Change-Id: I33be53f647faae2aad30a43c10980df950d5d7c2
2024-03-27 09:41:23 +00:00
Ghanshyam Mann
9d58a6d457 Update python classifier in setup.cfg
As per the current release tested runtime, we test
python version from 3.8 to 3.11 so updating the
same in python classifier in setup.cfg

Change-Id: Ie010eea38eb0861699b60f16dfd3e2e95ae33709
2024-01-09 19:22:04 -08:00
Lucian Petrut
c95ce4ec17 Add MAAS support
At the moment, Watcher can use a single bare metal provisioning
service: Openstack Ironic.

We're now adding support for Canonical's MAAS service [1], which
is commonly used along with Juju [2] to deploy Openstack.

In order to do so, we're building a metal client abstraction, with
concrete implementations for Ironic and MAAS. We'll pick the MAAS
client if the MAAS url is provided, otherwise defaulting to Ironic.

For now, we aren't updating the baremetal model collector since it
doesn't seem to be used by any of the existing Watcher strategy
implementations.

[1] https://maas.io/docs
[2] https://juju.is/docs

Implements: blueprint maas-support

Change-Id: I6861995598f6c542fa9c006131f10203f358e0a6
2023-12-11 10:21:33 +00:00
Zuul
9492c2190e Merge "vm workload consolidation: use actual host metrics" 2023-12-01 01:51:39 +00:00
Lucian Petrut
808f1bcee3 Update action json schema
Power-off actions created by the energy saving strategy include
a resource name property, which currently isn't part of the
action json schema. For this reason, json schema validation fails.

  Additional properties are not allowed ('resource_name' was unexpected)

We'll update the json schema, including the resource name property.

Change-Id: I924d36732a917c0be98b08c2f4128e9136356215
2023-11-15 01:11:56 +00:00
Lucian Petrut
3b224b5629 Fix object tests
A couple of object tests are failing, probably after a dependency
bump.

watcher.objects.base.objects is mocked, so the registered object
version isn't properly retrieved, leading to a type error:

    File "/mnt/data/workspace/watcher/watcher/tests/objects/test_objects.py",
    line 535, in test_hook_chooses_newer_properly
      reg.registration_hook(MyObj, 0)
    File "/mnt/data/workspace/watcher/watcher/objects/base.py",
    line 46, in registration_hook
      cur_version = versionutils.convert_version_to_tuple(
    File "/home/ubuntu/openstack_venv/lib/python3.10/site-packages/oslo_utils/versionutils.py",
    line 91, in convert_version_to_tuple
      version_str = re.sub(r'(\d+)(a|alpha|b|beta|rc)\d+$', '\\1', version_str)
    File "/usr/lib/python3.10/re.py", line 209, in sub
      return _compile(pattern, flags).sub(repl, string, count)
  TypeError: expected string or bytes-like object

We'll solve the issue by setting the VERSION attribute against
the mock object.

Change-Id: Ifeb38b98f1d702908531de5fc5c846bd1c53de4b
2023-11-14 10:38:40 +00:00
Lucian Petrut
424e9a76af vm workload consolidation: use actual host metrics
The "vm workload consolidation" strategy is summing up instance
usage in order to estimate host usage.

The problem is that some infrastructure services (e.g. OVS or Ceph
clients) may also use a significant amount of resources, which
would be ignored. This can impact Watcher's ability to detect
overloaded nodes and correctly rebalance the workload.

This commit will use the host metrics, if available. The proposed
implementation uses the maximum value between the host metric
and the sum of the instance metrics.

Note that we're holding a dict of host metric deltas in order to
account for planned migrations.

Change-Id: I82f474ee613f6c9a7c0a9d24a05cba41d2f68edb
2023-10-27 21:54:42 +03:00
Zuul
40e93407c7 Merge "Handle deprecated "cpu_util" metric" 2023-10-27 09:47:38 +00:00
Zuul
721aec1cb6 Merge "vm workload consolidation: allow cold migrations" 2023-10-27 09:47:36 +00:00
Zuul
8a3ee8f931 Merge "Improve vm_consolidation logging" 2023-10-27 09:20:13 +00:00
Lucian Petrut
00fea975e2 Handle deprecated "cpu_util" metric
The "cpu_util" metric has been deprecated a few years ago.
We'll obtain the same result by converting the cumulative cpu
time to a percentage, leveraging the rate of change aggregation.

Change-Id: I18fe0de6f74c785e674faceea0c48f44055818fe
2023-10-24 10:47:23 +00:00
Lucian Petrut
fd6562382e Avoid performing retries in case of missing resources
There may be no available metrics for instances that are stopped
or were recently spawned. This makes retries unnecessary and time
consuming.

For this reason, we'll ignore gnocchi MetricNotFound errors.

Change-Id: I79cd03bf04db634b931d6dfd32d5150f58e82044
2023-10-23 14:14:21 +00:00
Lucian Petrut
ec90891636 Improve vm_consolidation logging
We're adding a few info log messages in order to trace the
"vm consolidation" strategy more easily.

Change-Id: I8ce1a9dd173733f1b801839d3ad0c1269c4306bb
2023-10-23 14:10:02 +00:00
Lucian Petrut
7336a48057 vm workload consolidation: allow cold migrations
Although Watcher supports cold migrations, the vm workload
consolidation workflow only allows live migrations to be
performed.

We'll remove this unnecessary limitation so that stopped instances
could be cold migrated.

Change-Id: I4b41550f2255560febf8586722a0e02045c3a486
2023-10-23 13:03:18 +00:00
Lucian Petrut
922478fbda Unblock the CI gate
The Nova collector json schema validation started [1][2] failing after
the jsonschema upper constraint was bumped from 4.17.3 to 4.19.1 [3].

The reason is that jsonschema v4.18.0a1 switched to a reference
resolving library [4], which treats the aggregate "id" as a jsonschema
id and expects it to be a string [5]. For this reason, we're now getting
AttributeError exceptions.

As a workaround, we'll rename the "id" ref element as "host_aggr_id".

Also, the watcher-tempest-multinode job is configured to use Focal,
which is no longer supported by Devstack [6]. That being considered,
we'll switch to Ubuntu Jammy (22.04).

While at it, we're disabling Cinder Backup, which isn't used while
testing Watched. It currently causes Devstack failures since it
uses the Swift backend by default, which is disabled.

[1] https://paste.opendev.org/raw/bjQ1uIdbDMnmA1UEhxLL/
[2] https://paste.opendev.org/raw/bNgxqulBwBLYB7tNhrU4/
[3] ab0dcbdda2
[4] https://github.com/python-jsonschema/jsonschema/releases/tag/v4.18.0a1
[5] c23a5dc1c9/referencing/jsonschema.py (L54-L55C18)
[6] https://paste.openstack.org/raw/bSoSyXgbtmq6d9768HQn/

Change-Id: I300620c2ec4857b1e0d402a9b57a637f576eeb24
2023-10-23 09:21:55 +03:00
OpenStack Release Bot
9f0eca2343 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: I8a0c75ce5a4e5ae5cccd8eb1cb0325747a619122
2023-09-14 01:24:43 +00:00
Zuul
1e11c490a7 Merge "Add timeout option for Grafana request" 2023-08-29 11:21:46 +00:00
Zuul
8a7a8db661 Merge "Imported Translations from Zanata" 2023-08-28 06:21:40 +00:00
BubaVV
0610070e59 Add timeout option for Grafana request
Implemented config option to setup Grafana API request timeout

Change-Id: I8cbf8ce22f199fe22c0b162ba1f419169881f193
2023-08-23 17:46:19 +03:00
OpenStack Proposal Bot
a0997a0423 Imported Translations from Zanata
For more information about this automatic import see:
https://docs.openstack.org/i18n/latest/reviewing-translation-import.html

Change-Id: I37201577bd8d9c53db8ce6700f47d911359da6d2
2023-08-14 04:24:29 +00:00
chenker
4ea3eada3e Fix watcher comment
Change-Id: I4512cf1032e08934886d5e3ca858b3e05c3da76c
2023-08-13 00:00:12 +00:00
Zuul
cd1c0f3054 Merge "Imported Translations from Zanata" 2023-03-08 07:04:33 +00:00
OpenStack Proposal Bot
684350977d Imported Translations from Zanata
For more information about this automatic import see:
https://docs.openstack.org/i18n/latest/reviewing-translation-import.html

Change-Id: I4ee251e6d37a1b955c22dc6fdc04c1a08c9ae9b8
2023-03-02 03:28:31 +00:00
OpenStack Release Bot
d28630b759 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: Ia585893e7fef42e9991a2b81f604d1ff28c0a5ad
2023-02-28 13:31:08 +00:00
Zuul
f7fbaf46a2 Merge "Use new get_rpc_client API from oslo.messaging" 2023-02-09 01:25:15 +00:00
Zuul
e7cda537e7 Merge "Modify saving_energy log info" 2023-02-07 12:18:58 +00:00
chenker
c7be34fbaa update saving_energy docs
Change-Id: I3b0c86911a8d32912c2de2e2392af9539b8d9be0
2023-02-07 10:27:54 +00:00
chenker
52da088011 Modify saving_energy log info
Change-Id: I84879a453aa3ff78917d1136c62978b9d0e606de
2023-02-07 10:20:04 +00:00
Tobias Urdin
6ac3a6febf Fix passenv in tox.ini
Change-Id: If1ddb1d48eeb96191bcbfadd1a5e14f4350a02e4
2023-02-07 08:02:20 +00:00
Tobias Urdin
e36b77ad6d Use new get_rpc_client API from oslo.messaging
Use the new API that is consistent with
the existing API instead of instantiating the client
class directly.

This was introduced in release 14.1.0 here [1] and
added into oslo.messaging here [2]

[1] https://review.opendev.org/c/openstack/requirements/+/869340
[2] https://review.opendev.org/c/openstack/oslo.messaging/+/862419

Change-Id: I43c399a0c68473e40b8b71e9617c8334a439e675
2023-01-19 20:50:26 +00:00
Thierry Carrez
6003322711 Move queue declaration to project level
This moves the watcher queue declaration from the pipeline level
(where it is no longer valid) to the project level.

https: //lists.openstack.org/pipermail/openstack-discuss/2022-May/028603.html
Change-Id: I06923abb00f7eecd59587f44cd1f6a069e88a9fc
2022-09-26 14:19:58 +02:00
Zuul
f4ffca01b8 Merge "Switch to 2023.1 Python3 unit tests and generic template name" 2022-09-16 06:36:21 +00:00
Alfredo Moralejo
5d70c207cd Fix compatibility with oslo.db 12.1.0
oslo.db 12.1.0 has changed the default value for the 'autocommit'
parameter of 'LegacyEngineFacade' from 'True' to 'False'. This is a
necessary step to ensure compatibility with SQLAlchemy 2.0. However, we
are currently relying on the autocommit behavior and need changes to
explicitly manage sessions. Until that happens, we need to override the
default.

Co-Authored-By: Stephen Finucane <stephenfin@redhat.com>
Change-Id: I7db39d958d087322bfa0aad70dfbd04de9228dd7
2022-09-15 16:52:41 +02:00
OpenStack Release Bot
0b2e641d00 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: Ide6c6c398f8e6cdd590c6620a752ad802a1f5cf8
2022-09-13 12:30:33 +00:00
OpenStack Release Bot
ff84b052a5 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: I1726e33a14038712dbb9fd5e5c0cddf8ad872e69
2022-09-13 12:30:32 +00:00
Zuul
a43b040ebc Merge "Imported Translations from Zanata" 2022-08-30 10:44:52 +00:00
Zuul
749fa2507a Merge "Tests: fix requirements for unit tests" 2022-08-30 08:15:05 +00:00
OpenStack Proposal Bot
76d61362ee Imported Translations from Zanata
For more information about this automatic import see:
https://docs.openstack.org/i18n/latest/reviewing-translation-import.html

Change-Id: I95133dece6fdaf931dfed64015806430ba8d04f0
2022-08-29 04:12:15 +00:00
wangjiaqi07
c55143bc21 remove unicode from code
Change-Id: I747445d482a2fb40c2f39139c5fd2a0cb26c27bc
2022-08-19 14:17:10 +08:00
suzhengwei
7609df3370 Tests: fix requirements for unit tests
Add WebTest to test-requirements which used to be imported as a
transitive requirement via pecan, but the latest release of
pecan dropped this dependency. So make this requirement explicit.

Related-Bug: #1982110
Change-Id: I4852be23b489257aaa56d3fa22d27f72bcabf919
2022-07-28 16:14:13 +08:00
chenker
b57eac12cb Watcher DB upgrde compatibility consideration for add_apscheduler_jobs
Change-Id: I8896ff5731bb8c1bf88a5d7b926bd2a884100ea8
2022-04-28 02:21:06 +00:00
OpenStack Release Bot
ac6911d3c4 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: I5cf874842550de18ff777b909fd28e2c32e6d530
2022-03-10 12:14:06 +00:00
OpenStack Release Bot
23c2010681 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: Ic7c275b38fef9afc29577f81fe92546bb94b2930
2022-03-10 12:14:04 +00:00
zhurong
01d74d0a87 Remove ceilometerclient dependecy
Change-Id: Ifa0f2493aa8414a29dc2722b6636a33bc5808be6
2022-01-07 05:48:22 +00:00
OpenStack Release Bot
e4fab0ce7f 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: I328b3ccb76153fa0dbb4d174dd976412be049200
2021-09-15 17:14:09 +00:00
OpenStack Release Bot
76ecaaeb3a 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: If1c02305a153575c6a550844b0c6f45b74ea5ef3
2021-09-15 17:14:07 +00:00
chenker
6dd2f2a9c1 BugFix: Prevent float type variables from being passed to random
>>> random.sample([5,10], 1.3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.6/random.py", line 321, in sample
    result = [None] * k
TypeError: can't multiply sequence by non-int of type 'float'

Change-Id: Ifa5dca06f07220512579e4fe3c5c741aeffc71cc
2021-08-23 01:58:52 +00:00
Takashi Kajinami
a993849928 Use Block Storage API v3 instead of API v2
Block Storage API v2 was deprecated during Pike cycle and is being
removed during Xena cycle, and current v3 API should be used instead.

Change-Id: Ia5247742b31f5f07186ef908588f0972d3ac609f
2021-07-27 11:04:16 +09:00
Zuul
6dbac1f6ae Merge "Add watcher dashboard to devstack documentation" 2021-07-13 14:31:16 +00:00
sue
c28756c48b use HTTPStatus instead of direct code
Python introduced http.HTTPStatus since version 3.5,
and Wallaby has targeted a minimum version of python 3.6.

Change-Id: I45f732f0f59b8fae831bb6c07f4fdd98cdd7409a
2021-07-09 11:02:36 +02:00
Dantali0n
2414f66e38 Add watcher dashboard to devstack documentation
Since installing watcher dashboard is fixed in devstack deployments
we can update documentation so it recommends to install dashboard
plugin.

Change-Id: I284a1ec31536ea258cc1979ffd46b22d3e1ac18b
2021-07-09 10:37:28 +02:00
Dantali0n
546b730c9b 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: I00b6af1560c84d0b99b6044078c1d65d22b206b0
2021-07-06 12:48:55 +02:00
Zuul
75be54aa89 Merge "Manage constraints with testenv install_command" 2021-07-06 07:25:03 +00:00
Zuul
4e9e75f4a0 Merge "Enable tls-proxy as default in test jobs" 2021-07-05 14:17:37 +00:00
Dantali0n
e0779175cf Manage constraints with testenv install_command
This prevents having to duplicate the -c{} deps argument
across all environments in tox.ini

Change-Id: I621b581417f6967271b3a93385d592098689ae9e
2021-07-02 10:43:44 +00:00
songwenping
1235153b4c 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.

Wallaby support officially the following runtimes [1]:
- Python 3.6
- Python 3.8

During Victoria Python 3.7 was used as the default runtime [2] however this
version isn't longer officially supported.

[1] https://governance.openstack.org/tc/reference/runtimes/wallaby.html#python-runtimes-for-wallaby
[2] https://governance.openstack.org/tc/reference/runtimes/victoria.html#python-runtimes-for-victoria

Change-Id: Id4b5cb679c60fda4132a10f684ed5128c0965230
2021-07-01 09:37:08 +00:00
Zuul
bf5ce9aa3f Merge "setup.cfg: Replace dashes with underscores" 2021-07-01 09:06:01 +00:00
ricolin
204992ff6f Enable tls-proxy as default in test jobs
Change-Id: Idbc8850269d84d0a3041c17f4453b00452c9dab8
2021-07-01 08:25:12 +00:00
Zuul
386e288543 Merge "Add Python3 xena unit tests" 2021-07-01 07:59:41 +00:00
maaoyu
6f668133ad 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: Ide4d650a78829a6bc16d86b620e6b3fbed0bba06
2021-07-01 07:19:01 +00:00
Zuul
fb625bfa56 Merge "Update master for stable/wallaby" 2021-06-30 15:27:12 +00:00
Zuul
32cb132712 Merge "requirements: Drop os-testr" 2021-06-30 11:48:43 +00:00
ericxiett
9ca44fa3ab Replace deprecated with_lockmode with with_for_update
The Query.with_lockmode() method is deprecated since version 0.9.0
and will be removed in a future release. [1]
This patch replaces it with Query.with_for_update().
The 'faultstring' was been modified to 'Exactly 5 or 6 columns has to be
specified for iterator expression', so adds one space between "iterator"
and "expression" for 'expected_error_msg'.

Also use upper-constraints in doc build to avoid issues in pdf build.

[1]
https://docs.sqlalchemy.org/en/13/orm/query.html#sqlalchemy.orm.query.Query.with_lockmode

Closes-Bug: #1933226
Change-Id: I0ad514da647bb08790259fd27e56a41f6dbbbaa0
2021-06-30 09:18:54 +00:00
OpenStack Release Bot
2205f4e4e3 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: Ib957dae35926bf4c18e5a0c126904bbc67939f21
2021-03-22 10:01:44 +00:00
OpenStack Release Bot
7cd4373707 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: Ic38b5071799ca733545381e79b956d7f82db2a87
2021-03-22 10:01:40 +00:00
wangzihao
a2123088bf requirements: Drop os-testr
Drop os-testr switched to stestr

Change-Id: I116f6028641ab38a611c8085b145a2ef796aea8e
2021-03-12 15:08:43 +08:00
Ghanshyam Mann
863815153e [goal] Deprecate the JSON formatted policy file
As per the community goal of migrating the policy file
the format from JSON to YAML[1], we need to do two things:

1. Change the default value of '[oslo_policy] policy_file''
config option from 'policy.json' to 'policy.yaml' with
upgrade checks.

2. Deprecate the JSON formatted policy file on the project side
via warning in doc and releasenotes.

Also replace policy.json to policy.yaml ref from doc and tests.

[1]https://governance.openstack.org/tc/goals/selected/wallaby/migrate-policy-format-from-json-to-yaml.html

Change-Id: I207c02ba71fe60635fd3406c9c9364c11f259bae
2021-02-12 19:59:27 +00:00
Ghanshyam Mann
76270c8383 Fix gate requirement checks job
Current requirements-check job is failing with
below error:

ERROR: Requirement for package PrettyTable excludes a version not excluded in the global list.
  Local settings : {'<0.8'}
  Global settings: set()
  Unexpected     : set()
Validating test-requirements.txt

Keeping PrettyTable same as what we have in openstack/requirements repo

Change-Id: I63633d2932757ca23bcea69fd655a2499a5b6d31
2021-02-12 18:58:23 +00:00
Zuul
58de9c405a Merge "Use common rpc pattern for all services" 2021-02-03 10:09:25 +00:00
Zuul
8f0126f1fe Merge "incorrect name in unit test" 2021-02-03 10:02:43 +00:00
sue
ec21898978 incorrect name in unit test
Incorrect name would mislead new developer.

Change-Id: I6ea228035df4437162b6c559ebb7bfb16853c520
2021-01-26 09:43:05 +08:00
Erik Olof Gunnar Andersson
e61f9b5e88 Use common rpc pattern for all services
There is a commonly shared and proven rpc pattern used
across most OpenStack services that is already implemented
in watcher, but the functions are not used.

This patch basically makes use of the existing
rpc classes and removes some unnecessary code.

Change-Id: I57424561e0675a836d10b712ef1579a334f72018
2021-01-25 12:47:52 -08:00
Zuul
e91efbde01 Merge "remove bandit B322 check" 2021-01-25 06:40:08 +00:00
sue
63b6997c83 Drop lower-constraints
Lower-constraints is not a requirement of the OpenStack Python PTI
[0] and there currently is a discussion on the mailing list [1]
about dropping the test, with the oslo team already having done
so [2].

The new dependency resolver in pip fails due to incompatible
dependency versions in our lower-constraints file, meaning that
we were never providing any real guarantees with it.

To unblock the CI, I am disabling lower-constraints job for now,
with the option to reenable it in case we fix the constraints,
and based on the outcome of the mailing list discussions and
consensus.

[0]. https://governance.openstack.org/tc/reference/pti/python.html
[1]. http://lists.openstack.org/pipermail/openstack-discuss/2021-January/019672.html
[2]. http://lists.openstack.org/pipermail/openstack-discuss/2021-January/019659.html

Change-Id: I588fa809839cf3112dae24e356547100f7e89bc5
2021-01-21 03:28:56 +00:00
suzhengwei
262edc8cc9 remove bandit B322 check
The check for this call to input() has been removed.
The input method in Python 2 will read from standard input, evaluate and
run the resulting string as python source code. This is similar, though
in many ways worse, than using eval. On Python 2, use raw_input instead,
input is safe in Python 3.

Change-Id: I8654f0c197bfe88796b56e9d85f563cdded6e8a8
2021-01-04 07:36:44 +00:00
zhufl
204b276693 Fix missing self argument in instances_no_attached
instances_no_attached should have self as the first argument, this is
to add it.

Change-Id: I010d9d1e9ddb8790c398bcf06d0772a0d17f57ec
2020-11-27 17:01:52 +08:00
Zuul
f8a2877f24 Merge "Imported Translations from Zanata" 2020-11-10 09:43:45 +00:00
zhufl
af02bebca9 Fix parameter passed to IronicNodeNotFound exception
IronicNodeNotFound expects uuid parameter for the error message,
not name.

Change-Id: I9fefa98fa9fe6f6491e5f621190cac7d376db6c9
2020-11-02 15:48:27 +08:00
OpenStack Proposal Bot
3aaa20908d Imported Translations from Zanata
For more information about this automatic import see:
https://docs.openstack.org/i18n/latest/reviewing-translation-import.html

Change-Id: I1c07f65533761586bf9563376004eaf0897743cb
2020-10-29 10:29:55 +00:00
wu.chunyang
5097665be3 Remove the unused coding style modules
Python modules related to coding style checks (listed in blacklist.txt in
openstack/requirements repo) are dropped from lower-constraints.txt
they are not needed during installation.

Change-Id: Iadf4581646131f87803c2cebbc66bd55fdb56685
2020-10-22 00:19:35 +08:00
root
09f6e3bde5 Remove usage of six
Remove six-library Replace the following items with Python 3 style code.
- six.string_types
- six.moves
- six.iteritems

Change-Id: I30358b3b08cc076ac59bd325d0e11a3e2deabde3
2020-10-12 05:41:00 +00:00
root
f488636fb8 Bump py37 to py38 in tox.ini
In 'victoria' cycle, we should test py38 by default.

ref:
  https://governance.openstack.org/tc/reference/runtimes/victoria.html

Change-Id: I0a1d49d3f0b2401b5941cd510bc7627863947532
2020-10-12 03:24:16 +00:00
Zuul
11cb88c2cd Merge "Remove six" 2020-10-10 02:31:22 +00:00
xuanyandong
16a0486655 Remove six
Replace the following items with Python 3 style code.

- six.string_types
- six.integer_types
- six.moves
- six.PY2

Implements: blueprint six-removal

Change-Id: I2a0624bd4b455c7e5a0617f1253efa05485dc673
2020-09-30 16:25:13 +08:00
Zuul
2454d4d199 Merge "Add Python3 wallaby unit tests" 2020-09-30 04:06:23 +00:00
Zuul
45dca00dee Merge "Implements base method for time series metrics" 2020-09-27 02:45:20 +00:00
Zuul
09b2383685 Merge "[goal] Migrate testing to ubuntu focal" 2020-09-25 07:06:43 +00:00
OpenStack Release Bot
f8797a7f70 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: I8951721c8c06ba6ebde9b68665c9aa791ab7ef9b
2020-09-22 14:12:54 +00:00
OpenStack Release Bot
da283b49b8 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: I311548732398a680ba50a72273fb98bb16009be4
Sem-Ver: feature
2020-09-22 14:12:53 +00:00
Ghanshyam Mann
e21e5f609e [goal] 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#1886298
Bump the lower constraints for required deps which added python3.8 support
in their later version.

- Move multinode jobs to focal nodeset

Story: #2007865
Task: #40227

Closes-Bug: #1886298

[1] https://governance.openstack.org/tc/goals/selected/victoria/migrate-ci-cd-jobs-to-ubuntu-focal>

Depends-On: https://review.opendev.org/#/c/752294/

Change-Id: Iec953f3294087cd0b628b701ad3d684cea61c057
2020-09-17 10:59:59 +00:00
Dantali0n
cca0d9f7d7 Implements base method for time series metrics
Implements base method as well as some basic implementations to
retrieve time series metrics. Ceilometer can not be supported
as API documentation has been unavailable. Grafana will be
supported in follow-up patch.

Partially Implements: blueprint time-series-framework

Change-Id: I55414093324c8cff379b28f5b855f41a9265c2d3
2020-08-26 16:01:15 +02:00
113 changed files with 2720 additions and 1187 deletions

View File

@@ -1,9 +1,9 @@
- project: - project:
queue: watcher
templates: templates:
- check-requirements - check-requirements
- openstack-cover-jobs - openstack-cover-jobs
- openstack-lower-constraints-jobs - openstack-python3-jobs
- openstack-python3-victoria-jobs
- publish-openstack-docs-pti - publish-openstack-docs-pti
- release-notes-jobs-python3 - release-notes-jobs-python3
check: check:
@@ -13,10 +13,8 @@
- watcher-tempest-strategies - watcher-tempest-strategies
- watcher-tempest-actuator - watcher-tempest-actuator
- watcherclient-tempest-functional - watcherclient-tempest-functional
- watcher-tls-test
- watcher-tempest-functional-ipv6-only - watcher-tempest-functional-ipv6-only
gate: gate:
queue: watcher
jobs: jobs:
- watcher-tempest-functional - watcher-tempest-functional
- watcher-tempest-functional-ipv6-only - watcher-tempest-functional-ipv6-only
@@ -88,21 +86,10 @@
tempest_concurrency: 1 tempest_concurrency: 1
tempest_test_regex: watcher_tempest_plugin.tests.scenario.test_execute_strategies tempest_test_regex: watcher_tempest_plugin.tests.scenario.test_execute_strategies
- job:
name: watcher-tls-test
parent: watcher-tempest-multinode
group-vars:
subnode:
devstack_services:
tls-proxy: true
vars:
devstack_services:
tls-proxy: true
- job: - job:
name: watcher-tempest-multinode name: watcher-tempest-multinode
parent: watcher-tempest-functional parent: watcher-tempest-functional
nodeset: openstack-two-node-bionic nodeset: openstack-two-node-jammy
roles: roles:
- zuul: openstack/tempest - zuul: openstack/tempest
group-vars: group-vars:
@@ -120,8 +107,7 @@
watcher-api: false watcher-api: false
watcher-decision-engine: true watcher-decision-engine: true
watcher-applier: false watcher-applier: false
# We need to add TLS support for watcher plugin c-bak: false
tls-proxy: false
ceilometer: false ceilometer: false
ceilometer-acompute: false ceilometer-acompute: false
ceilometer-acentral: false ceilometer-acentral: false
@@ -169,7 +155,6 @@
devstack_plugins: devstack_plugins:
watcher: https://opendev.org/openstack/watcher watcher: https://opendev.org/openstack/watcher
devstack_services: devstack_services:
tls-proxy: false
watcher-api: true watcher-api: true
watcher-decision-engine: true watcher-decision-engine: true
watcher-applier: true watcher-applier: true

View File

@@ -17,6 +17,14 @@
Policies Policies
======== ========
.. warning::
JSON formatted policy file is deprecated since Watcher 6.0.0 (Wallaby).
This `oslopolicy-convert-json-to-yaml`__ tool will migrate your existing
JSON-formatted policy file to YAML in a backward-compatible way.
.. __: https://docs.openstack.org/oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html
Watcher's public API calls may be restricted to certain sets of users using a Watcher's public API calls may be restricted to certain sets of users using a
policy configuration file. This document explains exactly how policies are policy configuration file. This document explains exactly how policies are
configured and what they apply to. configured and what they apply to.

View File

@@ -56,8 +56,8 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Watcher' project = 'Watcher'
copyright = u'OpenStack Foundation' copyright = 'OpenStack Foundation'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['watcher.'] modindex_common_prefix = ['watcher.']
@@ -91,14 +91,14 @@ pygments_style = 'native'
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' # List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
man_pages = [ man_pages = [
('man/watcher-api', 'watcher-api', u'Watcher API Server', ('man/watcher-api', 'watcher-api', 'Watcher API Server',
[u'OpenStack'], 1), ['OpenStack'], 1),
('man/watcher-applier', 'watcher-applier', u'Watcher Applier', ('man/watcher-applier', 'watcher-applier', 'Watcher Applier',
[u'OpenStack'], 1), ['OpenStack'], 1),
('man/watcher-db-manage', 'watcher-db-manage', ('man/watcher-db-manage', 'watcher-db-manage',
u'Watcher Db Management Utility', [u'OpenStack'], 1), 'Watcher Db Management Utility', ['OpenStack'], 1),
('man/watcher-decision-engine', 'watcher-decision-engine', ('man/watcher-decision-engine', 'watcher-decision-engine',
u'Watcher Decision Engine', [u'OpenStack'], 1), 'Watcher Decision Engine', ['OpenStack'], 1),
] ]
# -- Options for HTML output -------------------------------------------------- # -- Options for HTML output --------------------------------------------------
@@ -128,8 +128,8 @@ openstackdocs_bug_tag = ''
latex_documents = [ latex_documents = [
('index', ('index',
'doc-watcher.tex', 'doc-watcher.tex',
u'Watcher Documentation', 'Watcher Documentation',
u'OpenStack Foundation', 'manual'), 'OpenStack Foundation', 'manual'),
] ]
# If false, no module index is generated. # If false, no module index is generated.

View File

@@ -372,7 +372,7 @@ You can configure and install Ceilometer by following the documentation below :
#. https://docs.openstack.org/ceilometer/latest #. https://docs.openstack.org/ceilometer/latest
The built-in strategy 'basic_consolidation' provided by watcher requires The built-in strategy 'basic_consolidation' provided by watcher requires
"**compute.node.cpu.percent**" and "**cpu_util**" measurements to be collected "**compute.node.cpu.percent**" and "**cpu**" measurements to be collected
by Ceilometer. by Ceilometer.
The measurements available depend on the hypervisors that OpenStack manages on The measurements available depend on the hypervisors that OpenStack manages on
the specific implementation. the specific implementation.

View File

@@ -47,6 +47,8 @@ unavailable as well as `instance_l3_cpu_cache`::
[[local|localrc]] [[local|localrc]]
enable_plugin watcher https://opendev.org/openstack/watcher enable_plugin watcher https://opendev.org/openstack/watcher
enable_plugin watcher-dashboard https://opendev.org/openstack/watcher-dashboard
enable_plugin ceilometer https://opendev.org/openstack/ceilometer.git enable_plugin ceilometer https://opendev.org/openstack/ceilometer.git
CEILOMETER_BACKEND=gnocchi CEILOMETER_BACKEND=gnocchi

View File

@@ -56,9 +56,6 @@ Here is an example showing how you can write a plugin called ``NewStrategy``:
# filepath: thirdparty/new.py # filepath: thirdparty/new.py
# import path: thirdparty.new # import path: thirdparty.new
import abc import abc
import six
from watcher._i18n import _ from watcher._i18n import _
from watcher.decision_engine.strategy.strategies import base from watcher.decision_engine.strategy.strategies import base
@@ -303,6 +300,6 @@ Using that you can now query the values for that specific metric:
.. code-block:: py .. code-block:: py
avg_meter = self.datasource_backend.statistic_aggregation( avg_meter = self.datasource_backend.statistic_aggregation(
instance.uuid, 'cpu_util', self.periods['instance'], instance.uuid, 'instance_cpu_usage', self.periods['instance'],
self.granularity, self.granularity,
aggregation=self.aggregation_method['instance']) aggregation=self.aggregation_method['instance'])

View File

@@ -26,8 +26,7 @@ metric service name plugins comment
``compute_monitors`` option ``compute_monitors`` option
to ``cpu.virt_driver`` in to ``cpu.virt_driver`` in
the nova.conf. the nova.conf.
``cpu_util`` ceilometer_ none cpu_util has been removed ``cpu`` ceilometer_ none
since Stein.
============================ ============ ======= =========================== ============================ ============ ======= ===========================
.. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute .. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute

View File

@@ -89,9 +89,9 @@ step 2: Create audit to do optimization
.. code-block:: shell .. code-block:: shell
$ openstack optimize audittemplate create \ $ openstack optimize audittemplate create \
at1 saving_energy --strategy saving_energy saving_energy_template1 saving_energy --strategy saving_energy
$ openstack optimize audit create -a at1 \ $ openstack optimize audit create -a saving_energy_audit1 \
-p free_used_percent=20.0 -p free_used_percent=20.0
External Links External Links

View File

@@ -22,14 +22,19 @@ The *vm_workload_consolidation* strategy requires the following metrics:
============================ ============ ======= ========================= ============================ ============ ======= =========================
metric service name plugins comment metric service name plugins comment
============================ ============ ======= ========================= ============================ ============ ======= =========================
``cpu_util`` ceilometer_ none cpu_util has been removed ``cpu`` ceilometer_ none
since Stein.
``memory.resident`` ceilometer_ none ``memory.resident`` ceilometer_ none
``memory`` ceilometer_ none ``memory`` ceilometer_ none
``disk.root.size`` ceilometer_ none ``disk.root.size`` ceilometer_ none
``compute.node.cpu.percent`` ceilometer_ none (optional) need to set the
``compute_monitors`` option
to ``cpu.virt_driver`` in the
nova.conf.
``hardware.memory.used`` ceilometer_ SNMP_ (optional)
============================ ============ ======= ========================= ============================ ============ ======= =========================
.. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute .. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute
.. _SNMP: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#snmp-based-meters
Cluster data model Cluster data model
****************** ******************

View File

@@ -27,9 +27,8 @@ metric service name plugins comment
to ``cpu.virt_driver`` in the to ``cpu.virt_driver`` in the
nova.conf. nova.conf.
``hardware.memory.used`` ceilometer_ SNMP_ ``hardware.memory.used`` ceilometer_ SNMP_
``cpu_util`` ceilometer_ none cpu_util has been removed ``cpu`` ceilometer_ none
since Stein. ``instance_ram_usage`` ceilometer_ none
``memory.resident`` ceilometer_ none
============================ ============ ======= ============================= ============================ ============ ======= =============================
.. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute .. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute
@@ -107,10 +106,10 @@ parameter type default Value description
period of all received ones. period of all received ones.
==================== ====== ===================== ============================= ==================== ====== ===================== =============================
.. |metrics| replace:: ["cpu_util", "memory.resident"] .. |metrics| replace:: ["instance_cpu_usage", "instance_ram_usage"]
.. |thresholds| replace:: {"cpu_util": 0.2, "memory.resident": 0.2} .. |thresholds| replace:: {"instance_cpu_usage": 0.2, "instance_ram_usage": 0.2}
.. |weights| replace:: {"cpu_util_weight": 1.0, "memory.resident_weight": 1.0} .. |weights| replace:: {"instance_cpu_usage_weight": 1.0, "instance_ram_usage_weight": 1.0}
.. |instance_metrics| replace:: {"cpu_util": "compute.node.cpu.percent", "memory.resident": "hardware.memory.used"} .. |instance_metrics| replace:: {"instance_cpu_usage": "compute.node.cpu.percent", "instance_ram_usage": "hardware.memory.used"}
.. |periods| replace:: {"instance": 720, "node": 600} .. |periods| replace:: {"instance": 720, "node": 600}
Efficacy Indicator Efficacy Indicator
@@ -136,8 +135,8 @@ How to use it ?
at1 workload_balancing --strategy workload_stabilization at1 workload_balancing --strategy workload_stabilization
$ openstack optimize audit create -a at1 \ $ openstack optimize audit create -a at1 \
-p thresholds='{"memory.resident": 0.05}' \ -p thresholds='{"instance_ram_usage": 0.05}' \
-p metrics='["memory.resident"]' -p metrics='["instance_ram_usage"]'
External Links External Links
-------------- --------------

View File

@@ -24,8 +24,7 @@ The *workload_balance* strategy requires the following metrics:
======================= ============ ======= ========================= ======================= ============ ======= =========================
metric service name plugins comment metric service name plugins comment
======================= ============ ======= ========================= ======================= ============ ======= =========================
``cpu_util`` ceilometer_ none cpu_util has been removed ``cpu`` ceilometer_ none
since Stein.
``memory.resident`` ceilometer_ none ``memory.resident`` ceilometer_ none
======================= ============ ======= ========================= ======================= ============ ======= =========================
@@ -65,15 +64,16 @@ Configuration
Strategy parameters are: Strategy parameters are:
============== ====== ============= ==================================== ============== ====== ==================== ====================================
parameter type default Value description parameter type default Value description
============== ====== ============= ==================================== ============== ====== ==================== ====================================
``metrics`` String 'cpu_util' Workload balance base on cpu or ram ``metrics`` String 'instance_cpu_usage' Workload balance base on cpu or ram
utilization. choice: ['cpu_util', utilization. Choices:
'memory.resident'] ['instance_cpu_usage',
``threshold`` Number 25.0 Workload threshold for migration 'instance_ram_usage']
``period`` Number 300 Aggregate time period of ceilometer ``threshold`` Number 25.0 Workload threshold for migration
============== ====== ============= ==================================== ``period`` Number 300 Aggregate time period of ceilometer
============== ====== ==================== ====================================
Efficacy Indicator Efficacy Indicator
------------------ ------------------
@@ -95,7 +95,7 @@ How to use it ?
at1 workload_balancing --strategy workload_balance at1 workload_balancing --strategy workload_balance
$ openstack optimize audit create -a at1 -p threshold=26.0 \ $ openstack optimize audit create -a at1 -p threshold=26.0 \
-p period=310 -p metrics=cpu_util -p period=310 -p metrics=instance_cpu_usage
External Links External Links
-------------- --------------

View File

@@ -1,150 +0,0 @@
alabaster==0.7.10
alembic==0.9.8
amqp==2.2.2
appdirs==1.4.3
APScheduler==3.5.1
asn1crypto==0.24.0
automaton==1.14.0
beautifulsoup4==4.6.0
cachetools==2.0.1
certifi==2018.1.18
cffi==1.11.5
chardet==3.0.4
cliff==2.11.0
cmd2==0.8.1
contextlib2==0.5.5
coverage==4.5.1
croniter==0.3.20
cryptography==2.1.4
debtcollector==1.19.0
decorator==4.2.1
deprecation==2.0
doc8==0.8.0
docutils==0.14
dogpile.cache==0.6.5
dulwich==0.19.0
enum34==1.1.6
enum-compat==0.0.2
eventlet==0.20.0
extras==1.0.0
fasteners==0.14.1
fixtures==3.0.0
freezegun==0.3.10
futurist==1.8.0
gitdb2==2.0.3
GitPython==2.1.8
gnocchiclient==7.0.1
greenlet==0.4.13
idna==2.6
imagesize==1.0.0
iso8601==0.1.12
Jinja2==2.10
jmespath==0.9.3
jsonpatch==1.21
jsonpointer==2.0
jsonschema==3.2.0
keystoneauth1==3.4.0
keystonemiddleware==4.21.0
kombu==4.1.0
linecache2==1.0.0
logutils==0.3.5
lxml==4.1.1
Mako==1.0.7
MarkupSafe==1.0
mccabe==0.2.1
microversion_parse==0.2.1
monotonic==1.4
msgpack==0.5.6
munch==2.2.0
netaddr==0.7.19
netifaces==0.10.6
networkx==2.2
openstacksdk==0.12.0
os-api-ref===1.4.0
os-client-config==1.29.0
os-service-types==1.2.0
os-testr==1.0.0
osc-lib==1.10.0
os-resource-classes==0.4.0
oslo.cache==1.29.0
oslo.concurrency==3.26.0
oslo.config==5.2.0
oslo.context==2.21.0
oslo.db==4.35.0
oslo.i18n==3.20.0
oslo.log==3.37.0
oslo.messaging==8.1.2
oslo.middleware==3.35.0
oslo.policy==1.34.0
oslo.reports==1.27.0
oslo.serialization==2.25.0
oslo.service==1.30.0
oslo.upgradecheck==0.1.0
oslo.utils==3.36.0
oslo.versionedobjects==1.32.0
oslotest==3.3.0
packaging==17.1
Paste==2.0.3
PasteDeploy==1.5.2
pbr==3.1.1
pecan==1.3.2
pika==0.10.0
pika-pool==0.1.3
prettytable==0.7.2
psutil==5.4.3
pycadf==2.7.0
pycparser==2.18
Pygments==2.2.0
pyinotify==0.9.6
pyOpenSSL==17.5.0
pyparsing==2.2.0
pyperclip==1.6.0
python-ceilometerclient==2.9.0
python-cinderclient==3.5.0
python-dateutil==2.7.0
python-editor==1.0.3
python-glanceclient==2.9.1
python-ironicclient==2.5.0
python-keystoneclient==3.15.0
python-mimeparse==1.6.0
python-monascaclient==1.12.0
python-neutronclient==6.7.0
python-novaclient==14.1.0
python-openstackclient==3.14.0
python-subunit==1.2.0
pytz==2018.3
PyYAML==3.12
repoze.lru==0.7
requests==2.18.4
requestsexceptions==1.4.0
restructuredtext-lint==1.1.3
rfc3986==1.1.0
Routes==2.4.1
simplegeneric==0.8.1
simplejson==3.13.2
smmap2==2.0.3
snowballstemmer==1.2.1
SQLAlchemy==1.2.5
sqlalchemy-migrate==0.11.0
sqlparse==0.2.4
statsd==3.2.2
stestr==2.0.0
stevedore==1.28.0
taskflow==3.7.1
Tempita==0.5.2
tenacity==4.9.0
testresources==2.0.1
testscenarios==0.5.0
testtools==2.3.0
traceback2==1.4.0
tzlocal==1.5.1
ujson==1.35
unittest2==1.1.0
urllib3==1.22
vine==1.1.4
waitress==1.1.0
warlock==1.3.0
WebOb==1.8.5
WebTest==2.0.29
wrapt==1.10.11
WSME==0.9.2

View File

@@ -0,0 +1,20 @@
---
upgrade:
- |
The default value of ``[oslo_policy] policy_file`` config option has
been changed from ``policy.json`` to ``policy.yaml``.
Operators who are utilizing customized or previously generated
static policy JSON files (which are not needed by default), should
generate new policy files or convert them in YAML format. Use the
`oslopolicy-convert-json-to-yaml
<https://docs.openstack.org/oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html>`_
tool to convert a JSON to YAML formatted policy file in
backward compatible way.
deprecations:
- |
Use of JSON policy files was deprecated by the ``oslo.policy`` library
during the Victoria development cycle. As a result, this deprecation is
being noted in the Wallaby cycle with an anticipated future removal of support
by ``oslo.policy``. As such operators will need to convert to YAML policy
files. Please see the upgrade notes for details on migration of any
custom policy files.

View File

@@ -0,0 +1,6 @@
===========================
2023.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.1

View File

@@ -0,0 +1,6 @@
===========================
2023.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.2

View File

@@ -53,7 +53,7 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
copyright = u'2016, Watcher developers' copyright = '2016, Watcher developers'
# Release notes are version independent # Release notes are version independent
# The short X.Y version. # The short X.Y version.
@@ -196,8 +196,8 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]) # (source start file, target name, title, author, documentclass [howto/manual])
latex_documents = [ latex_documents = [
('index', 'watcher.tex', u'Watcher Documentation', ('index', 'watcher.tex', 'Watcher Documentation',
u'Watcher developers', 'manual'), 'Watcher developers', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@@ -226,8 +226,8 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'watcher', u'Watcher Documentation', ('index', 'watcher', 'Watcher Documentation',
[u'Watcher developers'], 1) ['Watcher developers'], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@@ -240,8 +240,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'watcher', u'Watcher Documentation', ('index', 'watcher', 'Watcher Documentation',
u'Watcher developers', 'watcher', 'One line description of project.', 'Watcher developers', 'watcher', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]

View File

@@ -21,6 +21,13 @@ Contents:
:maxdepth: 1 :maxdepth: 1
unreleased unreleased
2023.2
2023.1
zed
yoga
xena
wallaby
victoria
ussuri ussuri
train train
stein stein

View File

@@ -1,14 +1,18 @@
# Andi Chandler <andi@gowling.com>, 2017. #zanata # Andi Chandler <andi@gowling.com>, 2017. #zanata
# Andi Chandler <andi@gowling.com>, 2018. #zanata # Andi Chandler <andi@gowling.com>, 2018. #zanata
# Andi Chandler <andi@gowling.com>, 2020. #zanata
# Andi Chandler <andi@gowling.com>, 2022. #zanata
# Andi Chandler <andi@gowling.com>, 2023. #zanata
# Andi Chandler <andi@gowling.com>, 2024. #zanata
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: python-watcher\n" "Project-Id-Version: python-watcher\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-11-08 01:22+0000\n" "POT-Creation-Date: 2024-05-31 14:40+0000\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2018-11-07 06:15+0000\n" "PO-Revision-Date: 2024-04-18 12:21+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n" "Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n" "Language-Team: English (United Kingdom)\n"
"Language: en_GB\n" "Language: en_GB\n"
@@ -54,6 +58,70 @@ msgstr "1.7.0"
msgid "1.9.0" msgid "1.9.0"
msgstr "1.9.0" msgstr "1.9.0"
msgid "2.0.0"
msgstr "2.0.0"
msgid "2023.1 Series Release Notes"
msgstr "2023.1 Series Release Notes"
msgid "2023.2 Series Release Notes"
msgstr "2023.2 Series Release Notes"
msgid "3.0.0"
msgstr "3.0.0"
msgid "4.0.0"
msgstr "4.0.0"
msgid "6.0.0"
msgstr "6.0.0"
msgid "A ``watcher-status upgrade check`` has been added for this."
msgstr "A ``watcher-status upgrade check`` has been added for this."
msgid ""
"A new threadpool for the decision engine that contributors can use to "
"improve the performance of many operations, primarily I/O bound onces. The "
"amount of workers used by the decision engine threadpool can be configured "
"to scale according to the available infrastructure using the "
"`watcher_decision_engine.max_general_workers` config option. Documentation "
"for contributors to effectively use this threadpool is available online: "
"https://docs.openstack.org/watcher/latest/contributor/concurrency.html"
msgstr ""
"A new threadpool for the decision engine that contributors can use to "
"improve the performance of many operations, primarily I/O bound onces. The "
"amount of workers used by the decision engine threadpool can be configured "
"to scale according to the available infrastructure using the "
"`watcher_decision_engine.max_general_workers` config option. Documentation "
"for contributors to effectively use this threadpool is available online: "
"https://docs.openstack.org/watcher/latest/contributor/concurrency.html"
msgid ""
"API calls while building the Compute data model will be retried upon "
"failure. The amount of failures allowed before giving up and the time before "
"reattempting are configurable. The `api_call_retries` and "
"`api_query_timeout` parameters in the `[collector]` group can be used to "
"adjust these paremeters. 10 retries with a 1 second time in between "
"reattempts is the default."
msgstr ""
"API calls while building the Compute data model will be retried upon "
"failure. The amount of failures allowed before giving up and the time before "
"reattempting are configurable. The `api_call_retries` and "
"`api_query_timeout` parameters in the `[collector]` group can be used to "
"adjust these parameters. 10 retries with a 1 second time in between "
"reattempts is the default."
msgid ""
"Add a new webhook API and a new audit type EVENT, the microversion is 1.4. "
"Now Watcher user can create audit with EVENT type and the audit will be "
"triggered by webhook API. The user guide is available online: https://docs."
"openstack.org/watcher/latest/user/event_type_audit.html"
msgstr ""
"Add a new webhook API and a new audit type EVENT, the microversion is 1.4. "
"Now Watcher user can create audit with EVENT type and the audit will be "
"triggered by webhook API. The user guide is available online: https://docs."
"openstack.org/watcher/latest/user/event_type_audit.html"
msgid "Add a service supervisor to watch Watcher deamons." msgid "Add a service supervisor to watch Watcher deamons."
msgstr "Add a service supervisor to watch Watcher daemons." msgstr "Add a service supervisor to watch Watcher daemons."
@@ -67,6 +135,24 @@ msgstr ""
"Add description property for dynamic action. Admin can see detail " "Add description property for dynamic action. Admin can see detail "
"information of any specify action." "information of any specify action."
msgid ""
"Add force field to Audit. User can set --force to enable the new option when "
"launching audit. If force is True, audit will be executed despite of ongoing "
"actionplan. The new audit may create a wrong actionplan if they use the same "
"data model."
msgstr ""
"Add force field to Audit. User can set --force to enable the new option when "
"launching audit. If force is True, audit will be executed despite of ongoing "
"actionplan. The new audit may create a wrong actionplan if they use the same "
"data model."
msgid ""
"Add keystone_client Group for user to configure 'interface' and "
"'region_name' by watcher.conf. The default value of 'interface' is 'admin'."
msgstr ""
"Add keystone_client Group for user to configure 'interface' and "
"'region_name' by watcher.conf. The default value of 'interface' is 'admin'."
msgid "Add notifications related to Action object." msgid "Add notifications related to Action object."
msgstr "Add notifications related to Action object." msgstr "Add notifications related to Action object."
@@ -79,6 +165,25 @@ msgstr "Add notifications related to Audit object."
msgid "Add notifications related to Service object." msgid "Add notifications related to Service object."
msgstr "Add notifications related to Service object." msgstr "Add notifications related to Service object."
msgid ""
"Add show data model api for Watcher. New in version 1.3. User can use "
"'openstack optimize datamodel list' command to view the current data model "
"information in memory. User can also add '--audit <Audit_UUID>' to view "
"specific data model in memory filted by the scope in audit. User can also "
"add '--detail' to view detailed information about current data model. User "
"can also add '--type <type>' to specify the type of data model. Default type "
"is 'compute'. In the future, type 'storage' and 'baremetal' will be "
"supported."
msgstr ""
"Add show data model API for Watcher. New in version 1.3. User can use "
"'openstack optimize datamodel list' command to view the current data model "
"information in memory. User can also add '--audit <Audit_UUID>' to view "
"specific data model in memory filtered by the scope in audit. User can also "
"add '--detail' to view detailed information about current data model. User "
"can also add '--type <type>' to specify the type of data model. Default type "
"is 'compute'. In the future, type 'storage' and 'baremetal' will be "
"supported."
msgid "" msgid ""
"Add start_time and end_time fields in audits table. User can set the start " "Add start_time and end_time fields in audits table. User can set the start "
"time and/or end time when creating CONTINUOUS audit." "time and/or end time when creating CONTINUOUS audit."
@@ -93,6 +198,19 @@ msgstr ""
"Add superseded state for an action plan if the cluster data model has " "Add superseded state for an action plan if the cluster data model has "
"changed after it has been created." "changed after it has been created."
msgid ""
"Added Placement API helper to Watcher. Now Watcher can get information about "
"resource providers, it can be used for the data model and strategies. Config "
"group placement_client with options 'api_version', 'interface' and "
"'region_name' is also added. The default values for 'api_version' and "
"'interface' are 1.29 and 'public', respectively."
msgstr ""
"Added Placement API helper to Watcher. Now Watcher can get information about "
"resource providers, it can be used for the data model and strategies. Config "
"group placement_client with options 'api_version', 'interface' and "
"'region_name' is also added. The default values for 'api_version' and "
"'interface' are 1.29 and 'public', respectively."
msgid "Added SUSPENDED audit state" msgid "Added SUSPENDED audit state"
msgstr "Added SUSPENDED audit state" msgstr "Added SUSPENDED audit state"
@@ -107,6 +225,31 @@ msgstr ""
"scoring engine by different Strategies, which improve the code and data " "scoring engine by different Strategies, which improve the code and data "
"model re-use." "model re-use."
msgid ""
"Added a new config option 'action_execution_rule' which is a dict type. Its "
"key field is strategy name and the value is 'ALWAYS' or 'ANY'. 'ALWAYS' "
"means the callback function returns True as usual. 'ANY' means the return "
"depends on the result of previous action execution. The callback returns "
"True if previous action gets failed, and the engine continues to run the "
"next action. If previous action executes success, the callback returns False "
"then the next action will be ignored. For strategies that aren't in "
"'action_execution_rule', the callback always returns True. Please add the "
"next section in the watcher.conf file if your strategy needs this feature. "
"[watcher_workflow_engines.taskflow] action_execution_rule = {'your strategy "
"name': 'ANY'}"
msgstr ""
"Added a new config option 'action_execution_rule' which is a dict type. Its "
"key field is strategy name and the value is 'ALWAYS' or 'ANY'. 'ALWAYS' "
"means the callback function returns True as usual. 'ANY' means the return "
"depends on the result of previous action execution. The callback returns "
"True if previous action gets failed, and the engine continues to run the "
"next action. If previous action executes success, the callback returns False "
"then the next action will be ignored. For strategies that aren't in "
"'action_execution_rule', the callback always returns True. Please add the "
"next section in the watcher.conf file if your strategy needs this feature. "
"[watcher_workflow_engines.taskflow] action_execution_rule = {'your strategy "
"name': 'ANY'}"
msgid "" msgid ""
"Added a new strategy based on the airflow of servers. This strategy makes " "Added a new strategy based on the airflow of servers. This strategy makes "
"decisions to migrate VMs to make the airflow uniform." "decisions to migrate VMs to make the airflow uniform."
@@ -248,6 +391,15 @@ msgstr ""
"The strategy migrates many instances and volumes efficiently with minimum " "The strategy migrates many instances and volumes efficiently with minimum "
"downtime automatically." "downtime automatically."
msgid ""
"Added strategy \"node resource consolidation\". This strategy is used to "
"centralize VMs to as few nodes as possible by VM migration. User can set an "
"input parameter to decide how to select the destination node."
msgstr ""
"Added strategy \"node resource consolidation\". This strategy is used to "
"centralize VMs to as few nodes as possible by VM migration. User can set an "
"input parameter to decide how to select the destination node."
msgid "" msgid ""
"Added strategy to identify and migrate a Noisy Neighbor - a low priority VM " "Added strategy to identify and migrate a Noisy Neighbor - a low priority VM "
"that negatively affects peformance of a high priority VM by over utilizing " "that negatively affects peformance of a high priority VM by over utilizing "
@@ -284,6 +436,19 @@ msgstr ""
msgid "Adds baremetal data model in Watcher" msgid "Adds baremetal data model in Watcher"
msgstr "Adds baremetal data model in Watcher" msgstr "Adds baremetal data model in Watcher"
msgid ""
"All datasources can now be configured to retry retrieving a metric upon "
"encountering an error. Between each attempt will be a set amount of time "
"which can be adjusted from the configuration. These configuration options "
"can be found in the `[watcher_datasources]` group and are named "
"`query_max_retries` and `query_timeout`."
msgstr ""
"All datasources can now be configured to retry retrieving a metric upon "
"encountering an error. Between each attempt will be a set amount of time "
"which can be adjusted from the configuration. These configuration options "
"can be found in the `[watcher_datasources]` group and are named "
"`query_max_retries` and `query_timeout`."
msgid "" msgid ""
"Allow decision engine to pass strategy parameters, like optimization " "Allow decision engine to pass strategy parameters, like optimization "
"threshold, to selected strategy, also strategy to provide parameters info to " "threshold, to selected strategy, also strategy to provide parameters info to "
@@ -293,6 +458,34 @@ msgstr ""
"threshold, to selected strategy, also strategy to provide parameters info to " "threshold, to selected strategy, also strategy to provide parameters info to "
"end user." "end user."
msgid ""
"Allow using file to override metric map. Override the metric map of each "
"datasource as soon as it is created by the manager. This override comes from "
"a file whose path is provided by a setting in config file. The setting is "
"`watcher_decision_engine/metric_map_path`. The file contains a map per "
"datasource whose keys are the metric names as recognized by watcher and the "
"value is the real name of the metric in the datasource. This setting "
"defaults to `/etc/watcher/metric_map.yaml`, and presence of this file is "
"optional."
msgstr ""
"Allow using file to override metric map. Override the metric map of each "
"datasource as soon as it is created by the manager. This override comes from "
"a file whose path is provided by a setting in config file. The setting is "
"`watcher_decision_engine/metric_map_path`. The file contains a map per "
"datasource whose keys are the metric names as recognized by watcher and the "
"value is the real name of the metric in the datasource. This setting "
"defaults to `/etc/watcher/metric_map.yaml`, and presence of this file is "
"optional."
msgid ""
"An Watcher API WSGI application script ``watcher-api-wsgi`` is now "
"available. It is auto-generated by ``pbr`` and allows to run the API service "
"using WSGI server (for example Nginx and uWSGI)."
msgstr ""
"An Watcher API WSGI application script ``watcher-api-wsgi`` is now "
"available. It is auto-generated by ``pbr`` and allows to run the API service "
"using WSGI server (for example Nginx and uWSGI)."
msgid "" msgid ""
"Audits have 'name' field now, that is more friendly to end users. Audit's " "Audits have 'name' field now, that is more friendly to end users. Audit's "
"name can't exceed 63 characters." "name can't exceed 63 characters."
@@ -300,9 +493,25 @@ msgstr ""
"Audits have 'name' field now, that is more friendly to end users. Audit's " "Audits have 'name' field now, that is more friendly to end users. Audit's "
"name can't exceed 63 characters." "name can't exceed 63 characters."
msgid ""
"Baremetal Model gets Audit scoper with an ability to exclude Ironic nodes."
msgstr ""
"Baremetal Model gets Audit scope with an ability to exclude Ironic nodes."
msgid "Bug Fixes" msgid "Bug Fixes"
msgstr "Bug Fixes" msgstr "Bug Fixes"
msgid ""
"Ceilometer Datasource has been deprecated since its API has been deprecated "
"in Ocata cycle. Watcher has supported Ceilometer for some releases after "
"Ocata to let users migrate to Gnocchi/Monasca datasources. Since Train "
"release, Ceilometer support will be removed."
msgstr ""
"Ceilometer Datasource has been deprecated since its API has been deprecated "
"in Ocata cycle. Watcher has supported Ceilometer for some releases after "
"Ocata to let users migrate to Gnocchi/Monasca datasources. Since Train "
"release, Ceilometer support will be removed."
msgid "Centralize all configuration options for Watcher." msgid "Centralize all configuration options for Watcher."
msgstr "Centralise all configuration options for Watcher." msgstr "Centralise all configuration options for Watcher."
@@ -360,6 +569,52 @@ msgstr ""
"Now instances from particular project in OpenStack can be excluded from " "Now instances from particular project in OpenStack can be excluded from "
"audit defining scope in audit templates." "audit defining scope in audit templates."
msgid ""
"For a large cloud infrastructure, retrieving data from Nova may take a long "
"time. To avoid getting too much data from Nova, building the compute data "
"model according to the scope of audit."
msgstr ""
"For a large cloud infrastructure, retrieving data from Nova may take a long "
"time. To avoid getting too much data from Nova, building the compute data "
"model according to the scope of audit."
msgid ""
"Grafana has been added as datasource that can be used for collecting "
"metrics. The configuration options allow to specify what metrics and how "
"they are stored in grafana so that no matter how Grafana is configured it "
"can still be used. The configuration can be done via the typical "
"configuration file but it is recommended to configure most options in the "
"yaml file for metrics. For a complete walkthrough on configuring Grafana "
"see: https://docs.openstack.org/watcher/latest/datasources/grafana.html"
msgstr ""
"Grafana has been added as datasource that can be used for collecting "
"metrics. The configuration options allow to specify what metrics and how "
"they are stored in Grafana so that no matter how Grafana is configured it "
"can still be used. The configuration can be done via the typical "
"configuration file but it is recommended to configure most options in the "
"yaml file for metrics. For a complete walkthrough on configuring Grafana "
"see: https://docs.openstack.org/watcher/latest/datasources/grafana.html"
msgid ""
"If Gnocchi was configured to have a custom amount of retries and or a custom "
"timeout then the configuration needs to moved into the "
"`[watcher_datasources]` group instead of the `[gnocchi_client]` group."
msgstr ""
"If Gnocchi was configured to have a custom amount of retries and or a custom "
"timeout then the configuration needs to moved into the "
"`[watcher_datasources]` group instead of the `[gnocchi_client]` group."
msgid ""
"Improved interface for datasource baseclass that better defines expected "
"values and types for parameters and return types of all abstract methods. "
"This allows all strategies to work with every datasource provided the "
"metrics are configured for that given datasource."
msgstr ""
"Improved interface for datasource baseclass that better defines expected "
"values and types for parameters and return types of all abstract methods. "
"This allows all strategies to work with every datasource provided the "
"metrics are configured for that given datasource."
msgid "" msgid ""
"Instance cold migration logic is now replaced with using Nova migrate " "Instance cold migration logic is now replaced with using Nova migrate "
"Server(migrate Action) API which has host option since v2.56." "Server(migrate Action) API which has host option since v2.56."
@@ -367,6 +622,17 @@ msgstr ""
"Instance cold migration logic is now replaced with using Nova migrate " "Instance cold migration logic is now replaced with using Nova migrate "
"Server(migrate Action) API which has host option since v2.56." "Server(migrate Action) API which has host option since v2.56."
msgid ""
"Many operations in the decision engine will block on I/O. Such I/O "
"operations can stall the execution of a sequential application "
"significantly. To reduce the potential bottleneck of many operations the "
"general purpose decision engine threadpool is introduced."
msgstr ""
"Many operations in the decision engine will block on I/O. Such I/O "
"operations can stall the execution of a sequential application "
"significantly. To reduce the potential bottleneck of many operations the "
"general purpose decision engine threadpool is introduced."
msgid "New Features" msgid "New Features"
msgstr "New Features" msgstr "New Features"
@@ -389,6 +655,13 @@ msgstr ""
"Nova API version is now set to 2.56 by default. This needs the migrate " "Nova API version is now set to 2.56 by default. This needs the migrate "
"action of migration type cold with destination_node parameter to work." "action of migration type cold with destination_node parameter to work."
msgid ""
"Now Watcher strategy can select specific planner beyond default. Strategy "
"can set planner property to specify its own planner."
msgstr ""
"Now Watcher strategy can select specific planner beyond default. Strategy "
"can set planner property to specify its own planner."
msgid "Ocata Series Release Notes" msgid "Ocata Series Release Notes"
msgstr "Ocata Series Release Notes" msgstr "Ocata Series Release Notes"
@@ -429,12 +702,77 @@ msgstr ""
"resources will be called \"Audit scope\" and will be defined in each audit " "resources will be called \"Audit scope\" and will be defined in each audit "
"template (which contains the audit settings)." "template (which contains the audit settings)."
msgid ""
"Python 2.7 support has been dropped. Last release of Watcher to support "
"py2.7 is OpenStack Train. The minimum version of Python now supported by "
"Watcher is Python 3.6."
msgstr ""
"Python 2.7 support has been dropped. Last release of Watcher to support "
"py2.7 is OpenStack Train. The minimum version of Python now supported by "
"Watcher is Python 3.6."
msgid "Queens Series Release Notes" msgid "Queens Series Release Notes"
msgstr "Queens Series Release Notes" msgstr "Queens Series Release Notes"
msgid "Rocky Series Release Notes" msgid "Rocky Series Release Notes"
msgstr "Rocky Series Release Notes" msgstr "Rocky Series Release Notes"
msgid ""
"Several strategies have changed the `node` parameter to `compute_node` to be "
"better aligned with terminology. These strategies include "
"`basic_consolidation` and `workload_stabilzation`. The `node` parameter will "
"remain supported during Train release and will be removed in the subsequent "
"release."
msgstr ""
"Several strategies have changed the `node` parameter to `compute_node` to be "
"better aligned with terminology. These strategies include "
"`basic_consolidation` and `workload_stabilzation`. The `node` parameter will "
"remain supported during Train release and will be removed in the subsequent "
"release."
msgid ""
"Specific strategies can override this order and use datasources which are "
"not listed in the global preference."
msgstr ""
"Specific strategies can override this order and use datasources which are "
"not listed in the global preference."
msgid "Stein Series Release Notes"
msgstr "Stein Series Release Notes"
msgid ""
"The building of the compute (Nova) data model will be done using the "
"decision engine threadpool, thereby, significantly reducing the total time "
"required to build it."
msgstr ""
"The building of the compute (Nova) data model will be done using the "
"decision engine threadpool, thereby, significantly reducing the total time "
"required to build it."
msgid ""
"The configuration options for query retries in `[gnocchi_client]` are "
"deprecated and the option in `[watcher_datasources]` should now be used."
msgstr ""
"The configuration options for query retries in `[gnocchi_client]` are "
"deprecated and the option in `[watcher_datasources]` should now be used."
msgid ""
"The default value of ``[oslo_policy] policy_file`` config option has been "
"changed from ``policy.json`` to ``policy.yaml``. Operators who are utilizing "
"customized or previously generated static policy JSON files (which are not "
"needed by default), should generate new policy files or convert them in YAML "
"format. Use the `oslopolicy-convert-json-to-yaml <https://docs.openstack.org/"
"oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html>`_ tool to "
"convert a JSON to YAML formatted policy file in backward compatible way."
msgstr ""
"The default value of ``[oslo_policy] policy_file`` config option has been "
"changed from ``policy.json`` to ``policy.yaml``. Operators who are utilizing "
"customized or previously generated static policy JSON files (which are not "
"needed by default), should generate new policy files or convert them in YAML "
"format. Use the `oslopolicy-convert-json-to-yaml <https://docs.openstack.org/"
"oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html>`_ tool to "
"convert a JSON to YAML formatted policy file in backward compatible way."
msgid "" msgid ""
"The graph model describes how VMs are associated to compute hosts. This " "The graph model describes how VMs are associated to compute hosts. This "
"allows for seeing relationships upfront between the entities and hence can " "allows for seeing relationships upfront between the entities and hence can "
@@ -455,6 +793,22 @@ msgstr ""
"was fixed. Before fixing, it booted an instance in the service project as a " "was fixed. Before fixing, it booted an instance in the service project as a "
"migrated instance." "migrated instance."
msgid ""
"The minimum required version of the ``[nova_client]/api_version`` value is "
"now enforced to be ``2.56`` which is available since the Queens version of "
"the nova compute service."
msgstr ""
"The minimum required version of the ``[nova_client]/api_version`` value is "
"now enforced to be ``2.56`` which is available since the Queens version of "
"the Nova compute service."
msgid ""
"The new strategy baseclass has significant changes in method parameters and "
"any out-of-tree strategies will have to be adopted."
msgstr ""
"The new strategy baseclass has significant changes in method parameters and "
"any out-of-tree strategies will have to be adopted."
msgid "" msgid ""
"There is new ability to create Watcher continuous audits with cron interval. " "There is new ability to create Watcher continuous audits with cron interval. "
"It means you may use, for example, optional argument '--interval \"\\*/5 \\* " "It means you may use, for example, optional argument '--interval \"\\*/5 \\* "
@@ -468,9 +822,45 @@ msgstr ""
"best effort basis and therefore, we recommend you to use a minimal cron " "best effort basis and therefore, we recommend you to use a minimal cron "
"interval of at least one minute." "interval of at least one minute."
msgid "Train Series Release Notes"
msgstr "Train Series Release Notes"
msgid "Upgrade Notes" msgid "Upgrade Notes"
msgstr "Upgrade Notes" msgstr "Upgrade Notes"
msgid ""
"Use of JSON policy files was deprecated by the ``oslo.policy`` library "
"during the Victoria development cycle. As a result, this deprecation is "
"being noted in the Wallaby cycle with an anticipated future removal of "
"support by ``oslo.policy``. As such operators will need to convert to YAML "
"policy files. Please see the upgrade notes for details on migration of any "
"custom policy files."
msgstr ""
"Use of JSON policy files was deprecated by the ``oslo.policy`` library "
"during the Victoria development cycle. As a result, this deprecation is "
"being noted in the Wallaby cycle with an anticipated future removal of "
"support by ``oslo.policy``. As such operators will need to convert to YAML "
"policy files. Please see the upgrade notes for details on migration of any "
"custom policy files."
msgid ""
"Using ``watcher/api/app.wsgi`` script is deprecated and it will be removed "
"in U release. Please switch to automatically generated ``watcher-api-wsgi`` "
"script instead."
msgstr ""
"Using ``watcher/api/app.wsgi`` script is deprecated and it will be removed "
"in U release. Please switch to automatically generated ``watcher-api-wsgi`` "
"script instead."
msgid "Ussuri Series Release Notes"
msgstr "Ussuri Series Release Notes"
msgid "Victoria Series Release Notes"
msgstr "Victoria Series Release Notes"
msgid "Wallaby Series Release Notes"
msgstr "Wallaby Series Release Notes"
msgid "" msgid ""
"Watcher can continuously optimize the OpenStack cloud for a specific " "Watcher can continuously optimize the OpenStack cloud for a specific "
"strategy or goal by triggering an audit periodically which generates an " "strategy or goal by triggering an audit periodically which generates an "
@@ -480,6 +870,15 @@ msgstr ""
"strategy or goal by triggering an audit periodically which generates an " "strategy or goal by triggering an audit periodically which generates an "
"action plan and run it automatically." "action plan and run it automatically."
msgid ""
"Watcher can get resource information such as total, allocation ratio and "
"reserved information from Placement API. Now we add some new fields to the "
"Watcher Data Model:"
msgstr ""
"Watcher can get resource information such as total, allocation ratio and "
"reserved information from Placement API. Now we add some new fields to the "
"Watcher Data Model:"
msgid "" msgid ""
"Watcher can now run specific actions in parallel improving the performances " "Watcher can now run specific actions in parallel improving the performances "
"dramatically when executing an action plan." "dramatically when executing an action plan."
@@ -517,6 +916,15 @@ msgstr ""
"includes all instances. It filters excluded instances when migration during " "includes all instances. It filters excluded instances when migration during "
"the audit." "the audit."
msgid ""
"Watcher now supports configuring which datasource to use and in which order. "
"This configuration is done by specifying datasources in the "
"watcher_datasources section:"
msgstr ""
"Watcher now supports configuring which datasource to use and in which order. "
"This configuration is done by specifying datasources in the "
"watcher_datasources section:"
msgid "" msgid ""
"Watcher removes the support to Nova legacy notifications because of Nova " "Watcher removes the support to Nova legacy notifications because of Nova "
"will deprecate them." "will deprecate them."
@@ -557,9 +965,24 @@ msgstr ""
"Watcher supports multiple metrics backend and relies on Ceilometer and " "Watcher supports multiple metrics backend and relies on Ceilometer and "
"Monasca." "Monasca."
msgid "We also add some new propeties:"
msgstr "We also add some new properties:"
msgid "Welcome to watcher's Release Notes documentation!" msgid "Welcome to watcher's Release Notes documentation!"
msgstr "Welcome to watcher's Release Notes documentation!" msgstr "Welcome to watcher's Release Notes documentation!"
msgid "Xena Series Release Notes"
msgstr "Xena Series Release Notes"
msgid "Yoga Series Release Notes"
msgstr "Yoga Series Release Notes"
msgid "Zed Series Release Notes"
msgstr "Zed Series Release Notes"
msgid "``[watcher_datasources] datasources = gnocchi,monasca,ceilometer``"
msgstr "``[watcher_datasources] datasources = gnocchi,monasca,ceilometer``"
msgid "" msgid ""
"all Watcher objects have been refactored to support OVO (oslo." "all Watcher objects have been refactored to support OVO (oslo."
"versionedobjects) which was a prerequisite step in order to implement " "versionedobjects) which was a prerequisite step in order to implement "
@@ -569,6 +992,21 @@ msgstr ""
"versionedobjects) which was a prerequisite step in order to implement " "versionedobjects) which was a prerequisite step in order to implement "
"versioned notifications." "versioned notifications."
msgid ""
"disk_gb_capacity: The amount of disk, take allocation ratio into account, "
"but do not include reserved."
msgstr ""
"disk_gb_capacity: The amount of disk, take allocation ratio into account, "
"but do not include reserved."
msgid ""
"disk_gb_reserved: The amount of disk a node has reserved for its own use."
msgstr ""
"disk_gb_reserved: The amount of disk a node has reserved for its own use."
msgid "disk_ratio: Disk allocation ratio."
msgstr "disk_ratio: Disk allocation ratio."
msgid "instance.create.end" msgid "instance.create.end"
msgstr "instance.create.end" msgstr "instance.create.end"
@@ -635,6 +1073,21 @@ msgstr "instance.unshelve.end"
msgid "instance.update" msgid "instance.update"
msgstr "instance.update" msgstr "instance.update"
msgid ""
"memory_mb_capacity: The amount of memory, take allocation ratio into "
"account, but do not include reserved."
msgstr ""
"memory_mb_capacity: The amount of memory, take allocation ratio into "
"account, but do not include reserved."
msgid ""
"memory_mb_reserved: The amount of memory a node has reserved for its own use."
msgstr ""
"memory_mb_reserved: The amount of memory a node has reserved for its own use."
msgid "memory_ratio: Memory allocation ratio."
msgstr "memory_ratio: Memory allocation ratio."
msgid "new:" msgid "new:"
msgstr "new:" msgstr "new:"
@@ -649,3 +1102,16 @@ msgstr "service.delete"
msgid "service.update" msgid "service.update"
msgstr "service.update" msgstr "service.update"
msgid ""
"vcpu_capacity: The amount of vcpu, take allocation ratio into account, but "
"do not include reserved."
msgstr ""
"vcpu_capacity: The amount of vcpu, take allocation ratio into account, but "
"do not include reserved."
msgid "vcpu_ratio: CPU allocation ratio."
msgstr "vcpu_ratio: CPU allocation ratio."
msgid "vcpu_reserved: The amount of cpu a node has reserved for its own use."
msgstr "vcpu_reserved: The amount of CPU a node has reserved for its own use."

View File

@@ -0,0 +1,6 @@
=============================
Victoria Series Release Notes
=============================
.. release-notes::
:branch: stable/victoria

View File

@@ -0,0 +1,6 @@
============================
Wallaby Series Release Notes
============================
.. release-notes::
:branch: stable/wallaby

View File

@@ -0,0 +1,6 @@
=========================
Xena Series Release Notes
=========================
.. release-notes::
:branch: stable/xena

View File

@@ -0,0 +1,6 @@
=========================
Yoga Series Release Notes
=========================
.. release-notes::
:branch: stable/yoga

View File

@@ -0,0 +1,6 @@
========================
Zed Series Release Notes
========================
.. release-notes::
:branch: stable/zed

View File

@@ -1,4 +1,4 @@
# The order of packages is significant, because pip processes them in the order # The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
@@ -7,30 +7,29 @@ jsonpatch>=1.21 # BSD
keystoneauth1>=3.4.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0
jsonschema>=3.2.0 # MIT jsonschema>=3.2.0 # MIT
keystonemiddleware>=4.21.0 # Apache-2.0 keystonemiddleware>=4.21.0 # Apache-2.0
lxml>=4.1.1 # BSD lxml>=4.5.1 # BSD
croniter>=0.3.20 # MIT License croniter>=0.3.20 # MIT License
os-resource-classes>=0.4.0 os-resource-classes>=0.4.0
oslo.concurrency>=3.26.0 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0
oslo.cache>=1.29.0 # Apache-2.0 oslo.cache>=1.29.0 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0 oslo.config>=6.8.0 # Apache-2.0
oslo.context>=2.21.0 # Apache-2.0 oslo.context>=2.21.0 # Apache-2.0
oslo.db>=4.35.0 # Apache-2.0 oslo.db>=4.44.0 # Apache-2.0
oslo.i18n>=3.20.0 # Apache-2.0 oslo.i18n>=3.20.0 # Apache-2.0
oslo.log>=3.37.0 # Apache-2.0 oslo.log>=3.37.0 # Apache-2.0
oslo.messaging>=8.1.2 # Apache-2.0 oslo.messaging>=14.1.0 # Apache-2.0
oslo.policy>=1.34.0 # Apache-2.0 oslo.policy>=3.6.0 # Apache-2.0
oslo.reports>=1.27.0 # Apache-2.0 oslo.reports>=1.27.0 # Apache-2.0
oslo.serialization>=2.25.0 # Apache-2.0 oslo.serialization>=2.25.0 # Apache-2.0
oslo.service>=1.30.0 # Apache-2.0 oslo.service>=1.30.0 # Apache-2.0
oslo.upgradecheck>=0.1.0 # Apache-2.0 oslo.upgradecheck>=1.3.0 # Apache-2.0
oslo.utils>=3.36.0 # Apache-2.0 oslo.utils>=3.36.0 # Apache-2.0
oslo.versionedobjects>=1.32.0 # Apache-2.0 oslo.versionedobjects>=1.32.0 # Apache-2.0
PasteDeploy>=1.5.2 # MIT PasteDeploy>=1.5.2 # MIT
pbr>=3.1.1 # Apache-2.0 pbr>=3.1.1 # Apache-2.0
pecan>=1.3.2 # BSD pecan>=1.3.2 # BSD
PrettyTable<0.8,>=0.7.2 # BSD PrettyTable>=0.7.2 # BSD
gnocchiclient>=7.0.1 # Apache-2.0 gnocchiclient>=7.0.1 # Apache-2.0
python-ceilometerclient>=2.9.0 # Apache-2.0
python-cinderclient>=3.5.0 # Apache-2.0 python-cinderclient>=3.5.0 # Apache-2.0
python-glanceclient>=2.9.1 # Apache-2.0 python-glanceclient>=2.9.1 # Apache-2.0
python-keystoneclient>=3.15.0 # Apache-2.0 python-keystoneclient>=3.15.0 # Apache-2.0
@@ -41,9 +40,9 @@ python-openstackclient>=3.14.0 # Apache-2.0
python-ironicclient>=2.5.0 # Apache-2.0 python-ironicclient>=2.5.0 # Apache-2.0
SQLAlchemy>=1.2.5 # MIT SQLAlchemy>=1.2.5 # MIT
stevedore>=1.28.0 # Apache-2.0 stevedore>=1.28.0 # Apache-2.0
taskflow>=3.7.1 # Apache-2.0 taskflow>=3.8.0 # Apache-2.0
WebOb>=1.8.5 # MIT WebOb>=1.8.5 # MIT
WSME>=0.9.2 # MIT WSME>=0.9.2 # MIT
networkx>=2.2 # BSD networkx>=2.4 # BSD
microversion_parse>=0.2.1 # Apache-2.0 microversion_parse>=0.2.1 # Apache-2.0
futurist>=1.8.0 # Apache-2.0 futurist>=1.8.0 # Apache-2.0

View File

@@ -1,12 +1,12 @@
[metadata] [metadata]
name = python-watcher name = python-watcher
summary = OpenStack Watcher provides a flexible and scalable resource optimization service for multi-tenant OpenStack-based clouds. summary = OpenStack Watcher provides a flexible and scalable resource optimization service for multi-tenant OpenStack-based clouds.
description-file = description_file =
README.rst README.rst
author = OpenStack author = OpenStack
author-email = openstack-discuss@lists.openstack.org author_email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/watcher/latest/ home_page = https://docs.openstack.org/watcher/latest/
python-requires = >=3.6 python_requires = >=3.8
classifier = classifier =
Environment :: OpenStack Environment :: OpenStack
Intended Audience :: Information Technology Intended Audience :: Information Technology
@@ -17,9 +17,10 @@ classifier =
Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[files] [files]
packages = packages =

View File

@@ -7,9 +7,9 @@ doc8>=0.8.0 # Apache-2.0
freezegun>=0.3.10 # Apache-2.0 freezegun>=0.3.10 # Apache-2.0
hacking>=3.0.1,<3.1.0 # Apache-2.0 hacking>=3.0.1,<3.1.0 # Apache-2.0
oslotest>=3.3.0 # Apache-2.0 oslotest>=3.3.0 # Apache-2.0
os-testr>=1.0.0 # Apache-2.0
testscenarios>=0.5.0 # Apache-2.0/BSD testscenarios>=0.5.0 # Apache-2.0/BSD
testtools>=2.3.0 # MIT testtools>=2.3.0 # MIT
stestr>=2.0.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0
os-api-ref>=1.4.0 # Apache-2.0 os-api-ref>=1.4.0 # Apache-2.0
bandit>=1.6.0 # Apache-2.0 bandit>=1.6.0 # Apache-2.0
WebTest>=2.0.27 # MIT

78
tox.ini
View File

@@ -1,37 +1,41 @@
[tox] [tox]
minversion = 2.0 minversion = 3.18.0
envlist = py36,py37,pep8 envlist = py3,pep8
skipsdist = True
ignore_basepython_conflict = True ignore_basepython_conflict = True
[testenv] [testenv]
basepython = python3 basepython = python3
usedevelop = True usedevelop = True
whitelist_externals = find allowlist_externals = find
rm rm
install_command = pip install {opts} {packages} install_command = pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} {opts} {packages}
setenv = setenv =
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
deps = deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
python-libmaas>=0.6.8
commands = commands =
rm -f .testrepository/times.dbm rm -f .testrepository/times.dbm
find . -type f -name "*.py[c|o]" -delete find . -type f -name "*.py[c|o]" -delete
stestr run {posargs} stestr run {posargs}
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY passenv =
http_proxy
HTTP_PROXY
https_proxy
HTTPS_PROXY
no_proxy
NO_PROXY
[testenv:pep8] [testenv:pep8]
commands = commands =
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
flake8 flake8
bandit -r watcher -x watcher/tests/* -n5 -ll -s B320,B322 #bandit -r watcher -x watcher/tests/* -n5 -ll -s B320
[testenv:venv] [testenv:venv]
setenv = PYTHONHASHSEED=0 setenv = PYTHONHASHSEED=0
deps = deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/doc/requirements.txt -r{toxinidir}/doc/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
@@ -49,14 +53,15 @@ commands =
[testenv:docs] [testenv:docs]
setenv = PYTHONHASHSEED=0 setenv = PYTHONHASHSEED=0
deps = -r{toxinidir}/doc/requirements.txt deps =
-r{toxinidir}/doc/requirements.txt
commands = commands =
rm -fr doc/build doc/source/api/ .autogenerated rm -fr doc/build doc/source/api/ .autogenerated
sphinx-build -W --keep-going -b html doc/source doc/build/html sphinx-build -W --keep-going -b html doc/source doc/build/html
[testenv:api-ref] [testenv:api-ref]
deps = -r{toxinidir}/doc/requirements.txt deps = -r{toxinidir}/doc/requirements.txt
whitelist_externals = bash allowlist_externals = bash
commands = commands =
bash -c 'rm -rf api-ref/build' bash -c 'rm -rf api-ref/build'
sphinx-build -W --keep-going -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html sphinx-build -W --keep-going -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html
@@ -73,6 +78,28 @@ commands =
commands = commands =
oslopolicy-sample-generator --config-file etc/watcher/oslo-policy-generator/watcher-policy-generator.conf oslopolicy-sample-generator --config-file etc/watcher/oslo-policy-generator/watcher-policy-generator.conf
[testenv:wheel]
commands = python setup.py bdist_wheel
[testenv:pdf-docs]
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
allowlist_externals =
rm
make
commands =
rm -rf doc/build/pdf
sphinx-build -W --keep-going -b latex doc/source doc/build/pdf
make -C doc/build/pdf
[testenv:releasenotes]
deps = -r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r watcher -x watcher/tests/* -n5 -ll -s B320
[flake8] [flake8]
filename = *.py,app.wsgi filename = *.py,app.wsgi
show-source=True show-source=True
@@ -82,9 +109,6 @@ builtins= _
enable-extensions = H106,H203,H904 enable-extensions = H106,H203,H904
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/,releasenotes exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/,releasenotes
[testenv:wheel]
commands = python setup.py bdist_wheel
[hacking] [hacking]
import_exceptions = watcher._i18n import_exceptions = watcher._i18n
@@ -108,33 +132,7 @@ extension =
N366 = checks:import_stock_mock N366 = checks:import_stock_mock
paths = ./watcher/hacking paths = ./watcher/hacking
[doc8] [doc8]
extension=.rst extension=.rst
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed # todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed
ignore-path=doc/source/image_src,doc/source/man,doc/source/api ignore-path=doc/source/image_src,doc/source/man,doc/source/api
[testenv:pdf-docs]
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
whitelist_externals =
rm
make
commands =
rm -rf doc/build/pdf
sphinx-build -W --keep-going -b latex doc/source doc/build/pdf
make -C doc/build/pdf
[testenv:releasenotes]
deps = -r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r watcher -x watcher/tests/* -n5 -ll -s B320
[testenv:lower-constraints]
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt

View File

@@ -57,6 +57,7 @@ are dynamically loaded by Watcher at launch time.
import datetime import datetime
from http import HTTPStatus
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
@@ -362,7 +363,7 @@ class ActionsController(rest.RestController):
return Action.convert_with_links(action) return Action.convert_with_links(action)
@wsme_pecan.wsexpose(Action, body=Action, status_code=201) @wsme_pecan.wsexpose(Action, body=Action, status_code=HTTPStatus.CREATED)
def post(self, action): def post(self, action):
"""Create a new action(forbidden). """Create a new action(forbidden).
@@ -422,7 +423,7 @@ class ActionsController(rest.RestController):
action_to_update.save() action_to_update.save()
return Action.convert_with_links(action_to_update) return Action.convert_with_links(action_to_update)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204) @wsme_pecan.wsexpose(None, types.uuid, status_code=HTTPStatus.NO_CONTENT)
def delete(self, action_uuid): def delete(self, action_uuid):
"""Delete a action(forbidden). """Delete a action(forbidden).

View File

@@ -56,6 +56,7 @@ state machine <action_plan_state_machine>`.
import datetime import datetime
from http import HTTPStatus
from oslo_log import log from oslo_log import log
import pecan import pecan
from pecan import rest from pecan import rest
@@ -460,7 +461,7 @@ class ActionPlansController(rest.RestController):
return ActionPlan.convert_with_links(action_plan) return ActionPlan.convert_with_links(action_plan)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204) @wsme_pecan.wsexpose(None, types.uuid, status_code=HTTPStatus.NO_CONTENT)
def delete(self, action_plan_uuid): def delete(self, action_plan_uuid):
"""Delete an action plan. """Delete an action plan.

View File

@@ -32,6 +32,7 @@ states, visit :ref:`the Audit State machine <audit_state_machine>`.
import datetime import datetime
from dateutil import tz from dateutil import tz
from http import HTTPStatus
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
@@ -595,7 +596,8 @@ class AuditsController(rest.RestController):
return Audit.convert_with_links(rpc_audit) return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201) @wsme_pecan.wsexpose(Audit, body=AuditPostType,
status_code=HTTPStatus.CREATED)
def post(self, audit_p): def post(self, audit_p):
"""Create a new audit. """Create a new audit.
@@ -717,7 +719,7 @@ class AuditsController(rest.RestController):
audit_to_update.save() audit_to_update.save()
return Audit.convert_with_links(audit_to_update) return Audit.convert_with_links(audit_to_update)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204) @wsme_pecan.wsexpose(None, wtypes.text, status_code=HTTPStatus.NO_CONTENT)
def delete(self, audit): def delete(self, audit):
"""Delete an audit. """Delete an audit.

View File

@@ -45,6 +45,7 @@ will be launched automatically or will need a manual confirmation from the
import datetime import datetime
from http import HTTPStatus
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
@@ -618,7 +619,7 @@ class AuditTemplatesController(rest.RestController):
@wsme.validate(types.uuid, AuditTemplatePostType) @wsme.validate(types.uuid, AuditTemplatePostType)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType, @wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
status_code=201) status_code=HTTPStatus.CREATED)
def post(self, audit_template_postdata): def post(self, audit_template_postdata):
"""Create a new audit template. """Create a new audit template.
@@ -694,7 +695,7 @@ class AuditTemplatesController(rest.RestController):
audit_template_to_update.save() audit_template_to_update.save()
return AuditTemplate.convert_with_links(audit_template_to_update) return AuditTemplate.convert_with_links(audit_template_to_update)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204) @wsme_pecan.wsexpose(None, wtypes.text, status_code=HTTPStatus.NO_CONTENT)
def delete(self, audit_template): def delete(self, audit_template):
"""Delete a audit template. """Delete a audit template.

View File

@@ -19,8 +19,6 @@ Service mechanism provides ability to monitor Watcher services state.
""" """
import datetime import datetime
import six
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from oslo_utils import timeutils from oslo_utils import timeutils
@@ -70,7 +68,7 @@ class Service(base.APIBase):
service = objects.Service.get(pecan.request.context, id) service = objects.Service.get(pecan.request.context, id)
last_heartbeat = (service.last_seen_up or service.updated_at or last_heartbeat = (service.last_seen_up or service.updated_at or
service.created_at) service.created_at)
if isinstance(last_heartbeat, six.string_types): if isinstance(last_heartbeat, str):
# NOTE(russellb) If this service came in over rpc via # NOTE(russellb) If this service came in over rpc via
# conductor, then the timestamp will be a string and needs to be # conductor, then the timestamp will be a string and needs to be
# converted back to a datetime. # converted back to a datetime.

View File

@@ -15,7 +15,6 @@
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import strutils from oslo_utils import strutils
import six
import wsme import wsme
from wsme import types as wtypes from wsme import types as wtypes
@@ -132,7 +131,7 @@ class JsonType(wtypes.UserType):
def __str__(self): def __str__(self):
# These are the json serializable native types # These are the json serializable native types
return ' | '.join(map(str, (wtypes.text, six.integer_types, float, return ' | '.join(map(str, (wtypes.text, int, float,
BooleanType, list, dict, None))) BooleanType, list, dict, None)))
@staticmethod @staticmethod

View File

@@ -14,6 +14,7 @@
Webhook endpoint for Watcher v1 REST API. Webhook endpoint for Watcher v1 REST API.
""" """
from http import HTTPStatus
from oslo_log import log from oslo_log import log
import pecan import pecan
from pecan import rest from pecan import rest
@@ -36,7 +37,7 @@ class WebhookController(rest.RestController):
self.dc_client = rpcapi.DecisionEngineAPI() self.dc_client = rpcapi.DecisionEngineAPI()
@wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype, @wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype,
status_code=202) status_code=HTTPStatus.ACCEPTED)
def post(self, audit_ident, body): def post(self, audit_ident, body):
"""Trigger the given audit. """Trigger the given audit.

View File

@@ -15,7 +15,7 @@
# under the License. # under the License.
from http import client as http_client from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from pecan import hooks from pecan import hooks
@@ -91,8 +91,8 @@ class NoExceptionTracebackHook(hooks.PecanHook):
# Do nothing if there is no error. # Do nothing if there is no error.
# Status codes in the range 200 (OK) to 399 (400 = BAD_REQUEST) are not # Status codes in the range 200 (OK) to 399 (400 = BAD_REQUEST) are not
# an error. # an error.
if (http_client.OK <= state.response.status_int < if (HTTPStatus.OK <= state.response.status_int <
http_client.BAD_REQUEST): HTTPStatus.BAD_REQUEST):
return return
json_body = state.response.json json_body = state.response.json

View File

@@ -17,17 +17,17 @@
# limitations under the License. # limitations under the License.
# #
import enum
import time import time
from oslo_log import log
from watcher._i18n import _ from watcher._i18n import _
from watcher.applier.actions import base from watcher.applier.actions import base
from watcher.common import exception from watcher.common import exception
from watcher.common.metal_helper import constants as metal_constants
from watcher.common.metal_helper import factory as metal_helper_factory
LOG = log.getLogger(__name__)
class NodeState(enum.Enum):
POWERON = 'on'
POWEROFF = 'off'
class ChangeNodePowerState(base.BaseAction): class ChangeNodePowerState(base.BaseAction):
@@ -43,8 +43,8 @@ class ChangeNodePowerState(base.BaseAction):
'state': str, 'state': str,
}) })
The `resource_id` references a ironic node id (list of available The `resource_id` references a baremetal node id (list of available
ironic node is returned by this command: ``ironic node-list``). ironic nodes is returned by this command: ``ironic node-list``).
The `state` value should either be `on` or `off`. The `state` value should either be `on` or `off`.
""" """
@@ -59,10 +59,14 @@ class ChangeNodePowerState(base.BaseAction):
'type': 'string', 'type': 'string',
"minlength": 1 "minlength": 1
}, },
'resource_name': {
'type': 'string',
"minlength": 1
},
'state': { 'state': {
'type': 'string', 'type': 'string',
'enum': [NodeState.POWERON.value, 'enum': [metal_constants.PowerState.ON.value,
NodeState.POWEROFF.value] metal_constants.PowerState.OFF.value]
} }
}, },
'required': ['resource_id', 'state'], 'required': ['resource_id', 'state'],
@@ -82,10 +86,10 @@ class ChangeNodePowerState(base.BaseAction):
return self._node_manage_power(target_state) return self._node_manage_power(target_state)
def revert(self): def revert(self):
if self.state == NodeState.POWERON.value: if self.state == metal_constants.PowerState.ON.value:
target_state = NodeState.POWEROFF.value target_state = metal_constants.PowerState.OFF.value
elif self.state == NodeState.POWEROFF.value: elif self.state == metal_constants.PowerState.OFF.value:
target_state = NodeState.POWERON.value target_state = metal_constants.PowerState.ON.value
return self._node_manage_power(target_state) return self._node_manage_power(target_state)
def _node_manage_power(self, state, retry=60): def _node_manage_power(self, state, retry=60):
@@ -93,30 +97,32 @@ class ChangeNodePowerState(base.BaseAction):
raise exception.IllegalArgumentException( raise exception.IllegalArgumentException(
message=_("The target state is not defined")) message=_("The target state is not defined"))
ironic_client = self.osc.ironic() metal_helper = metal_helper_factory.get_helper(self.osc)
nova_client = self.osc.nova() node = metal_helper.get_node(self.node_uuid)
current_state = ironic_client.node.get(self.node_uuid).power_state current_state = node.get_power_state()
# power state: 'power on' or 'power off', if current node state
# is the same as state, just return True if state == current_state.value:
if state in current_state:
return True return True
if state == NodeState.POWEROFF.value: if state == metal_constants.PowerState.OFF.value:
node_info = ironic_client.node.get(self.node_uuid).to_dict() compute_node = node.get_hypervisor_node().to_dict()
compute_node_id = node_info['extra']['compute_node_id']
compute_node = nova_client.hypervisors.get(compute_node_id)
compute_node = compute_node.to_dict()
if (compute_node['running_vms'] == 0): if (compute_node['running_vms'] == 0):
ironic_client.node.set_power_state( node.set_power_state(state)
self.node_uuid, state) else:
LOG.warning(
"Compute node %s has %s running vms and will "
"NOT be shut off.",
compute_node["hypervisor_hostname"],
compute_node['running_vms'])
return False
else: else:
ironic_client.node.set_power_state(self.node_uuid, state) node.set_power_state(state)
ironic_node = ironic_client.node.get(self.node_uuid) node = metal_helper.get_node(self.node_uuid)
while ironic_node.power_state == current_state and retry: while node.get_power_state() == current_state and retry:
time.sleep(10) time.sleep(10)
retry -= 1 retry -= 1
ironic_node = ironic_client.node.get(self.node_uuid) node = metal_helper.get_node(self.node_uuid)
if retry > 0: if retry > 0:
return True return True
else: else:
@@ -130,4 +136,4 @@ class ChangeNodePowerState(base.BaseAction):
def get_description(self): def get_description(self):
"""Description of the action""" """Description of the action"""
return ("Compute node power on/off through ironic.") return ("Compute node power on/off through Ironic or MaaS.")

View File

@@ -14,6 +14,7 @@
import sys import sys
from oslo_upgradecheck import common_checks
from oslo_upgradecheck import upgradecheck from oslo_upgradecheck import upgradecheck
from watcher._i18n import _ from watcher._i18n import _
@@ -43,6 +44,10 @@ class Checks(upgradecheck.UpgradeCommands):
_upgrade_checks = ( _upgrade_checks = (
# Added in Train. # Added in Train.
(_('Minimum Nova API Version'), _minimum_nova_api_version), (_('Minimum Nova API Version'), _minimum_nova_api_version),
# Added in Wallaby.
(_("Policy File JSON to YAML Migration"),
(common_checks.check_policy_json, {'conf': CONF})),
) )

View File

@@ -17,7 +17,7 @@ import time
from oslo_log import log from oslo_log import log
from cinderclient import exceptions as cinder_exception from cinderclient import exceptions as cinder_exception
from cinderclient.v2.volumes import Volume from cinderclient.v3.volumes import Volume
from watcher._i18n import _ from watcher._i18n import _
from watcher.common import clients from watcher.common import clients
from watcher.common import exception from watcher.common import exception

View File

@@ -25,6 +25,7 @@ from novaclient import api_versions as nova_api_versions
from novaclient import client as nvclient from novaclient import client as nvclient
from watcher.common import exception from watcher.common import exception
from watcher.common import utils
try: try:
from ceilometerclient import client as ceclient from ceilometerclient import client as ceclient
@@ -32,6 +33,12 @@ try:
except ImportError: except ImportError:
HAS_CEILCLIENT = False HAS_CEILCLIENT = False
try:
from maas import client as maas_client
except ImportError:
maas_client = None
CONF = cfg.CONF CONF = cfg.CONF
_CLIENTS_AUTH_GROUP = 'watcher_clients_auth' _CLIENTS_AUTH_GROUP = 'watcher_clients_auth'
@@ -74,6 +81,7 @@ class OpenStackClients(object):
self._monasca = None self._monasca = None
self._neutron = None self._neutron = None
self._ironic = None self._ironic = None
self._maas = None
self._placement = None self._placement = None
def _get_keystone_session(self): def _get_keystone_session(self):
@@ -265,6 +273,23 @@ class OpenStackClients(object):
session=self.session) session=self.session)
return self._ironic return self._ironic
def maas(self):
if self._maas:
return self._maas
if not maas_client:
raise exception.UnsupportedError(
"MAAS client unavailable. Please install python-libmaas.")
url = self._get_client_option('maas', 'url')
api_key = self._get_client_option('maas', 'api_key')
timeout = self._get_client_option('maas', 'timeout')
self._maas = utils.async_compat_call(
maas_client.connect,
url, apikey=api_key,
timeout=timeout)
return self._maas
@exception.wrap_keystone_exception @exception.wrap_keystone_exception
def placement(self): def placement(self):
if self._placement: if self._placement:

View File

@@ -25,6 +25,7 @@ SHOULD include dedicated exception logging.
import functools import functools
import sys import sys
from http import HTTPStatus
from keystoneclient import exceptions as keystone_exceptions from keystoneclient import exceptions as keystone_exceptions
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
@@ -62,7 +63,7 @@ class WatcherException(Exception):
""" """
msg_fmt = _("An unknown exception occurred") msg_fmt = _("An unknown exception occurred")
code = 500 code = HTTPStatus.INTERNAL_SERVER_ERROR
headers = {} headers = {}
safe = False safe = False
@@ -114,12 +115,12 @@ class UnsupportedError(WatcherException):
class NotAuthorized(WatcherException): class NotAuthorized(WatcherException):
msg_fmt = _("Not authorized") msg_fmt = _("Not authorized")
code = 403 code = HTTPStatus.FORBIDDEN
class NotAcceptable(WatcherException): class NotAcceptable(WatcherException):
msg_fmt = _("Request not acceptable.") msg_fmt = _("Request not acceptable.")
code = 406 code = HTTPStatus.NOT_ACCEPTABLE
class PolicyNotAuthorized(NotAuthorized): class PolicyNotAuthorized(NotAuthorized):
@@ -132,7 +133,7 @@ class OperationNotPermitted(NotAuthorized):
class Invalid(WatcherException, ValueError): class Invalid(WatcherException, ValueError):
msg_fmt = _("Unacceptable parameters") msg_fmt = _("Unacceptable parameters")
code = 400 code = HTTPStatus.BAD_REQUEST
class ObjectNotFound(WatcherException): class ObjectNotFound(WatcherException):
@@ -141,12 +142,12 @@ class ObjectNotFound(WatcherException):
class Conflict(WatcherException): class Conflict(WatcherException):
msg_fmt = _('Conflict') msg_fmt = _('Conflict')
code = 409 code = HTTPStatus.CONFLICT
class ResourceNotFound(ObjectNotFound): class ResourceNotFound(ObjectNotFound):
msg_fmt = _("The %(name)s resource %(id)s could not be found") msg_fmt = _("The %(name)s resource %(id)s could not be found")
code = 404 code = HTTPStatus.NOT_FOUND
class InvalidParameter(Invalid): class InvalidParameter(Invalid):

View File

View File

@@ -0,0 +1,81 @@
# Copyright 2023 Cloudbase Solutions
# 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 abc
from watcher.common import exception
from watcher.common.metal_helper import constants as metal_constants
class BaseMetalNode(abc.ABC):
hv_up_when_powered_off = False
def __init__(self, nova_node=None):
self._nova_node = nova_node
def get_hypervisor_node(self):
if not self._nova_node:
raise exception.Invalid(message="No associated hypervisor.")
return self._nova_node
def get_hypervisor_hostname(self):
return self.get_hypervisor_node().hypervisor_hostname
@abc.abstractmethod
def get_power_state(self):
# TODO(lpetrut): document the following methods
pass
@abc.abstractmethod
def get_id(self):
"""Return the node id provided by the bare metal service."""
pass
@abc.abstractmethod
def power_on(self):
pass
@abc.abstractmethod
def power_off(self):
pass
def set_power_state(self, state):
state = metal_constants.PowerState(state)
if state == metal_constants.PowerState.ON:
self.power_on()
elif state == metal_constants.PowerState.OFF:
self.power_off()
else:
raise exception.UnsupportedActionType(
"Cannot set power state: %s" % state)
class BaseMetalHelper(abc.ABC):
def __init__(self, osc):
self._osc = osc
@property
def nova_client(self):
if not getattr(self, "_nova_client", None):
self._nova_client = self._osc.nova()
return self._nova_client
@abc.abstractmethod
def list_compute_nodes(self):
pass
@abc.abstractmethod
def get_node(self, node_id):
pass

View File

@@ -0,0 +1,23 @@
# Copyright 2023 Cloudbase Solutions
# 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 enum
class PowerState(str, enum.Enum):
ON = "on"
OFF = "off"
UNKNOWN = "unknown"
ERROR = "error"

View File

@@ -0,0 +1,33 @@
# Copyright 2023 Cloudbase Solutions
# 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 oslo_config import cfg
from watcher.common import clients
from watcher.common.metal_helper import ironic
from watcher.common.metal_helper import maas
CONF = cfg.CONF
def get_helper(osc=None):
# TODO(lpetrut): consider caching this client.
if not osc:
osc = clients.OpenStackClients()
if CONF.maas_client.url:
return maas.MaasHelper(osc)
else:
return ironic.IronicHelper(osc)

View File

@@ -0,0 +1,94 @@
# Copyright 2023 Cloudbase Solutions
# 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 oslo_log import log
from watcher.common.metal_helper import base
from watcher.common.metal_helper import constants as metal_constants
LOG = log.getLogger(__name__)
POWER_STATES_MAP = {
'power on': metal_constants.PowerState.ON,
'power off': metal_constants.PowerState.OFF,
# For now, we only use ON/OFF states
'rebooting': metal_constants.PowerState.ON,
'soft power off': metal_constants.PowerState.OFF,
'soft reboot': metal_constants.PowerState.ON,
}
class IronicNode(base.BaseMetalNode):
hv_up_when_powered_off = True
def __init__(self, ironic_node, nova_node, ironic_client):
super().__init__(nova_node)
self._ironic_client = ironic_client
self._ironic_node = ironic_node
def get_power_state(self):
return POWER_STATES_MAP.get(self._ironic_node.power_state,
metal_constants.PowerState.UNKNOWN)
def get_id(self):
return self._ironic_node.uuid
def power_on(self):
self._ironic_client.node.set_power_state(self.get_id(), "on")
def power_off(self):
self._ironic_client.node.set_power_state(self.get_id(), "off")
class IronicHelper(base.BaseMetalHelper):
@property
def _client(self):
if not getattr(self, "_cached_client", None):
self._cached_client = self._osc.ironic()
return self._cached_client
def list_compute_nodes(self):
out_list = []
# TODO(lpetrut): consider using "detailed=True" instead of making
# an additional GET request per node
node_list = self._client.node.list()
for node in node_list:
node_info = self._client.node.get(node.uuid)
hypervisor_id = node_info.extra.get('compute_node_id', None)
if hypervisor_id is None:
LOG.warning('Cannot find compute_node_id in extra '
'of ironic node %s', node.uuid)
continue
hypervisor_node = self.nova_client.hypervisors.get(hypervisor_id)
if hypervisor_node is None:
LOG.warning('Cannot find hypervisor %s', hypervisor_id)
continue
out_node = IronicNode(node, hypervisor_node, self._client)
out_list.append(out_node)
return out_list
def get_node(self, node_id):
ironic_node = self._client.node.get(node_id)
compute_node_id = ironic_node.extra.get('compute_node_id')
if compute_node_id:
compute_node = self.nova_client.hypervisors.get(compute_node_id)
else:
compute_node = None
return IronicNode(ironic_node, compute_node, self._client)

View File

@@ -0,0 +1,125 @@
# Copyright 2023 Cloudbase Solutions
# 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 oslo_config import cfg
from oslo_log import log
from watcher.common import exception
from watcher.common.metal_helper import base
from watcher.common.metal_helper import constants as metal_constants
from watcher.common import utils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
try:
from maas.client import enum as maas_enum
except ImportError:
maas_enum = None
class MaasNode(base.BaseMetalNode):
hv_up_when_powered_off = False
def __init__(self, maas_node, nova_node, maas_client):
super().__init__(nova_node)
self._maas_client = maas_client
self._maas_node = maas_node
def get_power_state(self):
maas_state = utils.async_compat_call(
self._maas_node.query_power_state,
timeout=CONF.maas_client.timeout)
# python-libmaas may not be available, so we'll avoid a global
# variable.
power_states_map = {
maas_enum.PowerState.ON: metal_constants.PowerState.ON,
maas_enum.PowerState.OFF: metal_constants.PowerState.OFF,
maas_enum.PowerState.ERROR: metal_constants.PowerState.ERROR,
maas_enum.PowerState.UNKNOWN: metal_constants.PowerState.UNKNOWN,
}
return power_states_map.get(maas_state,
metal_constants.PowerState.UNKNOWN)
def get_id(self):
return self._maas_node.system_id
def power_on(self):
LOG.info("Powering on MAAS node: %s %s",
self._maas_node.fqdn,
self._maas_node.system_id)
utils.async_compat_call(
self._maas_node.power_on,
timeout=CONF.maas_client.timeout)
def power_off(self):
LOG.info("Powering off MAAS node: %s %s",
self._maas_node.fqdn,
self._maas_node.system_id)
utils.async_compat_call(
self._maas_node.power_off,
timeout=CONF.maas_client.timeout)
class MaasHelper(base.BaseMetalHelper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not maas_enum:
raise exception.UnsupportedError(
"MAAS client unavailable. Please install python-libmaas.")
@property
def _client(self):
if not getattr(self, "_cached_client", None):
self._cached_client = self._osc.maas()
return self._cached_client
def list_compute_nodes(self):
out_list = []
node_list = utils.async_compat_call(
self._client.machines.list,
timeout=CONF.maas_client.timeout)
compute_nodes = self.nova_client.hypervisors.list()
compute_node_map = dict()
for compute_node in compute_nodes:
compute_node_map[compute_node.hypervisor_hostname] = compute_node
for node in node_list:
hypervisor_node = compute_node_map.get(node.fqdn)
if not hypervisor_node:
LOG.info('Cannot find hypervisor %s', node.fqdn)
continue
out_node = MaasNode(node, hypervisor_node, self._client)
out_list.append(out_node)
return out_list
def _get_compute_node_by_hostname(self, hostname):
compute_nodes = self.nova_client.hypervisors.search(
hostname, detailed=True)
for compute_node in compute_nodes:
if compute_node.hypervisor_hostname == hostname:
return compute_node
def get_node(self, node_id):
maas_node = utils.async_compat_call(
self._client.machines.get, node_id,
timeout=CONF.maas_client.timeout)
compute_node = self._get_compute_node_by_hostname(maas_node.fqdn)
return MaasNode(maas_node, compute_node, self._client)

View File

@@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@@ -53,7 +54,7 @@ class PlacementHelper(object):
if rp_name: if rp_name:
url += '?name=%s' % rp_name url += '?name=%s' % rp_name
resp = self.get(url) resp = self.get(url)
if resp.status_code == 200: if resp.status_code == HTTPStatus.OK:
json_resp = resp.json() json_resp = resp.json()
return json_resp['resource_providers'] return json_resp['resource_providers']
@@ -77,7 +78,7 @@ class PlacementHelper(object):
""" """
url = '/resource_providers/%s/inventories' % rp_uuid url = '/resource_providers/%s/inventories' % rp_uuid
resp = self.get(url) resp = self.get(url)
if resp.status_code == 200: if resp.status_code == HTTPStatus.OK:
json = resp.json() json = resp.json()
return json['inventories'] return json['inventories']
msg = ("Failed to get resource provider %(rp_uuid)s inventories. " msg = ("Failed to get resource provider %(rp_uuid)s inventories. "
@@ -97,7 +98,7 @@ class PlacementHelper(object):
""" """
resp = self.get("/resource_providers/%s/traits" % rp_uuid) resp = self.get("/resource_providers/%s/traits" % rp_uuid)
if resp.status_code == 200: if resp.status_code == HTTPStatus.OK:
json = resp.json() json = resp.json()
return json['traits'] return json['traits']
msg = ("Failed to get resource provider %(rp_uuid)s traits. " msg = ("Failed to get resource provider %(rp_uuid)s traits. "
@@ -118,7 +119,7 @@ class PlacementHelper(object):
""" """
url = '/allocations/%s' % consumer_uuid url = '/allocations/%s' % consumer_uuid
resp = self.get(url) resp = self.get(url)
if resp.status_code == 200: if resp.status_code == HTTPStatus.OK:
json = resp.json() json = resp.json()
return json['allocations'] return json['allocations']
msg = ("Failed to get allocations for consumer %(c_uuid). " msg = ("Failed to get allocations for consumer %(c_uuid). "
@@ -139,7 +140,7 @@ class PlacementHelper(object):
""" """
url = '/resource_providers/%s/usages' % rp_uuid url = '/resource_providers/%s/usages' % rp_uuid
resp = self.get(url) resp = self.get(url)
if resp.status_code == 200: if resp.status_code == HTTPStatus.OK:
json = resp.json() json = resp.json()
return json['usages'] return json['usages']
msg = ("Failed to get resource provider %(rp_uuid)s usages. " msg = ("Failed to get resource provider %(rp_uuid)s usages. "
@@ -164,7 +165,7 @@ class PlacementHelper(object):
""" """
url = "/allocation_candidates?%s" % resources url = "/allocation_candidates?%s" % resources
resp = self.get(url) resp = self.get(url)
if resp.status_code == 200: if resp.status_code == HTTPStatus.OK:
data = resp.json() data = resp.json()
return data['provider_summaries'] return data['provider_summaries']

View File

@@ -18,6 +18,7 @@
import sys import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import opts
from oslo_policy import policy from oslo_policy import policy
from watcher.common import exception from watcher.common import exception
@@ -26,6 +27,12 @@ from watcher.common import policies
_ENFORCER = None _ENFORCER = None
CONF = cfg.CONF CONF = cfg.CONF
# TODO(gmann): Remove setting the default value of config policy_file
# once oslo_policy change the default value to 'policy.yaml'.
# https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49
DEFAULT_POLICY_FILE = 'policy.yaml'
opts.set_defaults(CONF, DEFAULT_POLICY_FILE)
# we can get a policy enforcer by this init. # we can get a policy enforcer by this init.
# oslo policy support change policy rule dynamically. # oslo policy support change policy rule dynamically.

View File

@@ -121,22 +121,40 @@ class RequestContextSerializer(messaging.Serializer):
def get_client(target, version_cap=None, serializer=None): def get_client(target, version_cap=None, serializer=None):
assert TRANSPORT is not None assert TRANSPORT is not None
serializer = RequestContextSerializer(serializer) serializer = RequestContextSerializer(serializer)
return messaging.RPCClient(TRANSPORT, return messaging.get_rpc_client(
target, TRANSPORT,
version_cap=version_cap, target,
serializer=serializer) version_cap=version_cap,
serializer=serializer
)
def get_server(target, endpoints, serializer=None): def get_server(target, endpoints, serializer=None):
assert TRANSPORT is not None assert TRANSPORT is not None
access_policy = dispatcher.DefaultRPCAccessPolicy access_policy = dispatcher.DefaultRPCAccessPolicy
serializer = RequestContextSerializer(serializer) serializer = RequestContextSerializer(serializer)
return messaging.get_rpc_server(TRANSPORT, return messaging.get_rpc_server(
target, TRANSPORT,
endpoints, target,
executor='eventlet', endpoints,
serializer=serializer, executor='eventlet',
access_policy=access_policy) serializer=serializer,
access_policy=access_policy
)
def get_notification_listener(targets, endpoints, serializer=None, pool=None):
assert NOTIFICATION_TRANSPORT is not None
serializer = RequestContextSerializer(serializer)
return messaging.get_notification_listener(
NOTIFICATION_TRANSPORT,
targets,
endpoints,
allow_requeue=False,
executor='eventlet',
pool=pool,
serializer=serializer
)
def get_notifier(publisher_id): def get_notifier(publisher_id):

View File

@@ -21,14 +21,12 @@ from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_log import _options from oslo_log import _options
from oslo_log import log from oslo_log import log
import oslo_messaging as om import oslo_messaging as messaging
from oslo_reports import guru_meditation_report as gmr from oslo_reports import guru_meditation_report as gmr
from oslo_reports import opts as gmr_opts from oslo_reports import opts as gmr_opts
from oslo_service import service from oslo_service import service
from oslo_service import wsgi from oslo_service import wsgi
from oslo_messaging.rpc import dispatcher
from watcher._i18n import _ from watcher._i18n import _
from watcher.api import app from watcher.api import app
from watcher.common import config from watcher.common import config
@@ -183,11 +181,6 @@ class Service(service.ServiceBase):
] ]
self.notification_endpoints = self.manager.notification_endpoints self.notification_endpoints = self.manager.notification_endpoints
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self._transport = None
self._notification_transport = None
self._conductor_client = None self._conductor_client = None
self.conductor_topic_handler = None self.conductor_topic_handler = None
@@ -201,27 +194,17 @@ class Service(service.ServiceBase):
self.notification_topics, self.notification_endpoints self.notification_topics, self.notification_endpoints
) )
@property
def transport(self):
if self._transport is None:
self._transport = om.get_rpc_transport(CONF)
return self._transport
@property
def notification_transport(self):
if self._notification_transport is None:
self._notification_transport = om.get_notification_transport(CONF)
return self._notification_transport
@property @property
def conductor_client(self): def conductor_client(self):
if self._conductor_client is None: if self._conductor_client is None:
target = om.Target( target = messaging.Target(
topic=self.conductor_topic, topic=self.conductor_topic,
version=self.API_VERSION, version=self.API_VERSION,
) )
self._conductor_client = om.RPCClient( self._conductor_client = rpc.get_client(
self.transport, target, serializer=self.serializer) target,
serializer=base.WatcherObjectSerializer()
)
return self._conductor_client return self._conductor_client
@conductor_client.setter @conductor_client.setter
@@ -229,21 +212,18 @@ class Service(service.ServiceBase):
self.conductor_client = c self.conductor_client = c
def build_topic_handler(self, topic_name, endpoints=()): def build_topic_handler(self, topic_name, endpoints=()):
access_policy = dispatcher.DefaultRPCAccessPolicy target = messaging.Target(
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
target = om.Target(
topic=topic_name, topic=topic_name,
# For compatibility, we can override it with 'host' opt # For compatibility, we can override it with 'host' opt
server=CONF.host or socket.gethostname(), server=CONF.host or socket.gethostname(),
version=self.api_version, version=self.api_version,
) )
return om.get_rpc_server( return rpc.get_server(
self.transport, target, endpoints, target, endpoints,
executor='eventlet', serializer=serializer, serializer=rpc.JsonPayloadSerializer()
access_policy=access_policy) )
def build_notification_handler(self, topic_names, endpoints=()): def build_notification_handler(self, topic_names, endpoints=()):
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
targets = [] targets = []
for topic in topic_names: for topic in topic_names:
kwargs = {} kwargs = {}
@@ -251,11 +231,13 @@ class Service(service.ServiceBase):
exchange, topic = topic.split('.') exchange, topic = topic.split('.')
kwargs['exchange'] = exchange kwargs['exchange'] = exchange
kwargs['topic'] = topic kwargs['topic'] = topic
targets.append(om.Target(**kwargs)) targets.append(messaging.Target(**kwargs))
return om.get_notification_listener(
self.notification_transport, targets, endpoints, return rpc.get_notification_listener(
executor='eventlet', serializer=serializer, targets, endpoints,
allow_requeue=False, pool=CONF.host) serializer=rpc.JsonPayloadSerializer(),
pool=CONF.host
)
def start(self): def start(self):
LOG.debug("Connecting to '%s'", CONF.transport_url) LOG.debug("Connecting to '%s'", CONF.transport_url)

View File

@@ -16,12 +16,16 @@
"""Utilities and helper functions.""" """Utilities and helper functions."""
import asyncio
import datetime import datetime
import inspect
import random import random
import re import re
import string import string
from croniter import croniter from croniter import croniter
import eventlet
from eventlet import tpool
from jsonschema import validators from jsonschema import validators
from oslo_config import cfg from oslo_config import cfg
@@ -162,3 +166,37 @@ Draft4Validator = validators.Draft4Validator
def random_string(n): def random_string(n):
return ''.join([random.choice( return ''.join([random.choice(
string.ascii_letters + string.digits) for i in range(n)]) string.ascii_letters + string.digits) for i in range(n)])
# Some clients (e.g. MAAS) use asyncio, which isn't compatible with Eventlet.
# As a workaround, we're delegating such calls to a native thread.
def async_compat_call(f, *args, **kwargs):
timeout = kwargs.pop('timeout', None)
async def async_wrapper():
ret = f(*args, **kwargs)
if inspect.isawaitable(ret):
return await asyncio.wait_for(ret, timeout)
return ret
def tpool_wrapper():
# This will run in a separate native thread. Ideally, there should be
# a single thread permanently running an asyncio loop, but for
# convenience we'll use eventlet.tpool, which leverages a thread pool.
#
# That being considered, we're setting up a temporary asyncio loop to
# handle this call.
loop = asyncio.new_event_loop()
try:
asyncio.set_event_loop(loop)
return loop.run_until_complete(async_wrapper())
finally:
loop.close()
# We'll use eventlet timeouts as an extra precaution and asyncio timeouts
# to avoid lingering threads. For consistency, we'll convert eventlet
# timeout exceptions to asyncio timeout errors.
with eventlet.timeout.Timeout(
seconds=timeout,
exception=asyncio.TimeoutError("Timeout: %ss" % timeout)):
return tpool.execute(tpool_wrapper)

View File

@@ -35,6 +35,7 @@ from watcher.conf import grafana_client
from watcher.conf import grafana_translators from watcher.conf import grafana_translators
from watcher.conf import ironic_client from watcher.conf import ironic_client
from watcher.conf import keystone_client from watcher.conf import keystone_client
from watcher.conf import maas_client
from watcher.conf import monasca_client from watcher.conf import monasca_client
from watcher.conf import neutron_client from watcher.conf import neutron_client
from watcher.conf import nova_client from watcher.conf import nova_client
@@ -54,6 +55,7 @@ db.register_opts(CONF)
planner.register_opts(CONF) planner.register_opts(CONF)
applier.register_opts(CONF) applier.register_opts(CONF)
decision_engine.register_opts(CONF) decision_engine.register_opts(CONF)
maas_client.register_opts(CONF)
monasca_client.register_opts(CONF) monasca_client.register_opts(CONF)
nova_client.register_opts(CONF) nova_client.register_opts(CONF)
glance_client.register_opts(CONF) glance_client.register_opts(CONF)

View File

@@ -134,7 +134,13 @@ GRAFANA_CLIENT_OPTS = [
"InfluxDB this will be the retention period. " "InfluxDB this will be the retention period. "
"These queries will need to be constructed using tools " "These queries will need to be constructed using tools "
"such as Postman. Example: SELECT cpu FROM {4}." "such as Postman. Example: SELECT cpu FROM {4}."
"cpu_percent WHERE host == '{1}' AND time > now()-{2}s")] "cpu_percent WHERE host == '{1}' AND time > now()-{2}s"),
cfg.IntOpt('http_timeout',
min=0,
default=60,
mutable=True,
help='Timeout for Grafana request')
]
def register_opts(conf): def register_opts(conf):

View File

@@ -0,0 +1,38 @@
# Copyright 2023 Cloudbase Solutions
# 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 oslo_config import cfg
maas_client = cfg.OptGroup(name='maas_client',
title='Configuration Options for MaaS')
MAAS_CLIENT_OPTS = [
cfg.StrOpt('url',
help='MaaS URL, example: http://1.2.3.4:5240/MAAS'),
cfg.StrOpt('api_key',
help='MaaS API authentication key.'),
cfg.IntOpt('timeout',
default=60,
help='MaaS client operation timeout in seconds.')]
def register_opts(conf):
conf.register_group(maas_client)
conf.register_opts(MAAS_CLIENT_OPTS, group=maas_client)
def list_opts():
return [(maas_client, MAAS_CLIENT_OPTS)]

View File

@@ -6,6 +6,7 @@ Create Date: 2017-03-24 11:21:29.036532
""" """
from alembic import op from alembic import op
from sqlalchemy import inspect
import sqlalchemy as sa import sqlalchemy as sa
from watcher.db.sqlalchemy import models from watcher.db.sqlalchemy import models
@@ -14,11 +15,20 @@ from watcher.db.sqlalchemy import models
revision = '0f6042416884' revision = '0f6042416884'
down_revision = '001' down_revision = '001'
def _table_exists(table_name):
bind = op.get_context().bind
insp = inspect(bind)
names = insp.get_table_names()
return any(t == table_name for t in names)
def upgrade(): def upgrade():
if _table_exists('apscheduler_jobs'):
return
op.create_table( op.create_table(
'apscheduler_jobs', 'apscheduler_jobs',
sa.Column('id', sa.Unicode(191, _warn_on_bytestring=False), sa.Column('id', sa.Unicode(191),
nullable=False), nullable=False),
sa.Column('next_run_time', sa.Float(25), index=True), sa.Column('next_run_time', sa.Float(25), index=True),
sa.Column('job_state', sa.LargeBinary, nullable=False), sa.Column('job_state', sa.LargeBinary, nullable=False),

View File

@@ -244,30 +244,36 @@ class Connection(api.BaseConnection):
for relationship in relationships: for relationship in relationships:
if not relationship.uselist: if not relationship.uselist:
# We have a One-to-X relationship # We have a One-to-X relationship
query = query.options(joinedload(relationship.key)) query = query.options(joinedload(
getattr(model, relationship.key)))
return query return query
def _create(self, model, values): def _create(self, model, values):
obj = model() session = get_session()
cleaned_values = {k: v for k, v in values.items() with session.begin():
if k not in self._get_relationships(model)} obj = model()
obj.update(cleaned_values) cleaned_values = {k: v for k, v in values.items()
obj.save() if k not in self._get_relationships(model)}
obj.update(cleaned_values)
obj.save(session=session)
session.commit()
return obj return obj
def _get(self, context, model, fieldname, value, eager): def _get(self, context, model, fieldname, value, eager):
query = model_query(model) session = get_session()
if eager: with session.begin():
query = self._set_eager_options(model, query) query = model_query(model, session=session)
if eager:
query = self._set_eager_options(model, query)
query = query.filter(getattr(model, fieldname) == value) query = query.filter(getattr(model, fieldname) == value)
if not context.show_deleted: if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None)) query = query.filter(model.deleted_at.is_(None))
try: try:
obj = query.one() obj = query.one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=value) raise exception.ResourceNotFound(name=model.__name__, id=value)
return obj return obj
@@ -278,7 +284,7 @@ class Connection(api.BaseConnection):
query = model_query(model, session=session) query = model_query(model, session=session)
query = add_identity_filter(query, id_) query = add_identity_filter(query, id_)
try: try:
ref = query.with_lockmode('update').one() ref = query.with_for_update().one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_) raise exception.ResourceNotFound(name=model.__name__, id=id_)
@@ -815,7 +821,7 @@ class Connection(api.BaseConnection):
query = model_query(models.Action, session=session) query = model_query(models.Action, session=session)
query = add_identity_filter(query, action_id) query = add_identity_filter(query, action_id)
try: try:
ref = query.with_lockmode('update').one() ref = query.with_for_update().one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ActionNotFound(action=action_id) raise exception.ActionNotFound(action=action_id)
@@ -900,7 +906,7 @@ class Connection(api.BaseConnection):
query = model_query(models.ActionPlan, session=session) query = model_query(models.ActionPlan, session=session)
query = add_identity_filter(query, action_plan_id) query = add_identity_filter(query, action_plan_id)
try: try:
ref = query.with_lockmode('update').one() ref = query.with_for_update().one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan_id) raise exception.ActionPlanNotFound(action_plan=action_plan_id)

View File

@@ -22,6 +22,7 @@ from apscheduler.jobstores.base import ConflictingIdError
from apscheduler.jobstores import sqlalchemy from apscheduler.jobstores import sqlalchemy
from apscheduler.util import datetime_to_utc_timestamp from apscheduler.util import datetime_to_utc_timestamp
from apscheduler.util import maybe_ref from apscheduler.util import maybe_ref
from apscheduler.util import utc_timestamp_to_datetime
from watcher.common import context from watcher.common import context
from watcher.common import service from watcher.common import service
@@ -32,7 +33,7 @@ try:
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
import pickle import pickle
from sqlalchemy import Table, MetaData, select, and_ from sqlalchemy import Table, MetaData, select, and_, null
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@@ -58,8 +59,7 @@ class WatcherJobStore(sqlalchemy.SQLAlchemyJobStore):
super(WatcherJobStore, self).__init__(url, engine, tablename, super(WatcherJobStore, self).__init__(url, engine, tablename,
metadata, pickle_protocol) metadata, pickle_protocol)
metadata = maybe_ref(metadata) or MetaData() metadata = maybe_ref(metadata) or MetaData()
self.jobs_t = Table(tablename, metadata, autoload=True, self.jobs_t = Table(tablename, metadata, autoload_with=engine)
autoload_with=engine)
service_ident = service.ServiceHeartbeat.get_service_name() service_ident = service.ServiceHeartbeat.get_service_name()
self.tag = tag or {'host': service_ident[0], 'name': service_ident[1]} self.tag = tag or {'host': service_ident[0], 'name': service_ident[1]}
self.service_id = objects.Service.list(context=context.make_context(), self.service_id = objects.Service.list(context=context.make_context(),
@@ -79,7 +79,8 @@ class WatcherJobStore(sqlalchemy.SQLAlchemyJobStore):
'tag': jsonutils.dumps(self.tag) 'tag': jsonutils.dumps(self.tag)
}) })
try: try:
self.engine.execute(insert) with self.engine.begin() as conn:
conn.execute(insert)
except IntegrityError: except IntegrityError:
raise ConflictingIdError(job.id) raise ConflictingIdError(job.id)
@@ -88,20 +89,36 @@ class WatcherJobStore(sqlalchemy.SQLAlchemyJobStore):
self._fix_paused_jobs_sorting(jobs) self._fix_paused_jobs_sorting(jobs)
return jobs return jobs
def get_next_run_time(self):
selectable = select(self.jobs_t.c.next_run_time).\
where(self.jobs_t.c.next_run_time != null()).\
order_by(self.jobs_t.c.next_run_time).limit(1)
with self.engine.begin() as connection:
# NOTE(danms): The apscheduler implementation of this gets a
# decimal.Decimal back from scalar() which causes
# utc_timestamp_to_datetime() to choke since it is expecting a
# python float. Assume this is SQLAlchemy 2.0 stuff, so just
# coerce to a float here.
next_run_time = connection.execute(selectable).scalar()
return utc_timestamp_to_datetime(float(next_run_time)
if next_run_time is not None
else None)
def _get_jobs(self, *conditions): def _get_jobs(self, *conditions):
jobs = [] jobs = []
conditions += (self.jobs_t.c.service_id == self.service_id,) conditions += (self.jobs_t.c.service_id == self.service_id,)
selectable = select( selectable = select(
[self.jobs_t.c.id, self.jobs_t.c.job_state, self.jobs_t.c.tag] self.jobs_t.c.id, self.jobs_t.c.job_state, self.jobs_t.c.tag
).order_by(self.jobs_t.c.next_run_time).where(and_(*conditions)) ).order_by(self.jobs_t.c.next_run_time).where(and_(*conditions))
failed_job_ids = set() failed_job_ids = set()
for row in self.engine.execute(selectable): with self.engine.begin() as conn:
try: for row in conn.execute(selectable):
jobs.append(self._reconstitute_job(row.job_state)) try:
except Exception: jobs.append(self._reconstitute_job(row.job_state))
self._logger.exception( except Exception:
'Unable to restore job "%s" -- removing it', row.id) self._logger.exception(
failed_job_ids.add(row.id) 'Unable to restore job "%s" -- removing it', row.id)
failed_job_ids.add(row.id)
# Remove all the jobs we failed to restore # Remove all the jobs we failed to restore
if failed_job_ids: if failed_job_ids:

View File

@@ -18,7 +18,6 @@ SQLAlchemy models for watcher service
from oslo_db.sqlalchemy import models from oslo_db.sqlalchemy import models
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import six.moves.urllib.parse as urlparse
from sqlalchemy import Boolean from sqlalchemy import Boolean
from sqlalchemy import Column from sqlalchemy import Column
from sqlalchemy import DateTime from sqlalchemy import DateTime
@@ -33,7 +32,7 @@ from sqlalchemy import String
from sqlalchemy import Text from sqlalchemy import Text
from sqlalchemy.types import TypeDecorator, TEXT from sqlalchemy.types import TypeDecorator, TEXT
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
import urllib.parse as urlparse
from watcher import conf from watcher import conf
CONF = conf.CONF CONF = conf.CONF

View File

@@ -19,6 +19,8 @@ import time
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.common import exception
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -54,7 +56,14 @@ class DataSourceBase(object):
instance_root_disk_size=None, instance_root_disk_size=None,
) )
def query_retry(self, f, *args, **kwargs): def _get_meter(self, meter_name):
"""Retrieve the meter from the metric map or raise error"""
meter = self.METRIC_MAP.get(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
return meter
def query_retry(self, f, *args, ignored_exc=None, **kwargs):
"""Attempts to retrieve metrics from the external service """Attempts to retrieve metrics from the external service
Attempts to access data from the external service and handles Attempts to access data from the external service and handles
@@ -62,15 +71,23 @@ class DataSourceBase(object):
to the value of query_max_retries to the value of query_max_retries
:param f: The method that performs the actual querying for metrics :param f: The method that performs the actual querying for metrics
:param args: Array of arguments supplied to the method :param args: Array of arguments supplied to the method
:param ignored_exc: An exception or tuple of exceptions that shouldn't
be retried, for example "NotFound" exceptions.
:param kwargs: The amount of arguments supplied to the method :param kwargs: The amount of arguments supplied to the method
:return: The value as retrieved from the external service :return: The value as retrieved from the external service
""" """
num_retries = CONF.watcher_datasources.query_max_retries num_retries = CONF.watcher_datasources.query_max_retries
timeout = CONF.watcher_datasources.query_timeout timeout = CONF.watcher_datasources.query_timeout
ignored_exc = ignored_exc or tuple()
for i in range(num_retries): for i in range(num_retries):
try: try:
return f(*args, **kwargs) return f(*args, **kwargs)
except ignored_exc as e:
LOG.debug("Got an ignored exception (%s) while calling: %s ",
e, f)
return
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
self.query_retry_reset(e) self.query_retry_reset(e)
@@ -122,6 +139,30 @@ class DataSourceBase(object):
pass pass
@abc.abstractmethod
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
"""Retrieves metrics based on the specified parameters over a period
:param resource: Resource object as defined in watcher models such as
ComputeNode and Instance
:param resource_type: Indicates which type of object is supplied
to the resource parameter
:param meter_name: The desired metric to retrieve as key from
METRIC_MAP
:param start_time: The datetime to start retrieving metrics for
:type start_time: datetime.datetime
:param end_time: The datetime to limit the retrieval of metrics to
:type end_time: datetime.datetime
:param granularity: Interval between samples in measurements in
seconds
:return: Dictionary of key value pairs with timestamps and metric
values
"""
pass
@abc.abstractmethod @abc.abstractmethod
def get_host_cpu_usage(self, resource, period, aggregate, def get_host_cpu_usage(self, resource, period, aggregate,
granularity=None): granularity=None):

View File

@@ -161,9 +161,7 @@ class CeilometerHelper(base.DataSourceBase):
end_time = datetime.datetime.utcnow() end_time = datetime.datetime.utcnow()
start_time = end_time - datetime.timedelta(seconds=int(period)) start_time = end_time - datetime.timedelta(seconds=int(period))
meter = self.METRIC_MAP.get(meter_name) meter = self._get_meter(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
if aggregate == 'mean': if aggregate == 'mean':
aggregate = 'avg' aggregate = 'avg'
@@ -194,6 +192,12 @@ class CeilometerHelper(base.DataSourceBase):
item_value *= 10 item_value *= 10
return item_value return item_value
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
raise NotImplementedError(
_('Ceilometer helper does not support statistic series method'))
def get_host_cpu_usage(self, resource, period, def get_host_cpu_usage(self, resource, period,
aggregate, granularity=None): aggregate, granularity=None):

View File

@@ -19,11 +19,11 @@
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from gnocchiclient import exceptions as gnc_exc
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.common import clients from watcher.common import clients
from watcher.common import exception
from watcher.decision_engine.datasources import base from watcher.decision_engine.datasources import base
CONF = cfg.CONF CONF = cfg.CONF
@@ -39,7 +39,7 @@ class GnocchiHelper(base.DataSourceBase):
host_inlet_temp='hardware.ipmi.node.temperature', host_inlet_temp='hardware.ipmi.node.temperature',
host_airflow='hardware.ipmi.node.airflow', host_airflow='hardware.ipmi.node.airflow',
host_power='hardware.ipmi.node.power', host_power='hardware.ipmi.node.power',
instance_cpu_usage='cpu_util', instance_cpu_usage='cpu',
instance_ram_usage='memory.resident', instance_ram_usage='memory.resident',
instance_ram_allocated='memory', instance_ram_allocated='memory',
instance_l3_cache_usage='cpu_l3_cache', instance_l3_cache_usage='cpu_l3_cache',
@@ -72,9 +72,7 @@ class GnocchiHelper(base.DataSourceBase):
stop_time = datetime.utcnow() stop_time = datetime.utcnow()
start_time = stop_time - timedelta(seconds=(int(period))) start_time = stop_time - timedelta(seconds=(int(period)))
meter = self.METRIC_MAP.get(meter_name) meter = self._get_meter(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
if aggregate == 'count': if aggregate == 'count':
aggregate = 'mean' aggregate = 'mean'
@@ -87,7 +85,9 @@ class GnocchiHelper(base.DataSourceBase):
kwargs = dict(query={"=": {"original_resource_id": resource_id}}, kwargs = dict(query={"=": {"original_resource_id": resource_id}},
limit=1) limit=1)
resources = self.query_retry( resources = self.query_retry(
f=self.gnocchi.resource.search, **kwargs) f=self.gnocchi.resource.search,
ignored_exc=gnc_exc.NotFound,
**kwargs)
if not resources: if not resources:
LOG.warning("The {0} resource {1} could not be " LOG.warning("The {0} resource {1} could not be "
@@ -96,6 +96,25 @@ class GnocchiHelper(base.DataSourceBase):
resource_id = resources[0]['id'] resource_id = resources[0]['id']
if meter_name == "instance_cpu_usage":
if resource_type != "instance":
LOG.warning("Unsupported resource type for metric "
"'instance_cpu_usage': ", resource_type)
return
# The "cpu_util" gauge (percentage) metric has been removed.
# We're going to obtain the same result by using the rate of change
# aggregate operation.
if aggregate not in ("mean", "rate:mean"):
LOG.warning("Unsupported aggregate for instance_cpu_usage "
"metric: %s. "
"Supported aggregates: mean, rate:mean ",
aggregate)
return
# TODO(lpetrut): consider supporting other aggregates.
aggregate = "rate:mean"
raw_kwargs = dict( raw_kwargs = dict(
metric=meter, metric=meter,
start=start_time, start=start_time,
@@ -108,7 +127,9 @@ class GnocchiHelper(base.DataSourceBase):
kwargs = {k: v for k, v in raw_kwargs.items() if k and v} kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry( statistics = self.query_retry(
f=self.gnocchi.metric.get_measures, **kwargs) f=self.gnocchi.metric.get_measures,
ignored_exc=gnc_exc.NotFound,
**kwargs)
return_value = None return_value = None
if statistics: if statistics:
@@ -120,6 +141,67 @@ class GnocchiHelper(base.DataSourceBase):
# Airflow from hardware.ipmi.node.airflow is reported as # Airflow from hardware.ipmi.node.airflow is reported as
# 1/10 th of actual CFM # 1/10 th of actual CFM
return_value *= 10 return_value *= 10
if meter_name == "instance_cpu_usage":
# "rate:mean" can return negative values for migrated vms.
return_value = max(0, return_value)
# We're converting the cumulative cpu time (ns) to cpu usage
# percentage.
vcpus = resource.vcpus
if not vcpus:
LOG.warning("instance vcpu count not set, assuming 1")
vcpus = 1
return_value *= 100 / (granularity * 10e+8) / vcpus
return return_value
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
meter = self._get_meter(meter_name)
resource_id = resource.uuid
if resource_type == 'compute_node':
resource_id = "%s_%s" % (resource.hostname, resource.hostname)
kwargs = dict(query={"=": {"original_resource_id": resource_id}},
limit=1)
resources = self.query_retry(
f=self.gnocchi.resource.search,
ignored_exc=gnc_exc.NotFound,
**kwargs)
if not resources:
LOG.warning("The {0} resource {1} could not be "
"found".format(self.NAME, resource_id))
return
resource_id = resources[0]['id']
raw_kwargs = dict(
metric=meter,
start=start_time,
stop=end_time,
resource_id=resource_id,
granularity=granularity,
)
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.gnocchi.metric.get_measures,
ignored_exc=gnc_exc.NotFound,
**kwargs)
return_value = None
if statistics:
# measure has structure [time, granularity, value]
if meter_name == 'host_airflow':
# Airflow from hardware.ipmi.node.airflow is reported as
# 1/10 th of actual CFM
return_value = {s[0]: s[2]*10 for s in statistics}
else:
return_value = {s[0]: s[2] for s in statistics}
return return_value return return_value

View File

@@ -18,9 +18,11 @@
from urllib import parse as urlparse from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher._i18n import _
from watcher.common import clients from watcher.common import clients
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.datasources import base from watcher.decision_engine.datasources import base
@@ -136,12 +138,13 @@ class GrafanaHelper(base.DataSourceBase):
raise exception.DataSourceNotAvailable(self.NAME) raise exception.DataSourceNotAvailable(self.NAME)
resp = requests.get(self._base_url + str(project_id) + '/query', resp = requests.get(self._base_url + str(project_id) + '/query',
params=params, headers=self._headers) params=params, headers=self._headers,
if resp.status_code == 200: timeout=CONF.grafana_client.http_timeout)
if resp.status_code == HTTPStatus.OK:
return resp return resp
elif resp.status_code == 400: elif resp.status_code == HTTPStatus.BAD_REQUEST:
LOG.error("Query for metric is invalid") LOG.error("Query for metric is invalid")
elif resp.status_code == 401: elif resp.status_code == HTTPStatus.UNAUTHORIZED:
LOG.error("Authorization token is invalid") LOG.error("Authorization token is invalid")
raise exception.DataSourceNotAvailable(self.NAME) raise exception.DataSourceNotAvailable(self.NAME)
@@ -188,6 +191,12 @@ class GrafanaHelper(base.DataSourceBase):
return result return result
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
raise NotImplementedError(
_('Grafana helper does not support statistic series method'))
def get_host_cpu_usage(self, resource, period=300, def get_host_cpu_usage(self, resource, period=300,
aggregate="mean", granularity=None): aggregate="mean", granularity=None):
return self.statistic_aggregation( return self.statistic_aggregation(

View File

@@ -21,7 +21,6 @@ import datetime
from monascaclient import exc from monascaclient import exc
from watcher.common import clients from watcher.common import clients
from watcher.common import exception
from watcher.decision_engine.datasources import base from watcher.decision_engine.datasources import base
@@ -90,9 +89,7 @@ class MonascaHelper(base.DataSourceBase):
stop_time = datetime.datetime.utcnow() stop_time = datetime.datetime.utcnow()
start_time = stop_time - datetime.timedelta(seconds=(int(period))) start_time = stop_time - datetime.timedelta(seconds=(int(period)))
meter = self.METRIC_MAP.get(meter_name) meter = self._get_meter(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
if aggregate == 'mean': if aggregate == 'mean':
aggregate = 'avg' aggregate = 'avg'
@@ -121,6 +118,34 @@ class MonascaHelper(base.DataSourceBase):
return cpu_usage return cpu_usage
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
meter = self._get_meter(meter_name)
raw_kwargs = dict(
name=meter,
start_time=start_time.isoformat(),
end_time=end_time.isoformat(),
dimensions={'hostname': resource.uuid},
statistics='avg',
group_by='*',
)
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.monasca.metrics.list_statistics, **kwargs)
result = {}
for stat in statistics:
v_index = stat['columns'].index('avg')
t_index = stat['columns'].index('timestamp')
result.update({r[t_index]: r[v_index] for r in stat['statistics']})
return result
def get_host_cpu_usage(self, resource, period, def get_host_cpu_usage(self, resource, period,
aggregate, granularity=None): aggregate, granularity=None):
return self.statistic_aggregation( return self.statistic_aggregation(

View File

@@ -205,7 +205,7 @@ class CinderModelBuilder(base.BaseModelBuilder):
"""Build a storage node from a Cinder storage node """Build a storage node from a Cinder storage node
:param node: A storage node :param node: A storage node
:type node: :py:class:`~cinderclient.v2.services.Service` :type node: :py:class:`~cinderclient.v3.services.Service`
""" """
# node.host is formatted as host@backendname since ocata, # node.host is formatted as host@backendname since ocata,
# or may be only host as of ocata # or may be only host as of ocata
@@ -233,7 +233,7 @@ class CinderModelBuilder(base.BaseModelBuilder):
"""Build a storage pool from a Cinder storage pool """Build a storage pool from a Cinder storage pool
:param pool: A storage pool :param pool: A storage pool
:type pool: :py:class:`~cinderclient.v2.pools.Pool` :type pool: :py:class:`~cinderclient.v3.pools.Pool`
:raises: exception.InvalidPoolAttributeValue :raises: exception.InvalidPoolAttributeValue
""" """
# build up the storage pool. # build up the storage pool.

View File

@@ -81,6 +81,7 @@ class BareMetalModelBuilder(base.BaseModelBuilder):
def __init__(self, osc): def __init__(self, osc):
self.osc = osc self.osc = osc
self.model = model_root.BaremetalModelRoot() self.model = model_root.BaremetalModelRoot()
# TODO(lpetrut): add MAAS support
self.ironic_helper = ironic_helper.IronicHelper(osc=self.osc) self.ironic_helper = ironic_helper.IronicHelper(osc=self.osc)
def add_ironic_node(self, node): def add_ironic_node(self, node):

View File

@@ -48,7 +48,7 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"type": "array", "type": "array",
"items": { "items": {
"anyOf": [ "anyOf": [
{"$ref": HOST_AGGREGATES + "id"}, {"$ref": HOST_AGGREGATES + "host_aggr_id"},
{"$ref": HOST_AGGREGATES + "name"}, {"$ref": HOST_AGGREGATES + "name"},
] ]
} }
@@ -98,7 +98,8 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"type": "array", "type": "array",
"items": { "items": {
"anyOf": [ "anyOf": [
{"$ref": HOST_AGGREGATES + "id"}, {"$ref":
HOST_AGGREGATES + "host_aggr_id"},
{"$ref": HOST_AGGREGATES + "name"}, {"$ref": HOST_AGGREGATES + "name"},
] ]
} }
@@ -129,7 +130,7 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"additionalProperties": False "additionalProperties": False
}, },
"host_aggregates": { "host_aggregates": {
"id": { "host_aggr_id": {
"properties": { "properties": {
"id": { "id": {
"oneOf": [ "oneOf": [

View File

@@ -157,7 +157,7 @@ class ModelRoot(nx.DiGraph, base.Model):
if node_list: if node_list:
return node_list[0] return node_list[0]
else: else:
raise exception.ComputeResourceNotFound raise exception.ComputeNodeNotFound(name=name)
except exception.ComputeResourceNotFound: except exception.ComputeResourceNotFound:
raise exception.ComputeNodeNotFound(name=name) raise exception.ComputeNodeNotFound(name=name)
@@ -631,7 +631,7 @@ class BaremetalModelRoot(nx.DiGraph, base.Model):
super(BaremetalModelRoot, self).remove_node(node.uuid) super(BaremetalModelRoot, self).remove_node(node.uuid)
except nx.NetworkXError as exc: except nx.NetworkXError as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.IronicNodeNotFound(name=node.uuid) raise exception.IronicNodeNotFound(uuid=node.uuid)
@lockutils.synchronized("baremetal_model") @lockutils.synchronized("baremetal_model")
def get_all_ironic_nodes(self): def get_all_ironic_nodes(self):
@@ -643,7 +643,7 @@ class BaremetalModelRoot(nx.DiGraph, base.Model):
try: try:
return self._get_by_uuid(uuid) return self._get_by_uuid(uuid)
except exception.BaremetalResourceNotFound: except exception.BaremetalResourceNotFound:
raise exception.IronicNodeNotFound(name=uuid) raise exception.IronicNodeNotFound(uuid=uuid)
def _get_by_uuid(self, uuid): def _get_by_uuid(self, uuid):
try: try:

View File

@@ -252,9 +252,6 @@ class BaseStrategy(loadable.Loadable, metaclass=abc.ABCMeta):
if not self.compute_model: if not self.compute_model:
raise exception.ClusterStateNotDefined() raise exception.ClusterStateNotDefined()
if self.compute_model.stale:
raise exception.ClusterStateStale()
LOG.debug(self.compute_model.to_string()) LOG.debug(self.compute_model.to_string())
def execute(self, audit=None): def execute(self, audit=None):

View File

@@ -18,8 +18,6 @@
# #
from oslo_log import log from oslo_log import log
import six
from watcher._i18n import _ from watcher._i18n import _
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.model import element from watcher.decision_engine.model import element
@@ -103,7 +101,7 @@ class HostMaintenance(base.HostMaintenanceBaseStrategy):
def get_instance_state_str(self, instance): def get_instance_state_str(self, instance):
"""Get instance state in string format""" """Get instance state in string format"""
if isinstance(instance.state, six.string_types): if isinstance(instance.state, str):
return instance.state return instance.state
elif isinstance(instance.state, element.InstanceState): elif isinstance(instance.state, element.InstanceState):
return instance.state.value return instance.state.value
@@ -116,7 +114,7 @@ class HostMaintenance(base.HostMaintenanceBaseStrategy):
def get_node_status_str(self, node): def get_node_status_str(self, node):
"""Get node status in string format""" """Get node status in string format"""
if isinstance(node.status, six.string_types): if isinstance(node.status, str):
return node.status return node.status
elif isinstance(node.status, element.ServiceState): elif isinstance(node.status, element.ServiceState):
return node.status.value return node.status.value

View File

@@ -23,6 +23,8 @@ from oslo_log import log
from watcher._i18n import _ from watcher._i18n import _
from watcher.common import exception from watcher.common import exception
from watcher.common.metal_helper import constants as metal_constants
from watcher.common.metal_helper import factory as metal_helper_factory
from watcher.decision_engine.strategy.strategies import base from watcher.decision_engine.strategy.strategies import base
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -81,7 +83,7 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
def __init__(self, config, osc=None): def __init__(self, config, osc=None):
super(SavingEnergy, self).__init__(config, osc) super(SavingEnergy, self).__init__(config, osc)
self._ironic_client = None self._metal_helper = None
self._nova_client = None self._nova_client = None
self.with_vms_node_pool = [] self.with_vms_node_pool = []
@@ -91,10 +93,10 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
self.min_free_hosts_num = 1 self.min_free_hosts_num = 1
@property @property
def ironic_client(self): def metal_helper(self):
if not self._ironic_client: if not self._metal_helper:
self._ironic_client = self.osc.ironic() self._metal_helper = metal_helper_factory.get_helper(self.osc)
return self._ironic_client return self._metal_helper
@property @property
def nova_client(self): def nova_client(self):
@@ -149,10 +151,10 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
:return: None :return: None
""" """
params = {'state': state, params = {'state': state,
'resource_name': node.hostname} 'resource_name': node.get_hypervisor_hostname()}
self.solution.add_action( self.solution.add_action(
action_type='change_node_power_state', action_type='change_node_power_state',
resource_id=node.uuid, resource_id=node.get_id(),
input_parameters=params) input_parameters=params)
def get_hosts_pool(self): def get_hosts_pool(self):
@@ -162,57 +164,61 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
""" """
node_list = self.ironic_client.node.list() node_list = self.metal_helper.list_compute_nodes()
for node in node_list: for node in node_list:
node_info = self.ironic_client.node.get(node.uuid) hypervisor_node = node.get_hypervisor_node().to_dict()
hypervisor_id = node_info.extra.get('compute_node_id', None)
if hypervisor_id is None:
LOG.warning(('Cannot find compute_node_id in extra '
'of ironic node %s'), node.uuid)
continue
hypervisor_node = self.nova_client.hypervisors.get(hypervisor_id)
if hypervisor_node is None:
LOG.warning(('Cannot find hypervisor %s'), hypervisor_id)
continue
node.hostname = hypervisor_node.hypervisor_hostname
hypervisor_node = hypervisor_node.to_dict()
compute_service = hypervisor_node.get('service', None) compute_service = hypervisor_node.get('service', None)
host_name = compute_service.get('host') host_name = compute_service.get('host')
LOG.debug("Found hypervisor: %s", hypervisor_node)
try: try:
self.compute_model.get_node_by_name(host_name) self.compute_model.get_node_by_name(host_name)
except exception.ComputeNodeNotFound: except exception.ComputeNodeNotFound:
LOG.info("The compute model does not contain the host: %s",
host_name)
continue continue
if not (hypervisor_node.get('state') == 'up'): if (node.hv_up_when_powered_off and
"""filter nodes that are not in 'up' state""" hypervisor_node.get('state') != 'up'):
# filter nodes that are not in 'up' state
LOG.info("Ignoring node that isn't in 'up' state: %s",
host_name)
continue continue
else: else:
if (hypervisor_node['running_vms'] == 0): if (hypervisor_node['running_vms'] == 0):
if (node_info.power_state == 'power on'): power_state = node.get_power_state()
if power_state == metal_constants.PowerState.ON:
self.free_poweron_node_pool.append(node) self.free_poweron_node_pool.append(node)
elif (node_info.power_state == 'power off'): elif power_state == metal_constants.PowerState.OFF:
self.free_poweroff_node_pool.append(node) self.free_poweroff_node_pool.append(node)
else:
LOG.info("Ignoring node %s, unknown state: %s",
node, power_state)
else: else:
self.with_vms_node_pool.append(node) self.with_vms_node_pool.append(node)
def save_energy(self): def save_energy(self):
need_poweron = max( need_poweron = int(max(
(len(self.with_vms_node_pool) * self.free_used_percent / 100), ( (len(self.with_vms_node_pool) * self.free_used_percent / 100), (
self.min_free_hosts_num)) self.min_free_hosts_num)))
len_poweron = len(self.free_poweron_node_pool) len_poweron = len(self.free_poweron_node_pool)
len_poweroff = len(self.free_poweroff_node_pool) len_poweroff = len(self.free_poweroff_node_pool)
LOG.debug("need_poweron: %s, len_poweron: %s, len_poweroff: %s",
need_poweron, len_poweron, len_poweroff)
if len_poweron > need_poweron: if len_poweron > need_poweron:
for node in random.sample(self.free_poweron_node_pool, for node in random.sample(self.free_poweron_node_pool,
(len_poweron - need_poweron)): (len_poweron - need_poweron)):
self.add_action_poweronoff_node(node, 'off') self.add_action_poweronoff_node(node,
LOG.debug("power off %s", node.uuid) metal_constants.PowerState.OFF)
LOG.info("power off %s", node.get_id())
elif len_poweron < need_poweron: elif len_poweron < need_poweron:
diff = need_poweron - len_poweron diff = need_poweron - len_poweron
for node in random.sample(self.free_poweroff_node_pool, for node in random.sample(self.free_poweroff_node_pool,
min(len_poweroff, diff)): min(len_poweroff, diff)):
self.add_action_poweronoff_node(node, 'on') self.add_action_poweronoff_node(node,
LOG.debug("power on %s", node.uuid) metal_constants.PowerState.ON)
LOG.info("power on %s", node.get_id())
def pre_execute(self): def pre_execute(self):
self._pre_execute() self._pre_execute()

View File

@@ -18,9 +18,13 @@
# limitations under the License. # limitations under the License.
# #
import collections
from oslo_log import log from oslo_log import log
import oslo_utils
from watcher._i18n import _ from watcher._i18n import _
from watcher.applier.actions import migration
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.model import element from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base from watcher.decision_engine.strategy.strategies import base
@@ -66,7 +70,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
AGGREGATE = 'mean' AGGREGATE = 'mean'
DATASOURCE_METRICS = ['instance_ram_allocated', 'instance_cpu_usage', DATASOURCE_METRICS = ['instance_ram_allocated', 'instance_cpu_usage',
'instance_ram_usage', 'instance_root_disk_size'] 'instance_ram_usage', 'instance_root_disk_size',
'host_cpu_usage', 'host_ram_usage']
MIGRATION = "migrate" MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state" CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
@@ -76,6 +81,11 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
self.number_of_migrations = 0 self.number_of_migrations = 0
self.number_of_released_nodes = 0 self.number_of_released_nodes = 0
self.datasource_instance_data_cache = dict() self.datasource_instance_data_cache = dict()
self.datasource_node_data_cache = dict()
# Host metric adjustments that take into account planned
# migrations.
self.host_metric_delta = collections.defaultdict(
lambda: collections.defaultdict(int))
@classmethod @classmethod
def get_name(cls): def get_name(cls):
@@ -196,12 +206,12 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
:return: None :return: None
""" """
instance_state_str = self.get_instance_state_str(instance) instance_state_str = self.get_instance_state_str(instance)
if instance_state_str not in (element.InstanceState.ACTIVE.value, if instance_state_str in (element.InstanceState.ACTIVE.value,
element.InstanceState.PAUSED.value): element.InstanceState.PAUSED.value):
# Watcher currently only supports live VM migration and block live migration_type = migration.Migrate.LIVE_MIGRATION
# VM migration which both requires migrated VM to be active. elif instance_state_str == element.InstanceState.STOPPED.value:
# When supported, the cold migration may be used as a fallback migration_type = migration.Migrate.COLD_MIGRATION
# migration mechanism to move non active VMs. else:
LOG.error( LOG.error(
'Cannot live migrate: instance_uuid=%(instance_uuid)s, ' 'Cannot live migrate: instance_uuid=%(instance_uuid)s, '
'state=%(instance_state)s.', dict( 'state=%(instance_state)s.', dict(
@@ -209,8 +219,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
instance_state=instance_state_str)) instance_state=instance_state_str))
return return
migration_type = 'live'
# Here will makes repeated actions to enable the same compute node, # Here will makes repeated actions to enable the same compute node,
# when migrating VMs to the destination node which is disabled. # when migrating VMs to the destination node which is disabled.
# Whether should we remove the same actions in the solution??? # Whether should we remove the same actions in the solution???
@@ -228,6 +236,18 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
destination_node) destination_node)
self.number_of_migrations += 1 self.number_of_migrations += 1
instance_util = self.get_instance_utilization(instance)
self.host_metric_delta[source_node.hostname]['cpu'] -= (
instance_util['cpu'])
# We'll deduce the vm allocated memory.
self.host_metric_delta[source_node.hostname]['ram'] -= (
instance.memory)
self.host_metric_delta[destination_node.hostname]['cpu'] += (
instance_util['cpu'])
self.host_metric_delta[destination_node.hostname]['ram'] += (
instance.memory)
def disable_unused_nodes(self): def disable_unused_nodes(self):
"""Generate actions for disabling unused nodes. """Generate actions for disabling unused nodes.
@@ -290,6 +310,21 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
disk=instance_disk_util) disk=instance_disk_util)
return self.datasource_instance_data_cache.get(instance.uuid) return self.datasource_instance_data_cache.get(instance.uuid)
def _get_node_total_utilization(self, node):
if node.hostname in self.datasource_node_data_cache:
return self.datasource_node_data_cache[node.hostname]
cpu = self.datasource_backend.get_host_cpu_usage(
node, self.period, self.AGGREGATE,
self.granularity)
ram = self.datasource_backend.get_host_ram_usage(
node, self.period, self.AGGREGATE,
self.granularity)
self.datasource_node_data_cache[node.hostname] = dict(
cpu=cpu, ram=ram)
return self.datasource_node_data_cache[node.hostname]
def get_node_utilization(self, node): def get_node_utilization(self, node):
"""Collect cpu, ram and disk utilization statistics of a node. """Collect cpu, ram and disk utilization statistics of a node.
@@ -307,8 +342,36 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
node_cpu_util += instance_util['cpu'] node_cpu_util += instance_util['cpu']
node_ram_util += instance_util['ram'] node_ram_util += instance_util['ram']
node_disk_util += instance_util['disk'] node_disk_util += instance_util['disk']
LOG.debug("instance utilization: %s %s",
instance, instance_util)
return dict(cpu=node_cpu_util, ram=node_ram_util, total_node_util = self._get_node_total_utilization(node)
total_node_cpu_util = total_node_util['cpu'] or 0
if total_node_cpu_util:
total_node_cpu_util = total_node_cpu_util * node.vcpus / 100
# account for planned migrations
total_node_cpu_util += self.host_metric_delta[node.hostname]['cpu']
total_node_ram_util = total_node_util['ram'] or 0
if total_node_ram_util:
total_node_ram_util /= oslo_utils.units.Ki
total_node_ram_util += self.host_metric_delta[node.hostname]['ram']
LOG.debug(
"node utilization: %s. "
"total instance cpu: %s, "
"total instance ram: %s, "
"total instance disk: %s, "
"total host cpu: %s, "
"total host ram: %s, "
"node delta usage: %s.",
node,
node_cpu_util, node_ram_util, node_disk_util,
total_node_cpu_util, total_node_ram_util,
self.host_metric_delta[node.hostname])
return dict(cpu=max(node_cpu_util, total_node_cpu_util),
ram=max(node_ram_util, total_node_ram_util),
disk=node_disk_util) disk=node_disk_util)
def get_node_capacity(self, node): def get_node_capacity(self, node):
@@ -388,8 +451,15 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
instance_utilization = self.get_instance_utilization(instance) instance_utilization = self.get_instance_utilization(instance)
metrics = ['cpu', 'ram', 'disk'] metrics = ['cpu', 'ram', 'disk']
for m in metrics: for m in metrics:
if (instance_utilization[m] + node_utilization[m] > fits = (instance_utilization[m] + node_utilization[m] <=
node_capacity[m] * cc[m]): node_capacity[m] * cc[m])
LOG.debug(
"Instance fits: %s, metric: %s, instance: %s, "
"node: %s, instance utilization: %s, "
"node utilization: %s, node capacity: %s, cc: %s",
fits, m, instance, node, instance_utilization[m],
node_utilization[m], node_capacity[m], cc[m])
if not fits:
return False return False
return True return True
@@ -424,6 +494,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
for a in actions: for a in actions:
self.solution.actions.remove(a) self.solution.actions.remove(a)
self.number_of_migrations -= 1 self.number_of_migrations -= 1
LOG.info("Optimized migrations: %s. "
"Source: %s, destination: %s", actions,
src_name, dst_name)
src_node = self.compute_model.get_node_by_name(src_name) src_node = self.compute_model.get_node_by_name(src_name)
dst_node = self.compute_model.get_node_by_name(dst_name) dst_node = self.compute_model.get_node_by_name(dst_name)
instance = self.compute_model.get_instance_by_uuid( instance = self.compute_model.get_instance_by_uuid(
@@ -460,6 +533,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
key=lambda x: self.get_instance_utilization( key=lambda x: self.get_instance_utilization(
x)['cpu'] x)['cpu']
): ):
LOG.info("Node %s overloaded, attempting to reduce load.",
node)
# skip exclude instance when migrating # skip exclude instance when migrating
if instance.watcher_exclude: if instance.watcher_exclude:
LOG.debug("Instance is excluded by scope, " LOG.debug("Instance is excluded by scope, "
@@ -468,11 +543,19 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
for destination_node in reversed(sorted_nodes): for destination_node in reversed(sorted_nodes):
if self.instance_fits( if self.instance_fits(
instance, destination_node, cc): instance, destination_node, cc):
LOG.info("Offload: found fitting "
"destination (%s) for instance: %s. "
"Planning migration.",
destination_node, instance.uuid)
self.add_migration(instance, node, self.add_migration(instance, node,
destination_node) destination_node)
break break
if not self.is_overloaded(node, cc): if not self.is_overloaded(node, cc):
LOG.info("Node %s no longer overloaded.", node)
break break
else:
LOG.info("Node still overloaded (%s), "
"continuing offload phase.", node)
def consolidation_phase(self, cc): def consolidation_phase(self, cc):
"""Perform consolidation phase. """Perform consolidation phase.
@@ -508,6 +591,10 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
break break
if self.instance_fits( if self.instance_fits(
instance, destination_node, cc): instance, destination_node, cc):
LOG.info("Consolidation: found fitting "
"destination (%s) for instance: %s. "
"Planning migration.",
destination_node, instance.uuid)
self.add_migration(instance, node, self.add_migration(instance, node,
destination_node) destination_node)
break break

View File

@@ -295,7 +295,7 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
self.threshold) self.threshold)
return self.solution return self.solution
# choose the server with largest cpu_util # choose the server with largest cpu usage
source_nodes = sorted(source_nodes, source_nodes = sorted(source_nodes,
reverse=True, reverse=True,
key=lambda x: (x[self._meter])) key=lambda x: (x[self._meter]))

View File

@@ -16,7 +16,7 @@ from dateutil.parser import parse
from oslo_log import log from oslo_log import log
from cinderclient.v2.volumes import Volume from cinderclient.v3.volumes import Volume
from novaclient.v2.servers import Server from novaclient.v2.servers import Server
from watcher._i18n import _ from watcher._i18n import _
from watcher.common import cinder_helper from watcher.common import cinder_helper
@@ -350,7 +350,7 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
def is_in_use(self, volume): def is_in_use(self, volume):
return getattr(volume, 'status') == IN_USE return getattr(volume, 'status') == IN_USE
def instances_no_attached(instances): def instances_no_attached(self, instances):
return [i for i in instances return [i for i in instances
if not getattr(i, "os-extended-volumes:volumes_attached")] if not getattr(i, "os-extended-volumes:volumes_attached")]

View File

@@ -128,22 +128,20 @@ def check_assert_called_once_with(logical_line, filename):
@flake8ext @flake8ext
def check_python3_xrange(logical_line): def check_python3_xrange(logical_line):
if re.search(r"\bxrange\s*\(", logical_line): if re.search(r"\bxrange\s*\(", logical_line):
yield(0, "N325: Do not use xrange. Use range, or six.moves.range for " yield(0, "N325: Do not use xrange. Use range for large loops.")
"large loops.")
@flake8ext @flake8ext
def check_no_basestring(logical_line): def check_no_basestring(logical_line):
if re.search(r"\bbasestring\b", logical_line): if re.search(r"\bbasestring\b", logical_line):
msg = ("N326: basestring is not Python3-compatible, use " msg = ("N326: basestring is not Python3-compatible, use str instead.")
"six.string_types instead.")
yield(0, msg) yield(0, msg)
@flake8ext @flake8ext
def check_python3_no_iteritems(logical_line): def check_python3_no_iteritems(logical_line):
if re.search(r".*\.iteritems\(\)", logical_line): if re.search(r".*\.iteritems\(\)", logical_line):
msg = ("N327: Use six.iteritems() instead of dict.iteritems().") msg = ("N327: Use dict.items() instead of dict.iteritems().")
yield(0, msg) yield(0, msg)

View File

@@ -1,14 +1,16 @@
# Andi Chandler <andi@gowling.com>, 2017. #zanata # Andi Chandler <andi@gowling.com>, 2017. #zanata
# Andi Chandler <andi@gowling.com>, 2018. #zanata # Andi Chandler <andi@gowling.com>, 2018. #zanata
# Andi Chandler <andi@gowling.com>, 2020. #zanata
# Andi Chandler <andi@gowling.com>, 2022. #zanata
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: watcher VERSION\n" "Project-Id-Version: watcher VERSION\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2020-04-26 02:09+0000\n" "POT-Creation-Date: 2022-08-29 03:03+0000\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2018-11-07 06:14+0000\n" "PO-Revision-Date: 2022-05-31 08:38+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n" "Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n" "Language-Team: English (United Kingdom)\n"
"Language: en_GB\n" "Language: en_GB\n"
@@ -187,10 +189,18 @@ msgstr "Audit Templates"
msgid "Audit parameter %(parameter)s are not allowed" msgid "Audit parameter %(parameter)s are not allowed"
msgstr "Audit parameter %(parameter)s are not allowed" msgstr "Audit parameter %(parameter)s are not allowed"
#, python-format
msgid "Audit state %(state)s is disallowed."
msgstr "Audit state %(state)s is disallowed."
#, python-format #, python-format
msgid "Audit type %(audit_type)s could not be found" msgid "Audit type %(audit_type)s could not be found"
msgstr "Audit type %(audit_type)s could not be found" msgstr "Audit type %(audit_type)s could not be found"
#, python-format
msgid "Audit type %(audit_type)s is disallowed."
msgstr "Audit type %(audit_type)s is disallowed."
#, python-format #, python-format
msgid "AuditTemplate %(audit_template)s could not be found" msgid "AuditTemplate %(audit_template)s could not be found"
msgstr "AuditTemplate %(audit_template)s could not be found" msgstr "AuditTemplate %(audit_template)s could not be found"
@@ -243,6 +253,9 @@ msgstr "Cannot overwrite UUID for an existing efficacy indicator."
msgid "Cannot remove 'goal' attribute from an audit template" msgid "Cannot remove 'goal' attribute from an audit template"
msgstr "Cannot remove 'goal' attribute from an audit template" msgstr "Cannot remove 'goal' attribute from an audit template"
msgid "Ceilometer helper does not support statistic series method"
msgstr "Ceilometer helper does not support statistic series method"
msgid "Cluster Maintaining" msgid "Cluster Maintaining"
msgstr "Cluster Maintaining" msgstr "Cluster Maintaining"
@@ -369,6 +382,9 @@ msgstr "Goal %(goal)s is invalid"
msgid "Goals" msgid "Goals"
msgstr "Goals" msgstr "Goals"
msgid "Grafana helper does not support statistic series method"
msgstr "Grafana helper does not support statistic series method"
msgid "Hardware Maintenance" msgid "Hardware Maintenance"
msgstr "Hardware Maintenance" msgstr "Hardware Maintenance"
@@ -434,10 +450,17 @@ msgstr "Limit should be positive"
msgid "Maximum time since last check-in for up service." msgid "Maximum time since last check-in for up service."
msgstr "Maximum time since last check-in for up service." msgstr "Maximum time since last check-in for up service."
#, python-format
msgid "Metric: %(metric)s not available"
msgstr "Metric: %(metric)s not available"
#, python-format #, python-format
msgid "Migration of type '%(migration_type)s' is not supported." msgid "Migration of type '%(migration_type)s' is not supported."
msgstr "Migration of type '%(migration_type)s' is not supported." msgstr "Migration of type '%(migration_type)s' is not supported."
msgid "Minimum Nova API Version"
msgstr "Minimum Nova API Version"
msgid "" msgid ""
"Name of this node. This can be an opaque identifier. It is not necessarily a " "Name of this node. This can be an opaque identifier. It is not necessarily a "
"hostname, FQDN, or IP address. However, the node name must be valid within " "hostname, FQDN, or IP address. However, the node name must be valid within "
@@ -451,10 +474,16 @@ msgstr ""
msgid "No %(metric)s metric for %(host)s found." msgid "No %(metric)s metric for %(host)s found."
msgstr "No %(metric)s metric for %(host)s found." msgstr "No %(metric)s metric for %(host)s found."
msgid "No datasources available"
msgstr "No datasources available"
#, python-format #, python-format
msgid "No strategy could be found to achieve the '%(goal)s' goal." msgid "No strategy could be found to achieve the '%(goal)s' goal."
msgstr "No strategy could be found to achieve the '%(goal)s' goal." msgstr "No strategy could be found to achieve the '%(goal)s' goal."
msgid "Node Resource Consolidation strategy"
msgstr "Node Resource Consolidation strategy"
msgid "Noisy Neighbor" msgid "Noisy Neighbor"
msgstr "Noisy Neighbour" msgstr "Noisy Neighbour"
@@ -479,6 +508,9 @@ msgstr ""
msgid "Plugins" msgid "Plugins"
msgstr "Plugins" msgstr "Plugins"
msgid "Policy File JSON to YAML Migration"
msgstr "Policy File JSON to YAML Migration"
#, python-format #, python-format
msgid "Policy doesn't allow %(action)s to be performed." msgid "Policy doesn't allow %(action)s to be performed."
msgstr "Policy doesn't allow %(action)s to be performed." msgstr "Policy doesn't allow %(action)s to be performed."
@@ -606,6 +638,10 @@ msgstr "Strategy %(strategy)s could not be found"
msgid "Strategy %(strategy)s is invalid" msgid "Strategy %(strategy)s is invalid"
msgstr "Strategy %(strategy)s is invalid" msgstr "Strategy %(strategy)s is invalid"
#, python-format
msgid "The %(data_model_type)s data model could not be found"
msgstr "The %(data_model_type)s data model could not be found"
#, python-format #, python-format
msgid "The %(name)s %(id)s could not be found" msgid "The %(name)s %(id)s could not be found"
msgstr "The %(name)s %(id)s could not be found" msgstr "The %(name)s %(id)s could not be found"
@@ -675,6 +711,13 @@ msgstr "The instance '%(name)s' could not be found"
msgid "The ironic node %(uuid)s could not be found" msgid "The ironic node %(uuid)s could not be found"
msgstr "The Ironic node %(uuid)s could not be found" msgstr "The Ironic node %(uuid)s could not be found"
#, python-format
msgid "The mapped compute node for instance '%(uuid)s' could not be found."
msgstr "The mapped compute node for instance '%(uuid)s' could not be found."
msgid "The node status is not defined"
msgstr "The node status is not defined"
msgid "The number of VM migrations to be performed." msgid "The number of VM migrations to be performed."
msgstr "The number of VM migrations to be performed." msgstr "The number of VM migrations to be performed."
@@ -738,6 +781,10 @@ msgstr "The total number of audited instances in strategy."
msgid "The total number of enabled compute nodes." msgid "The total number of enabled compute nodes."
msgstr "The total number of enabled compute nodes." msgstr "The total number of enabled compute nodes."
#, python-format
msgid "The value %(value)s for parameter %(parameter)s is invalid"
msgstr "The value %(value)s for parameter %(parameter)s is invalid"
msgid "The value of original standard deviation." msgid "The value of original standard deviation."
msgstr "The value of original standard deviation." msgstr "The value of original standard deviation."

View File

@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from http import HTTPStatus
from watcher.tests.api import base from watcher.tests.api import base
@@ -25,6 +27,6 @@ class TestBase(base.FunctionalTest):
response = self.get_json('/bad/path', response = self.get_json('/bad/path',
expect_errors=True, expect_errors=True,
headers={"Accept": "application/json"}) headers={"Accept": "application/json"})
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual("application/json", response.content_type) self.assertEqual("application/json", response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])

View File

@@ -14,14 +14,11 @@
"""Tests for the Pecan API hooks.""" """Tests for the Pecan API hooks."""
from unittest import mock from http import client as http_client
from oslo_config import cfg from oslo_config import cfg
import oslo_messaging as messaging import oslo_messaging as messaging
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import six from unittest import mock
from six.moves import http_client
from watcher.api.controllers import root from watcher.api.controllers import root
from watcher.api import hooks from watcher.api import hooks
from watcher.common import context from watcher.common import context
@@ -144,7 +141,7 @@ class TestNoExceptionTracebackHook(base.FunctionalTest):
# we don't care about this garbage. # we don't care about this garbage.
expected_msg = ("Remote error: %s %s" expected_msg = ("Remote error: %s %s"
% (test_exc_type, self.MSG_WITHOUT_TRACE) + % (test_exc_type, self.MSG_WITHOUT_TRACE) +
("\n[u'" if six.PY2 else "\n['")) "\n['")
actual_msg = jsonutils.loads( actual_msg = jsonutils.loads(
response.json['error_message'])['faultstring'] response.json['error_message'])['faultstring']
self.assertEqual(expected_msg, actual_msg) self.assertEqual(expected_msg, actual_msg)

View File

@@ -14,6 +14,7 @@ import datetime
import itertools import itertools
from unittest import mock from unittest import mock
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from wsme import types as wtypes from wsme import types as wtypes
@@ -102,7 +103,7 @@ class TestListAction(api_base.FunctionalTest):
response = self.get_json('/actions/%s' % action['uuid'], response = self.get_json('/actions/%s' % action['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
action = obj_utils.create_test_action(self.context, parents=None) action = obj_utils.create_test_action(self.context, parents=None)
@@ -125,7 +126,7 @@ class TestListAction(api_base.FunctionalTest):
action = obj_utils.create_test_action(self.context, parents=None) action = obj_utils.create_test_action(self.context, parents=None)
response = self.get_json('/actions/%s/detail' % action['uuid'], response = self.get_json('/actions/%s/detail' % action['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
action_list = [] action_list = []
@@ -266,7 +267,7 @@ class TestListAction(api_base.FunctionalTest):
url = '/actions?action_plan_uuid=%s&audit_uuid=%s' % ( url = '/actions?action_plan_uuid=%s&audit_uuid=%s' % (
action_plan.uuid, self.audit.uuid) action_plan.uuid, self.audit.uuid)
response = self.get_json(url, expect_errors=True) response = self.get_json(url, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
def test_many_with_sort_key_uuid(self): def test_many_with_sort_key_uuid(self):
action_plan = obj_utils.create_test_action_plan( action_plan = obj_utils.create_test_action_plan(
@@ -327,7 +328,7 @@ class TestListAction(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/actions?sort_key=%s' % 'bad_name', '/actions?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
def test_many_with_soft_deleted_action_plan_uuid(self): def test_many_with_soft_deleted_action_plan_uuid(self):
action_plan1 = obj_utils.create_test_action_plan( action_plan1 = obj_utils.create_test_action_plan(
@@ -488,7 +489,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/state', 'value': new_state, 'op': 'replace'}], [{'path': '/state', 'value': new_state, 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -516,7 +517,7 @@ class TestDelete(api_base.FunctionalTest):
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
response = self.delete('/actions/%s' % self.action.uuid, response = self.delete('/actions/%s' % self.action.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -536,7 +537,7 @@ class TestActionPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:defaut"}) rule: "rule:defaut"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -14,6 +14,7 @@ import datetime
import itertools import itertools
from unittest import mock from unittest import mock
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
@@ -87,7 +88,7 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json('/action_plans/%s' % action_plan['uuid'], response = self.get_json('/action_plans/%s' % action_plan['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
action_plan = obj_utils.create_test_action_plan(self.context) action_plan = obj_utils.create_test_action_plan(self.context)
@@ -113,7 +114,7 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/action_plan/%s/detail' % action_plan['uuid'], '/action_plan/%s/detail' % action_plan['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
action_plan_list = [] action_plan_list = []
@@ -260,7 +261,7 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/action_plans?sort_key=%s' % 'bad_name', '/action_plans?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
def test_links(self): def test_links(self):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
@@ -317,7 +318,7 @@ class TestDelete(api_base.FunctionalTest):
def test_delete_action_plan_without_action(self): def test_delete_action_plan_without_action(self):
response = self.delete('/action_plans/%s' % self.action_plan.uuid, response = self.delete('/action_plans/%s' % self.action_plan.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
self.action_plan.state = objects.action_plan.State.SUCCEEDED self.action_plan.state = objects.action_plan.State.SUCCEEDED
@@ -325,7 +326,7 @@ class TestDelete(api_base.FunctionalTest):
self.delete('/action_plans/%s' % self.action_plan.uuid) self.delete('/action_plans/%s' % self.action_plan.uuid)
response = self.get_json('/action_plans/%s' % self.action_plan.uuid, response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -345,20 +346,20 @@ class TestDelete(api_base.FunctionalTest):
expect_errors=True) expect_errors=True)
# The action plan does not exist anymore # The action plan does not exist anymore
self.assertEqual(404, ap_response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, ap_response.status_int)
self.assertEqual('application/json', ap_response.content_type) self.assertEqual('application/json', ap_response.content_type)
self.assertTrue(ap_response.json['error_message']) self.assertTrue(ap_response.json['error_message'])
# Nor does the action # Nor does the action
self.assertEqual(0, len(acts_response['actions'])) self.assertEqual(0, len(acts_response['actions']))
self.assertEqual(404, act_response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, act_response.status_int)
self.assertEqual('application/json', act_response.content_type) self.assertEqual('application/json', act_response.content_type)
self.assertTrue(act_response.json['error_message']) self.assertTrue(act_response.json['error_message'])
def test_delete_action_plan_not_found(self): def test_delete_action_plan_not_found(self):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
response = self.delete('/action_plans/%s' % uuid, expect_errors=True) response = self.delete('/action_plans/%s' % uuid, expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -388,7 +389,7 @@ class TestStart(api_base.FunctionalTest):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
response = self.post('/v1/action_plans/%s/%s' % response = self.post('/v1/action_plans/%s/%s' %
(uuid, 'start'), expect_errors=True) (uuid, 'start'), expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -401,11 +402,11 @@ class TestStart(api_base.FunctionalTest):
response = self.post('/v1/action_plans/%s/%s/' response = self.post('/v1/action_plans/%s/%s/'
% (self.action_plan.uuid, 'start'), % (self.action_plan.uuid, 'start'),
expect_errors=True) expect_errors=True)
self.assertEqual(200, response.status_int) self.assertEqual(HTTPStatus.OK, response.status_int)
act_response = self.get_json( act_response = self.get_json(
'/actions/%s' % action.uuid, '/actions/%s' % action.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(200, act_response.status_int) self.assertEqual(HTTPStatus.OK, act_response.status_int)
self.assertEqual('PENDING', act_response.json['state']) self.assertEqual('PENDING', act_response.json['state'])
self.assertEqual('application/json', act_response.content_type) self.assertEqual('application/json', act_response.content_type)
@@ -445,7 +446,7 @@ class TestPatch(api_base.FunctionalTest):
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_replace_non_existent_action_plan_denied(self): def test_replace_non_existent_action_plan_denied(self):
@@ -455,7 +456,7 @@ class TestPatch(api_base.FunctionalTest):
'value': objects.action_plan.State.PENDING, 'value': objects.action_plan.State.PENDING,
'op': 'replace'}], 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -465,7 +466,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/foo', 'value': 'bar', 'op': 'add'}], [{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_remove_denied(self): def test_remove_denied(self):
@@ -480,7 +481,7 @@ class TestPatch(api_base.FunctionalTest):
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_remove_uuid_denied(self): def test_remove_uuid_denied(self):
@@ -488,7 +489,7 @@ class TestPatch(api_base.FunctionalTest):
'/action_plans/%s' % self.action_plan.uuid, '/action_plans/%s' % self.action_plan.uuid,
[{'path': '/uuid', 'op': 'remove'}], [{'path': '/uuid', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -497,7 +498,7 @@ class TestPatch(api_base.FunctionalTest):
'/action_plans/%s' % self.action_plan.uuid, '/action_plans/%s' % self.action_plan.uuid,
[{'path': '/non-existent', 'op': 'remove'}], [{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_code) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -512,7 +513,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/state', 'value': new_state, [{'path': '/state', 'value': new_state,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
applier_mock.assert_called_once_with(mock.ANY, applier_mock.assert_called_once_with(mock.ANY,
self.action_plan.uuid) self.action_plan.uuid)
@@ -579,7 +580,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest):
self.assertNotEqual(self.new_state, initial_ap['state']) self.assertNotEqual(self.new_state, initial_ap['state'])
self.assertEqual(self.original_state, updated_ap['state']) self.assertEqual(self.original_state, updated_ap['state'])
self.assertEqual(400, response.status_code) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -618,7 +619,7 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
self.assertNotEqual(self.new_state, initial_ap['state']) self.assertNotEqual(self.new_state, initial_ap['state'])
self.assertEqual(self.new_state, updated_ap['state']) self.assertEqual(self.new_state, updated_ap['state'])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
class TestActionPlanPolicyEnforcement(api_base.FunctionalTest): class TestActionPlanPolicyEnforcement(api_base.FunctionalTest):
@@ -635,7 +636,7 @@ class TestActionPlanPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:defaut"}) rule: "rule:defaut"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -15,6 +15,7 @@ import itertools
from unittest import mock from unittest import mock
from urllib import parse as urlparse from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import timeutils from oslo_utils import timeutils
@@ -126,7 +127,7 @@ class TestListAuditTemplate(FunctionalTestWithSetup):
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % audit_template['uuid'], '/audit_templates/%s' % audit_template['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
audit_template = obj_utils.create_test_audit_template(self.context) audit_template = obj_utils.create_test_audit_template(self.context)
@@ -152,7 +153,7 @@ class TestListAuditTemplate(FunctionalTestWithSetup):
response = self.get_json( response = self.get_json(
'/audit_templates/%s/detail' % audit_template['uuid'], '/audit_templates/%s/detail' % audit_template['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
audit_template_list = [] audit_template_list = []
@@ -336,7 +337,7 @@ class TestListAuditTemplate(FunctionalTestWithSetup):
response = self.get_json( response = self.get_json(
'/audit_templates?sort_key=%s' % 'goal_bad_name', '/audit_templates?sort_key=%s' % 'goal_bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
class TestPatch(FunctionalTestWithSetup): class TestPatch(FunctionalTestWithSetup):
@@ -362,7 +363,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': new_goal_uuid, [{'path': '/goal', 'value': new_goal_uuid,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
@@ -386,7 +387,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': new_goal_uuid, [{'path': '/goal', 'value': new_goal_uuid,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.name) '/audit_templates/%s' % self.audit_template.name)
@@ -401,7 +402,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': self.fake_goal1.uuid, [{'path': '/goal', 'value': self.fake_goal1.uuid,
'op': 'replace'}], 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -416,7 +417,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': utils.generate_uuid(), [{'path': '/goal', 'value': utils.generate_uuid(),
'op': 'replace'}], 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_add_goal_uuid(self): def test_add_goal_uuid(self):
@@ -426,7 +427,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': self.fake_goal2.uuid, 'value': self.fake_goal2.uuid,
'op': 'add'}]) 'op': 'add'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int) self.assertEqual(HTTPStatus.OK, response.status_int)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
@@ -439,7 +440,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': self.fake_strategy1.uuid, 'value': self.fake_strategy1.uuid,
'op': 'add'}]) 'op': 'add'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int) self.assertEqual(HTTPStatus.OK, response.status_int)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
@@ -452,7 +453,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': self.fake_strategy2['uuid'], 'value': self.fake_strategy2['uuid'],
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int) self.assertEqual(HTTPStatus.OK, response.status_int)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
@@ -466,7 +467,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': utils.generate_uuid(), # Does not exist 'value': utils.generate_uuid(), # Does not exist
'op': 'replace'}], expect_errors=True) 'op': 'replace'}], expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_add_non_existent_property(self): def test_add_non_existent_property(self):
@@ -475,7 +476,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/foo', 'value': 'bar', 'op': 'add'}], [{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_remove_strategy(self): def test_remove_strategy(self):
@@ -492,7 +493,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/strategy', 'op': 'remove'}]) [{'path': '/strategy', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
def test_remove_goal(self): def test_remove_goal(self):
response = self.get_json( response = self.get_json(
@@ -503,7 +504,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'op': 'remove'}], [{'path': '/goal', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(403, response.status_code) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_code)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -512,7 +513,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/uuid', 'op': 'remove'}], [{'path': '/uuid', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -521,7 +522,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/non-existent', 'op': 'remove'}], [{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_code) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -538,7 +539,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict) response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
# Check location header # Check location header
self.assertIsNotNone(response.location) self.assertIsNotNone(response.location)
expected_location = \ expected_location = \
@@ -565,7 +566,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict) response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
# Check location header # Check location header
self.assertIsNotNone(response.location) self.assertIsNotNone(response.location)
expected_location = \ expected_location = \
@@ -613,7 +614,8 @@ class TestPost(FunctionalTestWithSetup):
strategy=self.fake_strategy1.uuid, scope=scope) strategy=self.fake_strategy1.uuid, scope=scope)
response = self.post_json('/audit_templates', response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True) audit_template_dict, expect_errors=True)
self.assertEqual(500, response.status_int) self.assertEqual(HTTPStatus.INTERNAL_SERVER_ERROR,
response.status_int)
def test_create_audit_template_does_autogenerate_id(self): def test_create_audit_template_does_autogenerate_id(self):
audit_template_dict = post_get_test_audit_template( audit_template_dict = post_get_test_audit_template(
@@ -635,7 +637,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict) response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertTrue(utils.is_uuid_like(response.json['uuid'])) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
def test_create_audit_template_with_invalid_goal(self): def test_create_audit_template_with_invalid_goal(self):
@@ -648,7 +650,7 @@ class TestPost(FunctionalTestWithSetup):
goal_uuid=utils.generate_uuid()) goal_uuid=utils.generate_uuid())
response = self.post_json('/audit_templates', response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True) audit_template_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_create_audit_template_with_invalid_strategy(self): def test_create_audit_template_with_invalid_strategy(self):
@@ -662,7 +664,7 @@ class TestPost(FunctionalTestWithSetup):
strategy_uuid=utils.generate_uuid()) strategy_uuid=utils.generate_uuid())
response = self.post_json('/audit_templates', response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True) audit_template_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_create_audit_template_with_unrelated_strategy(self): def test_create_audit_template_with_unrelated_strategy(self):
@@ -676,7 +678,7 @@ class TestPost(FunctionalTestWithSetup):
strategy=self.fake_strategy2['uuid']) strategy=self.fake_strategy2['uuid'])
response = self.post_json('/audit_templates', response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True) audit_template_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_create_audit_template_with_uuid(self): def test_create_audit_template_with_uuid(self):
@@ -689,7 +691,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict, response = self.post_json('/audit_templates', audit_template_dict,
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_create_audit_template_with_old_scope(self): def test_create_audit_template_with_old_scope(self):
@@ -710,7 +712,7 @@ class TestPost(FunctionalTestWithSetup):
strategy=self.fake_strategy1.uuid, scope=scope) strategy=self.fake_strategy1.uuid, scope=scope)
response = self.post_json('/audit_templates', response = self.post_json('/audit_templates',
audit_template_dict) audit_template_dict)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
class TestDelete(api_base.FunctionalTest): class TestDelete(api_base.FunctionalTest):
@@ -730,7 +732,7 @@ class TestDelete(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
urlparse.quote('/audit_templates/%s' % self.audit_template.uuid), urlparse.quote('/audit_templates/%s' % self.audit_template.uuid),
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -753,7 +755,7 @@ class TestDelete(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
urlparse.quote('/audit_templates/%s' % self.audit_template.name), urlparse.quote('/audit_templates/%s' % self.audit_template.name),
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -771,7 +773,7 @@ class TestDelete(api_base.FunctionalTest):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
response = self.delete( response = self.delete(
'/audit_templates/%s' % uuid, expect_errors=True) '/audit_templates/%s' % uuid, expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -784,7 +786,7 @@ class TestAuditTemplatePolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:defaut"}) rule: "rule:defaut"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -16,6 +16,7 @@ import itertools
from unittest import mock from unittest import mock
from urllib import parse as urlparse from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import timeutils from oslo_utils import timeutils
@@ -130,7 +131,7 @@ class TestListAudit(api_base.FunctionalTest):
response = self.get_json('/audits/%s' % audit['uuid'], response = self.get_json('/audits/%s' % audit['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
audit = obj_utils.create_test_audit(self.context) audit = obj_utils.create_test_audit(self.context)
@@ -153,7 +154,7 @@ class TestListAudit(api_base.FunctionalTest):
audit = obj_utils.create_test_audit(self.context) audit = obj_utils.create_test_audit(self.context)
response = self.get_json('/audits/%s/detail' % audit['uuid'], response = self.get_json('/audits/%s/detail' % audit['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
audit_list = [] audit_list = []
@@ -225,7 +226,7 @@ class TestListAudit(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/audits?sort_key=%s' % 'bad_name', '/audits?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
def test_links(self): def test_links(self):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
@@ -295,7 +296,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/state', 'value': new_state, [{'path': '/state', 'value': new_state,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid) response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(new_state, response['state']) self.assertEqual(new_state, response['state'])
@@ -308,7 +309,7 @@ class TestPatch(api_base.FunctionalTest):
'/audits/%s' % utils.generate_uuid(), '/audits/%s' % utils.generate_uuid(),
[{'path': '/state', 'value': objects.audit.State.SUCCEEDED, [{'path': '/state', 'value': objects.audit.State.SUCCEEDED,
'op': 'replace'}], expect_errors=True) 'op': 'replace'}], expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -318,7 +319,7 @@ class TestPatch(api_base.FunctionalTest):
'/audits/%s' % self.audit.uuid, '/audits/%s' % self.audit.uuid,
[{'path': '/state', 'value': new_state, 'op': 'add'}]) [{'path': '/state', 'value': new_state, 'op': 'add'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int) self.assertEqual(HTTPStatus.OK, response.status_int)
response = self.get_json('/audits/%s' % self.audit.uuid) response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(new_state, response['state']) self.assertEqual(new_state, response['state'])
@@ -329,7 +330,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/foo', 'value': 'bar', 'op': 'add'}], [{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_remove_ok(self): def test_remove_ok(self):
@@ -339,7 +340,7 @@ class TestPatch(api_base.FunctionalTest):
response = self.patch_json('/audits/%s' % self.audit.uuid, response = self.patch_json('/audits/%s' % self.audit.uuid,
[{'path': '/interval', 'op': 'remove'}]) [{'path': '/interval', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid) response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertIsNone(response['interval']) self.assertIsNone(response['interval'])
@@ -348,7 +349,7 @@ class TestPatch(api_base.FunctionalTest):
response = self.patch_json('/audits/%s' % self.audit.uuid, response = self.patch_json('/audits/%s' % self.audit.uuid,
[{'path': '/uuid', 'op': 'remove'}], [{'path': '/uuid', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -357,7 +358,7 @@ class TestPatch(api_base.FunctionalTest):
'/audits/%s' % self.audit.uuid, '/audits/%s' % self.audit.uuid,
[{'path': '/non-existent', 'op': 'remove'}], [{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_code) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -415,7 +416,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest):
'op': 'replace'}], 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_code) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
response = self.get_json('/audits/%s' % self.audit.uuid) response = self.get_json('/audits/%s' % self.audit.uuid)
@@ -462,7 +463,7 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
[{'path': '/state', 'value': self.new_state, [{'path': '/state', 'value': self.new_state,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(HTTPStatus.OK, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid) response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(self.new_state, response['state']) self.assertEqual(self.new_state, response['state'])
@@ -502,7 +503,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
# Check location header # Check location header
self.assertIsNotNone(response.location) self.assertIsNotNone(response.location)
expected_location = '/v1/audits/%s' % response.json['uuid'] expected_location = '/v1/audits/%s' % response.json['uuid']
@@ -527,7 +528,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict = post_get_test_audit(state=objects.audit.State.SUCCEEDED) audit_dict = post_get_test_audit(state=objects.audit.State.SUCCEEDED)
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -542,7 +543,7 @@ class TestPost(api_base.FunctionalTest):
'next_run_time', 'hostname']) 'next_run_time', 'hostname'])
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -557,7 +558,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid'])) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -573,7 +574,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid'])) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -590,7 +591,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid'])) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -608,7 +609,7 @@ class TestPost(api_base.FunctionalTest):
'01234567-8910-1112-1314-151617181920') '01234567-8910-1112-1314-151617181920')
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual("application/json", response.content_type) self.assertEqual("application/json", response.content_type)
expected_error_msg = ('The audit template UUID or name specified is ' expected_error_msg = ('The audit template UUID or name specified is '
'invalid') 'invalid')
@@ -643,7 +644,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid'])) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -660,7 +661,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertEqual(audit_dict['interval'], response.json['interval']) self.assertEqual(audit_dict['interval'], response.json['interval'])
@@ -679,7 +680,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertEqual(audit_dict['interval'], response.json['interval']) self.assertEqual(audit_dict['interval'], response.json['interval'])
@@ -698,9 +699,9 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(500, response.status_int) self.assertEqual(HTTPStatus.INTERNAL_SERVER_ERROR, response.status_int)
expected_error_msg = ('Exactly 5 or 6 columns has to be ' expected_error_msg = ('Exactly 5 or 6 columns has to be '
'specified for iteratorexpression.') 'specified for iterator expression.')
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message']) self.assertIn(expected_error_msg, response.json['error_message'])
@@ -714,7 +715,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
expected_error_msg = ('Interval of audit must be specified ' expected_error_msg = ('Interval of audit must be specified '
'for CONTINUOUS.') 'for CONTINUOUS.')
@@ -731,7 +732,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
expected_error_msg = 'Interval of audit must not be set for ONESHOT.' expected_error_msg = 'Interval of audit must not be set for ONESHOT.'
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -755,7 +756,7 @@ class TestPost(api_base.FunctionalTest):
del audit_dict['scope'] del audit_dict['scope']
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
assert not mock_trigger_audit.called assert not mock_trigger_audit.called
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@@ -769,7 +770,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
expected_error_msg = ('Specify parameters but no predefined ' expected_error_msg = ('Specify parameters but no predefined '
'strategy for audit, or no ' 'strategy for audit, or no '
'parameter spec in predefined strategy') 'parameter spec in predefined strategy')
@@ -792,7 +793,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
expected_error_msg = ('Specify parameters but no predefined ' expected_error_msg = ('Specify parameters but no predefined '
'strategy for audit, or no ' 'strategy for audit, or no '
'parameter spec in predefined strategy') 'parameter spec in predefined strategy')
@@ -816,7 +817,7 @@ class TestPost(api_base.FunctionalTest):
del audit_dict[k] del audit_dict[k]
response = self.post_json('/audits', audit_dict, expect_errors=True) response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual("application/json", response.content_type) self.assertEqual("application/json", response.content_type)
expected_error_msg = 'Audit parameter fake2 are not allowed' expected_error_msg = 'Audit parameter fake2 are not allowed'
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -875,13 +876,13 @@ class TestPost(api_base.FunctionalTest):
audit_dict['name'] = normal_name audit_dict['name'] = normal_name
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(normal_name, response.json['name']) self.assertEqual(normal_name, response.json['name'])
audit_dict['name'] = long_name audit_dict['name'] = long_name
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertNotEqual(long_name, response.json['name']) self.assertNotEqual(long_name, response.json['name'])
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@@ -905,7 +906,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict, audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.1'}) headers={'OpenStack-API-Version': 'infra-optim 1.1'})
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertEqual(audit_dict['interval'], response.json['interval']) self.assertEqual(audit_dict['interval'], response.json['interval'])
@@ -944,7 +945,7 @@ class TestPost(api_base.FunctionalTest):
headers={'OpenStack-API-Version': 'infra-optim 1.0'}, headers={'OpenStack-API-Version': 'infra-optim 1.0'},
expect_errors=True) expect_errors=True)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(406, response.status_int) self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
expected_error_msg = 'Request not acceptable.' expected_error_msg = 'Request not acceptable.'
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message']) self.assertIn(expected_error_msg, response.json['error_message'])
@@ -963,7 +964,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict, audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.2'}) headers={'OpenStack-API-Version': 'infra-optim 1.2'})
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertFalse(response.json['force']) self.assertFalse(response.json['force'])
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@@ -980,7 +981,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict, audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.2'}) headers={'OpenStack-API-Version': 'infra-optim 1.2'})
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertTrue(response.json['force']) self.assertTrue(response.json['force'])
@@ -1013,7 +1014,7 @@ class TestDelete(api_base.FunctionalTest):
'op': 'replace'}]) 'op': 'replace'}])
response = self.delete('/audits/%s' % self.audit.uuid, response = self.delete('/audits/%s' % self.audit.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -1025,7 +1026,7 @@ class TestDelete(api_base.FunctionalTest):
self.delete('/audits/%s' % self.audit.uuid) self.delete('/audits/%s' % self.audit.uuid)
response = self.get_json('/audits/%s' % self.audit.uuid, response = self.get_json('/audits/%s' % self.audit.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -1041,7 +1042,7 @@ class TestDelete(api_base.FunctionalTest):
def test_delete_audit_not_found(self): def test_delete_audit_not_found(self):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
response = self.delete('/audits/%s' % uuid, expect_errors=True) response = self.delete('/audits/%s' % uuid, expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -1058,7 +1059,7 @@ class TestAuditPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:defaut"}) rule: "rule:defaut"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -15,6 +15,7 @@
from unittest import mock from unittest import mock
from http import HTTPStatus
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from watcher.decision_engine import rpcapi as deapi from watcher.decision_engine import rpcapi as deapi
@@ -42,7 +43,7 @@ class TestListDataModel(api_base.FunctionalTest):
'/data_model/?data_model_type=compute', '/data_model/?data_model_type=compute',
headers={'OpenStack-API-Version': 'infra-optim 1.2'}, headers={'OpenStack-API-Version': 'infra-optim 1.2'},
expect_errors=True) expect_errors=True)
self.assertEqual(406, response.status_int) self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
class TestDataModelPolicyEnforcement(api_base.FunctionalTest): class TestDataModelPolicyEnforcement(api_base.FunctionalTest):
@@ -59,7 +60,7 @@ class TestDataModelPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:defaut"}) rule: "rule:defaut"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from urllib import parse as urlparse from urllib import parse as urlparse
@@ -59,7 +60,7 @@ class TestListGoal(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/goals/%s' % goal['uuid'], '/goals/%s' % goal['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
goal = obj_utils.create_test_goal(self.context) goal = obj_utils.create_test_goal(self.context)
@@ -71,7 +72,7 @@ class TestListGoal(api_base.FunctionalTest):
goal = obj_utils.create_test_goal(self.context) goal = obj_utils.create_test_goal(self.context)
response = self.get_json('/goals/%s/detail' % goal.uuid, response = self.get_json('/goals/%s/detail' % goal.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
goal_list = [] goal_list = []
@@ -139,7 +140,7 @@ class TestListGoal(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/goals?sort_key=%s' % 'bad_name', '/goals?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
class TestGoalPolicyEnforcement(api_base.FunctionalTest): class TestGoalPolicyEnforcement(api_base.FunctionalTest):
@@ -150,7 +151,7 @@ class TestGoalPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:default"}) rule: "rule:default"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from http import HTTPStatus
from watcher.api.controllers.v1 import versions from watcher.api.controllers.v1 import versions
from watcher.tests.api import base as api_base from watcher.tests.api import base as api_base
@@ -38,7 +40,7 @@ class TestMicroversions(api_base.FunctionalTest):
'10'])}, '10'])},
expect_errors=True, return_json=False) expect_errors=True, return_json=False)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(406, response.status_int) self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
expected_error_msg = ('Invalid value for' expected_error_msg = ('Invalid value for'
' OpenStack-API-Version header') ' OpenStack-API-Version header')
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -98,7 +100,7 @@ class TestMicroversions(api_base.FunctionalTest):
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE, headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'1.999'])}, '1.999'])},
expect_errors=True) expect_errors=True)
self.assertEqual(406, response.status_int) self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER) self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER) self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
expected_error_msg = ('Version 1.999 was requested but the minor ' expected_error_msg = ('Version 1.999 was requested but the minor '

View File

@@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from watcher.common import utils from watcher.common import utils
@@ -44,7 +45,7 @@ class TestListScoringEngine(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/scoring_engines/%s' % scoring_engine['name'], '/scoring_engines/%s' % scoring_engine['name'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
obj_utils.create_test_goal(self.context) obj_utils.create_test_goal(self.context)
@@ -63,7 +64,7 @@ class TestListScoringEngine(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/scoring_engines/%s/detail' % scoring_engine.id, '/scoring_engines/%s/detail' % scoring_engine.id,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
scoring_engine_list = [] scoring_engine_list = []
@@ -131,7 +132,7 @@ class TestListScoringEngine(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/goals?sort_key=%s' % 'bad_name', '/goals?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
class TestScoringEnginePolicyEnforcement(api_base.FunctionalTest): class TestScoringEnginePolicyEnforcement(api_base.FunctionalTest):
@@ -142,7 +143,7 @@ class TestScoringEnginePolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:default"}) rule: "rule:default"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from urllib import parse as urlparse from urllib import parse as urlparse
@@ -57,7 +58,7 @@ class TestListService(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/services/%s' % service['id'], '/services/%s' % service['id'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
service = obj_utils.create_test_service(self.context) service = obj_utils.create_test_service(self.context)
@@ -74,7 +75,7 @@ class TestListService(api_base.FunctionalTest):
service = obj_utils.create_test_service(self.context) service = obj_utils.create_test_service(self.context)
response = self.get_json('/services/%s/detail' % service.id, response = self.get_json('/services/%s/detail' % service.id,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
service_list = [] service_list = []
@@ -149,7 +150,7 @@ class TestListService(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/services?sort_key=%s' % 'bad_name', '/services?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
class TestServicePolicyEnforcement(api_base.FunctionalTest): class TestServicePolicyEnforcement(api_base.FunctionalTest):
@@ -160,7 +161,7 @@ class TestServicePolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:default"}) rule: "rule:default"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -13,6 +13,7 @@
from unittest import mock from unittest import mock
from urllib import parse as urlparse from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
@@ -88,7 +89,7 @@ class TestListStrategy(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/strategies/%s' % strategy['uuid'], '/strategies/%s' % strategy['uuid'],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_detail(self): def test_detail(self):
strategy = obj_utils.create_test_strategy(self.context) strategy = obj_utils.create_test_strategy(self.context)
@@ -104,7 +105,7 @@ class TestListStrategy(api_base.FunctionalTest):
strategy = obj_utils.create_test_strategy(self.context) strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json('/strategies/%s/detail' % strategy.uuid, response = self.get_json('/strategies/%s/detail' % strategy.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
def test_many(self): def test_many(self):
strategy_list = [] strategy_list = []
@@ -240,7 +241,7 @@ class TestListStrategy(api_base.FunctionalTest):
response = self.get_json( response = self.get_json(
'/strategies?sort_key=%s' % 'bad_name', '/strategies?sort_key=%s' % 'bad_name',
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
class TestStrategyPolicyEnforcement(api_base.FunctionalTest): class TestStrategyPolicyEnforcement(api_base.FunctionalTest):
@@ -256,7 +257,7 @@ class TestStrategyPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api", "default": "rule:admin_api",
rule: "rule:defaut"}) rule: "rule:defaut"})
response = func(*arg, **kwarg) response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int) self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue( self.assertTrue(
"Policy doesn't allow %s to be performed." % rule, "Policy doesn't allow %s to be performed." % rule,

View File

@@ -18,6 +18,8 @@ import webtest
import wsme import wsme
from wsme import types as wtypes from wsme import types as wtypes
from http import HTTPStatus
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.common import exception from watcher.common import exception
from watcher.common import utils from watcher.common import utils
@@ -120,68 +122,68 @@ class TestJsonPatchType(base.TestCase):
{'path': '/dict', 'op': 'add', {'path': '/dict', 'op': 'add',
'value': {'cat': 'meow'}}] 'value': {'cat': 'meow'}}]
ret = self._patch_json(valid_patches, False) ret = self._patch_json(valid_patches, False)
self.assertEqual(200, ret.status_int) self.assertEqual(HTTPStatus.OK, ret.status_int)
self.assertEqual(valid_patches, ret.json) self.assertEqual(valid_patches, ret.json)
def test_cannot_update_internal_attr(self): def test_cannot_update_internal_attr(self):
patch = [{'path': '/internal', 'op': 'replace', 'value': 'foo'}] patch = [{'path': '/internal', 'op': 'replace', 'value': 'foo'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_cannot_update_internal_dict_attr(self): def test_cannot_update_internal_dict_attr(self):
patch = [{'path': '/internal', 'op': 'replace', patch = [{'path': '/internal', 'op': 'replace',
'value': 'foo'}] 'value': 'foo'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_mandatory_attr(self): def test_mandatory_attr(self):
patch = [{'op': 'replace', 'path': '/mandatory', 'value': 'foo'}] patch = [{'op': 'replace', 'path': '/mandatory', 'value': 'foo'}]
ret = self._patch_json(patch, False) ret = self._patch_json(patch, False)
self.assertEqual(200, ret.status_int) self.assertEqual(HTTPStatus.OK, ret.status_int)
self.assertEqual(patch, ret.json) self.assertEqual(patch, ret.json)
def test_cannot_remove_mandatory_attr(self): def test_cannot_remove_mandatory_attr(self):
patch = [{'op': 'remove', 'path': '/mandatory'}] patch = [{'op': 'remove', 'path': '/mandatory'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_missing_required_fields_path(self): def test_missing_required_fields_path(self):
missing_path = [{'op': 'remove'}] missing_path = [{'op': 'remove'}]
ret = self._patch_json(missing_path, True) ret = self._patch_json(missing_path, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_missing_required_fields_op(self): def test_missing_required_fields_op(self):
missing_op = [{'path': '/foo'}] missing_op = [{'path': '/foo'}]
ret = self._patch_json(missing_op, True) ret = self._patch_json(missing_op, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_invalid_op(self): def test_invalid_op(self):
patch = [{'path': '/foo', 'op': 'invalid'}] patch = [{'path': '/foo', 'op': 'invalid'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_invalid_path(self): def test_invalid_path(self):
patch = [{'path': 'invalid-path', 'op': 'remove'}] patch = [{'path': 'invalid-path', 'op': 'remove'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_cannot_add_with_no_value(self): def test_cannot_add_with_no_value(self):
patch = [{'path': '/extra/foo', 'op': 'add'}] patch = [{'path': '/extra/foo', 'op': 'add'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])
def test_cannot_replace_with_no_value(self): def test_cannot_replace_with_no_value(self):
patch = [{'path': '/foo', 'op': 'replace'}] patch = [{'path': '/foo', 'op': 'replace'}]
ret = self._patch_json(patch, True) ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertTrue(ret.json['faultstring']) self.assertTrue(ret.json['faultstring'])

View File

@@ -12,6 +12,8 @@
from unittest import mock from unittest import mock
from http import HTTPStatus
from watcher.decision_engine import rpcapi as deapi from watcher.decision_engine import rpcapi as deapi
from watcher import objects from watcher import objects
from watcher.tests.api import base as api_base from watcher.tests.api import base as api_base
@@ -34,7 +36,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json( response = self.post_json(
'/webhooks/%s' % audit['uuid'], {}, '/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'}) headers={'OpenStack-API-Version': 'infra-optim 1.4'})
self.assertEqual(202, response.status_int) self.assertEqual(HTTPStatus.ACCEPTED, response.status_int)
mock_trigger_audit.assert_called_once_with( mock_trigger_audit.assert_called_once_with(
mock.ANY, audit['uuid']) mock.ANY, audit['uuid'])
@@ -43,7 +45,7 @@ class TestPost(api_base.FunctionalTest):
'/webhooks/no-audit', {}, '/webhooks/no-audit', {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'}, headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -53,7 +55,7 @@ class TestPost(api_base.FunctionalTest):
'/webhooks/%s' % audit['uuid'], {}, '/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'}, headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
@@ -66,6 +68,6 @@ class TestPost(api_base.FunctionalTest):
'/webhooks/%s' % audit['uuid'], {}, '/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'}, headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])

View File

@@ -19,134 +19,151 @@ import jsonschema
from watcher.applier.actions import base as baction from watcher.applier.actions import base as baction
from watcher.applier.actions import change_node_power_state from watcher.applier.actions import change_node_power_state
from watcher.common import clients from watcher.common.metal_helper import constants as m_constants
from watcher.common.metal_helper import factory as m_helper_factory
from watcher.tests import base from watcher.tests import base
from watcher.tests.decision_engine import fake_metal_helper
COMPUTE_NODE = "compute-1" COMPUTE_NODE = "compute-1"
@mock.patch.object(clients.OpenStackClients, 'nova')
@mock.patch.object(clients.OpenStackClients, 'ironic')
class TestChangeNodePowerState(base.TestCase): class TestChangeNodePowerState(base.TestCase):
def setUp(self): def setUp(self):
super(TestChangeNodePowerState, self).setUp() super(TestChangeNodePowerState, self).setUp()
p_m_factory = mock.patch.object(m_helper_factory, 'get_helper')
m_factory = p_m_factory.start()
self._metal_helper = m_factory.return_value
self.addCleanup(p_m_factory.stop)
# Let's avoid unnecessary sleep calls while running the test.
p_sleep = mock.patch('time.sleep')
p_sleep.start()
self.addCleanup(p_sleep.stop)
self.input_parameters = { self.input_parameters = {
baction.BaseAction.RESOURCE_ID: COMPUTE_NODE, baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
"state": change_node_power_state.NodeState.POWERON.value, "state": m_constants.PowerState.ON.value,
} }
self.action = change_node_power_state.ChangeNodePowerState( self.action = change_node_power_state.ChangeNodePowerState(
mock.Mock()) mock.Mock())
self.action.input_parameters = self.input_parameters self.action.input_parameters = self.input_parameters
def test_parameters_down(self, mock_ironic, mock_nova): def test_parameters_down(self):
self.action.input_parameters = { self.action.input_parameters = {
baction.BaseAction.RESOURCE_ID: COMPUTE_NODE, baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
self.action.STATE: self.action.STATE:
change_node_power_state.NodeState.POWEROFF.value} m_constants.PowerState.OFF.value}
self.assertTrue(self.action.validate_parameters()) self.assertTrue(self.action.validate_parameters())
def test_parameters_up(self, mock_ironic, mock_nova): def test_parameters_up(self):
self.action.input_parameters = { self.action.input_parameters = {
baction.BaseAction.RESOURCE_ID: COMPUTE_NODE, baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
self.action.STATE: self.action.STATE:
change_node_power_state.NodeState.POWERON.value} m_constants.PowerState.ON.value}
self.assertTrue(self.action.validate_parameters()) self.assertTrue(self.action.validate_parameters())
def test_parameters_exception_wrong_state(self, mock_ironic, mock_nova): def test_parameters_exception_wrong_state(self):
self.action.input_parameters = { self.action.input_parameters = {
baction.BaseAction.RESOURCE_ID: COMPUTE_NODE, baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
self.action.STATE: 'error'} self.action.STATE: 'error'}
self.assertRaises(jsonschema.ValidationError, self.assertRaises(jsonschema.ValidationError,
self.action.validate_parameters) self.action.validate_parameters)
def test_parameters_resource_id_empty(self, mock_ironic, mock_nova): def test_parameters_resource_id_empty(self):
self.action.input_parameters = { self.action.input_parameters = {
self.action.STATE: self.action.STATE:
change_node_power_state.NodeState.POWERON.value, m_constants.PowerState.ON.value,
} }
self.assertRaises(jsonschema.ValidationError, self.assertRaises(jsonschema.ValidationError,
self.action.validate_parameters) self.action.validate_parameters)
def test_parameters_applies_add_extra(self, mock_ironic, mock_nova): def test_parameters_applies_add_extra(self):
self.action.input_parameters = {"extra": "failed"} self.action.input_parameters = {"extra": "failed"}
self.assertRaises(jsonschema.ValidationError, self.assertRaises(jsonschema.ValidationError,
self.action.validate_parameters) self.action.validate_parameters)
def test_change_service_state_pre_condition(self, mock_ironic, mock_nova): def test_change_service_state_pre_condition(self):
try: try:
self.action.pre_condition() self.action.pre_condition()
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
def test_change_node_state_post_condition(self, mock_ironic, mock_nova): def test_change_node_state_post_condition(self):
try: try:
self.action.post_condition() self.action.post_condition()
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
def test_execute_node_service_state_with_poweron_target( def test_execute_node_service_state_with_poweron_target(self):
self, mock_ironic, mock_nova):
mock_irclient = mock_ironic.return_value
self.action.input_parameters["state"] = ( self.action.input_parameters["state"] = (
change_node_power_state.NodeState.POWERON.value) m_constants.PowerState.ON.value)
mock_irclient.node.get.side_effect = [ mock_nodes = [
mock.MagicMock(power_state='power off'), fake_metal_helper.get_mock_metal_node(
mock.MagicMock(power_state='power on')] power_state=m_constants.PowerState.OFF),
fake_metal_helper.get_mock_metal_node(
power_state=m_constants.PowerState.ON)
]
self._metal_helper.get_node.side_effect = mock_nodes
result = self.action.execute() result = self.action.execute()
self.assertTrue(result) self.assertTrue(result)
mock_irclient.node.set_power_state.assert_called_once_with( mock_nodes[0].set_power_state.assert_called_once_with(
COMPUTE_NODE, change_node_power_state.NodeState.POWERON.value) m_constants.PowerState.ON.value)
def test_execute_change_node_state_with_poweroff_target( def test_execute_change_node_state_with_poweroff_target(self):
self, mock_ironic, mock_nova):
mock_irclient = mock_ironic.return_value
mock_nvclient = mock_nova.return_value
mock_get = mock.MagicMock()
mock_get.to_dict.return_value = {'running_vms': 0}
mock_nvclient.hypervisors.get.return_value = mock_get
self.action.input_parameters["state"] = ( self.action.input_parameters["state"] = (
change_node_power_state.NodeState.POWEROFF.value) m_constants.PowerState.OFF.value)
mock_irclient.node.get.side_effect = [
mock.MagicMock(power_state='power on'), mock_nodes = [
mock.MagicMock(power_state='power on'), fake_metal_helper.get_mock_metal_node(
mock.MagicMock(power_state='power off')] power_state=m_constants.PowerState.ON),
fake_metal_helper.get_mock_metal_node(
power_state=m_constants.PowerState.ON),
fake_metal_helper.get_mock_metal_node(
power_state=m_constants.PowerState.OFF)
]
self._metal_helper.get_node.side_effect = mock_nodes
result = self.action.execute() result = self.action.execute()
self.assertTrue(result) self.assertTrue(result)
mock_irclient.node.set_power_state.assert_called_once_with( mock_nodes[0].set_power_state.assert_called_once_with(
COMPUTE_NODE, change_node_power_state.NodeState.POWEROFF.value) m_constants.PowerState.OFF.value)
def test_revert_change_node_state_with_poweron_target( def test_revert_change_node_state_with_poweron_target(self):
self, mock_ironic, mock_nova):
mock_irclient = mock_ironic.return_value
mock_nvclient = mock_nova.return_value
mock_get = mock.MagicMock()
mock_get.to_dict.return_value = {'running_vms': 0}
mock_nvclient.hypervisors.get.return_value = mock_get
self.action.input_parameters["state"] = ( self.action.input_parameters["state"] = (
change_node_power_state.NodeState.POWERON.value) m_constants.PowerState.ON.value)
mock_irclient.node.get.side_effect = [
mock.MagicMock(power_state='power on'), mock_nodes = [
mock.MagicMock(power_state='power on'), fake_metal_helper.get_mock_metal_node(
mock.MagicMock(power_state='power off')] power_state=m_constants.PowerState.ON),
fake_metal_helper.get_mock_metal_node(
power_state=m_constants.PowerState.ON),
fake_metal_helper.get_mock_metal_node(
power_state=m_constants.PowerState.OFF)
]
self._metal_helper.get_node.side_effect = mock_nodes
self.action.revert() self.action.revert()
mock_irclient.node.set_power_state.assert_called_once_with( mock_nodes[0].set_power_state.assert_called_once_with(
COMPUTE_NODE, change_node_power_state.NodeState.POWEROFF.value) m_constants.PowerState.OFF.value)
def test_revert_change_node_state_with_poweroff_target( def test_revert_change_node_state_with_poweroff_target(self):
self, mock_ironic, mock_nova):
mock_irclient = mock_ironic.return_value
self.action.input_parameters["state"] = ( self.action.input_parameters["state"] = (
change_node_power_state.NodeState.POWEROFF.value) m_constants.PowerState.OFF.value)
mock_irclient.node.get.side_effect = [ mock_nodes = [
mock.MagicMock(power_state='power off'), fake_metal_helper.get_mock_metal_node(
mock.MagicMock(power_state='power on')] power_state=m_constants.PowerState.OFF),
fake_metal_helper.get_mock_metal_node(
power_state=m_constants.PowerState.ON)
]
self._metal_helper.get_node.side_effect = mock_nodes
self.action.revert() self.action.revert()
mock_irclient.node.set_power_state.assert_called_once_with( mock_nodes[0].set_power_state.assert_called_once_with(
COMPUTE_NODE, change_node_power_state.NodeState.POWERON.value) m_constants.PowerState.ON.value)

View File

@@ -40,7 +40,7 @@ class TestApplierAPI(base.TestCase):
'check_api_version', 'check_api_version',
api_version=rpcapi.ApplierAPI().API_VERSION) api_version=rpcapi.ApplierAPI().API_VERSION)
def test_execute_audit_without_error(self): def test_execute_action_plan_without_error(self):
with mock.patch.object(om.RPCClient, 'cast') as mock_cast: with mock.patch.object(om.RPCClient, 'cast') as mock_cast:
action_plan_uuid = utils.generate_uuid() action_plan_uuid = utils.generate_uuid()
self.api.launch_action_plan(self.context, action_plan_uuid) self.api.launch_action_plan(self.context, action_plan_uuid)

View File

@@ -83,4 +83,4 @@ class TestCancelOngoingActionPlans(db_base.DbTestCase):
m_action_list.assert_called() m_action_list.assert_called()
m_plan_save.assert_called() m_plan_save.assert_called()
m_action_save.assert_called() m_action_save.assert_called()
self.assertEqual(self.action.state, objects.audit.State.CANCELLED) self.assertEqual(self.action.state, objects.action.State.CANCELLED)

View File

@@ -0,0 +1,96 @@
# Copyright 2023 Cloudbase Solutions
# 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 unittest import mock
from watcher.common import exception
from watcher.common.metal_helper import base as m_helper_base
from watcher.common.metal_helper import constants as m_constants
from watcher.tests import base
# The base classes have abstract methods, we'll need to
# stub them.
class MockMetalNode(m_helper_base.BaseMetalNode):
def get_power_state(self):
raise NotImplementedError()
def get_id(self):
raise NotImplementedError()
def power_on(self):
raise NotImplementedError()
def power_off(self):
raise NotImplementedError()
class MockMetalHelper(m_helper_base.BaseMetalHelper):
def list_compute_nodes(self):
pass
def get_node(self, node_id):
pass
class TestBaseMetalNode(base.TestCase):
def setUp(self):
super().setUp()
self._nova_node = mock.Mock()
self._node = MockMetalNode(self._nova_node)
def test_get_hypervisor_node(self):
self.assertEqual(
self._nova_node,
self._node.get_hypervisor_node())
def test_get_hypervisor_node_missing(self):
node = MockMetalNode()
self.assertRaises(
exception.Invalid,
node.get_hypervisor_node)
def test_get_hypervisor_hostname(self):
self.assertEqual(
self._nova_node.hypervisor_hostname,
self._node.get_hypervisor_hostname())
@mock.patch.object(MockMetalNode, 'power_on')
@mock.patch.object(MockMetalNode, 'power_off')
def test_set_power_state(self,
mock_power_off, mock_power_on):
self._node.set_power_state(m_constants.PowerState.ON)
mock_power_on.assert_called_once_with()
self._node.set_power_state(m_constants.PowerState.OFF)
mock_power_off.assert_called_once_with()
self.assertRaises(
exception.UnsupportedActionType,
self._node.set_power_state,
m_constants.PowerState.UNKNOWN)
class TestBaseMetalHelper(base.TestCase):
def setUp(self):
super().setUp()
self._osc = mock.Mock()
self._helper = MockMetalHelper(self._osc)
def test_nova_client_attr(self):
self.assertEqual(self._osc.nova.return_value,
self._helper.nova_client)

View File

@@ -0,0 +1,38 @@
# Copyright 2023 Cloudbase Solutions
# 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 unittest import mock
from watcher.common import clients
from watcher.common.metal_helper import factory
from watcher.common.metal_helper import ironic
from watcher.common.metal_helper import maas
from watcher.tests import base
class TestMetalHelperFactory(base.TestCase):
@mock.patch.object(clients, 'OpenStackClients')
@mock.patch.object(maas, 'MaasHelper')
@mock.patch.object(ironic, 'IronicHelper')
def test_factory(self, mock_ironic, mock_maas, mock_osc):
self.assertEqual(
mock_ironic.return_value,
factory.get_helper())
self.config(url="fake_maas_url", group="maas_client")
self.assertEqual(
mock_maas.return_value,
factory.get_helper())

View File

@@ -0,0 +1,128 @@
# Copyright 2023 Cloudbase Solutions
# 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 unittest import mock
from watcher.common.metal_helper import constants as m_constants
from watcher.common.metal_helper import ironic
from watcher.tests import base
class TestIronicNode(base.TestCase):
def setUp(self):
super().setUp()
self._wrapped_node = mock.Mock()
self._nova_node = mock.Mock()
self._ironic_client = mock.Mock()
self._node = ironic.IronicNode(
self._wrapped_node, self._nova_node, self._ironic_client)
def test_get_power_state(self):
states = (
"power on",
"power off",
"rebooting",
"soft power off",
"soft reboot",
'SomeOtherState')
type(self._wrapped_node).power_state = mock.PropertyMock(
side_effect=states)
expected_states = (
m_constants.PowerState.ON,
m_constants.PowerState.OFF,
m_constants.PowerState.ON,
m_constants.PowerState.OFF,
m_constants.PowerState.ON,
m_constants.PowerState.UNKNOWN)
for expected_state in expected_states:
actual_state = self._node.get_power_state()
self.assertEqual(expected_state, actual_state)
def test_get_id(self):
self.assertEqual(
self._wrapped_node.uuid,
self._node.get_id())
def test_power_on(self):
self._node.power_on()
self._ironic_client.node.set_power_state.assert_called_once_with(
self._wrapped_node.uuid, "on")
def test_power_off(self):
self._node.power_off()
self._ironic_client.node.set_power_state.assert_called_once_with(
self._wrapped_node.uuid, "off")
class TestIronicHelper(base.TestCase):
def setUp(self):
super().setUp()
self._mock_osc = mock.Mock()
self._mock_nova_client = self._mock_osc.nova.return_value
self._mock_ironic_client = self._mock_osc.ironic.return_value
self._helper = ironic.IronicHelper(osc=self._mock_osc)
def test_list_compute_nodes(self):
mock_machines = [
mock.Mock(
extra=dict(compute_node_id=mock.sentinel.compute_node_id)),
mock.Mock(
extra=dict(compute_node_id=mock.sentinel.compute_node_id2)),
mock.Mock(
extra=dict())
]
mock_hypervisor = mock.Mock()
self._mock_ironic_client.node.list.return_value = mock_machines
self._mock_ironic_client.node.get.side_effect = mock_machines
self._mock_nova_client.hypervisors.get.side_effect = (
mock_hypervisor, None)
out_nodes = self._helper.list_compute_nodes()
self.assertEqual(1, len(out_nodes))
out_node = out_nodes[0]
self.assertIsInstance(out_node, ironic.IronicNode)
self.assertEqual(mock_hypervisor, out_node._nova_node)
self.assertEqual(mock_machines[0], out_node._ironic_node)
self.assertEqual(self._mock_ironic_client, out_node._ironic_client)
def test_get_node(self):
mock_machine = mock.Mock(
extra=dict(compute_node_id=mock.sentinel.compute_node_id))
self._mock_ironic_client.node.get.return_value = mock_machine
out_node = self._helper.get_node(mock.sentinel.id)
self.assertEqual(self._mock_nova_client.hypervisors.get.return_value,
out_node._nova_node)
self.assertEqual(self._mock_ironic_client, out_node._ironic_client)
self.assertEqual(mock_machine, out_node._ironic_node)
def test_get_node_not_a_hypervisor(self):
mock_machine = mock.Mock(extra=dict(compute_node_id=None))
self._mock_ironic_client.node.get.return_value = mock_machine
out_node = self._helper.get_node(mock.sentinel.id)
self._mock_nova_client.hypervisors.get.assert_not_called()
self.assertIsNone(out_node._nova_node)
self.assertEqual(self._mock_ironic_client, out_node._ironic_client)
self.assertEqual(mock_machine, out_node._ironic_node)

View File

@@ -0,0 +1,126 @@
# Copyright 2023 Cloudbase Solutions
# 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 unittest import mock
try:
from maas.client import enum as maas_enum
except ImportError:
maas_enum = None
from watcher.common.metal_helper import constants as m_constants
from watcher.common.metal_helper import maas
from watcher.tests import base
class TestMaasNode(base.TestCase):
def setUp(self):
super().setUp()
self._wrapped_node = mock.Mock()
self._nova_node = mock.Mock()
self._maas_client = mock.Mock()
self._node = maas.MaasNode(
self._wrapped_node, self._nova_node, self._maas_client)
def test_get_power_state(self):
if not maas_enum:
self.skipTest("python-libmaas not intalled.")
self._wrapped_node.query_power_state.side_effect = (
maas_enum.PowerState.ON,
maas_enum.PowerState.OFF,
maas_enum.PowerState.ERROR,
maas_enum.PowerState.UNKNOWN,
'SomeOtherState')
expected_states = (
m_constants.PowerState.ON,
m_constants.PowerState.OFF,
m_constants.PowerState.ERROR,
m_constants.PowerState.UNKNOWN,
m_constants.PowerState.UNKNOWN)
for expected_state in expected_states:
actual_state = self._node.get_power_state()
self.assertEqual(expected_state, actual_state)
def test_get_id(self):
self.assertEqual(
self._wrapped_node.system_id,
self._node.get_id())
def test_power_on(self):
self._node.power_on()
self._wrapped_node.power_on.assert_called_once_with()
def test_power_off(self):
self._node.power_off()
self._wrapped_node.power_off.assert_called_once_with()
class TestMaasHelper(base.TestCase):
def setUp(self):
super().setUp()
self._mock_osc = mock.Mock()
self._mock_nova_client = self._mock_osc.nova.return_value
self._mock_maas_client = self._mock_osc.maas.return_value
self._helper = maas.MaasHelper(osc=self._mock_osc)
def test_list_compute_nodes(self):
compute_fqdn = "compute-0"
# some other MAAS node, not a Nova node
ctrl_fqdn = "ctrl-1"
mock_machines = [
mock.Mock(fqdn=compute_fqdn,
system_id=mock.sentinel.compute_node_id),
mock.Mock(fqdn=ctrl_fqdn,
system_id=mock.sentinel.ctrl_node_id),
]
mock_hypervisors = [
mock.Mock(hypervisor_hostname=compute_fqdn),
]
self._mock_maas_client.machines.list.return_value = mock_machines
self._mock_nova_client.hypervisors.list.return_value = mock_hypervisors
out_nodes = self._helper.list_compute_nodes()
self.assertEqual(1, len(out_nodes))
out_node = out_nodes[0]
self.assertIsInstance(out_node, maas.MaasNode)
self.assertEqual(mock.sentinel.compute_node_id, out_node.get_id())
self.assertEqual(compute_fqdn, out_node.get_hypervisor_hostname())
def test_get_node(self):
mock_machine = mock.Mock(fqdn='compute-0')
self._mock_maas_client.machines.get.return_value = mock_machine
mock_compute_nodes = [
mock.Mock(hypervisor_hostname="compute-011"),
mock.Mock(hypervisor_hostname="compute-0"),
mock.Mock(hypervisor_hostname="compute-01"),
]
self._mock_nova_client.hypervisors.search.return_value = (
mock_compute_nodes)
out_node = self._helper.get_node(mock.sentinel.id)
self.assertEqual(mock_compute_nodes[1], out_node._nova_node)
self.assertEqual(self._mock_maas_client, out_node._maas_client)
self.assertEqual(mock_machine, out_node._maas_node)

View File

@@ -13,9 +13,11 @@
# limitations under the License. # limitations under the License.
# #
import time
from unittest import mock from unittest import mock
from http import HTTPStatus
import time
from cinderclient import exceptions as cinder_exception from cinderclient import exceptions as cinder_exception
from watcher.common import cinder_helper from watcher.common import cinder_helper
@@ -291,7 +293,8 @@ class TestCinderHelper(base.TestCase):
volume = self.fake_volume() volume = self.fake_volume()
cinder_util.get_volume = mock.MagicMock() cinder_util.get_volume = mock.MagicMock()
cinder_util.get_volume.side_effect = cinder_exception.NotFound(404) cinder_util.get_volume.side_effect =\
cinder_exception.NotFound(HTTPStatus.NOT_FOUND)
result = cinder_util._can_get_volume(volume.id) result = cinder_util._can_get_volume(volume.id)
self.assertFalse(result) self.assertFalse(result)
@@ -339,7 +342,7 @@ class TestCinderHelper(base.TestCase):
cinder_util = cinder_helper.CinderHelper() cinder_util = cinder_helper.CinderHelper()
volume = self.fake_volume() volume = self.fake_volume()
side_effect = cinder_exception.NotFound(404) side_effect = cinder_exception.NotFound(HTTPStatus.NOT_FOUND)
cinder_util.cinder.volumes.get.side_effect = side_effect cinder_util.cinder.volumes.get.side_effect = side_effect
cinder_util.cinder.volumes.find.return_value = False cinder_util.cinder.volumes.find.return_value = False
result = cinder_util.get_volume(volume) result = cinder_util.get_volume(volume)

View File

@@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from ceilometerclient import client as ceclient
import ceilometerclient.v2.client as ceclient_v2
from unittest import mock from unittest import mock
from cinderclient import client as ciclient from cinderclient import client as ciclient
@@ -270,56 +268,6 @@ class TestClients(base.TestCase):
cinder_cached = osc.cinder() cinder_cached = osc.cinder()
self.assertEqual(cinder, cinder_cached) self.assertEqual(cinder, cinder_cached)
@mock.patch.object(ceclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_ceilometer(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._ceilometer = None
osc.ceilometer()
mock_call.assert_called_once_with(
CONF.ceilometer_client.api_version,
None,
endpoint_type=CONF.ceilometer_client.endpoint_type,
region_name=CONF.ceilometer_client.region_name,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
def test_clients_ceilometer_diff_vers(self, mock_get_redirect_client,
mock_session):
'''ceilometerclient currently only has one version (v2)'''
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
CONF.set_override('api_version', '2',
group='ceilometer_client')
osc = clients.OpenStackClients()
osc._ceilometer = None
osc.ceilometer()
self.assertEqual(ceclient_v2.Client,
type(osc.ceilometer()))
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
def test_clients_ceilometer_diff_endpoint(self, mock_get_redirect_client,
mock_session):
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
CONF.set_override('endpoint_type', 'publicURL',
group='ceilometer_client')
osc = clients.OpenStackClients()
osc._ceilometer = None
osc.ceilometer()
self.assertEqual('publicURL', osc.ceilometer().http_client.interface)
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
def test_clients_ceilometer_cached(self, mock_get_redirect_client,
mock_session):
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
osc = clients.OpenStackClients()
osc._ceilometer = None
ceilometer = osc.ceilometer()
ceilometer_cached = osc.ceilometer()
self.assertEqual(ceilometer, ceilometer_cached)
@mock.patch.object(netclient, 'Client') @mock.patch.object(netclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session') @mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_neutron(self, mock_session, mock_call): def test_clients_neutron(self, mock_session, mock_call):

View File

@@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from http import HTTPStatus
from unittest import mock from unittest import mock
from watcher.common import placement_helper from watcher.common import placement_helper
@@ -55,10 +56,10 @@ class TestPlacementHelper(base.TestCase):
kss_req.assert_called_once_with(url, method, **kwargs) kss_req.assert_called_once_with(url, method, **kwargs)
def test_get(self, kss_req): def test_get(self, kss_req):
kss_req.return_value = fake_requests.FakeResponse(200) kss_req.return_value = fake_requests.FakeResponse(HTTPStatus.OK)
url = '/resource_providers' url = '/resource_providers'
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(200, resp.status_code) self.assertEqual(HTTPStatus.OK, resp.status_code)
self._assert_keystone_called_once(kss_req, url, 'GET') self._assert_keystone_called_once(kss_req, url, 'GET')
def test_get_resource_providers_OK(self, kss_req): def test_get_resource_providers_OK(self, kss_req):
@@ -76,7 +77,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_resource_providers(rp_name) result = self.client.get_resource_providers(rp_name)
@@ -99,7 +100,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_resource_providers(rp_name) result = self.client.get_resource_providers(rp_name)
@@ -110,7 +111,8 @@ class TestPlacementHelper(base.TestCase):
def test_get_resource_providers_fail(self, kss_req): def test_get_resource_providers_fail(self, kss_req):
rp_name = 'compute' rp_name = 'compute'
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
400, content=jsonutils.dump_as_bytes(self.fake_err_msg)) HTTPStatus.BAD_REQUEST,
content=jsonutils.dump_as_bytes(self.fake_err_msg))
result = self.client.get_resource_providers(rp_name) result = self.client.get_resource_providers(rp_name)
self.assertIsNone(result) self.assertIsNone(result)
@@ -149,7 +151,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_inventories(rp_uuid) result = self.client.get_inventories(rp_uuid)
@@ -160,7 +162,8 @@ class TestPlacementHelper(base.TestCase):
def test_get_inventories_fail(self, kss_req): def test_get_inventories_fail(self, kss_req):
rp_uuid = uuidutils.generate_uuid() rp_uuid = uuidutils.generate_uuid()
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
404, content=jsonutils.dump_as_bytes(self.fake_err_msg)) HTTPStatus.NOT_FOUND,
content=jsonutils.dump_as_bytes(self.fake_err_msg))
result = self.client.get_inventories(rp_uuid) result = self.client.get_inventories(rp_uuid)
self.assertIsNone(result) self.assertIsNone(result)
@@ -175,7 +178,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_provider_traits(rp_uuid) result = self.client.get_provider_traits(rp_uuid)
@@ -186,7 +189,8 @@ class TestPlacementHelper(base.TestCase):
def test_get_provider_traits_fail(self, kss_req): def test_get_provider_traits_fail(self, kss_req):
rp_uuid = uuidutils.generate_uuid() rp_uuid = uuidutils.generate_uuid()
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
404, content=jsonutils.dump_as_bytes(self.fake_err_msg)) HTTPStatus.NOT_FOUND,
content=jsonutils.dump_as_bytes(self.fake_err_msg))
result = self.client.get_provider_traits(rp_uuid) result = self.client.get_provider_traits(rp_uuid)
self.assertIsNone(result) self.assertIsNone(result)
@@ -216,7 +220,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_allocations_for_consumer(c_uuid) result = self.client.get_allocations_for_consumer(c_uuid)
@@ -227,7 +231,8 @@ class TestPlacementHelper(base.TestCase):
def test_get_allocations_for_consumer_fail(self, kss_req): def test_get_allocations_for_consumer_fail(self, kss_req):
c_uuid = uuidutils.generate_uuid() c_uuid = uuidutils.generate_uuid()
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
404, content=jsonutils.dump_as_bytes(self.fake_err_msg)) HTTPStatus.NOT_FOUND,
content=jsonutils.dump_as_bytes(self.fake_err_msg))
result = self.client.get_allocations_for_consumer(c_uuid) result = self.client.get_allocations_for_consumer(c_uuid)
self.assertIsNone(result) self.assertIsNone(result)
@@ -245,7 +250,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_usages_for_resource_provider(rp_uuid) result = self.client.get_usages_for_resource_provider(rp_uuid)
@@ -256,7 +261,8 @@ class TestPlacementHelper(base.TestCase):
def test_get_usages_for_resource_provider_fail(self, kss_req): def test_get_usages_for_resource_provider_fail(self, kss_req):
rp_uuid = uuidutils.generate_uuid() rp_uuid = uuidutils.generate_uuid()
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
404, content=jsonutils.dump_as_bytes(self.fake_err_msg)) HTTPStatus.NOT_FOUND,
content=jsonutils.dump_as_bytes(self.fake_err_msg))
result = self.client.get_usages_for_resource_provider(rp_uuid) result = self.client.get_usages_for_resource_provider(rp_uuid)
self.assertIsNone(result) self.assertIsNone(result)
@@ -296,7 +302,7 @@ class TestPlacementHelper(base.TestCase):
} }
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
200, content=jsonutils.dump_as_bytes(mock_json_data)) HTTPStatus.OK, content=jsonutils.dump_as_bytes(mock_json_data))
result = self.client.get_candidate_providers(resources) result = self.client.get_candidate_providers(resources)
@@ -307,6 +313,7 @@ class TestPlacementHelper(base.TestCase):
def test_get_candidate_providers_fail(self, kss_req): def test_get_candidate_providers_fail(self, kss_req):
rp_uuid = uuidutils.generate_uuid() rp_uuid = uuidutils.generate_uuid()
kss_req.return_value = fake_requests.FakeResponse( kss_req.return_value = fake_requests.FakeResponse(
404, content=jsonutils.dump_as_bytes(self.fake_err_msg)) HTTPStatus.NOT_FOUND,
content=jsonutils.dump_as_bytes(self.fake_err_msg))
result = self.client.get_candidate_providers(rp_uuid) result = self.client.get_candidate_providers(rp_uuid)
self.assertIsNone(result) self.assertIsNone(result)

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