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 <amoralej@redhat.com>
This commit is contained in:
@@ -189,6 +189,13 @@ action_state:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
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:
|
action_type:
|
||||||
description: |
|
description: |
|
||||||
Action type based on specific API action. Actions in Watcher are
|
Action type based on specific API action. Actions in Watcher are
|
||||||
@@ -230,6 +237,13 @@ actionplan_state:
|
|||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
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
|
||||||
audit_autotrigger:
|
audit_autotrigger:
|
||||||
@@ -320,6 +334,13 @@ audit_state:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
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:
|
audit_strategy:
|
||||||
description: |
|
description: |
|
||||||
The UUID or name of the Strategy.
|
The UUID or name of the Strategy.
|
||||||
|
12
api-ref/source/samples/action-skip-request-with-message.json
Normal file
12
api-ref/source/samples/action-skip-request-with-message.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"value": "SKIPPED",
|
||||||
|
"path": "/state"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"value": "Skipping due to maintenance window",
|
||||||
|
"path": "/status_message"
|
||||||
|
}
|
||||||
|
]
|
7
api-ref/source/samples/action-skip-request.json
Normal file
7
api-ref/source/samples/action-skip-request.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"value": "SKIPPED",
|
||||||
|
"path": "/state"
|
||||||
|
}
|
||||||
|
]
|
29
api-ref/source/samples/action-skip-response.json
Normal file
29
api-ref/source/samples/action-skip-response.json
Normal file
@@ -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"
|
||||||
|
}
|
@@ -21,7 +21,8 @@
|
|||||||
"uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf",
|
"uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf",
|
||||||
"audit_uuid": "7d100b05-0a86-491f-98a7-f93da19b272a",
|
"audit_uuid": "7d100b05-0a86-491f-98a7-f93da19b272a",
|
||||||
"created_at": "2018-04-10T11:59:52.640067+00:00",
|
"created_at": "2018-04-10T11:59:52.640067+00:00",
|
||||||
"hostname": "controller"
|
"hostname": "controller",
|
||||||
|
"status_message": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -17,5 +17,6 @@
|
|||||||
"strategy_name": "dummy_with_resize",
|
"strategy_name": "dummy_with_resize",
|
||||||
"uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf",
|
"uuid": "4cbc4ede-0d25-481b-b86e-998dbbd4f8bf",
|
||||||
"audit_uuid": "7d100b05-0a86-491f-98a7-f93da19b272a",
|
"audit_uuid": "7d100b05-0a86-491f-98a7-f93da19b272a",
|
||||||
"hostname": "controller"
|
"hostname": "controller",
|
||||||
}
|
"status_message": null
|
||||||
|
}
|
||||||
|
@@ -24,7 +24,8 @@
|
|||||||
"duration": 3.2
|
"duration": 3.2
|
||||||
},
|
},
|
||||||
"action_type": "sleep",
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -22,5 +22,6 @@
|
|||||||
"message": "Welcome"
|
"message": "Welcome"
|
||||||
},
|
},
|
||||||
"action_type": "nop",
|
"action_type": "nop",
|
||||||
"created_at": "2018-04-10T11:59:12.725147+00:00"
|
"created_at": "2018-04-10T11:59:12.725147+00:00",
|
||||||
}
|
"status_message": null
|
||||||
|
}
|
||||||
|
@@ -51,5 +51,6 @@
|
|||||||
"updated_at": null,
|
"updated_at": null,
|
||||||
"hostname": null,
|
"hostname": null,
|
||||||
"start_time": null,
|
"start_time": null,
|
||||||
"end_time": null
|
"end_time": null,
|
||||||
|
"status_message": null
|
||||||
}
|
}
|
||||||
|
@@ -53,7 +53,8 @@
|
|||||||
"updated_at": "2018-04-06T09:44:01.604146+00:00",
|
"updated_at": "2018-04-06T09:44:01.604146+00:00",
|
||||||
"hostname": "controller",
|
"hostname": "controller",
|
||||||
"start_time": null,
|
"start_time": null,
|
||||||
"end_time": null
|
"end_time": null,
|
||||||
|
"status_message": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -51,5 +51,6 @@
|
|||||||
"updated_at": "2018-04-06T11:54:01.266447+00:00",
|
"updated_at": "2018-04-06T11:54:01.266447+00:00",
|
||||||
"hostname": "controller",
|
"hostname": "controller",
|
||||||
"start_time": null,
|
"start_time": null,
|
||||||
"end_time": null
|
"end_time": null,
|
||||||
|
"status_message": null
|
||||||
}
|
}
|
||||||
|
@@ -139,6 +139,7 @@ Response
|
|||||||
- global_efficacy: actionplan_global_efficacy
|
- global_efficacy: actionplan_global_efficacy
|
||||||
- links: links
|
- links: links
|
||||||
- hostname: actionplan_hostname
|
- hostname: actionplan_hostname
|
||||||
|
- status_message: actionplan_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Action Plan:**
|
**Example JSON representation of an Action Plan:**
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ Response
|
|||||||
- global_efficacy: actionplan_global_efficacy
|
- global_efficacy: actionplan_global_efficacy
|
||||||
- links: links
|
- links: links
|
||||||
- hostname: actionplan_hostname
|
- hostname: actionplan_hostname
|
||||||
|
- status_message: actionplan_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Audit:**
|
**Example JSON representation of an Audit:**
|
||||||
|
|
||||||
@@ -233,6 +235,7 @@ version 1:
|
|||||||
- global_efficacy: actionplan_global_efficacy
|
- global_efficacy: actionplan_global_efficacy
|
||||||
- links: links
|
- links: links
|
||||||
- hostname: actionplan_hostname
|
- hostname: actionplan_hostname
|
||||||
|
- status_message: actionplan_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Action Plan:**
|
**Example JSON representation of an Action Plan:**
|
||||||
|
|
||||||
|
@@ -114,6 +114,7 @@ Response
|
|||||||
- description: action_description
|
- description: action_description
|
||||||
- input_parameters: action_input_parameters
|
- input_parameters: action_input_parameters
|
||||||
- links: links
|
- links: links
|
||||||
|
- status_message: action_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Action:**
|
**Example JSON representation of an Action:**
|
||||||
|
|
||||||
@@ -151,8 +152,62 @@ Response
|
|||||||
- description: action_description
|
- description: action_description
|
||||||
- input_parameters: action_input_parameters
|
- input_parameters: action_input_parameters
|
||||||
- links: links
|
- links: links
|
||||||
|
- status_message: action_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Action:**
|
**Example JSON representation of an Action:**
|
||||||
|
|
||||||
.. literalinclude:: samples/actions-show-response.json
|
.. literalinclude:: samples/actions-show-response.json
|
||||||
:language: javascript
|
: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
|
@@ -85,6 +85,7 @@ version 1:
|
|||||||
- start_time: audit_starttime_resp
|
- start_time: audit_starttime_resp
|
||||||
- end_time: audit_endtime_resp
|
- end_time: audit_endtime_resp
|
||||||
- force: audit_force
|
- force: audit_force
|
||||||
|
- status_message: audit_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Audit:**
|
**Example JSON representation of an Audit:**
|
||||||
|
|
||||||
@@ -184,6 +185,7 @@ Response
|
|||||||
- start_time: audit_starttime_resp
|
- start_time: audit_starttime_resp
|
||||||
- end_time: audit_endtime_resp
|
- end_time: audit_endtime_resp
|
||||||
- force: audit_force
|
- force: audit_force
|
||||||
|
- status_message: audit_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Audit:**
|
**Example JSON representation of an Audit:**
|
||||||
|
|
||||||
@@ -231,6 +233,7 @@ Response
|
|||||||
- start_time: audit_starttime_resp
|
- start_time: audit_starttime_resp
|
||||||
- end_time: audit_endtime_resp
|
- end_time: audit_endtime_resp
|
||||||
- force: audit_force
|
- force: audit_force
|
||||||
|
- status_message: audit_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Audit:**
|
**Example JSON representation of an Audit:**
|
||||||
|
|
||||||
@@ -286,6 +289,7 @@ version 1:
|
|||||||
- start_time: audit_starttime_resp
|
- start_time: audit_starttime_resp
|
||||||
- end_time: audit_endtime_resp
|
- end_time: audit_endtime_resp
|
||||||
- force: audit_force
|
- force: audit_force
|
||||||
|
- status_message: audit_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Audit:**
|
**Example JSON representation of an Audit:**
|
||||||
|
|
||||||
@@ -341,6 +345,7 @@ Response
|
|||||||
- start_time: audit_starttime_resp
|
- start_time: audit_starttime_resp
|
||||||
- end_time: audit_endtime_resp
|
- end_time: audit_endtime_resp
|
||||||
- force: audit_force
|
- force: audit_force
|
||||||
|
- status_message: audit_status_message
|
||||||
|
|
||||||
**Example JSON representation of an Audit:**
|
**Example JSON representation of an Audit:**
|
||||||
|
|
||||||
|
@@ -268,7 +268,7 @@ function configure_tempest_for_watcher {
|
|||||||
# Please make sure to update this when the microversion is updated, otherwise
|
# Please make sure to update this when the microversion is updated, otherwise
|
||||||
# new tests may be skipped.
|
# new tests may be skipped.
|
||||||
TEMPEST_WATCHER_MIN_MICROVERSION=${TEMPEST_WATCHER_MIN_MICROVERSION:-"1.0"}
|
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
|
# Set microversion options in tempest.conf
|
||||||
iniset $TEMPEST_CONFIG optimize min_microversion $TEMPEST_WATCHER_MIN_MICROVERSION
|
iniset $TEMPEST_CONFIG optimize min_microversion $TEMPEST_WATCHER_MIN_MICROVERSION
|
||||||
|
@@ -481,6 +481,39 @@ change to a new value:
|
|||||||
.. image:: ./images/action_plan_state_machine.png
|
.. image:: ./images/action_plan_state_machine.png
|
||||||
:width: 100%
|
:width: 100%
|
||||||
|
|
||||||
|
.. _action_state_machine:
|
||||||
|
|
||||||
|
Action State Machine
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
An :ref:`Action <action_definition>` has a life-cycle and its current state may
|
||||||
|
be one of the following:
|
||||||
|
|
||||||
|
- **PENDING** : the :ref:`Action <action_definition>` has not been executed
|
||||||
|
yet by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||||
|
- **SKIPPED** : the :ref:`Action <action_definition>` will not be executed
|
||||||
|
because a predefined skipping condition is found by
|
||||||
|
:ref:`Watcher Applier <watcher_applier_definition>` or is explicitly
|
||||||
|
skipped by the :ref:`Administrator <administrator_definition>`.
|
||||||
|
- **ONGOING** : the :ref:`Action <action_definition>` is currently being
|
||||||
|
processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||||
|
- **SUCCEEDED** : the :ref:`Action <action_definition>` has been executed
|
||||||
|
successfully
|
||||||
|
- **FAILED** : an error occurred while trying to execute the
|
||||||
|
:ref:`Action <action_definition>`
|
||||||
|
- **DELETED** : the :ref:`Action <action_definition>` is still stored in the
|
||||||
|
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||||
|
any more through the Watcher APIs.
|
||||||
|
- **CANCELLED** : the :ref:`Action <action_definition>` was in **PENDING** or
|
||||||
|
**ONGOING** state and was cancelled by the
|
||||||
|
:ref:`Administrator <administrator_definition>`
|
||||||
|
|
||||||
|
The following diagram shows the different possible states of an
|
||||||
|
:ref:`Action <action_definition>` 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/
|
.. _Watcher API: https://docs.openstack.org/api-ref/resource-optimization/
|
||||||
|
23
doc/source/image_src/plantuml/action_state_machine.txt
Normal file
23
doc/source/image_src/plantuml/action_state_machine.txt
Normal file
@@ -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
|
BIN
doc/source/images/action_state_machine.png
Normal file
BIN
doc/source/images/action_state_machine.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
A new state ``SKIPPED`` has been added to the Actions. Actions can reach this state
|
||||||
|
in two situations:
|
||||||
|
|
||||||
|
* Watcher detects a specific pre-defined condition in the `pre_condition` phase.
|
||||||
|
|
||||||
|
* An admin sets the state to SKIPPED using a call to the new Patch API `/actions/{action_id}`
|
||||||
|
before the action plan is started.
|
||||||
|
|
||||||
|
An action in ``SKIPPED`` state will not be executed by Watcher as part of an ActionPlan
|
||||||
|
run.
|
||||||
|
|
||||||
|
Additionally, a new field ``status_message`` has been added to Audits, ActionPlans and
|
||||||
|
Actions which will be used to provide additional details about the state of an object.
|
||||||
|
|
||||||
|
All these changes have been introduced in a new Watcher ``API microversion 1.5``.
|
||||||
|
|
||||||
|
For additional information, see the API reference.
|
@@ -39,3 +39,9 @@ Added list data model API.
|
|||||||
---
|
---
|
||||||
Added Watcher webhook API. It can be used to trigger audit
|
Added Watcher webhook API. It can be used to trigger audit
|
||||||
with ``event`` type.
|
with ``event`` type.
|
||||||
|
|
||||||
|
1.5
|
||||||
|
---
|
||||||
|
Added support for SKIPPED actions status via PATCH support for Actions API.
|
||||||
|
This feature also introduces the ``status_message`` field to audits, actions
|
||||||
|
and action plans.
|
||||||
|
@@ -62,9 +62,11 @@ are dynamically loaded by Watcher at launch time.
|
|||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
import wsmeext.pecan as wsme_pecan
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.api.controllers import base
|
from watcher.api.controllers import base
|
||||||
from watcher.api.controllers import link
|
from watcher.api.controllers import link
|
||||||
from watcher.api.controllers.v1 import collection
|
from watcher.api.controllers.v1 import collection
|
||||||
@@ -82,14 +84,44 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
These fields are only made available when the request's API version
|
These fields are only made available when the request's API version
|
||||||
matches or exceeds the versions when these fields were introduced.
|
matches or exceeds the versions when these fields were introduced.
|
||||||
"""
|
"""
|
||||||
pass
|
if not api_utils.allow_skipped_action():
|
||||||
|
obj.status_message = wtypes.Unset
|
||||||
|
|
||||||
|
|
||||||
class ActionPatchType(types.JsonPatchType):
|
class ActionPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_state(patch):
|
||||||
|
serialized_patch = {'path': patch.path, 'op': patch.op}
|
||||||
|
if patch.value is not wtypes.Unset:
|
||||||
|
serialized_patch['value'] = patch.value
|
||||||
|
|
||||||
|
state_value = patch.value
|
||||||
|
if state_value and not hasattr(objects.action.State, state_value):
|
||||||
|
msg = _("Invalid state: %(state)s")
|
||||||
|
raise exception.PatchError(
|
||||||
|
patch=serialized_patch, reason=msg % dict(state=state_value))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(patch):
|
||||||
|
if patch.path == "/state":
|
||||||
|
ActionPatchType._validate_state(patch)
|
||||||
|
return types.JsonPatchType.validate(patch)
|
||||||
|
|
||||||
|
# We only allow to patch state and status_message
|
||||||
|
@staticmethod
|
||||||
|
def allowed_attrs():
|
||||||
|
return ["/state", "/status_message"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def internal_attrs():
|
||||||
|
return types.JsonPatchType.internal_attrs()
|
||||||
|
|
||||||
|
# We do not allow to remove any attribute via PATCH so setting all fields
|
||||||
|
# as mandatory
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mandatory_attrs():
|
def mandatory_attrs():
|
||||||
return []
|
return ["/" + item for item in objects.Action.fields.keys()]
|
||||||
|
|
||||||
|
|
||||||
class Action(base.APIBase):
|
class Action(base.APIBase):
|
||||||
@@ -141,6 +173,9 @@ class Action(base.APIBase):
|
|||||||
links = wtypes.wsattr([link.Link], readonly=True)
|
links = wtypes.wsattr([link.Link], readonly=True)
|
||||||
"""A list containing a self link and associated action links"""
|
"""A list containing a self link and associated action links"""
|
||||||
|
|
||||||
|
status_message = wtypes.text
|
||||||
|
"""Status message"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Action, self).__init__()
|
super(Action, self).__init__()
|
||||||
|
|
||||||
@@ -356,3 +391,85 @@ class ActionsController(rest.RestController):
|
|||||||
policy.enforce(context, 'action:get', action, action='action:get')
|
policy.enforce(context, 'action:get', action, action='action:get')
|
||||||
|
|
||||||
return Action.convert_with_links(action)
|
return Action.convert_with_links(action)
|
||||||
|
|
||||||
|
@wsme.validate(types.uuid, [ActionPatchType])
|
||||||
|
@wsme_pecan.wsexpose(Action, types.uuid, body=[ActionPatchType])
|
||||||
|
def patch(self, action_uuid, patch):
|
||||||
|
"""Update an existing action.
|
||||||
|
|
||||||
|
:param action_uuid: UUID of a action.
|
||||||
|
:param patch: a json PATCH document to apply to this action.
|
||||||
|
"""
|
||||||
|
if not api_utils.allow_skipped_action():
|
||||||
|
raise exception.Invalid(
|
||||||
|
_("API microversion 1.5 or higher is required."))
|
||||||
|
|
||||||
|
context = pecan.request.context
|
||||||
|
action_to_update = api_utils.get_resource(
|
||||||
|
'Action', action_uuid, eager=True)
|
||||||
|
policy.enforce(context, 'action:update', action_to_update,
|
||||||
|
action='action:update')
|
||||||
|
|
||||||
|
try:
|
||||||
|
action_dict = action_to_update.as_dict()
|
||||||
|
action = Action(**api_utils.apply_jsonpatch(action_dict, patch))
|
||||||
|
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Define allowed state transitions for actions
|
||||||
|
allowed_patch_transitions = [
|
||||||
|
(objects.action.State.PENDING, objects.action.State.SKIPPED),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Validate state transitions if state is being modified
|
||||||
|
if hasattr(action, 'state') and action.state != action_to_update.state:
|
||||||
|
transition = (action_to_update.state, action.state)
|
||||||
|
if transition not in allowed_patch_transitions:
|
||||||
|
error_message = _("State transition not allowed: "
|
||||||
|
"(%(initial_state)s -> %(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)
|
||||||
|
@@ -87,7 +87,8 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
These fields are only made available when the request's API version
|
These fields are only made available when the request's API version
|
||||||
matches or exceeds the versions when these fields were introduced.
|
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):
|
class ActionPlanPatchType(types.JsonPatchType):
|
||||||
@@ -112,7 +113,11 @@ class ActionPlanPatchType(types.JsonPatchType):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def internal_attrs():
|
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
|
@staticmethod
|
||||||
def mandatory_attrs():
|
def mandatory_attrs():
|
||||||
@@ -244,6 +249,9 @@ class ActionPlan(base.APIBase):
|
|||||||
hostname = wtypes.wsattr(wtypes.text, mandatory=False)
|
hostname = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||||
"""Hostname the actionplan is running on"""
|
"""Hostname the actionplan is running on"""
|
||||||
|
|
||||||
|
status_message = wtypes.text
|
||||||
|
"""Status message of the action plan"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(ActionPlan, self).__init__()
|
super(ActionPlan, self).__init__()
|
||||||
self.fields = []
|
self.fields = []
|
||||||
|
@@ -77,6 +77,8 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
obj.end_time = wtypes.Unset
|
obj.end_time = wtypes.Unset
|
||||||
if not api_utils.allow_force():
|
if not api_utils.allow_force():
|
||||||
obj.force = wtypes.Unset
|
obj.force = wtypes.Unset
|
||||||
|
if not api_utils.allow_skipped_action():
|
||||||
|
obj.status_message = wtypes.Unset
|
||||||
|
|
||||||
|
|
||||||
class AuditPostType(wtypes.Base):
|
class AuditPostType(wtypes.Base):
|
||||||
@@ -213,6 +215,14 @@ class AuditPatchType(types.JsonPatchType):
|
|||||||
def mandatory_attrs():
|
def mandatory_attrs():
|
||||||
return ['/audit_template_uuid', '/type']
|
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
|
@staticmethod
|
||||||
def validate(patch):
|
def validate(patch):
|
||||||
|
|
||||||
@@ -375,6 +385,9 @@ class Audit(base.APIBase):
|
|||||||
"""Allow Action Plan of this Audit be executed in parallel
|
"""Allow Action Plan of this Audit be executed in parallel
|
||||||
with other Action Plan"""
|
with other Action Plan"""
|
||||||
|
|
||||||
|
status_message = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||||
|
"""Status message of the audit"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.fields = []
|
self.fields = []
|
||||||
fields = list(objects.Audit.fields)
|
fields = list(objects.Audit.fields)
|
||||||
|
@@ -194,3 +194,12 @@ def allow_webhook_api():
|
|||||||
"""
|
"""
|
||||||
return pecan.request.version.minor >= (
|
return pecan.request.version.minor >= (
|
||||||
versions.VERSIONS.MINOR_4_WEBHOOK_API.value)
|
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)
|
||||||
|
@@ -23,7 +23,8 @@ class VERSIONS(enum.Enum):
|
|||||||
MINOR_2_FORCE = 2 # v1.2: Add force field to audit
|
MINOR_2_FORCE = 2 # v1.2: Add force field to audit
|
||||||
MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API
|
MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API
|
||||||
MINOR_4_WEBHOOK_API = 4 # v1.4: Add webhook trigger 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
|
# This is the version 1 API
|
||||||
|
@@ -49,6 +49,17 @@ rules = [
|
|||||||
'method': 'GET'
|
'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'
|
||||||
|
}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from oslo_config import cfg
|
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.api.controllers.v1 import action as api_action
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
|
from watcher.db import api as db_api
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
from watcher.tests.api import base as api_base
|
from watcher.tests.api import base as api_base
|
||||||
from watcher.tests.api import utils as api_utils
|
from watcher.tests.api import utils as api_utils
|
||||||
@@ -70,6 +72,36 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
response = self.get_json('/actions')
|
response = self.get_json('/actions')
|
||||||
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
||||||
self._assert_action_fields(response['actions'][0])
|
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):
|
def test_one_soft_deleted(self):
|
||||||
action = obj_utils.create_test_action(self.context, parents=None)
|
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.uuid, response['uuid'])
|
||||||
self.assertEqual(action.action_type, response['action_type'])
|
self.assertEqual(action.action_type, response['action_type'])
|
||||||
self.assertEqual(action.input_parameters, response['input_parameters'])
|
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)
|
self._assert_action_fields(response)
|
||||||
|
|
||||||
def test_get_one_soft_deleted(self):
|
def test_get_one_soft_deleted(self):
|
||||||
@@ -456,6 +524,166 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self.assertEqual(3, len(response['actions']))
|
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):
|
class TestActionPolicyEnforcement(api_base.FunctionalTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -495,6 +723,16 @@ class TestActionPolicyEnforcement(api_base.FunctionalTest):
|
|||||||
'/actions/detail',
|
'/actions/detail',
|
||||||
expect_errors=True)
|
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,
|
class TestActionPolicyEnforcementWithAdminContext(TestListAction,
|
||||||
api_base.AdminRoleTest):
|
api_base.AdminRoleTest):
|
||||||
|
@@ -51,6 +51,17 @@ class TestListActionPlan(api_base.FunctionalTest):
|
|||||||
self.assertEqual(action_plan.uuid,
|
self.assertEqual(action_plan.uuid,
|
||||||
response['action_plans'][0]["uuid"])
|
response['action_plans'][0]["uuid"])
|
||||||
self._assert_action_plans_fields(response['action_plans'][0])
|
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):
|
def test_one_soft_deleted(self):
|
||||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
action_plan = obj_utils.create_test_action_plan(self.context)
|
||||||
@@ -78,6 +89,29 @@ class TestListActionPlan(api_base.FunctionalTest):
|
|||||||
'unit': '%'}],
|
'unit': '%'}],
|
||||||
response['efficacy_indicators'])
|
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):
|
def test_get_one_soft_deleted(self):
|
||||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
action_plan = obj_utils.create_test_action_plan(self.context)
|
||||||
action_plan.soft_delete()
|
action_plan.soft_delete()
|
||||||
@@ -96,6 +130,30 @@ class TestListActionPlan(api_base.FunctionalTest):
|
|||||||
self.assertEqual(action_plan.uuid,
|
self.assertEqual(action_plan.uuid,
|
||||||
response['action_plans'][0]["uuid"])
|
response['action_plans'][0]["uuid"])
|
||||||
self._assert_action_plans_fields(response['action_plans'][0])
|
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):
|
def test_detail_soft_deleted(self):
|
||||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
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,
|
applier_mock.assert_called_once_with(mock.ANY,
|
||||||
self.action_plan.uuid)
|
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 = [
|
ALLOWED_TRANSITIONS = [
|
||||||
{"original_state": objects.action_plan.State.RECOMMENDED,
|
{"original_state": objects.action_plan.State.RECOMMENDED,
|
||||||
|
@@ -59,7 +59,7 @@ def post_get_test_audit_with_predefined_strategy(**kw):
|
|||||||
audit = api_utils.audit_post_data(**kw)
|
audit = api_utils.audit_post_data(**kw)
|
||||||
audit_template = db_utils.get_test_audit_template(
|
audit_template = db_utils.get_test_audit_template(
|
||||||
strategy_id=strategy['id'])
|
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'],
|
add_keys = {'audit_template_uuid': audit_template['uuid'],
|
||||||
}
|
}
|
||||||
for k in del_keys:
|
for k in del_keys:
|
||||||
@@ -104,6 +104,16 @@ class TestListAudit(api_base.FunctionalTest):
|
|||||||
self.assertEqual(audit.uuid, response['audits'][0]["uuid"])
|
self.assertEqual(audit.uuid, response['audits'][0]["uuid"])
|
||||||
self._assert_audit_fields(response['audits'][0])
|
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):
|
def test_one_soft_deleted(self):
|
||||||
audit = obj_utils.create_test_audit(self.context)
|
audit = obj_utils.create_test_audit(self.context)
|
||||||
audit.soft_delete()
|
audit.soft_delete()
|
||||||
@@ -120,6 +130,36 @@ class TestListAudit(api_base.FunctionalTest):
|
|||||||
response = self.get_json('/audits/%s' % audit['uuid'])
|
response = self.get_json('/audits/%s' % audit['uuid'])
|
||||||
self.assertEqual(audit.uuid, response['uuid'])
|
self.assertEqual(audit.uuid, response['uuid'])
|
||||||
self._assert_audit_fields(response)
|
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):
|
def test_get_one_soft_deleted(self):
|
||||||
audit = obj_utils.create_test_audit(self.context)
|
audit = obj_utils.create_test_audit(self.context)
|
||||||
@@ -138,6 +178,18 @@ class TestListAudit(api_base.FunctionalTest):
|
|||||||
response = self.get_json('/audits/detail')
|
response = self.get_json('/audits/detail')
|
||||||
self.assertEqual(audit.uuid, response['audits'][0]["uuid"])
|
self.assertEqual(audit.uuid, response['audits'][0]["uuid"])
|
||||||
self._assert_audit_fields(response['audits'][0])
|
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):
|
def test_detail_soft_deleted(self):
|
||||||
audit = obj_utils.create_test_audit(self.context)
|
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.assertEqual('application/json', response.content_type)
|
||||||
self.assertTrue(response.json['error_message'])
|
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):
|
def test_add_ok(self):
|
||||||
new_state = objects.audit.State.SUCCEEDED
|
new_state = objects.audit.State.SUCCEEDED
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
@@ -500,8 +561,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
state=objects.audit.State.PENDING,
|
state=objects.audit.State.PENDING,
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -542,7 +602,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
state=objects.audit.State.PENDING,
|
state=objects.audit.State.PENDING,
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
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)
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
|
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
|
||||||
@@ -556,7 +616,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname',
|
'next_run_time', 'hostname',
|
||||||
'audit_template_uuid', 'status_message'])
|
'audit_template_uuid'])
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -572,8 +632,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname',
|
'next_run_time', 'hostname',
|
||||||
'audit_template_uuid', 'strategy',
|
'audit_template_uuid', 'strategy'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -589,7 +648,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname',
|
'next_run_time', 'hostname',
|
||||||
'audit_template_uuid', 'status_message'],
|
'audit_template_uuid'],
|
||||||
use_named_goal=True)
|
use_named_goal=True)
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
@@ -606,8 +665,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
# Make the audit template UUID some garbage value
|
# Make the audit template UUID some garbage value
|
||||||
audit_dict['audit_template_uuid'] = (
|
audit_dict['audit_template_uuid'] = (
|
||||||
'01234567-8910-1112-1314-151617181920')
|
'01234567-8910-1112-1314-151617181920')
|
||||||
@@ -627,8 +685,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
state=objects.audit.State.PENDING,
|
state=objects.audit.State.PENDING,
|
||||||
params_to_exclude=['uuid', 'interval', 'scope',
|
params_to_exclude=['uuid', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
state = audit_dict['state']
|
state = audit_dict['state']
|
||||||
del audit_dict['state']
|
del audit_dict['state']
|
||||||
with mock.patch.object(self.dbapi, 'create_audit',
|
with mock.patch.object(self.dbapi, 'create_audit',
|
||||||
@@ -645,8 +702,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -661,8 +717,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = '1200'
|
audit_dict['interval'] = '1200'
|
||||||
|
|
||||||
@@ -681,8 +736,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = '* * * * *'
|
audit_dict['interval'] = '* * * * *'
|
||||||
|
|
||||||
@@ -701,8 +755,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = 'zxc'
|
audit_dict['interval'] = 'zxc'
|
||||||
|
|
||||||
@@ -722,8 +775,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
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(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
|
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
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(
|
audit_dict = post_get_test_audit(
|
||||||
state=objects.audit.State.PENDING,
|
state=objects.audit.State.PENDING,
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
de_mock.assert_called_once_with(mock.ANY, response.json['uuid'])
|
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(
|
audit_dict = post_get_test_audit(
|
||||||
parameters={'name': 'Tom'},
|
parameters={'name': 'Tom'},
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -827,7 +876,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
||||||
del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval',
|
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:
|
for k in del_keys:
|
||||||
del audit_dict[k]
|
del audit_dict[k]
|
||||||
|
|
||||||
@@ -850,7 +899,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
||||||
del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval',
|
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:
|
for k in del_keys:
|
||||||
del audit_dict[k]
|
del audit_dict[k]
|
||||||
|
|
||||||
@@ -906,8 +955,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['state', 'interval', 'scope',
|
params_to_exclude=['state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
normal_name = 'this audit name is just for test'
|
normal_name = 'this audit name is just for test'
|
||||||
# long_name length exceeds 63 characters
|
# long_name length exceeds 63 characters
|
||||||
long_name = normal_name + audit_dict['uuid']
|
long_name = normal_name + audit_dict['uuid']
|
||||||
@@ -934,8 +982,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal']
|
||||||
'status_message']
|
|
||||||
)
|
)
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = '1200'
|
audit_dict['interval'] = '1200'
|
||||||
@@ -971,8 +1018,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal']
|
||||||
'status_message']
|
|
||||||
)
|
)
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = '1200'
|
audit_dict['interval'] = '1200'
|
||||||
@@ -997,8 +1043,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
response = self.post_json(
|
response = self.post_json(
|
||||||
'/audits',
|
'/audits',
|
||||||
@@ -1014,8 +1059,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
audit_dict['force'] = True
|
audit_dict['force'] = True
|
||||||
response = self.post_json(
|
response = self.post_json(
|
||||||
@@ -1033,8 +1077,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
params_to_exclude=['uuid', 'state', 'interval', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal',
|
||||||
'audit_template_uuid', 'name',
|
'audit_template_uuid', 'name'])
|
||||||
'status_message'])
|
|
||||||
|
|
||||||
response = self.post_json(
|
response = self.post_json(
|
||||||
'/audits',
|
'/audits',
|
||||||
@@ -1159,8 +1202,7 @@ class TestAuditPolicyEnforcement(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit(
|
audit_dict = post_get_test_audit(
|
||||||
state=objects.audit.State.PENDING,
|
state=objects.audit.State.PENDING,
|
||||||
params_to_exclude=['uuid', 'state', 'scope',
|
params_to_exclude=['uuid', 'state', 'scope',
|
||||||
'next_run_time', 'hostname', 'goal',
|
'next_run_time', 'hostname', 'goal'])
|
||||||
'status_message'])
|
|
||||||
self._common_policy_check(
|
self._common_policy_check(
|
||||||
"audit:create", self.post_json, '/audits', audit_dict,
|
"audit:create", self.post_json, '/audits', audit_dict,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
|
@@ -22,6 +22,7 @@ policy_data = """
|
|||||||
"action:detail": "",
|
"action:detail": "",
|
||||||
"action:get": "",
|
"action:get": "",
|
||||||
"action:get_all": "",
|
"action:get_all": "",
|
||||||
|
"action:update": "",
|
||||||
|
|
||||||
"action_plan:delete": "",
|
"action_plan:delete": "",
|
||||||
"action_plan:detail": "",
|
"action_plan:detail": "",
|
||||||
|
Reference in New Issue
Block a user