Compare commits

...

90 Commits

Author SHA1 Message Date
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
Jenkins
ecb0e218a9 Merge "add Ocata release notes" 2017-01-26 15:41:02 +00:00
Jenkins
4228647d15 Merge "Add period input parameter to basic strategy" 2017-01-26 15:35:11 +00:00
Antoine Cabot
4f1d758a40 add Ocata release notes
Change-Id: Iff0c7601f6ea1b35de1afb1d71b8aff08a967eab
2017-01-26 14:03:12 +00:00
Jenkins
b4433db20a Merge "Fix invalid mock on ceilometerclient" 2017-01-26 13:54:48 +00:00
Hidekazu Nakamura
e03f56e7c7 Add period input parameter to basic strategy
This patch set adds new period strategy input parameter
which allows allows to specify the time length of
statistic aggregation.

Partial-Bug: #1614021

Change-Id: I1a276206e5b2c05d8f94acdeb866c8822fa84f35
2017-01-26 16:50:07 +03:00
Vincent Françoise
d925166a25 Fix invalid mock on ceilometerclient
The mocks on _get_alarm_client() are now failing because it was renamed
as _get_redirect_client(). This fix reflects this name change.

Change-Id: Id61e74dc4adc0c939661462953b322d65104a651
2017-01-26 10:42:30 +01:00
Jenkins
4a5d8cf709 Merge "Remove obsolete Resource element" 2017-01-25 13:54:39 +00:00
Jenkins
e3c6db11c6 Merge "Graph cluster model instead of mapping one" 2017-01-25 13:54:27 +00: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
Jenkins
840d422b01 Merge "Fix building of model with a scoped exclusion rule" 2017-01-25 11:59:42 +00:00
OpenStack Proposal Bot
e9d8a2882f Updated from global requirements
Change-Id: I82bcbf318a846a94c3d61eb49e3769387738692f
2017-01-24 20:03:01 +00:00
Alexander Chadin
6e09cdb5ac New Applier Workflow Engine
This patch set allows to execute Watcher Actions in parallel.
New config option max_workers sets max number of threads
to work.

Implements: blueprint parallel-applier
Change-Id: Ie4f3ed7e75936b434d308aa875eaa49d49f0c613
2017-01-24 18:26:22 +00:00
Vincent Françoise
edd3d219d5 Remove obsolete Resource element
Partially Implements: blueprint graph-based-cluster-model

Change-Id: I2765d1c7a864d6658c847dcd988314fc8f11049c
2017-01-24 18:26:11 +00:00
Vincent Françoise
d433d6b3c8 Graph cluster model instead of mapping one
In this changeset, I use https://review.openstack.org/#/c/362730/
as an example to make the existing ModelRoot fully graph-based.

Change-Id: I3a1ec8674b885d75221035459233722c18972f67
Implements: blueprint graph-based-cluster-model
2017-01-24 18:26:06 +00:00
David TARDIVEL
c5d4f9cb40 Fix building of model with a scoped exclusion rule
If we define an audit scope with a exclusion rule composed
with compute nodes, the compute model built is empty. I fix
the algo used to built this scoped compute model.

Change-Id: I675226c58474b7ea52b3f92e7b466adae40643b8
Closes-bug: #1658995
2017-01-24 18:25:13 +00:00
Vincent Françoise
41f579d464 Fix broken gates because of wrong pip command
Change-Id: I7d8cb4bfa7962819b7298149e94f06be44f2670e
2017-01-24 18:12:15 +01:00
Jenkins
1a2fa9addf Merge "Update configuration document" 2017-01-23 13:35:27 +00:00
Jenkins
9e5ca76116 Merge "Fix test_clients_monasca failure" 2017-01-23 13:31:00 +00:00
Jenkins
fa63b2a2b3 Merge "Updated from global requirements" 2017-01-23 13:30:54 +00:00
Jenkins
22cad5651e Merge "Fix bad CDMC update on reception of service.update" 2017-01-23 13:30:33 +00:00
Jenkins
a912977336 Merge "resolve KeyError exception" 2017-01-23 13:29:27 +00:00
licanwei
2d7a375338 Fix test_clients_monasca failure
'cafile','certfile','keyfile' and 'insecure'
 need mock override.

Change-Id: I289904ed38f22d4475fe04f2005b795d07cb8d83
Closes-Bug: #1658553
2017-01-23 10:57:08 +08:00
OpenStack Proposal Bot
45b82e1898 Updated from global requirements
Change-Id: I4378e3c39766f70456ae633e49390fd8244bde88
2017-01-21 15:59:07 +00:00
Jenkins
926dbc8392 Merge "New default planner" 2017-01-19 14:08:40 +00:00
Alexander Chadin
0e440d37ee New default planner
Co-Authored-By: Vincent Francoise <Vincent.FRANCOISE@b-com.com>
Change-Id: Ide2c8fc521488e486eac8f9f89d3f808ccf4b4d7
Implements: blueprint planner-storage-action-plan
2017-01-19 13:16:57 +03:00
Jenkins
66934f29d3 Merge "Should use glanceclient to get images" 2017-01-18 10:05:54 +00:00
Jenkins
7039a9d247 Merge "Updated from global requirements" 2017-01-17 13:39:49 +00:00
Jenkins
bbbae0b105 Merge "Fix dummy strategy to use input parameters" 2017-01-17 13:39:43 +00:00
Jenkins
1d08d2eea1 Merge "Modify the field in tox.ini" 2017-01-17 10:19:56 +00:00
zhuzeyu
23442a4da6 Modify the field in tox.ini
We use 'genconfig' field to generate configuration file generally.

TrivialFix

Change-Id: I152613103594dd7bcbc8b7f53a12e223a25bb051
2017-01-17 18:01:56 +08:00
Jenkins
ed88b436af Merge "Add action plan SUPERSEDED state" 2017-01-17 09:59:09 +00:00
David TARDIVEL
a48a16596f Add action plan SUPERSEDED state
An action plan can be now go to SUPERSEDED state if another
action plan is on going for instance. It could be the case with
tempest test, because we can have several scenario tests running
at the same time.

Change-Id: I5449e545676463f0ea601e67993e9dffa67929a7
Closes-bug: #1657031
2017-01-17 09:41:19 +01:00
Hidekazu Nakamura
8f4a856bd2 Fix dummy strategy to use input parameters
Dummy strategy does not use input parameters.
This patch fixes it.

Change-Id: I9aa0414869e6f2d52dca5cea6055ff81067448ef
2017-01-17 09:01:26 +09:00
OpenStack Proposal Bot
85a46ce4d4 Updated from global requirements
Change-Id: Id0dc6c33430a3691986acf8604ff67d8b2ae9a24
2017-01-16 17:30:38 +00:00
David TARDIVEL
35066dfe60 Update Server Consolidation global efficacy
As instance migration cost is petty compared to the cost of
compute node release, I update the way to compute the global
efficacy for a server consolidation goal. The new formula is simplest
and it's only based on compute node.

Change-Id: Ibcce31a85af70429f412c96c584a761d681366a2
2017-01-16 16:56:24 +01:00
Jenkins
fc9eb6e995 Merge "Multi datasource support for Basic Consolidation" 2017-01-16 09:54:24 +00:00
Jenkins
aaf6fd7959 Merge "Added Monasca Helper" 2017-01-16 09:54:02 +00:00
Cao Xuan Hoang
5e077f37ea Fix a typo in watcher/objects/base.py
Removed redundant 'the'

Change-Id: I03cd038c7b1a5d266cb9a1264adc3902c23c971c
2017-01-16 11:18:57 +07:00
licanwei
2ec8bc10cd resolve KeyError exception
In the optimize solution stage when launching audit
based vm_workload_consolidation strategy.
The instance map in node should change when migration
action was removed. Otherwise a KeyError exception
will be thrown.

Change-Id: I054b6b12922892a02d155b4ddc001b19890d32c5
Closes-Bug: #1656157
2017-01-14 11:21:42 +08:00
Jenkins
89cea83c85 Merge "Add auto_trigger support to watcher" 2017-01-13 17:07:29 +00:00
David TARDIVEL
2dd00a2037 Enable notification for vm task state update
Add a script command to enable Nova compute.instance.update
notifications on VM and task state changes.

Change-Id: I639f003d92e184085a332f53c6783e5eca0002fe
2017-01-13 10:42:24 +01:00
Kevin_Zheng
59e13fd1f3 Should use glanceclient to get images
Getting images from Novaclient is refered as proxy API
and was deprecated to use in nova. We should use
Glanceclient to get images instead.

Closes-bug: #1655516
Change-Id: Ie7d89e857d149e11b3c9b44c980b0be5cb0cc35f
2017-01-13 02:07:04 +00:00
Vincent Françoise
4235ef7c24 Multi datasource support for Basic Consolidation
In this changeset, I added the support for both Monasca and
Ceilometer for the basic_consolidation strategy.

Partially Implements: blueprint monasca-support

Change-Id: Ide98550fbf4a29954e46650190a05be1b8800317
2017-01-12 17:51:59 +01:00
Vincent Françoise
a015af1bd2 Added Monasca Helper
In this changeset, I implemented a Helper class to deal with
Monasca requests.

Change-Id: I14cfab2c45451b8bb2ea5f1f48254b41fa5abae8
Partially-Implements: blueprint monasca-support
2017-01-12 17:51:58 +01:00
avnish
dad90b63fd Removed unnecessary utf-8 encoding
Change-Id: I967f933abd6c49f4262d39c2f406405a9ef18b16
2017-01-11 15:23:06 +05:30
Jenkins
c6e5f98008 Merge "Enable coverage report in console output" 2017-01-11 09:00:18 +00:00
OpenStack Proposal Bot
1341c0ee02 Updated from global requirements
Change-Id: I31678175f4e6cb5b55e45714b058ad548ce50ca9
2017-01-01 12:18:37 +00:00
Jeremy Liu
6e99fcffc3 Enable coverage report in console output
Change-Id: Iaba796a4126b2e106af5b4688cd6c94559a42a64
2016-12-30 16:37:24 +08:00
Jenkins
a57f54ab8f Merge "multinode devstack update for live-migration" 2016-12-28 16:51:38 +00:00
Jenkins
d1490e3fa7 Merge "Fix TypeError if no input_parameters added" 2016-12-27 16:21:12 +00:00
Jenkins
1324baf9f5 Merge "Add additional depencencies of CentOS 7" 2016-12-27 09:27:29 +00:00
Hidekazu Nakamura
0adc7d91e6 Fix TypeError if no input_parameters added
By calling solution.add_action with no input_parameters, TypeError:
'NoneType' object does not support item assignment was occurred.
This patch fix it.

Change-Id: Ia2ad0c18bc20468ca73c0ab70495fac2c90e0640
Closes-Bug: #1647927
2016-12-27 13:49:54 +09:00
Hidekazu Nakamura
0d7ded0bb3 Update configuration document
In this changeset, Adding admin role to watcher user of
admin project is removed.

Change-Id: Icc8167b12ec0669045d100f8b9ea94d2ac08837d
2016-12-27 13:01:50 +09:00
Hidekazu Nakamura
80dfbd6334 Add additional depencencies of CentOS 7
In this changeset, additional dependencies of CentOS 7 is added.

Change-Id: Ie513448ae39b9c006360792732a967d337775d8b
2016-12-27 09:35:36 +09:00
licanwei
7d40b3d4c3 Fix reference http
<http://docs.openstack.org/admin-guide-cloud/
telemetry-measurements.html> ==>
<http://docs.openstack.org/admin-guide/telemetry-measurements.html>

Change-Id: Iea22902db12b9bb05b9cd2d9a4ac95818f00f133
2016-12-26 16:07:12 +08:00
Jenkins
cedf70559e Merge "Updated from global requirements" 2016-12-20 09:33:21 +00:00
Jenkins
cc561c528f Merge "Fix variable name error" 2016-12-20 09:27:31 +00:00
鲍昱蒙00205026
87b494d52a remove incorrect inline comment
Change-Id: I4f5b48a07e146c727ebf724d1993633aa707cec8
2016-12-20 09:25:41 +08:00
Alexander Chadin
d0bca1f2ab Add auto_trigger support to watcher
This patch set adds support of auto-triggering of action plans.

Change-Id: I36b7dff8eab5f6ebb18f6f4e752cf4b263456293
Partially-Implements: blueprint automatic-triggering-audit
2016-12-19 12:37:26 +03:00
licanwei
068178f12a Fix variable name error
doc/source/dev/plugin/cdmc-plugin.rst
'collector' should be 'dummy_collector'

Change-Id: I2f8f7107faa4b1f707f424b696c9bc3c6d7e22f4
2016-12-19 11:36:41 +08:00
OpenStack Proposal Bot
99e6c4aebb Updated from global requirements
Change-Id: I2f191bc9e9316dfb2d029624a0c8773a576de004
2016-12-17 21:40:43 +00:00
Jenkins
b446f8afd2 Merge "Fix some incorrect description in doc." 2016-12-16 17:06:42 +00:00
Jenkins
7783ebfb71 Merge "Fix method name in doc/source/dev/plugin/action-plugin.rst" 2016-12-16 17:02:41 +00:00
Jenkins
22cfc495f4 Merge "Improve the instruction of vm_workload_consolidation." 2016-12-16 16:38:45 +00:00
Jenkins
360b0119d6 Merge "remove unused log" 2016-12-16 16:38:35 +00:00
OpenStack Proposal Bot
48fc90d7b6 Updated from global requirements
Change-Id: I8ffe399bda77a605f97c5128957b07d405ac5169
2016-12-16 13:49:28 +00:00
Jenkins
c5ff387ae9 Merge "Repair log parameter error" 2016-12-16 09:55:54 +00:00
David TARDIVEL
1bc6b0e605 Fix bad CDMC update on reception of service.update
When we receive a incoming 'service.update' notification
from nova, with disabled: false, we should set the related
compute node status to ENABLED.

Change-Id: Ib8075a5cf786f81ec41423805433f83ae721cbbd
Closes-bug: #1650485
2016-12-16 10:11:38 +01:00
Jenkins
345083e090 Merge "Function call pass parameter error" 2016-12-16 08:47:55 +00:00
licanwei
1a17c4b7ac remove unused log
remove unused log

Change-Id: Iad207a708388ecf940d502ef8f5f64d5cc8ff80e
2016-12-14 15:18:02 +08:00
licanwei
c4dfbd5855 Repair log parameter error
Multi-parameters should form a dict type

Change-Id: Ia883de6a0e34cebd28ba7fb0ecd105acb6cf2830
2016-12-14 14:24:20 +08:00
Amy Fong
1981f3964e multinode devstack update for live-migration
For multinode setup, a couple of updates to /etc/nova/nova.conf was
needed for live migration.

serial_console needed to be disabled and vncserver_listen needed to
be set to any for the vncserver to accept connections from all of the
compute nodes.

Change-Id: I62ebc2a07ca525bd80da130981f337806b2b89ae
Closes-Bug: #1649638
2016-12-13 12:31:51 -05:00
licanwei
d792e3cfae Function call pass parameter error
add_migration(self, instance_uuid, source_node,
                      destination_node, model)
param source_node: node object
param destination_node: node object

but in optimize_solution(), The incoming parameters are
source_node_uuid and destination_node_uuid.
This causes an exception:
AttributeError: 'unicode' object has no attribute 'state'

Change-Id: Ia27f219caa007f2b49ff9efc2544d5b4d894fe65
Closes-Bug: #1649441
2016-12-13 16:44:30 +08:00
zte-hanrong
f66eb463ca Improve the instruction of vm_workload_consolidation.
The goal of "vm_consolidation" is not existent by default.

Use the goal of "server_consolidation" instead for shell command.

Change-Id: Icef5536a337fa88a4504e23e4de6d2e96c45d7b6
2016-12-07 16:50:51 +08:00
Hidekazu Nakamura
6a323ed54f Fix method name in doc/source/dev/plugin/action-plugin.rst
precondition -> pre_condition
postcondition -> post_condition

Change-Id: Ia8edc9b428c14ea35225cbbe1a54b67a33151a64
2016-12-07 15:50:51 +09:00
ericxiett
d252d47cc0 Fix some incorrect description in doc.
doc/source/deploy/configuration.rst
* 'The Watcher system is a collection of services
that provides support to optimize your IAAS plateform. '

Fix: Typo, plateform -> platform

* Additionally, the Bare Metal service has certain
external dependencies, which are very similar to
other OpenStack services:

Fix: Bare Metal -> Watcher

* Please check your hypervisor configuration to correctly
handle instance migration.

Fix: ref of ``instance migration`` is http://docs.openstack.org/
admin-guide/compute-live-migration-usage.html

Change-Id: I00ab282a0f9ffcfddf745df1dd67418a43d70b10
Closes-Bug: #1646290
2016-12-06 04:00:18 +08:00
200 changed files with 8177 additions and 3038 deletions

View File

@@ -129,6 +129,7 @@ function create_watcher_conf {
iniset $WATCHER_CONF oslo_messaging_notifications driver "messaging"
iniset $NOVA_CONF oslo_messaging_notifications topics "notifications,watcher_notifications"
iniset $NOVA_CONF notifications notify_on_state_change "vm_and_task_state"
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR "watcher_clients_auth"
@@ -187,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

@@ -44,3 +44,6 @@ LOGDAYS=2
[[post-config|$NOVA_CONF]]
[DEFAULT]
compute_monitors=cpu.virt_driver
notify_on_state_change = vm_and_task_state
[notifications]
notify_on_state_change = vm_and_task_state

View File

@@ -45,3 +45,6 @@ LOGDAYS=2
[[post-config|$NOVA_CONF]]
[DEFAULT]
compute_monitors=cpu.virt_driver
notify_on_state_change = vm_and_task_state
[notifications]
notify_on_state_change = vm_and_task_state

View File

@@ -1,4 +1,3 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");

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

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# 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

View File

@@ -15,7 +15,7 @@ Service overview
================
The Watcher system is a collection of services that provides support to
optimize your IAAS plateform. The Watcher service may, depending upon
optimize your IAAS platform. The Watcher service may, depending upon
configuration, interact with several other OpenStack services. This includes:
- the OpenStack Identity service (`keystone`_) for request authentication and
@@ -37,7 +37,7 @@ The Watcher service includes the following components:
- `watcher-dashboard`_: An Horizon plugin for interacting with the Watcher
service.
Additionally, the Bare Metal service has certain external dependencies, which
Additionally, the Watcher service has certain external dependencies, which
are very similar to other OpenStack services:
- A database to store audit and action plan information and state. You can set
@@ -86,7 +86,6 @@ Configure the Identity service for the Watcher service
--tenant=KEYSTONE_SERVICE_PROJECT_NAME
$ keystone user-role-add --user=watcher \
--tenant=KEYSTONE_SERVICE_PROJECT_NAME --role=admin
$ keystone user-role-add --user=watcher --tenant=admin --role=admin
or (by using python-openstackclient 1.8.0+)
@@ -97,7 +96,6 @@ Configure the Identity service for the Watcher service
--project=KEYSTONE_SERVICE_PROJECT_NAME
$ openstack role add --project KEYSTONE_SERVICE_PROJECT_NAME \
--user watcher admin
$ openstack role add --user watcher --project admin admin
#. You must register the Watcher Service with the Identity Service so that
@@ -169,7 +167,7 @@ these following commands::
$ git clone git://git.openstack.org/openstack/watcher
$ cd watcher/
$ tox -econfig
$ tox -e genconfig
$ vi etc/watcher/watcher.conf.sample
@@ -368,7 +366,7 @@ Configure Nova compute
Please check your hypervisor configuration to correctly handle
`instance migration`_.
.. _`instance migration`: http://docs.openstack.org/admin-guide-cloud/compute-configuring-migrations.html
.. _`instance migration`: http://docs.openstack.org/admin-guide/compute-live-migration-usage.html
Configure Measurements
======================

View File

@@ -193,6 +193,37 @@ must exist in every other compute node's stack user's authorized_keys file and
every compute node's public ECDSA key needs to be in every other compute
node's root user's known_hosts file.
Disable serial console
----------------------
Serial console needs to be disabled for live migration to work.
On both the controller and compute node, in /etc/nova/nova.conf
[serial_console]
enabled = False
Alternatively, in devstack's local.conf:
[[post-config|$NOVA_CONF]]
[serial_console]
#enabled=false
VNC server configuration
------------------------
The VNC server listening parameter needs to be set to any address so
that the server can accept connections from all of the compute nodes.
On both the controller and compute node, in /etc/nova/nova.conf
vncserver_listen = 0.0.0.0
Alternatively, in devstack's local.conf:
VNCSERVER_LISTEN=0.0.0.0
Environment final checkup
-------------------------

View File

@@ -85,6 +85,9 @@ your platform.
$ sudo yum install openssl-devel libffi-devel mysql-devel
* CentOS 7::
$ sudo yum install gcc python-devel libxml2-devel libxslt-devel mariadb-devel
PyPi Packages and VirtualEnv
----------------------------

View File

@@ -30,12 +30,12 @@ implement:
implement. This is the first function to be called by the
:ref:`applier <watcher_applier_definition>` before any further processing
and its role is to validate the input parameters that were provided to it.
- The :py:meth:`~.BaseAction.precondition` is called before the execution of
- The :py:meth:`~.BaseAction.pre_condition` is called before the execution of
an action. This method is a hook that can be used to perform some
initializations or to make some more advanced validation on its input
parameters. If you wish to block the execution based on this factor, you
simply have to ``raise`` an exception.
- The :py:meth:`~.BaseAction.postcondition` is called after the execution of
- The :py:meth:`~.BaseAction.post_condition` is called after the execution of
an action. As this function is called regardless of whether an action
succeeded or not, this can prove itself useful to perform cleanup
operations.
@@ -71,11 +71,11 @@ Here is an example showing how you can write a plugin called ``DummyAction``:
# Does nothing
pass
def precondition(self):
def pre_condition(self):
# No pre-checks are done here
pass
def postcondition(self):
def post_condition(self):
# Nothing done here
pass

View File

@@ -268,5 +268,5 @@ At this point, you can use your new cluster data model plugin in your
# [...]
dummy_collector = self.collector_manager.get_cluster_model_collector(
"dummy") # "dummy" is the name of the entry point we declared earlier
dummy_model = collector.get_latest_cluster_data_model()
dummy_model = dummy_collector.get_latest_cluster_data_model()
# Do some stuff with this model

View File

@@ -245,75 +245,70 @@ Querying metrics
A large set of metrics, generated by OpenStack modules, can be used in your
strategy implementation. To collect these metrics, Watcher provides a
`Helper`_ to the Ceilometer API, which makes this API reusable and easier
to used.
`Helper`_ for two data sources which are `Ceilometer`_ and `Monasca`_. If you
wish to query metrics from a different data source, you can implement your own
and directly use it from within your new strategy. Indeed, strategies in
Watcher have the cluster data models decoupled from the data sources which
means that you may keep the former while changing the latter.
The recommended way for you to support a new data source is to implement a new
helper that would encapsulate within separate methods the queries you need to
perform. To then use it, you would just have to instantiate it within your
strategy.
If you want to use your own metrics database backend, please refer to the
`Ceilometer developer guide`_. Indeed, Ceilometer's pluggable model allows
for various types of backends. A list of the available backends is located
here_. The Ceilosca project is a good example of how to create your own
pluggable backend.
If you want to use Ceilometer but with your own metrics database backend,
please refer to the `Ceilometer developer guide`_. The list of the available
Ceilometer backends is located here_. The `Ceilosca`_ project is a good example
of how to create your own pluggable backend. Moreover, if your strategy
requires new metrics not covered by Ceilometer, you can add them through a
`Ceilometer plugin`_.
Finally, if your strategy requires new metrics not covered by Ceilometer, you
can add them through a Ceilometer `plugin`_.
.. _`Helper`: https://github.com/openstack/watcher/blob/master/watcher/decision_engine/cluster/history/ceilometer.py
.. _`Ceilometer developer guide`: http://docs.openstack.org/developer/ceilometer/architecture.html#storing-the-data
.. _`Ceilometer`: http://docs.openstack.org/developer/ceilometer/
.. _`Monasca`: https://github.com/openstack/monasca-api/blob/master/docs/monasca-api-spec.md
.. _`here`: http://docs.openstack.org/developer/ceilometer/install/dbreco.html#choosing-a-database-backend
.. _`plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
.. _`Ceilometer plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
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

@@ -71,6 +71,9 @@ parameter type default Value description
be tried by the strategy while
searching for potential candidates.
To remove the limit, set it to 0
``period`` Number 7200 The time interval in seconds
for getting statistic aggregation
from metric data source
====================== ====== ============= ===================================
Efficacy Indicator

View File

@@ -89,7 +89,7 @@ How to use it ?
.. code-block:: shell
$ openstack optimize audittemplate create \
at1 vm_consolidation --strategy vm_workload_consolidation
at1 server_consolidation --strategy vm_workload_consolidation
$ openstack optimize audit create -a at1

View File

@@ -1,4 +1,4 @@
To generate the sample watcher.conf file, run the following
command from the top level of the watcher directory:
tox -econfig
tox -e genconfig

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
---
features:
- Watcher can continuously optimize the OpenStack cloud for a specific
strategy or goal by triggering an audit periodically which generates
an action plan and run it automatically.

View File

@@ -0,0 +1,3 @@
---
features:
- Centralize all configuration options for Watcher.

View File

@@ -0,0 +1,3 @@
---
features:
- Watcher database can now be upgraded thanks to Alembic.

View File

@@ -0,0 +1,5 @@
---
features:
- Provides a generic way to define the scope of an audit. The set of audited
resources will be called "Audit scope" and will be defined in each audit
template (which contains the audit settings).

View File

@@ -0,0 +1,6 @@
---
features:
- The graph model describes how VMs are associated to compute hosts.
This allows for seeing relationships upfront between the entities and hence
can be used to identify hot/cold spots in the data center and influence
a strategy decision.

View File

@@ -0,0 +1,4 @@
---
features:
- Watcher supports multiple metrics backend and relies on Ceilometer and
Monasca.

View File

@@ -0,0 +1,4 @@
---
features:
- Watcher can now run specific actions in parallel improving the performances
dramatically when executing an action plan.

View File

@@ -0,0 +1,4 @@
---
features:
- Add superseded state for an action plan if the cluster data model has
changed after it has been created.

View File

@@ -0,0 +1,8 @@
---
features:
- Provide a notification mechanism into Watcher that supports versioning.
Whenever a Watcher object is created, updated or deleted, a versioned
notification will, if it's relevant, be automatically sent to notify in order
to allow an event-driven style of architecture within Watcher. Moreover, it
will also give other services and/or 3rd party softwares (e.g. monitoring
solutions or rules engines) the ability to react to such events.

View File

@@ -0,0 +1,3 @@
---
features:
- Add a service supervisor to watch Watcher deamons.

View File

@@ -0,0 +1,5 @@
---
features:
- all Watcher objects have been refactored to support OVO
(oslo.versionedobjects) which was a prerequisite step in order to implement
versioned notifications.

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# watcher documentation build configuration file, created by
# sphinx-quickstart on Fri Jun 3 11:37:52 2016.
#

View File

@@ -5,14 +5,14 @@
apscheduler # MIT License
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
jsonpatch>=1.1 # BSD
keystoneauth1>=2.14.0 # Apache-2.0
keystonemiddleware!=4.5.0,>=4.2.0 # Apache-2.0
lxml>=2.3 # BSD
keystoneauth1>=2.18.0 # Apache-2.0
keystonemiddleware>=4.12.0 # Apache-2.0
lxml!=3.7.0,>=2.3 # BSD
oslo.concurrency>=3.8.0 # Apache-2.0
oslo.cache>=1.5.0 # Apache-2.0
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
oslo.context>=2.9.0 # Apache-2.0
oslo.db!=4.13.1,!=4.13.2,>=4.11.0 # Apache-2.0
oslo.db>=4.15.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.messaging>=5.14.0 # Apache-2.0
@@ -31,12 +31,15 @@ python-ceilometerclient>=2.5.0 # Apache-2.0
python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0
python-glanceclient>=2.5.0 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0
python-monascaclient>=1.1.0 # Apache-2.0
python-neutronclient>=5.1.0 # Apache-2.0
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
python-novaclient!=7.0.0,>=6.0.0 # Apache-2.0
python-openstackclient>=3.3.0 # Apache-2.0
six>=1.9.0 # MIT
SQLAlchemy<1.1.0,>=1.0.10 # MIT
stevedore>=1.17.1 # Apache-2.0
taskflow>=1.26.0 # Apache-2.0
taskflow>=2.7.0 # Apache-2.0
WebOb>=1.6.0 # MIT
WSME>=0.8 # MIT
networkx>=1.10 # BSD

View File

@@ -64,6 +64,7 @@ watcher_scoring_engine_containers =
watcher_strategies =
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
dummy_with_scorer = watcher.decision_engine.strategy.strategies.dummy_with_scorer:DummyWithScorer
dummy_with_resize = watcher.decision_engine.strategy.strategies.dummy_with_resize:DummyWithResize
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
outlet_temperature = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
@@ -76,12 +77,14 @@ watcher_actions =
nop = watcher.applier.actions.nop:Nop
sleep = watcher.applier.actions.sleep:Sleep
change_nova_service_state = watcher.applier.actions.change_nova_service_state:ChangeNovaServiceState
resize = watcher.applier.actions.resize:Resize
watcher_workflow_engines =
taskflow = watcher.applier.workflow_engine.default:DefaultWorkFlowEngine
watcher_planners =
default = watcher.decision_engine.planner.default:DefaultPlanner
weight = watcher.decision_engine.planner.weight:WeightPlanner
workload_stabilization = watcher.decision_engine.planner.workload_stabilization:WorkloadStabilizationPlanner
watcher_cluster_data_model_collectors =
compute = watcher.decision_engine.model.collector.nova:NovaClusterDataModelCollector

10
tox.ini
View File

@@ -6,9 +6,7 @@ skipsdist = True
[testenv]
usedevelop = True
whitelist_externals = find
install_command =
pip install -U --force-reinstall -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} {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt
@@ -27,7 +25,9 @@ setenv = PYTHONHASHSEED=0
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
commands =
python setup.py testr --coverage --testr-args='{posargs}'
coverage report
[testenv:docs]
setenv = PYTHONHASHSEED=0
@@ -38,7 +38,7 @@ commands =
[testenv:debug]
commands = oslo_debug_helper -t watcher/tests {posargs}
[testenv:config]
[testenv:genconfig]
sitepackages = False
commands =
oslo-config-generator --config-file etc/watcher/watcher-config-generator.conf

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
# 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

View File

@@ -88,7 +88,6 @@ class Action(base.APIBase):
between the internal object model and the API representation of a action.
"""
_action_plan_uuid = None
_next_uuid = None
def _get_action_plan_uuid(self):
return self._action_plan_uuid
@@ -105,22 +104,6 @@ class Action(base.APIBase):
except exception.ActionPlanNotFound:
self._action_plan_uuid = None
def _get_next_uuid(self):
return self._next_uuid
def _set_next_uuid(self, value):
if value == wtypes.Unset:
self._next_uuid = wtypes.Unset
elif value and self._next_uuid != value:
try:
action_next = objects.Action.get(
pecan.request.context, value)
self._next_uuid = action_next.uuid
self.next = action_next.id
except exception.ActionNotFound:
self.action_next_uuid = None
# raise e
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action"""
@@ -138,10 +121,8 @@ class Action(base.APIBase):
input_parameters = types.jsontype
"""One or more key/value pairs """
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
_set_next_uuid,
mandatory=True)
"""This next action UUID"""
parents = wtypes.wsattr(types.jsontype, readonly=True)
"""UUIDs of parent actions"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated action links"""
@@ -152,7 +133,6 @@ class Action(base.APIBase):
self.fields = []
fields = list(objects.Action.fields)
fields.append('action_plan_uuid')
fields.append('next_uuid')
for field in fields:
# Skip fields we do not expose.
if not hasattr(self, field):
@@ -163,15 +143,13 @@ class Action(base.APIBase):
self.fields.append('action_plan_id')
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
wtypes.Unset))
setattr(self, 'next_uuid', kwargs.get('next',
wtypes.Unset))
@staticmethod
def _convert_with_links(action, url, expand=True):
if not expand:
action.unset_fields_except(['uuid', 'state', 'next', 'next_uuid',
'action_plan_uuid', 'action_plan_id',
'action_type'])
action.unset_fields_except(['uuid', 'state', 'action_plan_uuid',
'action_plan_id', 'action_type',
'parents'])
action.links = [link.Link.make_link('self', url,
'actions', action.uuid),
@@ -193,9 +171,9 @@ class Action(base.APIBase):
state='PENDING',
created_at=datetime.datetime.utcnow(),
deleted_at=None,
updated_at=datetime.datetime.utcnow())
updated_at=datetime.datetime.utcnow(),
parents=[])
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample._next_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -216,17 +194,6 @@ class ActionCollection(collection.Collection):
collection.actions = [Action.convert_with_links(p, expand)
for p in actions]
if 'sort_key' in kwargs:
reverse = False
if kwargs['sort_key'] == 'next_uuid':
if 'sort_dir' in kwargs:
reverse = True if kwargs['sort_dir'] == 'desc' else False
collection.actions = sorted(
collection.actions,
key=lambda action: action.next_uuid or '',
reverse=reverse)
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@classmethod
@@ -268,10 +235,7 @@ class ActionsController(rest.RestController):
if audit_uuid:
filters['audit_uuid'] = audit_uuid
if sort_key == 'next_uuid':
sort_db_key = None
else:
sort_db_key = sort_key
sort_db_key = sort_key
actions = objects.Action.list(pecan.request.context,
limit,

View File

@@ -106,7 +106,7 @@ class ActionPlanPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ["audit_id", "state", "first_action_id"]
return ["audit_id", "state"]
class ActionPlan(base.APIBase):
@@ -120,7 +120,6 @@ class ActionPlan(base.APIBase):
_audit_uuid = None
_strategy_uuid = None
_strategy_name = None
_first_action_uuid = None
_efficacy_indicators = None
def _get_audit_uuid(self):
@@ -137,21 +136,6 @@ class ActionPlan(base.APIBase):
except exception.AuditNotFound:
self._audit_uuid = None
def _get_first_action_uuid(self):
return self._first_action_uuid
def _set_first_action_uuid(self, value):
if value == wtypes.Unset:
self._first_action_uuid = wtypes.Unset
elif value and self._first_action_uuid != value:
try:
first_action = objects.Action.get(pecan.request.context,
value)
self._first_action_uuid = first_action.uuid
self.first_action_id = first_action.id
except exception.ActionNotFound:
self._first_action_uuid = None
def _get_efficacy_indicators(self):
if self._efficacy_indicators is None:
self._set_efficacy_indicators(wtypes.Unset)
@@ -220,11 +204,6 @@ class ActionPlan(base.APIBase):
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action plan"""
first_action_uuid = wsme.wsproperty(
types.uuid, _get_first_action_uuid, _set_first_action_uuid,
mandatory=True)
"""The UUID of the first action this action plans links to"""
audit_uuid = wsme.wsproperty(types.uuid, _get_audit_uuid, _set_audit_uuid,
mandatory=True)
"""The UUID of the audit this port belongs to"""
@@ -263,7 +242,6 @@ class ActionPlan(base.APIBase):
setattr(self, field, kwargs.get(field, wtypes.Unset))
self.fields.append('audit_uuid')
self.fields.append('first_action_uuid')
self.fields.append('efficacy_indicators')
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
@@ -271,16 +249,13 @@ class ActionPlan(base.APIBase):
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
fields.append('strategy_name')
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'first_action_uuid',
kwargs.get('first_action_id', wtypes.Unset))
@staticmethod
def _convert_with_links(action_plan, url, expand=True):
if not expand:
action_plan.unset_fields_except(
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name',
'first_action_uuid'])
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name'])
action_plan.links = [
link.Link.make_link(
@@ -305,7 +280,6 @@ class ActionPlan(base.APIBase):
created_at=datetime.datetime.utcnow(),
deleted_at=None,
updated_at=datetime.datetime.utcnow())
sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720'
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
sample._efficacy_indicators = [{'description': 'Test indicator',
'name': 'test_indicator',
@@ -481,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')
@@ -500,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

@@ -69,6 +69,8 @@ class AuditPostType(wtypes.Base):
scope = wtypes.wsattr(types.jsontype, readonly=True)
auto_trigger = wtypes.wsattr(bool, mandatory=False)
def as_audit(self, context):
audit_type_values = [val.value for val in objects.audit.AuditType]
if self.audit_type not in audit_type_values:
@@ -115,7 +117,8 @@ class AuditPostType(wtypes.Base):
goal_id=self.goal,
strategy_id=self.strategy,
interval=self.interval,
scope=self.scope,)
scope=self.scope,
auto_trigger=self.auto_trigger)
class AuditPatchType(types.JsonPatchType):
@@ -257,6 +260,9 @@ class Audit(base.APIBase):
scope = wsme.wsattr(types.jsontype, mandatory=False)
"""Audit Scope"""
auto_trigger = wsme.wsattr(bool, mandatory=False, default=False)
"""Autoexecute action plan once audit is succeeded"""
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.Audit.fields)
@@ -313,7 +319,8 @@ class Audit(base.APIBase):
deleted_at=None,
updated_at=datetime.datetime.utcnow(),
interval=7200,
scope=[])
scope=[],
auto_trigger=False)
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'

View File

@@ -1,5 +1,3 @@
# coding: utf-8
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#

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

@@ -0,0 +1,106 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2017 Servionica
#
# Authors: Alexander Chadin <a.chadin@servionica.ru>
#
# 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_log import log
import six
import voluptuous
from watcher._i18n import _, _LC
from watcher.applier.actions import base
from watcher.common import nova_helper
from watcher.common import utils
LOG = log.getLogger(__name__)
class Resize(base.BaseAction):
"""Resizes a server with specified flavor.
This action will allow you to resize a server to another flavor.
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'flavor': str, # should be either ID or Name of Flavor
})
The `resource_id` is the UUID of the server to resize.
The `flavor` is the ID or Name of Flavor (Nova accepts either ID or Name
of Flavor to resize() function).
"""
# input parameters constants
FLAVOR = 'flavor'
def check_resource_id(self, value):
if (value is not None and
len(value) > 0 and not
utils.is_uuid_like(value)):
raise voluptuous.Invalid(_("The parameter "
"resource_id is invalid."))
@property
def schema(self):
return voluptuous.Schema({
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
voluptuous.Required(self.FLAVOR):
voluptuous.All(voluptuous.Any(*six.string_types),
voluptuous.Length(min=1)),
})
@property
def instance_uuid(self):
return self.resource_id
@property
def flavor(self):
return self.input_parameters.get(self.FLAVOR)
def resize(self):
nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Resize instance %s to %s flavor", self.instance_uuid,
self.flavor)
instance = nova.find_instance(self.instance_uuid)
result = None
if instance:
try:
result = nova.resize_instance(
instance_id=self.instance_uuid, flavor=self.flavor)
except Exception as exc:
LOG.exception(exc)
LOG.critical(
_LC("Unexpected error occurred. Resizing failed for "
"instance %s."), self.instance_uuid)
return result
def execute(self):
return self.resize()
def revert(self):
return self.migrate(destination=self.source_node)
def pre_condition(self):
# TODO(jed): check if the instance exists / check if the instance is on
# the source_node
pass
def post_condition(self):
# TODO(jed): check extra parameters (network response, etc.)
pass

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

@@ -15,10 +15,12 @@
# limitations under the License.
#
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log
from taskflow import engines
from taskflow.patterns import graph_flow as gf
from taskflow import task
from taskflow import task as flow_task
from watcher._i18n import _LE, _LW, _LC
from watcher.applier.workflow_engine import base
@@ -48,6 +50,18 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
# (True to allow v execution or False to not).
return True
@classmethod
def get_config_opts(cls):
return [
cfg.IntOpt(
'max_workers',
default=processutils.get_worker_count(),
min=1,
required=True,
help='Number of workers for taskflow engine '
'to execute actions.')
]
def execute(self, actions):
try:
# NOTE(jed) We want to have a strong separation of concern
@@ -56,34 +70,32 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
# We want to provide the 'taskflow' engine by
# default although we still want to leave the possibility for
# the users to change it.
# todo(jed) we need to change the way the actions are stored.
# The current implementation only use a linked list of actions.
# The current implementation uses graph with linked actions.
# todo(jed) add olso conf for retry and name
flow = gf.Flow("watcher_flow")
previous = None
actions_uuid = {}
for a in actions:
task = TaskFlowActionContainer(a, self)
flow.add(task)
if previous is None:
previous = task
# we have only one Action in the Action Plan
if len(actions) == 1:
nop = TaskFlowNop()
flow.add(nop)
flow.link(previous, nop)
else:
# decider == guard (UML)
flow.link(previous, task, decider=self.decider)
previous = task
actions_uuid[a.uuid] = task
e = engines.load(flow)
for a in actions:
for parent_id in a.parents:
flow.link(actions_uuid[parent_id], actions_uuid[a.uuid],
decider=self.decider)
e = engines.load(
flow, engine='parallel',
max_workers=self.config.max_workers)
e.run()
return flow
except Exception as e:
raise exception.WorkflowExecutionException(error=e)
class TaskFlowActionContainer(task.Task):
class TaskFlowActionContainer(flow_task.Task):
def __init__(self, db_action, engine):
name = "action_type:{0} uuid:{1}".format(db_action.action_type,
db_action.uuid)
@@ -148,7 +160,7 @@ class TaskFlowActionContainer(task.Task):
LOG.critical(_LC("Oops! We need a disaster recover plan."))
class TaskFlowNop(task.Task):
class TaskFlowNop(flow_task.Task):
"""This class is used in case of the workflow have only one Action.
We need at least two atoms to create a link.

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

@@ -15,9 +15,9 @@ from cinderclient import client as ciclient
from glanceclient import client as glclient
from keystoneauth1 import loading as ka_loading
from keystoneclient import client as keyclient
from monascaclient import client as monclient
from neutronclient.neutron import client as netclient
from novaclient import client as nvclient
from oslo_config import cfg
from watcher.common import exception
@@ -41,12 +41,13 @@ class OpenStackClients(object):
self._glance = None
self._cinder = None
self._ceilometer = None
self._monasca = None
self._neutron = None
def _get_keystone_session(self):
auth = ka_loading.load_auth_from_conf_options(cfg.CONF,
auth = ka_loading.load_auth_from_conf_options(CONF,
_CLIENTS_AUTH_GROUP)
sess = ka_loading.load_session_from_conf_options(cfg.CONF,
sess = ka_loading.load_session_from_conf_options(CONF,
_CLIENTS_AUTH_GROUP,
auth=auth)
return sess
@@ -62,7 +63,7 @@ class OpenStackClients(object):
return self._session
def _get_client_option(self, client, option):
return getattr(getattr(cfg.CONF, '%s_client' % client), option)
return getattr(getattr(CONF, '%s_client' % client), option)
@exception.wrap_keystone_exception
def keystone(self):
@@ -112,6 +113,35 @@ class OpenStackClients(object):
session=self.session)
return self._ceilometer
@exception.wrap_keystone_exception
def monasca(self):
if self._monasca:
return self._monasca
monascaclient_version = self._get_client_option(
'monasca', 'api_version')
token = self.session.get_token()
watcher_clients_auth_config = CONF.get(_CLIENTS_AUTH_GROUP)
service_type = 'monitoring'
monasca_kwargs = {
'auth_url': watcher_clients_auth_config.auth_url,
'cert_file': watcher_clients_auth_config.certfile,
'insecure': watcher_clients_auth_config.insecure,
'key_file': watcher_clients_auth_config.keyfile,
'keystone_timeout': watcher_clients_auth_config.timeout,
'os_cacert': watcher_clients_auth_config.cafile,
'service_type': service_type,
'token': token,
'username': watcher_clients_auth_config.username,
'password': watcher_clients_auth_config.password,
}
endpoint = self.session.get_endpoint(service_type=service_type)
self._monasca = monclient.Client(
monascaclient_version, endpoint, **monasca_kwargs)
return self._monasca
@exception.wrap_keystone_exception
def neutron(self):
if self._neutron:

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")
@@ -258,6 +266,12 @@ class ActionPlanReferenced(Invalid):
"multiple actions")
class ActionPlanIsOngoing(Conflict):
msg_fmt = _("Action Plan %(action_plan)s is currently running. "
"New Action Plan %(new_action_plan)s will be set as "
"SUPERSEDED")
class ActionNotFound(ResourceNotFound):
msg_fmt = _("Action %(action)s could not be found")
@@ -276,6 +290,10 @@ class ActionFilterCombinationProhibited(Invalid):
"prohibited")
class UnsupportedActionType(UnsupportedError):
msg_fmt = _("Provided %(action_type) is not supported yet")
class EfficacyIndicatorNotFound(ResourceNotFound):
msg_fmt = _("Efficacy indicator %(efficacy_indicator)s could not be found")
@@ -368,6 +386,11 @@ class NoMetricValuesForInstance(WatcherException):
msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.")
class UnsupportedDataSource(UnsupportedError):
msg_fmt = _("Datasource %(datasource)s is not supported "
"by strategy %(strategy)s")
class NoSuchMetricForHost(WatcherException):
msg_fmt = _("No %(metric)s metric for %(host)s found.")
@@ -387,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

@@ -23,6 +23,7 @@ import time
from oslo_log import log
import cinderclient.exceptions as ciexceptions
import glanceclient.exc as glexceptions
import novaclient.exceptions as nvexceptions
from watcher.common import clients
@@ -63,6 +64,15 @@ class NovaHelper(object):
LOG.exception(exc)
raise exception.ComputeNodeNotFound(name=node_hostname)
def get_instance_list(self):
return self.nova.servers.list(search_opts={'all_tenants': True})
def get_service(self, service_id):
return self.nova.services.find(id=service_id)
def get_flavor(self, flavor_id):
return self.nova.flavors.get(flavor_id)
def get_aggregate_list(self):
return self.nova.aggregates.list()
@@ -142,7 +152,7 @@ class NovaHelper(object):
# We'll use the same name for the new instance.
imagedict = getattr(instance, "image")
image_id = imagedict["id"]
image = self.nova.images.get(image_id)
image = self.glance.images.get(image_id)
new_image_name = getattr(image, "name")
instance_name = getattr(instance, "name")
@@ -304,6 +314,70 @@ class NovaHelper(object):
return True
def resize_instance(self, instance_id, flavor, retry=120):
"""This method resizes given instance with specified flavor.
This method uses the Nova built-in resize()
action to do a resize of a given instance.
It returns True if the resize was successful,
False otherwise.
:param instance_id: the unique id of the instance to resize.
:param flavor: the name or ID of the flavor to resize to.
"""
LOG.debug("Trying a resize of instance %s to flavor '%s'" % (
instance_id, flavor))
# Looking for the instance to resize
instance = self.find_instance(instance_id)
flavor_id = None
try:
flavor_id = self.nova.flavors.get(flavor)
except nvexceptions.NotFound:
flavor_id = [f.id for f in self.nova.flavors.list() if
f.name == flavor][0]
except nvexceptions.ClientException as e:
LOG.debug("Nova client exception occurred while resizing "
"instance %s. Exception: %s", instance_id, e)
if not flavor_id:
LOG.debug("Flavor not found: %s" % flavor)
return False
if not instance:
LOG.debug("Instance not found: %s" % instance_id)
return False
instance_status = getattr(instance, 'OS-EXT-STS:vm_state')
LOG.debug(
"Instance %s is in '%s' status." % (instance_id,
instance_status))
instance.resize(flavor=flavor_id)
while getattr(instance,
'OS-EXT-STS:vm_state') != 'resized' \
and retry:
instance = self.nova.servers.get(instance.id)
LOG.debug(
'Waiting the resize of {0} to {1}'.format(
instance, flavor_id))
time.sleep(1)
retry -= 1
instance_status = getattr(instance, 'status')
if instance_status != 'VERIFY_RESIZE':
return False
instance.confirm_resize()
LOG.debug("Resizing succeeded : instance %s is now on flavor "
"'%s'.", instance_id, flavor_id)
return True
def live_migrate_instance(self, instance_id, dest_hostname,
block_migration=False, retry=120):
"""This method does a live migration of a given instance
@@ -575,8 +649,8 @@ class NovaHelper(object):
return
try:
image = self.nova.images.get(image_id)
except nvexceptions.NotFound:
image = self.glance.images.get(image_id)
except glexceptions.NotFound:
LOG.debug("Image '%s' not found " % image_id)
return
@@ -644,6 +718,16 @@ class NovaHelper(object):
return network_id
def get_instance_by_uuid(self, instance_uuid):
return [instance for instance in
self.nova.servers.list(search_opts={"all_tenants": True,
"uuid": instance_uuid})]
def get_instance_by_name(self, instance_name):
return [instance for instance in
self.nova.servers.list(search_opts={"all_tenants": True,
"name": instance_name})]
def get_instances_by_node(self, host):
return [instance for instance in
self.nova.servers.list(search_opts={"all_tenants": True})

View File

@@ -28,6 +28,7 @@ from watcher.conf import db
from watcher.conf import decision_engine
from watcher.conf import exception
from watcher.conf import glance_client
from watcher.conf import monasca_client
from watcher.conf import neutron_client
from watcher.conf import nova_client
from watcher.conf import paths
@@ -46,6 +47,7 @@ db.register_opts(CONF)
planner.register_opts(CONF)
applier.register_opts(CONF)
decision_engine.register_opts(CONF)
monasca_client.register_opts(CONF)
nova_client.register_opts(CONF)
glance_client.register_opts(CONF)
cinder_client.register_opts(CONF)

View File

@@ -40,7 +40,7 @@ APPLIER_MANAGER_OPTS = [
cfg.StrOpt('workflow_engine',
default='taskflow',
required=True,
help='Select the engine to use to execute the workflow')
help='Select the engine to use to execute the workflow'),
]

View File

@@ -0,0 +1,36 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 Intel Corp
#
# Authors: Prudhvi Rao Shedimbi <prudhvi.rao.shedimbi@intel.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
monasca_client = cfg.OptGroup(name='monasca_client',
title='Configuration Options for Monasca')
MONASCA_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2_0',
help='Version of Monasca API to use in monascaclient.')]
def register_opts(conf):
conf.register_group(monasca_client)
conf.register_opts(MONASCA_CLIENT_OPTS, group=monasca_client)
def list_opts():
return [('monasca_client', MONASCA_CLIENT_OPTS)]

View File

@@ -22,7 +22,7 @@ watcher_planner = cfg.OptGroup(name='watcher_planner',
title='Defines the parameters of '
'the planner')
default_planner = 'default'
default_planner = 'weight'
WATCHER_PLANNER_OPTS = {
cfg.StrOpt('planner',

View File

@@ -165,9 +165,9 @@ class CeilometerHelper(object):
values = []
for index, sample in enumerate(samples):
values.append(
{'sample_%s' % index: {'timestamp': sample._info['timestamp'],
'value': sample._info[
'counter_volume']}})
{'sample_%s' % index: {
'timestamp': sample._info['timestamp'],
'value': sample._info['counter_volume']}})
return values
def get_last_sample_value(self, resource_id, meter_name):

View File

@@ -0,0 +1,124 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 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.
import datetime
from monascaclient import exc
from watcher.common import clients
class MonascaHelper(object):
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
self.osc = osc if osc else clients.OpenStackClients()
self.monasca = self.osc.monasca()
def query_retry(self, f, *args, **kwargs):
try:
return f(*args, **kwargs)
except exc.HTTPUnauthorized:
self.osc.reset_clients()
self.monasca = self.osc.monasca()
return f(*args, **kwargs)
except Exception:
raise
def _format_time_params(self, start_time, end_time, period):
"""Format time-related params to the correct Monasca format
:param start_time: Start datetime from which metrics will be used
:param end_time: End datetime from which metrics will be used
:param period: interval in seconds (int)
:return: start ISO time, end ISO time, period
"""
if not period:
period = int(datetime.timedelta(hours=3).total_seconds())
if not start_time:
start_time = (
datetime.datetime.utcnow() -
datetime.timedelta(seconds=period))
start_timestamp = None if not start_time else start_time.isoformat()
end_timestamp = None if not end_time else end_time.isoformat()
return start_timestamp, end_timestamp, period
def statistics_list(self, meter_name, dimensions, start_time=None,
end_time=None, period=None,):
"""List of statistics."""
start_timestamp, end_timestamp, period = self._format_time_params(
start_time, end_time, period
)
raw_kwargs = dict(
name=meter_name,
start_time=start_timestamp,
end_time=end_timestamp,
dimensions=dimensions,
)
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.monasca.metrics.list_measurements, **kwargs)
return statistics
def statistic_aggregation(self,
meter_name,
dimensions,
start_time=None,
end_time=None,
period=None,
aggregate='avg',
group_by='*'):
"""Representing a statistic aggregate by operators
:param meter_name: meter names of which we want the statistics
:param dimensions: dimensions (dict)
:param start_time: Start datetime from which metrics will be used
:param end_time: End datetime from which metrics will be used
:param period: Sampling `period`: In seconds. If no period is given,
only one aggregate statistic is returned. If given, a
faceted result will be returned, divided into given
periods. Periods with no data are ignored.
:param aggregate: Should be either 'avg', 'count', 'min' or 'max'
:return: A list of dict with each dict being a distinct result row
"""
start_timestamp, end_timestamp, period = self._format_time_params(
start_time, end_time, period
)
raw_kwargs = dict(
name=meter_name,
start_time=start_timestamp,
end_time=end_timestamp,
dimensions=dimensions,
period=period,
statistics=aggregate,
group_by=group_by,
)
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.monasca.metrics.list_statistics, **kwargs)
return statistics

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

@@ -663,6 +663,9 @@ class Connection(api.BaseConnection):
if values.get('state') is None:
values['state'] = objects.audit.State.PENDING
if not values.get('auto_trigger'):
values['auto_trigger'] = False
try:
audit = self._create(models.Audit, values)
except db_exc.DBDuplicateEntry:
@@ -743,6 +746,9 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
if values.get('state') is None:
values['state'] = objects.action.State.PENDING
try:
action = self._create(models.Action, values)
except db_exc.DBDuplicateEntry:

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

@@ -19,6 +19,7 @@ SQLAlchemy models for watcher service
from oslo_db.sqlalchemy import models
from oslo_serialization import jsonutils
import six.moves.urllib.parse as urlparse
from sqlalchemy import Boolean
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy.ext.declarative import declarative_base
@@ -176,6 +177,7 @@ class Audit(Base):
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
scope = Column(JSONEncodedList, nullable=True)
auto_trigger = Column(Boolean, nullable=False)
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
@@ -191,7 +193,6 @@ class ActionPlan(Base):
)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36))
first_action_id = Column(Integer)
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
state = Column(String(20), nullable=True)
@@ -217,7 +218,7 @@ class Action(Base):
action_type = Column(String(255), nullable=False)
input_parameters = Column(JSONEncodedDict, nullable=True)
state = Column(String(20), nullable=True)
next = Column(String(36), nullable=True)
parents = Column(JSONEncodedList, nullable=True)
action_plan = orm.relationship(
ActionPlan, foreign_keys=action_plan_id, lazy=None)

View File

@@ -22,6 +22,8 @@ import six
from oslo_log import log
from watcher.applier import rpcapi
from watcher.common import exception
from watcher.decision_engine.planner import manager as planner_manager
from watcher.decision_engine.strategy.context import default as default_context
from watcher import notifications
@@ -79,11 +81,13 @@ class AuditHandler(BaseAuditHandler):
request_context, audit,
action=fields.NotificationAction.PLANNER,
phase=fields.NotificationPhase.START)
self.planner.schedule(request_context, audit.id, solution)
action_plan = self.planner.schedule(request_context, audit.id,
solution)
notifications.audit.send_action_notification(
request_context, audit,
action=fields.NotificationAction.PLANNER,
phase=fields.NotificationPhase.END)
return action_plan
except Exception:
notifications.audit.send_action_notification(
request_context, audit,
@@ -104,15 +108,30 @@ class AuditHandler(BaseAuditHandler):
self.update_audit_state(audit, objects.audit.State.ONGOING)
def post_execute(self, audit, solution, request_context):
self.do_schedule(request_context, audit, solution)
# change state of the audit to SUCCEEDED
self.update_audit_state(audit, objects.audit.State.SUCCEEDED)
action_plan = self.do_schedule(request_context, audit, solution)
a_plan_filters = {'state': objects.action_plan.State.ONGOING}
ongoing_action_plans = objects.ActionPlan.list(
request_context, filters=a_plan_filters)
if ongoing_action_plans:
action_plan.state = objects.action_plan.State.SUPERSEDED
action_plan.save()
raise exception.ActionPlanIsOngoing(
action_plan=ongoing_action_plans[0].uuid,
new_action_plan=action_plan.uuid)
elif audit.auto_trigger:
applier_client = rpcapi.ApplierAPI()
applier_client.launch_action_plan(request_context,
action_plan.uuid)
def execute(self, audit, request_context):
try:
self.pre_execute(audit, request_context)
solution = self.do_execute(audit, request_context)
self.post_execute(audit, solution, request_context)
except exception.ActionPlanIsOngoing as e:
LOG.exception(e)
if audit.audit_type == objects.audit.AuditType.ONESHOT.value:
self.update_audit_state(audit, objects.audit.State.CANCELLED)
except Exception as e:
LOG.exception(e)
self.update_audit_state(audit, objects.audit.State.FAILED)

View File

@@ -82,9 +82,6 @@ class ContinuousAuditHandler(base.AuditHandler):
if not self._is_audit_inactive(audit):
self.execute(audit, request_context)
def post_execute(self, audit, solution, request_context):
self.do_schedule(request_context, audit, solution)
def launch_audits_periodically(self):
audit_context = context.RequestContext(is_admin=True)
audit_filters = {

View File

@@ -15,6 +15,7 @@
# limitations under the License.
from watcher.decision_engine.audit import base
from watcher import objects
class OneShotAuditHandler(base.AuditHandler):
@@ -24,3 +25,9 @@ class OneShotAuditHandler(base.AuditHandler):
audit, request_context)
return solution
def post_execute(self, audit, solution, request_context):
super(OneShotAuditHandler, self).post_execute(audit, solution,
request_context)
# change state of the audit to SUCCEEDED
self.update_audit_state(audit, objects.audit.State.SUCCEEDED)

View File

@@ -1,80 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@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.
#
"""
The :ref:`Cluster History <cluster_history_definition>` contains all the
previously collected timestamped data such as metrics and events associated
to any :ref:`managed resource <managed_resource_definition>` of the
:ref:`Cluster <cluster_definition>`.
Just like the :ref:`Cluster Data Model <cluster_data_model_definition>`, this
history may be used by any :ref:`Strategy <strategy_definition>` in order to
find the most optimal :ref:`Solution <solution_definition>` during an
:ref:`Audit <audit_definition>`.
In the Watcher project, a generic
:ref:`Cluster History <cluster_history_definition>`
API is proposed with some helper classes in order to :
- share a common measurement (events or metrics) naming based on what is
defined in Ceilometer.
See `the full list of available measurements <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
- share common meter types (Cumulative, Delta, Gauge) based on what is
defined in Ceilometer.
See `the full list of meter types <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
- simplify the development of a new :ref:`Strategy <strategy_definition>`
- avoid duplicating the same code in several
:ref:`Strategies <strategy_definition>`
- have a better consistency between the different
:ref:`Strategies <strategy_definition>`
- avoid any strong coupling with any external metrics/events storage system
(the proposed API and measurement naming system acts as a pivot format)
Note however that a developer can use his/her own history management system if
the Ceilometer system does not fit his/her needs as long as the
:ref:`Strategy <strategy_definition>` is able to produce a
:ref:`Solution <solution_definition>` for the requested
:ref:`Goal <goal_definition>`.
The :ref:`Cluster History <cluster_history_definition>` data may be persisted
in any appropriate storage system (InfluxDB, OpenTSDB, MongoDB,...).
""" # noqa
import abc
import six
"""Work in progress Helper to query metrics"""
@six.add_metaclass(abc.ABCMeta)
class BaseClusterHistory(object):
@abc.abstractmethod
def statistic_aggregation(self, resource_id, meter_name, period,
aggregate='avg'):
raise NotImplementedError()
@abc.abstractmethod
def get_last_sample_values(self, resource_id, meter_name, limit=1):
raise NotImplementedError()
def query_sample(self, meter_name, query, limit=1):
raise NotImplementedError()
def statistic_list(self, meter_name, query=None, period=None):
raise NotImplementedError()

View File

@@ -1,45 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@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 watcher.common import ceilometer_helper
from watcher.decision_engine.cluster.history import base
class CeilometerClusterHistory(base.BaseClusterHistory):
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
super(CeilometerClusterHistory, self).__init__()
self.ceilometer = ceilometer_helper.CeilometerHelper(osc=osc)
def statistic_list(self, meter_name, query=None, period=None):
return self.ceilometer.statistic_list(meter_name, query, period)
def query_sample(self, meter_name, query, limit=1):
return self.ceilometer.query_sample(meter_name, query, limit)
def get_last_sample_values(self, resource_id, meter_name, limit=1):
return self.ceilometer.get_last_sample_values(resource_id, meter_name,
limit)
def statistic_aggregation(self, resource_id, meter_name, period,
aggregate='avg'):
return self.ceilometer.statistic_aggregation(resource_id, meter_name,
period,
aggregate)

View File

@@ -104,6 +104,20 @@ class MigrationEfficacy(IndicatorSpecification):
voluptuous.Range(min=0, max=100), required=True)
class ComputeNodesCount(IndicatorSpecification):
def __init__(self):
super(ComputeNodesCount, self).__init__(
name="compute_nodes_count",
description=_("The total number of enabled compute nodes."),
unit=None,
)
@property
def schema(self):
return voluptuous.Schema(
voluptuous.Range(min=0), required=True)
class ReleasedComputeNodesCount(IndicatorSpecification):
def __init__(self):
super(ReleasedComputeNodesCount, self).__init__(

View File

@@ -33,20 +33,21 @@ class ServerConsolidation(base.EfficacySpecification):
def get_indicators_specifications(self):
return [
indicators.ComputeNodesCount(),
indicators.ReleasedComputeNodesCount(),
indicators.InstanceMigrationsCount(),
]
def get_global_efficacy_indicator(self, indicators_map=None):
value = 0
if indicators_map and indicators_map.instance_migrations_count > 0:
if indicators_map and indicators_map.compute_nodes_count > 0:
value = (float(indicators_map.released_compute_nodes_count) /
float(indicators_map.instance_migrations_count)) * 100
float(indicators_map.compute_nodes_count)) * 100
return efficacy.Indicator(
name="released_nodes_ratio",
description=_("Ratio of released compute nodes divided by the "
"number of VM migrations."),
"total number of enabled compute nodes."),
unit='%',
value=value,
)

View File

@@ -34,3 +34,7 @@ class Model(object):
@abc.abstractmethod
def to_string(self):
raise NotImplementedError()
@abc.abstractmethod
def to_xml(self):
raise NotImplementedError()

View File

@@ -108,12 +108,15 @@ import copy
import threading
from oslo_config import cfg
from oslo_log import log
import six
from watcher.common import clients
from watcher.common.loader import loadable
from watcher.decision_engine.model import model_root
LOG = log.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class BaseClusterDataModelCollector(loadable.LoadableSingleton):
@@ -169,6 +172,8 @@ class BaseClusterDataModelCollector(loadable.LoadableSingleton):
]
def get_latest_cluster_data_model(self):
LOG.debug("Creating copy")
LOG.debug(self.cluster_data_model.to_xml())
return copy.deepcopy(self.cluster_data_model)
def synchronize(self):

View File

@@ -1,23 +1,21 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
# Copyright (c) 2017 Intel Innovation and Research Ireland Ltd.
#
# 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
# 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.
# 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_log import log
from watcher.common import exception
from watcher.common import nova_helper
from watcher.decision_engine.model.collector import base
from watcher.decision_engine.model import element
@@ -30,13 +28,12 @@ LOG = log.getLogger(__name__)
class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"""Nova cluster data model collector
The Nova cluster data model collector creates an in-memory
representation of the resources exposed by the compute service.
The Nova cluster data model collector creates an in-memory
representation of the resources exposed by the compute service.
"""
def __init__(self, config, osc=None):
super(NovaClusterDataModelCollector, self).__init__(config, osc)
self.wrapper = nova_helper.NovaHelper(osc=self.osc)
@property
def notification_endpoints(self):
@@ -62,49 +59,312 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"""Build the compute cluster data model"""
LOG.debug("Building latest Nova cluster data model")
model = model_root.ModelRoot()
mem = element.Resource(element.ResourceType.memory)
num_cores = element.Resource(element.ResourceType.cpu_cores)
disk = element.Resource(element.ResourceType.disk)
disk_capacity = element.Resource(element.ResourceType.disk_capacity)
model.create_resource(mem)
model.create_resource(num_cores)
model.create_resource(disk)
model.create_resource(disk_capacity)
builder = ModelBuilder(self.osc)
return builder.execute()
flavor_cache = {}
nodes = self.wrapper.get_compute_node_list()
for n in nodes:
service = self.wrapper.nova.services.find(id=n.service['id'])
# create node in cluster_model_collector
node = element.ComputeNode(n.id)
node.uuid = service.host
node.hostname = n.hypervisor_hostname
# set capacity
mem.set_capacity(node, n.memory_mb)
disk.set_capacity(node, n.free_disk_gb)
disk_capacity.set_capacity(node, n.local_gb)
num_cores.set_capacity(node, n.vcpus)
node.state = n.state
node.status = n.status
model.add_node(node)
instances = self.wrapper.get_instances_by_node(str(service.host))
for v in instances:
# create VM in cluster_model_collector
instance = element.Instance()
instance.uuid = v.id
# nova/nova/compute/instance_states.py
instance.state = getattr(v, 'OS-EXT-STS:vm_state')
# set capacity
self.wrapper.get_flavor_instance(v, flavor_cache)
mem.set_capacity(instance, v.flavor['ram'])
# FIXME: update all strategies to use disk_capacity
# for instances instead of disk
disk.set_capacity(instance, v.flavor['disk'])
disk_capacity.set_capacity(instance, v.flavor['disk'])
num_cores.set_capacity(instance, v.flavor['vcpus'])
class ModelBuilder(object):
"""Build the graph-based model
model.map_instance(instance, node)
This model builder adds the following data"
return model
- Compute-related knowledge (Nova)
- TODO(v-francoise): Storage-related knowledge (Cinder)
- TODO(v-francoise): Network-related knowledge (Neutron)
NOTE(v-francoise): This model builder is meant to be extended in the future
to also include both storage and network information respectively coming
from Cinder and Neutron. Some prelimary work has been done in this
direction in https://review.openstack.org/#/c/362730 but since we cannot
guarantee a sufficient level of consistency for neither the storage nor the
network part before the end of the Ocata cycle, this work has been
re-scheduled for Pike. In the meantime, all the associated code has been
commented out.
"""
def __init__(self, osc):
self.osc = osc
self.model = model_root.ModelRoot()
self.nova = osc.nova()
self.nova_helper = nova_helper.NovaHelper(osc=self.osc)
# self.neutron = osc.neutron()
# self.cinder = osc.cinder()
def _add_physical_layer(self):
"""Add the physical layer of the graph.
This includes components which represent actual infrastructure
hardware.
"""
for cnode in self.nova_helper.get_compute_node_list():
self.add_compute_node(cnode)
def add_compute_node(self, node):
# Build and add base node.
compute_node = self.build_compute_node(node)
self.model.add_node(compute_node)
# NOTE(v-francoise): we can encapsulate capabilities of the node
# (special instruction sets of CPUs) in the attributes; as well as
# sub-nodes can be added re-presenting e.g. GPUs/Accelerators etc.
# # Build & add disk, memory, network and cpu nodes.
# disk_id, disk_node = self.build_disk_compute_node(base_id, node)
# self.add_node(disk_id, disk_node)
# mem_id, mem_node = self.build_memory_compute_node(base_id, node)
# self.add_node(mem_id, mem_node)
# net_id, net_node = self._build_network_compute_node(base_id)
# self.add_node(net_id, net_node)
# cpu_id, cpu_node = self.build_cpu_compute_node(base_id, node)
# self.add_node(cpu_id, cpu_node)
# # Connect the base compute node to the dependant nodes.
# self.add_edges_from([(base_id, disk_id), (base_id, mem_id),
# (base_id, cpu_id), (base_id, net_id)],
# label="contains")
def build_compute_node(self, node):
"""Build a compute node from a Nova compute node
:param node: A node hypervisor instance
:type node: :py:class:`~novaclient.v2.hypervisors.Hypervisor`
"""
# build up the compute node.
compute_service = self.nova_helper.get_service(node.service["id"])
node_attributes = {
"id": node.id,
"uuid": compute_service.host,
"hostname": node.hypervisor_hostname,
"memory": node.memory_mb,
"disk": node.free_disk_gb,
"disk_capacity": node.local_gb,
"vcpus": node.vcpus,
"state": node.state,
"status": node.status}
compute_node = element.ComputeNode(**node_attributes)
# compute_node = self._build_node("physical", "compute", "hypervisor",
# node_attributes)
return compute_node
# def _build_network_compute_node(self, base_node):
# attributes = {}
# net_node = self._build_node("physical", "network", "NIC", attributes)
# net_id = "{}_network".format(base_node)
# return net_id, net_node
# def build_disk_compute_node(self, base_node, compute):
# # Build disk node attributes.
# disk_attributes = {
# "size_gb": compute.local_gb,
# "used_gb": compute.local_gb_used,
# "available_gb": compute.free_disk_gb}
# disk_node = self._build_node("physical", "storage", "disk",
# disk_attributes)
# disk_id = "{}_disk".format(base_node)
# return disk_id, disk_node
# def build_memory_compute_node(self, base_node, compute):
# # Build memory node attributes.
# memory_attrs = {"size_mb": compute.memory_mb,
# "used_mb": compute.memory_mb_used,
# "available_mb": compute.free_ram_mb}
# memory_node = self._build_node("physical", "memory", "memory",
# memory_attrs)
# memory_id = "{}_memory".format(base_node)
# return memory_id, memory_node
# def build_cpu_compute_node(self, base_node, compute):
# # Build memory node attributes.
# cpu_attributes = {"vcpus": compute.vcpus,
# "vcpus_used": compute.vcpus_used,
# "info": jsonutils.loads(compute.cpu_info)}
# cpu_node = self._build_node("physical", "cpu", "cpu", cpu_attributes)
# cpu_id = "{}_cpu".format(base_node)
# return cpu_id, cpu_node
# @staticmethod
# def _build_node(layer, category, node_type, attributes):
# return {"layer": layer, "category": category, "type": node_type,
# "attributes": attributes}
def _add_virtual_layer(self):
"""Add the virtual layer to the graph.
This layer is the virtual components of the infrastructure,
such as vms.
"""
self._add_virtual_servers()
# self._add_virtual_network()
# self._add_virtual_storage()
def _add_virtual_servers(self):
all_instances = self.nova_helper.get_instance_list()
for inst in all_instances:
# Add Node
instance = self._build_instance_node(inst)
self.model.add_instance(instance)
# Get the cnode_name uuid.
cnode_uuid = getattr(inst, "OS-EXT-SRV-ATTR:host")
if cnode_uuid is None:
# The instance is not attached to any Compute node
continue
try:
# Nova compute node
# cnode = self.nova_helper.get_compute_node_by_hostname(
# cnode_uuid)
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')
except exception.ComputeNodeNotFound:
continue
def _build_instance_node(self, instance):
"""Build an instance node
Create an instance node for the graph using nova and the
`server` nova object.
:param instance: Nova VM object.
:return: A instance node for the graph.
"""
flavor = self.nova_helper.get_flavor(instance.flavor["id"])
instance_attributes = {
"uuid": instance.id,
"human_id": instance.human_id,
"memory": flavor.ram,
"disk": flavor.disk,
"disk_capacity": flavor.disk,
"vcpus": flavor.vcpus,
"state": getattr(instance, "OS-EXT-STS:vm_state")}
# node_attributes = dict()
# node_attributes["layer"] = "virtual"
# node_attributes["category"] = "compute"
# node_attributes["type"] = "compute"
# node_attributes["attributes"] = instance_attributes
return element.Instance(**instance_attributes)
# def _add_virtual_storage(self):
# try:
# volumes = self.cinder.volumes.list()
# except Exception:
# return
# for volume in volumes:
# volume_id, volume_node = self._build_storage_node(volume)
# self.add_node(volume_id, volume_node)
# host = self._get_volume_host_id(volume_node)
# self.add_edge(volume_id, host)
# # Add connections to an instance.
# if volume_node['attributes']['attachments']:
# for attachment in volume_node['attributes']['attachments']:
# self.add_edge(volume_id, attachment['server_id'],
# label='ATTACHED_TO')
# volume_node['attributes'].pop('attachments')
# def _add_virtual_network(self):
# try:
# routers = self.neutron.list_routers()
# except Exception:
# return
# for network in self.neutron.list_networks()['networks']:
# self.add_node(*self._build_network(network))
# for router in routers['routers']:
# self.add_node(*self._build_router(router))
# router_interfaces, _, compute_ports = self._group_ports()
# for router_interface in router_interfaces:
# interface = self._build_router_interface(router_interface)
# router_interface_id = interface[0]
# router_interface_node = interface[1]
# router_id = interface[2]
# self.add_node(router_interface_id, router_interface_node)
# self.add_edge(router_id, router_interface_id)
# network_id = router_interface_node['attributes']['network_id']
# self.add_edge(router_interface_id, network_id)
# for compute_port in compute_ports:
# cp_id, cp_node, instance_id = self._build_compute_port_node(
# compute_port)
# self.add_node(cp_id, cp_node)
# self.add_edge(cp_id, vm_id)
# net_id = cp_node['attributes']['network_id']
# self.add_edge(net_id, cp_id)
# # Connect port to physical node
# phys_net_node = "{}_network".format(cp_node['attributes']
# ['binding:host_id'])
# self.add_edge(cp_id, phys_net_node)
# def _get_volume_host_id(self, volume_node):
# host = volume_node['attributes']['os-vol-host-attr:host']
# if host.find('@') != -1:
# host = host.split('@')[0]
# elif host.find('#') != -1:
# host = host.split('#')[0]
# return "{}_disk".format(host)
# def _build_storage_node(self, volume_obj):
# volume = volume_obj.__dict__
# volume["name"] = volume["id"]
# volume.pop("id")
# volume.pop("manager")
# node = self._build_node("virtual", "storage", 'volume', volume)
# return volume["name"], node
# def _build_compute_port_node(self, compute_port):
# compute_port["name"] = compute_port["id"]
# compute_port.pop("id")
# nde_type = "{}_port".format(
# compute_port["device_owner"].split(":")[0])
# compute_port.pop("device_owner")
# device_id = compute_port["device_id"]
# compute_port.pop("device_id")
# node = self._build_node("virtual", "network", nde_type, compute_port)
# return compute_port["name"], node, device_id
# def _group_ports(self):
# router_interfaces = []
# floating_ips = []
# compute_ports = []
# interface_types = ["network:router_interface",
# 'network:router_gateway']
# for port in self.neutron.list_ports()['ports']:
# if port['device_owner'] in interface_types:
# router_interfaces.append(port)
# elif port['device_owner'].startswith('compute:'):
# compute_ports.append(port)
# elif port['device_owner'] == 'network:floatingip':
# floating_ips.append(port)
# return router_interfaces, floating_ips, compute_ports
# def _build_router_interface(self, interface):
# interface["name"] = interface["id"]
# interface.pop("id")
# node_type = interface["device_owner"].split(":")[1]
# node = self._build_node("virtual", "network", node_type, interface)
# return interface["name"], node, interface["device_id"]
# def _build_router(self, router):
# router_attrs = {"uuid": router['id'],
# "name": router['name'],
# "state": router['status']}
# node = self._build_node('virtual', 'network', 'router', router_attrs)
# return str(router['id']), node
# def _build_network(self, network):
# node = self._build_node('virtual', 'network', 'network', network)
# return network['id'], node
def execute(self):
"""Instantiates the graph with the openstack cluster data.
The graph is populated along 2 layers: virtual and physical. As each
new layer is built connections are made back to previous layers.
"""
self._add_physical_layer()
self._add_virtual_layer()
return self.model

View File

@@ -16,10 +16,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from watcher.decision_engine.model.element import disk_info
from watcher.decision_engine.model.element import instance
from watcher.decision_engine.model.element import node
from watcher.decision_engine.model.element import resource
ServiceState = node.ServiceState
ComputeNode = node.ComputeNode
@@ -27,12 +25,4 @@ ComputeNode = node.ComputeNode
InstanceState = instance.InstanceState
Instance = instance.Instance
DiskInfo = disk_info.DiskInfo
ResourceType = resource.ResourceType
Resource = resource.Resource
__all__ = [
'ServiceState', 'ComputeNode', 'InstanceState', 'Instance',
'DiskInfo', 'ResourceType', 'Resource']
__all__ = ['ServiceState', 'ComputeNode', 'InstanceState', 'Instance']

View File

@@ -17,13 +17,52 @@
# limitations under the License.
import abc
import collections
from lxml import etree
from oslo_log import log
import six
from watcher.objects import base
from watcher.objects import fields as wfields
LOG = log.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class Element(object):
class Element(base.WatcherObject, base.WatcherObjectDictCompat,
base.WatcherComparableObject):
# Initial version
VERSION = '1.0'
fields = {}
def __init__(self, context=None, **kwargs):
for name, field in self.fields.items():
# The idea here is to force the initialization of unspecified
# fields that have a default value
if (name not in kwargs and not field.nullable and
field.default != wfields.UnspecifiedDefault):
kwargs[name] = field.default
super(Element, self).__init__(context, **kwargs)
@abc.abstractmethod
def accept(self, visitor):
raise NotImplementedError()
def as_xml_element(self):
sorted_fieldmap = []
for field in self.fields:
try:
value = str(self[field])
sorted_fieldmap.append((field, value))
except Exception as exc:
LOG.exception(exc)
attrib = collections.OrderedDict(sorted_fieldmap)
element_name = self.__class__.__name__
instance_el = etree.Element(element_name, attrib=attrib)
return instance_el

View File

@@ -19,39 +19,15 @@ import abc
import six
from watcher.decision_engine.model.element import base
from watcher.objects import fields as wfields
@six.add_metaclass(abc.ABCMeta)
class ComputeResource(base.Element):
def __init__(self):
self._uuid = ""
self._human_id = ""
self._hostname = ""
VERSION = '1.0'
@property
def uuid(self):
return self._uuid
@uuid.setter
def uuid(self, u):
self._uuid = u
@property
def hostname(self):
return self._hostname
@hostname.setter
def hostname(self, h):
self._hostname = h
@property
def human_id(self):
return self._human_id
@human_id.setter
def human_id(self, h):
self._human_id = h
def __str__(self):
return "[{0}]".format(self.uuid)
fields = {
"uuid": wfields.StringField(),
"human_id": wfields.StringField(default=""),
}

View File

@@ -1,59 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>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 watcher.decision_engine.model.element import base
class DiskInfo(base.Element):
def __init__(self):
self.name = ""
self.major = 0
self.minor = 0
self.size = 0
self.scheduler = ""
def accept(self, visitor):
raise NotImplementedError()
def set_size(self, size):
"""DiskInfo
:param size: Size in bytes
"""
self.size = size
def get_size(self):
return self.size
def set_scheduler(self, scheduler):
"""DiskInfo
I/O Scheduler noop cfq deadline
:param scheduler:
:return:
"""
self.scheduler = scheduler
def set_device_name(self, name):
"""Device name
:param name:
"""
self.name = name
def get_device_name(self):
return self.name

View File

@@ -17,6 +17,8 @@
import enum
from watcher.decision_engine.model.element import compute_resource
from watcher.objects import base
from watcher.objects import fields as wfields
class InstanceState(enum.Enum):
@@ -36,19 +38,17 @@ class InstanceState(enum.Enum):
ERROR = 'error'
@base.WatcherObjectRegistry.register_if(False)
class Instance(compute_resource.ComputeResource):
def __init__(self):
super(Instance, self).__init__()
self._state = InstanceState.ACTIVE.value
fields = {
"state": wfields.StringField(default=InstanceState.ACTIVE.value),
"memory": wfields.NonNegativeIntegerField(),
"disk": wfields.IntegerField(),
"disk_capacity": wfields.NonNegativeIntegerField(),
"vcpus": wfields.NonNegativeIntegerField(),
}
def accept(self, visitor):
raise NotImplementedError()
@property
def state(self):
return self._state
@state.setter
def state(self, state):
self._state = state

View File

@@ -17,6 +17,8 @@
import enum
from watcher.decision_engine.model.element import compute_resource
from watcher.objects import base
from watcher.objects import fields as wfields
class ServiceState(enum.Enum):
@@ -26,29 +28,20 @@ class ServiceState(enum.Enum):
DISABLED = 'disabled'
@base.WatcherObjectRegistry.register_if(False)
class ComputeNode(compute_resource.ComputeResource):
def __init__(self, id):
super(ComputeNode, self).__init__()
self.id = id
self._state = ServiceState.ONLINE.value
self._status = ServiceState.ENABLED.value
fields = {
"id": wfields.NonNegativeIntegerField(),
"hostname": wfields.StringField(),
"status": wfields.StringField(default=ServiceState.ENABLED.value),
"state": wfields.StringField(default=ServiceState.ONLINE.value),
"memory": wfields.NonNegativeIntegerField(),
"disk": wfields.IntegerField(),
"disk_capacity": wfields.NonNegativeIntegerField(),
"vcpus": wfields.NonNegativeIntegerField(),
}
def accept(self, visitor):
raise NotImplementedError()
@property
def state(self):
return self._state
@state.setter
def state(self, state):
self._state = state
@property
def status(self):
return self._status
@status.setter
def status(self, s):
self._status = s

View File

@@ -1,63 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>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.
import enum
from watcher.common import exception
class ResourceType(enum.Enum):
cpu_cores = 'num_cores'
memory = 'memory'
disk = 'disk'
disk_capacity = 'disk_capacity'
class Resource(object):
def __init__(self, name, capacity=None):
"""Resource
:param name: ResourceType
:param capacity: max
:return:
"""
self._name = name
self.capacity = capacity
self.mapping = {}
@property
def name(self):
return self._name
@name.setter
def name(self, n):
self._name = n
def set_capacity(self, element, value):
self.mapping[element.uuid] = value
def unset_capacity(self, element):
del self.mapping[element.uuid]
def get_capacity_by_uuid(self, uuid):
try:
return self.mapping[str(uuid)]
except KeyError:
raise exception.CapacityNotDefined(
capacity=self.name.value, resource=str(uuid))
def get_capacity(self, element):
return self.get_capacity_by_uuid(element.uuid)

View File

@@ -1,101 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>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_concurrency import lockutils
from oslo_log import log
from watcher._i18n import _LW
LOG = log.getLogger(__name__)
class Mapping(object):
def __init__(self, model):
self.model = model
self.compute_node_mapping = {}
self.instance_mapping = {}
def map(self, node, instance):
"""Select the node where the instance is launched
:param node: the node
:param instance: the virtual machine or instance
"""
with lockutils.lock(__name__):
# init first
if node.uuid not in self.compute_node_mapping.keys():
self.compute_node_mapping[node.uuid] = set()
# map node => instances
self.compute_node_mapping[node.uuid].add(instance.uuid)
# map instance => node
self.instance_mapping[instance.uuid] = node.uuid
def unmap(self, node, instance):
"""Remove the instance from the node
:param node: the node
:param instance: the virtual machine or instance
"""
self.unmap_by_uuid(node.uuid, instance.uuid)
def unmap_by_uuid(self, node_uuid, instance_uuid):
"""Remove the instance (by id) from the node (by id)
:rtype : object
"""
with lockutils.lock(__name__):
if str(node_uuid) in self.compute_node_mapping:
self.compute_node_mapping[str(node_uuid)].remove(
str(instance_uuid))
# remove instance
self.instance_mapping.pop(instance_uuid)
else:
LOG.warning(
_LW("Trying to delete the instance %(instance)s but it "
"was not found on node %(node)s") %
{'instance': instance_uuid, 'node': node_uuid})
def get_mapping(self):
return self.compute_node_mapping
def get_node_from_instance(self, instance):
return self.get_node_by_instance_uuid(instance.uuid)
def get_node_by_instance_uuid(self, instance_uuid):
"""Getting host information from the guest instance
:param instance: the uuid of the instance
:return: node
"""
return self.model.get_node_by_uuid(
self.instance_mapping[str(instance_uuid)])
def get_node_instances(self, node):
"""Get the list of instances running on the node
:param node:
:return:
"""
return self.get_node_instances_by_uuid(node.uuid)
def get_node_instances_by_uuid(self, node_uuid):
if str(node_uuid) in self.compute_node_mapping.keys():
return self.compute_node_mapping[str(node_uuid)]
else:
# empty
return set()

View File

@@ -1,39 +1,40 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
# Copyright (c) 2016 Intel Innovation and Research Ireland Ltd.
#
# 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
# 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.
# 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 collections
"""
Openstack implementation of the cluster graph.
"""
from lxml import etree
import networkx as nx
from oslo_concurrency import lockutils
from oslo_log import log
import six
from watcher._i18n import _
from watcher.common import exception
from watcher.common import utils
from watcher.decision_engine.model import base
from watcher.decision_engine.model import element
from watcher.decision_engine.model import mapping
LOG = log.getLogger(__name__)
class ModelRoot(base.Model):
class ModelRoot(nx.DiGraph, base.Model):
"""Cluster graph for an Openstack cluster."""
def __init__(self, stale=False):
self._nodes = utils.Struct()
self._instances = utils.Struct()
self.mapping = mapping.Mapping(self)
self.resource = utils.Struct()
super(ModelRoot, self).__init__()
self.stale = stale
def __nonzero__(self):
@@ -41,35 +42,47 @@ class ModelRoot(base.Model):
__bool__ = __nonzero__
def assert_node(self, obj):
@staticmethod
def assert_node(obj):
if not isinstance(obj, element.ComputeNode):
raise exception.IllegalArgumentException(
message=_("'obj' argument type is not valid"))
message=_("'obj' argument type is not valid: %s") % type(obj))
def assert_instance(self, obj):
@staticmethod
def assert_instance(obj):
if not isinstance(obj, element.Instance):
raise exception.IllegalArgumentException(
message=_("'obj' argument type is not valid"))
@lockutils.synchronized("model_root")
def add_node(self, node):
self.assert_node(node)
self._nodes[node.uuid] = node
super(ModelRoot, self).add_node(node.uuid, node)
@lockutils.synchronized("model_root")
def remove_node(self, node):
self.assert_node(node)
if str(node.uuid) not in self._nodes:
try:
super(ModelRoot, self).remove_node(node.uuid)
except nx.NetworkXError as exc:
LOG.exception(exc)
raise exception.ComputeNodeNotFound(name=node.uuid)
else:
del self._nodes[node.uuid]
@lockutils.synchronized("model_root")
def add_instance(self, instance):
self.assert_instance(instance)
self._instances[instance.uuid] = instance
try:
super(ModelRoot, self).add_node(instance.uuid, instance)
except nx.NetworkXError as exc:
LOG.exception(exc)
raise exception.InstanceNotFound(name=instance.uuid)
@lockutils.synchronized("model_root")
def remove_instance(self, instance):
self.assert_instance(instance)
del self._instances[instance.uuid]
super(ModelRoot, self).remove_node(instance.uuid)
@lockutils.synchronized("model_root")
def map_instance(self, instance, node):
"""Map a newly created instance to a node
@@ -82,38 +95,25 @@ class ModelRoot(base.Model):
instance = self.get_instance_by_uuid(instance)
if isinstance(node, six.string_types):
node = self.get_node_by_uuid(node)
self.assert_node(node)
self.assert_instance(instance)
self.add_instance(instance)
self.mapping.map(node, instance)
self.add_edge(instance.uuid, node.uuid)
@lockutils.synchronized("model_root")
def unmap_instance(self, instance, node):
"""Unmap an instance from a node
:param instance: :py:class:`~.Instance` object or instance UUID
:type instance: str or :py:class:`~.Instance`
:param node: :py:class:`~.ComputeNode` object or node UUID
:type node: str or :py:class:`~.Instance`
"""
if isinstance(instance, six.string_types):
instance = self.get_instance_by_uuid(instance)
if isinstance(node, six.string_types):
node = self.get_node_by_uuid(node)
self.add_instance(instance)
self.mapping.unmap(node, instance)
self.remove_edge(instance.uuid, node.uuid)
def delete_instance(self, instance, node=None):
if node is not None:
self.mapping.unmap(node, instance)
self.assert_instance(instance)
self.remove_instance(instance)
for resource in self.resource.values():
try:
resource.unset_capacity(instance)
except KeyError:
pass
@lockutils.synchronized("model_root")
def migrate_instance(self, instance, source_node, destination_node):
"""Migrate single instance from source_node to destination_node
@@ -122,96 +122,84 @@ class ModelRoot(base.Model):
:param destination_node:
:return:
"""
self.assert_instance(instance)
self.assert_node(source_node)
self.assert_node(destination_node)
if source_node == destination_node:
return False
# unmap
self.mapping.unmap(source_node, instance)
self.remove_edge(instance.uuid, source_node.uuid)
# map
self.mapping.map(destination_node, instance)
self.add_edge(instance.uuid, destination_node.uuid)
return True
@lockutils.synchronized("model_root")
def get_all_compute_nodes(self):
return self._nodes
return {uuid: cn for uuid, cn in self.nodes(data=True)
if isinstance(cn, element.ComputeNode)}
def get_node_by_uuid(self, node_uuid):
if str(node_uuid) not in self._nodes:
raise exception.ComputeNodeNotFound(name=node_uuid)
return self._nodes[str(node_uuid)]
@lockutils.synchronized("model_root")
def get_node_by_uuid(self, 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):
if str(uuid) not in self._instances:
try:
return self._get_by_uuid(uuid)
except exception.ComputeResourceNotFound:
raise exception.InstanceNotFound(name=uuid)
return self._instances[str(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):
"""Getting host information from the guest instance
:param instance_uuid: the uuid of the instance
:return: node
"""
if str(instance_uuid) not in self.mapping.instance_mapping:
raise exception.InstanceNotFound(name=instance_uuid)
return self.get_node_by_uuid(
self.mapping.instance_mapping[str(instance_uuid)])
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 self._instances
def get_mapping(self):
return self.mapping
def create_resource(self, r):
self.resource[str(r.name)] = r
def get_resource_by_uuid(self, resource_id):
return self.resource[str(resource_id)]
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):
return self.mapping.get_node_instances(node)
self.assert_node(node)
node_instances = []
for instance_uuid in self.predecessors(node.uuid):
instance = self._get_by_uuid(instance_uuid)
if isinstance(instance, element.Instance):
node_instances.append(instance)
def _build_compute_node_element(self, compute_node):
attrib = collections.OrderedDict(
id=six.text_type(compute_node.id), uuid=compute_node.uuid,
human_id=compute_node.human_id, hostname=compute_node.hostname,
state=compute_node.state, status=compute_node.status)
for resource_name, resource in sorted(
self.resource.items(), key=lambda x: x[0]):
res_value = resource.get_capacity(compute_node)
if res_value is not None:
attrib[resource_name] = six.text_type(res_value)
compute_node_el = etree.Element("ComputeNode", attrib=attrib)
return compute_node_el
def _build_instance_element(self, instance):
attrib = collections.OrderedDict(
uuid=instance.uuid, human_id=instance.human_id,
hostname=instance.hostname, state=instance.state)
for resource_name, resource in sorted(
self.resource.items(), key=lambda x: x[0]):
res_value = resource.get_capacity(instance)
if res_value is not None:
attrib[resource_name] = six.text_type(res_value)
instance_el = etree.Element("Instance", attrib=attrib)
return instance_el
return node_instances
def to_string(self):
return self.to_xml()
def to_xml(self):
root = etree.Element("ModelRoot")
# Build compute node tree
for cn in sorted(self.get_all_compute_nodes().values(),
key=lambda cn: cn.uuid):
compute_node_el = self._build_compute_node_element(cn)
compute_node_el = cn.as_xml_element()
# Build mapped instance tree
node_instance_uuids = self.get_node_instances(cn)
for instance_uuid in sorted(node_instance_uuids):
instance = self.get_instance_by_uuid(instance_uuid)
instance_el = self._build_instance_element(instance)
node_instances = self.get_node_instances(cn)
for instance in sorted(node_instances, key=lambda x: x.uuid):
instance_el = instance.as_xml_element()
compute_node_el.append(instance_el)
root.append(compute_node_el)
@@ -221,51 +209,23 @@ class ModelRoot(base.Model):
key=lambda inst: inst.uuid):
try:
self.get_node_by_instance_uuid(instance.uuid)
except exception.InstanceNotFound:
root.append(self._build_instance_element(instance))
except (exception.InstanceNotFound, exception.ComputeNodeNotFound):
root.append(instance.as_xml_element())
return etree.tostring(root, pretty_print=True).decode('utf-8')
@classmethod
def from_xml(cls, data):
model = cls()
root = etree.fromstring(data)
mem = element.Resource(element.ResourceType.memory)
num_cores = element.Resource(element.ResourceType.cpu_cores)
disk = element.Resource(element.ResourceType.disk)
disk_capacity = element.Resource(element.ResourceType.disk_capacity)
model.create_resource(mem)
model.create_resource(num_cores)
model.create_resource(disk)
model.create_resource(disk_capacity)
for cn in root.findall('.//ComputeNode'):
node = element.ComputeNode(cn.get('id'))
node.uuid = cn.get('uuid')
node.hostname = cn.get('hostname')
# set capacity
mem.set_capacity(node, int(cn.get(str(mem.name))))
disk.set_capacity(node, int(cn.get(str(disk.name))))
disk_capacity.set_capacity(
node, int(cn.get(str(disk_capacity.name))))
num_cores.set_capacity(node, int(cn.get(str(num_cores.name))))
node.state = cn.get('state')
node.status = cn.get('status')
node = element.ComputeNode(**cn.attrib)
model.add_node(node)
for inst in root.findall('.//Instance'):
instance = element.Instance()
instance.uuid = inst.get('uuid')
instance.state = inst.get('state')
mem.set_capacity(instance, int(inst.get(str(mem.name))))
disk.set_capacity(instance, int(inst.get(str(disk.name))))
disk_capacity.set_capacity(
instance, int(inst.get(str(disk_capacity.name))))
num_cores.set_capacity(
instance, int(inst.get(str(num_cores.name))))
instance = element.Instance(**inst.attrib)
model.add_instance(instance)
parent = inst.getparent()
if parent.tag == 'ComputeNode':

Some files were not shown because too many files have changed in this diff Show More