Add action update command to support skipping actions manually

This commit introduces the "openstack optimize action update" command
that allows cloud admins to manually update action states. The only
current use case is to mark actions as SKIPPED before starting an
action plan.

Additionally, the option `--reason` can be used to provide a text that
will be stored as part of the status_message field.

Command usage:
  openstack optimize action update --state SKIPPED --reason "reason" <uuid>

The feature requires Watcher API microversion 1.5 or higher and includes
automatic version checking.

In order to assert specific strings in stderr, I'm enabling
`merge_stderr` option by default in execute. It's totally backwards
compatible, so I'm not parametrizing it.

Implements: blueprint add-skip-actions

Assisted-By: Claude (claude-sonnet-4)

Depends-On: https://review.opendev.org/c/openstack/watcher/+/955753/
Change-Id: Ice88c0ab58c0cfd784c707620da89a891055ffc2
Signed-off-by: Alfredo Moralejo <amoralej@redhat.com>
This commit is contained in:
Alfredo Moralejo
2025-08-08 11:33:57 +02:00
committed by Douglas Viroel
parent 489d8c3e74
commit d17bfa04ad
18 changed files with 494 additions and 16 deletions

View File

@@ -24,7 +24,7 @@ The watcher client is the command-line interface (CLI) for the
Infrastructure Optimization service (watcher) API
and its extensions.
This chapter documents :command:`watcher` version ``1.3.0``.
This chapter documents watcherclient version ``4.9.0``.
For help on a specific :command:`watcher` command, enter:
@@ -214,6 +214,37 @@ Show detailed information about a given action.
``-h, --help``
show this help message and exit
.. _watcher_action_update:
watcher action update
---------------------
.. code-block:: console
usage: watcher action update [-h] [-f {html,json,shell,table,value,yaml}]
[-c COLUMN] [--max-width <integer>] [--fit-width]
[--print-empty] [--noindent] [--prefix PREFIX]
[--state <state>] [--reason <reason>] <action>
Update action command.
**Positional arguments:**
``<action>``
UUID of the action
**Optional arguments:**
``-h, --help``
show this help message and exit
``--state <state>``
New state for the action (e.g., SKIPPED)
``--reason <reason>``
Reason for the action state change.
.. _watcher_actionplan_cancel:
watcher actionplan cancel

View File

@@ -0,0 +1,24 @@
---
features:
- |
Added support for updating action state through the new
``openstack optimize action update`` command. This feature allows
operators to manually change action states. The command
supports the following options:
* ``--state <state>`` - New state for the action (required)
* ``--reason <reason>`` - Optional reason for the state change
Currently, the only use case for this update is to Skip an action
before starting an Action Plan with an optional reason by setting
the state to SKIPPED:
$ openstack optimize action update --state SKIPPED --reason "Manual skip" <action-uuid>
This feature requires Watcher API microversion 1.5 or higher.
upgrade:
- |
The maximum supported API version has been increased from 1.1 to 1.5
to support the new action update functionality. This change maintains
full backward compatibility with existing deployments.

View File

@@ -60,6 +60,7 @@ openstack.infra_optim.v1 =
optimize_action_show = watcherclient.v1.action_shell:ShowAction
optimize_action_list = watcherclient.v1.action_shell:ListAction
optimize_action_update = watcherclient.v1.action_shell:UpdateAction
optimize_scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine
optimize_scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine
@@ -99,6 +100,7 @@ watcherclient.v1 =
action_show = watcherclient.v1.action_shell:ShowAction
action_list = watcherclient.v1.action_shell:ListAction
action_update = watcherclient.v1.action_shell:UpdateAction
scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine
scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine

View File

@@ -32,4 +32,4 @@ API_MIN_VERSION = api_versioning.APIVersion("1.0")
# when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some
# backward incompatible change.
API_MAX_VERSION = api_versioning.APIVersion("1.1")
API_MAX_VERSION = api_versioning.APIVersion("1.5")

View File

@@ -28,6 +28,7 @@ if not LOG.handlers:
MINOR_1_START_END_TIMING = '1.1'
MINOR_2_FORCE_AUDIT = '1.2'
MINOR_5_ACTION_UPDATE = '1.5'
HEADER_NAME = "OpenStack-API-Version"
# key is a deprecated version and value is an alternative version.
DEPRECATED_VERSIONS = {}
@@ -54,6 +55,15 @@ def launch_audit_forced(requested_version):
APIVersion(MINOR_2_FORCE_AUDIT))
def action_update_supported(requested_version):
"""Check if we should support action update functionality.
Version 1.5 of the API added support for updating action state.
"""
return (APIVersion(requested_version) >=
APIVersion(MINOR_5_ACTION_UPDATE))
class APIVersion(object):
"""This class represents an API Version Request.

View File

@@ -41,7 +41,7 @@ from watcherclient import exceptions
# Record the latest version that this client was tested with.
DEFAULT_VER = '1.latest'
# Minor version 4 for adding webhook API
LAST_KNOWN_API_VERSION = 4
LAST_KNOWN_API_VERSION = 5
LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
LOG = logging.getLogger(__name__)

View File

@@ -34,7 +34,7 @@ def credentials():
return [x for sub in creds_dict.items() for x in sub]
def execute(cmd, fail_ok=False, merge_stderr=False):
def execute(cmd, fail_ok=False, merge_stderr=True):
"""Executes specified command for the given action."""
cmdlist = shlex.split(cmd)
cmdlist.extend(credentials())

View File

@@ -79,3 +79,142 @@ class ActionTests(base.TestCase):
self.assertIn(action_uuid, action)
self.assert_table_structure([action],
self.detailed_list_fields)
class ActionUpdateTests(base.TestCase):
"""Functional tests for action update functionality."""
# Use API version 1.5 for action update tests
api_version = 1.5
dummy_name = 'dummy'
audit_template_name = 'b' + uuidutils.generate_uuid()
audit_uuid = None
action_uuid = None
@classmethod
def setUpClass(cls):
# Create audit template
template_raw_output = cls.watcher(
'audittemplate create %s dummy -s dummy' % cls.audit_template_name)
template_output = cls.parse_show_as_object(template_raw_output)
# Create audit
audit_output = cls.parse_show_as_object(cls.watcher(
'audit create -a %s' % template_output['Name']))
cls.audit_uuid = audit_output['UUID']
# Wait for audit to complete
audit_created = test_utils.call_until_true(
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
duration=600,
sleep_for=2)
if not audit_created:
raise Exception('Audit has not been succeeded')
# Get an action to test updates on
action_list = cls.parse_show(cls.watcher('action list --audit %s'
% cls.audit_uuid))
if action_list:
cls.action_uuid = list(action_list[0])[0]
@classmethod
def tearDownClass(cls):
# Clean up: Delete Action Plan and all related actions
if cls.audit_uuid:
output = cls.parse_show(
cls.watcher('actionplan list --audit %s' % cls.audit_uuid))
if output:
action_plan_uuid = list(output[0])[0]
raw_output = cls.watcher(
'actionplan delete %s' % action_plan_uuid)
cls.assertOutput('', raw_output)
# Delete audit
raw_output = cls.watcher('audit delete %s' % cls.audit_uuid)
cls.assertOutput('', raw_output)
# Delete template
raw_output = cls.watcher(
'audittemplate delete %s' % cls.audit_template_name)
cls.assertOutput('', raw_output)
def test_action_update_with_state_and_reason(self):
"""Test updating action state with reason using API 1.5"""
if not self.action_uuid:
self.skipTest("No actions available for testing")
# Update action state to SKIPPED with reason
raw_output = self.watcher(
'action update --state SKIPPED --reason "Functional test skip" %s'
% self.action_uuid)
# Verify the action was updated
action = self.parse_show_as_object(
self.watcher('action show %s' % self.action_uuid))
self.assertEqual('SKIPPED', action['State'])
self.assertEqual('Action skipped by user. Reason: Functional test '
'skip', action['Status Message'])
# Verify output contains the action UUID
self.assertIn(self.action_uuid, raw_output)
def test_action_update_with_state_only(self):
"""Test updating action state without reason"""
if not self.action_uuid:
self.skipTest("No actions available for testing")
# Update action state to SKIPPED without reason
raw_output = self.watcher(
'action update --state SKIPPED %s' % self.action_uuid)
# Verify the action was updated
action = self.parse_show_as_object(
self.watcher('action show %s' % self.action_uuid))
self.assertEqual('SKIPPED', action['State'])
# Verify output contains the action UUID
self.assertIn(self.action_uuid, raw_output)
def test_action_update_missing_state_fails(self):
"""Test that action update fails when no state is provided"""
if not self.action_uuid:
self.skipTest("No actions available for testing")
# This should fail because --state is required
raw_output = self.watcher(
'action update %s' % self.action_uuid, fail_ok=True)
# Should contain error message about missing state
self.assertIn(
'At least one field update is required for this operation',
raw_output)
def test_action_update_nonexistent_action_fails(self):
"""Test that action update fails for non-existent action"""
fake_uuid = uuidutils.generate_uuid()
# This should fail because the action doesn't exist
raw_output = self.watcher(
'action update --state SKIPPED %s' % fake_uuid, fail_ok=True)
# Should contain error message about action not found
self.assertIn('404', raw_output)
class ActionUpdateApiVersionTests(base.TestCase):
"""Test action update functionality with different API versions."""
# Use API version 1.0 to test version checking
api_version = 1.0
def test_action_update_unsupported_api_version(self):
"""Test that action update fails with API version < 1.5"""
fake_uuid = uuidutils.generate_uuid()
# This should fail because API version 1.0 doesn't support updates
raw_output = self.watcher(
'action update --state SKIPPED %s' % fake_uuid, fail_ok=True)
# Should contain error message about unsupported API version
self.assertIn('not supported in API version', raw_output)
self.assertIn('Minimum required version is 1.5', raw_output)

View File

@@ -148,3 +148,22 @@ class GetAPIVersionTestCase(utils.BaseTestCase):
self.assertEqual(mock_apiversion.return_value,
api_versioning.get_api_version(version))
mock_apiversion.assert_called_once_with(version)
class APIVersionFunctionsTestCase(utils.BaseTestCase):
def test_action_update_supported_true(self):
# Test versions >= 1.5 support action update
self.assertTrue(api_versioning.action_update_supported("1.5"))
self.assertTrue(api_versioning.action_update_supported("1.6"))
self.assertTrue(api_versioning.action_update_supported("2.0"))
def test_action_update_supported_false(self):
# Test versions < 1.5 do not support action update
self.assertFalse(api_versioning.action_update_supported("1.0"))
self.assertFalse(api_versioning.action_update_supported("1.1"))
self.assertFalse(api_versioning.action_update_supported("1.4"))
def test_action_update_supported_edge_case(self):
# Test exact boundary
self.assertTrue(api_versioning.action_update_supported("1.5"))
self.assertFalse(api_versioning.action_update_supported("1.4"))

View File

@@ -92,6 +92,10 @@ fake_responses = {
{},
None,
),
'PATCH': (
{},
ACTION1,
),
},
'/v1/actions/detail?action_plan_uuid=%s' % ACTION1['action_plan']:
{
@@ -264,3 +268,12 @@ class ActionManagerTest(testtools.TestCase):
self.assertEqual(ACTION1['uuid'], action.uuid)
self.assertEqual(ACTION1['action_plan'], action.action_plan)
self.assertEqual(ACTION1['next'], action.next)
def test_actions_update(self):
patch = [{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'}]
action = self.mgr.update(ACTION1['uuid'], patch)
expect = [
('PATCH', '/v1/actions/%s' % ACTION1['uuid'], {}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(ACTION1['uuid'], action.uuid)

View File

@@ -79,8 +79,9 @@ class ActionPlanShellTest(base.CommandTestCase):
FIELD_LABELS = resource_fields.ACTION_PLAN_FIELD_LABELS
GLOBAL_EFFICACY_FIELDS = resource_fields.GLOBAL_EFFICACY_FIELDS
def setUp(self):
super(self.__class__, self).setUp()
def setUp(self, os_infra_optim_api_version='1.0'):
super(ActionPlanShellTest, self).setUp(
os_infra_optim_api_version=os_infra_optim_api_version)
p_audit_manager = mock.patch.object(resource, 'AuditManager')
p_audit_template_manager = mock.patch.object(
@@ -336,3 +337,14 @@ class ActionPlanShellTest(base.CommandTestCase):
self.assertEqual(1, exit_code)
self.assertEqual('', result)
class ActionPlanShellTest15(ActionPlanShellTest):
def setUp(self):
super(ActionPlanShellTest15, self).setUp(
os_infra_optim_api_version='1.5')
v15 = dict(status_message=None)
for action_plan in (ACTION_PLAN_1, ACTION_PLAN_2):
action_plan.update(v15)
self.FIELDS.extend(['status_message'])
self.FIELD_LABELS.extend(['Status Message'])

View File

@@ -15,6 +15,7 @@
import datetime
import io
import unittest
from unittest import mock
from watcherclient import exceptions
@@ -78,8 +79,9 @@ class ActionShellTest(base.CommandTestCase):
FIELDS = resource_fields.ACTION_FIELDS
FIELD_LABELS = resource_fields.ACTION_FIELD_LABELS
def setUp(self):
super(self.__class__, self).setUp()
def setUp(self, os_infra_optim_api_version='1.0'):
super(ActionShellTest, self).setUp(
os_infra_optim_api_version=os_infra_optim_api_version)
p_action_manager = mock.patch.object(resource, 'ActionManager')
p_action_plan_manager = mock.patch.object(
@@ -176,3 +178,106 @@ class ActionShellTest(base.CommandTestCase):
self.assertEqual(1, exit_code)
self.assertEqual('', result)
def test_do_action_update_unsupported_version(self):
exit_code, result = self.run_cmd(
'action update --state SKIPPED '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588',
formatting=None)
self.assertEqual(1, exit_code)
self.assertEqual('', result)
class ActionShellTest15(ActionShellTest):
def setUp(self):
super(ActionShellTest15, self).setUp(os_infra_optim_api_version='1.5')
v15 = dict(status_message=None)
for action in (ACTION_1, ACTION_2, ACTION_3):
action.update(v15)
self.FIELDS.extend(['status_message'])
self.FIELD_LABELS.extend(['Status Message'])
def test_do_action_update_with_state_only(self):
action = resource.Action(mock.Mock(), ACTION_1)
self.m_action_mgr.update.return_value = action
exit_code, result = self.run_cmd(
'action update --state SKIPPED '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(action, self.FIELDS, self.FIELD_LABELS),
result)
expected_patch = [
{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'}
]
self.m_action_mgr.update.assert_called_once_with(
'770ef053-ecb3-48b0-85b5-d55a2dbc6588', expected_patch)
def test_do_action_update_with_state_and_reason(self):
action = resource.Action(mock.Mock(), ACTION_1)
self.m_action_mgr.update.return_value = action
exit_code, result = self.run_cmd(
'action update --state SKIPPED --reason "Manual skip" '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(action, self.FIELDS, self.FIELD_LABELS),
result)
expected_patch = [
{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'},
{'op': 'replace', 'path': '/status_message',
'value': 'Manual skip'}
]
self.m_action_mgr.update.assert_called_once_with(
'770ef053-ecb3-48b0-85b5-d55a2dbc6588', expected_patch)
def test_do_action_update_with_reason_only(self):
action = resource.Action(mock.Mock(), ACTION_1)
self.m_action_mgr.update.return_value = action
exit_code, result = self.run_cmd(
'action update --reason "Manual skip" '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(action, self.FIELDS, self.FIELD_LABELS),
result)
expected_patch = [
{'op': 'replace', 'path': '/status_message',
'value': 'Manual skip'}
]
self.m_action_mgr.update.assert_called_once_with(
'770ef053-ecb3-48b0-85b5-d55a2dbc6588', expected_patch)
def test_do_action_update_no_fields_to_update(self):
exit_code, result = self.run_cmd(
'action update 770ef053-ecb3-48b0-85b5-d55a2dbc6588',
formatting=None)
self.assertEqual(1, exit_code)
self.assertEqual('', result)
def test_do_action_update_action_not_found(self):
self.m_action_mgr.update.side_effect = exceptions.HTTPNotFound
exit_code, result = self.run_cmd(
'action update --state SKIPPED not_found_uuid',
formatting=None)
self.assertEqual(1, exit_code)
self.assertEqual('', result)
@unittest.skip("Action update is supported in API version 1.5")
def test_do_action_update_unsupported_version(self):
pass

View File

@@ -494,8 +494,9 @@ class AuditShellTestv11(AuditShellTest):
class AuditShellTestv12(AuditShellTest):
def setUp(self):
super(AuditShellTestv12, self).setUp(os_infra_optim_api_version='1.2')
def setUp(self, os_infra_optim_api_version='1.2'):
super(AuditShellTestv12, self).setUp(
os_infra_optim_api_version=os_infra_optim_api_version)
v11 = dict(start_time=None, end_time=None)
v12 = dict(force=False)
for audit in (self.AUDIT_1, self.AUDIT_2, self.AUDIT_3):
@@ -697,3 +698,13 @@ class AuditShellTestv12(AuditShellTest):
name='my_audit',
force=False
)
class AuditShellTestv15(AuditShellTestv12):
def setUp(self):
super(AuditShellTestv15, self).setUp(os_infra_optim_api_version='1.5')
v15 = dict(status_message=None)
for audit in (self.AUDIT_1, self.AUDIT_2, self.AUDIT_3):
audit.update(v15)
self.FIELDS.extend(['status_message'])
self.FIELD_LABELS.extend(['Status Message'])

View File

@@ -83,3 +83,6 @@ class ActionManager(base.Manager):
return self._list(self._path(action_id))[0]
except IndexError:
return None
def update(self, action_id, patch):
return self._update(self._path(action_id), patch)

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import io
from cliff.formatters import yaml_format
@@ -20,12 +21,23 @@ from osc_lib import utils
from oslo_utils import uuidutils
from watcherclient._i18n import _
from watcherclient.common import api_versioning
from watcherclient.common import command
from watcherclient.common import utils as common_utils
from watcherclient import exceptions
from watcherclient.v1 import resource_fields as res_fields
def drop_unsupported_field(app_args, fields, field_labels):
fields = copy.copy(fields)
field_labels = copy.copy(field_labels)
api_ver = app_args.os_infra_optim_api_version
if not api_versioning.action_update_supported(api_ver):
fields.remove('status_message')
field_labels.remove('Status Message')
return fields, field_labels
def format_global_efficacy(global_efficacy):
formatted_global_eff = {}
for eff in global_efficacy:
@@ -101,6 +113,8 @@ class ShowActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)
@@ -178,6 +192,8 @@ class ListActionPlan(command.Lister):
if parsed_args.detail:
fields = res_fields.ACTION_PLAN_FIELDS
field_labels = res_fields.ACTION_PLAN_FIELD_LABELS
fields, field_labels = drop_unsupported_field(
self.app_args, fields, field_labels)
else:
fields = res_fields.ACTION_PLAN_SHORT_LIST_FIELDS
field_labels = res_fields.ACTION_PLAN_SHORT_LIST_FIELD_LABELS
@@ -239,6 +255,8 @@ class UpdateActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)
@@ -265,6 +283,8 @@ class StartActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)
@@ -314,5 +334,7 @@ class CancelActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)

View File

@@ -13,15 +13,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from osc_lib import utils
from oslo_utils import uuidutils
from watcherclient._i18n import _
from watcherclient.common import api_versioning
from watcherclient.common import command
from watcherclient.common import utils as common_utils
from watcherclient import exceptions
from watcherclient.v1 import resource_fields as res_fields
def drop_unsupported_field(app_args, fields, field_labels):
fields = copy.copy(fields)
field_labels = copy.copy(field_labels)
api_ver = app_args.os_infra_optim_api_version
if not api_versioning.action_update_supported(api_ver):
fields.remove('status_message')
field_labels.remove('Status Message')
return fields, field_labels
class ShowAction(command.ShowOne):
"""Show detailed information about a given action."""
@@ -44,6 +58,8 @@ class ShowAction(command.ShowOne):
columns = res_fields.ACTION_FIELDS
column_headers = res_fields.ACTION_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action, columns)
@@ -104,6 +120,8 @@ class ListAction(command.Lister):
if parsed_args.detail:
fields = res_fields.ACTION_FIELDS
field_labels = res_fields.ACTION_FIELD_LABELS
fields, field_labels = drop_unsupported_field(
self.app_args, fields, field_labels)
else:
fields = res_fields.ACTION_SHORT_LIST_FIELDS
field_labels = res_fields.ACTION_SHORT_LIST_FIELD_LABELS
@@ -119,3 +137,67 @@ class ListAction(command.Lister):
return (field_labels,
(utils.get_item_properties(item, fields) for item in data))
class UpdateAction(command.ShowOne):
"""Update action command."""
def get_parser(self, prog_name):
parser = super(UpdateAction, self).get_parser(prog_name)
parser.add_argument(
'action',
metavar='<action>',
help=_('UUID of the action'))
parser.add_argument(
'--state',
metavar='<state>',
help=_('New state for the action (e.g., SKIPPED)'))
parser.add_argument(
'--reason',
metavar='<reason>',
help=_('Reason for the state change'))
return parser
def take_action(self, parsed_args):
client = getattr(self.app.client_manager, "infra-optim")
# Check if action update is supported in the requested API version
api_ver = self.app_args.os_infra_optim_api_version
if not api_versioning.action_update_supported(api_ver):
raise exceptions.CommandError(
_("Action update is not supported in API version %s. "
"Minimum required version is 1.5.") % api_ver)
if not parsed_args.state and not parsed_args.reason:
raise exceptions.CommandError(
_("At least one field update is required for this operation"))
if not uuidutils.is_uuid_like(parsed_args.action):
raise exceptions.ValidationError()
patch = []
if parsed_args.state:
patch.append({
'op': 'replace',
'path': '/state',
'value': parsed_args.state
})
if parsed_args.reason:
patch.append({
'op': 'replace',
'path': '/status_message',
'value': parsed_args.reason
})
try:
action = client.action.update(parsed_args.action, patch)
except exceptions.HTTPNotFound as exc:
raise exceptions.CommandError(str(exc))
columns = res_fields.ACTION_FIELDS
column_headers = res_fields.ACTION_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action, columns)

View File

@@ -38,6 +38,9 @@ def drop_unsupported_field(app_args, fields, field_labels):
if not api_versioning.launch_audit_forced(api_ver):
fields.remove('force')
field_labels.remove('Force')
if not api_versioning.action_update_supported(api_ver):
fields.remove('status_message')
field_labels.remove('Status Message')
return fields, field_labels

View File

@@ -33,13 +33,14 @@ AUDIT_TEMPLATE_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Goal', 'Strategy']
AUDIT_FIELDS = ['uuid', 'name', 'created_at', 'updated_at', 'deleted_at',
'state', 'audit_type', 'parameters', 'interval', 'goal_name',
'strategy_name', 'scope', 'auto_trigger', 'next_run_time',
'hostname', 'start_time', 'end_time', 'force']
'hostname', 'start_time', 'end_time', 'force',
'status_message']
AUDIT_FIELD_LABELS = ['UUID', 'Name', 'Created At', 'Updated At', 'Deleted At',
'State', 'Audit Type', 'Parameters', 'Interval', 'Goal',
'Strategy', 'Audit Scope', 'Auto Trigger',
'Next Run Time', 'Hostname', 'Start Time', 'End Time',
'Force']
'Force', 'Status Message']
AUDIT_SHORT_LIST_FIELDS = ['uuid', 'name', 'audit_type',
'state', 'goal_name', 'strategy_name',
@@ -51,12 +52,13 @@ AUDIT_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Audit Type', 'State', 'Goal',
# Action Plan
ACTION_PLAN_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'audit_uuid', 'strategy_name', 'state',
'efficacy_indicators', 'global_efficacy', 'hostname']
'efficacy_indicators', 'global_efficacy', 'hostname',
'status_message']
ACTION_PLAN_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Audit', 'Strategy', 'State',
'Efficacy indicators', 'Global efficacy',
'Hostname']
'Hostname', 'Status Message']
ACTION_PLAN_SHORT_LIST_FIELDS = ['uuid', 'audit_uuid', 'state',
'updated_at', 'global_efficacy']
@@ -69,11 +71,11 @@ GLOBAL_EFFICACY_FIELDS = ['value', 'unit', 'name', 'description']
# Action
ACTION_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at', 'parents',
'state', 'action_plan_uuid', 'action_type',
'input_parameters', 'description']
'input_parameters', 'description', 'status_message']
ACTION_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Parents', 'State', 'Action Plan', 'Action',
'Parameters', 'Description']
'Parameters', 'Description', 'Status Message']
ACTION_SHORT_LIST_FIELDS = ['uuid', 'parents',
'state', 'action_plan_uuid', 'action_type']