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.
Files changed (104) hide show
  1. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/AUTHORS +1 -0
  2. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/ChangeLog +14 -0
  3. {watcher_tempest_plugin-3.5.0/watcher_tempest_plugin.egg-info → watcher_tempest_plugin-3.6.0}/PKG-INFO +1 -1
  4. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/config.py +17 -0
  5. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/api/admin/test_data_model.py +65 -0
  6. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/base.py +139 -35
  7. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_continuous_audit.py +171 -0
  8. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_data_model.py +161 -0
  9. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_actuator.py +4 -10
  10. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py +87 -0
  11. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py +61 -0
  12. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_host_maintenance.py +26 -27
  13. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_node_resource_consolidation.py +102 -0
  14. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_storage_balance.py +69 -0
  15. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_strategies_real.py +41 -28
  16. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_vm_workload_consolidation.py +91 -0
  17. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/test_execute_workload_balance.py +29 -23
  18. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_workload_stabilization.py +121 -0
  19. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/tests/scenario/test_execute_zone_migration.py +197 -0
  20. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin/utils.py +25 -0
  21. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0/watcher_tempest_plugin.egg-info}/PKG-INFO +1 -1
  22. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/SOURCES.txt +6 -2
  23. watcher_tempest_plugin-3.6.0/watcher_tempest_plugin.egg-info/pbr.json +1 -0
  24. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py +0 -148
  25. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py +0 -93
  26. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_storage_balance.py +0 -120
  27. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_strategies.py +0 -236
  28. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_vm_workload_consolidation.py +0 -151
  29. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_workload_balancing.py +0 -124
  30. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin/tests/scenario/test_execute_zone_migration.py +0 -106
  31. watcher_tempest_plugin-3.5.0/watcher_tempest_plugin.egg-info/pbr.json +0 -1
  32. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.coveragerc +0 -0
  33. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.mailmap +0 -0
  34. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.stestr.conf +0 -0
  35. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.testr.conf +0 -0
  36. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/.zuul.yaml +0 -0
  37. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/CONTRIBUTING.rst +0 -0
  38. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/HACKING.rst +0 -0
  39. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/LICENSE +0 -0
  40. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/README.rst +0 -0
  41. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/requirements.txt +0 -0
  42. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/conf.py +0 -0
  43. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/contributing.rst +0 -0
  44. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/index.rst +0 -0
  45. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/doc/source/readme.rst +0 -0
  46. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/pyproject.toml +0 -0
  47. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/notes/.placeholder +0 -0
  48. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/notes/drop-py-2-7-eb1979587d237f5e.yaml +0 -0
  49. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/notes/drop-python38-support-eeb09a8bc0170ce2.yaml +0 -0
  50. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/_static/.placeholder +0 -0
  51. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/_templates/.placeholder +0 -0
  52. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/conf.py +0 -0
  53. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/index.rst +0 -0
  54. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/releasenotes/source/unreleased.rst +0 -0
  55. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/requirements.txt +0 -0
  56. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/setup.cfg +0 -0
  57. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/setup.py +0 -0
  58. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/test-requirements.txt +0 -0
  59. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/tox.ini +0 -0
  60. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/__init__.py +0 -0
  61. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/infra_optim_clients.py +0 -0
  62. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/plugin.py +0 -0
  63. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/__init__.py +0 -0
  64. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/base.py +0 -0
  65. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/__init__.py +0 -0
  66. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/__init__.py +0 -0
  67. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/json/__init__.py +0 -0
  68. {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
  69. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/infra_optim/v1/json/client.py +0 -0
  70. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/__init__.py +0 -0
  71. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/prometheus_client.py +0 -0
  72. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/v1/__init__.py +0 -0
  73. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/v1/json/__init__.py +0 -0
  74. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/services/metric/v1/json/client.py +0 -0
  75. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/__init__.py +0 -0
  76. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/__init__.py +0 -0
  77. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/__init__.py +0 -0
  78. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/base.py +0 -0
  79. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_action.py +0 -0
  80. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_action_plan.py +0 -0
  81. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_api_discovery.py +0 -0
  82. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_audit.py +0 -0
  83. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_audit_template.py +0 -0
  84. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_goal.py +0 -0
  85. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_scoring_engine.py +0 -0
  86. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_service.py +0 -0
  87. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/api/admin/test_strategy.py +0 -0
  88. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/__init__.py +0 -0
  89. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/__init__.py +0 -0
  90. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/base.py +0 -0
  91. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_action.py +0 -0
  92. {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
  93. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_audit.py +0 -0
  94. {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
  95. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_goal.py +0 -0
  96. {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
  97. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_service.py +0 -0
  98. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/client_functional/v1/test_strategy.py +0 -0
  99. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin/tests/scenario/__init__.py +0 -0
  100. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/dependency_links.txt +0 -0
  101. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/entry_points.txt +0 -0
  102. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/not-zip-safe +0 -0
  103. {watcher_tempest_plugin-3.5.0 → watcher_tempest_plugin-3.6.0}/watcher_tempest_plugin.egg-info/requires.txt +0 -0
  104. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: watcher-tempest-plugin
3
- Version: 3.5.0
3
+ Version: 3.6.0
4
4
  Summary: Tempest plugin watcher-tempest-plugin
5
5
  Home-page: https://docs.openstack.org/watcher/latest/
6
6
  Author: OpenStack
@@ -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 execute_strategy(self, goal_name, strategy_name,
938
- expected_actions=[], **audit_kwargs):
939
- """Execute an action plan based on the specific strategy
940
-
941
- - create an audit template with the specific strategy
942
- - run the audit to create an action plan
943
- - get the action plan
944
- - Verify that all action types in the expected_actions
945
- list are present in the action plan.
946
- - run the action plan
947
- - get results and make sure it succeeded
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
- audit_template['uuid'],
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, audit['uuid']),
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 ('FAILED', 'CANCELLED', 'SUSPENDED'):
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=audit['uuid'])
989
- action_plan = action_plans['action_plans'][0]
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
- _, action_plan = self.client.show_action_plan(action_plan['uuid'])
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 expected_actions:
996
- action_types = {a['action_type'] for a in created_actions}
997
- if set(expected_actions) != action_types:
998
- self.fail("The audit has found action types %s when expecting "
999
- "%s" % (action_types, expected_actions))
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
- if action_plan['state'] in ('SUPERSEDED', 'SUCCEEDED'):
1002
- # This means the action plan is superseded so we cannot trigger it,
1003
- # or it is empty.
1004
- return
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(action_plan['uuid'])
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, action_plan['uuid']),
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(action_plan['uuid'])
1016
- _, action_list = self.client.list_actions(
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'], ('SUCCEEDED', 'SUPERSEDED'))
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
- for action in action_list['actions']:
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))