Compare commits

...

23 Commits

Author SHA1 Message Date
Jenkins
1caf89686c Merge "Add checking audit state" into stable/ocata 2017-02-16 13:53:58 +00:00
Hidekazu Nakamura
ed3224835a Add checking audit state
This patch adds checking audit state when updating an existing audit
in accordance with audit state machine.

Closes-Bug: #1662406

Change-Id: I20610c83169b77f141974a5cebe33818a4bf0728
(cherry picked from commit 0d83354c57)
2017-02-16 13:16:06 +00:00
ericxiett
8a7e316f73 Fix that remove 'strategy' attribute does not work.
The 'strategy_id' attribute is the one that not exposes
to API. But the 'PATCH' API always update this attribute.
So this change does not work when choose 'remove' op.
This patch sets value for 'strategy_id' attribute.

Change-Id: I1597fb5d4985bb8271ad3cea7ea5f0adb7de65f4
Closes-Bug: #1662395
(cherry picked from commit e55c73be0e)
2017-02-16 13:15:48 +00:00
Jenkins
112ac3bbdf Merge "Fix log level error to warning" into stable/ocata 2017-02-14 10:27:17 +00:00
licanwei
d1ab697612 Fix the mapping between the instance and the node
The argument to the add_edge function should be instance.uuid
and node.uuid, not instance and node

Change-Id: Ida694f9158d3eb26e7f31062a18844472ea3c6fa
Closes-Bug: #1662810
2017-02-08 15:37:01 +00:00
Hidekazu Nakamura
095ca0ffb2 Fix log level error to warning
When action plan is currently running, new action plan is set as
SUPERSEDED and error log reported. This patch changes log level
from error to warning.

Change-Id: I931218843d8f09340bd5363256164807d514446b
Closes-Bug: #1662450
(cherry picked from commit 58711aaaec)
2017-02-08 00:05:44 +00:00
OpenStack Release Bot
e1131a65d8 Update UPPER_CONSTRAINTS_FILE for stable/ocata
Change-Id: Ie56e3186f2c9e42aff817fc7c71c4da93abb1c5f
2017-02-02 18:23:36 +00:00
OpenStack Release Bot
5e507e56f4 Update .gitreview for stable/ocata
Change-Id: I5a431c04e85183baf05736de3920d6610d4add03
2017-02-02 18:23:36 +00:00
Jenkins
62cb8a8d29 Merge "Add release note for action plan notifications" 2017-02-02 17:46:18 +00:00
Jenkins
50e5e86c75 Merge "Added action_plan.execution.* actions" 2017-02-02 17:44:52 +00:00
Jenkins
e3dd5c2a7e Merge "Added action_plan.create|update|delete notifs" 2017-02-02 17:44:08 +00:00
Jenkins
630c2cbb79 Merge "Updated graph model to use attr_dict" 2017-02-02 17:25:52 +00:00
Vincent Françoise
d49c6c16a6 Added action_plan.execution.* actions
Partially Implements: blueprint action-plan-versioned-notifications-api

Change-Id: I9bd346c19f1cafcaa720de554fd9c056c76de050
2017-02-02 18:05:26 +01:00
Vincent Françoise
e51e7e4317 Added action_plan.create|update|delete notifs
In this changeset, I added 3 notifications:

- action_plan.create
- action_plan.update
- action_plan.delete

Partially Implements: blueprint action-plan-versioned-notifications-api

Change-Id: I8821fc6f47e7486037839d81bed9e28020b02fdd
2017-02-02 18:05:24 +01:00
Antoine Cabot
244d28afa6 Add release note for action plan notifications
Change-Id: I7670e8866835b099a2f6d91b4c486e1056d52a97
2017-02-02 17:35:07 +01:00
David TARDIVEL
7ac1d0d048 Add first alembic version for db migration
Alembic provides command script to update a relational
database, using SQLAlchemy. I provide in this patchset the
initial version python script for create a Ocata db version.

Implements: blueprint db-migration

Change-Id: I7bda4286bc8141bb4fcfba3837070966ef506b5d
2017-02-01 14:40:11 +01:00
Vincent Françoise
52d701a56e Updated graph model to use attr_dict
This patchset makes use of a more idiomatic structure of networkx

Change-Id: Iccc4e9f0cc14cccadb2959ff8d68cd68367c4da3
2017-01-31 12:15:45 +01:00
Vincent Françoise
ea1fd5967a Fix context error for user
In this changeset, I fixed an error in the Watcher RequestContext
that was introduced by the release of oslo.context==2.12.X which
makes user_id an alias of user (among others)
https://github.com/openstack/oslo.context/blob/2.12.1/oslo_context/context.py#L258-L269

Change-Id: I4e35c675de7ee01db90f700eece0b28413b34c47
2017-01-31 11:07:03 +01:00
Jenkins
547bf0529f Merge "Fix multinode tempest test failure" 2017-01-27 15:31:02 +00:00
Vincent Françoise
659cbf3207 Idiomatic loop for calculate_num_migrations
Change-Id: Iddcb6dfbed2d2793df9c33408858f1a8d4600d10
2017-01-27 14:59:44 +01:00
Jenkins
eb5a362287 Merge "Documentation update" 2017-01-27 09:53:02 +00:00
Vincent Françoise
198d827645 Fix multinode tempest test failure
Change-Id: Ibb5cbb93d53ff45d66ab4f0c0d4b12e908170d35
2017-01-26 17:35:55 +01:00
David TARDIVEL
59c5adc8ad Documentation update
Here is a new architecture diagram with some updates on the
glossary and on descriptions of architecture elements.
I also fix some warning logs.

Closes-Bug: #1657405
Change-Id: I442082d702fc8667e9397c090da51ca1ead5d86e
2017-01-25 13:10:56 +01:00
78 changed files with 3420 additions and 602 deletions

View File

@@ -2,3 +2,4 @@
host=review.openstack.org host=review.openstack.org
port=29418 port=29418
project=openstack/watcher.git project=openstack/watcher.git
defaultbranch=stable/ocata

View File

@@ -188,7 +188,7 @@ function init_watcher {
recreate_database watcher recreate_database watcher
# Create watcher schema # Create watcher schema
$WATCHER_BIN_DIR/watcher-db-manage --config-file $WATCHER_CONF create_schema $WATCHER_BIN_DIR/watcher-db-manage --config-file $WATCHER_CONF upgrade head
fi fi
create_watcher_cache_dir create_watcher_cache_dir
} }

View File

@@ -0,0 +1,54 @@
{
"publisher_id": "infra-optim:node0",
"payload": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"display_name": "test strategy",
"name": "TEST",
"updated_at": null,
"parameters_spec": {},
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "StrategyPayload"
},
"created_at": null,
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"audit_type": "ONESHOT",
"scope": [],
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"parameters": {},
"interval": null,
"deleted_at": null,
"state": "PENDING",
"created_at": "2016-10-18T09:52:05Z",
"updated_at": null
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "TerseAuditPayload"
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"global_efficacy": {},
"deleted_at": null,
"state": "RECOMMENDED",
"updated_at": null
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "ActionPlanCreatePayload"
},
"priority": "INFO",
"message_id": "5148bff1-ea06-4ad6-8e4e-8c85ca5eb629",
"event_type": "action_plan.create",
"timestamp": "2016-10-18 09:52:05.219414"
}

View File

@@ -0,0 +1,54 @@
{
"publisher_id": "infra-optim:node0",
"timestamp": "2016-10-18 09:52:05.219414",
"payload": {
"watcher_object.data": {
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"created_at": "2016-10-18T09:52:05Z",
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.data": {
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"interval": null,
"audit_type": "ONESHOT",
"scope": [],
"updated_at": null,
"deleted_at": null,
"state": "PENDING",
"created_at": "2016-10-18T09:52:05Z",
"parameters": {}
},
"watcher_object.version": "1.0",
"watcher_object.name": "TerseAuditPayload",
"watcher_object.namespace": "watcher"
},
"global_efficacy": {},
"updated_at": null,
"deleted_at": null,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.data": {
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"created_at": "2016-10-18T09:52:05Z",
"name": "TEST",
"display_name": "test strategy",
"deleted_at": null,
"updated_at": null,
"parameters_spec": {}
},
"watcher_object.version": "1.0",
"watcher_object.name": "StrategyPayload",
"watcher_object.namespace": "watcher"
},
"state": "DELETED"
},
"watcher_object.version": "1.0",
"watcher_object.name": "ActionPlanDeletePayload",
"watcher_object.namespace": "watcher"
},
"event_type": "action_plan.delete",
"message_id": "3d137686-a1fd-4683-ab40-c4210aac2140",
"priority": "INFO"
}

View File

@@ -0,0 +1,55 @@
{
"event_type": "action_plan.execution.end",
"payload": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "ActionPlanActionPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null,
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "TerseAuditPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null,
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"scope": [],
"audit_type": "ONESHOT",
"state": "SUCCEEDED",
"parameters": {},
"interval": null,
"updated_at": null
}
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"fault": null,
"state": "ONGOING",
"global_efficacy": {},
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null,
"name": "TEST",
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"parameters_spec": {},
"display_name": "test strategy",
"updated_at": null
}
},
"updated_at": null
}
},
"priority": "INFO",
"message_id": "3984dc2b-8aef-462b-a220-8ae04237a56e",
"timestamp": "2016-10-18 09:52:05.219414",
"publisher_id": "infra-optim:node0"
}

View File

@@ -0,0 +1,65 @@
{
"event_type": "action_plan.execution.error",
"publisher_id": "infra-optim:node0",
"priority": "ERROR",
"message_id": "9a45c5ae-0e21-4300-8fa0-5555d52a66d9",
"payload": {
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher",
"watcher_object.name": "ActionPlanActionPayload",
"watcher_object.data": {
"fault": {
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher",
"watcher_object.name": "ExceptionPayload",
"watcher_object.data": {
"exception_message": "TEST",
"module_name": "watcher.tests.notifications.test_action_plan_notification",
"function_name": "test_send_action_plan_action_with_error",
"exception": "WatcherException"
}
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"created_at": "2016-10-18T09:52:05Z",
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher",
"watcher_object.name": "StrategyPayload",
"watcher_object.data": {
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"created_at": "2016-10-18T09:52:05Z",
"name": "TEST",
"updated_at": null,
"display_name": "test strategy",
"parameters_spec": {},
"deleted_at": null
}
},
"updated_at": null,
"deleted_at": null,
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher",
"watcher_object.name": "TerseAuditPayload",
"watcher_object.data": {
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"created_at": "2016-10-18T09:52:05Z",
"scope": [],
"updated_at": null,
"audit_type": "ONESHOT",
"interval": null,
"deleted_at": null,
"state": "PENDING"
}
},
"global_efficacy": {},
"state": "ONGOING"
}
},
"timestamp": "2016-10-18 09:52:05.219414"
}

View File

@@ -0,0 +1,55 @@
{
"event_type": "action_plan.execution.start",
"payload": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "ActionPlanActionPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null,
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "TerseAuditPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null,
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"scope": [],
"audit_type": "ONESHOT",
"state": "PENDING",
"parameters": {},
"interval": null,
"updated_at": null
}
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"fault": null,
"state": "ONGOING",
"global_efficacy": {},
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": null,
"name": "TEST",
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"parameters_spec": {},
"display_name": "test strategy",
"updated_at": null
}
},
"updated_at": null
}
},
"priority": "INFO",
"message_id": "3984dc2b-8aef-462b-a220-8ae04237a56e",
"timestamp": "2016-10-18 09:52:05.219414",
"publisher_id": "infra-optim:node0"
}

View File

@@ -0,0 +1,63 @@
{
"payload": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"audit_type": "ONESHOT",
"scope": [],
"created_at": "2016-10-18T09:52:05Z",
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"interval": null,
"updated_at": null,
"state": "PENDING",
"deleted_at": null,
"parameters": {}
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "TerseAuditPayload"
},
"created_at": "2016-10-18T09:52:05Z",
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"updated_at": null,
"state_update": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"old_state": "PENDING",
"state": "ONGOING"
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "ActionPlanStateUpdatePayload"
},
"state": "ONGOING",
"deleted_at": null,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.version": "1.0",
"watcher_object.data": {
"name": "TEST",
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"display_name": "test strategy",
"created_at": "2016-10-18T09:52:05Z",
"updated_at": null,
"deleted_at": null,
"parameters_spec": {}
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "StrategyPayload"
},
"global_efficacy": {}
},
"watcher_object.namespace": "watcher",
"watcher_object.name": "ActionPlanUpdatePayload"
},
"publisher_id": "infra-optim:node0",
"priority": "INFO",
"timestamp": "2016-10-18 09:52:05.219414",
"event_type": "action_plan.update",
"message_id": "0a8a7329-fd5a-4ec6-97d7-2b776ce51a4c"
}

View File

@@ -10,6 +10,7 @@
"state": "PENDING", "state": "PENDING",
"updated_at": null, "updated_at": null,
"deleted_at": null, "deleted_at": null,
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -26,6 +27,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -10,6 +10,7 @@
"state": "DELETED", "state": "DELETED",
"updated_at": null, "updated_at": null,
"deleted_at": null, "deleted_at": null,
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -26,6 +27,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -11,6 +11,7 @@
"updated_at": null, "updated_at": null,
"deleted_at": null, "deleted_at": null,
"fault": null, "fault": null,
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -27,6 +28,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -21,6 +21,7 @@
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0" "watcher_object.version": "1.0"
}, },
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -37,6 +38,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -11,6 +11,7 @@
"updated_at": null, "updated_at": null,
"deleted_at": null, "deleted_at": null,
"fault": null, "fault": null,
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -27,6 +28,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -11,6 +11,7 @@
"updated_at": null, "updated_at": null,
"deleted_at": null, "deleted_at": null,
"fault": null, "fault": null,
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -27,6 +28,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -21,6 +21,7 @@
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0" "watcher_object.version": "1.0"
}, },
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -37,6 +38,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -11,6 +11,7 @@
"updated_at": null, "updated_at": null,
"deleted_at": null, "deleted_at": null,
"fault": null, "fault": null,
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
@@ -27,6 +28,7 @@
}, },
"interval": null, "interval": null,
"scope": [], "scope": [],
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"parameters_spec": { "parameters_spec": {

View File

@@ -4,6 +4,7 @@
"payload": { "payload": {
"watcher_object.name": "AuditUpdatePayload", "watcher_object.name": "AuditUpdatePayload",
"watcher_object.data": { "watcher_object.data": {
"strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"strategy": { "strategy": {
"watcher_object.name": "StrategyPayload", "watcher_object.name": "StrategyPayload",
"watcher_object.data": { "watcher_object.data": {
@@ -36,6 +37,7 @@
"scope": [], "scope": [],
"created_at": "2016-11-04T16:51:21Z", "created_at": "2016-11-04T16:51:21Z",
"uuid": "f1e0d912-afd9-4bf2-91ef-c99cd08cc1ef", "uuid": "f1e0d912-afd9-4bf2-91ef-c99cd08cc1ef",
"goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"goal": { "goal": {
"watcher_object.name": "GoalPayload", "watcher_object.name": "GoalPayload",
"watcher_object.data": { "watcher_object.data": {

View File

@@ -21,7 +21,7 @@ Overview
Below you will find a diagram, showing the main components of Watcher: Below you will find a diagram, showing the main components of Watcher:
.. image:: ./images/architecture.svg .. image:: ./images/architecture.svg
:width: 100% :width: 110%
.. _components_definition: .. _components_definition:
@@ -37,13 +37,12 @@ AMQP Bus
The AMQP message bus handles internal asynchronous communications between the The AMQP message bus handles internal asynchronous communications between the
different Watcher components. different Watcher components.
.. _cluster_history_db_definition: .. _cluster_datasource_definition:
Cluster History Database Datasource
------------------------ ----------
This component stores the data related to the This component stores the metrics related to the cluster.
:ref:`Cluster History <cluster_history_definition>`.
It can potentially rely on any appropriate storage system (InfluxDB, OpenTSDB, It can potentially rely on any appropriate storage system (InfluxDB, OpenTSDB,
MongoDB,...) but will probably be more performant when using MongoDB,...) but will probably be more performant when using
@@ -51,14 +50,6 @@ MongoDB,...) but will probably be more performant when using
which are optimized for handling time series data, which are arrays of numbers which are optimized for handling time series data, which are arrays of numbers
indexed by time (a datetime or a datetime range). indexed by time (a datetime or a datetime range).
.. _cluster_model_db_definition:
Cluster Model Database
------------------------
This component stores the data related to the
:ref:`Cluster Data Model <cluster_data_model_definition>`.
.. _archi_watcher_api_definition: .. _archi_watcher_api_definition:
Watcher API Watcher API
@@ -193,8 +184,8 @@ data:
:ref:`Managed resources <managed_resource_definition>` (e.g., the data stored :ref:`Managed resources <managed_resource_definition>` (e.g., the data stored
in the Nova database). These models gives a strategy the ability to reason on in the Nova database). These models gives a strategy the ability to reason on
the current state of a given :ref:`cluster <cluster_definition>`. the current state of a given :ref:`cluster <cluster_definition>`.
- The data stored in the :ref:`Cluster History Database - The data stored in the :ref:`Cluster Datasource
<cluster_history_db_definition>` which provides information about the past of <cluster_datasource_definition>` which provides information about the past of
the :ref:`Cluster <cluster_definition>`. the :ref:`Cluster <cluster_definition>`.
Here below is a sequence diagram showing how the Decision Engine builds and Here below is a sequence diagram showing how the Decision Engine builds and
@@ -452,6 +443,10 @@ state may be one of the following:
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in - **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
**RECOMMENDED**, **PENDING** or **ONGOING** state and was cancelled by the **RECOMMENDED**, **PENDING** or **ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>` :ref:`Administrator <administrator_definition>`
- **SUPERSEDED** : the :ref:`Action Plan <action_plan_definition>` was in
RECOMMENDED state and was automatically superseded by Watcher, due to an
expiration delay or an update of the
:ref:`Cluster data model <cluster_data_model_definition>`
The following diagram shows the different possible states of an The following diagram shows the different possible states of an

View File

@@ -271,57 +271,44 @@ requires new metrics not covered by Ceilometer, you can add them through a
.. _`Ceilometer plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html .. _`Ceilometer plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py .. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
Read usage metrics using the Watcher Datasource Helper
------------------------------------------------------
Read usage metrics using the Python binding The following code snippet shows how to invoke a Datasource Helper class:
-------------------------------------------
You can find the information about the Ceilometer Python binding on the
OpenStack `ceilometer client python API documentation
<http://docs.openstack.org/developer/python-ceilometerclient/api.html>`_
To facilitate the process, Watcher provides the ``osc`` attribute to every
strategy which includes clients to major OpenStack services, including
Ceilometer. So to access it within your strategy, you can do the following:
.. code-block:: py .. code-block:: py
# Within your strategy "execute()" from watcher.datasource import ceilometer as ceil
cclient = self.osc.ceilometer from watcher.datasource import monasca as mon
# TODO: Do something here
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@property
def monasca(self):
if self._monasca is None:
self._monasca = mon.MonascaHelper(osc=self.osc)
return self._monasca
Using that you can now query the values for that specific metric: Using that you can now query the values for that specific metric:
.. code-block:: py .. code-block:: py
query = None # e.g. [{'field': 'foo', 'op': 'le', 'value': 34},] if self.config.datasource == "ceilometer":
value_cpu = cclient.samples.list( resource_id = "%s_%s" % (node.uuid, node.hostname)
meter_name='cpu_util', return self.ceilometer.statistic_aggregation(
limit=10, q=query) resource_id=resource_id,
meter_name='compute.node.cpu.percent',
period="7200",
Read usage metrics using the Watcher Cluster History Helper aggregate='avg',
----------------------------------------------------------- )
elif self.config.datasource == "monasca":
Here below is the abstract ``BaseClusterHistory`` class of the Helper. statistics = self.monasca.statistic_aggregation(
meter_name='compute.node.cpu.percent',
.. autoclass:: watcher.decision_engine.cluster.history.base.BaseClusterHistory dimensions=dict(hostname=node.uuid),
:members: period=7200,
:noindex: aggregate='avg'
)
The following code snippet shows how to create a Cluster History class:
.. code-block:: py
from watcher.decision_engine.cluster.history import ceilometer as ceil
query_history = ceil.CeilometerClusterHistory()
Using that you can now query the values for that specific metric:
.. code-block:: py
query_history.statistic_aggregation(resource_id=compute_node.uuid,
meter_name='compute.node.cpu.percent',
period="7200",
aggregate='avg'
)

View File

@@ -101,12 +101,6 @@ Cluster Data Model (CDM)
.. watcher-term:: watcher.decision_engine.model.collector.base .. watcher-term:: watcher.decision_engine.model.collector.base
.. _cluster_history_definition:
Cluster History
===============
.. watcher-term:: watcher.decision_engine.cluster.history.base
.. _controller_node_definition: .. _controller_node_definition:

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -9,8 +9,10 @@ FAILED --> DELETED : Administrator removes\nAction Plan
SUCCEEDED --> DELETED : Administrator removes\nAction Plan SUCCEEDED --> DELETED : Administrator removes\nAction Plan
ONGOING --> CANCELLED : Administrator cancels\nAction Plan ONGOING --> CANCELLED : Administrator cancels\nAction Plan
RECOMMENDED --> CANCELLED : Administrator cancels\nAction Plan RECOMMENDED --> CANCELLED : Administrator cancels\nAction Plan
RECOMMENDED --> SUPERSEDED : The Watcher Decision Engine supersedes\nAction Plan
PENDING --> CANCELLED : Administrator cancels\nAction Plan PENDING --> CANCELLED : Administrator cancels\nAction Plan
CANCELLED --> DELETED CANCELLED --> DELETED
SUPERSEDED --> DELETED
DELETED --> [*] DELETED --> [*]
@enduml @enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -60,6 +60,7 @@ table(audits) {
interval : Integer, nullable interval : Integer, nullable
parameters : JSONEncodedDict, nullable parameters : JSONEncodedDict, nullable
scope : JSONEncodedList, nullable scope : JSONEncodedList, nullable
auto_trigger: Boolean
created_at : DateTime created_at : DateTime
updated_at : DateTime updated_at : DateTime
@@ -73,7 +74,6 @@ table(action_plans) {
foreign_key("audit_id : Integer, nullable") foreign_key("audit_id : Integer, nullable")
foreign_key("strategy_id : Integer") foreign_key("strategy_id : Integer")
uuid : String[36] uuid : String[36]
first_action_id : Integer
state : String[20], nullable state : String[20], nullable
global_efficacy : JSONEncodedDict, nullable global_efficacy : JSONEncodedDict, nullable
@@ -91,7 +91,7 @@ table(actions) {
action_type : String[255] action_type : String[255]
input_parameters : JSONEncodedDict, nullable input_parameters : JSONEncodedDict, nullable
state : String[20], nullable state : String[20], nullable
next : String[36], nullable parents : JSONEncodedList, nullable
created_at : DateTime created_at : DateTime
updated_at : DateTime updated_at : DateTime

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,3 @@
---
features:
- Add notifications related to Action plan object.

View File

@@ -6,7 +6,7 @@ skipsdist = True
[testenv] [testenv]
usedevelop = True usedevelop = True
whitelist_externals = find whitelist_externals = find
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/ocata} {opts} {packages}
setenv = setenv =
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt deps = -r{toxinidir}/test-requirements.txt

View File

@@ -455,7 +455,8 @@ class ActionPlansController(rest.RestController):
:param action_plan_uuid: UUID of a action. :param action_plan_uuid: UUID of a action.
""" """
context = pecan.request.context context = pecan.request.context
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid) action_plan = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
policy.enforce(context, 'action_plan:delete', action_plan, policy.enforce(context, 'action_plan:delete', action_plan,
action='action_plan:delete') action='action_plan:delete')
@@ -474,8 +475,8 @@ class ActionPlansController(rest.RestController):
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
context = pecan.request.context context = pecan.request.context
action_plan_to_update = api_utils.get_resource('ActionPlan', action_plan_to_update = api_utils.get_resource(
action_plan_uuid) 'ActionPlan', action_plan_uuid, eager=True)
policy.enforce(context, 'action_plan:update', action_plan_to_update, policy.enforce(context, 'action_plan:update', action_plan_to_update,
action='action_plan:update') action='action_plan:update')

View File

@@ -50,6 +50,21 @@ from watcher.decision_engine import rpcapi
from watcher import objects 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): class AuditPostType(wtypes.Base):
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=False) audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=False)
@@ -561,6 +576,17 @@ class AuditsController(rest.RestController):
except api_utils.JSONPATCH_EXCEPTIONS as e: except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=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 # Update only the fields that have changed
for field in objects.Audit.fields: for field in objects.Audit.fields:
try: try:

View File

@@ -333,6 +333,7 @@ class AuditTemplate(base.APIBase):
self.fields.append('goal_id') self.fields.append('goal_id')
self.fields.append('strategy_id') self.fields.append('strategy_id')
setattr(self, 'strategy_id', kwargs.get('strategy_id', wtypes.Unset))
# goal_uuid & strategy_uuid are not part of # goal_uuid & strategy_uuid are not part of
# objects.AuditTemplate.fields because they're API-only attributes. # objects.AuditTemplate.fields because they're API-only attributes.

View File

@@ -73,6 +73,12 @@ def apply_jsonpatch(doc, patch):
return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch)) return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch))
def get_patch_value(patch, key):
for p in patch:
if p['op'] == 'replace' and p['path'] == '/%s' % key:
return p['value']
def as_filters_dict(**filters): def as_filters_dict(**filters):
filters_dict = {} filters_dict = {}
for filter_name, filter_value in filters.items(): for filter_name, filter_value in filters.items():

View File

@@ -20,32 +20,47 @@ from oslo_log import log
from watcher.applier.action_plan import base from watcher.applier.action_plan import base
from watcher.applier import default from watcher.applier import default
from watcher import notifications
from watcher import objects from watcher import objects
from watcher.objects import fields
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class DefaultActionPlanHandler(base.BaseActionPlanHandler): class DefaultActionPlanHandler(base.BaseActionPlanHandler):
def __init__(self, context, service, action_plan_uuid): def __init__(self, context, service, action_plan_uuid):
super(DefaultActionPlanHandler, self).__init__() super(DefaultActionPlanHandler, self).__init__()
self.ctx = context self.ctx = context
self.service = service self.service = service
self.action_plan_uuid = action_plan_uuid self.action_plan_uuid = action_plan_uuid
def update_action_plan(self, uuid, state):
action_plan = objects.ActionPlan.get_by_uuid(self.ctx, uuid)
action_plan.state = state
action_plan.save()
def execute(self): def execute(self):
try: try:
self.update_action_plan(self.action_plan_uuid, action_plan = objects.ActionPlan.get_by_uuid(
objects.action_plan.State.ONGOING) self.ctx, self.action_plan_uuid, eager=True)
action_plan.state = objects.action_plan.State.ONGOING
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
action=fields.NotificationAction.EXECUTION,
phase=fields.NotificationPhase.START)
applier = default.DefaultApplier(self.ctx, self.service) applier = default.DefaultApplier(self.ctx, self.service)
applier.execute(self.action_plan_uuid) applier.execute(self.action_plan_uuid)
state = objects.action_plan.State.SUCCEEDED
action_plan.state = objects.action_plan.State.SUCCEEDED
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
action=fields.NotificationAction.EXECUTION,
phase=fields.NotificationPhase.END)
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
state = objects.action_plan.State.FAILED action_plan.state = objects.action_plan.State.FAILED
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
action=fields.NotificationAction.EXECUTION,
priority=fields.NotificationPriority.ERROR,
phase=fields.NotificationPhase.ERROR)
finally: finally:
self.update_action_plan(self.action_plan_uuid, state) action_plan.save()

View File

@@ -56,7 +56,7 @@ class DefaultApplier(base.BaseApplier):
def execute(self, action_plan_uuid): def execute(self, action_plan_uuid):
LOG.debug("Executing action plan %s ", action_plan_uuid) LOG.debug("Executing action plan %s ", action_plan_uuid)
filters = {'action_plan_uuid': action_plan_uuid} filters = {'action_plan_uuid': action_plan_uuid}
actions = objects.Action.list(self.context, actions = objects.Action.list(self.context, filters=filters)
filters=filters)
return self.engine.execute(actions) return self.engine.execute(actions)

View File

@@ -71,7 +71,7 @@ def add_command_parsers(subparsers):
"Optionally, use --revision to specify an alembic revision " "Optionally, use --revision to specify an alembic revision "
"string to upgrade to.") "string to upgrade to.")
parser.set_defaults(func=DBCommand.upgrade) parser.set_defaults(func=DBCommand.upgrade)
parser.add_argument('--revision', nargs='?') parser.add_argument('revision', nargs='?')
parser = subparsers.add_parser( parser = subparsers.add_parser(
'downgrade', 'downgrade',
@@ -79,10 +79,10 @@ def add_command_parsers(subparsers):
"While optional, one should generally use --revision to " "While optional, one should generally use --revision to "
"specify the alembic revision string to downgrade to.") "specify the alembic revision string to downgrade to.")
parser.set_defaults(func=DBCommand.downgrade) parser.set_defaults(func=DBCommand.downgrade)
parser.add_argument('--revision', nargs='?') parser.add_argument('revision', nargs='?')
parser = subparsers.add_parser('stamp') parser = subparsers.add_parser('stamp')
parser.add_argument('--revision', nargs='?') parser.add_argument('revision', nargs='?')
parser.set_defaults(func=DBCommand.stamp) parser.set_defaults(func=DBCommand.stamp)
parser = subparsers.add_parser( parser = subparsers.add_parser(

View File

@@ -75,7 +75,7 @@ class RequestContext(context.RequestContext):
self.domain_name = domain_name self.domain_name = domain_name
self.domain_id = domain_id self.domain_id = domain_id
self.auth_token_info = auth_token_info self.auth_token_info = auth_token_info
self.user_id = user_id self.user_id = user_id or user
self.project_id = project_id self.project_id = project_id
if not timestamp: if not timestamp:
timestamp = timeutils.utcnow() timestamp = timeutils.utcnow()

View File

@@ -174,6 +174,14 @@ class EagerlyLoadedAuditRequired(InvalidAudit):
msg_fmt = _("Audit %(audit)s was not eagerly loaded") msg_fmt = _("Audit %(audit)s was not eagerly loaded")
class InvalidActionPlan(Invalid):
msg_fmt = _("Action plan %(action_plan)s is invalid")
class EagerlyLoadedActionPlanRequired(InvalidActionPlan):
msg_fmt = _("Action plan %(action_plan)s was not eagerly loaded")
class InvalidUUID(Invalid): class InvalidUUID(Invalid):
msg_fmt = _("Expected a uuid but received %(uuid)s") msg_fmt = _("Expected a uuid but received %(uuid)s")
@@ -402,11 +410,15 @@ class WildcardCharacterIsUsed(WatcherException):
# Model # Model
class InstanceNotFound(WatcherException): class ComputeResourceNotFound(WatcherException):
msg_fmt = _("The compute resource '%(name)s' could not be found")
class InstanceNotFound(ComputeResourceNotFound):
msg_fmt = _("The instance '%(name)s' could not be found") msg_fmt = _("The instance '%(name)s' could not be found")
class ComputeNodeNotFound(WatcherException): class ComputeNodeNotFound(ComputeResourceNotFound):
msg_fmt = _("The compute node %(name)s could not be found") msg_fmt = _("The compute node %(name)s could not be found")

View File

@@ -1,15 +0,0 @@
Please see https://alembic.readthedocs.org/en/latest/index.html for general documentation
To create alembic migrations use:
$ watcher-db-manage revision --message "description of revision" --autogenerate
Stamp db with most recent migration version, without actually running migrations
$ watcher-db-manage stamp head
Upgrade can be performed by:
$ watcher-db-manage upgrade
$ watcher-db-manage upgrade head
Downgrading db:
$ watcher-db-manage downgrade
$ watcher-db-manage downgrade base

View File

@@ -0,0 +1,62 @@
The migrations in the alembic/versions contain the changes needed to migrate
from older Watcher releases to newer versions. A migration occurs by executing
a script that details the changes needed to upgrade/downgrade the database. The
migration scripts are ordered so that multiple scripts can run sequentially to
update the database. The scripts are executed by Watcher's migration wrapper
which uses the Alembic library to manage the migration. Watcher supports
migration from Ocata or later.
If you are a deployer or developer and want to migrate from Ocata to later
release you must first add version tracking to the database::
$ watcher-db-manage --config-file /path/to/watcher.conf stamp ocata
You can upgrade to the latest database version via::
$ watcher-db-manage --config-file /path/to/watcher.conf upgrade head
To check the current database version::
$ watcher-db-manage --config-file /path/to/watcher.conf current
To create a script to run the migration offline::
$ watcher-db-manage --config-file /path/to/watcher.conf upgrade head --sql
To run the offline migration between specific migration versions::
$ watcher-db-manage --config-file /path/to/watcher.conf upgrade \
<start version>:<end version> --sql
Upgrade the database incrementally::
$ watcher-db-manage --config-file /path/to/watcher.conf upgrade --delta \
<# of revs>
Downgrade the database by a certain number of revisions::
$ watcher-db-manage --config-file /path/to/watcher.conf downgrade --delta \
<# of revs>
Create new revision::
$ watcher-db-manage --config-file /path/to/watcher.conf revision \
-m "description of revision" --autogenerate
Create a blank file::
$ watcher-db-manage --config-file /path/to/watcher.conf revision \
-m "description of revision"
Please see https://alembic.readthedocs.org/en/latest/index.html for general
documentation

View File

@@ -0,0 +1,203 @@
"""ocata release
Revision ID: 9894235b4278
Revises: None
Create Date: 2017-02-01 09:40:05.065981
"""
from alembic import op
import oslo_db
import sqlalchemy as sa
from watcher.db.sqlalchemy import models
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
def upgrade():
op.create_table(
'goals',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=63), nullable=False),
sa.Column('display_name', sa.String(length=63), nullable=False),
sa.Column('efficacy_specification', models.JSONEncodedList(),
nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', 'deleted', name='uniq_goals0name'),
sa.UniqueConstraint('uuid', name='uniq_goals0uuid')
)
op.create_table(
'scoring_engines',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=63), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('metainfo', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', 'deleted',
name='uniq_scoring_engines0name'),
sa.UniqueConstraint('uuid', name='uniq_scoring_engines0uuid')
)
op.create_table(
'services',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('host', sa.String(length=255), nullable=False),
sa.Column('last_seen_up', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('host', 'name', 'deleted',
name='uniq_services0host0name0deleted')
)
op.create_table(
'strategies',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=63), nullable=False),
sa.Column('display_name', sa.String(length=63), nullable=False),
sa.Column('goal_id', sa.Integer(), nullable=False),
sa.Column('parameters_spec', models.JSONEncodedDict(),
nullable=True),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', 'deleted', name='uniq_strategies0name'),
sa.UniqueConstraint('uuid', name='uniq_strategies0uuid')
)
op.create_table(
'audit_templates',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=63), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('goal_id', sa.Integer(), nullable=False),
sa.Column('strategy_id', sa.Integer(), nullable=True),
sa.Column('scope', models.JSONEncodedList(),
nullable=True),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
sa.ForeignKeyConstraint(['strategy_id'], ['strategies.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', 'deleted',
name='uniq_audit_templates0name'),
sa.UniqueConstraint('uuid', name='uniq_audit_templates0uuid')
)
op.create_table(
'audits',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('audit_type', sa.String(length=20), nullable=True),
sa.Column('state', sa.String(length=20), nullable=True),
sa.Column('parameters', models.JSONEncodedDict(), nullable=True),
sa.Column('interval', sa.Integer(), nullable=True),
sa.Column('goal_id', sa.Integer(), nullable=False),
sa.Column('strategy_id', sa.Integer(), nullable=True),
sa.Column('scope', models.JSONEncodedList(), nullable=True),
sa.Column('auto_trigger', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
sa.ForeignKeyConstraint(['strategy_id'], ['strategies.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_audits0uuid')
)
op.create_table(
'action_plans',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('audit_id', sa.Integer(), nullable=False),
sa.Column('strategy_id', sa.Integer(), nullable=False),
sa.Column('state', sa.String(length=20), nullable=True),
sa.Column('global_efficacy', models.JSONEncodedDict(), nullable=True),
sa.ForeignKeyConstraint(['audit_id'], ['audits.id'], ),
sa.ForeignKeyConstraint(['strategy_id'], ['strategies.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_action_plans0uuid')
)
op.create_table(
'actions',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('action_plan_id', sa.Integer(), nullable=False),
sa.Column('action_type', sa.String(length=255), nullable=False),
sa.Column('input_parameters', models.JSONEncodedDict(), nullable=True),
sa.Column('state', sa.String(length=20), nullable=True),
sa.Column('parents', models.JSONEncodedList(), nullable=True),
sa.ForeignKeyConstraint(['action_plan_id'], ['action_plans.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_actions0uuid')
)
op.create_table(
'efficacy_indicators',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(),
nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=63), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('unit', sa.String(length=63), nullable=True),
sa.Column('value', sa.Numeric(), nullable=True),
sa.Column('action_plan_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['action_plan_id'], ['action_plans.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_efficacy_indicators0uuid')
)
def downgrade():
op.drop_table('efficacy_indicators')
op.drop_table('actions')
op.drop_table('action_plans')
op.drop_table('audits')
op.drop_table('audit_templates')
op.drop_table('strategies')
op.drop_table('services')
op.drop_table('scoring_engines')
op.drop_table('goals')

View File

@@ -54,7 +54,7 @@ def upgrade(revision, config=None):
revision = revision or 'head' revision = revision or 'head'
config = config or _alembic_config() config = config or _alembic_config()
alembic.command.upgrade(config, revision or 'head') alembic.command.upgrade(config, revision)
def create_schema(config=None, engine=None): def create_schema(config=None, engine=None):

View File

@@ -129,7 +129,7 @@ class AuditHandler(BaseAuditHandler):
solution = self.do_execute(audit, request_context) solution = self.do_execute(audit, request_context)
self.post_execute(audit, solution, request_context) self.post_execute(audit, solution, request_context)
except exception.ActionPlanIsOngoing as e: except exception.ActionPlanIsOngoing as e:
LOG.exception(e) LOG.warning(e)
if audit.audit_type == objects.audit.AuditType.ONESHOT.value: if audit.audit_type == objects.audit.AuditType.ONESHOT.value:
self.update_audit_state(audit, objects.audit.State.CANCELLED) self.update_audit_state(audit, objects.audit.State.CANCELLED)
except Exception as e: except Exception as e:

View File

@@ -215,8 +215,7 @@ class ModelBuilder(object):
compute_node = self.model.get_node_by_uuid( compute_node = self.model.get_node_by_uuid(
cnode_uuid) cnode_uuid)
# Connect the instance to its compute node # Connect the instance to its compute node
self.model.add_edge( self.model.map_instance(instance, compute_node)
instance, compute_node, label='RUNS_ON')
except exception.ComputeNodeNotFound: except exception.ComputeNodeNotFound:
continue continue

View File

@@ -30,7 +30,8 @@ LOG = log.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class Element(base.WatcherObject, base.WatcherObjectDictCompat): class Element(base.WatcherObject, base.WatcherObjectDictCompat,
base.WatcherComparableObject):
# Initial version # Initial version
VERSION = '1.0' VERSION = '1.0'

View File

@@ -57,13 +57,13 @@ class ModelRoot(nx.DiGraph, base.Model):
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def add_node(self, node): def add_node(self, node):
self.assert_node(node) self.assert_node(node)
super(ModelRoot, self).add_node(node) super(ModelRoot, self).add_node(node.uuid, node)
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def remove_node(self, node): def remove_node(self, node):
self.assert_node(node) self.assert_node(node)
try: try:
super(ModelRoot, self).remove_node(node) super(ModelRoot, self).remove_node(node.uuid)
except nx.NetworkXError as exc: except nx.NetworkXError as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.ComputeNodeNotFound(name=node.uuid) raise exception.ComputeNodeNotFound(name=node.uuid)
@@ -72,7 +72,7 @@ class ModelRoot(nx.DiGraph, base.Model):
def add_instance(self, instance): def add_instance(self, instance):
self.assert_instance(instance) self.assert_instance(instance)
try: try:
super(ModelRoot, self).add_node(instance) super(ModelRoot, self).add_node(instance.uuid, instance)
except nx.NetworkXError as exc: except nx.NetworkXError as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.InstanceNotFound(name=instance.uuid) raise exception.InstanceNotFound(name=instance.uuid)
@@ -80,7 +80,7 @@ class ModelRoot(nx.DiGraph, base.Model):
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def remove_instance(self, instance): def remove_instance(self, instance):
self.assert_instance(instance) self.assert_instance(instance)
super(ModelRoot, self).remove_node(instance) super(ModelRoot, self).remove_node(instance.uuid)
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def map_instance(self, instance, node): def map_instance(self, instance, node):
@@ -98,7 +98,7 @@ class ModelRoot(nx.DiGraph, base.Model):
self.assert_node(node) self.assert_node(node)
self.assert_instance(instance) self.assert_instance(instance)
self.add_edge(instance, node) self.add_edge(instance.uuid, node.uuid)
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def unmap_instance(self, instance, node): def unmap_instance(self, instance, node):
@@ -107,7 +107,7 @@ class ModelRoot(nx.DiGraph, base.Model):
if isinstance(node, six.string_types): if isinstance(node, six.string_types):
node = self.get_node_by_uuid(node) node = self.get_node_by_uuid(node)
self.remove_edge(instance, node) self.remove_edge(instance.uuid, node.uuid)
def delete_instance(self, instance, node=None): def delete_instance(self, instance, node=None):
self.assert_instance(instance) self.assert_instance(instance)
@@ -130,55 +130,59 @@ class ModelRoot(nx.DiGraph, base.Model):
return False return False
# unmap # unmap
self.remove_edge(instance, source_node) self.remove_edge(instance.uuid, source_node.uuid)
# map # map
self.add_edge(instance, destination_node) self.add_edge(instance.uuid, destination_node.uuid)
return True return True
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def get_all_compute_nodes(self): def get_all_compute_nodes(self):
return {cn.uuid: cn for cn in self.nodes() return {uuid: cn for uuid, cn in self.nodes(data=True)
if isinstance(cn, element.ComputeNode)} if isinstance(cn, element.ComputeNode)}
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def get_node_by_uuid(self, uuid): def get_node_by_uuid(self, uuid):
for graph_node in self.nodes(): try:
if (isinstance(graph_node, element.ComputeNode) and return self._get_by_uuid(uuid)
graph_node.uuid == uuid): except exception.ComputeResourceNotFound:
return graph_node raise exception.ComputeNodeNotFound(name=uuid)
raise exception.ComputeNodeNotFound(name=uuid)
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def get_instance_by_uuid(self, uuid): def get_instance_by_uuid(self, uuid):
return self._get_instance_by_uuid(uuid) try:
return self._get_by_uuid(uuid)
except exception.ComputeResourceNotFound:
raise exception.InstanceNotFound(name=uuid)
def _get_instance_by_uuid(self, uuid): def _get_by_uuid(self, uuid):
for graph_node in self.nodes(): try:
if (isinstance(graph_node, element.Instance) and return self.node[uuid]
graph_node.uuid == str(uuid)): except Exception as exc:
return graph_node LOG.exception(exc)
raise exception.InstanceNotFound(name=uuid) raise exception.ComputeResourceNotFound(name=uuid)
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def get_node_by_instance_uuid(self, instance_uuid): def get_node_by_instance_uuid(self, instance_uuid):
instance = self._get_instance_by_uuid(instance_uuid) instance = self._get_by_uuid(instance_uuid)
for node in self.neighbors(instance): for node_uuid in self.neighbors(instance.uuid):
node = self._get_by_uuid(node_uuid)
if isinstance(node, element.ComputeNode): if isinstance(node, element.ComputeNode):
return node return node
raise exception.ComputeNodeNotFound(name=instance_uuid) raise exception.ComputeNodeNotFound(name=instance_uuid)
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def get_all_instances(self): def get_all_instances(self):
return {inst.uuid: inst for inst in self.nodes() return {uuid: inst for uuid, inst in self.nodes(data=True)
if isinstance(inst, element.Instance)} if isinstance(inst, element.Instance)}
@lockutils.synchronized("model_root") @lockutils.synchronized("model_root")
def get_node_instances(self, node): def get_node_instances(self, node):
self.assert_node(node) self.assert_node(node)
node_instances = [] node_instances = []
for neighbor in self.predecessors(node): for instance_uuid in self.predecessors(node.uuid):
if isinstance(neighbor, element.Instance): instance = self._get_by_uuid(instance_uuid)
node_instances.append(neighbor) if isinstance(instance, element.Instance):
node_instances.append(instance)
return node_instances return node_instances

View File

@@ -41,7 +41,7 @@ class WeightPlanner(base.BasePlanner):
*Limitations* *Limitations*
- This planner requires to have action_weights and parallelization configs - This planner requires to have action_weights and parallelization configs
tuned well. tuned well.
""" """
def __init__(self, config): def __init__(self, config):

View File

@@ -169,7 +169,7 @@ class DefaultScope(base.BaseScope):
try: try:
node_name = cluster_model.get_node_by_instance_uuid( node_name = cluster_model.get_node_by_instance_uuid(
instance_uuid).uuid instance_uuid).uuid
except exception.InstanceNotFound: except exception.ComputeResourceNotFound:
LOG.warning(_LW("The following instance %s cannot be found. " LOG.warning(_LW("The following instance %s cannot be found. "
"It might be deleted from CDM along with node" "It might be deleted from CDM along with node"
" instance was hosted on."), " instance was hosted on."),

View File

@@ -415,11 +415,11 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
sorted_score): sorted_score):
number_migrations = 0 number_migrations = 0
for mig_instance, __ in sorted_instances: for mig_instance, __ in sorted_instances:
for j in range(0, len(sorted_score)): for node_uuid, __ in sorted_score:
mig_source_node = self.compute_model.get_node_by_uuid( mig_source_node = self.compute_model.get_node_by_uuid(
node_to_release) node_to_release)
mig_destination_node = self.compute_model.get_node_by_uuid( mig_destination_node = self.compute_model.get_node_by_uuid(
sorted_score[j][0]) node_uuid)
result = self.check_migration( result = self.check_migration(
mig_source_node, mig_destination_node, mig_instance) mig_source_node, mig_destination_node, mig_instance)

View File

@@ -350,7 +350,7 @@ class Syncer(object):
for strategy_id, synced_strategy in self.strategy_mapping.items(): for strategy_id, synced_strategy in self.strategy_mapping.items():
filters = {"strategy_id": strategy_id} filters = {"strategy_id": strategy_id}
stale_action_plans = objects.ActionPlan.list( stale_action_plans = objects.ActionPlan.list(
self.ctx, filters=filters) self.ctx, filters=filters, eager=True)
# Update strategy IDs for all stale action plans (w/o saving) # Update strategy IDs for all stale action plans (w/o saving)
for action_plan in stale_action_plans: for action_plan in stale_action_plans:
@@ -369,7 +369,7 @@ class Syncer(object):
for audit_id, synced_audit in self.stale_audits_map.items(): for audit_id, synced_audit in self.stale_audits_map.items():
filters = {"audit_id": audit_id} filters = {"audit_id": audit_id}
stale_action_plans = objects.ActionPlan.list( stale_action_plans = objects.ActionPlan.list(
self.ctx, filters=filters) self.ctx, filters=filters, eager=True)
# Update audit IDs for all stale action plans (w/o saving) # Update audit IDs for all stale action plans (w/o saving)
for action_plan in stale_action_plans: for action_plan in stale_action_plans:
@@ -448,7 +448,7 @@ class Syncer(object):
audit.id].state = objects.audit.State.CANCELLED audit.id].state = objects.audit.State.CANCELLED
stale_action_plans = objects.ActionPlan.list( stale_action_plans = objects.ActionPlan.list(
self.ctx, filters=filters) self.ctx, filters=filters, eager=True)
for action_plan in stale_action_plans: for action_plan in stale_action_plans:
LOG.warning( LOG.warning(
_LW("Action Plan '%(action_plan)s' references a " _LW("Action Plan '%(action_plan)s' references a "

View File

@@ -20,6 +20,7 @@
# need to be changed after we moved these function inside the package # need to be changed after we moved these function inside the package
# Todo(gibi): remove these imports after legacy notifications using these are # Todo(gibi): remove these imports after legacy notifications using these are
# transformed to versioned notifications # transformed to versioned notifications
from watcher.notifications import action_plan # noqa
from watcher.notifications import audit # noqa from watcher.notifications import audit # noqa
from watcher.notifications import exception # noqa from watcher.notifications import exception # noqa
from watcher.notifications import goal # noqa from watcher.notifications import goal # noqa

View File

@@ -0,0 +1,312 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2017 b<>com
#
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
from watcher.common import context as wcontext
from watcher.common import exception
from watcher.notifications import audit as audit_notifications
from watcher.notifications import base as notificationbase
from watcher.notifications import exception as exception_notifications
from watcher.notifications import strategy as strategy_notifications
from watcher import objects
from watcher.objects import base
from watcher.objects import fields as wfields
CONF = cfg.CONF
@base.WatcherObjectRegistry.register_notification
class ActionPlanPayload(notificationbase.NotificationPayloadBase):
SCHEMA = {
'uuid': ('action_plan', 'uuid'),
'state': ('action_plan', 'state'),
'global_efficacy': ('action_plan', 'global_efficacy'),
'audit_uuid': ('audit', 'uuid'),
'strategy_uuid': ('strategy', 'uuid'),
'created_at': ('action_plan', 'created_at'),
'updated_at': ('action_plan', 'updated_at'),
'deleted_at': ('action_plan', 'deleted_at'),
}
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'uuid': wfields.UUIDField(),
'state': wfields.StringField(),
'global_efficacy': wfields.FlexibleDictField(nullable=True),
'audit_uuid': wfields.UUIDField(),
'strategy_uuid': wfields.UUIDField(),
'audit': wfields.ObjectField('TerseAuditPayload'),
'strategy': wfields.ObjectField('StrategyPayload'),
'created_at': wfields.DateTimeField(nullable=True),
'updated_at': wfields.DateTimeField(nullable=True),
'deleted_at': wfields.DateTimeField(nullable=True),
}
def __init__(self, action_plan, audit, strategy, **kwargs):
super(ActionPlanPayload, self).__init__(
audit=audit, strategy=strategy, **kwargs)
self.populate_schema(
action_plan=action_plan, audit=audit, strategy=strategy)
@base.WatcherObjectRegistry.register_notification
class ActionPlanStateUpdatePayload(notificationbase.NotificationPayloadBase):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'old_state': wfields.StringField(nullable=True),
'state': wfields.StringField(nullable=True),
}
@base.WatcherObjectRegistry.register_notification
class ActionPlanCreatePayload(ActionPlanPayload):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {}
def __init__(self, action_plan, audit, strategy):
super(ActionPlanCreatePayload, self).__init__(
action_plan=action_plan,
audit=audit,
strategy=strategy)
@base.WatcherObjectRegistry.register_notification
class ActionPlanUpdatePayload(ActionPlanPayload):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'state_update': wfields.ObjectField('ActionPlanStateUpdatePayload'),
}
def __init__(self, action_plan, state_update, audit, strategy):
super(ActionPlanUpdatePayload, self).__init__(
action_plan=action_plan,
state_update=state_update,
audit=audit,
strategy=strategy)
@base.WatcherObjectRegistry.register_notification
class ActionPlanActionPayload(ActionPlanPayload):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'fault': wfields.ObjectField('ExceptionPayload', nullable=True),
}
def __init__(self, action_plan, audit, strategy, **kwargs):
super(ActionPlanActionPayload, self).__init__(
action_plan=action_plan,
audit=audit,
strategy=strategy,
**kwargs)
@base.WatcherObjectRegistry.register_notification
class ActionPlanDeletePayload(ActionPlanPayload):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {}
def __init__(self, action_plan, audit, strategy):
super(ActionPlanDeletePayload, self).__init__(
action_plan=action_plan,
audit=audit,
strategy=strategy)
@notificationbase.notification_sample('action_plan-execution-error.json')
@notificationbase.notification_sample('action_plan-execution-end.json')
@notificationbase.notification_sample('action_plan-execution-start.json')
@base.WatcherObjectRegistry.register_notification
class ActionPlanActionNotification(notificationbase.NotificationBase):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': wfields.ObjectField('ActionPlanActionPayload')
}
@notificationbase.notification_sample('action_plan-create.json')
@base.WatcherObjectRegistry.register_notification
class ActionPlanCreateNotification(notificationbase.NotificationBase):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': wfields.ObjectField('ActionPlanCreatePayload')
}
@notificationbase.notification_sample('action_plan-update.json')
@base.WatcherObjectRegistry.register_notification
class ActionPlanUpdateNotification(notificationbase.NotificationBase):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': wfields.ObjectField('ActionPlanUpdatePayload')
}
@notificationbase.notification_sample('action_plan-delete.json')
@base.WatcherObjectRegistry.register_notification
class ActionPlanDeleteNotification(notificationbase.NotificationBase):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': wfields.ObjectField('ActionPlanDeletePayload')
}
def _get_common_payload(action_plan):
audit = None
strategy = None
try:
audit = action_plan.audit
strategy = action_plan.strategy
except NotImplementedError:
raise exception.EagerlyLoadedActionPlanRequired(
action_plan=action_plan.uuid)
goal = objects.Goal.get(
wcontext.make_context(show_deleted=True), audit.goal_id)
audit_payload = audit_notifications.TerseAuditPayload(
audit=audit, goal_uuid=goal.uuid)
strategy_payload = strategy_notifications.StrategyPayload(
strategy=strategy)
return audit_payload, strategy_payload
def send_create(context, action_plan, service='infra-optim', host=None):
"""Emit an action_plan.create notification."""
audit_payload, strategy_payload = _get_common_payload(action_plan)
versioned_payload = ActionPlanCreatePayload(
action_plan=action_plan,
audit=audit_payload,
strategy=strategy_payload,
)
notification = ActionPlanCreateNotification(
priority=wfields.NotificationPriority.INFO,
event_type=notificationbase.EventType(
object='action_plan',
action=wfields.NotificationAction.CREATE),
publisher=notificationbase.NotificationPublisher(
host=host or CONF.host,
binary=service),
payload=versioned_payload)
notification.emit(context)
def send_update(context, action_plan, service='infra-optim',
host=None, old_state=None):
"""Emit an action_plan.update notification."""
audit_payload, strategy_payload = _get_common_payload(action_plan)
state_update = ActionPlanStateUpdatePayload(
old_state=old_state,
state=action_plan.state if old_state else None)
versioned_payload = ActionPlanUpdatePayload(
action_plan=action_plan,
state_update=state_update,
audit=audit_payload,
strategy=strategy_payload,
)
notification = ActionPlanUpdateNotification(
priority=wfields.NotificationPriority.INFO,
event_type=notificationbase.EventType(
object='action_plan',
action=wfields.NotificationAction.UPDATE),
publisher=notificationbase.NotificationPublisher(
host=host or CONF.host,
binary=service),
payload=versioned_payload)
notification.emit(context)
def send_delete(context, action_plan, service='infra-optim', host=None):
"""Emit an action_plan.delete notification."""
audit_payload, strategy_payload = _get_common_payload(action_plan)
versioned_payload = ActionPlanDeletePayload(
action_plan=action_plan,
audit=audit_payload,
strategy=strategy_payload,
)
notification = ActionPlanDeleteNotification(
priority=wfields.NotificationPriority.INFO,
event_type=notificationbase.EventType(
object='action_plan',
action=wfields.NotificationAction.DELETE),
publisher=notificationbase.NotificationPublisher(
host=host or CONF.host,
binary=service),
payload=versioned_payload)
notification.emit(context)
def send_action_notification(context, action_plan, action, phase=None,
priority=wfields.NotificationPriority.INFO,
service='infra-optim', host=None):
"""Emit an action_plan action notification."""
audit_payload, strategy_payload = _get_common_payload(action_plan)
fault = None
if phase == wfields.NotificationPhase.ERROR:
fault = exception_notifications.ExceptionPayload.from_exception()
versioned_payload = ActionPlanActionPayload(
action_plan=action_plan,
audit=audit_payload,
strategy=strategy_payload,
fault=fault,
)
notification = ActionPlanActionNotification(
priority=priority,
event_type=notificationbase.EventType(
object='action_plan',
action=action,
phase=phase),
publisher=notificationbase.NotificationPublisher(
host=host or CONF.host,
binary=service),
payload=versioned_payload)
notification.emit(context)

View File

@@ -30,7 +30,7 @@ CONF = cfg.CONF
@base.WatcherObjectRegistry.register_notification @base.WatcherObjectRegistry.register_notification
class AuditPayload(notificationbase.NotificationPayloadBase): class TerseAuditPayload(notificationbase.NotificationPayloadBase):
SCHEMA = { SCHEMA = {
'uuid': ('audit', 'uuid'), 'uuid': ('audit', 'uuid'),
@@ -57,19 +57,54 @@ class AuditPayload(notificationbase.NotificationPayloadBase):
'scope': wfields.FlexibleListOfDictField(nullable=True), 'scope': wfields.FlexibleListOfDictField(nullable=True),
'goal_uuid': wfields.UUIDField(), 'goal_uuid': wfields.UUIDField(),
'strategy_uuid': wfields.UUIDField(nullable=True), 'strategy_uuid': wfields.UUIDField(nullable=True),
'goal': wfields.ObjectField('GoalPayload'),
'strategy': wfields.ObjectField('StrategyPayload', nullable=True),
'created_at': wfields.DateTimeField(nullable=True), 'created_at': wfields.DateTimeField(nullable=True),
'updated_at': wfields.DateTimeField(nullable=True), 'updated_at': wfields.DateTimeField(nullable=True),
'deleted_at': wfields.DateTimeField(nullable=True), 'deleted_at': wfields.DateTimeField(nullable=True),
} }
def __init__(self, audit, **kwargs): def __init__(self, audit, goal_uuid, strategy_uuid=None, **kwargs):
super(AuditPayload, self).__init__(**kwargs) super(TerseAuditPayload, self).__init__(
goal_uuid=goal_uuid, strategy_uuid=strategy_uuid, **kwargs)
self.populate_schema(audit=audit) self.populate_schema(audit=audit)
@base.WatcherObjectRegistry.register_notification
class AuditPayload(TerseAuditPayload):
SCHEMA = {
'uuid': ('audit', 'uuid'),
'audit_type': ('audit', 'audit_type'),
'state': ('audit', 'state'),
'parameters': ('audit', 'parameters'),
'interval': ('audit', 'interval'),
'scope': ('audit', 'scope'),
'created_at': ('audit', 'created_at'),
'updated_at': ('audit', 'updated_at'),
'deleted_at': ('audit', 'deleted_at'),
}
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'goal': wfields.ObjectField('GoalPayload'),
'strategy': wfields.ObjectField('StrategyPayload', nullable=True),
}
def __init__(self, audit, goal, strategy=None, **kwargs):
if not kwargs.get('goal_uuid'):
kwargs['goal_uuid'] = goal.uuid
if strategy and not kwargs.get('strategy_uuid'):
kwargs['strategy_uuid'] = strategy.uuid
super(AuditPayload, self).__init__(
audit=audit, goal=goal,
strategy=strategy, **kwargs)
@base.WatcherObjectRegistry.register_notification @base.WatcherObjectRegistry.register_notification
class AuditStateUpdatePayload(notificationbase.NotificationPayloadBase): class AuditStateUpdatePayload(notificationbase.NotificationPayloadBase):
# Version 1.0: Initial version # Version 1.0: Initial version
@@ -91,6 +126,7 @@ class AuditCreatePayload(AuditPayload):
super(AuditCreatePayload, self).__init__( super(AuditCreatePayload, self).__init__(
audit=audit, audit=audit,
goal=goal, goal=goal,
goal_uuid=goal.uuid,
strategy=strategy) strategy=strategy)
@@ -107,6 +143,7 @@ class AuditUpdatePayload(AuditPayload):
audit=audit, audit=audit,
state_update=state_update, state_update=state_update,
goal=goal, goal=goal,
goal_uuid=goal.uuid,
strategy=strategy) strategy=strategy)
@@ -122,6 +159,7 @@ class AuditActionPayload(AuditPayload):
super(AuditActionPayload, self).__init__( super(AuditActionPayload, self).__init__(
audit=audit, audit=audit,
goal=goal, goal=goal,
goal_uuid=goal.uuid,
strategy=strategy, strategy=strategy,
**kwargs) **kwargs)
@@ -136,6 +174,7 @@ class AuditDeletePayload(AuditPayload):
super(AuditDeletePayload, self).__init__( super(AuditDeletePayload, self).__init__(
audit=audit, audit=audit,
goal=goal, goal=goal,
goal_uuid=goal.uuid,
strategy=strategy) strategy=strategy)

View File

@@ -13,6 +13,7 @@
# under the License. # under the License.
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log
from watcher.common import exception from watcher.common import exception
from watcher.common import rpc from watcher.common import rpc
@@ -20,6 +21,7 @@ from watcher.objects import base
from watcher.objects import fields as wfields from watcher.objects import fields as wfields
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__)
# Definition of notification levels in increasing order of severity # Definition of notification levels in increasing order of severity
NOTIFY_LEVELS = { NOTIFY_LEVELS = {
@@ -59,7 +61,8 @@ class EventType(NotificationObject):
# Version 1.0: Initial version # Version 1.0: Initial version
# Version 1.1: Added STRATEGY action in NotificationAction enum # Version 1.1: Added STRATEGY action in NotificationAction enum
# Version 1.2: Added PLANNER action in NotificationAction enum # Version 1.2: Added PLANNER action in NotificationAction enum
VERSION = '1.2' # Version 1.3: Added EXECUTION action in NotificationAction enum
VERSION = '1.3'
fields = { fields = {
'object': wfields.StringField(), 'object': wfields.StringField(),
@@ -171,6 +174,7 @@ class NotificationBase(NotificationObject):
def _emit(self, context, event_type, publisher_id, payload): def _emit(self, context, event_type, publisher_id, payload):
notifier = rpc.get_notifier(publisher_id) notifier = rpc.get_notifier(publisher_id)
notify = getattr(notifier, self.priority) notify = getattr(notifier, self.priority)
LOG.debug("Emitting notification `%s`", event_type)
notify(context, event_type=event_type, payload=payload) notify(context, event_type=event_type, payload=payload)
def emit(self, context): def emit(self, context):

View File

@@ -72,6 +72,7 @@ state may be one of the following:
from watcher.common import exception from watcher.common import exception
from watcher.common import utils from watcher.common import utils
from watcher.db import api as db_api from watcher.db import api as db_api
from watcher import notifications
from watcher import objects from watcher import objects
from watcher.objects import base from watcher.objects import base
from watcher.objects import fields as wfields from watcher.objects import fields as wfields
@@ -117,6 +118,39 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
'strategy': (objects.Strategy, 'strategy_id'), 'strategy': (objects.Strategy, 'strategy_id'),
} }
# Proxified field so we can keep the previous value after an update
_state = None
_old_state = None
# NOTE(v-francoise): The way oslo.versionedobjects works is by using a
# __new__ that will automatically create the attributes referenced in
# fields. These attributes are properties that raise an exception if no
# value has been assigned, which means that they store the actual field
# value in an "_obj_%(field)s" attribute. So because we want to proxify a
# value that is already proxified, we have to do what you see below.
@property
def _obj_state(self):
return self._state
@property
def _obj_old_state(self):
return self._old_state
@property
def old_state(self):
return self._old_state
@_obj_old_state.setter
def _obj_old_state(self, value):
self._old_state = value
@_obj_state.setter
def _obj_state(self, value):
if self._old_state is None and self._state is None:
self._state = value
else:
self._old_state, self._state = self._state, value
@base.remotable_classmethod @base.remotable_classmethod
def get(cls, context, action_plan_id, eager=False): def get(cls, context, action_plan_id, eager=False):
"""Find a action_plan based on its id or uuid and return a Action object. """Find a action_plan based on its id or uuid and return a Action object.
@@ -198,6 +232,11 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
# notifications containing information about the related relationships # notifications containing information about the related relationships
self._from_db_object(self, db_action_plan, eager=True) self._from_db_object(self, db_action_plan, eager=True)
def _notify():
notifications.action_plan.send_create(self._context, self)
_notify()
@base.remotable @base.remotable
def destroy(self): def destroy(self):
"""Delete the action plan from the DB""" """Delete the action plan from the DB"""
@@ -221,8 +260,16 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
""" """
updates = self.obj_get_changes() updates = self.obj_get_changes()
db_obj = self.dbapi.update_action_plan(self.uuid, updates) db_obj = self.dbapi.update_action_plan(self.uuid, updates)
obj = self._from_db_object(self, db_obj, eager=False) obj = self._from_db_object(
self.__class__(self._context), db_obj, eager=False)
self.obj_refresh(obj) self.obj_refresh(obj)
def _notify():
notifications.action_plan.send_update(
self._context, self, old_state=self.old_state)
_notify()
self.obj_reset_changes() self.obj_reset_changes()
@base.remotable @base.remotable
@@ -262,3 +309,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
obj = self._from_db_object( obj = self._from_db_object(
self.__class__(self._context), db_obj, eager=False) self.__class__(self._context), db_obj, eager=False)
self.obj_refresh(obj) self.obj_refresh(obj)
def _notify():
notifications.action_plan.send_delete(self._context, self)
_notify()

View File

@@ -92,6 +92,10 @@ class WatcherObjectDictCompat(ovo_base.VersionedObjectDictCompat):
pass pass
class WatcherComparableObject(ovo_base.ComparableVersionedObject):
pass
class WatcherPersistentObject(object): class WatcherPersistentObject(object):
"""Mixin class for Persistent objects. """Mixin class for Persistent objects.

View File

@@ -128,8 +128,9 @@ class NotificationAction(BaseWatcherEnum):
STRATEGY = 'strategy' STRATEGY = 'strategy'
PLANNER = 'planner' PLANNER = 'planner'
EXECUTION = 'execution'
ALL = (CREATE, UPDATE, EXCEPTION, DELETE, STRATEGY, PLANNER) ALL = (CREATE, UPDATE, EXCEPTION, DELETE, STRATEGY, PLANNER, EXECUTION)
class NotificationPriorityField(BaseEnumField): class NotificationPriorityField(BaseEnumField):

View File

@@ -11,6 +11,7 @@
# limitations under the License. # limitations under the License.
import datetime import datetime
import itertools
import mock import mock
from oslo_config import cfg from oslo_config import cfg
@@ -267,7 +268,7 @@ class TestPatch(api_base.FunctionalTest):
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
new_state = objects.audit.State.SUCCEEDED new_state = objects.audit.State.CANCELLED
response = self.get_json('/audits/%s' % self.audit.uuid) response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertNotEqual(new_state, response['state']) self.assertNotEqual(new_state, response['state'])
@@ -343,6 +344,128 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
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},
]
class TestPatchStateTransitionDenied(api_base.FunctionalTest):
STATES = [
ap_state for ap_state in objects.audit.State.__dict__
if not ap_state.startswith("_")
]
scenarios = [
(
"%s -> %s" % (original_state, new_state),
{"original_state": original_state,
"new_state": new_state},
)
for original_state, new_state
in list(itertools.product(STATES, STATES))
if original_state != new_state
and {"original_state": original_state,
"new_state": new_state} not in ALLOWED_TRANSITIONS
]
def setUp(self):
super(TestPatchStateTransitionDenied, self).setUp()
obj_utils.create_test_goal(self.context)
obj_utils.create_test_strategy(self.context)
obj_utils.create_test_audit_template(self.context)
self.audit = obj_utils.create_test_audit(self.context,
state=self.original_state)
p = mock.patch.object(db_api.BaseConnection, 'update_audit')
self.mock_audit_update = p.start()
self.mock_audit_update.side_effect = self._simulate_rpc_audit_update
self.addCleanup(p.stop)
def _simulate_rpc_audit_update(self, audit):
audit.save()
return audit
def test_replace_denied(self):
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertNotEqual(self.new_state, response['state'])
response = self.patch_json(
'/audits/%s' % self.audit.uuid,
[{'path': '/state', 'value': self.new_state,
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_code)
self.assertTrue(response.json['error_message'])
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(self.original_state, response['state'])
class TestPatchStateTransitionOk(api_base.FunctionalTest):
scenarios = [
(
"%s -> %s" % (transition["original_state"],
transition["new_state"]),
transition
)
for transition in ALLOWED_TRANSITIONS
]
def setUp(self):
super(TestPatchStateTransitionOk, self).setUp()
obj_utils.create_test_goal(self.context)
obj_utils.create_test_strategy(self.context)
obj_utils.create_test_audit_template(self.context)
self.audit = obj_utils.create_test_audit(self.context,
state=self.original_state)
p = mock.patch.object(db_api.BaseConnection, 'update_audit')
self.mock_audit_update = p.start()
self.mock_audit_update.side_effect = self._simulate_rpc_audit_update
self.addCleanup(p.stop)
def _simulate_rpc_audit_update(self, audit):
audit.save()
return audit
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertNotEqual(self.new_state, response['state'])
response = self.patch_json(
'/audits/%s' % self.audit.uuid,
[{'path': '/state', 'value': self.new_state,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(self.new_state, response['state'])
return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at)
class TestPost(api_base.FunctionalTest): class TestPost(api_base.FunctionalTest):
def setUp(self): def setUp(self):

View File

@@ -18,6 +18,9 @@
import mock import mock
from watcher.applier.action_plan import default from watcher.applier.action_plan import default
from watcher.applier import default as ap_applier
from watcher import notifications
from watcher import objects
from watcher.objects import action_plan as ap_objects from watcher.objects import action_plan as ap_objects
from watcher.tests.db import base from watcher.tests.db import base
from watcher.tests.objects import utils as obj_utils from watcher.tests.objects import utils as obj_utils
@@ -25,17 +28,67 @@ from watcher.tests.objects import utils as obj_utils
class TestDefaultActionPlanHandler(base.DbTestCase): class TestDefaultActionPlanHandler(base.DbTestCase):
class FakeApplierException(Exception):
pass
def setUp(self): def setUp(self):
super(TestDefaultActionPlanHandler, self).setUp() super(TestDefaultActionPlanHandler, self).setUp()
p_action_plan_notifications = mock.patch.object(
notifications, 'action_plan', autospec=True)
self.m_action_plan_notifications = p_action_plan_notifications.start()
self.addCleanup(p_action_plan_notifications.stop)
obj_utils.create_test_goal(self.context) obj_utils.create_test_goal(self.context)
obj_utils.create_test_strategy(self.context) obj_utils.create_test_strategy(self.context)
obj_utils.create_test_audit(self.context) obj_utils.create_test_audit(self.context)
self.action_plan = obj_utils.create_test_action_plan(self.context) self.action_plan = obj_utils.create_test_action_plan(self.context)
def test_launch_action_plan(self): @mock.patch.object(objects.ActionPlan, "get_by_uuid")
def test_launch_action_plan(self, m_get_action_plan):
m_get_action_plan.return_value = self.action_plan
command = default.DefaultActionPlanHandler( command = default.DefaultActionPlanHandler(
self.context, mock.MagicMock(), self.action_plan.uuid) self.context, mock.MagicMock(), self.action_plan.uuid)
command.execute() command.execute()
action_plan = ap_objects.ActionPlan.get_by_uuid(
self.context, self.action_plan.uuid) expected_calls = [
self.assertEqual(ap_objects.State.SUCCEEDED, action_plan.state) mock.call(self.context, self.action_plan,
action=objects.fields.NotificationAction.EXECUTION,
phase=objects.fields.NotificationPhase.START),
mock.call(self.context, self.action_plan,
action=objects.fields.NotificationAction.EXECUTION,
phase=objects.fields.NotificationPhase.END)]
self.assertEqual(ap_objects.State.SUCCEEDED, self.action_plan.state)
self.assertEqual(
expected_calls,
self.m_action_plan_notifications
.send_action_notification
.call_args_list)
@mock.patch.object(ap_applier.DefaultApplier, "execute")
@mock.patch.object(objects.ActionPlan, "get_by_uuid")
def test_launch_action_plan_with_error(self, m_get_action_plan, m_execute):
m_get_action_plan.return_value = self.action_plan
m_execute.side_effect = self.FakeApplierException
command = default.DefaultActionPlanHandler(
self.context, mock.MagicMock(), self.action_plan.uuid)
command.execute()
expected_calls = [
mock.call(self.context, self.action_plan,
action=objects.fields.NotificationAction.EXECUTION,
phase=objects.fields.NotificationPhase.START),
mock.call(self.context, self.action_plan,
action=objects.fields.NotificationAction.EXECUTION,
priority=objects.fields.NotificationPriority.ERROR,
phase=objects.fields.NotificationPhase.ERROR)]
self.assertEqual(ap_objects.State.FAILED, self.action_plan.state)
self.assertEqual(
expected_calls,
self.m_action_plan_notifications
.send_action_notification
.call_args_list)

View File

@@ -89,7 +89,7 @@ def get_test_audit(**kwargs):
'updated_at': kwargs.get('updated_at'), 'updated_at': kwargs.get('updated_at'),
'deleted_at': kwargs.get('deleted_at'), 'deleted_at': kwargs.get('deleted_at'),
'parameters': kwargs.get('parameters', {}), 'parameters': kwargs.get('parameters', {}),
'interval': kwargs.get('period', 3600), 'interval': kwargs.get('interval', 3600),
'goal_id': kwargs.get('goal_id', 1), 'goal_id': kwargs.get('goal_id', 1),
'strategy_id': kwargs.get('strategy_id', None), 'strategy_id': kwargs.get('strategy_id', None),
'scope': kwargs.get('scope', []), 'scope': kwargs.get('scope', []),

View File

@@ -175,12 +175,19 @@ class TestAutoTriggerActionPlan(base.DbTestCase):
self.ongoing_action_plan = obj_utils.create_test_action_plan( self.ongoing_action_plan = obj_utils.create_test_action_plan(
self.context, self.context,
uuid=uuidutils.generate_uuid(), uuid=uuidutils.generate_uuid(),
audit_id=self.audit.id) audit_id=self.audit.id,
strategy_id=self.strategy.id,
audit=self.audit,
strategy=self.strategy,
)
self.recommended_action_plan = obj_utils.create_test_action_plan( self.recommended_action_plan = obj_utils.create_test_action_plan(
self.context, self.context,
uuid=uuidutils.generate_uuid(), uuid=uuidutils.generate_uuid(),
state=objects.action_plan.State.ONGOING, state=objects.action_plan.State.ONGOING,
audit_id=self.audit.id audit_id=self.audit.id,
strategy_id=self.strategy.id,
audit=self.audit,
strategy=self.strategy,
) )
@mock.patch.object(objects.action_plan.ActionPlan, 'list') @mock.patch.object(objects.action_plan.ActionPlan, 'list')

View File

@@ -153,7 +153,6 @@ class FakeMonascaMetrics(object):
# measurements[uuid] = random.randint(1, 4) # measurements[uuid] = random.randint(1, 4)
measurements[uuid] = 8 measurements[uuid] = 8
# import ipdb; ipdb.set_trace()
return [{'columns': ['avg'], return [{'columns': ['avg'],
'statistics': [[float(measurements[str(uuid)])]]}] 'statistics': [[float(measurements[str(uuid)])]]}]
# return float(measurements[str(uuid)]) # return float(measurements[str(uuid)])

View File

@@ -60,6 +60,7 @@ class TestActionScheduling(base.DbTestCase):
def setUp(self): def setUp(self):
super(TestActionScheduling, self).setUp() super(TestActionScheduling, self).setUp()
self.goal = db_utils.create_test_goal(name="dummy")
self.strategy = db_utils.create_test_strategy(name="dummy") self.strategy = db_utils.create_test_strategy(name="dummy")
self.audit = db_utils.create_test_audit( self.audit = db_utils.create_test_audit(
uuid=utils.generate_uuid(), strategy_id=self.strategy.id) uuid=utils.generate_uuid(), strategy_id=self.strategy.id)

View File

@@ -61,6 +61,7 @@ class TestActionScheduling(base.DbTestCase):
def setUp(self): def setUp(self):
super(TestActionScheduling, self).setUp() super(TestActionScheduling, self).setUp()
self.goal = db_utils.create_test_goal(name="dummy")
self.strategy = db_utils.create_test_strategy(name="dummy") self.strategy = db_utils.create_test_strategy(name="dummy")
self.audit = db_utils.create_test_audit( self.audit = db_utils.create_test_audit(
uuid=utils.generate_uuid(), strategy_id=self.strategy.id) uuid=utils.generate_uuid(), strategy_id=self.strategy.id)

View File

@@ -42,10 +42,8 @@ class TestDefaultScope(base.TestCase):
for i in range(2)] for i in range(2)]
model = default.DefaultScope(audit_scope, model = default.DefaultScope(audit_scope,
osc=mock.Mock()).get_scoped_model(cluster) osc=mock.Mock()).get_scoped_model(cluster)
expected_edges = [('INSTANCE_2', 'Node_1')] expected_edges = [('INSTANCE_2', 'Node_1')]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()] self.assertEqual(sorted(expected_edges), sorted(model.edges()))
self.assertEqual(sorted(expected_edges), sorted(edges))
@mock.patch.object(nova_helper.NovaHelper, 'get_availability_zone_list') @mock.patch.object(nova_helper.NovaHelper, 'get_availability_zone_list')
def test_get_scoped_model_without_scope(self, mock_zone_list): def test_get_scoped_model_without_scope(self, mock_zone_list):
@@ -67,8 +65,7 @@ class TestDefaultScope(base.TestCase):
('INSTANCE_6', 'Node_3'), ('INSTANCE_6', 'Node_3'),
('INSTANCE_7', 'Node_4'), ('INSTANCE_7', 'Node_4'),
] ]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()] self.assertEqual(sorted(expected_edges), sorted(model.edges()))
self.assertEqual(sorted(expected_edges), sorted(edges))
@mock.patch.object(nova_helper.NovaHelper, 'get_aggregate_detail') @mock.patch.object(nova_helper.NovaHelper, 'get_aggregate_detail')
@mock.patch.object(nova_helper.NovaHelper, 'get_aggregate_list') @mock.patch.object(nova_helper.NovaHelper, 'get_aggregate_list')
@@ -199,8 +196,7 @@ class TestDefaultScope(base.TestCase):
('INSTANCE_1', 'Node_0'), ('INSTANCE_1', 'Node_0'),
('INSTANCE_6', 'Node_3'), ('INSTANCE_6', 'Node_3'),
('INSTANCE_7', 'Node_4')] ('INSTANCE_7', 'Node_4')]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()] self.assertEqual(sorted(expected_edges), sorted(model.edges()))
self.assertEqual(sorted(expected_edges), sorted(edges))
def test_remove_instances_from_model(self): def test_remove_instances_from_model(self):
model = self.fake_cluster.generate_scenario_1() model = self.fake_cluster.generate_scenario_1()
@@ -213,5 +209,4 @@ class TestDefaultScope(base.TestCase):
('INSTANCE_5', 'Node_2'), ('INSTANCE_5', 'Node_2'),
('INSTANCE_6', 'Node_3'), ('INSTANCE_6', 'Node_3'),
('INSTANCE_7', 'Node_4')] ('INSTANCE_7', 'Node_4')]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()] self.assertEqual(sorted(expected_edges), sorted(model.edges()))
self.assertEqual(sorted(expected_edges), sorted(edges))

View File

@@ -0,0 +1,419 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import freezegun
import mock
import oslo_messaging as om
from watcher.common import exception
from watcher.common import rpc
from watcher import notifications
from watcher import objects
from watcher.tests.db import base
from watcher.tests.objects import utils
@freezegun.freeze_time('2016-10-18T09:52:05.219414')
class TestActionPlanNotification(base.DbTestCase):
def setUp(self):
super(TestActionPlanNotification, self).setUp()
p_get_notifier = mock.patch.object(rpc, 'get_notifier')
m_get_notifier = p_get_notifier.start()
self.addCleanup(p_get_notifier.stop)
self.m_notifier = mock.Mock(spec=om.Notifier)
def fake_get_notifier(publisher_id):
self.m_notifier.publisher_id = publisher_id
return self.m_notifier
m_get_notifier.side_effect = fake_get_notifier
self.goal = utils.create_test_goal(mock.Mock())
self.audit = utils.create_test_audit(mock.Mock(), interval=None)
self.strategy = utils.create_test_strategy(mock.Mock())
def test_send_invalid_action_plan(self):
action_plan = utils.get_test_action_plan(
mock.Mock(), state='DOESNOTMATTER', audit_id=1)
self.assertRaises(
exception.InvalidActionPlan,
notifications.action_plan.send_update,
mock.MagicMock(), action_plan, host='node0')
def test_send_action_plan_update(self):
action_plan = utils.create_test_action_plan(
mock.Mock(), state=objects.action_plan.State.ONGOING,
audit_id=self.audit.id, strategy_id=self.strategy.id,
audit=self.audit, strategy=self.strategy)
notifications.action_plan.send_update(
mock.MagicMock(), action_plan, host='node0',
old_state=objects.action_plan.State.PENDING)
# The 1st notification is because we created the object.
# The 2nd notification is because we created the action plan object.
self.assertEqual(3, self.m_notifier.info.call_count)
notification = self.m_notifier.info.call_args[1]
payload = notification['payload']
self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id)
self.assertDictEqual(
{
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"global_efficacy": {},
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"updated_at": None,
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"name": "TEST",
"parameters_spec": {},
"created_at": "2016-10-18T09:52:05Z",
"display_name": "test strategy",
"deleted_at": None
},
"watcher_object.name": "StrategyPayload"
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.data": {
"interval": None,
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"strategy_uuid": None,
"goal_uuid": (
"f7ad87ae-4298-91cf-93a0-f35a852e3652"),
"deleted_at": None,
"scope": [],
"state": "PENDING",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
"audit_type": "ONESHOT"
},
"watcher_object.name": "TerseAuditPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"deleted_at": None,
"state": "ONGOING",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
"state_update": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"old_state": "PENDING",
"state": "ONGOING"
},
"watcher_object.name": "ActionPlanStateUpdatePayload"
},
},
"watcher_object.name": "ActionPlanUpdatePayload"
},
payload
)
def test_send_action_plan_create(self):
action_plan = utils.get_test_action_plan(
mock.Mock(), state=objects.action_plan.State.PENDING,
audit_id=self.audit.id, strategy_id=self.strategy.id,
audit=self.audit.as_dict(), strategy=self.strategy.as_dict())
notifications.action_plan.send_create(
mock.MagicMock(), action_plan, host='node0')
self.assertEqual(2, self.m_notifier.info.call_count)
notification = self.m_notifier.info.call_args[1]
payload = notification['payload']
self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id)
self.assertDictEqual(
{
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"global_efficacy": {},
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"updated_at": None,
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"name": "TEST",
"parameters_spec": {},
"created_at": "2016-10-18T09:52:05Z",
"display_name": "test strategy",
"deleted_at": None
},
"watcher_object.name": "StrategyPayload"
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.data": {
"interval": None,
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"strategy_uuid": None,
"goal_uuid": (
"f7ad87ae-4298-91cf-93a0-f35a852e3652"),
"deleted_at": None,
"scope": [],
"state": "PENDING",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
"audit_type": "ONESHOT"
},
"watcher_object.name": "TerseAuditPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"deleted_at": None,
"state": "PENDING",
"updated_at": None,
"created_at": None,
},
"watcher_object.name": "ActionPlanCreatePayload"
},
payload
)
def test_send_action_plan_delete(self):
action_plan = utils.create_test_action_plan(
mock.Mock(), state=objects.action_plan.State.DELETED,
audit_id=self.audit.id, strategy_id=self.strategy.id)
notifications.action_plan.send_delete(
mock.MagicMock(), action_plan, host='node0')
# The 1st notification is because we created the audit object.
# The 2nd notification is because we created the action plan object.
self.assertEqual(3, self.m_notifier.info.call_count)
notification = self.m_notifier.info.call_args[1]
payload = notification['payload']
self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id)
self.assertDictEqual(
{
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"global_efficacy": {},
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"updated_at": None,
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"name": "TEST",
"parameters_spec": {},
"created_at": "2016-10-18T09:52:05Z",
"display_name": "test strategy",
"deleted_at": None
},
"watcher_object.name": "StrategyPayload"
},
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061",
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.data": {
"interval": None,
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"strategy_uuid": None,
"goal_uuid": (
"f7ad87ae-4298-91cf-93a0-f35a852e3652"),
"deleted_at": None,
"scope": [],
"state": "PENDING",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
"audit_type": "ONESHOT"
},
"watcher_object.name": "TerseAuditPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"deleted_at": None,
"state": "DELETED",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
},
"watcher_object.name": "ActionPlanDeletePayload"
},
payload
)
def test_send_action_plan_action(self):
action_plan = utils.create_test_action_plan(
mock.Mock(), state=objects.action_plan.State.ONGOING,
audit_id=self.audit.id, strategy_id=self.strategy.id,
audit=self.audit, strategy=self.strategy)
notifications.action_plan.send_action_notification(
mock.MagicMock(), action_plan, host='node0',
action='execution', phase='start')
# The 1st notification is because we created the audit object.
# The 2nd notification is because we created the action plan object.
self.assertEqual(3, self.m_notifier.info.call_count)
notification = self.m_notifier.info.call_args[1]
self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id)
self.assertDictEqual(
{
"event_type": "action_plan.execution.start",
"payload": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": None,
"fault": None,
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.namespace": "watcher",
"watcher_object.name": "TerseAuditPayload",
"watcher_object.version": "1.0",
"watcher_object.data": {
"interval": None,
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"strategy_uuid": None,
"goal_uuid": (
"f7ad87ae-4298-91cf-93a0-f35a852e3652"),
"deleted_at": None,
"scope": [],
"state": "PENDING",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
"audit_type": "ONESHOT"
}
},
"global_efficacy": {},
"state": "ONGOING",
"strategy_uuid": (
"cb3d0b58-4415-4d90-b75b-1e96878730e3"),
"strategy": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": None,
"display_name": "test strategy",
"name": "TEST",
"parameters_spec": {},
"updated_at": None,
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"updated_at": None,
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061"
},
"watcher_object.name": "ActionPlanActionPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
}
},
notification
)
def test_send_action_plan_action_with_error(self):
action_plan = utils.create_test_action_plan(
mock.Mock(), state=objects.action_plan.State.ONGOING,
audit_id=self.audit.id, strategy_id=self.strategy.id,
audit=self.audit, strategy=self.strategy)
try:
# This is to load the exception in sys.exc_info()
raise exception.WatcherException("TEST")
except exception.WatcherException:
notifications.action_plan.send_action_notification(
mock.MagicMock(), action_plan, host='node0',
action='execution', priority='error', phase='error')
self.assertEqual(1, self.m_notifier.error.call_count)
notification = self.m_notifier.error.call_args[1]
self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id)
self.assertDictEqual(
{
"event_type": "action_plan.execution.error",
"payload": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": None,
"fault": {
"watcher_object.data": {
"exception": "WatcherException",
"exception_message": "TEST",
"function_name": (
"test_send_action_plan_action_with_error"),
"module_name": "watcher.tests.notifications."
"test_action_plan_notification"
},
"watcher_object.name": "ExceptionPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"audit": {
"watcher_object.data": {
"interval": None,
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"strategy_uuid": None,
"goal_uuid": (
"f7ad87ae-4298-91cf-93a0-f35a852e3652"),
"deleted_at": None,
"scope": [],
"state": "PENDING",
"updated_at": None,
"created_at": "2016-10-18T09:52:05Z",
"audit_type": "ONESHOT"
},
"watcher_object.name": "TerseAuditPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"global_efficacy": {},
"state": "ONGOING",
"strategy_uuid": (
"cb3d0b58-4415-4d90-b75b-1e96878730e3"),
"strategy": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": None,
"display_name": "test strategy",
"name": "TEST",
"parameters_spec": {},
"updated_at": None,
"uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"updated_at": None,
"uuid": "76be87bd-3422-43f9-93a0-e85a577e3061"
},
"watcher_object.name": "ActionPlanActionPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
}
},
notification
)

View File

@@ -43,8 +43,8 @@ class TestAuditNotification(base.DbTestCase):
self.strategy = utils.create_test_strategy(mock.Mock()) self.strategy = utils.create_test_strategy(mock.Mock())
def test_send_invalid_audit(self): def test_send_invalid_audit(self):
audit = utils.get_test_audit(mock.Mock(), state='DOESNOTMATTER', audit = utils.get_test_audit(
goal_id=1) mock.Mock(), interval=None, state='DOESNOTMATTER', goal_id=1)
self.assertRaises( self.assertRaises(
exception.InvalidAudit, exception.InvalidAudit,
@@ -53,7 +53,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_update_with_strategy(self): def test_send_audit_update_with_strategy(self):
audit = utils.create_test_audit( audit = utils.create_test_audit(
mock.Mock(), state=objects.audit.State.ONGOING, mock.Mock(), interval=None, state=objects.audit.State.ONGOING,
goal_id=self.goal.id, strategy_id=self.strategy.id, goal_id=self.goal.id, strategy_id=self.strategy.id,
goal=self.goal, strategy=self.strategy) goal=self.goal, strategy=self.strategy)
notifications.audit.send_update( notifications.audit.send_update(
@@ -71,7 +71,8 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
"watcher_object.data": { "watcher_object.data": {
"interval": 3600, "interval": None,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": { "strategy": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -88,6 +89,7 @@ class TestAuditNotification(base.DbTestCase):
}, },
"parameters": {}, "parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": { "goal": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -125,7 +127,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_update_without_strategy(self): def test_send_audit_update_without_strategy(self):
audit = utils.get_test_audit( audit = utils.get_test_audit(
mock.Mock(), state=objects.audit.State.ONGOING, mock.Mock(), interval=None, state=objects.audit.State.ONGOING,
goal_id=self.goal.id, goal=self.goal) goal_id=self.goal.id, goal=self.goal)
notifications.audit.send_update( notifications.audit.send_update(
mock.MagicMock(), audit, host='node0', mock.MagicMock(), audit, host='node0',
@@ -141,9 +143,10 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
"watcher_object.data": { "watcher_object.data": {
"interval": 3600, "interval": None,
"parameters": {}, "parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": { "goal": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -158,6 +161,7 @@ class TestAuditNotification(base.DbTestCase):
}, },
"watcher_object.name": "GoalPayload" "watcher_object.name": "GoalPayload"
}, },
"strategy_uuid": None,
"strategy": None, "strategy": None,
"deleted_at": None, "deleted_at": None,
"scope": [], "scope": [],
@@ -182,7 +186,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_create(self): def test_send_audit_create(self):
audit = utils.get_test_audit( audit = utils.get_test_audit(
mock.Mock(), state=objects.audit.State.PENDING, mock.Mock(), interval=None, state=objects.audit.State.PENDING,
goal_id=self.goal.id, strategy_id=self.strategy.id, goal_id=self.goal.id, strategy_id=self.strategy.id,
goal=self.goal.as_dict(), strategy=self.strategy.as_dict()) goal=self.goal.as_dict(), strategy=self.strategy.as_dict())
notifications.audit.send_create( notifications.audit.send_create(
@@ -198,7 +202,8 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
"watcher_object.data": { "watcher_object.data": {
"interval": 3600, "interval": None,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": { "strategy": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -215,6 +220,7 @@ class TestAuditNotification(base.DbTestCase):
}, },
"parameters": {}, "parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": { "goal": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -243,7 +249,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_delete(self): def test_send_audit_delete(self):
audit = utils.create_test_audit( audit = utils.create_test_audit(
mock.Mock(), state=objects.audit.State.DELETED, mock.Mock(), interval=None, state=objects.audit.State.DELETED,
goal_id=self.goal.id, strategy_id=self.strategy.id) goal_id=self.goal.id, strategy_id=self.strategy.id)
notifications.audit.send_delete( notifications.audit.send_delete(
mock.MagicMock(), audit, host='node0') mock.MagicMock(), audit, host='node0')
@@ -259,7 +265,8 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
"watcher_object.data": { "watcher_object.data": {
"interval": 3600, "interval": None,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": { "strategy": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -276,6 +283,7 @@ class TestAuditNotification(base.DbTestCase):
}, },
"parameters": {}, "parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": { "goal": {
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0", "watcher_object.version": "1.0",
@@ -304,7 +312,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_action(self): def test_send_audit_action(self):
audit = utils.create_test_audit( audit = utils.create_test_audit(
mock.Mock(), state=objects.audit.State.ONGOING, mock.Mock(), interval=None, state=objects.audit.State.ONGOING,
goal_id=self.goal.id, strategy_id=self.strategy.id, goal_id=self.goal.id, strategy_id=self.strategy.id,
goal=self.goal, strategy=self.strategy) goal=self.goal, strategy=self.strategy)
notifications.audit.send_action_notification( notifications.audit.send_action_notification(
@@ -326,6 +334,7 @@ class TestAuditNotification(base.DbTestCase):
"created_at": "2016-10-18T09:52:05Z", "created_at": "2016-10-18T09:52:05Z",
"deleted_at": None, "deleted_at": None,
"fault": None, "fault": None,
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z", "created_at": "2016-10-18T09:52:05Z",
@@ -340,10 +349,12 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0" "watcher_object.version": "1.0"
}, },
"interval": 3600, "interval": None,
"parameters": {}, "parameters": {},
"scope": [], "scope": [],
"state": "ONGOING", "state": "ONGOING",
"strategy_uuid": (
"cb3d0b58-4415-4d90-b75b-1e96878730e3"),
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z", "created_at": "2016-10-18T09:52:05Z",
@@ -371,7 +382,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_action_with_error(self): def test_send_audit_action_with_error(self):
audit = utils.create_test_audit( audit = utils.create_test_audit(
mock.Mock(), state=objects.audit.State.ONGOING, mock.Mock(), interval=None, state=objects.audit.State.ONGOING,
goal_id=self.goal.id, strategy_id=self.strategy.id, goal_id=self.goal.id, strategy_id=self.strategy.id,
goal=self.goal, strategy=self.strategy) goal=self.goal, strategy=self.strategy)
@@ -407,6 +418,7 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0" "watcher_object.version": "1.0"
}, },
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": { "goal": {
"watcher_object.data": { "watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z", "created_at": "2016-10-18T09:52:05Z",
@@ -421,10 +433,12 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher", "watcher_object.namespace": "watcher",
"watcher_object.version": "1.0" "watcher_object.version": "1.0"
}, },
"interval": 3600, "interval": None,
"parameters": {}, "parameters": {},
"scope": [], "scope": [],
"state": "ONGOING", "state": "ONGOING",
"strategy_uuid": (
"cb3d0b58-4415-4d90-b75b-1e96878730e3"),
"strategy": { "strategy": {
"watcher_object.data": { "watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z", "created_at": "2016-10-18T09:52:05Z",

View File

@@ -250,10 +250,11 @@ class TestNotificationBase(testbase.TestCase):
expected_notification_fingerprints = { expected_notification_fingerprints = {
'EventType': '1.2-633c2d32fa849d2a6f8bda3b0db88332', 'EventType': '1.3-4258a2c86eca79fd34a7dffe1278eab9',
'ExceptionNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ExceptionNotification': '1.0-9b69de0724fda8310d05e18418178866',
'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b', 'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b',
'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545', 'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545',
'TerseAuditPayload': '1.0-aaf31166b8698f08d12cae98c380b8e0',
'AuditPayload': '1.0-30c85c834648c8ca11f54fc5e084d86b', 'AuditPayload': '1.0-30c85c834648c8ca11f54fc5e084d86b',
'AuditStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5', 'AuditStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5',
'AuditUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', 'AuditUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866',
@@ -266,6 +267,16 @@ expected_notification_fingerprints = {
'AuditActionPayload': '1.0-09f5d005f94ba9e5f6b9200170332c52', 'AuditActionPayload': '1.0-09f5d005f94ba9e5f6b9200170332c52',
'GoalPayload': '1.0-fa1fecb8b01dd047eef808ded4d50d1a', 'GoalPayload': '1.0-fa1fecb8b01dd047eef808ded4d50d1a',
'StrategyPayload': '1.0-94f01c137b083ac236ae82573c1fcfc1', 'StrategyPayload': '1.0-94f01c137b083ac236ae82573c1fcfc1',
'ActionPlanActionPayload': '1.0-34871caf18e9b43a28899953c1c9733a',
'ActionPlanCreateNotification': '1.0-9b69de0724fda8310d05e18418178866',
'ActionPlanCreatePayload': '1.0-ffc3087acd73351b14f3dcc30e105027',
'ActionPlanDeleteNotification': '1.0-9b69de0724fda8310d05e18418178866',
'ActionPlanDeletePayload': '1.0-ffc3087acd73351b14f3dcc30e105027',
'ActionPlanPayload': '1.0-ffc3087acd73351b14f3dcc30e105027',
'ActionPlanStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5',
'ActionPlanUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866',
'ActionPlanUpdatePayload': '1.0-7912a45fe53775c721f42aa87f06a023',
'ActionPlanActionNotification': '1.0-9b69de0724fda8310d05e18418178866',
} }

View File

@@ -20,6 +20,7 @@ import mock
from watcher.common import exception from watcher.common import exception
from watcher.db.sqlalchemy import api as db_api from watcher.db.sqlalchemy import api as db_api
from watcher import notifications
from watcher import objects from watcher import objects
from watcher.tests.db import base from watcher.tests.db import base
from watcher.tests.db import utils from watcher.tests.db import utils
@@ -34,16 +35,19 @@ class TestActionPlanObject(base.DbTestCase):
('non_eager', dict( ('non_eager', dict(
eager=False, eager=False,
fake_action_plan=utils.get_test_action_plan( fake_action_plan=utils.get_test_action_plan(
created_at=datetime.datetime.utcnow(),
audit_id=audit_id, audit_id=audit_id,
strategy_id=strategy_id))), strategy_id=strategy_id))),
('eager_with_non_eager_load', dict( ('eager_with_non_eager_load', dict(
eager=True, eager=True,
fake_action_plan=utils.get_test_action_plan( fake_action_plan=utils.get_test_action_plan(
created_at=datetime.datetime.utcnow(),
audit_id=audit_id, audit_id=audit_id,
strategy_id=strategy_id))), strategy_id=strategy_id))),
('eager_with_eager_load', dict( ('eager_with_eager_load', dict(
eager=True, eager=True,
fake_action_plan=utils.get_test_action_plan( fake_action_plan=utils.get_test_action_plan(
created_at=datetime.datetime.utcnow(),
strategy_id=strategy_id, strategy_id=strategy_id,
strategy=utils.get_test_strategy(id=strategy_id), strategy=utils.get_test_strategy(id=strategy_id),
audit_id=audit_id, audit_id=audit_id,
@@ -52,6 +56,13 @@ class TestActionPlanObject(base.DbTestCase):
def setUp(self): def setUp(self):
super(TestActionPlanObject, self).setUp() super(TestActionPlanObject, self).setUp()
p_action_plan_notifications = mock.patch.object(
notifications, 'action_plan', autospec=True)
self.m_action_plan_notifications = p_action_plan_notifications.start()
self.addCleanup(p_action_plan_notifications.stop)
self.m_send_update = self.m_action_plan_notifications.send_update
self.fake_audit = utils.create_test_audit(id=self.audit_id) self.fake_audit = utils.create_test_audit(id=self.audit_id)
self.fake_strategy = utils.create_test_strategy( self.fake_strategy = utils.create_test_strategy(
id=self.strategy_id, name="DUMMY") id=self.strategy_id, name="DUMMY")
@@ -80,6 +91,7 @@ class TestActionPlanObject(base.DbTestCase):
self.context, action_plan_id, eager=self.eager) self.context, action_plan_id, eager=self.eager)
self.assertEqual(self.context, action_plan._context) self.assertEqual(self.context, action_plan._context)
self.eager_load_action_plan_assert(action_plan) self.eager_load_action_plan_assert(action_plan)
self.assertEqual(0, self.m_send_update.call_count)
@mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid') @mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid')
def test_get_by_uuid(self, mock_get_action_plan): def test_get_by_uuid(self, mock_get_action_plan):
@@ -91,6 +103,7 @@ class TestActionPlanObject(base.DbTestCase):
self.context, uuid, eager=self.eager) self.context, uuid, eager=self.eager)
self.assertEqual(self.context, action_plan._context) self.assertEqual(self.context, action_plan._context)
self.eager_load_action_plan_assert(action_plan) self.eager_load_action_plan_assert(action_plan)
self.assertEqual(0, self.m_send_update.call_count)
def test_get_bad_id_and_uuid(self): def test_get_bad_id_and_uuid(self):
self.assertRaises(exception.InvalidIdentity, self.assertRaises(exception.InvalidIdentity,
@@ -107,14 +120,26 @@ class TestActionPlanObject(base.DbTestCase):
self.assertEqual(self.context, action_plans[0]._context) self.assertEqual(self.context, action_plans[0]._context)
for action_plan in action_plans: for action_plan in action_plans:
self.eager_load_action_plan_assert(action_plan) self.eager_load_action_plan_assert(action_plan)
self.assertEqual(0, self.m_send_update.call_count)
@mock.patch.object(db_api.Connection, 'update_action_plan') @mock.patch.object(db_api.Connection, 'update_action_plan')
@mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid') @mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid')
def test_save(self, mock_get_action_plan, mock_update_action_plan): def test_save(self, mock_get_action_plan, mock_update_action_plan):
mock_get_action_plan.return_value = self.fake_action_plan mock_get_action_plan.return_value = self.fake_action_plan
fake_saved_action_plan = self.fake_action_plan.copy() fake_saved_action_plan = self.fake_action_plan.copy()
fake_saved_action_plan['deleted_at'] = datetime.datetime.utcnow() fake_saved_action_plan['state'] = objects.action_plan.State.SUCCEEDED
fake_saved_action_plan['updated_at'] = datetime.datetime.utcnow()
mock_update_action_plan.return_value = fake_saved_action_plan mock_update_action_plan.return_value = fake_saved_action_plan
expected_action_plan = fake_saved_action_plan.copy()
expected_action_plan[
'created_at'] = expected_action_plan['created_at'].replace(
tzinfo=iso8601.iso8601.Utc())
expected_action_plan[
'updated_at'] = expected_action_plan['updated_at'].replace(
tzinfo=iso8601.iso8601.Utc())
uuid = self.fake_action_plan['uuid'] uuid = self.fake_action_plan['uuid']
action_plan = objects.ActionPlan.get_by_uuid( action_plan = objects.ActionPlan.get_by_uuid(
self.context, uuid, eager=self.eager) self.context, uuid, eager=self.eager)
@@ -127,6 +152,14 @@ class TestActionPlanObject(base.DbTestCase):
uuid, {'state': objects.action_plan.State.SUCCEEDED}) uuid, {'state': objects.action_plan.State.SUCCEEDED})
self.assertEqual(self.context, action_plan._context) self.assertEqual(self.context, action_plan._context)
self.eager_load_action_plan_assert(action_plan) self.eager_load_action_plan_assert(action_plan)
self.m_send_update.assert_called_once_with(
self.context, action_plan,
old_state=self.fake_action_plan['state'])
self.assertEqual(
{k: v for k, v in expected_action_plan.items()
if k not in action_plan.object_fields},
{k: v for k, v in action_plan.as_dict().items()
if k not in action_plan.object_fields})
@mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid') @mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid')
def test_refresh(self, mock_get_action_plan): def test_refresh(self, mock_get_action_plan):
@@ -150,6 +183,13 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase):
def setUp(self): def setUp(self):
super(TestCreateDeleteActionPlanObject, self).setUp() super(TestCreateDeleteActionPlanObject, self).setUp()
p_action_plan_notifications = mock.patch.object(
notifications, 'action_plan', autospec=True)
self.m_action_plan_notifications = p_action_plan_notifications.start()
self.addCleanup(p_action_plan_notifications.stop)
self.m_send_update = self.m_action_plan_notifications.send_update
self.fake_strategy = utils.create_test_strategy(name="DUMMY") self.fake_strategy = utils.create_test_strategy(name="DUMMY")
self.fake_audit = utils.create_test_audit() self.fake_audit = utils.create_test_audit()
self.fake_action_plan = utils.get_test_action_plan( self.fake_action_plan = utils.get_test_action_plan(
@@ -202,7 +242,8 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase):
del expected_action_plan['strategy'] del expected_action_plan['strategy']
m_get_efficacy_indicator_list.return_value = [efficacy_indicator] m_get_efficacy_indicator_list.return_value = [efficacy_indicator]
action_plan = objects.ActionPlan.get_by_uuid(self.context, uuid) action_plan = objects.ActionPlan.get_by_uuid(
self.context, uuid, eager=False)
action_plan.soft_delete() action_plan.soft_delete()
m_get_action_plan.assert_called_once_with( m_get_action_plan.assert_called_once_with(

View File

@@ -282,6 +282,7 @@ class TestAuditObjectSendNotifications(base.DbTestCase):
def test_send_create_notification(self, m_create_audit): def test_send_create_notification(self, m_create_audit):
audit = objutils.get_test_audit( audit = objutils.get_test_audit(
self.context, self.context,
id=1,
goal_id=self.fake_goal.id, goal_id=self.fake_goal.id,
strategy_id=self.fake_strategy.id, strategy_id=self.fake_strategy.id,
goal=self.fake_goal.as_dict(), goal=self.fake_goal.as_dict(),

View File

@@ -30,21 +30,27 @@ def _load_related_objects(context, cls, db_data):
return obj_data return obj_data
def _load_test_obj(context, cls, obj_data, **kw):
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del obj_data['id']
obj = cls(context)
for key in obj_data:
setattr(obj, key, obj_data[key])
return obj
def get_test_audit_template(context, **kw): def get_test_audit_template(context, **kw):
"""Return a AuditTemplate object with appropriate attributes. """Return a AuditTemplate object with appropriate attributes.
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_audit_template = db_utils.get_test_audit_template(**kw) obj_cls = objects.AuditTemplate
# Let DB generate ID if it isn't specified explicitly db_data = db_utils.get_test_audit_template(**kw)
if 'id' not in kw: obj_data = _load_related_objects(context, obj_cls, db_data)
del db_audit_template['id']
audit_template = objects.AuditTemplate(context)
for key in db_audit_template:
setattr(audit_template, key, db_audit_template[key])
return audit_template return _load_test_obj(context, obj_cls, obj_data, **kw)
def create_test_audit_template(context, **kw): def create_test_audit_template(context, **kw):
@@ -64,16 +70,11 @@ def get_test_audit(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_audit = db_utils.get_test_audit(**kw) obj_cls = objects.Audit
obj_data = _load_related_objects(context, objects.Audit, db_audit) db_data = db_utils.get_test_audit(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
# Let DB generate ID if it isn't specified explicitly return _load_test_obj(context, obj_cls, obj_data, **kw)
if 'id' not in kw:
del db_audit['id']
audit = objects.Audit(context)
for key in obj_data:
setattr(audit, key, obj_data[key])
return audit
def create_test_audit(context, **kw): def create_test_audit(context, **kw):
@@ -93,14 +94,11 @@ def get_test_action_plan(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_action_plan = db_utils.get_test_action_plan(**kw) obj_cls = objects.ActionPlan
# Let DB generate ID if it isn't specified explicitly db_data = db_utils.get_test_action_plan(**kw)
if 'id' not in kw: obj_data = _load_related_objects(context, obj_cls, db_data)
del db_action_plan['id']
action_plan = objects.ActionPlan(context) return _load_test_obj(context, obj_cls, obj_data, **kw)
for key in db_action_plan:
setattr(action_plan, key, db_action_plan[key])
return action_plan
def create_test_action_plan(context, **kw): def create_test_action_plan(context, **kw):
@@ -120,14 +118,11 @@ def get_test_action(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_action = db_utils.get_test_action(**kw) obj_cls = objects.Action
# Let DB generate ID if it isn't specified explicitly db_data = db_utils.get_test_action(**kw)
if 'id' not in kw: obj_data = _load_related_objects(context, obj_cls, db_data)
del db_action['id']
action = objects.Action(context) return _load_test_obj(context, obj_cls, obj_data, **kw)
for key in db_action:
setattr(action, key, db_action[key])
return action
def create_test_action(context, **kw): def create_test_action(context, **kw):
@@ -147,14 +142,11 @@ def get_test_goal(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_goal = db_utils.get_test_goal(**kw) obj_cls = objects.Goal
# Let DB generate ID if it isn't specified explicitly db_data = db_utils.get_test_goal(**kw)
if 'id' not in kw: obj_data = _load_related_objects(context, obj_cls, db_data)
del db_goal['id']
goal = objects.Goal(context) return _load_test_obj(context, obj_cls, obj_data, **kw)
for key in db_goal:
setattr(goal, key, db_goal[key])
return goal
def create_test_goal(context, **kw): def create_test_goal(context, **kw):
@@ -174,11 +166,11 @@ def get_test_scoring_engine(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_scoring_engine = db_utils.get_test_scoring_engine(**kw) obj_cls = objects.ScoringEngine
scoring_engine = objects.ScoringEngine(context) db_data = db_utils.get_test_scoring_engine(**kw)
for key in db_scoring_engine: obj_data = _load_related_objects(context, obj_cls, db_data)
setattr(scoring_engine, key, db_scoring_engine[key])
return scoring_engine return _load_test_obj(context, obj_cls, obj_data, **kw)
def create_test_scoring_engine(context, **kw): def create_test_scoring_engine(context, **kw):
@@ -198,13 +190,11 @@ def get_test_service(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_service = db_utils.get_test_service(**kw) obj_cls = objects.Service
service = objects.Service(context) db_data = db_utils.get_test_service(**kw)
for key in db_service: obj_data = _load_related_objects(context, obj_cls, db_data)
if key == 'last_seen_up':
db_service[key] = None return _load_test_obj(context, obj_cls, obj_data, **kw)
setattr(service, key, db_service[key])
return service
def create_test_service(context, **kw): def create_test_service(context, **kw):
@@ -224,22 +214,11 @@ def get_test_strategy(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_strategy = db_utils.get_test_strategy(**kw) obj_cls = objects.Strategy
# Let DB generate ID if it isn't specified explicitly db_data = db_utils.get_test_strategy(**kw)
if 'id' not in kw: obj_data = _load_related_objects(context, obj_cls, db_data)
del db_strategy['id']
strategy = objects.Strategy(context)
for key in db_strategy:
setattr(strategy, key, db_strategy[key])
# ObjectField checks for the object type, so if we want to simulate a return _load_test_obj(context, obj_cls, obj_data, **kw)
# non-eager object loading, the field should not be referenced at all.
# Contrarily, eager loading need the data to be casted to the object type
# that was specified by the ObjectField.
if kw.get('goal'):
strategy.goal = objects.Goal(context, **kw.get('goal'))
return strategy
def create_test_strategy(context, **kw): def create_test_strategy(context, **kw):
@@ -259,14 +238,11 @@ def get_test_efficacy_indicator(context, **kw):
NOTE: The object leaves the attributes marked as changed, such NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB. that a create() could be used to commit it to the DB.
""" """
db_efficacy_indicator = db_utils.get_test_efficacy_indicator(**kw) obj_cls = objects.EfficacyIndicator
# Let DB generate ID if it isn't specified explicitly db_data = db_utils.get_test_efficacy_indicator(**kw)
if 'id' not in kw: obj_data = _load_related_objects(context, obj_cls, db_data)
del db_efficacy_indicator['id']
efficacy_indicator = objects.EfficacyIndicator(context) return _load_test_obj(context, obj_cls, obj_data, **kw)
for key in db_efficacy_indicator:
setattr(efficacy_indicator, key, db_efficacy_indicator[key])
return efficacy_indicator
def create_test_efficacy_indicator(context, **kw): def create_test_efficacy_indicator(context, **kw):

View File

@@ -213,6 +213,18 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
return self._patch_request('/action_plans', action_plan_uuid, patch) return self._patch_request('/action_plans', action_plan_uuid, patch)
@base.handle_errors
def start_action_plan(self, action_plan_uuid):
"""Start the specified action plan
:param action_plan_uuid: The unique identifier of the action_plan
:return: Tuple with the server response and the updated action_plan
"""
return self._patch_request(
'/action_plans', action_plan_uuid,
[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}])
# ### GOALS ### # # ### GOALS ### #
@base.handle_errors @base.handle_errors

View File

@@ -214,7 +214,7 @@ class BaseInfraOptimTest(test.BaseTestCase):
audit_uuid = audit['uuid'] audit_uuid = audit['uuid']
assert test.call_until_true( assert test.call_until_true(
func=functools.partial(cls.has_audit_succeeded, audit_uuid), func=functools.partial(cls.has_audit_finished, audit_uuid),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
) )

View File

@@ -35,7 +35,7 @@ class TestShowListAction(base.BaseInfraOptimTest):
_, cls.audit = cls.create_audit(cls.audit_template['uuid']) _, cls.audit = cls.create_audit(cls.audit_template['uuid'])
assert test.call_until_true( assert test.call_until_true(
func=functools.partial(cls.has_audit_succeeded, cls.audit['uuid']), func=functools.partial(cls.has_audit_finished, cls.audit['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
) )
@@ -45,19 +45,23 @@ class TestShowListAction(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_show_one_action(self): def test_show_one_action(self):
_, action_uuid = self.client.list_actions( _, body = self.client.list_actions(
action_plan_uuid=self.action_plan['uuid'])['actions'][0]['uuid'] action_plan_uuid=self.action_plan["uuid"])
_, action = self.client.show_action(action_uuid) actions = body['actions']
self.assertEqual(action_uuid, action['uuid']) _, action = self.client.show_action(actions[0]["uuid"])
self.assertEqual("nop", action['action_type'])
self.assertEqual(self.action_plan["uuid"], action['action_plan_uuid'])
self.assertEqual("PENDING", action['state']) self.assertEqual("PENDING", action['state'])
@test.attr(type='smoke') @test.attr(type='smoke')
def test_show_action_with_links(self): def test_show_action_with_links(self):
_, action_uuid = self.client.list_actions( _, body = self.client.list_actions(
action_plan_uuid=self.action_plan['uuid'])['actions'][0]['uuid'] action_plan_uuid=self.action_plan["uuid"])
_, action = self.client.show_action(action_uuid) actions = body['actions']
_, action = self.client.show_action(actions[0]["uuid"])
self.assertIn('links', action.keys()) self.assertIn('links', action.keys())
self.assertEqual(2, len(action['links'])) self.assertEqual(2, len(action['links']))
self.assertIn(action['uuid'], action['links'][0]['href']) self.assertIn(action['uuid'], action['links'][0]['href'])

View File

@@ -29,12 +29,12 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_create_action_plan(self): def test_create_action_plan(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
_, audit = self.create_audit(audit_template['uuid']) _, audit = self.create_audit(audit_template['uuid'])
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_finished, audit['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
@@ -49,12 +49,12 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_delete_action_plan(self): def test_delete_action_plan(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
_, audit = self.create_audit(audit_template['uuid']) _, audit = self.create_audit(audit_template['uuid'])
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_finished, audit['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
@@ -71,12 +71,12 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_execute_dummy_action_plan(self): def test_execute_dummy_action_plan(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
_, audit = self.create_audit(audit_template['uuid']) _, audit = self.create_audit(audit_template['uuid'])
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_finished, audit['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
@@ -86,11 +86,13 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
_, action_plan = self.client.show_action_plan(action_plan['uuid']) _, action_plan = self.client.show_action_plan(action_plan['uuid'])
if action_plan['state'] in ['SUPERSEDED', 'SUCCEEDED']:
# This means the action plan is superseded so we cannot trigger it,
# or it is empty.
return
# Execute the action by changing its state to PENDING # Execute the action by changing its state to PENDING
_, updated_ap = self.client.update_action_plan( _, updated_ap = self.client.start_action_plan(action_plan['uuid'])
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}]
)
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial( func=functools.partial(
@@ -110,12 +112,12 @@ class TestShowListActionPlan(base.BaseInfraOptimTest):
@classmethod @classmethod
def resource_setup(cls): def resource_setup(cls):
super(TestShowListActionPlan, cls).resource_setup() super(TestShowListActionPlan, cls).resource_setup()
_, cls.goal = cls.client.show_goal("DUMMY") _, cls.goal = cls.client.show_goal("dummy")
_, cls.audit_template = cls.create_audit_template(cls.goal['uuid']) _, cls.audit_template = cls.create_audit_template(cls.goal['uuid'])
_, cls.audit = cls.create_audit(cls.audit_template['uuid']) _, cls.audit = cls.create_audit(cls.audit_template['uuid'])
assert test.call_until_true( assert test.call_until_true(
func=functools.partial(cls.has_audit_succeeded, cls.audit['uuid']), func=functools.partial(cls.has_audit_finished, cls.audit['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
) )

View File

@@ -38,7 +38,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_create_audit_oneshot(self): def test_create_audit_oneshot(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
audit_params = dict( audit_params = dict(
@@ -56,7 +56,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_create_audit_continuous(self): def test_create_audit_continuous(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
audit_params = dict( audit_params = dict(
@@ -85,7 +85,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_create_audit_with_invalid_state(self): def test_create_audit_with_invalid_state(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
audit_params = dict( audit_params = dict(
@@ -98,7 +98,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_create_audit_with_no_state(self): def test_create_audit_with_no_state(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
audit_params = dict( audit_params = dict(
@@ -120,7 +120,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke') @test.attr(type='smoke')
def test_delete_audit(self): def test_delete_audit(self):
_, goal = self.client.show_goal("DUMMY") _, goal = self.client.show_goal("dummy")
_, audit_template = self.create_audit_template(goal['uuid']) _, audit_template = self.create_audit_template(goal['uuid'])
_, body = self.create_audit(audit_template['uuid']) _, body = self.create_audit(audit_template['uuid'])
audit_uuid = body['uuid'] audit_uuid = body['uuid']
@@ -158,7 +158,7 @@ class TestShowListAudit(base.BaseInfraOptimTest):
@classmethod @classmethod
def resource_setup(cls): def resource_setup(cls):
super(TestShowListAudit, cls).resource_setup() super(TestShowListAudit, cls).resource_setup()
_, cls.goal = cls.client.show_goal("DUMMY") _, cls.goal = cls.client.show_goal("dummy")
_, cls.audit_template = cls.create_audit_template(cls.goal['uuid']) _, cls.audit_template = cls.create_audit_template(cls.goal['uuid'])
_, cls.audit = cls.create_audit(cls.audit_template['uuid']) _, cls.audit = cls.create_audit(cls.audit_template['uuid'])

View File

@@ -36,6 +36,11 @@ CONF = config.CONF
class BaseInfraOptimScenarioTest(manager.ScenarioTest): class BaseInfraOptimScenarioTest(manager.ScenarioTest):
"""Base class for Infrastructure Optimization API tests.""" """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')
# States where the object can only be DELETED (end of its life-cycle)
FINISHED_STATES = ('FAILED', 'SUCCEEDED', 'CANCELLED', 'SUPERSEDED')
@classmethod @classmethod
def setup_credentials(cls): def setup_credentials(cls):
cls._check_network_config() cls._check_network_config()
@@ -142,6 +147,11 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
return audit.get('state') == 'SUCCEEDED' return audit.get('state') == 'SUCCEEDED'
@classmethod
def has_audit_finished(cls, audit_uuid):
_, audit = cls.client.show_audit(audit_uuid)
return audit.get('state') in cls.FINISHED_STATES
# ### ACTION PLANS ### # # ### ACTION PLANS ### #
def delete_action_plan(self, action_plan_uuid): def delete_action_plan(self, action_plan_uuid):

View File

@@ -148,24 +148,31 @@ class TestExecuteBasicStrategy(base.BaseInfraOptimScenarioTest):
try: try:
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial( func=functools.partial(
self.has_audit_succeeded, audit['uuid']), self.has_audit_finished, audit['uuid']),
duration=600, duration=600,
sleep_for=2 sleep_for=2
)) ))
except ValueError: except ValueError:
self.fail("The audit has failed!") self.fail("The audit has failed!")
_, finished_audit = self.client.show_audit(audit['uuid'])
if finished_audit.get('state') in ('FAILED', 'CANCELLED'):
self.fail("The audit ended in unexpected state: %s!",
finished_audit.get('state'))
_, action_plans = self.client.list_action_plans( _, action_plans = self.client.list_action_plans(
audit_uuid=audit['uuid']) audit_uuid=audit['uuid'])
action_plan = action_plans['action_plans'][0] action_plan = action_plans['action_plans'][0]
_, action_plan = self.client.show_action_plan(action_plan['uuid']) _, action_plan = self.client.show_action_plan(action_plan['uuid'])
if action_plan['state'] in ('SUPERSEDED', 'SUCCEEDED'):
# This means the action plan is superseded so we cannot trigger it,
# or it is empty.
return
# Execute the action by changing its state to PENDING # Execute the action by changing its state to PENDING
_, updated_ap = self.client.update_action_plan( _, updated_ap = self.client.start_action_plan(action_plan['uuid'])
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}]
)
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial( func=functools.partial(

View File

@@ -42,21 +42,26 @@ class TestExecuteDummyStrategy(base.BaseInfraOptimScenarioTest):
_, audit = self.create_audit(audit_template['uuid']) _, audit = self.create_audit(audit_template['uuid'])
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_finished, audit['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
self.assertTrue(self.has_audit_succeeded(audit['uuid']))
_, action_plans = self.client.list_action_plans( _, action_plans = self.client.list_action_plans(
audit_uuid=audit['uuid']) audit_uuid=audit['uuid'])
action_plan = action_plans['action_plans'][0] action_plan = action_plans['action_plans'][0]
_, action_plan = self.client.show_action_plan(action_plan['uuid']) _, action_plan = self.client.show_action_plan(action_plan['uuid'])
if action_plan['state'] in ['SUPERSEDED', 'SUCCEEDED']:
# This means the action plan is superseded so we cannot trigger it,
# or it is empty.
return
# Execute the action by changing its state to PENDING # Execute the action by changing its state to PENDING
_, updated_ap = self.client.update_action_plan( _, updated_ap = self.client.start_action_plan(action_plan['uuid'])
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}]
)
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial( func=functools.partial(