Compare commits

...

75 Commits

Author SHA1 Message Date
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
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
155 changed files with 4968 additions and 2491 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"

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

@@ -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,22 +245,30 @@ 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

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 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',

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

@@ -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

@@ -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

@@ -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

@@ -258,6 +258,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 +282,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 +378,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.")

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

@@ -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

@@ -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,51 @@
# 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):
# 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)
@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)
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)
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)
@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, node)
@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, node)
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,80 @@ 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, source_node)
# map
self.mapping.map(destination_node, instance)
self.add_edge(instance, destination_node)
return True
@lockutils.synchronized("model_root")
def get_all_compute_nodes(self):
return self._nodes
return {cn.uuid: cn for cn in self.nodes()
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):
for graph_node in self.nodes():
if (isinstance(graph_node, element.ComputeNode) and
graph_node.uuid == uuid):
return graph_node
raise exception.ComputeNodeNotFound(name=uuid)
@lockutils.synchronized("model_root")
def get_instance_by_uuid(self, uuid):
if str(uuid) not in self._instances:
raise exception.InstanceNotFound(name=uuid)
return self._instances[str(uuid)]
return self._get_instance_by_uuid(uuid)
def _get_instance_by_uuid(self, uuid):
for graph_node in self.nodes():
if (isinstance(graph_node, element.Instance) and
graph_node.uuid == str(uuid)):
return graph_node
raise exception.InstanceNotFound(name=uuid)
@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_instance_by_uuid(instance_uuid)
for node in self.neighbors(instance):
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 {inst.uuid: inst for inst in self.nodes()
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 neighbor in self.predecessors(node):
if isinstance(neighbor, element.Instance):
node_instances.append(neighbor)
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 +205,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':

View File

@@ -18,7 +18,7 @@
from oslo_log import log
from watcher._i18n import _LI
from watcher._i18n import _LI, _LW
from watcher.common import exception
from watcher.common import nova_helper
from watcher.decision_engine.model import element
@@ -40,14 +40,21 @@ class NovaNotification(base.NotificationEndpoint):
self._nova = nova_helper.NovaHelper()
return self._nova
def get_or_create_instance(self, uuid):
def get_or_create_instance(self, instance_uuid, node_uuid=None):
try:
instance = self.cluster_data_model.get_instance_by_uuid(uuid)
if node_uuid:
self.get_or_create_node(node_uuid)
except exception.ComputeNodeNotFound:
LOG.warning(_LW("Could not find compute node %(node)s for "
"instance %(instance)s"),
dict(node=node_uuid, instance=instance_uuid))
try:
instance = self.cluster_data_model.get_instance_by_uuid(
instance_uuid)
except exception.InstanceNotFound:
# The instance didn't exist yet so we create a new instance object
LOG.debug("New instance created: %s", uuid)
instance = element.Instance()
instance.uuid = uuid
LOG.debug("New instance created: %s", instance_uuid)
instance = element.Instance(uuid=instance_uuid)
self.cluster_data_model.add_instance(instance)
@@ -57,21 +64,19 @@ class NovaNotification(base.NotificationEndpoint):
instance_data = data['nova_object.data']
instance_flavor_data = instance_data['flavor']['nova_object.data']
instance.state = instance_data['state']
instance.hostname = instance_data['host_name']
instance.human_id = instance_data['display_name']
memory_mb = instance_flavor_data['memory_mb']
num_cores = instance_flavor_data['vcpus']
disk_gb = instance_flavor_data['root_gb']
self.update_capacity(element.ResourceType.memory, instance, memory_mb)
self.update_capacity(
element.ResourceType.cpu_cores, instance, num_cores)
self.update_capacity(
element.ResourceType.disk, instance, disk_gb)
self.update_capacity(
element.ResourceType.disk_capacity, instance, disk_gb)
instance.update({
'state': instance_data['state'],
'hostname': instance_data['host_name'],
'human_id': instance_data['display_name'],
'memory': memory_mb,
'vcpus': num_cores,
'disk': disk_gb,
'disk_capacity': disk_gb,
})
try:
node = self.get_or_create_node(instance_data['host'])
@@ -82,26 +87,20 @@ class NovaNotification(base.NotificationEndpoint):
self.update_instance_mapping(instance, node)
def update_capacity(self, resource_id, obj, value):
resource = self.cluster_data_model.get_resource_by_uuid(resource_id)
resource.set_capacity(obj, value)
def legacy_update_instance(self, instance, data):
instance.state = data['state']
instance.hostname = data['hostname']
instance.human_id = data['display_name']
memory_mb = data['memory_mb']
num_cores = data['vcpus']
disk_gb = data['root_gb']
self.update_capacity(element.ResourceType.memory, instance, memory_mb)
self.update_capacity(
element.ResourceType.cpu_cores, instance, num_cores)
self.update_capacity(
element.ResourceType.disk, instance, disk_gb)
self.update_capacity(
element.ResourceType.disk_capacity, instance, disk_gb)
instance.update({
'state': data['state'],
'hostname': data['hostname'],
'human_id': data['display_name'],
'memory': memory_mb,
'vcpus': num_cores,
'disk': disk_gb,
'disk_capacity': disk_gb,
})
try:
node = self.get_or_create_node(data['host'])
@@ -115,32 +114,34 @@ class NovaNotification(base.NotificationEndpoint):
def update_compute_node(self, node, data):
"""Update the compute node using the notification data."""
node_data = data['nova_object.data']
node.hostname = node_data['host']
node.state = (
node_state = (
element.ServiceState.OFFLINE.value
if node_data['forced_down'] else element.ServiceState.ONLINE.value)
node.status = (
node_status = (
element.ServiceState.DISABLED.value
if node_data['host'] else element.ServiceState.ENABLED.value)
if node_data['disabled'] else element.ServiceState.ENABLED.value)
node.update({
'hostname': node_data['host'],
'state': node_state,
'status': node_status,
})
def create_compute_node(self, node_hostname):
"""Update the compute node by querying the Nova API."""
try:
_node = self.nova.get_compute_node_by_hostname(node_hostname)
node = element.ComputeNode(_node.id)
node.uuid = node_hostname
node.hostname = _node.hypervisor_hostname
node.state = _node.state
node.status = _node.status
self.update_capacity(
element.ResourceType.memory, node, _node.memory_mb)
self.update_capacity(
element.ResourceType.cpu_cores, node, _node.vcpus)
self.update_capacity(
element.ResourceType.disk, node, _node.free_disk_gb)
self.update_capacity(
element.ResourceType.disk_capacity, node, _node.local_gb)
node = element.ComputeNode(
id=_node.id,
uuid=node_hostname,
hostname=_node.hypervisor_hostname,
state=_node.state,
status=_node.status,
memory=_node.memory_mb,
vcpus=_node.vcpus,
disk=_node.free_disk_gb,
disk_capacity=_node.local_gb,
)
return node
except Exception as exc:
LOG.exception(exc)
@@ -160,6 +161,7 @@ class NovaNotification(base.NotificationEndpoint):
node = self.create_compute_node(uuid)
LOG.debug("New compute node created: %s", uuid)
self.cluster_data_model.add_node(node)
LOG.debug("New compute node mapped: %s", uuid)
return node
def update_instance_mapping(self, instance, node):
@@ -170,18 +172,20 @@ class NovaNotification(base.NotificationEndpoint):
return
try:
try:
old_node = self.get_or_create_node(node.uuid)
current_node = (
self.cluster_data_model.get_node_by_instance_uuid(
instance.uuid) or self.get_or_create_node(node.uuid))
except exception.ComputeNodeNotFound as exc:
LOG.exception(exc)
# If we can't create the node,
# we consider the instance as unmapped
old_node = None
current_node = None
LOG.debug("Mapped node %s found", node.uuid)
if node and node != old_node:
if current_node and node != current_node:
LOG.debug("Unmapping instance %s from %s",
instance.uuid, node.uuid)
self.cluster_data_model.unmap_instance(instance, old_node)
self.cluster_data_model.unmap_instance(instance, current_node)
except exception.InstanceNotFound:
# The instance didn't exist yet so we map it for the first time
LOG.debug("New instance: mapping it to %s", node.uuid)
@@ -221,6 +225,7 @@ class ServiceUpdated(VersionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
node_data = payload['nova_object.data']
node_uuid = node_data['host']
try:
@@ -262,10 +267,12 @@ class InstanceCreated(VersionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_data = payload['nova_object.data']
instance_uuid = instance_data['uuid']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = instance_data.get('host')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
self.update_instance(instance, payload)
@@ -294,9 +301,11 @@ class InstanceUpdated(VersionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_data = payload['nova_object.data']
instance_uuid = instance_data['uuid']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = instance_data.get('host')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
self.update_instance(instance, payload)
@@ -317,10 +326,12 @@ class InstanceDeletedEnd(VersionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_data = payload['nova_object.data']
instance_uuid = instance_data['uuid']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = instance_data.get('host')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
try:
node = self.get_or_create_node(instance_data['host'])
@@ -348,9 +359,11 @@ class LegacyInstanceUpdated(UnversionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_uuid = payload['instance_id']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = payload.get('node')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
self.legacy_update_instance(instance, payload)
@@ -371,9 +384,11 @@ class LegacyInstanceCreatedEnd(UnversionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_uuid = payload['instance_id']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = payload.get('node')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
self.legacy_update_instance(instance, payload)
@@ -394,8 +409,10 @@ class LegacyInstanceDeletedEnd(UnversionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_uuid = payload['instance_id']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = payload.get('node')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
try:
node = self.get_or_create_node(payload['host'])
@@ -423,8 +440,10 @@ class LegacyLiveMigratedEnd(UnversionnedNotificationEndpoint):
dict(event=event_type,
publisher=publisher_id,
metadata=metadata))
LOG.debug(payload)
instance_uuid = payload['instance_id']
instance = self.get_or_create_instance(instance_uuid)
node_uuid = payload.get('node')
instance = self.get_or_create_instance(instance_uuid, node_uuid)
self.legacy_update_instance(instance, payload)

View File

@@ -1,169 +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 oslo_config import cfg
from oslo_log import log
from watcher._i18n import _LW
from watcher.common import utils
from watcher.decision_engine.planner import base
from watcher import objects
LOG = log.getLogger(__name__)
class DefaultPlanner(base.BasePlanner):
"""Default planner implementation
This implementation comes with basic rules with a set of action types that
are weighted. An action having a lower weight will be scheduled before the
other ones. The set of action types can be specified by 'weights' in the
``watcher.conf``. You need to associate a different weight to all available
actions into the configuration file, otherwise you will get an error when
the new action will be referenced in the solution produced by a strategy.
"""
weights_dict = {
'nop': 0,
'sleep': 1,
'change_nova_service_state': 2,
'migrate': 3,
}
@classmethod
def get_config_opts(cls):
return [
cfg.DictOpt(
'weights',
help="These weights are used to schedule the actions",
default=cls.weights_dict),
]
def create_action(self,
action_plan_id,
action_type,
input_parameters=None):
uuid = utils.generate_uuid()
action = {
'uuid': uuid,
'action_plan_id': int(action_plan_id),
'action_type': action_type,
'input_parameters': input_parameters,
'state': objects.action.State.PENDING,
'next': None,
}
return action
def schedule(self, context, audit_id, solution):
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
priorities = self.config.weights
action_plan = self._create_action_plan(context, audit_id, solution)
actions = list(solution.actions)
to_schedule = []
for action in actions:
json_action = self.create_action(
action_plan_id=action_plan.id,
action_type=action.get('action_type'),
input_parameters=action.get('input_parameters'))
to_schedule.append((priorities[action.get('action_type')],
json_action))
self._create_efficacy_indicators(
context, action_plan.id, solution.efficacy_indicators)
# scheduling
scheduled = sorted(to_schedule, key=lambda x: (x[0]))
if len(scheduled) == 0:
LOG.warning(_LW("The action plan is empty"))
action_plan.first_action_id = None
action_plan.state = objects.action_plan.State.SUCCEEDED
action_plan.save()
else:
# create the first action
parent_action = self._create_action(context,
scheduled[0][1],
None)
# remove first
scheduled.pop(0)
action_plan.first_action_id = parent_action.id
action_plan.save()
for s_action in scheduled:
current_action = self._create_action(context, s_action[1],
parent_action)
parent_action = current_action
return action_plan
def _create_action_plan(self, context, audit_id, solution):
strategy = objects.Strategy.get_by_name(
context, solution.strategy.name)
action_plan_dict = {
'uuid': utils.generate_uuid(),
'audit_id': audit_id,
'strategy_id': strategy.id,
'first_action_id': None,
'state': objects.action_plan.State.RECOMMENDED,
'global_efficacy': solution.global_efficacy,
}
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
new_action_plan.create()
return new_action_plan
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
efficacy_indicators = []
for indicator in indicators:
efficacy_indicator_dict = {
'uuid': utils.generate_uuid(),
'name': indicator.name,
'description': indicator.description,
'unit': indicator.unit,
'value': indicator.value,
'action_plan_id': action_plan_id,
}
new_efficacy_indicator = objects.EfficacyIndicator(
context, **efficacy_indicator_dict)
new_efficacy_indicator.create()
efficacy_indicators.append(new_efficacy_indicator)
return efficacy_indicators
def _create_action(self, context, _action, parent_action):
try:
LOG.debug("Creating the %s in the Watcher database",
_action.get("action_type"))
new_action = objects.Action(context, **_action)
new_action.create()
new_action.save()
if parent_action:
parent_action.next = new_action.id
parent_action.save()
return new_action
except Exception as exc:
LOG.exception(exc)
raise

View File

@@ -0,0 +1,233 @@
# -*- encoding: utf-8 -*-
#
# Authors: Vincent Francoise <Vincent.FRANCOISE@b-com.com>
# 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.
import collections
import networkx as nx
from oslo_config import cfg
from oslo_config import types
from oslo_log import log
from watcher._i18n import _LW
from watcher.common import utils
from watcher.decision_engine.planner import base
from watcher import objects
LOG = log.getLogger(__name__)
class WeightPlanner(base.BasePlanner):
"""Weight planner implementation
This implementation builds actions with parents in accordance with weights.
Set of actions having a lower weight will be scheduled before
the other ones. There are two config options to configure:
action_weights and parallelization.
*Limitations*
- This planner requires to have action_weights and parallelization configs
tuned well.
"""
def __init__(self, config):
super(WeightPlanner, self).__init__(config)
action_weights = {
'turn_host_to_acpi_s3_state': 10,
'resize': 20,
'migrate': 30,
'sleep': 40,
'change_nova_service_state': 50,
'nop': 60,
}
parallelization = {
'turn_host_to_acpi_s3_state': 2,
'resize': 2,
'migrate': 2,
'sleep': 1,
'change_nova_service_state': 1,
'nop': 1,
}
@classmethod
def get_config_opts(cls):
return [
cfg.Opt(
'weights',
type=types.Dict(value_type=types.Integer()),
help="These weights are used to schedule the actions. "
"Action Plan will be build in accordance with sets of "
"actions ordered by descending weights."
"Two action types cannot have the same weight. ",
default=cls.action_weights),
cfg.Opt(
'parallelization',
type=types.Dict(value_type=types.Integer()),
help="Number of actions to be run in parallel on a per "
"action type basis.",
default=cls.parallelization),
]
@staticmethod
def format_action(action_plan_id, action_type,
input_parameters=None, parents=()):
return {
'uuid': utils.generate_uuid(),
'action_plan_id': int(action_plan_id),
'action_type': action_type,
'input_parameters': input_parameters,
'state': objects.action.State.PENDING,
'parents': parents or None,
}
@staticmethod
def chunkify(lst, n):
"""Yield successive n-sized chunks from lst."""
if n < 1:
# Just to make sure the number is valid
n = 1
# Split a flat list in a list of chunks of size n.
# e.g. chunkify([0, 1, 2, 3, 4], 2) -> [[0, 1], [2, 3], [4]]
for i in range(0, len(lst), n):
yield lst[i:i + n]
def compute_action_graph(self, sorted_weighted_actions):
reverse_weights = {v: k for k, v in self.config.weights.items()}
# leaf_groups contains a list of list of nodes called groups
# each group is a set of nodes from which a future node will
# branch off (parent nodes).
# START --> migrate-1 --> migrate-3
# \ \--> resize-1 --> FINISH
# \--> migrate-2 -------------/
# In the above case migrate-1 will the only memeber of the leaf
# group that migrate-3 will use as parent group, whereas
# resize-1 will have both migrate-2 and migrate-3 in its
# parent/leaf group
leaf_groups = []
action_graph = nx.DiGraph()
# We iterate through each action type category (sorted by weight) to
# insert them in a Directed Acyclic Graph
for idx, (weight, actions) in enumerate(sorted_weighted_actions):
action_chunks = self.chunkify(
actions, self.config.parallelization[reverse_weights[weight]])
# We split the actions into chunks/layers that will have to be
# spread across all the available branches of the graph
for chunk_idx, actions_chunk in enumerate(action_chunks):
for action in actions_chunk:
action_graph.add_node(action)
# all other actions
parent_nodes = []
if not idx and not chunk_idx:
parent_nodes = []
elif leaf_groups:
parent_nodes = leaf_groups
for parent_node in parent_nodes:
action_graph.add_edge(parent_node, action)
action.parents.append(parent_node.uuid)
if leaf_groups:
leaf_groups = []
leaf_groups.extend([a for a in actions_chunk])
return action_graph
def schedule(self, context, audit_id, solution):
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
action_plan = self.create_action_plan(context, audit_id, solution)
sorted_weighted_actions = self.get_sorted_actions_by_weight(
context, action_plan, solution)
action_graph = self.compute_action_graph(sorted_weighted_actions)
self._create_efficacy_indicators(
context, action_plan.id, solution.efficacy_indicators)
if len(action_graph.nodes()) == 0:
LOG.warning(_LW("The action plan is empty"))
action_plan.state = objects.action_plan.State.SUCCEEDED
action_plan.save()
self.create_scheduled_actions(action_plan, action_graph)
return action_plan
def get_sorted_actions_by_weight(self, context, action_plan, solution):
# We need to make them immutable to add them to the graph
action_objects = list([
objects.Action(
context, uuid=utils.generate_uuid(), parents=[],
action_plan_id=action_plan.id, **a)
for a in solution.actions])
# This is a dict of list with each being a weight and the list being
# all the actions associated to this weight
weighted_actions = collections.defaultdict(list)
for action in action_objects:
action_weight = self.config.weights[action.action_type]
weighted_actions[action_weight].append(action)
return reversed(sorted(weighted_actions.items(), key=lambda x: x[0]))
def create_scheduled_actions(self, action_plan, graph):
for action in graph.nodes():
LOG.debug("Creating the %s in the Watcher database",
action.action_type)
try:
action.create()
except Exception as exc:
LOG.exception(exc)
raise
def create_action_plan(self, context, audit_id, solution):
strategy = objects.Strategy.get_by_name(
context, solution.strategy.name)
action_plan_dict = {
'uuid': utils.generate_uuid(),
'audit_id': audit_id,
'strategy_id': strategy.id,
'state': objects.action_plan.State.RECOMMENDED,
'global_efficacy': solution.global_efficacy,
}
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
new_action_plan.create()
return new_action_plan
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
efficacy_indicators = []
for indicator in indicators:
efficacy_indicator_dict = {
'uuid': utils.generate_uuid(),
'name': indicator.name,
'description': indicator.description,
'unit': indicator.unit,
'value': indicator.value,
'action_plan_id': action_plan_id,
}
new_efficacy_indicator = objects.EfficacyIndicator(
context, **efficacy_indicator_dict)
new_efficacy_indicator.create()
efficacy_indicators.append(new_efficacy_indicator)
return efficacy_indicators

View File

@@ -0,0 +1,301 @@
# -*- encoding: 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
#
# 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 abc
from oslo_config import cfg
from oslo_config import types
from oslo_log import log
from watcher._i18n import _LW
from watcher.common import clients
from watcher.common import exception
from watcher.common import nova_helper
from watcher.common import utils
from watcher.decision_engine.planner import base
from watcher import objects
LOG = log.getLogger(__name__)
class WorkloadStabilizationPlanner(base.BasePlanner):
"""Workload Stabilization planner implementation
This implementation comes with basic rules with a set of action types that
are weighted. An action having a lower weight will be scheduled before the
other ones. The set of action types can be specified by 'weights' in the
``watcher.conf``. You need to associate a different weight to all available
actions into the configuration file, otherwise you will get an error when
the new action will be referenced in the solution produced by a strategy.
*Limitations*
- This is a proof of concept that is not meant to be used in production
"""
def __init__(self, config):
super(WorkloadStabilizationPlanner, self).__init__(config)
self._osc = clients.OpenStackClients()
@property
def osc(self):
return self._osc
weights_dict = {
'turn_host_to_acpi_s3_state': 0,
'resize': 1,
'migrate': 2,
'sleep': 3,
'change_nova_service_state': 4,
'nop': 5,
}
@classmethod
def get_config_opts(cls):
return [
cfg.Opt(
'weights',
type=types.Dict(value_type=types.Integer()),
help="These weights are used to schedule the actions",
default=cls.weights_dict),
]
def create_action(self,
action_plan_id,
action_type,
input_parameters=None):
uuid = utils.generate_uuid()
action = {
'uuid': uuid,
'action_plan_id': int(action_plan_id),
'action_type': action_type,
'input_parameters': input_parameters,
'state': objects.action.State.PENDING,
'parents': None
}
return action
def load_child_class(self, child_name):
for c in BaseActionValidator.__subclasses__():
if child_name == c.action_name:
return c()
return None
def schedule(self, context, audit_id, solution):
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
weights = self.config.weights
action_plan = self._create_action_plan(context, audit_id, solution)
actions = list(solution.actions)
to_schedule = []
for action in actions:
json_action = self.create_action(
action_plan_id=action_plan.id,
action_type=action.get('action_type'),
input_parameters=action.get('input_parameters'))
to_schedule.append((weights[action.get('action_type')],
json_action))
self._create_efficacy_indicators(
context, action_plan.id, solution.efficacy_indicators)
# scheduling
scheduled = sorted(to_schedule, key=lambda weight: (weight[0]),
reverse=True)
if len(scheduled) == 0:
LOG.warning(_LW("The action plan is empty"))
action_plan.state = objects.action_plan.State.SUCCEEDED
action_plan.save()
else:
resource_action_map = {}
scheduled_actions = [x[1] for x in scheduled]
for action in scheduled_actions:
a_type = action['action_type']
if a_type != 'turn_host_to_acpi_s3_state':
plugin_action = self.load_child_class(
action.get("action_type"))
if not plugin_action:
raise exception.UnsupportedActionType(
action_type=action.get("action_type"))
db_action = self._create_action(context, action)
parents = plugin_action.validate_parents(
resource_action_map, action)
if parents:
db_action.parents = parents
db_action.save()
# if we have an action that will make host unreachable, we need
# to complete all actions (resize and migration type)
# related to the host.
# Note(alexchadin): turn_host_to_acpi_s3_state doesn't
# actually exist. Placed code shows relations between
# action types.
# TODO(alexchadin): add turn_host_to_acpi_s3_state action type.
else:
host_to_acpi_s3 = action['input_parameters']['resource_id']
host_actions = resource_action_map.get(host_to_acpi_s3)
action_parents = []
if host_actions:
resize_actions = [x[0] for x in host_actions
if x[1] == 'resize']
migrate_actions = [x[0] for x in host_actions
if x[1] == 'migrate']
resize_migration_parents = [
x.parents for x in
[objects.Action.get_by_uuid(context, resize_action)
for resize_action in resize_actions]]
# resize_migration_parents should be one level list
resize_migration_parents = [
parent for sublist in resize_migration_parents
for parent in sublist]
action_parents.extend([uuid for uuid in
resize_actions])
action_parents.extend([uuid for uuid in
migrate_actions if uuid not in
resize_migration_parents])
db_action = self._create_action(context, action)
db_action.parents = action_parents
db_action.save()
return action_plan
def _create_action_plan(self, context, audit_id, solution):
strategy = objects.Strategy.get_by_name(
context, solution.strategy.name)
action_plan_dict = {
'uuid': utils.generate_uuid(),
'audit_id': audit_id,
'strategy_id': strategy.id,
'state': objects.action_plan.State.RECOMMENDED,
'global_efficacy': solution.global_efficacy,
}
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
new_action_plan.create()
return new_action_plan
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
efficacy_indicators = []
for indicator in indicators:
efficacy_indicator_dict = {
'uuid': utils.generate_uuid(),
'name': indicator.name,
'description': indicator.description,
'unit': indicator.unit,
'value': indicator.value,
'action_plan_id': action_plan_id,
}
new_efficacy_indicator = objects.EfficacyIndicator(
context, **efficacy_indicator_dict)
new_efficacy_indicator.create()
efficacy_indicators.append(new_efficacy_indicator)
return efficacy_indicators
def _create_action(self, context, _action):
try:
LOG.debug("Creating the %s in the Watcher database",
_action.get("action_type"))
new_action = objects.Action(context, **_action)
new_action.create()
return new_action
except Exception as exc:
LOG.exception(exc)
raise
class BaseActionValidator(object):
action_name = None
def __init__(self):
super(BaseActionValidator, self).__init__()
self._osc = None
@property
def osc(self):
if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@abc.abstractmethod
def validate_parents(self, resource_action_map, action):
raise NotImplementedError()
def _mapping(self, resource_action_map, resource_id, action_uuid,
action_type):
if resource_id not in resource_action_map:
resource_action_map[resource_id] = [(action_uuid,
action_type,)]
else:
resource_action_map[resource_id].append((action_uuid,
action_type,))
class MigrationActionValidator(BaseActionValidator):
action_name = "migrate"
def validate_parents(self, resource_action_map, action):
instance_uuid = action['input_parameters']['resource_id']
host_name = action['input_parameters']['source_node']
self._mapping(resource_action_map, instance_uuid, action['uuid'],
'migrate')
self._mapping(resource_action_map, host_name, action['uuid'],
'migrate')
class ResizeActionValidator(BaseActionValidator):
action_name = "resize"
def validate_parents(self, resource_action_map, action):
nova = nova_helper.NovaHelper(osc=self.osc)
instance_uuid = action['input_parameters']['resource_id']
parent_actions = resource_action_map.get(instance_uuid)
host_of_instance = nova.get_hostname(
nova.get_instance_by_uuid(instance_uuid)[0])
self._mapping(resource_action_map, host_of_instance, action['uuid'],
'resize')
if parent_actions:
return [x[0] for x in parent_actions]
else:
return []
class ChangeNovaServiceStateActionValidator(BaseActionValidator):
action_name = "change_nova_service_state"
def validate_parents(self, resource_action_map, action):
host_name = action['input_parameters']['resource_id']
self._mapping(resource_action_map, host_name, action.uuid,
'change_nova_service_state')
return []
class SleepActionValidator(BaseActionValidator):
action_name = "sleep"
def validate_parents(self, resource_action_map, action):
return []
class NOPActionValidator(BaseActionValidator):
action_name = "nop"
def validate_parents(self, resource_action_map, action):
return []

View File

@@ -13,9 +13,6 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import copy
from oslo_log import log
@@ -101,9 +98,8 @@ class DefaultScope(base.BaseScope):
self._osc = osc
self.wrapper = nova_helper.NovaHelper(osc=self._osc)
def _remove_instance(self, cluster_model, instance_uuid, node_name):
def remove_instance(self, cluster_model, instance, node_name):
node = cluster_model.get_node_by_uuid(node_name)
instance = cluster_model.get_instance_by_uuid(instance_uuid)
cluster_model.delete_instance(instance, node)
def _check_wildcard(self, aggregate_list):
@@ -147,7 +143,7 @@ class DefaultScope(base.BaseScope):
if zone.zoneName in zone_names or include_all_nodes:
allowed_nodes.extend(zone.hosts.keys())
def _exclude_resources(self, resources, **kwargs):
def exclude_resources(self, resources, **kwargs):
instances_to_exclude = kwargs.get('instances')
nodes_to_exclude = kwargs.get('nodes')
for resource in resources:
@@ -160,37 +156,38 @@ class DefaultScope(base.BaseScope):
[host['name'] for host
in resource['compute_nodes']])
def _remove_node_from_model(self, nodes_to_remove, cluster_model):
for node_name in nodes_to_remove:
instances = copy.copy(
cluster_model.get_mapping().get_node_instances_by_uuid(
node_name))
for instance_uuid in instances:
self._remove_instance(cluster_model, instance_uuid, node_name)
node = cluster_model.get_node_by_uuid(node_name)
def remove_nodes_from_model(self, nodes_to_remove, cluster_model):
for node_uuid in nodes_to_remove:
node = cluster_model.get_node_by_uuid(node_uuid)
instances = cluster_model.get_node_instances(node)
for instance in instances:
self.remove_instance(cluster_model, instance, node_uuid)
cluster_model.remove_node(node)
def _remove_instances_from_model(self, instances_to_remove, cluster_model):
def remove_instances_from_model(self, instances_to_remove, cluster_model):
for instance_uuid in instances_to_remove:
try:
node_name = (cluster_model.get_mapping()
.get_node_by_instance_uuid(instance_uuid).uuid)
except KeyError:
node_name = cluster_model.get_node_by_instance_uuid(
instance_uuid).uuid
except exception.InstanceNotFound:
LOG.warning(_LW("The following instance %s cannot be found. "
"It might be deleted from CDM along with node"
" instance was hosted on."),
instance_uuid)
continue
self._remove_instance(cluster_model, instance_uuid, node_name)
self.remove_instance(
cluster_model,
cluster_model.get_instance_by_uuid(instance_uuid),
node_name)
def get_scoped_model(self, cluster_model):
"""Leave only nodes and instances proposed in the audit scope"""
if not cluster_model:
return None
allowed_nodes = []
nodes_to_exclude = []
nodes_to_remove = set()
instances_to_exclude = []
model_hosts = list(cluster_model.get_all_compute_nodes().keys())
@@ -205,15 +202,16 @@ class DefaultScope(base.BaseScope):
self._collect_zones(rule['availability_zones'],
allowed_nodes)
elif 'exclude' in rule:
self._exclude_resources(
self.exclude_resources(
rule['exclude'], instances=instances_to_exclude,
nodes=nodes_to_exclude)
instances_to_remove = set(instances_to_exclude)
nodes_to_remove = set(model_hosts) - set(allowed_nodes)
if allowed_nodes:
nodes_to_remove = set(model_hosts) - set(allowed_nodes)
nodes_to_remove.update(nodes_to_exclude)
self._remove_node_from_model(nodes_to_remove, cluster_model)
self._remove_instances_from_model(instances_to_remove, cluster_model)
self.remove_nodes_from_model(nodes_to_remove, cluster_model)
self.remove_instances_from_model(instances_to_remove, cluster_model)
return cluster_model

View File

@@ -41,6 +41,9 @@ class DefaultSolution(base.BaseSolution):
if baction.BaseAction.RESOURCE_ID in input_parameters.keys():
raise exception.ReservedWord(name=baction.BaseAction.
RESOURCE_ID)
else:
input_parameters = {}
if resource_id is not None:
input_parameters[baction.BaseAction.RESOURCE_ID] = resource_id
action = {

View File

@@ -35,11 +35,13 @@ migration is possible on your OpenStack cluster.
"""
from oslo_config import cfg
from oslo_log import log
from watcher._i18n import _, _LE, _LI, _LW
from watcher.common import exception
from watcher.decision_engine.cluster.history import ceilometer as cch
from watcher.datasource import ceilometer as ceil
from watcher.datasource import monasca as mon
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -52,6 +54,15 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent'
INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util'
METRIC_NAMES = dict(
ceilometer=dict(
host_cpu_usage='compute.node.cpu.percent',
instance_cpu_usage='cpu_util'),
monasca=dict(
host_cpu_usage='cpu.percent',
instance_cpu_usage='vm.cpu.utilization_perc'),
)
MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
@@ -64,6 +75,8 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
"""
super(BasicConsolidation, self).__init__(config, osc)
# set default value for the number of enabled compute nodes
self.number_of_enabled_nodes = 0
# set default value for the number of released nodes
self.number_of_released_nodes = 0
# set default value for the number of migrations
@@ -73,6 +86,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
self.efficacy = 100
self._ceilometer = None
self._monasca = None
# TODO(jed): improve threshold overbooking?
self.threshold_mem = 1
@@ -87,6 +101,10 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
def migration_attempts(self):
return self.input_parameters.get('migration_attempts', 0)
@property
def period(self):
return self.input_parameters.get('period', 7200)
@classmethod
def get_display_name(cls):
return _("Basic offline consolidation")
@@ -108,19 +126,45 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
"type": "number",
"default": 0
},
"period": {
"description": "The time interval in seconds for "
"getting statistic aggregation",
"type": "number",
"default": 7200
},
},
}
@classmethod
def get_config_opts(cls):
return [
cfg.StrOpt(
"datasource",
help="Data source to use in order to query the needed metrics",
default="ceilometer",
choices=["ceilometer", "monasca"]),
]
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = cch.CeilometerClusterHistory(osc=self.osc)
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@ceilometer.setter
def ceilometer(self, ceilometer):
self._ceilometer = ceilometer
@property
def monasca(self):
if self._monasca is None:
self._monasca = mon.MonascaHelper(osc=self.osc)
return self._monasca
@monasca.setter
def monasca(self, monasca):
self._monasca = monasca
def check_migration(self, source_node, destination_node,
instance_to_migrate):
"""Check if the migration is possible
@@ -139,24 +183,16 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
total_cores = 0
total_disk = 0
total_mem = 0
cpu_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores)
disk_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk)
memory_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory)
for instance_id in self.compute_model.mapping.get_node_instances(
for instance in self.compute_model.get_node_instances(
destination_node):
instance = self.compute_model.get_instance_by_uuid(instance_id)
total_cores += cpu_capacity.get_capacity(instance)
total_disk += disk_capacity.get_capacity(instance)
total_mem += memory_capacity.get_capacity(instance)
total_cores += instance.vcpus
total_disk += instance.disk
total_mem += instance.memory
# capacity requested by the compute node
total_cores += cpu_capacity.get_capacity(instance_to_migrate)
total_disk += disk_capacity.get_capacity(instance_to_migrate)
total_mem += memory_capacity.get_capacity(instance_to_migrate)
total_cores += instance_to_migrate.vcpus
total_disk += instance_to_migrate.disk
total_mem += instance_to_migrate.memory
return self.check_threshold(destination_node, total_cores, total_disk,
total_mem)
@@ -175,12 +211,9 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
:param total_mem: total memory used by the virtual machine
:return: True if the threshold is not exceed
"""
cpu_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(destination_node)
disk_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk).get_capacity(destination_node)
memory_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory).get_capacity(destination_node)
cpu_capacity = destination_node.vcpus
disk_capacity = destination_node.disk
memory_capacity = destination_node.memory
return (cpu_capacity >= total_cores * self.threshold_cores and
disk_capacity >= total_disk * self.threshold_disk and
@@ -196,14 +229,9 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
:param total_memory_used:
:return:
"""
cpu_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(compute_resource)
disk_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk).get_capacity(compute_resource)
memory_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory).get_capacity(compute_resource)
cpu_capacity = compute_resource.vcpus
disk_capacity = compute_resource.disk
memory_capacity = compute_resource.memory
score_cores = (1 - (float(cpu_capacity) - float(total_cores_used)) /
float(cpu_capacity))
@@ -221,6 +249,64 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
# TODO(jed): take in account weight
return (score_cores + score_disk + score_memory) / 3
def get_node_cpu_usage(self, node):
metric_name = self.METRIC_NAMES[
self.config.datasource]['host_cpu_usage']
if self.config.datasource == "ceilometer":
resource_id = "%s_%s" % (node.uuid, node.hostname)
return self.ceilometer.statistic_aggregation(
resource_id=resource_id,
meter_name=metric_name,
period=self.period,
aggregate='avg',
)
elif self.config.datasource == "monasca":
statistics = self.monasca.statistic_aggregation(
meter_name=metric_name,
dimensions=dict(hostname=node.uuid),
period=self.period,
aggregate='avg'
)
cpu_usage = None
for stat in statistics:
avg_col_idx = stat['columns'].index('avg')
values = [r[avg_col_idx] for r in stat['statistics']]
value = float(sum(values)) / len(values)
cpu_usage = value
return cpu_usage
raise exception.UnsupportedDataSource(
strategy=self.name, datasource=self.config.datasource)
def get_instance_cpu_usage(self, instance):
metric_name = self.METRIC_NAMES[
self.config.datasource]['instance_cpu_usage']
if self.config.datasource == "ceilometer":
return self.ceilometer.statistic_aggregation(
resource_id=instance.uuid,
meter_name=metric_name,
period=self.period,
aggregate='avg'
)
elif self.config.datasource == "monasca":
statistics = self.monasca.statistic_aggregation(
meter_name=metric_name,
dimensions=dict(resource_id=instance.uuid),
period=self.period,
aggregate='avg'
)
cpu_usage = None
for stat in statistics:
avg_col_idx = stat['columns'].index('avg')
values = [r[avg_col_idx] for r in stat['statistics']]
value = float(sum(values)) / len(values)
cpu_usage = value
return cpu_usage
raise exception.UnsupportedDataSource(
strategy=self.name, datasource=self.config.datasource)
def calculate_score_node(self, node):
"""Calculate the score that represent the utilization level
@@ -228,65 +314,39 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
:return: Score for the given compute node
:rtype: float
"""
resource_id = "%s_%s" % (node.uuid, node.hostname)
host_avg_cpu_util = self.ceilometer.statistic_aggregation(
resource_id=resource_id,
meter_name=self.HOST_CPU_USAGE_METRIC_NAME,
period="7200",
aggregate='avg')
host_avg_cpu_util = self.get_node_cpu_usage(node)
if host_avg_cpu_util is None:
resource_id = "%s_%s" % (node.uuid, node.hostname)
LOG.error(
_LE("No values returned by %(resource_id)s "
"for %(metric_name)s") % dict(
resource_id=resource_id,
metric_name=self.HOST_CPU_USAGE_METRIC_NAME))
metric_name=self.METRIC_NAMES[
self.config.datasource]['host_cpu_usage']))
host_avg_cpu_util = 100
cpu_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(node)
total_cores_used = cpu_capacity * (host_avg_cpu_util / 100.0)
total_cores_used = node.vcpus * (host_avg_cpu_util / 100.0)
return self.calculate_weight(node, total_cores_used, 0, 0)
def calculate_migration_efficacy(self):
"""Calculate migration efficacy
:return: The efficacy tells us that every VM migration resulted
in releasing on node
"""
if self.number_of_migrations > 0:
return (float(self.number_of_released_nodes) / float(
self.number_of_migrations)) * 100
else:
return 0
def calculate_score_instance(self, instance):
"""Calculate Score of virtual machine
:param instance: the virtual machine
:return: score
"""
instance_cpu_utilization = self.ceilometer. \
statistic_aggregation(
resource_id=instance.uuid,
meter_name=self.INSTANCE_CPU_USAGE_METRIC_NAME,
period="7200",
aggregate='avg'
)
instance_cpu_utilization = self.get_instance_cpu_usage(instance)
if instance_cpu_utilization is None:
LOG.error(
_LE("No values returned by %(resource_id)s "
"for %(metric_name)s") % dict(
resource_id=instance.uuid,
metric_name=self.INSTANCE_CPU_USAGE_METRIC_NAME))
metric_name=self.METRIC_NAMES[
self.config.datasource]['instance_cpu_usage']))
instance_cpu_utilization = 100
cpu_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(instance)
total_cores_used = cpu_capacity * (instance_cpu_utilization / 100.0)
total_cores_used = instance.vcpus * (instance_cpu_utilization / 100.0)
return self.calculate_weight(instance, total_cores_used, 0, 0)
@@ -312,28 +372,27 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
"""Calculate score of nodes based on load by VMs"""
score = []
for node in self.compute_model.get_all_compute_nodes().values():
count = self.compute_model.mapping.get_node_instances(node)
if len(count) > 0:
if node.status == element.ServiceState.ENABLED.value:
self.number_of_enabled_nodes += 1
instances = self.compute_model.get_node_instances(node)
if len(instances) > 0:
result = self.calculate_score_node(node)
else:
# The node has not VMs
result = 0
if len(count) > 0:
score.append((node.uuid, result))
return score
def node_and_instance_score(self, sorted_scores):
"""Get List of VMs from node"""
node_to_release = sorted_scores[len(sorted_scores) - 1][0]
instances_to_migrate = self.compute_model.mapping.get_node_instances(
instances_to_migrate = self.compute_model.get_node_instances(
self.compute_model.get_node_by_uuid(node_to_release))
instance_score = []
for instance_id in instances_to_migrate:
instance = self.compute_model.get_instance_by_uuid(instance_id)
for instance in instances_to_migrate:
if instance.state == element.InstanceState.ACTIVE.value:
instance_score.append(
(instance_id, self.calculate_score_instance(instance)))
(instance, self.calculate_score_instance(instance)))
return node_to_release, instance_score
@@ -346,8 +405,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
mig_source_node.uuid,
mig_destination_node.uuid)
if len(self.compute_model.mapping.get_node_instances(
mig_source_node)) == 0:
if len(self.compute_model.get_node_instances(mig_source_node)) == 0:
self.add_change_service_state(mig_source_node.
uuid,
element.ServiceState.DISABLED.value)
@@ -356,10 +414,8 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
def calculate_num_migrations(self, sorted_instances, node_to_release,
sorted_score):
number_migrations = 0
for instance in sorted_instances:
for mig_instance, __ in sorted_instances:
for j in range(0, len(sorted_score)):
mig_instance = self.compute_model.get_instance_by_uuid(
instance[0])
mig_source_node = self.compute_model.get_node_by_uuid(
node_to_release)
mig_destination_node = self.compute_model.get_node_by_uuid(
@@ -399,15 +455,6 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
def do_execute(self):
unsuccessful_migration = 0
for node_uuid, node in self.compute_model.get_all_compute_nodes(
).items():
node_instances = self.compute_model.mapping.get_node_instances(
node)
if node_instances:
if node.state == element.ServiceState.ENABLED:
self.add_change_service_state(
node_uuid, element.ServiceState.DISABLED.value)
scores = self.compute_score_of_nodes()
# Sort compute nodes by Score decreasing
sorted_scores = sorted(scores, reverse=True, key=lambda x: (x[1]))
@@ -444,6 +491,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
sorted_scores.pop()
infos = {
"compute_nodes_count": self.number_of_enabled_nodes,
"released_compute_nodes_count": self.number_of_released_nodes,
"instance_migrations_count": self.number_of_migrations,
"efficacy": self.efficacy
@@ -452,6 +500,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
def post_execute(self):
self.solution.set_efficacy_indicators(
compute_nodes_count=self.number_of_enabled_nodes,
released_compute_nodes_count=self.number_of_released_nodes,
instance_migrations_count=self.number_of_migrations,
)

View File

@@ -60,12 +60,12 @@ class DummyStrategy(base.DummyBaseStrategy):
self.solution.add_action(action_type=self.NOP,
input_parameters=parameters)
parameters = {'message': 'Welcome'}
parameters = {'message': para2}
self.solution.add_action(action_type=self.NOP,
input_parameters=parameters)
self.solution.add_action(action_type=self.SLEEP,
input_parameters={'duration': 5.0})
input_parameters={'duration': para1})
def post_execute(self):
pass

View File

@@ -0,0 +1,121 @@
# -*- encoding: 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
#
# 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
from watcher._i18n import _
from watcher.decision_engine.strategy.strategies import base
LOG = log.getLogger(__name__)
class DummyWithResize(base.DummyBaseStrategy):
"""Dummy strategy used for integration testing via Tempest
*Description*
This strategy does not provide any useful optimization. Its only purpose
is to be used by Tempest tests.
*Requirements*
<None>
*Limitations*
Do not use in production.
*Spec URL*
<None>
"""
NOP = "nop"
SLEEP = "sleep"
def pre_execute(self):
pass
def do_execute(self):
para1 = self.input_parameters.para1
para2 = self.input_parameters.para2
LOG.debug("Executing Dummy strategy with para1=%(p1)f, para2=%(p2)s",
{'p1': para1, 'p2': para2})
parameters = {'message': 'hello World'}
self.solution.add_action(action_type=self.NOP,
input_parameters=parameters)
parameters = {'message': 'Welcome'}
self.solution.add_action(action_type=self.NOP,
input_parameters=parameters)
self.solution.add_action(action_type=self.SLEEP,
input_parameters={'duration': 5.0})
self.solution.add_action(
action_type='migrate',
resource_id='b199db0c-1408-4d52-b5a5-5ca14de0ff36',
input_parameters={
'source_node': 'compute2',
'destination_node': 'compute3',
'migration_type': 'live'})
self.solution.add_action(
action_type='migrate',
resource_id='8db1b3c1-7938-4c34-8c03-6de14b874f8f',
input_parameters={
'source_node': 'compute2',
'destination_node': 'compute3',
'migration_type': 'live'}
)
self.solution.add_action(
action_type='resize',
resource_id='8db1b3c1-7938-4c34-8c03-6de14b874f8f',
input_parameters={'flavor': 'x2'}
)
def post_execute(self):
pass
@classmethod
def get_name(cls):
return "dummy_with_resize"
@classmethod
def get_display_name(cls):
return _("Dummy strategy with resize")
@classmethod
def get_translatable_display_name(cls):
return "Dummy strategy with resize"
@classmethod
def get_schema(cls):
# Mandatory default setting for each element
return {
"properties": {
"para1": {
"description": "number parameter example",
"type": "number",
"default": 3.2,
"minimum": 1.0,
"maximum": 10.2,
},
"para2": {
"description": "string parameter example",
"type": "string",
"default": "hello"
},
},
}

View File

@@ -32,7 +32,7 @@ from oslo_log import log
from watcher._i18n import _, _LW, _LI
from watcher.common import exception as wexc
from watcher.decision_engine.cluster.history import ceilometer as ceil
from watcher.datasource import ceilometer as ceil
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -114,26 +114,23 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@ceilometer.setter
def ceilometer(self, c):
self._ceilometer = c
def calc_used_res(self, node, cpu_capacity,
memory_capacity, disk_capacity):
def calc_used_resource(self, node):
"""Calculate the used vcpus, memory and disk based on VM flavors"""
instances = self.compute_model.mapping.get_node_instances(node)
instances = self.compute_model.get_node_instances(node)
vcpus_used = 0
memory_mb_used = 0
disk_gb_used = 0
if len(instances) > 0:
for instance_id in instances:
instance = self.compute_model.get_instance_by_uuid(instance_id)
vcpus_used += cpu_capacity.get_capacity(instance)
memory_mb_used += memory_capacity.get_capacity(instance)
disk_gb_used += disk_capacity.get_capacity(instance)
for instance in instances:
vcpus_used += instance.vcpus
memory_mb_used += instance.memory
disk_gb_used += instance.disk
return vcpus_used, memory_mb_used, disk_gb_used
@@ -146,9 +143,7 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
hosts_need_release = []
hosts_target = []
for node_id in nodes:
node = self.compute_model.get_node_by_uuid(
node_id)
for node in nodes.values():
resource_id = node.uuid
outlet_temp = self.ceilometer.statistic_aggregation(
@@ -174,49 +169,38 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
"""Pick up an active instance to migrate from provided hosts"""
for instance_data in hosts:
mig_source_node = instance_data['node']
instances_of_src = self.compute_model.mapping.get_node_instances(
instances_of_src = self.compute_model.get_node_instances(
mig_source_node)
if len(instances_of_src) > 0:
for instance_id in instances_of_src:
try:
# select the first active instance to migrate
instance = self.compute_model.get_instance_by_uuid(
instance_id)
if (instance.state !=
element.InstanceState.ACTIVE.value):
LOG.info(_LI("Instance not active, skipped: %s"),
instance.uuid)
continue
return mig_source_node, instance
except wexc.InstanceNotFound as e:
LOG.exception(e)
LOG.info(_LI("Instance not found"))
for instance in instances_of_src:
try:
# select the first active instance to migrate
if (instance.state !=
element.InstanceState.ACTIVE.value):
LOG.info(_LI("Instance not active, skipped: %s"),
instance.uuid)
continue
return mig_source_node, instance
except wexc.InstanceNotFound as e:
LOG.exception(e)
LOG.info(_LI("Instance not found"))
return None
def filter_dest_servers(self, hosts, instance_to_migrate):
"""Only return hosts with sufficient available resources"""
cpu_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores)
disk_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk)
memory_capacity = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory)
required_cores = cpu_capacity.get_capacity(instance_to_migrate)
required_disk = disk_capacity.get_capacity(instance_to_migrate)
required_memory = memory_capacity.get_capacity(instance_to_migrate)
required_cores = instance_to_migrate.vcpus
required_disk = instance_to_migrate.disk
required_memory = instance_to_migrate.memory
# filter nodes without enough resource
dest_servers = []
for instance_data in hosts:
host = instance_data['node']
# available
cores_used, mem_used, disk_used = self.calc_used_res(
host, cpu_capacity, memory_capacity, disk_capacity)
cores_available = cpu_capacity.get_capacity(host) - cores_used
disk_available = disk_capacity.get_capacity(host) - disk_used
mem_available = memory_capacity.get_capacity(host) - mem_used
cores_used, mem_used, disk_used = self.calc_used_resource(host)
cores_available = host.vcpus - cores_used
disk_available = host.disk - disk_used
mem_available = host.memory - mem_used
if cores_available >= required_cores \
and disk_available >= required_disk \
and mem_available >= required_memory:

View File

@@ -45,9 +45,9 @@ airflow is higher than the specified threshold.
from oslo_log import log
from watcher._i18n import _, _LE, _LI, _LW
from watcher._i18n import _, _LI, _LW
from watcher.common import exception as wexc
from watcher.decision_engine.cluster.history import ceilometer as ceil
from watcher.datasource import ceilometer as ceil
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -110,7 +110,7 @@ class UniformAirflow(base.BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@ceilometer.setter
@@ -164,18 +164,16 @@ class UniformAirflow(base.BaseStrategy):
},
}
def calculate_used_resource(self, node, cap_cores, cap_mem, cap_disk):
def calculate_used_resource(self, node):
"""Compute the used vcpus, memory and disk based on instance flavors"""
instances = self.compute_model.mapping.get_node_instances(node)
instances = self.compute_model.get_node_instances(node)
vcpus_used = 0
memory_mb_used = 0
disk_gb_used = 0
for instance_id in instances:
instance = self.compute_model.get_instance_by_uuid(
instance_id)
vcpus_used += cap_cores.get_capacity(instance)
memory_mb_used += cap_mem.get_capacity(instance)
disk_gb_used += cap_disk.get_capacity(instance)
for instance in instances:
vcpus_used += instance.vcpus
memory_mb_used += instance.memory
disk_gb_used += instance.disk
return vcpus_used, memory_mb_used, disk_gb_used
@@ -187,7 +185,7 @@ class UniformAirflow(base.BaseStrategy):
instances_tobe_migrate = []
for nodemap in hosts:
source_node = nodemap['node']
source_instances = self.compute_model.mapping.get_node_instances(
source_instances = self.compute_model.get_node_instances(
source_node)
if source_instances:
inlet_t = self.ceilometer.statistic_aggregation(
@@ -203,55 +201,36 @@ class UniformAirflow(base.BaseStrategy):
if (power < self.threshold_power and
inlet_t < self.threshold_inlet_t):
# hardware issue, migrate all instances from this node
for instance_id in source_instances:
try:
instance = (self.compute_model.
get_instance_by_uuid(instance_id))
instances_tobe_migrate.append(instance)
except wexc.InstanceNotFound:
LOG.error(_LE("Instance not found; error: %s"),
instance_id)
for instance in source_instances:
instances_tobe_migrate.append(instance)
return source_node, instances_tobe_migrate
else:
# migrate the first active instance
for instance_id in source_instances:
try:
instance = (self.compute_model.
get_instance_by_uuid(instance_id))
if (instance.state !=
element.InstanceState.ACTIVE.value):
LOG.info(
_LI("Instance not active, skipped: %s"),
instance.uuid)
continue
instances_tobe_migrate.append(instance)
return source_node, instances_tobe_migrate
except wexc.InstanceNotFound:
LOG.error(_LE("Instance not found; error: %s"),
instance_id)
for instance in source_instances:
if (instance.state !=
element.InstanceState.ACTIVE.value):
LOG.info(
_LI("Instance not active, skipped: %s"),
instance.uuid)
continue
instances_tobe_migrate.append(instance)
return source_node, instances_tobe_migrate
else:
LOG.info(_LI("Instance not found on node: %s"),
source_node.uuid)
def filter_destination_hosts(self, hosts, instances_to_migrate):
"""Find instance and host with sufficient available resources"""
cap_cores = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores)
cap_disk = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk)
cap_mem = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory)
# large instance go first
# large instances go first
instances_to_migrate = sorted(
instances_to_migrate, reverse=True,
key=lambda x: (cap_cores.get_capacity(x)))
key=lambda x: (x.vcpus))
# find hosts for instances
destination_hosts = []
for instance_to_migrate in instances_to_migrate:
required_cores = cap_cores.get_capacity(instance_to_migrate)
required_disk = cap_disk.get_capacity(instance_to_migrate)
required_mem = cap_mem.get_capacity(instance_to_migrate)
required_cores = instance_to_migrate.vcpus
required_disk = instance_to_migrate.disk
required_mem = instance_to_migrate.memory
dest_migrate_info = {}
for nodemap in hosts:
host = nodemap['node']
@@ -259,13 +238,13 @@ class UniformAirflow(base.BaseStrategy):
# calculate the available resources
nodemap['cores_used'], nodemap['mem_used'],\
nodemap['disk_used'] = self.calculate_used_resource(
host, cap_cores, cap_mem, cap_disk)
cores_available = (cap_cores.get_capacity(host) -
host)
cores_available = (host.vcpus -
nodemap['cores_used'])
disk_available = (cap_disk.get_capacity(host) -
disk_available = (host.disk -
nodemap['disk_used'])
mem_available = (
cap_mem.get_capacity(host) - nodemap['mem_used'])
host.memory - nodemap['mem_used'])
if (cores_available >= required_cores and
disk_available >= required_disk and
mem_available >= required_mem):

View File

@@ -58,8 +58,7 @@ import six
from watcher._i18n import _, _LE, _LI
from watcher.common import exception
from watcher.decision_engine.cluster.history import ceilometer \
as ceilometer_cluster_history
from watcher.datasource import ceilometer as ceil
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -91,8 +90,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = (ceilometer_cluster_history.
CeilometerClusterHistory(osc=self.osc))
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@ceilometer.setter
@@ -110,9 +108,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
return state.value
else:
LOG.error(_LE('Unexpexted resource state type, '
'state=%(state)s, state_type=%(st)s.'),
state=state,
st=type(state))
'state=%(state)s, state_type=%(st)s.') % dict(
state=state,
st=type(state)))
raise exception.WatcherException
def add_action_enable_compute_node(self, node):
@@ -141,18 +139,14 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
input_parameters=params)
self.number_of_released_nodes += 1
def add_migration(self, instance_uuid, source_node,
destination_node, model):
def add_migration(self, instance, source_node, destination_node):
"""Add an action for VM migration into the solution.
:param instance_uuid: instance uuid
:param instance: instance object
:param source_node: node object
:param destination_node: node object
:param model: model_root object
:return: None
"""
instance = model.get_instance_by_uuid(instance_uuid)
instance_state_str = self.get_state_str(instance.state)
if instance_state_str != element.InstanceState.ACTIVE.value:
# Watcher curently only supports live VM migration and block live
@@ -161,9 +155,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
# migration mechanism to move non active VMs.
LOG.error(
_LE('Cannot live migrate: instance_uuid=%(instance_uuid)s, '
'state=%(instance_state)s.'),
instance_uuid=instance_uuid,
instance_state=instance_state_str)
'state=%(instance_state)s.') % dict(
instance_uuid=instance.uuid,
instance_state=instance_state_str))
return
migration_type = 'live'
@@ -171,41 +165,39 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
destination_node_state_str = self.get_state_str(destination_node.state)
if destination_node_state_str == element.ServiceState.DISABLED.value:
self.add_action_enable_compute_node(destination_node)
model.mapping.unmap(source_node, instance)
model.mapping.map(destination_node, instance)
params = {'migration_type': migration_type,
'source_node': source_node.uuid,
'destination_node': destination_node.uuid}
self.solution.add_action(action_type='migrate',
resource_id=instance.uuid,
input_parameters=params)
self.number_of_migrations += 1
if self.compute_model.migrate_instance(
instance, source_node, destination_node):
params = {'migration_type': migration_type,
'source_node': source_node.uuid,
'destination_node': destination_node.uuid}
self.solution.add_action(action_type='migrate',
resource_id=instance.uuid,
input_parameters=params)
self.number_of_migrations += 1
def disable_unused_nodes(self, model):
def disable_unused_nodes(self):
"""Generate actions for disablity of unused nodes.
:param model: model_root object
:return: None
"""
for node in model.get_all_compute_nodes().values():
if (len(model.mapping.get_node_instances(node)) == 0 and
for node in self.compute_model.get_all_compute_nodes().values():
if (len(self.compute_model.get_node_instances(node)) == 0 and
node.status !=
element.ServiceState.DISABLED.value):
self.add_action_disable_node(node)
def get_instance_utilization(self, instance_uuid, model,
def get_instance_utilization(self, instance,
period=3600, aggr='avg'):
"""Collect cpu, ram and disk utilization statistics of a VM.
:param instance_uuid: instance object
:param model: model_root object
:param instance: instance object
:param period: seconds
:param aggr: string
:return: dict(cpu(number of vcpus used), ram(MB used), disk(B used))
"""
if instance_uuid in self.ceilometer_instance_data_cache.keys():
return self.ceilometer_instance_data_cache.get(instance_uuid)
if instance.uuid in self.ceilometer_instance_data_cache.keys():
return self.ceilometer_instance_data_cache.get(instance.uuid)
cpu_util_metric = 'cpu_util'
ram_util_metric = 'memory.usage'
@@ -213,61 +205,54 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
ram_alloc_metric = 'memory'
disk_alloc_metric = 'disk.root.size'
instance_cpu_util = self.ceilometer.statistic_aggregation(
resource_id=instance_uuid, meter_name=cpu_util_metric,
resource_id=instance.uuid, meter_name=cpu_util_metric,
period=period, aggregate=aggr)
instance_cpu_cores = model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(
model.get_instance_by_uuid(instance_uuid))
if instance_cpu_util:
total_cpu_utilization = (
instance_cpu_cores * (instance_cpu_util / 100.0))
instance.vcpus * (instance_cpu_util / 100.0))
else:
total_cpu_utilization = instance_cpu_cores
total_cpu_utilization = instance.vcpus
instance_ram_util = self.ceilometer.statistic_aggregation(
resource_id=instance_uuid, meter_name=ram_util_metric,
resource_id=instance.uuid, meter_name=ram_util_metric,
period=period, aggregate=aggr)
if not instance_ram_util:
instance_ram_util = self.ceilometer.statistic_aggregation(
resource_id=instance_uuid, meter_name=ram_alloc_metric,
resource_id=instance.uuid, meter_name=ram_alloc_metric,
period=period, aggregate=aggr)
instance_disk_util = self.ceilometer.statistic_aggregation(
resource_id=instance_uuid, meter_name=disk_alloc_metric,
resource_id=instance.uuid, meter_name=disk_alloc_metric,
period=period, aggregate=aggr)
if not instance_ram_util or not instance_disk_util:
LOG.error(
_LE('No values returned by %(resource_id)s '
'for memory.usage or disk.root.size'),
resource_id=instance_uuid
)
_LE('No values returned by %s for memory.usage '
'or disk.root.size'), instance.uuid)
raise exception.NoDataFound
self.ceilometer_instance_data_cache[instance_uuid] = dict(
self.ceilometer_instance_data_cache[instance.uuid] = dict(
cpu=total_cpu_utilization, ram=instance_ram_util,
disk=instance_disk_util)
return self.ceilometer_instance_data_cache.get(instance_uuid)
return self.ceilometer_instance_data_cache.get(instance.uuid)
def get_node_utilization(self, node, model, period=3600, aggr='avg'):
def get_node_utilization(self, node, period=3600, aggr='avg'):
"""Collect cpu, ram and disk utilization statistics of a node.
:param node: node object
:param model: model_root object
:param period: seconds
:param aggr: string
:return: dict(cpu(number of cores used), ram(MB used), disk(B used))
"""
node_instances = model.mapping.get_node_instances_by_uuid(
node.uuid)
node_instances = self.compute_model.get_node_instances(node)
node_ram_util = 0
node_disk_util = 0
node_cpu_util = 0
for instance_uuid in node_instances:
for instance in node_instances:
instance_util = self.get_instance_utilization(
instance_uuid, model, period, aggr)
instance, period, aggr)
node_cpu_util += instance_util['cpu']
node_ram_util += instance_util['ram']
node_disk_util += instance_util['disk']
@@ -275,53 +260,40 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
return dict(cpu=node_cpu_util, ram=node_ram_util,
disk=node_disk_util)
def get_node_capacity(self, node, model):
def get_node_capacity(self, node):
"""Collect cpu, ram and disk capacity of a node.
:param node: node object
:param model: model_root object
:return: dict(cpu(cores), ram(MB), disk(B))
"""
node_cpu_capacity = model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(node)
return dict(cpu=node.vcpus, ram=node.memory, disk=node.disk_capacity)
node_disk_capacity = model.get_resource_by_uuid(
element.ResourceType.disk_capacity).get_capacity(node)
node_ram_capacity = model.get_resource_by_uuid(
element.ResourceType.memory).get_capacity(node)
return dict(cpu=node_cpu_capacity, ram=node_ram_capacity,
disk=node_disk_capacity)
def get_relative_node_utilization(self, node, model):
"""Return relative node utilization (rhu).
def get_relative_node_utilization(self, node):
"""Return relative node utilization.
:param node: node object
:param model: model_root object
:return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>}
"""
rhu = {}
util = self.get_node_utilization(node, model)
cap = self.get_node_capacity(node, model)
relative_node_utilization = {}
util = self.get_node_utilization(node)
cap = self.get_node_capacity(node)
for k in util.keys():
rhu[k] = float(util[k]) / float(cap[k])
return rhu
relative_node_utilization[k] = float(util[k]) / float(cap[k])
return relative_node_utilization
def get_relative_cluster_utilization(self, model):
def get_relative_cluster_utilization(self):
"""Calculate relative cluster utilization (rcu).
RCU is an average of relative utilizations (rhu) of active nodes.
:param model: model_root object
:return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>}
"""
nodes = model.get_all_compute_nodes().values()
nodes = self.compute_model.get_all_compute_nodes().values()
rcu = {}
counters = {}
for node in nodes:
node_state_str = self.get_state_str(node.state)
if node_state_str == element.ServiceState.ENABLED.value:
rhu = self.get_relative_node_utilization(
node, model)
rhu = self.get_relative_node_utilization(node)
for k in rhu.keys():
if k not in rcu:
rcu[k] = 0
@@ -333,39 +305,35 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
rcu[k] /= counters[k]
return rcu
def is_overloaded(self, node, model, cc):
def is_overloaded(self, node, cc):
"""Indicate whether a node is overloaded.
This considers provided resource capacity coefficients (cc).
:param node: node object
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
:return: [True, False]
"""
node_capacity = self.get_node_capacity(node, model)
node_capacity = self.get_node_capacity(node)
node_utilization = self.get_node_utilization(
node, model)
node)
metrics = ['cpu']
for m in metrics:
if node_utilization[m] > node_capacity[m] * cc[m]:
return True
return False
def instance_fits(self, instance_uuid, node, model, cc):
def instance_fits(self, instance, node, cc):
"""Indicate whether is a node able to accommodate a VM.
This considers provided resource capacity coefficients (cc).
:param instance_uuid: string
:param instance: :py:class:`~.element.Instance`
:param node: node object
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
:return: [True, False]
"""
node_capacity = self.get_node_capacity(node, model)
node_utilization = self.get_node_utilization(
node, model)
instance_utilization = self.get_instance_utilization(
instance_uuid, model)
node_capacity = self.get_node_capacity(node)
node_utilization = self.get_node_utilization(node)
instance_utilization = self.get_instance_utilization(instance)
metrics = ['cpu', 'ram', 'disk']
for m in metrics:
if (instance_utilization[m] + node_utilization[m] >
@@ -373,7 +341,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
return False
return True
def optimize_solution(self, model):
def optimize_solution(self):
"""Optimize solution.
This is done by eliminating unnecessary or circular set of migrations
@@ -386,8 +354,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
one migration instead of two.
* A->B, B->A => remove A->B and B->A as they do not result
in a new VM placement.
:param model: model_root object
"""
migrate_actions = (
a for a in self.solution.actions if a[
@@ -401,15 +367,20 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
'input_parameters'][
'resource_id'] == instance_uuid)
if len(actions) > 1:
src = actions[0]['input_parameters']['source_node']
dst = actions[-1]['input_parameters']['destination_node']
src_uuid = actions[0]['input_parameters']['source_node']
dst_uuid = actions[-1]['input_parameters']['destination_node']
for a in actions:
self.solution.actions.remove(a)
self.number_of_migrations -= 1
if src != dst:
self.add_migration(instance_uuid, src, dst, model)
src_node = self.compute_model.get_node_by_uuid(src_uuid)
dst_node = self.compute_model.get_node_by_uuid(dst_uuid)
instance = self.compute_model.get_instance_by_uuid(
instance_uuid)
if self.compute_model.migrate_instance(
instance, dst_node, src_node):
self.add_migration(instance, src_node, dst_node)
def offload_phase(self, model, cc):
def offload_phase(self, cc):
"""Perform offloading phase.
This considers provided resource capacity coefficients.
@@ -425,29 +396,28 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
the node enabler in this phase doesn't necessarily results
in more enabled nodes in the final solution.
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
"""
sorted_nodes = sorted(
model.get_all_compute_nodes().values(),
key=lambda x: self.get_node_utilization(x, model)['cpu'])
self.compute_model.get_all_compute_nodes().values(),
key=lambda x: self.get_node_utilization(x)['cpu'])
for node in reversed(sorted_nodes):
if self.is_overloaded(node, model, cc):
if self.is_overloaded(node, cc):
for instance in sorted(
model.mapping.get_node_instances(node),
self.compute_model.get_node_instances(node),
key=lambda x: self.get_instance_utilization(
x, model)['cpu']
x)['cpu']
):
for destination_node in reversed(sorted_nodes):
if self.instance_fits(
instance, destination_node, model, cc):
instance, destination_node, cc):
self.add_migration(instance, node,
destination_node, model)
destination_node)
break
if not self.is_overloaded(node, model, cc):
if not self.is_overloaded(node, cc):
break
def consolidation_phase(self, model, cc):
def consolidation_phase(self, cc):
"""Perform consolidation phase.
This considers provided resource capacity coefficients.
@@ -459,26 +429,25 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
in the system than less cpu utilizied VMs which can be later used
to fill smaller CPU capacity gaps.
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
"""
sorted_nodes = sorted(
model.get_all_compute_nodes().values(),
key=lambda x: self.get_node_utilization(x, model)['cpu'])
self.compute_model.get_all_compute_nodes().values(),
key=lambda x: self.get_node_utilization(x)['cpu'])
asc = 0
for node in sorted_nodes:
instances = sorted(
model.mapping.get_node_instances(node),
key=lambda x: self.get_instance_utilization(x, model)['cpu'])
self.compute_model.get_node_instances(node),
key=lambda x: self.get_instance_utilization(x)['cpu'])
for instance in reversed(instances):
dsc = len(sorted_nodes) - 1
for destination_node in reversed(sorted_nodes):
if asc >= dsc:
break
if self.instance_fits(
instance, destination_node, model, cc):
instance, destination_node, cc):
self.add_migration(instance, node,
destination_node, model)
destination_node)
break
dsc -= 1
asc += 1
@@ -507,26 +476,26 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
:param original_model: root_model object
"""
LOG.info(_LI('Executing Smart Strategy'))
model = self.compute_model
rcu = self.get_relative_cluster_utilization(model)
self.ceilometer_vm_data_cache = dict()
rcu = self.get_relative_cluster_utilization()
cc = {'cpu': 1.0, 'ram': 1.0, 'disk': 1.0}
# Offloading phase
self.offload_phase(model, cc)
self.offload_phase(cc)
# Consolidation phase
self.consolidation_phase(model, cc)
self.consolidation_phase(cc)
# Optimize solution
self.optimize_solution(model)
self.optimize_solution()
# disable unused nodes
self.disable_unused_nodes(model)
self.disable_unused_nodes()
rcu_after = self.get_relative_cluster_utilization(model)
rcu_after = self.get_relative_cluster_utilization()
info = {
"compute_nodes_count": len(
self.compute_model.get_all_compute_nodes()),
'number_of_migrations': self.number_of_migrations,
'number_of_released_nodes':
self.number_of_released_nodes,
@@ -538,6 +507,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
def post_execute(self):
self.solution.set_efficacy_indicators(
compute_nodes_count=len(
self.compute_model.get_all_compute_nodes()),
released_compute_nodes_count=self.number_of_released_nodes,
instance_migrations_count=self.number_of_migrations,
)

View File

@@ -51,7 +51,7 @@ from oslo_log import log
from watcher._i18n import _, _LE, _LI, _LW
from watcher.common import exception as wexc
from watcher.decision_engine.cluster.history import ceilometer as ceil
from watcher.datasource import ceilometer as ceil
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -108,7 +108,7 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@ceilometer.setter
@@ -145,18 +145,16 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
},
}
def calculate_used_resource(self, node, cap_cores, cap_mem,
cap_disk):
def calculate_used_resource(self, node):
"""Calculate the used vcpus, memory and disk based on VM flavors"""
instances = self.compute_model.mapping.get_node_instances(node)
instances = self.compute_model.get_node_instances(node)
vcpus_used = 0
memory_mb_used = 0
disk_gb_used = 0
for instance_id in instances:
instance = self.compute_model.get_instance_by_uuid(instance_id)
vcpus_used += cap_cores.get_capacity(instance)
memory_mb_used += cap_mem.get_capacity(instance)
disk_gb_used += cap_disk.get_capacity(instance)
for instance in instances:
vcpus_used += instance.vcpus
memory_mb_used += instance.memory
disk_gb_used += instance.disk
return vcpus_used, memory_mb_used, disk_gb_used
@@ -169,27 +167,25 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
"""
for instance_data in hosts:
source_node = instance_data['node']
source_instances = self.compute_model.mapping.get_node_instances(
source_instances = self.compute_model.get_node_instances(
source_node)
if source_instances:
delta_workload = instance_data['workload'] - avg_workload
min_delta = 1000000
instance_id = None
for inst_id in source_instances:
for instance in source_instances:
try:
# select the first active VM to migrate
instance = self.compute_model.get_instance_by_uuid(
inst_id)
if (instance.state !=
element.InstanceState.ACTIVE.value):
LOG.debug("Instance not active, skipped: %s",
instance.uuid)
continue
current_delta = (
delta_workload - workload_cache[inst_id])
delta_workload - workload_cache[instance.uuid])
if 0 <= current_delta < min_delta:
min_delta = current_delta
instance_id = inst_id
instance_id = instance.uuid
except wexc.InstanceNotFound:
LOG.error(_LE("Instance not found; error: %s"),
instance_id)
@@ -203,18 +199,10 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
def filter_destination_hosts(self, hosts, instance_to_migrate,
avg_workload, workload_cache):
'''Only return hosts with sufficient available resources'''
cap_cores = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores)
cap_disk = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk)
cap_mem = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory)
required_cores = cap_cores.get_capacity(instance_to_migrate)
required_disk = cap_disk.get_capacity(instance_to_migrate)
required_mem = cap_mem.get_capacity(instance_to_migrate)
"""Only return hosts with sufficient available resources"""
required_cores = instance_to_migrate.vcpus
required_disk = instance_to_migrate.disk
required_mem = instance_to_migrate.memory
# filter nodes without enough resource
destination_hosts = []
@@ -224,16 +212,16 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
workload = instance_data['workload']
# calculate the available resources
cores_used, mem_used, disk_used = self.calculate_used_resource(
host, cap_cores, cap_mem, cap_disk)
cores_available = cap_cores.get_capacity(host) - cores_used
disk_available = cap_disk.get_capacity(host) - disk_used
mem_available = cap_mem.get_capacity(host) - mem_used
host)
cores_available = host.vcpus - cores_used
disk_available = host.disk - disk_used
mem_available = host.memory - mem_used
if (
cores_available >= required_cores and
disk_available >= required_disk and
mem_available >= required_mem and
(src_instance_workload + workload) < self.threshold / 100 *
cap_cores.get_capacity(host)
((src_instance_workload + workload) <
self.threshold / 100 * host.vcpus)
):
destination_hosts.append(instance_data)
@@ -252,26 +240,20 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
cluster_size = len(nodes)
if not nodes:
raise wexc.ClusterEmpty()
# get cpu cores capacity of nodes and instances
cap_cores = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores)
overload_hosts = []
nonoverload_hosts = []
# total workload of cluster
# it's the total core numbers being utilized in a cluster.
cluster_workload = 0.0
# use workload_cache to store the workload of VMs for reuse purpose
workload_cache = {}
for node_id in nodes:
node = self.compute_model.get_node_by_uuid(
node_id)
instances = self.compute_model.mapping.get_node_instances(node)
node = self.compute_model.get_node_by_uuid(node_id)
instances = self.compute_model.get_node_instances(node)
node_workload = 0.0
for instance_id in instances:
instance = self.compute_model.get_instance_by_uuid(instance_id)
for instance in instances:
try:
cpu_util = self.ceilometer.statistic_aggregation(
resource_id=instance_id,
resource_id=instance.uuid,
meter_name=self._meter,
period=self._period,
aggregate='avg')
@@ -280,21 +262,19 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
LOG.error(_LE("Can not get cpu_util from Ceilometer"))
continue
if cpu_util is None:
LOG.debug("Instance (%s): cpu_util is None", instance_id)
LOG.debug("Instance (%s): cpu_util is None", instance.uuid)
continue
instance_cores = cap_cores.get_capacity(instance)
workload_cache[instance_id] = cpu_util * instance_cores / 100
node_workload += workload_cache[instance_id]
LOG.debug("VM (%s): cpu_util %f", instance_id, cpu_util)
node_cores = cap_cores.get_capacity(node)
hy_cpu_util = node_workload / node_cores * 100
workload_cache[instance.uuid] = cpu_util * instance.vcpus / 100
node_workload += workload_cache[instance.uuid]
LOG.debug("VM (%s): cpu_util %f", instance.uuid, cpu_util)
node_cpu_util = node_workload / node.vcpus * 100
cluster_workload += node_workload
instance_data = {
'node': node, "cpu_util": hy_cpu_util,
'node': node, "cpu_util": node_cpu_util,
'workload': node_workload}
if hy_cpu_util >= self.threshold:
if node_cpu_util >= self.threshold:
# mark the node to release resources
overload_hosts.append(instance_data)
else:

View File

@@ -40,8 +40,7 @@ import oslo_utils
from watcher._i18n import _LI, _LW, _
from watcher.common import exception
from watcher.decision_engine.cluster.history import ceilometer as \
ceilometer_cluster_history
from watcher.datasource import ceilometer as ceil
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -157,8 +156,7 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = (ceilometer_cluster_history.
CeilometerClusterHistory(osc=self.osc))
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
return self._ceilometer
@property
@@ -187,20 +185,17 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
(instance_load['vcpus'] / float(host_vcpus)))
@MEMOIZE
def get_instance_load(self, instance_uuid):
def get_instance_load(self, instance):
"""Gathering instance load through ceilometer statistic.
:param instance_uuid: instance for which statistic is gathered.
:param instance: instance for which statistic is gathered.
:return: dict
"""
LOG.debug('get_instance_load started')
instance_vcpus = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(
self.compute_model.get_instance_by_uuid(instance_uuid))
instance_load = {'uuid': instance_uuid, 'vcpus': instance_vcpus}
instance_load = {'uuid': instance.uuid, 'vcpus': instance.vcpus}
for meter in self.metrics:
avg_meter = self.ceilometer.statistic_aggregation(
resource_id=instance_uuid,
resource_id=instance.uuid,
meter_name=meter,
period=self.periods['instance'],
aggregate='min'
@@ -209,8 +204,8 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
LOG.warning(
_LW("No values returned by %(resource_id)s "
"for %(metric_name)s") % dict(
resource_id=instance_uuid,
metric_name=meter))
resource_id=instance.uuid,
metric_name=meter))
avg_meter = 0
if meter == 'cpu_util':
avg_meter /= float(100)
@@ -221,10 +216,8 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
normalized_hosts = copy.deepcopy(hosts)
for host in normalized_hosts:
if 'memory.resident' in normalized_hosts[host]:
h_memory = self.compute_model.get_resource_by_uuid(
element.ResourceType.memory).get_capacity(
self.compute_model.get_node_by_uuid(host))
normalized_hosts[host]['memory.resident'] /= float(h_memory)
node = self.compute_model.get_node_by_uuid(host)
normalized_hosts[host]['memory.resident'] /= float(node.memory)
return normalized_hosts
@@ -239,13 +232,9 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
hosts_load = {}
for node_id, node in self.get_available_nodes().items():
hosts_load[node_id] = {}
host_vcpus = self.compute_model.get_resource_by_uuid(
element.ResourceType.cpu_cores).get_capacity(
self.compute_model.get_node_by_uuid(node_id))
hosts_load[node_id]['vcpus'] = host_vcpus
hosts_load[node_id]['vcpus'] = node.vcpus
for metric in self.metrics:
resource_id = ''
meter_name = self.instance_metrics[metric]
if re.match('^compute.node', meter_name) is not None:
@@ -296,34 +285,31 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
" for %s in weight dict.") % metric)
return weighted_sd
def calculate_migration_case(self, hosts, instance_id,
src_node_id, dst_node_id):
def calculate_migration_case(self, hosts, instance, src_node, dst_node):
"""Calculate migration case
Return list of standard deviation values, that appearing in case of
migration of instance from source host to destination host
:param hosts: hosts with their workload
:param instance_id: the virtual machine
:param src_node_id: the source node id
:param dst_node_id: the destination node id
:param instance: the virtual machine
:param src_node: the source node
:param dst_node: the destination node
:return: list of standard deviation values
"""
migration_case = []
new_hosts = copy.deepcopy(hosts)
instance_load = self.get_instance_load(instance_id)
d_host_vcpus = new_hosts[dst_node_id]['vcpus']
s_host_vcpus = new_hosts[src_node_id]['vcpus']
instance_load = self.get_instance_load(instance)
s_host_vcpus = new_hosts[src_node.uuid]['vcpus']
d_host_vcpus = new_hosts[dst_node.uuid]['vcpus']
for metric in self.metrics:
if metric is 'cpu_util':
new_hosts[src_node_id][metric] -= self.transform_instance_cpu(
instance_load,
s_host_vcpus)
new_hosts[dst_node_id][metric] += self.transform_instance_cpu(
instance_load,
d_host_vcpus)
new_hosts[src_node.uuid][metric] -= (
self.transform_instance_cpu(instance_load, s_host_vcpus))
new_hosts[dst_node.uuid][metric] += (
self.transform_instance_cpu(instance_load, d_host_vcpus))
else:
new_hosts[src_node_id][metric] -= instance_load[metric]
new_hosts[dst_node_id][metric] += instance_load[metric]
new_hosts[src_node.uuid][metric] -= instance_load[metric]
new_hosts[dst_node.uuid][metric] += instance_load[metric]
normalized_hosts = self.normalize_hosts_load(new_hosts)
for metric in self.metrics:
migration_case.append(self.get_sd(normalized_hosts, metric))
@@ -345,29 +331,27 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
instance_host_map = []
nodes = list(self.get_available_nodes())
for source_hp_id in nodes:
for src_host in nodes:
src_node = self.compute_model.get_node_by_uuid(src_host)
c_nodes = copy.copy(nodes)
c_nodes.remove(source_hp_id)
c_nodes.remove(src_host)
node_list = yield_nodes(c_nodes)
instances_id = self.compute_model.get_mapping(). \
get_node_instances_by_uuid(source_hp_id)
for instance_id in instances_id:
for instance in self.compute_model.get_node_instances(src_node):
min_sd_case = {'value': len(self.metrics)}
instance = self.compute_model.get_instance_by_uuid(instance_id)
if instance.state not in [element.InstanceState.ACTIVE.value,
element.InstanceState.PAUSED.value]:
continue
for dst_node_id in next(node_list):
sd_case = self.calculate_migration_case(hosts, instance_id,
source_hp_id,
dst_node_id)
for dst_host in next(node_list):
dst_node = self.compute_model.get_node_by_uuid(dst_host)
sd_case = self.calculate_migration_case(
hosts, instance, src_node, dst_node)
weighted_sd = self.calculate_weighted_sd(sd_case[:-1])
if weighted_sd < min_sd_case['value']:
min_sd_case = {
'host': dst_node_id, 'value': weighted_sd,
's_host': source_hp_id, 'instance': instance_id}
'host': dst_node.uuid, 'value': weighted_sd,
's_host': src_node.uuid, 'instance': instance.uuid}
instance_host_map.append(min_sd_case)
return sorted(instance_host_map, key=lambda x: x['value'])
@@ -438,19 +422,16 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
min_sd = 1
balanced = False
for instance_host in migration:
dst_hp_disk = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk).get_capacity(
self.compute_model.get_node_by_uuid(
instance_host['host']))
instance_disk = self.compute_model.get_resource_by_uuid(
element.ResourceType.disk).get_capacity(
self.compute_model.get_instance_by_uuid(
instance_host['instance']))
if instance_disk > dst_hp_disk:
instance = self.compute_model.get_instance_by_uuid(
instance_host['instance'])
src_node = self.compute_model.get_node_by_uuid(
instance_host['s_host'])
dst_node = self.compute_model.get_node_by_uuid(
instance_host['host'])
if instance.disk > dst_node.disk:
continue
instance_load = self.calculate_migration_case(
hosts_load, instance_host['instance'],
instance_host['s_host'], instance_host['host'])
hosts_load, instance, src_node, dst_node)
weighted_sd = self.calculate_weighted_sd(instance_load[:-1])
if weighted_sd < min_sd:
min_sd = weighted_sd

View File

@@ -37,7 +37,8 @@ class Action(base.WatcherPersistentObject, base.WatcherObject,
# Version 1.0: Initial version
# Version 1.1: Added 'action_plan' object field
VERSION = '1.1'
# Version 2.0: Removed 'next' object field, Added 'parents' object field
VERSION = '2.0'
dbapi = db_api.get_instance()
@@ -48,7 +49,7 @@ class Action(base.WatcherPersistentObject, base.WatcherObject,
'action_type': wfields.StringField(nullable=True),
'input_parameters': wfields.DictField(nullable=True),
'state': wfields.StringField(nullable=True),
'next': wfields.IntegerField(nullable=True),
'parents': wfields.ListOfStringsField(nullable=True),
'action_plan': wfields.ObjectField('ActionPlan', nullable=True),
}

View File

@@ -85,6 +85,7 @@ class State(object):
SUCCEEDED = 'SUCCEEDED'
DELETED = 'DELETED'
CANCELLED = 'CANCELLED'
SUPERSEDED = 'SUPERSEDED'
@base.WatcherObjectRegistry.register
@@ -94,7 +95,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
# Version 1.0: Initial version
# Version 1.1: Added 'audit' and 'strategy' object field
# Version 1.2: audit_id is not nullable anymore
VERSION = '1.2'
# Version 2.0: Removed 'first_action_id' object field
VERSION = '2.0'
dbapi = db_api.get_instance()
@@ -103,7 +105,6 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
'uuid': wfields.UUIDField(),
'audit_id': wfields.IntegerField(),
'strategy_id': wfields.IntegerField(),
'first_action_id': wfields.IntegerField(nullable=True),
'state': wfields.StringField(nullable=True),
'global_efficacy': wfields.FlexibleDictField(nullable=True),

View File

@@ -79,7 +79,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
# Version 1.0: Initial version
# Version 1.1: Added 'goal' and 'strategy' object field
VERSION = '1.1'
# Version 1.2 Added 'auto_trigger' boolean field
VERSION = '1.2'
dbapi = db_api.get_instance()
@@ -93,6 +94,7 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
'scope': wfields.FlexibleListOfDictField(nullable=True),
'goal_id': wfields.IntegerField(),
'strategy_id': wfields.IntegerField(nullable=True),
'auto_trigger': wfields.BooleanField(),
'goal': wfields.ObjectField('Goal', nullable=True),
'strategy': wfields.ObjectField('Strategy', nullable=True),

View File

@@ -104,7 +104,7 @@ class WatcherPersistentObject(object):
}
# Mapping between the object field name and a 2-tuple pair composed of
# its object type (e.g. objects.RelatedObject) and the the name of the
# its object type (e.g. objects.RelatedObject) and the name of the
# model field related ID (or UUID) foreign key field.
# e.g.:
#

View File

@@ -17,21 +17,21 @@
import ast
import six
from oslo_log import log
from oslo_versionedobjects import fields
LOG = log.getLogger(__name__)
BaseEnumField = fields.BaseEnumField
BooleanField = fields.BooleanField
DateTimeField = fields.DateTimeField
Enum = fields.Enum
FloatField = fields.FloatField
IntegerField = fields.IntegerField
ListOfStringsField = fields.ListOfStringsField
NonNegativeFloatField = fields.NonNegativeFloatField
NonNegativeIntegerField = fields.NonNegativeIntegerField
ObjectField = fields.ObjectField
StringField = fields.StringField
UnspecifiedDefault = fields.UnspecifiedDefault
UUIDField = fields.UUIDField

View File

@@ -34,7 +34,7 @@ def post_get_test_action(**kw):
del action['action_plan_id']
action['action_plan_uuid'] = kw.get('action_plan_uuid',
action_plan['uuid'])
action['next'] = None
action['parents'] = None
return action
@@ -42,7 +42,7 @@ class TestActionObject(base.TestCase):
def test_action_init(self):
action_dict = api_utils.action_post_data(action_plan_id=None,
next=None)
parents=None)
del action_dict['state']
action = api_action.Action(**action_dict)
self.assertEqual(wtypes.Unset, action.state)
@@ -67,13 +67,13 @@ class TestListAction(api_base.FunctionalTest):
self.assertIn(field, action)
def test_one(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
response = self.get_json('/actions')
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
self._assert_action_fields(response['actions'][0])
def test_one_soft_deleted(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
action.soft_delete()
response = self.get_json('/actions',
headers={'X-Show-Deleted': 'True'})
@@ -84,7 +84,7 @@ class TestListAction(api_base.FunctionalTest):
self.assertEqual([], response['actions'])
def test_get_one(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
response = self.get_json('/actions/%s' % action['uuid'])
self.assertEqual(action.uuid, response['uuid'])
self.assertEqual(action.action_type, response['action_type'])
@@ -92,7 +92,7 @@ class TestListAction(api_base.FunctionalTest):
self._assert_action_fields(response)
def test_get_one_soft_deleted(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
action.soft_delete()
response = self.get_json('/actions/%s' % action['uuid'],
headers={'X-Show-Deleted': 'True'})
@@ -104,13 +104,13 @@ class TestListAction(api_base.FunctionalTest):
self.assertEqual(404, response.status_int)
def test_detail(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
response = self.get_json('/actions/detail')
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
self._assert_action_fields(response['actions'][0])
def test_detail_soft_deleted(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
action.soft_delete()
response = self.get_json('/actions/detail',
headers={'X-Show-Deleted': 'True'})
@@ -121,7 +121,7 @@ class TestListAction(api_base.FunctionalTest):
self.assertEqual([], response['actions'])
def test_detail_against_single(self):
action = obj_utils.create_test_action(self.context, next=None)
action = obj_utils.create_test_action(self.context, parents=None)
response = self.get_json('/actions/%s/detail' % action['uuid'],
expect_errors=True)
self.assertEqual(404, response.status_int)
@@ -312,18 +312,23 @@ class TestListAction(api_base.FunctionalTest):
set([act['uuid'] for act in response['actions']
if act['action_plan_uuid'] == action_plan2.uuid]))
def test_many_with_next_uuid(self):
def test_many_with_parents(self):
action_list = []
for id_ in range(5):
action = obj_utils.create_test_action(self.context, id=id_,
uuid=utils.generate_uuid(),
next=id_ + 1)
if id_ > 0:
action = obj_utils.create_test_action(
self.context, id=id_, uuid=utils.generate_uuid(),
parents=[action_list[id_ - 1]])
else:
action = obj_utils.create_test_action(
self.context, id=id_, uuid=utils.generate_uuid(),
parents=[])
action_list.append(action.uuid)
response = self.get_json('/actions')
response_actions = response['actions']
for id_ in range(4):
self.assertEqual(response_actions[id_]['next_uuid'],
response_actions[id_ + 1]['uuid'])
self.assertEqual(response_actions[id_]['uuid'],
response_actions[id_ + 1]['parents'][0])
def test_many_without_soft_deleted(self):
action_list = []
@@ -357,30 +362,6 @@ class TestListAction(api_base.FunctionalTest):
uuids = [s['uuid'] for s in response['actions']]
self.assertEqual(sorted(action_list), sorted(uuids))
def test_many_with_sort_key_next_uuid(self):
for id_ in range(5):
obj_utils.create_test_action(self.context, id=id_,
uuid=utils.generate_uuid(),
next=id_ + 1)
response = self.get_json('/actions/')
reference_uuids = [
s.get('next_uuid', '') for s in response['actions']
]
response = self.get_json('/actions/?sort_key=next_uuid')
self.assertEqual(5, len(response['actions']))
uuids = [(s['next_uuid'] if 'next_uuid' in s else '')
for s in response['actions']]
self.assertEqual(sorted(reference_uuids), uuids)
response = self.get_json('/actions/?sort_key=next_uuid&sort_dir=desc')
self.assertEqual(5, len(response['actions']))
uuids = [(s['next_uuid'] if 'next_uuid' in s else '')
for s in response['actions']]
self.assertEqual(sorted(reference_uuids, reverse=True), uuids)
def test_links(self):
uuid = utils.generate_uuid()
obj_utils.create_test_action(self.context, id=1, uuid=uuid)
@@ -393,18 +374,15 @@ class TestListAction(api_base.FunctionalTest):
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
def test_collection_links(self):
next = -1
parents = None
for id_ in range(5):
action = obj_utils.create_test_action(self.context, id=id_,
uuid=utils.generate_uuid(),
next=next)
next = action.id
parents=parents)
parents = [action.id]
response = self.get_json('/actions/?limit=3')
self.assertEqual(3, len(response['actions']))
next_marker = response['actions'][-1]['uuid']
self.assertIn(next_marker, response['next'])
def test_collection_links_default_limit(self):
cfg.CONF.set_override('max_limit', 3, 'api',
enforce_type=True)
@@ -414,9 +392,6 @@ class TestListAction(api_base.FunctionalTest):
response = self.get_json('/actions')
self.assertEqual(3, len(response['actions']))
next_marker = response['actions'][-1]['uuid']
self.assertIn(next_marker, response['next'])
class TestPatch(api_base.FunctionalTest):
@@ -426,7 +401,7 @@ class TestPatch(api_base.FunctionalTest):
obj_utils.create_test_strategy(self.context)
obj_utils.create_test_audit(self.context)
obj_utils.create_test_action_plan(self.context)
self.action = obj_utils.create_test_action(self.context, next=None)
self.action = obj_utils.create_test_action(self.context, parents=None)
p = mock.patch.object(db_api.BaseConnection, 'update_action')
self.mock_action_update = p.start()
self.mock_action_update.side_effect = self._simulate_rpc_action_update
@@ -461,7 +436,7 @@ class TestDelete(api_base.FunctionalTest):
self.strategy = obj_utils.create_test_strategy(self.context)
self.audit = obj_utils.create_test_audit(self.context)
self.action_plan = obj_utils.create_test_action_plan(self.context)
self.action = obj_utils.create_test_action(self.context, next=None)
self.action = obj_utils.create_test_action(self.context, parents=None)
p = mock.patch.object(db_api.BaseConnection, 'update_action')
self.mock_action_update = p.start()
self.mock_action_update.side_effect = self._simulate_rpc_action_update

View File

@@ -77,14 +77,6 @@ class TestListActionPlan(api_base.FunctionalTest):
'unit': '%'}],
response['efficacy_indicators'])
def test_get_one_with_first_action(self):
action_plan = obj_utils.create_test_action_plan(self.context)
action = obj_utils.create_test_action(self.context, id=1)
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
self.assertEqual(action_plan.uuid, response['uuid'])
self.assertEqual(action.uuid, response['first_action_uuid'])
self._assert_action_plans_fields(response)
def test_get_one_soft_deleted(self):
action_plan = obj_utils.create_test_action_plan(self.context)
action_plan.soft_delete()
@@ -322,7 +314,7 @@ class TestDelete(api_base.FunctionalTest):
def test_delete_action_plan_with_action(self):
action = obj_utils.create_test_action(
self.context, id=self.action_plan.first_action_id)
self.context, id=1)
self.delete('/action_plans/%s' % self.action_plan.uuid)
ap_response = self.get_json('/action_plans/%s' % self.action_plan.uuid,

View File

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

View File

@@ -1,4 +1,3 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
# -*- encoding: 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
#
# 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 __future__ import unicode_literals
import mock
import voluptuous
from watcher.applier.actions import base as baction
from watcher.applier.actions import resize
from watcher.common import clients
from watcher.common import nova_helper
from watcher.tests import base
class TestResize(base.TestCase):
INSTANCE_UUID = "94ae2f92-b7fd-4da7-9e97-f13504ae98c4"
def setUp(self):
super(TestResize, self).setUp()
self.r_osc_cls = mock.Mock()
self.r_helper_cls = mock.Mock()
self.r_helper = mock.Mock(spec=nova_helper.NovaHelper)
self.r_helper_cls.return_value = self.r_helper
self.r_osc = mock.Mock(spec=clients.OpenStackClients)
self.r_osc_cls.return_value = self.r_osc
r_openstack_clients = mock.patch.object(
clients, "OpenStackClients", self.r_osc_cls)
r_nova_helper = mock.patch.object(
nova_helper, "NovaHelper", self.r_helper_cls)
r_openstack_clients.start()
r_nova_helper.start()
self.addCleanup(r_openstack_clients.stop)
self.addCleanup(r_nova_helper.stop)
self.input_parameters = {
"flavor": "x1",
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
}
self.action = resize.Resize(mock.Mock())
self.action.input_parameters = self.input_parameters
def test_parameters(self):
params = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
self.action.FLAVOR: 'x1'}
self.action.input_parameters = params
self.assertTrue(self.action.validate_parameters())
def test_parameters_exception_empty_fields(self):
parameters = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
self.action.FLAVOR: None}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual([(['flavor'], voluptuous.TypeInvalid)],
[(e.path, type(e)) for e in exc.errors])
def test_parameters_exception_flavor(self):
parameters = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
self.action.FLAVOR: None}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual(
[(['flavor'], voluptuous.TypeInvalid)],
[(e.path, type(e)) for e in exc.errors])
def test_parameters_exception_resource_id(self):
parameters = {baction.BaseAction.RESOURCE_ID: "EFEF",
self.action.FLAVOR: 'x1'}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual(
[(['resource_id'], voluptuous.Invalid)],
[(e.path, type(e)) for e in exc.errors])
def test_execute_resize(self):
self.r_helper.find_instance.return_value = self.INSTANCE_UUID
self.action.execute()
self.r_helper.resize_instance.assert_called_once_with(
instance_id=self.INSTANCE_UUID, flavor='x1')

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.

View File

@@ -1,4 +1,3 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>

View File

@@ -59,6 +59,7 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
config=mock.Mock(),
context=self.context,
applier_manager=mock.MagicMock())
self.engine.config.max_workers = 2
@mock.patch('taskflow.engines.load')
@mock.patch('taskflow.patterns.graph_flow.Flow.link')
@@ -70,14 +71,15 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc:
self.fail(exc)
def create_action(self, action_type, parameters, next):
def create_action(self, action_type, parameters, parents, uuid=None):
action = {
'uuid': utils.generate_uuid(),
'uuid': uuid or utils.generate_uuid(),
'action_plan_id': 0,
'action_type': action_type,
'input_parameters': parameters,
'state': objects.action.State.PENDING,
'next': next,
'parents': parents,
}
new_action = objects.Action(self.context, **action)
new_action.create()
@@ -113,10 +115,89 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc:
self.fail(exc)
def test_execute_nop_sleep(self):
actions = []
first_nop = self.create_action("nop", {'message': 'test'}, [])
second_nop = self.create_action("nop", {'message': 'second test'}, [])
sleep = self.create_action("sleep", {'duration': 0.0},
[first_nop.uuid, second_nop.uuid])
actions.extend([first_nop, second_nop, sleep])
try:
self.engine.execute(actions)
self.check_actions_state(actions, objects.action.State.SUCCEEDED)
except Exception as exc:
self.fail(exc)
def test_execute_with_parents(self):
actions = []
first_nop = self.create_action(
"nop", {'message': 'test'}, [],
uuid='bc7eee5c-4fbe-4def-9744-b539be55aa19')
second_nop = self.create_action(
"nop", {'message': 'second test'}, [],
uuid='0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23')
first_sleep = self.create_action(
"sleep", {'duration': 0.0}, [first_nop.uuid, second_nop.uuid],
uuid='be436531-0da3-4dad-a9c0-ea1d2aff6496')
second_sleep = self.create_action(
"sleep", {'duration': 0.0}, [first_sleep.uuid],
uuid='9eb51e14-936d-4d12-a500-6ba0f5e0bb1c')
actions.extend([first_nop, second_nop, first_sleep, second_sleep])
expected_nodes = [
{'uuid': 'bc7eee5c-4fbe-4def-9744-b539be55aa19',
'input_parameters': {u'message': u'test'},
'action_plan_id': 0, 'state': u'PENDING', 'parents': [],
'action_type': u'nop', 'id': 1},
{'uuid': '0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
'input_parameters': {u'message': u'second test'},
'action_plan_id': 0, 'state': u'PENDING', 'parents': [],
'action_type': u'nop', 'id': 2},
{'uuid': 'be436531-0da3-4dad-a9c0-ea1d2aff6496',
'input_parameters': {u'duration': 0.0},
'action_plan_id': 0, 'state': u'PENDING',
'parents': [u'bc7eee5c-4fbe-4def-9744-b539be55aa19',
u'0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23'],
'action_type': u'sleep', 'id': 3},
{'uuid': '9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
'input_parameters': {u'duration': 0.0},
'action_plan_id': 0, 'state': u'PENDING',
'parents': [u'be436531-0da3-4dad-a9c0-ea1d2aff6496'],
'action_type': u'sleep', 'id': 4}]
expected_edges = [
('action_type:nop uuid:0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496'),
('action_type:nop uuid:bc7eee5c-4fbe-4def-9744-b539be55aa19',
'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496'),
('action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496',
'action_type:sleep uuid:9eb51e14-936d-4d12-a500-6ba0f5e0bb1c')]
try:
flow = self.engine.execute(actions)
actual_nodes = sorted([x[0]._db_action.as_dict()
for x in flow.iter_nodes()],
key=lambda x: x['id'])
for expected, actual in zip(expected_nodes, actual_nodes):
for key in expected.keys():
self.assertIn(expected[key], actual.values())
actual_edges = [(u.name, v.name)
for (u, v, _) in flow.iter_links()]
for edge in expected_edges:
self.assertIn(edge, actual_edges)
self.check_actions_state(actions, objects.action.State.SUCCEEDED)
except Exception as exc:
self.fail(exc)
def test_execute_with_two_actions(self):
actions = []
second = self.create_action("sleep", {'duration': 0.0}, None)
first = self.create_action("nop", {'message': 'test'}, second.id)
first = self.create_action("nop", {'message': 'test'}, None)
actions.append(first)
actions.append(second)
@@ -132,8 +213,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
actions = []
third = self.create_action("nop", {'message': 'next'}, None)
second = self.create_action("sleep", {'duration': 0.0}, third.id)
first = self.create_action("nop", {'message': 'hello'}, second.id)
second = self.create_action("sleep", {'duration': 0.0}, None)
first = self.create_action("nop", {'message': 'hello'}, None)
self.check_action_state(first, objects.action.State.PENDING)
self.check_action_state(second, objects.action.State.PENDING)
@@ -154,8 +235,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
actions = []
third = self.create_action("no_exist", {'message': 'next'}, None)
second = self.create_action("sleep", {'duration': 0.0}, third.id)
first = self.create_action("nop", {'message': 'hello'}, second.id)
second = self.create_action("sleep", {'duration': 0.0}, None)
first = self.create_action("nop", {'message': 'hello'}, None)
self.check_action_state(first, objects.action.State.PENDING)
self.check_action_state(second, objects.action.State.PENDING)

View File

@@ -17,38 +17,31 @@ from cinderclient.v1 import client as ciclient_v1
from glanceclient import client as glclient
from keystoneauth1 import loading as ka_loading
import mock
from monascaclient import client as monclient
from monascaclient.v2_0 import client as monclient_v2
from neutronclient.neutron import client as netclient
from neutronclient.v2_0 import client as netclient_v2
from novaclient import client as nvclient
from oslo_config import cfg
from watcher.common import clients
from watcher import conf
from watcher.tests import base
CONF = conf.CONF
class TestClients(base.TestCase):
def setUp(self):
super(TestClients, self).setUp()
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='nova_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='glance_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='cinder_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='ceilometer_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='neutron_client')
def test_get_keystone_session(self):
def _register_watcher_clients_auth_opts(self):
_AUTH_CONF_GROUP = 'watcher_clients_auth'
ka_loading.register_auth_conf_options(cfg.CONF, _AUTH_CONF_GROUP)
ka_loading.register_session_conf_options(cfg.CONF, _AUTH_CONF_GROUP)
ka_loading.register_auth_conf_options(CONF, _AUTH_CONF_GROUP)
ka_loading.register_session_conf_options(CONF, _AUTH_CONF_GROUP)
CONF.set_override('auth_type', 'password', group=_AUTH_CONF_GROUP)
cfg.CONF.set_override('auth_type', 'password',
group=_AUTH_CONF_GROUP)
# ka_loading.load_auth_from_conf_options(CONF, _AUTH_CONF_GROUP)
# ka_loading.load_session_from_conf_options(CONF, _AUTH_CONF_GROUP)
# CONF.set_override(
# 'auth-url', 'http://server.ip:35357', group=_AUTH_CONF_GROUP)
# If we don't clean up the _AUTH_CONF_GROUP conf options, then other
# tests that run after this one will fail, complaining about required
@@ -56,27 +49,29 @@ class TestClients(base.TestCase):
def cleanup_conf_from_loading():
# oslo_config doesn't seem to allow unregistering groups through a
# single method, so we do this instead
cfg.CONF.reset()
del cfg.CONF._groups[_AUTH_CONF_GROUP]
CONF.reset()
del CONF._groups[_AUTH_CONF_GROUP]
self.addCleanup(cleanup_conf_from_loading)
osc = clients.OpenStackClients()
expected = {'username': 'foousername',
'password': 'foopassword',
'auth_url': 'http://server.ip:35357',
'user_domain_id': 'foouserdomainid',
'project_domain_id': 'fooprojdomainid'}
def reset_register_opts_mock(conf_obj, original_method):
conf_obj.register_opts = original_method
original_register_opts = cfg.CONF.register_opts
original_register_opts = CONF.register_opts
self.addCleanup(reset_register_opts_mock,
cfg.CONF,
CONF,
original_register_opts)
expected = {'username': 'foousername',
'password': 'foopassword',
'auth_url': 'http://server.ip:35357',
'cafile': None,
'certfile': None,
'keyfile': None,
'insecure': False,
'user_domain_id': 'foouserdomainid',
'project_domain_id': 'fooprojdomainid'}
# Because some of the conf options for auth plugins are not registered
# until right before they are loaded, and because the method that does
# the actual loading of the conf option values is an anonymous method
@@ -88,10 +83,21 @@ class TestClients(base.TestCase):
ret = original_register_opts(*args, **kwargs)
if 'group' in kwargs and kwargs['group'] == _AUTH_CONF_GROUP:
for key, value in expected.items():
cfg.CONF.set_override(key, value, group=_AUTH_CONF_GROUP)
CONF.set_override(key, value, group=_AUTH_CONF_GROUP)
return ret
cfg.CONF.register_opts = mock_register_opts
CONF.register_opts = mock_register_opts
def test_get_keystone_session(self):
self._register_watcher_clients_auth_opts()
osc = clients.OpenStackClients()
expected = {'username': 'foousername',
'password': 'foopassword',
'auth_url': 'http://server.ip:35357',
'user_domain_id': 'foouserdomainid',
'project_domain_id': 'fooprojdomainid'}
sess = osc.session
self.assertEqual(expected['auth_url'], sess.auth.auth_url)
@@ -107,13 +113,12 @@ class TestClients(base.TestCase):
osc = clients.OpenStackClients()
osc._nova = None
osc.nova()
mock_call.assert_called_once_with(cfg.CONF.nova_client.api_version,
mock_call.assert_called_once_with(CONF.nova_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_nova_diff_vers(self, mock_session):
cfg.CONF.set_override('api_version', '2.3',
group='nova_client')
CONF.set_override('api_version', '2.3', group='nova_client')
osc = clients.OpenStackClients()
osc._nova = None
osc.nova()
@@ -133,13 +138,12 @@ class TestClients(base.TestCase):
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
mock_call.assert_called_once_with(cfg.CONF.glance_client.api_version,
mock_call.assert_called_once_with(CONF.glance_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance_diff_vers(self, mock_session):
cfg.CONF.set_override('api_version', '1',
group='glance_client')
CONF.set_override('api_version', '1', group='glance_client')
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
@@ -159,13 +163,12 @@ class TestClients(base.TestCase):
osc = clients.OpenStackClients()
osc._cinder = None
osc.cinder()
mock_call.assert_called_once_with(cfg.CONF.cinder_client.api_version,
mock_call.assert_called_once_with(CONF.cinder_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_cinder_diff_vers(self, mock_session):
cfg.CONF.set_override('api_version', '1',
group='cinder_client')
CONF.set_override('api_version', '1', group='cinder_client')
osc = clients.OpenStackClients()
osc._cinder = None
osc.cinder()
@@ -186,18 +189,18 @@ class TestClients(base.TestCase):
osc._ceilometer = None
osc.ceilometer()
mock_call.assert_called_once_with(
cfg.CONF.ceilometer_client.api_version,
CONF.ceilometer_client.api_version,
None,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_alarm_client')
def test_clients_ceilometer_diff_vers(self, mock_get_alarm_client,
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
def test_clients_ceilometer_diff_vers(self, mock_get_redirect_client,
mock_session):
'''ceilometerclient currently only has one version (v2)'''
mock_get_alarm_client.return_value = [mock.Mock(), mock.Mock()]
cfg.CONF.set_override('api_version', '2',
group='ceilometer_client')
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
CONF.set_override('api_version', '2',
group='ceilometer_client')
osc = clients.OpenStackClients()
osc._ceilometer = None
osc.ceilometer()
@@ -205,10 +208,10 @@ class TestClients(base.TestCase):
type(osc.ceilometer()))
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_alarm_client')
def test_clients_ceilometer_cached(self, mock_get_alarm_client,
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
def test_clients_ceilometer_cached(self, mock_get_redirect_client,
mock_session):
mock_get_alarm_client.return_value = [mock.Mock(), mock.Mock()]
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
osc = clients.OpenStackClients()
osc._ceilometer = None
ceilometer = osc.ceilometer()
@@ -221,14 +224,14 @@ class TestClients(base.TestCase):
osc = clients.OpenStackClients()
osc._neutron = None
osc.neutron()
mock_call.assert_called_once_with(cfg.CONF.neutron_client.api_version,
mock_call.assert_called_once_with(CONF.neutron_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_neutron_diff_vers(self, mock_session):
'''neutronclient currently only has one version (v2)'''
cfg.CONF.set_override('api_version', '2.0',
group='neutron_client')
CONF.set_override('api_version', '2.0',
group='neutron_client')
osc = clients.OpenStackClients()
osc._neutron = None
osc.neutron()
@@ -242,3 +245,51 @@ class TestClients(base.TestCase):
neutron = osc.neutron()
neutron_cached = osc.neutron()
self.assertEqual(neutron, neutron_cached)
@mock.patch.object(monclient, 'Client')
@mock.patch.object(ka_loading, 'load_session_from_conf_options')
def test_clients_monasca(self, mock_session, mock_call):
mock_session.return_value = mock.Mock(
get_endpoint=mock.Mock(return_value='test_endpoint'),
get_token=mock.Mock(return_value='test_token'),)
self._register_watcher_clients_auth_opts()
osc = clients.OpenStackClients()
osc._monasca = None
osc.monasca()
mock_call.assert_called_once_with(
CONF.monasca_client.api_version,
'test_endpoint',
auth_url='http://server.ip:35357', cert_file=None, insecure=False,
key_file=None, keystone_timeout=None, os_cacert=None,
password='foopassword', service_type='monitoring',
token='test_token', username='foousername')
@mock.patch.object(ka_loading, 'load_session_from_conf_options')
def test_clients_monasca_diff_vers(self, mock_session):
mock_session.return_value = mock.Mock(
get_endpoint=mock.Mock(return_value='test_endpoint'),
get_token=mock.Mock(return_value='test_token'),)
self._register_watcher_clients_auth_opts()
CONF.set_override('api_version', '2_0', group='monasca_client')
osc = clients.OpenStackClients()
osc._monasca = None
osc.monasca()
self.assertEqual(monclient_v2.Client, type(osc.monasca()))
@mock.patch.object(ka_loading, 'load_session_from_conf_options')
def test_clients_monasca_cached(self, mock_session):
mock_session.return_value = mock.Mock(
get_endpoint=mock.Mock(return_value='test_endpoint'),
get_token=mock.Mock(return_value='test_token'),)
self._register_watcher_clients_auth_opts()
osc = clients.OpenStackClients()
osc._monasca = None
monasca = osc.monasca()
monasca_cached = osc.monasca()
self.assertEqual(monasca, monasca_cached)

View File

@@ -38,6 +38,7 @@ class TestNovaHelper(base.TestCase):
self.instance_uuid = "fb5311b7-37f3-457e-9cde-6494a3c59bfe"
self.source_node = "ldev-indeedsrv005"
self.destination_node = "ldev-indeedsrv006"
self.flavor_name = "x1"
@staticmethod
def fake_server(*args, **kwargs):
@@ -89,6 +90,22 @@ class TestNovaHelper(base.TestCase):
result = nova_util.set_host_offline("rennes")
self.assertFalse(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_resize_instance(self, mock_glance, mock_cinder,
mock_neutron, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
setattr(server, 'status', 'VERIFY_RESIZE')
self.fake_nova_find_list(nova_util, find=server, list=server)
is_success = nova_util.resize_instance(self.instance_uuid,
self.flavor_name)
self.assertTrue(is_success)
setattr(server, 'status', 'SOMETHING_ELSE')
is_success = nova_util.resize_instance(self.instance_uuid,
self.flavor_name)
self.assertFalse(is_success)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_live_migrate_instance(self, mock_glance, mock_cinder,
mock_neutron, mock_nova):

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