Merge "Added suspended audit state"
This commit is contained in:
@@ -407,6 +407,9 @@ be one of the following:
|
||||
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
- **SUSPENDED** : the :ref:`Audit <audit_definition>` was in **ONGOING**
|
||||
state and was suspended by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
|
||||
The following diagram shows the different possible states of an
|
||||
:ref:`Audit <audit_definition>` and what event makes the state change to a new
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
PENDING --> ONGOING: Audit request is received\nby the Watcher Decision Engine
|
||||
ONGOING --> FAILED: Audit fails\n(no solution found, technical error, ...)
|
||||
ONGOING --> SUCCEEDED: The Watcher Decision Engine\ncould find at least one Solution
|
||||
ONGOING --> SUSPENDED: Administrator wants to\nsuspend the Audit
|
||||
SUSPENDED --> ONGOING: Administrator wants to\nresume the Audit
|
||||
FAILED --> DELETED : Administrator wants to\narchive/delete the Audit
|
||||
SUCCEEDED --> DELETED : Administrator wants to\narchive/delete the Audit
|
||||
PENDING --> CANCELLED : Administrator cancels\nthe Audit
|
||||
ONGOING --> CANCELLED : Administrator cancels\nthe Audit
|
||||
CANCELLED --> DELETED : Administrator wants to\narchive/delete the Audit
|
||||
SUSPENDED --> DELETED: Administrator wants to\narchive/delete the Audit
|
||||
DELETED --> [*]
|
||||
|
||||
@enduml
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added SUSPENDED audit state
|
||||
@@ -50,21 +50,6 @@ from watcher.decision_engine import rpcapi
|
||||
from watcher import objects
|
||||
|
||||
|
||||
ALLOWED_AUDIT_TRANSITIONS = {
|
||||
objects.audit.State.PENDING:
|
||||
[objects.audit.State.ONGOING, objects.audit.State.CANCELLED],
|
||||
objects.audit.State.ONGOING:
|
||||
[objects.audit.State.FAILED, objects.audit.State.SUCCEEDED,
|
||||
objects.audit.State.CANCELLED],
|
||||
objects.audit.State.FAILED:
|
||||
[objects.audit.State.DELETED],
|
||||
objects.audit.State.SUCCEEDED:
|
||||
[objects.audit.State.DELETED],
|
||||
objects.audit.State.CANCELLED:
|
||||
[objects.audit.State.DELETED]
|
||||
}
|
||||
|
||||
|
||||
class AuditPostType(wtypes.Base):
|
||||
|
||||
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=False)
|
||||
@@ -144,8 +129,15 @@ class AuditPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def validate(patch):
|
||||
serialized_patch = {'path': patch.path, 'op': patch.op}
|
||||
if patch.path in AuditPatchType.mandatory_attrs():
|
||||
|
||||
def is_new_state_none(p):
|
||||
return p.path == '/state' and p.op == 'replace' and p.value is None
|
||||
|
||||
serialized_patch = {'path': patch.path,
|
||||
'op': patch.op,
|
||||
'value': patch.value}
|
||||
if (patch.path in AuditPatchType.mandatory_attrs() or
|
||||
is_new_state_none(patch)):
|
||||
msg = _("%(field)s can't be updated.")
|
||||
raise exception.PatchError(
|
||||
patch=serialized_patch,
|
||||
@@ -572,21 +564,22 @@ class AuditsController(rest.RestController):
|
||||
|
||||
try:
|
||||
audit_dict = audit_to_update.as_dict()
|
||||
|
||||
initial_state = audit_dict['state']
|
||||
new_state = api_utils.get_patch_value(patch, 'state')
|
||||
if not api_utils.check_audit_state_transition(
|
||||
patch, initial_state):
|
||||
error_message = _("State transition not allowed: "
|
||||
"(%(initial_state)s -> %(new_state)s)")
|
||||
raise exception.PatchError(
|
||||
patch=patch,
|
||||
reason=error_message % dict(
|
||||
initial_state=initial_state, new_state=new_state))
|
||||
|
||||
audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch))
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
initial_state = audit_dict['state']
|
||||
new_state = api_utils.get_patch_value(patch, 'state')
|
||||
allowed_states = ALLOWED_AUDIT_TRANSITIONS.get(initial_state, [])
|
||||
if new_state is not None and new_state not in allowed_states:
|
||||
error_message = _("State transition not allowed: "
|
||||
"(%(initial_state)s -> %(new_state)s)")
|
||||
raise exception.PatchError(
|
||||
patch=patch,
|
||||
reason=error_message % dict(
|
||||
initial_state=initial_state, new_state=new_state))
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.Audit.fields:
|
||||
try:
|
||||
|
||||
@@ -79,6 +79,15 @@ def get_patch_value(patch, key):
|
||||
return p['value']
|
||||
|
||||
|
||||
def check_audit_state_transition(patch, initial):
|
||||
is_transition_valid = True
|
||||
state_value = get_patch_value(patch, "state")
|
||||
if state_value is not None:
|
||||
is_transition_valid = objects.audit.AuditStateTransitionManager(
|
||||
).check_transition(initial, state_value)
|
||||
return is_transition_valid
|
||||
|
||||
|
||||
def as_filters_dict(**filters):
|
||||
filters_dict = {}
|
||||
for filter_name, filter_value in filters.items():
|
||||
|
||||
@@ -49,9 +49,7 @@ class ContinuousAuditHandler(base.AuditHandler):
|
||||
def _is_audit_inactive(self, audit):
|
||||
audit = objects.Audit.get_by_uuid(
|
||||
self.context_show_deleted, audit.uuid)
|
||||
if audit.state in (objects.audit.State.CANCELLED,
|
||||
objects.audit.State.DELETED,
|
||||
objects.audit.State.FAILED):
|
||||
if objects.audit.AuditStateTransitionManager().is_inactive(audit):
|
||||
# if audit isn't in active states, audit's job must be removed to
|
||||
# prevent using of inactive audit in future.
|
||||
job_to_delete = [job for job in self.jobs
|
||||
|
||||
@@ -46,6 +46,9 @@ be one of the following:
|
||||
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
- **SUSPENDED** : the :ref:`Audit <audit_definition>` was in **ONGOING**
|
||||
state and was suspended by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
"""
|
||||
|
||||
import enum
|
||||
@@ -66,6 +69,7 @@ class State(object):
|
||||
CANCELLED = 'CANCELLED'
|
||||
DELETED = 'DELETED'
|
||||
PENDING = 'PENDING'
|
||||
SUSPENDED = 'SUSPENDED'
|
||||
|
||||
|
||||
class AuditType(enum.Enum):
|
||||
@@ -296,3 +300,25 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
||||
notifications.audit.send_delete(self._context, self)
|
||||
|
||||
_notify()
|
||||
|
||||
|
||||
class AuditStateTransitionManager(object):
|
||||
|
||||
TRANSITIONS = {
|
||||
State.PENDING: [State.ONGOING, State.CANCELLED],
|
||||
State.ONGOING: [State.FAILED, State.SUCCEEDED,
|
||||
State.CANCELLED, State.SUSPENDED],
|
||||
State.FAILED: [State.DELETED],
|
||||
State.SUCCEEDED: [State.DELETED],
|
||||
State.CANCELLED: [State.DELETED],
|
||||
State.SUSPENDED: [State.ONGOING, State.DELETED],
|
||||
}
|
||||
|
||||
INACTIVE_STATES = (State.CANCELLED, State.DELETED,
|
||||
State.FAILED, State.SUSPENDED)
|
||||
|
||||
def check_transition(self, initial, new):
|
||||
return new in self.TRANSITIONS.get(initial, [])
|
||||
|
||||
def is_inactive(self, audit):
|
||||
return audit.state in self.INACTIVE_STATES
|
||||
|
||||
@@ -345,23 +345,10 @@ class TestPatch(api_base.FunctionalTest):
|
||||
|
||||
|
||||
ALLOWED_TRANSITIONS = [
|
||||
{"original_state": objects.audit.State.PENDING,
|
||||
"new_state": objects.audit.State.ONGOING},
|
||||
{"original_state": objects.audit.State.PENDING,
|
||||
"new_state": objects.audit.State.CANCELLED},
|
||||
{"original_state": objects.audit.State.ONGOING,
|
||||
"new_state": objects.audit.State.FAILED},
|
||||
{"original_state": objects.audit.State.ONGOING,
|
||||
"new_state": objects.audit.State.SUCCEEDED},
|
||||
{"original_state": objects.audit.State.ONGOING,
|
||||
"new_state": objects.audit.State.CANCELLED},
|
||||
{"original_state": objects.audit.State.FAILED,
|
||||
"new_state": objects.audit.State.DELETED},
|
||||
{"original_state": objects.audit.State.SUCCEEDED,
|
||||
"new_state": objects.audit.State.DELETED},
|
||||
{"original_state": objects.audit.State.CANCELLED,
|
||||
"new_state": objects.audit.State.DELETED},
|
||||
]
|
||||
{"original_state": key, "new_state": value}
|
||||
for key, values in (
|
||||
objects.audit.AuditStateTransitionManager.TRANSITIONS.items())
|
||||
for value in values]
|
||||
|
||||
|
||||
class TestPatchStateTransitionDenied(api_base.FunctionalTest):
|
||||
|
||||
@@ -53,6 +53,10 @@ class TestDbAuditFilters(base.DbTestCase):
|
||||
self.audit3 = utils.create_test_audit(
|
||||
audit_template_id=self.audit_template.id, id=3, uuid=None,
|
||||
state=objects.audit.State.CANCELLED)
|
||||
with freezegun.freeze_time(self.FAKE_OLDER_DATE):
|
||||
self.audit4 = utils.create_test_audit(
|
||||
audit_template_id=self.audit_template.id, id=4, uuid=None,
|
||||
state=objects.audit.State.SUSPENDED)
|
||||
|
||||
def _soft_delete_audits(self):
|
||||
with freezegun.freeze_time(self.FAKE_TODAY):
|
||||
@@ -92,8 +96,9 @@ class TestDbAuditFilters(base.DbTestCase):
|
||||
res = self.dbapi.get_audit_list(
|
||||
self.context, filters={'deleted': False})
|
||||
|
||||
self.assertEqual([self.audit2['id'], self.audit3['id']],
|
||||
[r.id for r in res])
|
||||
self.assertEqual(
|
||||
[self.audit2['id'], self.audit3['id'], self.audit4['id']],
|
||||
[r.id for r in res])
|
||||
|
||||
def test_get_audit_list_filter_deleted_at_eq(self):
|
||||
self._soft_delete_audits()
|
||||
@@ -154,7 +159,7 @@ class TestDbAuditFilters(base.DbTestCase):
|
||||
self.context, filters={'created_at__lt': self.FAKE_TODAY})
|
||||
|
||||
self.assertEqual(
|
||||
[self.audit2['id'], self.audit3['id']],
|
||||
[self.audit2['id'], self.audit3['id'], self.audit4['id']],
|
||||
[r.id for r in res])
|
||||
|
||||
def test_get_audit_list_filter_created_at_lte(self):
|
||||
@@ -162,7 +167,7 @@ class TestDbAuditFilters(base.DbTestCase):
|
||||
self.context, filters={'created_at__lte': self.FAKE_OLD_DATE})
|
||||
|
||||
self.assertEqual(
|
||||
[self.audit2['id'], self.audit3['id']],
|
||||
[self.audit2['id'], self.audit3['id'], self.audit4['id']],
|
||||
[r.id for r in res])
|
||||
|
||||
def test_get_audit_list_filter_created_at_gt(self):
|
||||
@@ -230,18 +235,22 @@ class TestDbAuditFilters(base.DbTestCase):
|
||||
def test_get_audit_list_filter_state_in(self):
|
||||
res = self.dbapi.get_audit_list(
|
||||
self.context,
|
||||
filters={'state__in': (objects.audit.State.FAILED,
|
||||
objects.audit.State.CANCELLED)})
|
||||
filters={
|
||||
'state__in':
|
||||
objects.audit.AuditStateTransitionManager.INACTIVE_STATES
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
[self.audit2['id'], self.audit3['id']],
|
||||
[self.audit2['id'], self.audit3['id'], self.audit4['id']],
|
||||
[r.id for r in res])
|
||||
|
||||
def test_get_audit_list_filter_state_notin(self):
|
||||
res = self.dbapi.get_audit_list(
|
||||
self.context,
|
||||
filters={'state__notin': (objects.audit.State.FAILED,
|
||||
objects.audit.State.CANCELLED)})
|
||||
filters={
|
||||
'state__notin':
|
||||
objects.audit.AuditStateTransitionManager.INACTIVE_STATES
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
[self.audit1['id']],
|
||||
|
||||
@@ -274,16 +274,18 @@ class TestContinuousAuditHandler(base.DbTestCase):
|
||||
audit_handler = continuous.ContinuousAuditHandler(mock.MagicMock())
|
||||
mock_list.return_value = self.audits
|
||||
mock_jobs.return_value = mock.MagicMock()
|
||||
self.audits[1].state = objects.audit.State.CANCELLED
|
||||
calls = [mock.call(audit_handler.execute_audit, 'interval',
|
||||
args=[mock.ANY, mock.ANY],
|
||||
seconds=3600,
|
||||
name='execute_audit',
|
||||
next_run_time=mock.ANY)]
|
||||
audit_handler.launch_audits_periodically()
|
||||
m_add_job.assert_has_calls(calls)
|
||||
|
||||
audit_handler.update_audit_state(self.audits[1],
|
||||
objects.audit.State.CANCELLED)
|
||||
is_inactive = audit_handler._is_audit_inactive(self.audits[1])
|
||||
for state in [objects.audit.State.CANCELLED,
|
||||
objects.audit.State.SUSPENDED]:
|
||||
self.audits[1].state = state
|
||||
calls = [mock.call(audit_handler.execute_audit, 'interval',
|
||||
args=[mock.ANY, mock.ANY],
|
||||
seconds=3600,
|
||||
name='execute_audit',
|
||||
next_run_time=mock.ANY)]
|
||||
audit_handler.launch_audits_periodically()
|
||||
m_add_job.assert_has_calls(calls)
|
||||
|
||||
audit_handler.update_audit_state(self.audits[1], state)
|
||||
is_inactive = audit_handler._is_audit_inactive(self.audits[1])
|
||||
self.assertTrue(is_inactive)
|
||||
|
||||
@@ -27,9 +27,16 @@ class BaseInfraOptimTest(test.BaseTestCase):
|
||||
"""Base class for Infrastructure Optimization API tests."""
|
||||
|
||||
# States where the object is waiting for some event to perform a transition
|
||||
IDLE_STATES = ('RECOMMENDED', 'FAILED', 'SUCCEEDED', 'CANCELLED')
|
||||
IDLE_STATES = ('RECOMMENDED',
|
||||
'FAILED',
|
||||
'SUCCEEDED',
|
||||
'CANCELLED',
|
||||
'SUSPENDED')
|
||||
# States where the object can only be DELETED (end of its life-cycle)
|
||||
FINISHED_STATES = ('FAILED', 'SUCCEEDED', 'CANCELLED', 'SUPERSEDED')
|
||||
FINISHED_STATES = ('FAILED',
|
||||
'SUCCEEDED',
|
||||
'CANCELLED',
|
||||
'SUPERSEDED')
|
||||
|
||||
@classmethod
|
||||
def setup_credentials(cls):
|
||||
|
||||
@@ -29,7 +29,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
|
||||
"""Tests for audit."""
|
||||
|
||||
audit_states = ['ONGOING', 'SUCCEEDED', 'FAILED',
|
||||
'CANCELLED', 'DELETED', 'PENDING']
|
||||
'CANCELLED', 'DELETED', 'PENDING', 'SUSPENDED']
|
||||
|
||||
def assert_expected(self, expected, actual,
|
||||
keys=('created_at', 'updated_at',
|
||||
@@ -154,7 +154,7 @@ class TestShowListAudit(base.BaseInfraOptimTest):
|
||||
"""Tests for audit."""
|
||||
|
||||
audit_states = ['ONGOING', 'SUCCEEDED', 'FAILED',
|
||||
'CANCELLED', 'DELETED', 'PENDING']
|
||||
'CANCELLED', 'DELETED', 'PENDING', 'SUSPENDED']
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
|
||||
@@ -156,7 +156,7 @@ class TestExecuteBasicStrategy(base.BaseInfraOptimScenarioTest):
|
||||
self.fail("The audit has failed!")
|
||||
|
||||
_, finished_audit = self.client.show_audit(audit['uuid'])
|
||||
if finished_audit.get('state') in ('FAILED', 'CANCELLED'):
|
||||
if finished_audit.get('state') in ('FAILED', 'CANCELLED', 'SUSPENDED'):
|
||||
self.fail("The audit ended in unexpected state: %s!"
|
||||
% finished_audit.get('state'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user