From e06f1b0475b1544902d8f0f03faca1abcfcc5dce Mon Sep 17 00:00:00 2001 From: Alfredo Moralejo Date: Wed, 6 Aug 2025 17:37:15 +0200 Subject: [PATCH] API changes for skipped actions: patch actions and status_message This patch implements the changes in the API required for the skipped action blueprint. It includes: - New field `status_message` is visible in API get calls for Audits, ActionPlans and Audits. - New Patch call is added to `/actions/{action_id}` which allows to manually move actions in PENDING state to SKIPPED for ActionPlans which have not been started. - A new API microversion 1.5 is added for these changes. It also adds requried tests and documentation. Implements: blueprint add-skip-actions Assisted-By: Cursor (claude-4-sonnet) Change-Id: I71fb9af76085e5941a7fd3e9e4c89d6f3a3ada47 Signed-off-by: Alfredo Moralejo --- api-ref/source/parameters.yaml | 21 ++ .../action-skip-request-with-message.json | 12 + .../source/samples/action-skip-request.json | 7 + .../source/samples/action-skip-response.json | 29 +++ .../actionplan-list-detailed-response.json | 3 +- .../samples/actionplan-show-response.json | 5 +- .../actions-list-detailed-response.json | 5 +- .../source/samples/actions-show-response.json | 5 +- .../source/samples/audit-create-response.json | 3 +- .../samples/audit-list-detailed-response.json | 3 +- .../source/samples/audit-show-response.json | 3 +- api-ref/source/watcher-api-v1-actionplans.inc | 3 + api-ref/source/watcher-api-v1-actions.inc | 55 ++++ api-ref/source/watcher-api-v1-audits.inc | 5 + devstack/lib/watcher | 2 +- doc/source/architecture.rst | 33 +++ .../plantuml/action_state_machine.txt | 23 ++ doc/source/images/action_state_machine.png | Bin 0 -> 77122 bytes ...int-add-skip-actions-4a5a997dc1133f13.yaml | 20 ++ .../controllers/rest_api_version_history.rst | 6 + watcher/api/controllers/v1/action.py | 121 ++++++++- watcher/api/controllers/v1/action_plan.py | 12 +- watcher/api/controllers/v1/audit.py | 13 + watcher/api/controllers/v1/utils.py | 9 + watcher/api/controllers/v1/versions.py | 3 +- watcher/common/policies/action.py | 11 + watcher/tests/api/v1/test_actions.py | 238 ++++++++++++++++++ watcher/tests/api/v1/test_actions_plans.py | 76 ++++++ watcher/tests/api/v1/test_audits.py | 130 ++++++---- watcher/tests/fake_policy.py | 1 + 30 files changed, 797 insertions(+), 60 deletions(-) create mode 100644 api-ref/source/samples/action-skip-request-with-message.json create mode 100644 api-ref/source/samples/action-skip-request.json create mode 100644 api-ref/source/samples/action-skip-response.json create mode 100644 doc/source/image_src/plantuml/action_state_machine.txt create mode 100644 doc/source/images/action_state_machine.png create mode 100644 releasenotes/notes/blueprint-add-skip-actions-4a5a997dc1133f13.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index b454e363b..289cbaab7 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -189,6 +189,13 @@ action_state: in: body required: true type: string +action_status_message: + description: | + Message with additional information about the Action state. + in: body + required: false + type: string + min_version: 1.5 action_type: description: | Action type based on specific API action. Actions in Watcher are @@ -230,6 +237,13 @@ actionplan_state: in: body required: false type: string +actionplan_status_message: + description: | + Message with additional information about the Action Plan state. + in: body + required: false + type: string + min_version: 1.5 # Audit audit_autotrigger: @@ -320,6 +334,13 @@ audit_state: in: body required: true type: string +audit_status_message: + description: | + Message with additional information about the Audit state. + in: body + required: false + type: string + min_version: 1.5 audit_strategy: description: | The UUID or name of the Strategy. diff --git a/api-ref/source/samples/action-skip-request-with-message.json b/api-ref/source/samples/action-skip-request-with-message.json new file mode 100644 index 000000000..3bae1ed71 --- /dev/null +++ b/api-ref/source/samples/action-skip-request-with-message.json @@ -0,0 +1,12 @@ +[ + { + "op": "replace", + "value": "SKIPPED", + "path": "/state" + }, + { + "op": "replace", + "value": "Skipping due to maintenance window", + "path": "/status_message" + } +] \ No newline at end of file diff --git a/api-ref/source/samples/action-skip-request.json b/api-ref/source/samples/action-skip-request.json new file mode 100644 index 000000000..4aa51c2f4 --- /dev/null +++ b/api-ref/source/samples/action-skip-request.json @@ -0,0 +1,7 @@ +[ + { + "op": "replace", + "value": "SKIPPED", + "path": "/state" + } +] \ No newline at end of file diff --git a/api-ref/source/samples/action-skip-response.json b/api-ref/source/samples/action-skip-response.json new file mode 100644 index 000000000..153757146 --- /dev/null +++ b/api-ref/source/samples/action-skip-response.json @@ -0,0 +1,29 @@ +{ + "state": "SKIPPED", + "description": "Migrate instance to another compute node", + "parents": [ + "b4529294-1de6-4302-b57a-9b5d5dc363c6" + ], + "links": [ + { + "rel": "self", + "href": "http://controller:9322/v1/actions/54acc7a0-91b0-46ea-a5f7-4ae2b9df0b0a" + }, + { + "rel": "bookmark", + "href": "http://controller:9322/actions/54acc7a0-91b0-46ea-a5f7-4ae2b9df0b0a" + } + ], + "action_plan_uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf", + "uuid": "54acc7a0-91b0-46ea-a5f7-4ae2b9df0b0a", + "deleted_at": null, + "updated_at": "2018-04-10T12:15:44.026973+00:00", + "input_parameters": { + "migration_type": "live", + "destination_node": "compute-2", + "resource_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef" + }, + "action_type": "migrate", + "created_at": "2018-04-10T11:59:12.725147+00:00", + "status_message": "Action skipped by user. Reason:Skipping due to maintenance window" +} \ No newline at end of file diff --git a/api-ref/source/samples/actionplan-list-detailed-response.json b/api-ref/source/samples/actionplan-list-detailed-response.json index cf2dd4e0c..71b8bc87d 100644 --- a/api-ref/source/samples/actionplan-list-detailed-response.json +++ b/api-ref/source/samples/actionplan-list-detailed-response.json @@ -21,7 +21,8 @@ "uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf", "audit_uuid": "7d100b05-0a86-491f-98a7-f93da19b272a", "created_at": "2018-04-10T11:59:52.640067+00:00", - "hostname": "controller" + "hostname": "controller", + "status_message": null } ] } diff --git a/api-ref/source/samples/actionplan-show-response.json b/api-ref/source/samples/actionplan-show-response.json index ce8af09fb..c649d9f8b 100644 --- a/api-ref/source/samples/actionplan-show-response.json +++ b/api-ref/source/samples/actionplan-show-response.json @@ -17,5 +17,6 @@ "strategy_name": "dummy_with_resize", "uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf", "audit_uuid": "7d100b05-0a86-491f-98a7-f93da19b272a", - "hostname": "controller" -} \ No newline at end of file + "hostname": "controller", + "status_message": null +} diff --git a/api-ref/source/samples/actions-list-detailed-response.json b/api-ref/source/samples/actions-list-detailed-response.json index cd0d98677..5da125fa9 100644 --- a/api-ref/source/samples/actions-list-detailed-response.json +++ b/api-ref/source/samples/actions-list-detailed-response.json @@ -24,7 +24,8 @@ "duration": 3.2 }, "action_type": "sleep", - "created_at": "2018-03-26T11:56:08.235226+00:00" + "created_at": "2018-03-26T11:56:08.235226+00:00", + "status_message": null } ] -} \ No newline at end of file +} diff --git a/api-ref/source/samples/actions-show-response.json b/api-ref/source/samples/actions-show-response.json index b5959bd88..fb3f3e25b 100644 --- a/api-ref/source/samples/actions-show-response.json +++ b/api-ref/source/samples/actions-show-response.json @@ -22,5 +22,6 @@ "message": "Welcome" }, "action_type": "nop", - "created_at": "2018-04-10T11:59:12.725147+00:00" -} \ No newline at end of file + "created_at": "2018-04-10T11:59:12.725147+00:00", + "status_message": null +} diff --git a/api-ref/source/samples/audit-create-response.json b/api-ref/source/samples/audit-create-response.json index 3c11bc52d..f6cf89f49 100644 --- a/api-ref/source/samples/audit-create-response.json +++ b/api-ref/source/samples/audit-create-response.json @@ -51,5 +51,6 @@ "updated_at": null, "hostname": null, "start_time": null, - "end_time": null + "end_time": null, + "status_message": null } diff --git a/api-ref/source/samples/audit-list-detailed-response.json b/api-ref/source/samples/audit-list-detailed-response.json index a4a9240e1..0311e30c7 100644 --- a/api-ref/source/samples/audit-list-detailed-response.json +++ b/api-ref/source/samples/audit-list-detailed-response.json @@ -53,7 +53,8 @@ "updated_at": "2018-04-06T09:44:01.604146+00:00", "hostname": "controller", "start_time": null, - "end_time": null + "end_time": null, + "status_message": null } ] } diff --git a/api-ref/source/samples/audit-show-response.json b/api-ref/source/samples/audit-show-response.json index a4e7bf201..dd7a8a07b 100644 --- a/api-ref/source/samples/audit-show-response.json +++ b/api-ref/source/samples/audit-show-response.json @@ -51,5 +51,6 @@ "updated_at": "2018-04-06T11:54:01.266447+00:00", "hostname": "controller", "start_time": null, - "end_time": null + "end_time": null, + "status_message": null } diff --git a/api-ref/source/watcher-api-v1-actionplans.inc b/api-ref/source/watcher-api-v1-actionplans.inc index 7fa122019..99b39b73e 100644 --- a/api-ref/source/watcher-api-v1-actionplans.inc +++ b/api-ref/source/watcher-api-v1-actionplans.inc @@ -139,6 +139,7 @@ Response - global_efficacy: actionplan_global_efficacy - links: links - hostname: actionplan_hostname + - status_message: actionplan_status_message **Example JSON representation of an Action Plan:** @@ -177,6 +178,7 @@ Response - global_efficacy: actionplan_global_efficacy - links: links - hostname: actionplan_hostname + - status_message: actionplan_status_message **Example JSON representation of an Audit:** @@ -233,6 +235,7 @@ version 1: - global_efficacy: actionplan_global_efficacy - links: links - hostname: actionplan_hostname + - status_message: actionplan_status_message **Example JSON representation of an Action Plan:** diff --git a/api-ref/source/watcher-api-v1-actions.inc b/api-ref/source/watcher-api-v1-actions.inc index 837d4f79c..0968bb73f 100644 --- a/api-ref/source/watcher-api-v1-actions.inc +++ b/api-ref/source/watcher-api-v1-actions.inc @@ -114,6 +114,7 @@ Response - description: action_description - input_parameters: action_input_parameters - links: links + - status_message: action_status_message **Example JSON representation of an Action:** @@ -151,8 +152,62 @@ Response - description: action_description - input_parameters: action_input_parameters - links: links + - status_message: action_status_message **Example JSON representation of an Action:** .. literalinclude:: samples/actions-show-response.json :language: javascript + +Skip Action +=========== + +.. rest_method:: PATCH /v1/actions/{action_ident} + +Skips an Action resource by changing its state to SKIPPED. + +.. note:: + Only Actions in PENDING state can be skipped. The Action must belong to + an Action Plan in RECOMMENDED or PENDING state. This operation requires + API microversion 1.5 or later. + +Normal response codes: 200 + +Error codes: 400,404,403,409 + +Request +------- + +.. rest_parameters:: parameters.yaml + + - action_ident: action_ident + +**Example Action skip request:** + +.. literalinclude:: samples/action-skip-request.json + :language: javascript + +**Example Action skip request with custom status message:** + +.. literalinclude:: samples/action-skip-request-with-message.json + :language: javascript + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - uuid: uuid + - action_type: action_type + - state: action_state + - action_plan_uuid: action_action_plan_uuid + - parents: action_parents + - description: action_description + - input_parameters: action_input_parameters + - links: links + - status_message: action_status_message + +**Example JSON representation of a skipped Action:** + +.. literalinclude:: samples/action-skip-response.json + :language: javascript \ No newline at end of file diff --git a/api-ref/source/watcher-api-v1-audits.inc b/api-ref/source/watcher-api-v1-audits.inc index 6f738aacd..1e5c90a1a 100644 --- a/api-ref/source/watcher-api-v1-audits.inc +++ b/api-ref/source/watcher-api-v1-audits.inc @@ -85,6 +85,7 @@ version 1: - start_time: audit_starttime_resp - end_time: audit_endtime_resp - force: audit_force + - status_message: audit_status_message **Example JSON representation of an Audit:** @@ -184,6 +185,7 @@ Response - start_time: audit_starttime_resp - end_time: audit_endtime_resp - force: audit_force + - status_message: audit_status_message **Example JSON representation of an Audit:** @@ -231,6 +233,7 @@ Response - start_time: audit_starttime_resp - end_time: audit_endtime_resp - force: audit_force + - status_message: audit_status_message **Example JSON representation of an Audit:** @@ -286,6 +289,7 @@ version 1: - start_time: audit_starttime_resp - end_time: audit_endtime_resp - force: audit_force + - status_message: audit_status_message **Example JSON representation of an Audit:** @@ -341,6 +345,7 @@ Response - start_time: audit_starttime_resp - end_time: audit_endtime_resp - force: audit_force + - status_message: audit_status_message **Example JSON representation of an Audit:** diff --git a/devstack/lib/watcher b/devstack/lib/watcher index e3543b5a0..fe04d4c11 100644 --- a/devstack/lib/watcher +++ b/devstack/lib/watcher @@ -268,7 +268,7 @@ function configure_tempest_for_watcher { # Please make sure to update this when the microversion is updated, otherwise # new tests may be skipped. TEMPEST_WATCHER_MIN_MICROVERSION=${TEMPEST_WATCHER_MIN_MICROVERSION:-"1.0"} - TEMPEST_WATCHER_MAX_MICROVERSION=${TEMPEST_WATCHER_MAX_MICROVERSION:-"1.4"} + TEMPEST_WATCHER_MAX_MICROVERSION=${TEMPEST_WATCHER_MAX_MICROVERSION:-"1.5"} # Set microversion options in tempest.conf iniset $TEMPEST_CONFIG optimize min_microversion $TEMPEST_WATCHER_MIN_MICROVERSION diff --git a/doc/source/architecture.rst b/doc/source/architecture.rst index 16f567df7..67ecb601c 100644 --- a/doc/source/architecture.rst +++ b/doc/source/architecture.rst @@ -481,6 +481,39 @@ change to a new value: .. image:: ./images/action_plan_state_machine.png :width: 100% +.. _action_state_machine: + +Action State Machine +------------------------- + +An :ref:`Action ` has a life-cycle and its current state may +be one of the following: + +- **PENDING** : the :ref:`Action ` has not been executed + yet by the :ref:`Watcher Applier ` +- **SKIPPED** : the :ref:`Action ` will not be executed + because a predefined skipping condition is found by + :ref:`Watcher Applier ` or is explicitly + skipped by the :ref:`Administrator `. +- **ONGOING** : the :ref:`Action ` is currently being + processed by the :ref:`Watcher Applier ` +- **SUCCEEDED** : the :ref:`Action ` has been executed + successfully +- **FAILED** : an error occurred while trying to execute the + :ref:`Action ` +- **DELETED** : the :ref:`Action ` is still stored in the + :ref:`Watcher database ` but is not returned + any more through the Watcher APIs. +- **CANCELLED** : the :ref:`Action ` was in **PENDING** or + **ONGOING** state and was cancelled by the + :ref:`Administrator ` + +The following diagram shows the different possible states of an +:ref:`Action ` and what event makes the state change +change to a new value: + +.. image:: ./images/action_state_machine.png + :width: 100% .. _Watcher API: https://docs.openstack.org/api-ref/resource-optimization/ diff --git a/doc/source/image_src/plantuml/action_state_machine.txt b/doc/source/image_src/plantuml/action_state_machine.txt new file mode 100644 index 000000000..ce8c40aae --- /dev/null +++ b/doc/source/image_src/plantuml/action_state_machine.txt @@ -0,0 +1,23 @@ +@startuml + +skinparam ArrowColor DarkRed +skinparam StateBorderColor DarkRed +skinparam StateBackgroundColor LightYellow +skinparam Shadowing true + +[*] --> PENDING: The Watcher Planner\ncreates the Action +PENDING --> SKIPPED: The Action detects skipping condition\n in pre_condition or was\n skipped by cloud Admin. +PENDING --> FAILED: The Action fails unexpectedly\n in pre_condition. +PENDING --> ONGOING: The Watcher Applier starts executing/n the action. +ONGOING --> FAILED: Something failed while executing\nthe Action in the Watcher Applier +ONGOING --> SUCCEEDED: The Watcher Applier executed\nthe Action successfully +FAILED --> DELETED : Administrator removes\nAction Plan +SUCCEEDED --> DELETED : Administrator removes\n theAction +ONGOING --> CANCELLED : The Action was cancelled\n as part of an Action Plan cancellation. +PENDING --> CANCELLED : The Action was cancelled\n as part of an Action Plan cancellation. +CANCELLED --> DELETED +FAILED --> DELETED +SKIPPED --> DELETED +DELETED --> [*] + +@enduml diff --git a/doc/source/images/action_state_machine.png b/doc/source/images/action_state_machine.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb806f4121f1a878acc5109300e9c523c83ac4c GIT binary patch literal 77122 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 %(new_state)s)") + raise exception.Conflict( + patch=patch, + message=error_message % dict( + initial_state=action_to_update.state, + new_state=action.state)) + action_plan = action_to_update.action_plan + if action_plan.state not in [objects.action_plan.State.RECOMMENDED, + objects.action_plan.State.PENDING]: + error_message = _("State update not allowed for actionplan " + "state: %(ap_state)s") + raise exception.Conflict( + patch=patch, + message=error_message % dict( + ap_state=action_plan.state)) + + status_message = _("Action skipped by user.") + # status_message update only allowed with status update + if (hasattr(action, 'status_message') and + action.status_message != action_to_update.status_message): + if action.state == action_to_update.state: + error_message = _( + "status_message update only allowed with state change") + raise exception.PatchError( + patch=patch, + reason=error_message) + else: + status_message = (_("%(status_message)s Reason: %(reason)s") + % dict(status_message=status_message, + reason=action.status_message)) + + action.status_message = status_message + + # Update only the fields that have changed + for field in objects.Action.fields: + try: + patch_val = getattr(action, field) + except AttributeError: + # Ignore fields that aren't exposed in the API + continue + if patch_val == wtypes.Unset: + patch_val = None + if action_to_update[field] != patch_val: + action_to_update[field] = patch_val + + action_to_update.save() + return Action.convert_with_links(action_to_update) diff --git a/watcher/api/controllers/v1/action_plan.py b/watcher/api/controllers/v1/action_plan.py index 8c77cac20..52c40d0d0 100644 --- a/watcher/api/controllers/v1/action_plan.py +++ b/watcher/api/controllers/v1/action_plan.py @@ -87,7 +87,8 @@ def hide_fields_in_newer_versions(obj): These fields are only made available when the request's API version matches or exceeds the versions when these fields were introduced. """ - pass + if not api_utils.allow_skipped_action(): + obj.status_message = wtypes.Unset class ActionPlanPatchType(types.JsonPatchType): @@ -112,7 +113,11 @@ class ActionPlanPatchType(types.JsonPatchType): @staticmethod def internal_attrs(): - return types.JsonPatchType.internal_attrs() + # There are global internal attributes and object specific ones. + # /status_message is only modified internally based on state changes + # and is not exposed in the patch API. + ap_internal_attrs = ['/status_message'] + return types.JsonPatchType.internal_attrs() + ap_internal_attrs @staticmethod def mandatory_attrs(): @@ -244,6 +249,9 @@ class ActionPlan(base.APIBase): hostname = wtypes.wsattr(wtypes.text, mandatory=False) """Hostname the actionplan is running on""" + status_message = wtypes.text + """Status message of the action plan""" + def __init__(self, **kwargs): super(ActionPlan, self).__init__() self.fields = [] diff --git a/watcher/api/controllers/v1/audit.py b/watcher/api/controllers/v1/audit.py index 4e59b17aa..1918f3937 100644 --- a/watcher/api/controllers/v1/audit.py +++ b/watcher/api/controllers/v1/audit.py @@ -77,6 +77,8 @@ def hide_fields_in_newer_versions(obj): obj.end_time = wtypes.Unset if not api_utils.allow_force(): obj.force = wtypes.Unset + if not api_utils.allow_skipped_action(): + obj.status_message = wtypes.Unset class AuditPostType(wtypes.Base): @@ -213,6 +215,14 @@ class AuditPatchType(types.JsonPatchType): def mandatory_attrs(): return ['/audit_template_uuid', '/type'] + @staticmethod + def internal_attrs(): + # There are global internal attributes and object specific ones. + # /status_message is only modified internally based on state changes + # and is not exposed in the patch API for Audits. + audit_internal_attrs = ['/status_message'] + return types.JsonPatchType.internal_attrs() + audit_internal_attrs + @staticmethod def validate(patch): @@ -375,6 +385,9 @@ class Audit(base.APIBase): """Allow Action Plan of this Audit be executed in parallel with other Action Plan""" + status_message = wtypes.wsattr(wtypes.text, mandatory=False) + """Status message of the audit""" + def __init__(self, **kwargs): self.fields = [] fields = list(objects.Audit.fields) diff --git a/watcher/api/controllers/v1/utils.py b/watcher/api/controllers/v1/utils.py index 25fb77e16..23cce2920 100644 --- a/watcher/api/controllers/v1/utils.py +++ b/watcher/api/controllers/v1/utils.py @@ -194,3 +194,12 @@ def allow_webhook_api(): """ return pecan.request.version.minor >= ( versions.VERSIONS.MINOR_4_WEBHOOK_API.value) + + +def allow_skipped_action(): + """Check if we should support skipped action. + + Version 1.5 of the API added support to skipped actions. + """ + return pecan.request.version.minor >= ( + versions.VERSIONS.MINOR_5_SKIPPED_ACTION.value) diff --git a/watcher/api/controllers/v1/versions.py b/watcher/api/controllers/v1/versions.py index 369c40064..f7dfbae77 100644 --- a/watcher/api/controllers/v1/versions.py +++ b/watcher/api/controllers/v1/versions.py @@ -23,7 +23,8 @@ class VERSIONS(enum.Enum): MINOR_2_FORCE = 2 # v1.2: Add force field to audit MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API MINOR_4_WEBHOOK_API = 4 # v1.4: Add webhook trigger API - MINOR_MAX_VERSION = 4 + MINOR_5_SKIPPED_ACTION = 5 # v1.5: Add skipped action support + MINOR_MAX_VERSION = 5 # This is the version 1 API diff --git a/watcher/common/policies/action.py b/watcher/common/policies/action.py index 5e87d7c42..19f050ec9 100644 --- a/watcher/common/policies/action.py +++ b/watcher/common/policies/action.py @@ -49,6 +49,17 @@ rules = [ 'method': 'GET' } ] + ), + policy.DocumentedRuleDefault( + name=ACTION % 'update', + check_str=base.RULE_ADMIN_API, + description='Update an action.', + operations=[ + { + 'path': '/v1/actions/{action_id}', + 'method': 'PATCH' + } + ] ) ] diff --git a/watcher/tests/api/v1/test_actions.py b/watcher/tests/api/v1/test_actions.py index ab5cc6a74..2ae31f701 100644 --- a/watcher/tests/api/v1/test_actions.py +++ b/watcher/tests/api/v1/test_actions.py @@ -11,6 +11,7 @@ # limitations under the License. import itertools +from unittest import mock from http import HTTPStatus from oslo_config import cfg @@ -19,6 +20,7 @@ from wsme import types as wtypes from watcher.api.controllers.v1 import action as api_action from watcher.common import utils +from watcher.db import api as db_api from watcher import objects from watcher.tests.api import base as api_base from watcher.tests.api import utils as api_utils @@ -70,6 +72,36 @@ class TestListAction(api_base.FunctionalTest): response = self.get_json('/actions') self.assertEqual(action.uuid, response['actions'][0]["uuid"]) self._assert_action_fields(response['actions'][0]) + self.assertNotIn('status_message', response['actions'][0]) + + def test_one_with_status_message(self): + action = obj_utils.create_test_action( + self.context, parents=None, status_message='Fake message') + response = self.get_json( + '/actions', headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action.uuid, response['actions'][0]["uuid"]) + self._assert_action_fields(response['actions'][0]) + # status_message is not in the basic actions list + self.assertNotIn('status_message', response['actions'][0]) + + def test_list_detail(self): + action = obj_utils.create_test_action( + self.context, status_message='Fake message', parents=None) + response = self.get_json('/actions/detail') + self.assertEqual(action.uuid, response['actions'][0]["uuid"]) + self._assert_action_fields(response['actions'][0]) + self.assertNotIn('status_message', response['actions'][0]) + + def test_list_detail_with_status_message(self): + action = obj_utils.create_test_action( + self.context, status_message='Fake message', parents=None) + response = self.get_json( + '/actions/detail', + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action.uuid, response['actions'][0]["uuid"]) + self._assert_action_fields(response['actions'][0]) + self.assertEqual( + 'Fake message', response['actions'][0]["status_message"]) def test_one_soft_deleted(self): action = obj_utils.create_test_action(self.context, parents=None) @@ -88,6 +120,42 @@ class TestListAction(api_base.FunctionalTest): self.assertEqual(action.uuid, response['uuid']) self.assertEqual(action.action_type, response['action_type']) self.assertEqual(action.input_parameters, response['input_parameters']) + self.assertNotIn('status_message', response) + self._assert_action_fields(response) + + def test_get_one_with_status_message(self): + action = obj_utils.create_test_action( + self.context, parents=None, status_message='test') + response = self.get_json( + '/actions/%s' % action['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action.uuid, response['uuid']) + self.assertEqual(action.action_type, response['action_type']) + self.assertEqual(action.input_parameters, response['input_parameters']) + self.assertEqual('test', response['status_message']) + self._assert_action_fields(response) + + def test_get_one_with_hidden_status_message(self): + action = obj_utils.create_test_action( + self.context, parents=None, status_message='test') + response = self.get_json( + '/actions/%s' % action['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.4'}) + self.assertEqual(action.uuid, response['uuid']) + self.assertEqual(action.action_type, response['action_type']) + self.assertEqual(action.input_parameters, response['input_parameters']) + self.assertNotIn('status_message', response) + self._assert_action_fields(response) + + def test_get_one_with_empty_status_message(self): + action = obj_utils.create_test_action(self.context, parents=None) + response = self.get_json( + '/actions/%s' % action['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action.uuid, response['uuid']) + self.assertEqual(action.action_type, response['action_type']) + self.assertEqual(action.input_parameters, response['input_parameters']) + self.assertIsNone(response['status_message']) self._assert_action_fields(response) def test_get_one_soft_deleted(self): @@ -456,6 +524,166 @@ class TestListAction(api_base.FunctionalTest): self.assertEqual(3, len(response['actions'])) +class TestPatchAction(api_base.FunctionalTest): + + def setUp(self): + super(TestPatchAction, self).setUp() + obj_utils.create_test_goal(self.context) + obj_utils.create_test_strategy(self.context) + obj_utils.create_test_audit(self.context) + self.action_plan = obj_utils.create_test_action_plan( + self.context, + state=objects.action_plan.State.PENDING) + self.action = obj_utils.create_test_action(self.context, parents=None) + p = mock.patch.object(db_api.BaseConnection, 'update_action') + self.mock_action_update = p.start() + self.mock_action_update.side_effect = self._simulate_rpc_action_update + self.addCleanup(p.stop) + + def _simulate_rpc_action_update(self, action): + action.save() + return action + + def test_patch_action_not_allowed_old_microversion(self): + """Test that action patch is not allowed in older microversions""" + new_state = objects.action.State.SKIPPED + response = self.get_json('/actions/%s' % self.action.uuid) + self.assertNotEqual(new_state, response['state']) + + # Test with API version 1.4 (should fail) + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': new_state, 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.4'}, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) + self.assertTrue(response.json['error_message']) + + def test_patch_action_allowed_new_microversion(self): + """Test that action patch is allowed in microversion 1.5+""" + new_state = objects.action.State.SKIPPED + response = self.get_json('/actions/%s' % self.action.uuid) + self.assertNotEqual(new_state, response['state']) + + # Test with API version 1.5 (should succeed) + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': new_state, 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual('application/json', response.content_type) + self.assertEqual(HTTPStatus.OK, response.status_int) + self.assertEqual(new_state, response.json['state']) + self.assertEqual('Action skipped by user.', + response.json['status_message']) + + def test_patch_action_invalid_state_transition(self): + """Test that invalid state transitions are rejected""" + # Try to transition from PENDING to SUCCEEDED (should fail) + new_state = objects.action.State.SUCCEEDED + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': new_state, 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + self.assertEqual(HTTPStatus.CONFLICT, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + self.assertIn("State transition not allowed: (PENDING -> SUCCEEDED)", + response.json['error_message']) + + def test_patch_action_skip_non_pending_ap(self): + """Test transition conditions on parent actionplan + + The PENDING to SKIPPED transition is not allowed if + the actionplan is not PENDING or RECOMMENDED state + """ + self.action_plan.state = objects.action_plan.State.ONGOING + self.action_plan.save() + new_state = objects.action.State.SKIPPED + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': new_state, 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(HTTPStatus.CONFLICT, response.status_int) + self.assertTrue(response.json['error_message']) + self.assertIn("State update not allowed for actionplan state: ONGOING", + response.json['error_message']) + + def test_patch_action_skip_transition_with_status_message(self): + """Test that PENDING to SKIPPED transition is allowed""" + new_state = objects.action.State.SKIPPED + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': new_state, 'op': 'replace'}, + {'path': '/status_message', 'value': 'test message', + 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual('application/json', response.content_type) + self.assertEqual(HTTPStatus.OK, response.status_int) + self.assertEqual(new_state, response.json['state']) + self.assertEqual( + 'Action skipped by user. Reason: test message', + response.json['status_message']) + + def test_patch_action_invalid_state_value(self): + """Test that invalid state values are rejected""" + invalid_state = "INVALID_STATE" + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': invalid_state, 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + + def test_patch_action_remove_status_message_not_allowed(self): + """Test that remove fields is not allowed""" + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/status_message', 'op': 'remove'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + self.assertIn("is a mandatory attribute and can not be removed", + response.json['error_message']) + + def test_patch_action_status_message_not_allowed(self): + """Test that status_message cannot be patched directly""" + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/status_message', 'value': 'test message', + 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertIn("status_message update only allowed with state change", + response.json['error_message']) + self.assertIsNone(self.action.status_message) + + def test_patch_action_one_allowed_one_not_allowed(self): + """Test that status_message cannot be patched directly""" + new_state = objects.action.State.SKIPPED + response = self.patch_json( + '/actions/%s' % self.action.uuid, + [{'path': '/state', 'value': new_state, 'op': 'replace'}, + {'path': '/action_plan_id', 'value': 56, 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + self.assertIn("\'/action_plan_id\' is not an allowed attribute and " + "can not be updated", response.json['error_message']) + self.assertIsNone(self.action.status_message) + + class TestActionPolicyEnforcement(api_base.FunctionalTest): def setUp(self): @@ -495,6 +723,16 @@ class TestActionPolicyEnforcement(api_base.FunctionalTest): '/actions/detail', expect_errors=True) + def test_policy_disallow_patch(self): + action = obj_utils.create_test_action(self.context) + self._common_policy_check( + "action:update", self.patch_json, + '/actions/%s' % action.uuid, + [{'path': '/state', 'value': objects.action.State.SKIPPED, + 'op': 'replace'}], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}, + expect_errors=True) + class TestActionPolicyEnforcementWithAdminContext(TestListAction, api_base.AdminRoleTest): diff --git a/watcher/tests/api/v1/test_actions_plans.py b/watcher/tests/api/v1/test_actions_plans.py index 7058b3724..e428750d6 100644 --- a/watcher/tests/api/v1/test_actions_plans.py +++ b/watcher/tests/api/v1/test_actions_plans.py @@ -51,6 +51,17 @@ class TestListActionPlan(api_base.FunctionalTest): self.assertEqual(action_plan.uuid, response['action_plans'][0]["uuid"]) self._assert_action_plans_fields(response['action_plans'][0]) + self.assertNotIn('status_message', response['action_plans'][0]) + + def test_one_with_status_message(self): + action_plan = obj_utils.create_test_action_plan( + self.context, headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + response = self.get_json('/action_plans') + self.assertEqual(action_plan.uuid, + response['action_plans'][0]["uuid"]) + self._assert_action_plans_fields(response['action_plans'][0]) + # status_message is not in the basic action_plans list + self.assertNotIn('status_message', response['action_plans'][0]) def test_one_soft_deleted(self): action_plan = obj_utils.create_test_action_plan(self.context) @@ -78,6 +89,29 @@ class TestListActionPlan(api_base.FunctionalTest): 'unit': '%'}], response['efficacy_indicators']) + def test_get_one_ok_with_status_message(self): + action_plan = obj_utils.create_test_action_plan( + self.context, status_message='Fake message') + obj_utils.create_test_efficacy_indicator( + self.context, action_plan_id=action_plan['id']) + response = self.get_json( + '/action_plans/%s' % action_plan['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action_plan.uuid, response['uuid']) + self._assert_action_plans_fields(response) + self.assertEqual("Fake message", response['status_message']) + + def test_get_one_ok_with_empty_status_message(self): + action_plan = obj_utils.create_test_action_plan(self.context) + obj_utils.create_test_efficacy_indicator( + self.context, action_plan_id=action_plan['id']) + response = self.get_json( + '/action_plans/%s' % action_plan['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action_plan.uuid, response['uuid']) + self._assert_action_plans_fields(response) + self.assertIsNone(response['status_message']) + def test_get_one_soft_deleted(self): action_plan = obj_utils.create_test_action_plan(self.context) action_plan.soft_delete() @@ -96,6 +130,30 @@ class TestListActionPlan(api_base.FunctionalTest): self.assertEqual(action_plan.uuid, response['action_plans'][0]["uuid"]) self._assert_action_plans_fields(response['action_plans'][0]) + self.assertNotIn('status_message', response['action_plans'][0]) + + def test_detail_with_status_message(self): + action_plan = obj_utils.create_test_action_plan( + self.context, status_message='Fake message') + response = self.get_json( + '/action_plans/detail', + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(action_plan.uuid, + response['action_plans'][0]["uuid"]) + self._assert_action_plans_fields(response['action_plans'][0]) + self.assertEqual( + "Fake message", response['action_plans'][0]['status_message']) + + def test_detail_with_hidden_status_message(self): + action_plan = obj_utils.create_test_action_plan( + self.context, status_message='Fake message') + response = self.get_json( + '/action_plans/detail', + headers={'OpenStack-API-Version': 'infra-optim 1.4'}) + self.assertEqual(action_plan.uuid, + response['action_plans'][0]["uuid"]) + self._assert_action_plans_fields(response['action_plans'][0]) + self.assertNotIn('status_message', response['action_plans'][0]) def test_detail_soft_deleted(self): action_plan = obj_utils.create_test_action_plan(self.context) @@ -518,6 +576,24 @@ class TestPatch(api_base.FunctionalTest): applier_mock.assert_called_once_with(mock.ANY, self.action_plan.uuid) + def test_replace_status_message_denied(self): + response = self.patch_json( + '/action_plans/%s' % self.action_plan.uuid, + [{'path': '/status_message', 'value': 'test', 'op': 'replace'}], + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + + def test_add_status_message_denied(self): + response = self.patch_json( + '/action_plans/%s' % self.action_plan.uuid, + [{'path': '/status_message', 'value': 'test', 'op': 'add'}], + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + ALLOWED_TRANSITIONS = [ {"original_state": objects.action_plan.State.RECOMMENDED, diff --git a/watcher/tests/api/v1/test_audits.py b/watcher/tests/api/v1/test_audits.py index 1a2e7b859..c754c4918 100644 --- a/watcher/tests/api/v1/test_audits.py +++ b/watcher/tests/api/v1/test_audits.py @@ -59,7 +59,7 @@ def post_get_test_audit_with_predefined_strategy(**kw): audit = api_utils.audit_post_data(**kw) audit_template = db_utils.get_test_audit_template( strategy_id=strategy['id']) - del_keys = ['goal_id', 'strategy_id', 'status_message'] + del_keys = ['goal_id', 'strategy_id'] add_keys = {'audit_template_uuid': audit_template['uuid'], } for k in del_keys: @@ -104,6 +104,16 @@ class TestListAudit(api_base.FunctionalTest): self.assertEqual(audit.uuid, response['audits'][0]["uuid"]) self._assert_audit_fields(response['audits'][0]) + def test_list_with_status_message(self): + audit = obj_utils.create_test_audit( + self.context, status_message='Fake message') + response = self.get_json( + '/audits', headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(audit.uuid, response['audits'][0]["uuid"]) + self._assert_audit_fields(response['audits'][0]) + # status_message is not in the basic actions list + self.assertNotIn('status_message', response['audits'][0]) + def test_one_soft_deleted(self): audit = obj_utils.create_test_audit(self.context) audit.soft_delete() @@ -120,6 +130,36 @@ class TestListAudit(api_base.FunctionalTest): response = self.get_json('/audits/%s' % audit['uuid']) self.assertEqual(audit.uuid, response['uuid']) self._assert_audit_fields(response) + self.assertNotIn('status_message', response) + + def test_get_one_with_status_message(self): + audit = obj_utils.create_test_audit( + self.context, status_message='Fake message') + response = self.get_json( + '/audits/%s' % audit['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(audit.uuid, response['uuid']) + self._assert_audit_fields(response) + self.assertEqual('Fake message', response['status_message']) + + def test_get_one_with_hidden_status_message(self): + audit = obj_utils.create_test_audit( + self.context, status_message='Fake message') + response = self.get_json( + '/audits/%s' % audit['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.4'}) + self.assertEqual(audit.uuid, response['uuid']) + self._assert_audit_fields(response) + self.assertNotIn('status_message', response) + + def test_get_one_with_empty_status_message(self): + audit = obj_utils.create_test_audit( + self.context) + response = self.get_json( + '/audits/%s' % audit['uuid'], + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(audit.uuid, response['uuid']) + self.assertIsNone(response['status_message']) def test_get_one_soft_deleted(self): audit = obj_utils.create_test_audit(self.context) @@ -138,6 +178,18 @@ class TestListAudit(api_base.FunctionalTest): response = self.get_json('/audits/detail') self.assertEqual(audit.uuid, response['audits'][0]["uuid"]) self._assert_audit_fields(response['audits'][0]) + self.assertNotIn('status_message', response['audits'][0]) + + def test_detail_with_status_message(self): + audit = obj_utils.create_test_audit( + self.context, status_message='Fake message') + response = self.get_json( + '/audits/detail', + headers={'OpenStack-API-Version': 'infra-optim 1.5'}) + self.assertEqual(audit.uuid, response['audits'][0]["uuid"]) + self._assert_audit_fields(response['audits'][0]) + self.assertEqual( + 'Fake message', response['audits'][0]['status_message']) def test_detail_soft_deleted(self): audit = obj_utils.create_test_audit(self.context) @@ -314,6 +366,15 @@ class TestPatch(api_base.FunctionalTest): self.assertEqual('application/json', response.content_type) self.assertTrue(response.json['error_message']) + def test_replace_status_message_denied(self): + response = self.patch_json( + '/audits/%s' % utils.generate_uuid(), + [{'path': '/status_message', 'value': 'test', 'op': 'replace'}], + expect_errors=True) + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + def test_add_ok(self): new_state = objects.audit.State.SUCCEEDED response = self.patch_json( @@ -500,8 +561,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( state=objects.audit.State.PENDING, params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) response = self.post_json('/audits', audit_dict) self.assertEqual('application/json', response.content_type) @@ -542,7 +602,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( state=objects.audit.State.PENDING, params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'status_message']) + 'next_run_time', 'hostname']) response = self.post_json('/audits', audit_dict, expect_errors=True) self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) @@ -556,7 +616,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', 'next_run_time', 'hostname', - 'audit_template_uuid', 'status_message']) + 'audit_template_uuid']) response = self.post_json('/audits', audit_dict) self.assertEqual('application/json', response.content_type) @@ -572,8 +632,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', 'next_run_time', 'hostname', - 'audit_template_uuid', 'strategy', - 'status_message']) + 'audit_template_uuid', 'strategy']) response = self.post_json('/audits', audit_dict) self.assertEqual('application/json', response.content_type) @@ -589,7 +648,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', 'next_run_time', 'hostname', - 'audit_template_uuid', 'status_message'], + 'audit_template_uuid'], use_named_goal=True) response = self.post_json('/audits', audit_dict) @@ -606,8 +665,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) # Make the audit template UUID some garbage value audit_dict['audit_template_uuid'] = ( '01234567-8910-1112-1314-151617181920') @@ -627,8 +685,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( state=objects.audit.State.PENDING, params_to_exclude=['uuid', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) state = audit_dict['state'] del audit_dict['state'] with mock.patch.object(self.dbapi, 'create_audit', @@ -645,8 +702,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) response = self.post_json('/audits', audit_dict) self.assertEqual('application/json', response.content_type) @@ -661,8 +717,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value audit_dict['interval'] = '1200' @@ -681,8 +736,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value audit_dict['interval'] = '* * * * *' @@ -701,8 +755,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value audit_dict['interval'] = 'zxc' @@ -722,8 +775,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value response = self.post_json('/audits', audit_dict, expect_errors=True) @@ -740,8 +792,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value response = self.post_json('/audits', audit_dict, expect_errors=True) @@ -757,8 +808,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( state=objects.audit.State.PENDING, params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) response = self.post_json('/audits', audit_dict) de_mock.assert_called_once_with(mock.ANY, response.json['uuid']) @@ -780,8 +830,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( parameters={'name': 'Tom'}, params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) response = self.post_json('/audits', audit_dict, expect_errors=True) self.assertEqual('application/json', response.content_type) @@ -827,7 +876,7 @@ class TestPost(api_base.FunctionalTest): audit_dict['audit_template_uuid'] = audit_template['uuid'] del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval', - 'scope', 'next_run_time', 'hostname', 'status_message'] + 'scope', 'next_run_time', 'hostname'] for k in del_keys: del audit_dict[k] @@ -850,7 +899,7 @@ class TestPost(api_base.FunctionalTest): audit_dict['audit_template_uuid'] = audit_template['uuid'] del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval', - 'scope', 'next_run_time', 'hostname', 'status_message'] + 'scope', 'next_run_time', 'hostname'] for k in del_keys: del audit_dict[k] @@ -906,8 +955,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) normal_name = 'this audit name is just for test' # long_name length exceeds 63 characters long_name = normal_name + audit_dict['uuid'] @@ -934,8 +982,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message'] + 'next_run_time', 'hostname', 'goal'] ) audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value audit_dict['interval'] = '1200' @@ -971,8 +1018,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message'] + 'next_run_time', 'hostname', 'goal'] ) audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value audit_dict['interval'] = '1200' @@ -997,8 +1043,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) response = self.post_json( '/audits', @@ -1014,8 +1059,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) audit_dict['force'] = True response = self.post_json( @@ -1033,8 +1077,7 @@ class TestPost(api_base.FunctionalTest): audit_dict = post_get_test_audit( params_to_exclude=['uuid', 'state', 'interval', 'scope', 'next_run_time', 'hostname', 'goal', - 'audit_template_uuid', 'name', - 'status_message']) + 'audit_template_uuid', 'name']) response = self.post_json( '/audits', @@ -1159,8 +1202,7 @@ class TestAuditPolicyEnforcement(api_base.FunctionalTest): audit_dict = post_get_test_audit( state=objects.audit.State.PENDING, params_to_exclude=['uuid', 'state', 'scope', - 'next_run_time', 'hostname', 'goal', - 'status_message']) + 'next_run_time', 'hostname', 'goal']) self._common_policy_check( "audit:create", self.post_json, '/audits', audit_dict, expect_errors=True) diff --git a/watcher/tests/fake_policy.py b/watcher/tests/fake_policy.py index 4d493e096..f809fa229 100644 --- a/watcher/tests/fake_policy.py +++ b/watcher/tests/fake_policy.py @@ -22,6 +22,7 @@ policy_data = """ "action:detail": "", "action:get": "", "action:get_all": "", + "action:update": "", "action_plan:delete": "", "action_plan:detail": "",