Compare commits

...

28 Commits

Author SHA1 Message Date
OpenDev Sysadmins
8d5e816eaa OpenDev Migration Patch
This commit was bulk generated and pushed by the OpenDev sysadmins
as a part of the Git hosting and code review systems migration
detailed in these mailing list posts:

http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003603.html
http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004920.html

Attempts have been made to correct repository namespaces and
hostnames based on simple pattern matching, but it's possible some
were updated incorrectly or missed entirely. Please reach out to us
via the contact information listed at https://opendev.org/ with any
questions you may have.
2019-04-19 19:40:45 +00:00
Ian Wienand
9c3736cb1b Replace openstack.org git:// URLs with https://
This is a mechanically generated change to replace openstack.org
git:// URLs with https:// equivalents.

This is in aid of a planned future move of the git hosting
infrastructure to a self-hosted instance of gitea (https://gitea.io),
which does not support the git wire protocol at this stage.

This update should result in no functional change.

For more information see the thread at

 http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003825.html

Change-Id: I9e141aa8e7387a2a8d96fce1d3f54a159d33f4d9
2019-03-24 20:36:25 +00:00
Nguyen Hai
520cdb3c51 import zuul job settings from project-config
This is a mechanically generated patch to complete step 1 of moving
the zuul job settings out of project-config and into each project
repository.

Because there will be a separate patch on each branch, the branch
specifiers for branch-specific jobs have been removed.

Because this patch is generated by a script, there may be some
cosmetic changes to the layout of the YAML file(s) as the contents are
normalized.

See the python3-first goal document for details:
https://governance.openstack.org/tc/goals/stein/python3-first.html

Change-Id: I3e87bc419d17348c71733c79016a27fa6c00955c
Story: #2002586
Task: #24344
2018-08-19 00:58:37 +09:00
OpenStack Proposal Bot
4d8d1cd356 Updated from global requirements
Change-Id: I8c3d97e4003ecc5aeeb1b5304fcb36a58367fa9f
2017-05-15 23:41:13 +00:00
OpenStack Proposal Bot
6d4fb3c3eb Updated from global requirements
Change-Id: Idb62b9824ccd8cad7a4084bd085ccf014d5610fa
2017-03-09 18:26:17 +00:00
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
83 changed files with 3436 additions and 609 deletions

View File

@@ -1,4 +1,5 @@
[gerrit]
host=review.openstack.org
host=review.opendev.org
port=29418
project=openstack/watcher.git
defaultbranch=stable/ocata

9
.zuul.yaml Normal file
View File

@@ -0,0 +1,9 @@
- project:
templates:
- openstack-python-jobs
- openstack-python35-jobs
- publish-openstack-sphinx-docs
- check-requirements
- release-notes-jobs
gate:
queue: watcher

View File

@@ -188,7 +188,7 @@ function init_watcher {
recreate_database watcher
# 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
create_watcher_cache_dir
}

View File

@@ -35,7 +35,7 @@ VNCSERVER_PROXYCLIENT_ADDRESS=$HOST_IP
NOVA_INSTANCES_PATH=/opt/stack/data/instances
# Enable the Ceilometer plugin for the compute agent
enable_plugin ceilometer git://git.openstack.org/openstack/ceilometer
enable_plugin ceilometer https://git.openstack.org/openstack/ceilometer
disable_service ceilometer-acentral,ceilometer-collector,ceilometer-api
LOGFILE=$DEST/logs/stack.sh.log

View File

@@ -28,13 +28,13 @@ ENABLED_SERVICES+=,q-svc,q-dhcp,q-meta,q-agt,q-l3,neutron
enable_service n-cauth
# Enable the Watcher Dashboard plugin
# enable_plugin watcher-dashboard git://git.openstack.org/openstack/watcher-dashboard
# enable_plugin watcher-dashboard https://git.openstack.org/openstack/watcher-dashboard
# Enable the Watcher plugin
enable_plugin watcher git://git.openstack.org/openstack/watcher
enable_plugin watcher https://git.openstack.org/openstack/watcher
# Enable the Ceilometer plugin
enable_plugin ceilometer git://git.openstack.org/openstack/ceilometer
enable_plugin ceilometer https://git.openstack.org/openstack/ceilometer
# This is the controller node, so disable the ceilometer compute agent
disable_service ceilometer-acompute

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ Overview
Below you will find a diagram, showing the main components of Watcher:
.. image:: ./images/architecture.svg
:width: 100%
:width: 110%
.. _components_definition:
@@ -37,13 +37,12 @@ AMQP Bus
The AMQP message bus handles internal asynchronous communications between the
different Watcher components.
.. _cluster_history_db_definition:
.. _cluster_datasource_definition:
Cluster History Database
------------------------
Datasource
----------
This component stores the data related to the
:ref:`Cluster History <cluster_history_definition>`.
This component stores the metrics related to the cluster.
It can potentially rely on any appropriate storage system (InfluxDB, OpenTSDB,
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
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:
Watcher API
@@ -193,8 +184,8 @@ data:
: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
the current state of a given :ref:`cluster <cluster_definition>`.
- The data stored in the :ref:`Cluster History Database
<cluster_history_db_definition>` which provides information about the past of
- The data stored in the :ref:`Cluster Datasource
<cluster_datasource_definition>` which provides information about the past of
the :ref:`Cluster <cluster_definition>`.
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
**RECOMMENDED**, **PENDING** or **ONGOING** state and was cancelled by the
: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

View File

@@ -165,7 +165,7 @@ You can easily generate and update a sample configuration file
named :ref:`watcher.conf.sample <watcher_sample_configuration_files>` by using
these following commands::
$ git clone git://git.openstack.org/openstack/watcher
$ git clone https://git.openstack.org/openstack/watcher
$ cd watcher/
$ tox -e genconfig
$ vi etc/watcher/watcher.conf.sample

View File

@@ -19,7 +19,7 @@ model. To enable the Watcher plugin with DevStack, add the following to the
`[[local|localrc]]` section of your controller's `local.conf` to enable the
Watcher plugin::
enable_plugin watcher git://git.openstack.org/openstack/watcher
enable_plugin watcher https://git.openstack.org/openstack/watcher
For more detailed instructions, see `Detailed DevStack Instructions`_. Check
out the `DevStack documentation`_ for more information regarding DevStack.

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
.. _`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
-------------------------------------------
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:
The following code snippet shows how to invoke a Datasource Helper class:
.. code-block:: py
# Within your strategy "execute()"
cclient = self.osc.ceilometer
# TODO: Do something here
from watcher.datasource import ceilometer as ceil
from watcher.datasource import monasca as mon
@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:
.. code-block:: py
query = None # e.g. [{'field': 'foo', 'op': 'le', 'value': 34},]
value_cpu = cclient.samples.list(
meter_name='cpu_util',
limit=10, q=query)
Read usage metrics using the Watcher Cluster History Helper
-----------------------------------------------------------
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
.. autoclass:: watcher.decision_engine.cluster.history.base.BaseClusterHistory
:members:
:noindex:
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'
)
if self.config.datasource == "ceilometer":
resource_id = "%s_%s" % (node.uuid, node.hostname)
return self.ceilometer.statistic_aggregation(
resource_id=resource_id,
meter_name='compute.node.cpu.percent',
period="7200",
aggregate='avg',
)
elif self.config.datasource == "monasca":
statistics = self.monasca.statistic_aggregation(
meter_name='compute.node.cpu.percent',
dimensions=dict(hostname=node.uuid),
period=7200,
aggregate='avg'
)

View File

@@ -101,12 +101,6 @@ Cluster Data Model (CDM)
.. watcher-term:: watcher.decision_engine.model.collector.base
.. _cluster_history_definition:
Cluster History
===============
.. watcher-term:: watcher.decision_engine.cluster.history.base
.. _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
ONGOING --> 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
CANCELLED --> DELETED
SUPERSEDED --> DELETED
DELETED --> [*]
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -60,6 +60,7 @@ table(audits) {
interval : Integer, nullable
parameters : JSONEncodedDict, nullable
scope : JSONEncodedList, nullable
auto_trigger: Boolean
created_at : DateTime
updated_at : DateTime
@@ -73,7 +74,6 @@ table(action_plans) {
foreign_key("audit_id : Integer, nullable")
foreign_key("strategy_id : Integer")
uuid : String[36]
first_action_id : Integer
state : String[20], nullable
global_efficacy : JSONEncodedDict, nullable
@@ -91,7 +91,7 @@ table(actions) {
action_type : String[255]
input_parameters : JSONEncodedDict, nullable
state : String[20], nullable
next : String[36], nullable
parents : JSONEncodedList, nullable
created_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]
usedevelop = True
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 =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt

View File

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

View File

@@ -50,6 +50,21 @@ from watcher.decision_engine import rpcapi
from watcher import objects
ALLOWED_AUDIT_TRANSITIONS = {
objects.audit.State.PENDING:
[objects.audit.State.ONGOING, objects.audit.State.CANCELLED],
objects.audit.State.ONGOING:
[objects.audit.State.FAILED, objects.audit.State.SUCCEEDED,
objects.audit.State.CANCELLED],
objects.audit.State.FAILED:
[objects.audit.State.DELETED],
objects.audit.State.SUCCEEDED:
[objects.audit.State.DELETED],
objects.audit.State.CANCELLED:
[objects.audit.State.DELETED]
}
class AuditPostType(wtypes.Base):
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=False)
@@ -561,6 +576,17 @@ class AuditsController(rest.RestController):
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
initial_state = audit_dict['state']
new_state = api_utils.get_patch_value(patch, 'state')
allowed_states = ALLOWED_AUDIT_TRANSITIONS.get(initial_state, [])
if new_state is not None and new_state not in allowed_states:
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
raise exception.PatchError(
patch=patch,
reason=error_message % dict(
initial_state=initial_state, new_state=new_state))
# Update only the fields that have changed
for field in objects.Audit.fields:
try:

View File

@@ -333,6 +333,7 @@ class AuditTemplate(base.APIBase):
self.fields.append('goal_id')
self.fields.append('strategy_id')
setattr(self, 'strategy_id', kwargs.get('strategy_id', wtypes.Unset))
# goal_uuid & strategy_uuid are not part of
# 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))
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):
filters_dict = {}
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 import default
from watcher import notifications
from watcher import objects
from watcher.objects import fields
LOG = log.getLogger(__name__)
class DefaultActionPlanHandler(base.BaseActionPlanHandler):
def __init__(self, context, service, action_plan_uuid):
super(DefaultActionPlanHandler, self).__init__()
self.ctx = context
self.service = service
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):
try:
self.update_action_plan(self.action_plan_uuid,
objects.action_plan.State.ONGOING)
action_plan = objects.ActionPlan.get_by_uuid(
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.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:
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:
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):
LOG.debug("Executing action plan %s ", action_plan_uuid)
filters = {'action_plan_uuid': action_plan_uuid}
actions = objects.Action.list(self.context,
filters=filters)
actions = objects.Action.list(self.context, filters=filters)
return self.engine.execute(actions)

View File

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

View File

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

View File

@@ -174,6 +174,14 @@ class EagerlyLoadedAuditRequired(InvalidAudit):
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):
msg_fmt = _("Expected a uuid but received %(uuid)s")
@@ -402,11 +410,15 @@ class WildcardCharacterIsUsed(WatcherException):
# 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")
class ComputeNodeNotFound(WatcherException):
class ComputeNodeNotFound(ComputeResourceNotFound):
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'
config = config or _alembic_config()
alembic.command.upgrade(config, revision or 'head')
alembic.command.upgrade(config, revision)
def create_schema(config=None, engine=None):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -415,11 +415,11 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
sorted_score):
number_migrations = 0
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(
node_to_release)
mig_destination_node = self.compute_model.get_node_by_uuid(
sorted_score[j][0])
node_uuid)
result = self.check_migration(
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():
filters = {"strategy_id": strategy_id}
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)
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():
filters = {"audit_id": audit_id}
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)
for action_plan in stale_action_plans:
@@ -448,7 +448,7 @@ class Syncer(object):
audit.id].state = objects.audit.State.CANCELLED
stale_action_plans = objects.ActionPlan.list(
self.ctx, filters=filters)
self.ctx, filters=filters, eager=True)
for action_plan in stale_action_plans:
LOG.warning(
_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
# Todo(gibi): remove these imports after legacy notifications using these are
# transformed to versioned notifications
from watcher.notifications import action_plan # noqa
from watcher.notifications import audit # noqa
from watcher.notifications import exception # 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
class AuditPayload(notificationbase.NotificationPayloadBase):
class TerseAuditPayload(notificationbase.NotificationPayloadBase):
SCHEMA = {
'uuid': ('audit', 'uuid'),
@@ -57,19 +57,54 @@ class AuditPayload(notificationbase.NotificationPayloadBase):
'scope': wfields.FlexibleListOfDictField(nullable=True),
'goal_uuid': wfields.UUIDField(),
'strategy_uuid': wfields.UUIDField(nullable=True),
'goal': wfields.ObjectField('GoalPayload'),
'strategy': wfields.ObjectField('StrategyPayload', nullable=True),
'created_at': wfields.DateTimeField(nullable=True),
'updated_at': wfields.DateTimeField(nullable=True),
'deleted_at': wfields.DateTimeField(nullable=True),
}
def __init__(self, audit, **kwargs):
super(AuditPayload, self).__init__(**kwargs)
def __init__(self, audit, goal_uuid, strategy_uuid=None, **kwargs):
super(TerseAuditPayload, self).__init__(
goal_uuid=goal_uuid, strategy_uuid=strategy_uuid, **kwargs)
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
class AuditStateUpdatePayload(notificationbase.NotificationPayloadBase):
# Version 1.0: Initial version
@@ -91,6 +126,7 @@ class AuditCreatePayload(AuditPayload):
super(AuditCreatePayload, self).__init__(
audit=audit,
goal=goal,
goal_uuid=goal.uuid,
strategy=strategy)
@@ -107,6 +143,7 @@ class AuditUpdatePayload(AuditPayload):
audit=audit,
state_update=state_update,
goal=goal,
goal_uuid=goal.uuid,
strategy=strategy)
@@ -122,6 +159,7 @@ class AuditActionPayload(AuditPayload):
super(AuditActionPayload, self).__init__(
audit=audit,
goal=goal,
goal_uuid=goal.uuid,
strategy=strategy,
**kwargs)
@@ -136,6 +174,7 @@ class AuditDeletePayload(AuditPayload):
super(AuditDeletePayload, self).__init__(
audit=audit,
goal=goal,
goal_uuid=goal.uuid,
strategy=strategy)

View File

@@ -13,6 +13,7 @@
# under the License.
from oslo_config import cfg
from oslo_log import log
from watcher.common import exception
from watcher.common import rpc
@@ -20,6 +21,7 @@ from watcher.objects import base
from watcher.objects import fields as wfields
CONF = cfg.CONF
LOG = log.getLogger(__name__)
# Definition of notification levels in increasing order of severity
NOTIFY_LEVELS = {
@@ -59,7 +61,8 @@ class EventType(NotificationObject):
# Version 1.0: Initial version
# Version 1.1: Added STRATEGY 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 = {
'object': wfields.StringField(),
@@ -171,6 +174,7 @@ class NotificationBase(NotificationObject):
def _emit(self, context, event_type, publisher_id, payload):
notifier = rpc.get_notifier(publisher_id)
notify = getattr(notifier, self.priority)
LOG.debug("Emitting notification `%s`", event_type)
notify(context, event_type=event_type, payload=payload)
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 utils
from watcher.db import api as db_api
from watcher import notifications
from watcher import objects
from watcher.objects import base
from watcher.objects import fields as wfields
@@ -117,6 +118,39 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
'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
def get(cls, context, action_plan_id, eager=False):
"""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
self._from_db_object(self, db_action_plan, eager=True)
def _notify():
notifications.action_plan.send_create(self._context, self)
_notify()
@base.remotable
def destroy(self):
"""Delete the action plan from the DB"""
@@ -221,8 +260,16 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
"""
updates = self.obj_get_changes()
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)
def _notify():
notifications.action_plan.send_update(
self._context, self, old_state=self.old_state)
_notify()
self.obj_reset_changes()
@base.remotable
@@ -262,3 +309,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
obj = self._from_db_object(
self.__class__(self._context), db_obj, eager=False)
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
class WatcherComparableObject(ovo_base.ComparableVersionedObject):
pass
class WatcherPersistentObject(object):
"""Mixin class for Persistent objects.

View File

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

View File

@@ -11,6 +11,7 @@
# limitations under the License.
import datetime
import itertools
import mock
from oslo_config import cfg
@@ -267,7 +268,7 @@ class TestPatch(api_base.FunctionalTest):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
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)
self.assertNotEqual(new_state, response['state'])
@@ -343,6 +344,128 @@ class TestPatch(api_base.FunctionalTest):
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):
def setUp(self):

View File

@@ -18,6 +18,9 @@
import mock
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.tests.db import base
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 FakeApplierException(Exception):
pass
def setUp(self):
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_strategy(self.context)
obj_utils.create_test_audit(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(
self.context, mock.MagicMock(), self.action_plan.uuid)
command.execute()
action_plan = ap_objects.ActionPlan.get_by_uuid(
self.context, self.action_plan.uuid)
self.assertEqual(ap_objects.State.SUCCEEDED, action_plan.state)
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,
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'),
'deleted_at': kwargs.get('deleted_at'),
'parameters': kwargs.get('parameters', {}),
'interval': kwargs.get('period', 3600),
'interval': kwargs.get('interval', 3600),
'goal_id': kwargs.get('goal_id', 1),
'strategy_id': kwargs.get('strategy_id', None),
'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.context,
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.context,
uuid=uuidutils.generate_uuid(),
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')

View File

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

View File

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

View File

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

View File

@@ -42,10 +42,8 @@ class TestDefaultScope(base.TestCase):
for i in range(2)]
model = default.DefaultScope(audit_scope,
osc=mock.Mock()).get_scoped_model(cluster)
expected_edges = [('INSTANCE_2', 'Node_1')]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()]
self.assertEqual(sorted(expected_edges), sorted(edges))
self.assertEqual(sorted(expected_edges), sorted(model.edges()))
@mock.patch.object(nova_helper.NovaHelper, 'get_availability_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_7', 'Node_4'),
]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()]
self.assertEqual(sorted(expected_edges), sorted(edges))
self.assertEqual(sorted(expected_edges), sorted(model.edges()))
@mock.patch.object(nova_helper.NovaHelper, 'get_aggregate_detail')
@mock.patch.object(nova_helper.NovaHelper, 'get_aggregate_list')
@@ -199,8 +196,7 @@ class TestDefaultScope(base.TestCase):
('INSTANCE_1', 'Node_0'),
('INSTANCE_6', 'Node_3'),
('INSTANCE_7', 'Node_4')]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()]
self.assertEqual(sorted(expected_edges), sorted(edges))
self.assertEqual(sorted(expected_edges), sorted(model.edges()))
def test_remove_instances_from_model(self):
model = self.fake_cluster.generate_scenario_1()
@@ -213,5 +209,4 @@ class TestDefaultScope(base.TestCase):
('INSTANCE_5', 'Node_2'),
('INSTANCE_6', 'Node_3'),
('INSTANCE_7', 'Node_4')]
edges = [(src.uuid, dst.uuid) for src, dst in model.edges()]
self.assertEqual(sorted(expected_edges), sorted(edges))
self.assertEqual(sorted(expected_edges), sorted(model.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())
def test_send_invalid_audit(self):
audit = utils.get_test_audit(mock.Mock(), state='DOESNOTMATTER',
goal_id=1)
audit = utils.get_test_audit(
mock.Mock(), interval=None, state='DOESNOTMATTER', goal_id=1)
self.assertRaises(
exception.InvalidAudit,
@@ -53,7 +53,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_update_with_strategy(self):
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=self.goal, strategy=self.strategy)
notifications.audit.send_update(
@@ -71,7 +71,8 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"interval": 3600,
"interval": None,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -88,6 +89,7 @@ class TestAuditNotification(base.DbTestCase):
},
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -125,7 +127,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_update_without_strategy(self):
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)
notifications.audit.send_update(
mock.MagicMock(), audit, host='node0',
@@ -141,9 +143,10 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"interval": 3600,
"interval": None,
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -158,6 +161,7 @@ class TestAuditNotification(base.DbTestCase):
},
"watcher_object.name": "GoalPayload"
},
"strategy_uuid": None,
"strategy": None,
"deleted_at": None,
"scope": [],
@@ -182,7 +186,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_create(self):
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=self.goal.as_dict(), strategy=self.strategy.as_dict())
notifications.audit.send_create(
@@ -198,7 +202,8 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"interval": 3600,
"interval": None,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -215,6 +220,7 @@ class TestAuditNotification(base.DbTestCase):
},
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -243,7 +249,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_delete(self):
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)
notifications.audit.send_delete(
mock.MagicMock(), audit, host='node0')
@@ -259,7 +265,8 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
"watcher_object.data": {
"interval": 3600,
"interval": None,
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
"strategy": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -276,6 +283,7 @@ class TestAuditNotification(base.DbTestCase):
},
"parameters": {},
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": {
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0",
@@ -304,7 +312,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_action(self):
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=self.goal, strategy=self.strategy)
notifications.audit.send_action_notification(
@@ -326,6 +334,7 @@ class TestAuditNotification(base.DbTestCase):
"created_at": "2016-10-18T09:52:05Z",
"deleted_at": None,
"fault": None,
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
@@ -340,10 +349,12 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"interval": 3600,
"interval": None,
"parameters": {},
"scope": [],
"state": "ONGOING",
"strategy_uuid": (
"cb3d0b58-4415-4d90-b75b-1e96878730e3"),
"strategy": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
@@ -371,7 +382,7 @@ class TestAuditNotification(base.DbTestCase):
def test_send_audit_action_with_error(self):
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=self.goal, strategy=self.strategy)
@@ -407,6 +418,7 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
"goal": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",
@@ -421,10 +433,12 @@ class TestAuditNotification(base.DbTestCase):
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"interval": 3600,
"interval": None,
"parameters": {},
"scope": [],
"state": "ONGOING",
"strategy_uuid": (
"cb3d0b58-4415-4d90-b75b-1e96878730e3"),
"strategy": {
"watcher_object.data": {
"created_at": "2016-10-18T09:52:05Z",

View File

@@ -250,10 +250,11 @@ class TestNotificationBase(testbase.TestCase):
expected_notification_fingerprints = {
'EventType': '1.2-633c2d32fa849d2a6f8bda3b0db88332',
'EventType': '1.3-4258a2c86eca79fd34a7dffe1278eab9',
'ExceptionNotification': '1.0-9b69de0724fda8310d05e18418178866',
'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b',
'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545',
'TerseAuditPayload': '1.0-aaf31166b8698f08d12cae98c380b8e0',
'AuditPayload': '1.0-30c85c834648c8ca11f54fc5e084d86b',
'AuditStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5',
'AuditUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866',
@@ -266,6 +267,16 @@ expected_notification_fingerprints = {
'AuditActionPayload': '1.0-09f5d005f94ba9e5f6b9200170332c52',
'GoalPayload': '1.0-fa1fecb8b01dd047eef808ded4d50d1a',
'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.db.sqlalchemy import api as db_api
from watcher import notifications
from watcher import objects
from watcher.tests.db import base
from watcher.tests.db import utils
@@ -34,16 +35,19 @@ class TestActionPlanObject(base.DbTestCase):
('non_eager', dict(
eager=False,
fake_action_plan=utils.get_test_action_plan(
created_at=datetime.datetime.utcnow(),
audit_id=audit_id,
strategy_id=strategy_id))),
('eager_with_non_eager_load', dict(
eager=True,
fake_action_plan=utils.get_test_action_plan(
created_at=datetime.datetime.utcnow(),
audit_id=audit_id,
strategy_id=strategy_id))),
('eager_with_eager_load', dict(
eager=True,
fake_action_plan=utils.get_test_action_plan(
created_at=datetime.datetime.utcnow(),
strategy_id=strategy_id,
strategy=utils.get_test_strategy(id=strategy_id),
audit_id=audit_id,
@@ -52,6 +56,13 @@ class TestActionPlanObject(base.DbTestCase):
def setUp(self):
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_strategy = utils.create_test_strategy(
id=self.strategy_id, name="DUMMY")
@@ -80,6 +91,7 @@ class TestActionPlanObject(base.DbTestCase):
self.context, action_plan_id, eager=self.eager)
self.assertEqual(self.context, action_plan._context)
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')
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.assertEqual(self.context, action_plan._context)
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):
self.assertRaises(exception.InvalidIdentity,
@@ -107,14 +120,26 @@ class TestActionPlanObject(base.DbTestCase):
self.assertEqual(self.context, action_plans[0]._context)
for action_plan in action_plans:
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, 'get_action_plan_by_uuid')
def test_save(self, mock_get_action_plan, mock_update_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['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
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']
action_plan = objects.ActionPlan.get_by_uuid(
self.context, uuid, eager=self.eager)
@@ -127,6 +152,14 @@ class TestActionPlanObject(base.DbTestCase):
uuid, {'state': objects.action_plan.State.SUCCEEDED})
self.assertEqual(self.context, action_plan._context)
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')
def test_refresh(self, mock_get_action_plan):
@@ -150,6 +183,13 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase):
def setUp(self):
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_audit = utils.create_test_audit()
self.fake_action_plan = utils.get_test_action_plan(
@@ -202,7 +242,8 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase):
del expected_action_plan['strategy']
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()
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):
audit = objutils.get_test_audit(
self.context,
id=1,
goal_id=self.fake_goal.id,
strategy_id=self.fake_strategy.id,
goal=self.fake_goal.as_dict(),

View File

@@ -30,21 +30,27 @@ def _load_related_objects(context, cls, db_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):
"""Return a AuditTemplate object with appropriate attributes.
NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB.
"""
db_audit_template = db_utils.get_test_audit_template(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_audit_template['id']
audit_template = objects.AuditTemplate(context)
for key in db_audit_template:
setattr(audit_template, key, db_audit_template[key])
obj_cls = objects.AuditTemplate
db_data = db_utils.get_test_audit_template(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return audit_template
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_audit = db_utils.get_test_audit(**kw)
obj_data = _load_related_objects(context, objects.Audit, db_audit)
obj_cls = objects.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
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
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_action_plan = db_utils.get_test_action_plan(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_action_plan['id']
action_plan = objects.ActionPlan(context)
for key in db_action_plan:
setattr(action_plan, key, db_action_plan[key])
return action_plan
obj_cls = objects.ActionPlan
db_data = db_utils.get_test_action_plan(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_action = db_utils.get_test_action(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_action['id']
action = objects.Action(context)
for key in db_action:
setattr(action, key, db_action[key])
return action
obj_cls = objects.Action
db_data = db_utils.get_test_action(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_goal = db_utils.get_test_goal(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_goal['id']
goal = objects.Goal(context)
for key in db_goal:
setattr(goal, key, db_goal[key])
return goal
obj_cls = objects.Goal
db_data = db_utils.get_test_goal(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_scoring_engine = db_utils.get_test_scoring_engine(**kw)
scoring_engine = objects.ScoringEngine(context)
for key in db_scoring_engine:
setattr(scoring_engine, key, db_scoring_engine[key])
return scoring_engine
obj_cls = objects.ScoringEngine
db_data = db_utils.get_test_scoring_engine(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_service = db_utils.get_test_service(**kw)
service = objects.Service(context)
for key in db_service:
if key == 'last_seen_up':
db_service[key] = None
setattr(service, key, db_service[key])
return service
obj_cls = objects.Service
db_data = db_utils.get_test_service(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_strategy = db_utils.get_test_strategy(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_strategy['id']
strategy = objects.Strategy(context)
for key in db_strategy:
setattr(strategy, key, db_strategy[key])
obj_cls = objects.Strategy
db_data = db_utils.get_test_strategy(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
# ObjectField checks for the object type, so if we want to simulate a
# 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
return _load_test_obj(context, obj_cls, obj_data, **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
that a create() could be used to commit it to the DB.
"""
db_efficacy_indicator = db_utils.get_test_efficacy_indicator(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_efficacy_indicator['id']
efficacy_indicator = objects.EfficacyIndicator(context)
for key in db_efficacy_indicator:
setattr(efficacy_indicator, key, db_efficacy_indicator[key])
return efficacy_indicator
obj_cls = objects.EfficacyIndicator
db_data = db_utils.get_test_efficacy_indicator(**kw)
obj_data = _load_related_objects(context, obj_cls, db_data)
return _load_test_obj(context, obj_cls, obj_data, **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)
@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 ### #
@base.handle_errors

View File

@@ -214,7 +214,7 @@ class BaseInfraOptimTest(test.BaseTestCase):
audit_uuid = audit['uuid']
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,
sleep_for=.5
)

View File

@@ -35,7 +35,7 @@ class TestShowListAction(base.BaseInfraOptimTest):
_, cls.audit = cls.create_audit(cls.audit_template['uuid'])
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,
sleep_for=.5
)
@@ -45,19 +45,23 @@ class TestShowListAction(base.BaseInfraOptimTest):
@test.attr(type='smoke')
def test_show_one_action(self):
_, action_uuid = self.client.list_actions(
action_plan_uuid=self.action_plan['uuid'])['actions'][0]['uuid']
_, action = self.client.show_action(action_uuid)
_, body = self.client.list_actions(
action_plan_uuid=self.action_plan["uuid"])
actions = body['actions']
self.assertEqual(action_uuid, action['uuid'])
self.assertEqual("nop", action['action_type'])
_, action = self.client.show_action(actions[0]["uuid"])
self.assertEqual(self.action_plan["uuid"], action['action_plan_uuid'])
self.assertEqual("PENDING", action['state'])
@test.attr(type='smoke')
def test_show_action_with_links(self):
_, action_uuid = self.client.list_actions(
action_plan_uuid=self.action_plan['uuid'])['actions'][0]['uuid']
_, action = self.client.show_action(action_uuid)
_, body = self.client.list_actions(
action_plan_uuid=self.action_plan["uuid"])
actions = body['actions']
_, action = self.client.show_action(actions[0]["uuid"])
self.assertIn('links', action.keys())
self.assertEqual(2, len(action['links']))
self.assertIn(action['uuid'], action['links'][0]['href'])

View File

@@ -29,12 +29,12 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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 = self.create_audit(audit_template['uuid'])
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,
sleep_for=.5
))
@@ -49,12 +49,12 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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 = self.create_audit(audit_template['uuid'])
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,
sleep_for=.5
))
@@ -71,12 +71,12 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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 = self.create_audit(audit_template['uuid'])
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,
sleep_for=.5
))
@@ -86,11 +86,13 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
_, 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
_, updated_ap = self.client.update_action_plan(
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}]
)
_, updated_ap = self.client.start_action_plan(action_plan['uuid'])
self.assertTrue(test.call_until_true(
func=functools.partial(
@@ -110,12 +112,12 @@ class TestShowListActionPlan(base.BaseInfraOptimTest):
@classmethod
def resource_setup(cls):
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 = cls.create_audit(cls.audit_template['uuid'])
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,
sleep_for=.5
)

View File

@@ -38,7 +38,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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_params = dict(
@@ -56,7 +56,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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_params = dict(
@@ -85,7 +85,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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_params = dict(
@@ -98,7 +98,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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_params = dict(
@@ -120,7 +120,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
@test.attr(type='smoke')
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'])
_, body = self.create_audit(audit_template['uuid'])
audit_uuid = body['uuid']
@@ -158,7 +158,7 @@ class TestShowListAudit(base.BaseInfraOptimTest):
@classmethod
def resource_setup(cls):
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 = cls.create_audit(cls.audit_template['uuid'])

View File

@@ -36,6 +36,11 @@ CONF = config.CONF
class BaseInfraOptimScenarioTest(manager.ScenarioTest):
"""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
def setup_credentials(cls):
cls._check_network_config()
@@ -142,6 +147,11 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
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 ### #
def delete_action_plan(self, action_plan_uuid):

View File

@@ -148,24 +148,31 @@ class TestExecuteBasicStrategy(base.BaseInfraOptimScenarioTest):
try:
self.assertTrue(test.call_until_true(
func=functools.partial(
self.has_audit_succeeded, audit['uuid']),
self.has_audit_finished, audit['uuid']),
duration=600,
sleep_for=2
))
except ValueError:
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(
audit_uuid=audit['uuid'])
action_plan = action_plans['action_plans'][0]
_, 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
_, updated_ap = self.client.update_action_plan(
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}]
)
_, updated_ap = self.client.start_action_plan(action_plan['uuid'])
self.assertTrue(test.call_until_true(
func=functools.partial(

View File

@@ -42,21 +42,26 @@ class TestExecuteDummyStrategy(base.BaseInfraOptimScenarioTest):
_, audit = self.create_audit(audit_template['uuid'])
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,
sleep_for=.5
))
self.assertTrue(self.has_audit_succeeded(audit['uuid']))
_, action_plans = self.client.list_action_plans(
audit_uuid=audit['uuid'])
action_plan = action_plans['action_plans'][0]
_, 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
_, updated_ap = self.client.update_action_plan(
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'PENDING'}]
)
_, updated_ap = self.client.start_action_plan(action_plan['uuid'])
self.assertTrue(test.call_until_true(
func=functools.partial(