Skip to content

Commit fe8aa16

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add action update command to support skipping actions manually"
2 parents 02fcffb + d17bfa0 commit fe8aa16

File tree

18 files changed

+494
-16
lines changed

18 files changed

+494
-16
lines changed

doc/source/cli/details.rst

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ The watcher client is the command-line interface (CLI) for the
2424
Infrastructure Optimization service (watcher) API
2525
and its extensions.
2626

27-
This chapter documents :command:`watcher` version ``1.3.0``.
27+
This chapter documents watcherclient version ``4.9.0``.
2828

2929
For help on a specific :command:`watcher` command, enter:
3030

@@ -214,6 +214,37 @@ Show detailed information about a given action.
214214
``-h, --help``
215215
show this help message and exit
216216

217+
.. _watcher_action_update:
218+
219+
watcher action update
220+
---------------------
221+
222+
.. code-block:: console
223+
224+
usage: watcher action update [-h] [-f {html,json,shell,table,value,yaml}]
225+
[-c COLUMN] [--max-width <integer>] [--fit-width]
226+
[--print-empty] [--noindent] [--prefix PREFIX]
227+
[--state <state>] [--reason <reason>] <action>
228+
229+
Update action command.
230+
231+
**Positional arguments:**
232+
233+
``<action>``
234+
UUID of the action
235+
236+
**Optional arguments:**
237+
238+
``-h, --help``
239+
show this help message and exit
240+
241+
``--state <state>``
242+
New state for the action (e.g., SKIPPED)
243+
244+
``--reason <reason>``
245+
Reason for the action state change.
246+
247+
217248
.. _watcher_actionplan_cancel:
218249

219250
watcher actionplan cancel
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
features:
3+
- |
4+
Added support for updating action state through the new
5+
``openstack optimize action update`` command. This feature allows
6+
operators to manually change action states. The command
7+
supports the following options:
8+
9+
* ``--state <state>`` - New state for the action (required)
10+
* ``--reason <reason>`` - Optional reason for the state change
11+
12+
Currently, the only use case for this update is to Skip an action
13+
before starting an Action Plan with an optional reason by setting
14+
the state to SKIPPED:
15+
16+
$ openstack optimize action update --state SKIPPED --reason "Manual skip" <action-uuid>
17+
18+
This feature requires Watcher API microversion 1.5 or higher.
19+
20+
upgrade:
21+
- |
22+
The maximum supported API version has been increased from 1.1 to 1.5
23+
to support the new action update functionality. This change maintains
24+
full backward compatibility with existing deployments.

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ openstack.infra_optim.v1 =
6060

6161
optimize_action_show = watcherclient.v1.action_shell:ShowAction
6262
optimize_action_list = watcherclient.v1.action_shell:ListAction
63+
optimize_action_update = watcherclient.v1.action_shell:UpdateAction
6364

6465
optimize_scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine
6566
optimize_scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine
@@ -99,6 +100,7 @@ watcherclient.v1 =
99100

100101
action_show = watcherclient.v1.action_shell:ShowAction
101102
action_list = watcherclient.v1.action_shell:ListAction
103+
action_update = watcherclient.v1.action_shell:UpdateAction
102104

103105
scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine
104106
scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine

watcherclient/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@
3232
# when client supported the max version, and bumped sequentially, otherwise
3333
# the client may break due to server side new version may include some
3434
# backward incompatible change.
35-
API_MAX_VERSION = api_versioning.APIVersion("1.1")
35+
API_MAX_VERSION = api_versioning.APIVersion("1.5")

watcherclient/common/api_versioning.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
MINOR_1_START_END_TIMING = '1.1'
3030
MINOR_2_FORCE_AUDIT = '1.2'
31+
MINOR_5_ACTION_UPDATE = '1.5'
3132
HEADER_NAME = "OpenStack-API-Version"
3233
# key is a deprecated version and value is an alternative version.
3334
DEPRECATED_VERSIONS = {}
@@ -54,6 +55,15 @@ def launch_audit_forced(requested_version):
5455
APIVersion(MINOR_2_FORCE_AUDIT))
5556

5657

58+
def action_update_supported(requested_version):
59+
"""Check if we should support action update functionality.
60+
61+
Version 1.5 of the API added support for updating action state.
62+
"""
63+
return (APIVersion(requested_version) >=
64+
APIVersion(MINOR_5_ACTION_UPDATE))
65+
66+
5767
class APIVersion(object):
5868
"""This class represents an API Version Request.
5969

watcherclient/common/httpclient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
# Record the latest version that this client was tested with.
4242
DEFAULT_VER = '1.latest'
4343
# Minor version 4 for adding webhook API
44-
LAST_KNOWN_API_VERSION = 4
44+
LAST_KNOWN_API_VERSION = 5
4545
LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
4646

4747
LOG = logging.getLogger(__name__)

watcherclient/tests/client_functional/v1/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def credentials():
3434
return [x for sub in creds_dict.items() for x in sub]
3535

3636

37-
def execute(cmd, fail_ok=False, merge_stderr=False):
37+
def execute(cmd, fail_ok=False, merge_stderr=True):
3838
"""Executes specified command for the given action."""
3939
cmdlist = shlex.split(cmd)
4040
cmdlist.extend(credentials())

watcherclient/tests/client_functional/v1/test_action.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,142 @@ def test_action_show(self):
7979
self.assertIn(action_uuid, action)
8080
self.assert_table_structure([action],
8181
self.detailed_list_fields)
82+
83+
84+
class ActionUpdateTests(base.TestCase):
85+
"""Functional tests for action update functionality."""
86+
87+
# Use API version 1.5 for action update tests
88+
api_version = 1.5
89+
dummy_name = 'dummy'
90+
audit_template_name = 'b' + uuidutils.generate_uuid()
91+
audit_uuid = None
92+
action_uuid = None
93+
94+
@classmethod
95+
def setUpClass(cls):
96+
# Create audit template
97+
template_raw_output = cls.watcher(
98+
'audittemplate create %s dummy -s dummy' % cls.audit_template_name)
99+
template_output = cls.parse_show_as_object(template_raw_output)
100+
101+
# Create audit
102+
audit_output = cls.parse_show_as_object(cls.watcher(
103+
'audit create -a %s' % template_output['Name']))
104+
cls.audit_uuid = audit_output['UUID']
105+
106+
# Wait for audit to complete
107+
audit_created = test_utils.call_until_true(
108+
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
109+
duration=600,
110+
sleep_for=2)
111+
if not audit_created:
112+
raise Exception('Audit has not been succeeded')
113+
114+
# Get an action to test updates on
115+
action_list = cls.parse_show(cls.watcher('action list --audit %s'
116+
% cls.audit_uuid))
117+
if action_list:
118+
cls.action_uuid = list(action_list[0])[0]
119+
120+
@classmethod
121+
def tearDownClass(cls):
122+
# Clean up: Delete Action Plan and all related actions
123+
if cls.audit_uuid:
124+
output = cls.parse_show(
125+
cls.watcher('actionplan list --audit %s' % cls.audit_uuid))
126+
if output:
127+
action_plan_uuid = list(output[0])[0]
128+
raw_output = cls.watcher(
129+
'actionplan delete %s' % action_plan_uuid)
130+
cls.assertOutput('', raw_output)
131+
132+
# Delete audit
133+
raw_output = cls.watcher('audit delete %s' % cls.audit_uuid)
134+
cls.assertOutput('', raw_output)
135+
136+
# Delete template
137+
raw_output = cls.watcher(
138+
'audittemplate delete %s' % cls.audit_template_name)
139+
cls.assertOutput('', raw_output)
140+
141+
def test_action_update_with_state_and_reason(self):
142+
"""Test updating action state with reason using API 1.5"""
143+
if not self.action_uuid:
144+
self.skipTest("No actions available for testing")
145+
146+
# Update action state to SKIPPED with reason
147+
raw_output = self.watcher(
148+
'action update --state SKIPPED --reason "Functional test skip" %s'
149+
% self.action_uuid)
150+
151+
# Verify the action was updated
152+
action = self.parse_show_as_object(
153+
self.watcher('action show %s' % self.action_uuid))
154+
self.assertEqual('SKIPPED', action['State'])
155+
self.assertEqual('Action skipped by user. Reason: Functional test '
156+
'skip', action['Status Message'])
157+
158+
# Verify output contains the action UUID
159+
self.assertIn(self.action_uuid, raw_output)
160+
161+
def test_action_update_with_state_only(self):
162+
"""Test updating action state without reason"""
163+
if not self.action_uuid:
164+
self.skipTest("No actions available for testing")
165+
166+
# Update action state to SKIPPED without reason
167+
raw_output = self.watcher(
168+
'action update --state SKIPPED %s' % self.action_uuid)
169+
170+
# Verify the action was updated
171+
action = self.parse_show_as_object(
172+
self.watcher('action show %s' % self.action_uuid))
173+
self.assertEqual('SKIPPED', action['State'])
174+
175+
# Verify output contains the action UUID
176+
self.assertIn(self.action_uuid, raw_output)
177+
178+
def test_action_update_missing_state_fails(self):
179+
"""Test that action update fails when no state is provided"""
180+
if not self.action_uuid:
181+
self.skipTest("No actions available for testing")
182+
183+
# This should fail because --state is required
184+
raw_output = self.watcher(
185+
'action update %s' % self.action_uuid, fail_ok=True)
186+
187+
# Should contain error message about missing state
188+
self.assertIn(
189+
'At least one field update is required for this operation',
190+
raw_output)
191+
192+
def test_action_update_nonexistent_action_fails(self):
193+
"""Test that action update fails for non-existent action"""
194+
fake_uuid = uuidutils.generate_uuid()
195+
196+
# This should fail because the action doesn't exist
197+
raw_output = self.watcher(
198+
'action update --state SKIPPED %s' % fake_uuid, fail_ok=True)
199+
200+
# Should contain error message about action not found
201+
self.assertIn('404', raw_output)
202+
203+
204+
class ActionUpdateApiVersionTests(base.TestCase):
205+
"""Test action update functionality with different API versions."""
206+
207+
# Use API version 1.0 to test version checking
208+
api_version = 1.0
209+
210+
def test_action_update_unsupported_api_version(self):
211+
"""Test that action update fails with API version < 1.5"""
212+
fake_uuid = uuidutils.generate_uuid()
213+
214+
# This should fail because API version 1.0 doesn't support updates
215+
raw_output = self.watcher(
216+
'action update --state SKIPPED %s' % fake_uuid, fail_ok=True)
217+
218+
# Should contain error message about unsupported API version
219+
self.assertIn('not supported in API version', raw_output)
220+
self.assertIn('Minimum required version is 1.5', raw_output)

watcherclient/tests/unit/common/test_api_versioning.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,22 @@ def test_major_and_minor_parts_is_presented(self, mock_apiversion):
148148
self.assertEqual(mock_apiversion.return_value,
149149
api_versioning.get_api_version(version))
150150
mock_apiversion.assert_called_once_with(version)
151+
152+
153+
class APIVersionFunctionsTestCase(utils.BaseTestCase):
154+
def test_action_update_supported_true(self):
155+
# Test versions >= 1.5 support action update
156+
self.assertTrue(api_versioning.action_update_supported("1.5"))
157+
self.assertTrue(api_versioning.action_update_supported("1.6"))
158+
self.assertTrue(api_versioning.action_update_supported("2.0"))
159+
160+
def test_action_update_supported_false(self):
161+
# Test versions < 1.5 do not support action update
162+
self.assertFalse(api_versioning.action_update_supported("1.0"))
163+
self.assertFalse(api_versioning.action_update_supported("1.1"))
164+
self.assertFalse(api_versioning.action_update_supported("1.4"))
165+
166+
def test_action_update_supported_edge_case(self):
167+
# Test exact boundary
168+
self.assertTrue(api_versioning.action_update_supported("1.5"))
169+
self.assertFalse(api_versioning.action_update_supported("1.4"))

watcherclient/tests/unit/v1/test_action.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
{},
9393
None,
9494
),
95+
'PATCH': (
96+
{},
97+
ACTION1,
98+
),
9599
},
96100
'/v1/actions/detail?action_plan_uuid=%s' % ACTION1['action_plan']:
97101
{
@@ -264,3 +268,12 @@ def test_actions_show(self):
264268
self.assertEqual(ACTION1['uuid'], action.uuid)
265269
self.assertEqual(ACTION1['action_plan'], action.action_plan)
266270
self.assertEqual(ACTION1['next'], action.next)
271+
272+
def test_actions_update(self):
273+
patch = [{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'}]
274+
action = self.mgr.update(ACTION1['uuid'], patch)
275+
expect = [
276+
('PATCH', '/v1/actions/%s' % ACTION1['uuid'], {}, patch),
277+
]
278+
self.assertEqual(expect, self.api.calls)
279+
self.assertEqual(ACTION1['uuid'], action.uuid)

0 commit comments

Comments
 (0)