watcher-tempest-plugin 3.5.0__tar.gz → 3.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/AUTHORS +1 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/ChangeLog +14 -0
- {watcher_tempest_plugin-3.5.0/watcher_tempest_plugin.egg-info → watcher_tempest_plugin-3.6.0}/PKG-INFO +1 -1
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/config.py +17 -0
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/api/admin/test_data_model.py +65 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/base.py +139 -35
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_continuous_audit.py +171 -0
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_data_model.py +161 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_actuator.py +4 -10
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py +87 -0
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py +61 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_host_maintenance.py +26 -27
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_node_resource_consolidation.py +102 -0
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_storage_balance.py +69 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_strategies_real.py +41 -28
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_vm_workload_consolidation.py +91 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_workload_balance.py +29 -23
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_workload_stabilization.py +121 -0
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_zone_migration.py +197 -0
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/utils.py +25 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0/watcher_tempest_plugin.egg-info}/PKG-INFO +1 -1
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/SOURCES.txt +6 -2
- watcher_tempest_plugin-3.6.0/watcher_tempest_plugin.egg-info/pbr.json +1 -0
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py +0 -148
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py +0 -93
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_storage_balance.py +0 -120
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_strategies.py +0 -236
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_vm_workload_consolidation.py +0 -151
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_workload_balancing.py +0 -124
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_zone_migration.py +0 -106
- watcher_tempest_plugin-3.5.0/watcher_tempest_plugin.egg-info/pbr.json +0 -1
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.coveragerc +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.mailmap +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.stestr.conf +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.testr.conf +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.zuul.yaml +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/CONTRIBUTING.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/HACKING.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/LICENSE +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/README.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/requirements.txt +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/conf.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/contributing.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/index.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/readme.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/pyproject.toml +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/notes/.placeholder +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/notes/drop-py-2-7-eb1979587d237f5e.yaml +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/notes/drop-python38-support-eeb09a8bc0170ce2.yaml +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/_static/.placeholder +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/_templates/.placeholder +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/conf.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/index.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/unreleased.rst +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/requirements.txt +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/setup.cfg +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/setup.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/test-requirements.txt +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/tox.ini +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/infra_optim_clients.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/plugin.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/base.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/json/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/json/api_microversion_fixture.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/json/client.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/prometheus_client.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/v1/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/v1/json/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/v1/json/client.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/base.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_action.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_action_plan.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_api_discovery.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_audit.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_audit_template.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_goal.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_scoring_engine.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_service.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_strategy.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/base.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_action.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_action_plan.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_audit.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_audit_template.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_goal.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_scoring_engine.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_service.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_strategy.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/__init__.py +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/dependency_links.txt +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/entry_points.txt +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/not-zip-safe +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/requires.txt +0 -0
- {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/top_level.txt +0 -0
|
@@ -40,6 +40,7 @@ digambar <digambarpatil15@yahoo.co.in>
|
|
|
40
40
|
ghanshyam <gmann@ghanshyammann.com>
|
|
41
41
|
gujin <gu.jin@99cloud.net>
|
|
42
42
|
huang.zhiping <huang.zhiping@99cloud.net>
|
|
43
|
+
jgilaber <jgilaber@redhat.com>
|
|
43
44
|
licanwei <li.canwei2@zte.com.cn>
|
|
44
45
|
limin.lc <limin.lc@inspur.com>
|
|
45
46
|
liyou01 <liyou01@inspur.com>
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
CHANGES
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
+
3.6.0
|
|
5
|
+
-----
|
|
6
|
+
|
|
7
|
+
* Skip Storage Capacity test until it's fixed
|
|
8
|
+
* Add tests for extended compute datamodel
|
|
9
|
+
* Remove server\_state check from data\_model test
|
|
10
|
+
* Fail if action plan cannot be execute
|
|
11
|
+
* Remove resource\_setup skip checks
|
|
12
|
+
* Refactor execute\_strategy method into smaller helpers
|
|
13
|
+
* Organize strategy tests and remove duplicated tests
|
|
14
|
+
* Add basic datamodel list tests
|
|
15
|
+
* Add a scenario test with continuous audit
|
|
16
|
+
|
|
4
17
|
3.5.0
|
|
5
18
|
-----
|
|
6
19
|
|
|
7
20
|
* Add stable/2025.2 job
|
|
21
|
+
* Add test for volume retype with zone migration
|
|
8
22
|
* Check for single instance migration to backup node
|
|
9
23
|
* Add Aetos jobs in check and experimental pipelines
|
|
10
24
|
* Add custom flavor and dynamic threshold to workload\_balance tests
|
{watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/config.py
RENAMED
|
@@ -127,4 +127,21 @@ OptimizationGroup = [
|
|
|
127
127
|
help="Upper version of the test target microversion range. "
|
|
128
128
|
"The format is 'X.Y', where 'X' and 'Y' are int values."
|
|
129
129
|
),
|
|
130
|
+
cfg.BoolOpt(
|
|
131
|
+
"run_continuous_audit_tests",
|
|
132
|
+
default=False,
|
|
133
|
+
help="Whether or not to run continuous audit scenario tests.",
|
|
134
|
+
),
|
|
135
|
+
cfg.BoolOpt(
|
|
136
|
+
"run_extended_attributes_tests",
|
|
137
|
+
default=False,
|
|
138
|
+
help="Whether or not to run compute model extended attributes tests. "
|
|
139
|
+
"Note that Watcher also needs to be configured to enable the "
|
|
140
|
+
"collection of extended attributes for the compute model and the "
|
|
141
|
+
"watcher option nova_client.api_version must be set configured "
|
|
142
|
+
"accordingly. Note that some attributes require the model "
|
|
143
|
+
"collector to run and update the model, so make sure that your "
|
|
144
|
+
"`watcher_cluster_data_model_collectors` period configuration is "
|
|
145
|
+
"less than 300 seconds."
|
|
146
|
+
),
|
|
130
147
|
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Copyright 2025 Red Hat, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
# not use this file except in compliance with the License. You may obtain
|
|
5
|
+
# a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
+
# License for the specific language governing permissions and limitations
|
|
13
|
+
# under the License.
|
|
14
|
+
|
|
15
|
+
from tempest.lib import decorators
|
|
16
|
+
from tempest.lib import exceptions
|
|
17
|
+
|
|
18
|
+
from watcher_tempest_plugin.tests.api.admin import base
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestListDataModel(base.BaseInfraOptimTest):
|
|
22
|
+
"""Tests for data models"""
|
|
23
|
+
|
|
24
|
+
min_microversion = "1.3"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def resource_setup(cls):
|
|
28
|
+
super(TestListDataModel, cls).resource_setup()
|
|
29
|
+
|
|
30
|
+
@decorators.attr(type='smoke')
|
|
31
|
+
@decorators.idempotent_id('61b54061-d68c-4179-bb00-d72c97373224')
|
|
32
|
+
def test_list_data_models(self):
|
|
33
|
+
_, body = self.client.list_data_models()
|
|
34
|
+
|
|
35
|
+
self.assertIn('context', body)
|
|
36
|
+
|
|
37
|
+
# NOTE(dviroel): list data model returns at least
|
|
38
|
+
# one element for each compute node. But we don't want to
|
|
39
|
+
# if there is no enabled compute nodes.
|
|
40
|
+
if len(body['context']) > 0:
|
|
41
|
+
context_keys = body['context'][0].keys()
|
|
42
|
+
# assert that the context has some of the expected fields
|
|
43
|
+
# note that since we don't have any instances, we don't have
|
|
44
|
+
# server fields available
|
|
45
|
+
expected_fields = set([
|
|
46
|
+
'node_hostname', 'node_uuid', 'node_vcpus', 'node_memory',
|
|
47
|
+
'node_state', 'node_vcpu_ratio', 'node_vcpu_reserved'])
|
|
48
|
+
self.assertTrue(expected_fields.issubset(set(context_keys)))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestNegativeListDataModel(base.BaseInfraOptimTest):
|
|
52
|
+
"""Negative tests for data models"""
|
|
53
|
+
|
|
54
|
+
min_microversion = "1.3"
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def resource_setup(cls):
|
|
58
|
+
super(TestNegativeListDataModel, cls).resource_setup()
|
|
59
|
+
|
|
60
|
+
@decorators.attr(type='smoke')
|
|
61
|
+
@decorators.idempotent_id('dd0f7313-e170-4de8-b9ce-840ef3cfdd5f')
|
|
62
|
+
def test_list_data_models_storage(self):
|
|
63
|
+
|
|
64
|
+
self.assertRaises(exceptions.NotFound, self.client.list_data_models,
|
|
65
|
+
data_model_type="storage")
|
|
@@ -53,14 +53,26 @@ CONF = config.CONF
|
|
|
53
53
|
# Note that it will not affect the version of the Nova API used by
|
|
54
54
|
# Watcher, only the version used by tempest when used from those tests.
|
|
55
55
|
NOVA_API_VERSION_CREATE_WITH_HOST = '2.74'
|
|
56
|
+
# Nova API version that adds 'pinned_availability_zone' to GET /servers/details
|
|
57
|
+
# response.
|
|
58
|
+
NOVA_API_VERSION_SERVER_PINNED_AZ = '2.96'
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
59
62
|
"""Base class for Infrastructure Optimization API tests."""
|
|
60
63
|
|
|
64
|
+
# Default Goal and Strategy to be defined in each test class that inherit
|
|
65
|
+
# from this base class.
|
|
66
|
+
GOAL = None
|
|
67
|
+
STRATEGY = None
|
|
68
|
+
|
|
61
69
|
min_microversion = None
|
|
62
70
|
max_microversion = manager.LATEST_MICROVERSION
|
|
63
71
|
|
|
72
|
+
# Holds the initial configuration of available compute nodes, to assist
|
|
73
|
+
# the rollback procedure.
|
|
74
|
+
initial_compute_nodes_setup = []
|
|
75
|
+
|
|
64
76
|
# States where the object is waiting for some event to perform a transition
|
|
65
77
|
IDLE_STATES = ('RECOMMENDED', 'FAILED', 'SUCCEEDED', 'CANCELLED')
|
|
66
78
|
# States where the object can only be DELETED (end of its life-cycle)
|
|
@@ -261,6 +273,12 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
261
273
|
return all([ap['state'] in cls.AP_FINISHED_STATES
|
|
262
274
|
for ap in action_plans['action_plans']])
|
|
263
275
|
|
|
276
|
+
def check_min_enabled_compute_nodes(self, min_nodes):
|
|
277
|
+
enabled_compute_nodes = self.get_enabled_compute_nodes()
|
|
278
|
+
msg = ("This test required at least %s enabled compute nodes, but "
|
|
279
|
+
"only %s were found." % (min_nodes, len(enabled_compute_nodes)))
|
|
280
|
+
self.assertGreaterEqual(len(enabled_compute_nodes), min_nodes, msg=msg)
|
|
281
|
+
|
|
264
282
|
def wait_for_all_action_plans_to_finish(self):
|
|
265
283
|
assert test_utils.call_until_true(
|
|
266
284
|
func=self._are_all_action_plans_finished,
|
|
@@ -911,6 +929,11 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
911
929
|
_, audit = cls.client.show_audit(audit_uuid)
|
|
912
930
|
return audit.get('state') in cls.AUDIT_FINISHED_STATES
|
|
913
931
|
|
|
932
|
+
@classmethod
|
|
933
|
+
def is_audit_ongoing(cls, audit_uuid):
|
|
934
|
+
_, audit = cls.client.show_audit(audit_uuid)
|
|
935
|
+
return audit.get('state') == 'ONGOING'
|
|
936
|
+
|
|
914
937
|
# ### ACTION PLANS ### #
|
|
915
938
|
|
|
916
939
|
def delete_action_plan(self, action_plan_uuid):
|
|
@@ -922,6 +945,10 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
922
945
|
resp, _ = self.client.delete_action_plan(action_plan_uuid)
|
|
923
946
|
return resp
|
|
924
947
|
|
|
948
|
+
def has_action_plans(self, audit_uuid=None):
|
|
949
|
+
_, action_plans = self.client.list_action_plans(audit_uuid=audit_uuid)
|
|
950
|
+
return len(action_plans['action_plans']) > 0
|
|
951
|
+
|
|
925
952
|
def has_action_plan_finished(self, action_plan_uuid):
|
|
926
953
|
_, action_plan = self.client.show_action_plan(action_plan_uuid)
|
|
927
954
|
return action_plan.get('state') in self.AP_FINISHED_STATES
|
|
@@ -934,23 +961,36 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
934
961
|
return False
|
|
935
962
|
return True
|
|
936
963
|
|
|
937
|
-
def
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
964
|
+
def has_action_plans_recommended(self, audit_uuid=None):
|
|
965
|
+
_, action_plans = self.client.list_action_plans(audit_uuid=audit_uuid)
|
|
966
|
+
for ap in action_plans['action_plans']:
|
|
967
|
+
_, action_plan = self.client.show_action_plan(ap['uuid'])
|
|
968
|
+
if action_plan.get('state') == 'RECOMMENDED':
|
|
969
|
+
return True
|
|
970
|
+
return False
|
|
971
|
+
|
|
972
|
+
def create_audit_template_for_strategy(self, goal_name=None,
|
|
973
|
+
strategy_name=None):
|
|
974
|
+
"""Create and return an audit template for the given goal/strategy."""
|
|
975
|
+
|
|
976
|
+
goal_name = goal_name or self.GOAL
|
|
977
|
+
strategy_name = strategy_name or self.STRATEGY
|
|
978
|
+
|
|
949
979
|
_, goal = self.client.show_goal(goal_name)
|
|
950
980
|
_, strategy = self.client.show_strategy(strategy_name)
|
|
951
981
|
_, audit_template = self.create_audit_template(
|
|
952
982
|
goal['uuid'], strategy=strategy['uuid'])
|
|
953
983
|
|
|
984
|
+
return audit_template
|
|
985
|
+
|
|
986
|
+
def create_audit_and_wait(self, audit_template_uuid, **audit_kwargs):
|
|
987
|
+
"""Create an audit from template and wait until it finishes.
|
|
988
|
+
|
|
989
|
+
Returns the finished audit object (dict).
|
|
990
|
+
Raises on FAILED/CANCELLED states.
|
|
991
|
+
"""
|
|
992
|
+
# Ensure previous action plans are finished before
|
|
993
|
+
# creating a new audit
|
|
954
994
|
self.assertTrue(test_utils.call_until_true(
|
|
955
995
|
func=functools.partial(
|
|
956
996
|
self.has_action_plans_finished),
|
|
@@ -962,8 +1002,9 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
962
1002
|
state = audit_kwargs.pop('state', None)
|
|
963
1003
|
interval = audit_kwargs.pop('interval', None)
|
|
964
1004
|
parameters = audit_kwargs.pop('parameters', None)
|
|
1005
|
+
|
|
965
1006
|
_, audit = self.create_audit(
|
|
966
|
-
|
|
1007
|
+
audit_template_uuid,
|
|
967
1008
|
audit_type=audit_type,
|
|
968
1009
|
state=state,
|
|
969
1010
|
interval=interval,
|
|
@@ -972,7 +1013,8 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
972
1013
|
try:
|
|
973
1014
|
self.assertTrue(test_utils.call_until_true(
|
|
974
1015
|
func=functools.partial(
|
|
975
|
-
self.has_audit_finished,
|
|
1016
|
+
self.has_audit_finished,
|
|
1017
|
+
audit['uuid']),
|
|
976
1018
|
duration=600,
|
|
977
1019
|
sleep_for=2
|
|
978
1020
|
))
|
|
@@ -980,46 +1022,67 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
980
1022
|
self.fail("The audit has failed!")
|
|
981
1023
|
|
|
982
1024
|
_, finished_audit = self.client.show_audit(audit['uuid'])
|
|
983
|
-
if finished_audit.get('state') in (
|
|
1025
|
+
if finished_audit.get('state') in (
|
|
1026
|
+
'FAILED', 'CANCELLED', 'SUSPENDED'):
|
|
984
1027
|
self.fail("The audit ended in unexpected state: %s!"
|
|
985
1028
|
% finished_audit.get('state'))
|
|
986
1029
|
|
|
1030
|
+
return finished_audit
|
|
1031
|
+
|
|
1032
|
+
def get_action_plan_and_validate_actions(
|
|
1033
|
+
self, audit_uuid, expected_action_types=None):
|
|
1034
|
+
"""Fetch action plan for a finished audit and validate actions.
|
|
1035
|
+
|
|
1036
|
+
Returns the action_plan object (dict).
|
|
1037
|
+
"""
|
|
987
1038
|
_, action_plans = self.client.list_action_plans(
|
|
988
|
-
audit_uuid=
|
|
989
|
-
|
|
1039
|
+
audit_uuid=audit_uuid)
|
|
1040
|
+
action_plan_ref = action_plans['action_plans'][0]
|
|
1041
|
+
_, action_plan = self.client.show_action_plan(
|
|
1042
|
+
action_plan_ref['uuid'])
|
|
990
1043
|
|
|
991
|
-
|
|
992
|
-
created_actions = self.client.list_actions(
|
|
1044
|
+
actions = self.client.list_actions(
|
|
993
1045
|
action_plan_uuid=action_plan["uuid"])[1]['actions']
|
|
994
1046
|
|
|
995
|
-
if
|
|
996
|
-
action_types = {a['action_type'] for a in
|
|
997
|
-
if set(
|
|
998
|
-
self.fail(
|
|
999
|
-
|
|
1047
|
+
if expected_action_types:
|
|
1048
|
+
action_types = {a['action_type'] for a in actions}
|
|
1049
|
+
if set(expected_action_types) != action_types:
|
|
1050
|
+
self.fail(
|
|
1051
|
+
"The audit has found action types %s when expecting %s"
|
|
1052
|
+
% (action_types, expected_action_types)
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
return action_plan, actions
|
|
1000
1056
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1057
|
+
def execute_action_plan_and_validate_states(
|
|
1058
|
+
self, action_plan_uuid,
|
|
1059
|
+
expected_action_plan_states=['SUCCEEDED'],
|
|
1060
|
+
expected_action_states=['SUCCEEDED']):
|
|
1061
|
+
"""Start the action plan execution and wait for completion.
|
|
1005
1062
|
|
|
1063
|
+
Returns the finished action plan object (dict).
|
|
1064
|
+
"""
|
|
1006
1065
|
# Execute the action by changing its state to PENDING
|
|
1007
|
-
_, updated_ap = self.client.start_action_plan(
|
|
1066
|
+
_, updated_ap = self.client.start_action_plan(action_plan_uuid)
|
|
1008
1067
|
|
|
1009
1068
|
self.assertTrue(test_utils.call_until_true(
|
|
1010
1069
|
func=functools.partial(
|
|
1011
|
-
self.has_action_plan_finished,
|
|
1070
|
+
self.has_action_plan_finished,
|
|
1071
|
+
action_plan_uuid
|
|
1072
|
+
),
|
|
1012
1073
|
duration=600,
|
|
1013
1074
|
sleep_for=2
|
|
1014
1075
|
))
|
|
1015
|
-
_, finished_ap = self.client.show_action_plan(
|
|
1016
|
-
_,
|
|
1076
|
+
_, finished_ap = self.client.show_action_plan(action_plan_uuid)
|
|
1077
|
+
_, finished_actions = self.client.list_actions(
|
|
1017
1078
|
action_plan_uuid=finished_ap["uuid"])
|
|
1018
1079
|
self.assertIn(updated_ap['state'], ('PENDING', 'ONGOING'))
|
|
1019
|
-
self.assertIn(finished_ap['state'],
|
|
1080
|
+
self.assertIn(finished_ap['state'], expected_action_plan_states)
|
|
1081
|
+
|
|
1082
|
+
for action in finished_actions['actions']:
|
|
1083
|
+
self.assertIn(action.get('state'), expected_action_states)
|
|
1020
1084
|
|
|
1021
|
-
|
|
1022
|
-
self.assertEqual('SUCCEEDED', action.get('state'))
|
|
1085
|
+
return finished_ap, finished_actions
|
|
1023
1086
|
|
|
1024
1087
|
def check_node_trait(self, node_id, trait):
|
|
1025
1088
|
"""Check if trait is in node traits
|
|
@@ -1070,6 +1133,47 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
|
|
|
1070
1133
|
for s in body.get('context', [])
|
|
1071
1134
|
if 'server_uuid' in s and 'node_hostname' in s]
|
|
1072
1135
|
|
|
1136
|
+
def wait_for_instances_attributes_in_model(self, instances, attributes_map,
|
|
1137
|
+
timeout=300):
|
|
1138
|
+
"""Waits until all instances have attributes updated in the model.
|
|
1139
|
+
|
|
1140
|
+
Builds a list of instances that have all attributes updated in the
|
|
1141
|
+
model. Compare with the list of instances provided as argument.
|
|
1142
|
+
|
|
1143
|
+
:param instances: List of instances to wait for.
|
|
1144
|
+
:param attributes_map: Map withattributes and expected values.
|
|
1145
|
+
:param timeout: Timeout in seconds.
|
|
1146
|
+
|
|
1147
|
+
:raises: Exception if attributes were not updated in the model.
|
|
1148
|
+
"""
|
|
1149
|
+
|
|
1150
|
+
timeout_end = time.time() + timeout
|
|
1151
|
+
|
|
1152
|
+
_, body = self.client.list_data_models(data_model_type="compute")
|
|
1153
|
+
|
|
1154
|
+
# Get all server_uuid from model that have all attributes updated
|
|
1155
|
+
# according to the attributes_map.
|
|
1156
|
+
model_ids = [
|
|
1157
|
+
s['server_uuid']
|
|
1158
|
+
for s in body.get('context', [])
|
|
1159
|
+
if 'server_uuid' in s and all(
|
|
1160
|
+
s.get(k) == v for k, v in attributes_map.items())]
|
|
1161
|
+
|
|
1162
|
+
instance_ids = [i['id'] for i in instances]
|
|
1163
|
+
|
|
1164
|
+
# Check all instances are in the model list built.
|
|
1165
|
+
while (not set(instance_ids) <= set(model_ids) or not model_ids):
|
|
1166
|
+
time.sleep(15)
|
|
1167
|
+
if time.time() >= timeout_end:
|
|
1168
|
+
raise Exception("Attributes were not updated in the model.")
|
|
1169
|
+
|
|
1170
|
+
_, body = self.client.list_data_models(data_model_type="compute")
|
|
1171
|
+
model_ids = [
|
|
1172
|
+
s['server_uuid']
|
|
1173
|
+
for s in body.get('context', [])
|
|
1174
|
+
if 'server_uuid' in s and all(
|
|
1175
|
+
s.get(k) == v for k, v in attributes_map.items())]
|
|
1176
|
+
|
|
1073
1177
|
def wait_delete_instances_from_model(self, timeout=300):
|
|
1074
1178
|
"""Waits until all deleted instaces be removed from model."""
|
|
1075
1179
|
timeout_end = time.time() + timeout
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright 2025 Red Hat, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
# not use this file except in compliance with the License. You may obtain
|
|
5
|
+
# a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
+
# License for the specific language governing permissions and limitations
|
|
13
|
+
# under the License.
|
|
14
|
+
|
|
15
|
+
import functools
|
|
16
|
+
|
|
17
|
+
from tempest import config
|
|
18
|
+
from tempest.lib.common.utils import test_utils
|
|
19
|
+
from tempest.lib import decorators
|
|
20
|
+
|
|
21
|
+
from watcher_tempest_plugin.tests.scenario import base
|
|
22
|
+
|
|
23
|
+
CONF = config.CONF
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestContinuousAudit(base.BaseInfraOptimScenarioTest):
|
|
27
|
+
"""Tests that create continuous audits and execute strategies."""
|
|
28
|
+
|
|
29
|
+
# Minimal version required for list data models
|
|
30
|
+
min_microversion = "1.3"
|
|
31
|
+
# Minimal version required for _create_one_instance_per_host
|
|
32
|
+
compute_min_microversion = base.NOVA_API_VERSION_CREATE_WITH_HOST
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def skip_checks(cls):
|
|
36
|
+
super().skip_checks()
|
|
37
|
+
if not CONF.optimize.run_continuous_audit_tests:
|
|
38
|
+
raise cls.skipException(
|
|
39
|
+
"Continuous audit tests are disabled.")
|
|
40
|
+
|
|
41
|
+
if CONF.compute.min_compute_nodes < 2:
|
|
42
|
+
raise cls.skipException(
|
|
43
|
+
"Less than 2 compute nodes, skipping multinode tests.")
|
|
44
|
+
|
|
45
|
+
if not CONF.compute_feature_enabled.live_migration:
|
|
46
|
+
raise cls.skipException("Live migration is not enabled")
|
|
47
|
+
|
|
48
|
+
@decorators.idempotent_id("bd6a18e9-bd51-4164-9e7f-4f19d9b428ba")
|
|
49
|
+
@decorators.attr(type=['strategy', 'zone_migration'])
|
|
50
|
+
def test_continuous_audit_zone_migration(self):
|
|
51
|
+
"""Create a continuous audit using zone_migration strategy"""
|
|
52
|
+
# NOTE(dviroel): We don't need to start/trigger the action plan to
|
|
53
|
+
# validate continous audits, but we need to create the workload and
|
|
54
|
+
# check if the strategy will get updates from the model.
|
|
55
|
+
self.check_min_enabled_compute_nodes(2)
|
|
56
|
+
self.addCleanup(self.wait_delete_instances_from_model)
|
|
57
|
+
|
|
58
|
+
src_host = self.get_enabled_compute_nodes()[0]['host']
|
|
59
|
+
audit_parameters = {
|
|
60
|
+
"compute_nodes": [{"src_node": src_host}],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
audit_template = self.create_audit_template_for_strategy(
|
|
64
|
+
goal_name="hardware_maintenance", strategy_name="zone_migration")
|
|
65
|
+
|
|
66
|
+
self.assertTrue(test_utils.call_until_true(
|
|
67
|
+
func=functools.partial(
|
|
68
|
+
self.has_action_plans_finished),
|
|
69
|
+
duration=600,
|
|
70
|
+
sleep_for=2
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# 1. Create a continuous audit that should propose
|
|
74
|
+
# SUCCEEDED action plans, without actions, since
|
|
75
|
+
# we don't have any instances to move yet
|
|
76
|
+
# NOTE(dviroel): This is intentional to force a
|
|
77
|
+
# cluster model update from continous audit thread.
|
|
78
|
+
_, audit = self.create_audit(
|
|
79
|
+
audit_template["uuid"],
|
|
80
|
+
audit_type="CONTINUOUS",
|
|
81
|
+
interval="10",
|
|
82
|
+
parameters=audit_parameters)
|
|
83
|
+
|
|
84
|
+
# Wait until it reach the ONGOING state, otherwise fail
|
|
85
|
+
try:
|
|
86
|
+
self.assertTrue(test_utils.call_until_true(
|
|
87
|
+
func=functools.partial(
|
|
88
|
+
self.is_audit_ongoing, audit["uuid"]),
|
|
89
|
+
duration=300,
|
|
90
|
+
sleep_for=2
|
|
91
|
+
))
|
|
92
|
+
except ValueError:
|
|
93
|
+
self.fail("Audit failed to reach ONGOING state.")
|
|
94
|
+
|
|
95
|
+
# Wait for an action plan from this audit
|
|
96
|
+
try:
|
|
97
|
+
self.assertTrue(test_utils.call_until_true(
|
|
98
|
+
func=functools.partial(
|
|
99
|
+
self.has_action_plans, audit["uuid"]),
|
|
100
|
+
duration=300,
|
|
101
|
+
sleep_for=10
|
|
102
|
+
))
|
|
103
|
+
except ValueError:
|
|
104
|
+
self.fail("Audit failed to create an action plan.")
|
|
105
|
+
|
|
106
|
+
_, action_plans = self.client.list_action_plans(
|
|
107
|
+
audit_uuid=audit["uuid"])
|
|
108
|
+
action_plan = action_plans["action_plans"][0]
|
|
109
|
+
|
|
110
|
+
# Since a new run of the continuous audit will change the last
|
|
111
|
+
# action plan to CANCELLED. It is hard to guarantee that we will
|
|
112
|
+
# always have a SUCCEEDED action plan.
|
|
113
|
+
self.assertIn(action_plan['state'], ('SUCCEEDED', 'CANCELLED'))
|
|
114
|
+
|
|
115
|
+
# Action plan should have no actions
|
|
116
|
+
created_actions = self.client.list_actions(
|
|
117
|
+
action_plan_uuid=action_plan["uuid"])[1]["actions"]
|
|
118
|
+
self.assertEqual(len(created_actions), 0)
|
|
119
|
+
|
|
120
|
+
# 2. Create a instance in the host, that is our
|
|
121
|
+
# src_host in zone_migration.
|
|
122
|
+
instance = self._create_instance(host=src_host)
|
|
123
|
+
# wait for compute model updates
|
|
124
|
+
self.wait_for_instances_in_model([instance])
|
|
125
|
+
|
|
126
|
+
# Wait for a first RECOMMENDED action plan from this audit
|
|
127
|
+
try:
|
|
128
|
+
self.assertTrue(test_utils.call_until_true(
|
|
129
|
+
func=functools.partial(
|
|
130
|
+
self.has_action_plans_recommended, audit["uuid"]),
|
|
131
|
+
duration=300,
|
|
132
|
+
sleep_for=10
|
|
133
|
+
))
|
|
134
|
+
except ValueError:
|
|
135
|
+
self.fail("Audit failed to create a RECOMMENDED action plan.")
|
|
136
|
+
|
|
137
|
+
# Update state to CANCELLED, so it can be deleted afterwards
|
|
138
|
+
_, audit = self.client.update_audit(
|
|
139
|
+
audit["uuid"],
|
|
140
|
+
[{"op": "replace", "path": "/state", "value": "CANCELLED"}]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
self.assertTrue(test_utils.call_until_true(
|
|
145
|
+
func=functools.partial(
|
|
146
|
+
self.has_audit_finished, audit["uuid"]),
|
|
147
|
+
duration=300,
|
|
148
|
+
sleep_for=2
|
|
149
|
+
))
|
|
150
|
+
except ValueError:
|
|
151
|
+
self.fail("The audit failed to reach one of finished states.")
|
|
152
|
+
|
|
153
|
+
# 3. Check that the audit created a RECOMMENDED action plan, with
|
|
154
|
+
# a migrate action
|
|
155
|
+
_, action_plans = self.client.list_action_plans(
|
|
156
|
+
audit_uuid=audit["uuid"])
|
|
157
|
+
recommended_aps = [ap for ap in action_plans["action_plans"]
|
|
158
|
+
if ap["state"] == "RECOMMENDED"]
|
|
159
|
+
|
|
160
|
+
# At least one action plan should be a RECOMMENDED one
|
|
161
|
+
self.assertGreaterEqual(len(recommended_aps), 1)
|
|
162
|
+
|
|
163
|
+
# Validate action plan action type
|
|
164
|
+
created_actions = self.client.list_actions(
|
|
165
|
+
action_plan_uuid=recommended_aps[0]["uuid"])[1]["actions"]
|
|
166
|
+
|
|
167
|
+
expected_actions = ["migrate"]
|
|
168
|
+
action_types = {a["action_type"] for a in created_actions}
|
|
169
|
+
if set(expected_actions) != action_types:
|
|
170
|
+
self.fail("The audit has found action types %s when expecting "
|
|
171
|
+
"%s" % (action_types, expected_actions))
|