Compare commits

..

81 Commits

Author SHA1 Message Date
Jenkins
3d398b4d22 Merge "Documentation for plugins-parameters" 2016-05-30 09:41:53 +00:00
Jenkins
585fbeb9ee Merge "Watcher plugins table in Guru meditation reports" 2016-05-30 09:41:49 +00:00
Jenkins
e9f237dc80 Merge "Enabled config parameters to plugins" 2016-05-30 09:40:30 +00:00
Vincent Françoise
38f6700144 Documentation for plugins-parameters
In this changeset, I updated the documentation to explain how to
add configuration options for each type of plugin.

Partially Implements: plugins-parameters

Change-Id: Ifd373da64207110492b4a62f1cb7f13b029a45d2
2016-05-30 10:28:29 +02:00
junjie huang
30bdf29002 Workload balance migration strategy implementation
This is one of the algorithm of Intel thermal POC.
It's based on the VM workloads of hypervisors.

Change-Id: I45ab0cf0f05786e6f68025bdd315f38381900a68
blueprint: workload-balance-migration-strategy
2016-05-30 07:52:05 +00:00
Vincent Françoise
e6f147d81d Watcher plugins table in Guru meditation reports
In this changeset, I added the list of all the available plugins
for the current instance of any given Watcher service.

Partially Implements: blueprint plugins-parameters

Change-Id: I58c9724a229712b0322a578f0f89a61b38dfd80a
2016-05-30 09:48:37 +02:00
Vincent Françoise
5aa6b16238 Enabled config parameters to plugins
In this changeset, I added the possibility for all plugins to define
configuration parameters for themselves.

Partially Implements: blueprint plugins-parameters

Change-Id: I676b2583b3b4841c64c862b2b0c234b4eb5fd0fd
2016-05-30 09:48:34 +02:00
Jenkins
dcb5c1f9fc Merge "Add Overload standard deviation strategy" 2016-05-30 07:10:17 +00:00
Jenkins
4ba01cbbcf Merge "Update Watcher documentation" 2016-05-27 15:58:01 +00:00
Jenkins
d91d72d2c2 Merge "Add goal name as filter for strategy list cmd" 2016-05-27 15:51:22 +00:00
Jenkins
083bc2bed4 Merge "Add goal_name & strategy_name in /audit_templates" 2016-05-27 15:51:14 +00:00
Alexander Chadin
9d3671af37 Add Overload standard deviation strategy
The main purpose of this strategy is to choose the pair VM:dest_host that
minimizes the standard deviation in a cluster best.

Change-Id: I95a31b7bcab83411ef6b6e1e01818ca21ef96883
Implements: blueprint watcher-overload-sd
2016-05-27 16:16:36 +03:00
Jenkins
3b88e37680 Merge "Added cold VM migration support" 2016-05-27 12:33:46 +00:00
David TARDIVEL
6eee64502f Add goal name as filter for strategy list cmd
This changeset add the possibility to use the goal name as a
stragegy list filter.

Change-Id: Ibaf45e694f115308f19e9bcd3023fe2e6d1750cd
2016-05-27 11:20:55 +02:00
David TARDIVEL
b9231f65cc Update Watcher documentation
We introduced a new watcher plugin for OpenStack CLI. This patchset
updates accordingly the watcher documentation and schemas.

Partially Implements: blueprint openstackclient-plugin

Change-Id: Ib00469c8645fff21f5ba95951379827dbd359c69
2016-05-27 09:40:06 +02:00
OpenStack Proposal Bot
b9b505a518 Updated from global requirements
Change-Id: Ia90ce890fac40ddb6d38effd022ca71e9a7fc52f
2016-05-26 17:07:34 +00:00
cima
388ef9f11c Added cold VM migration support
Cold migration enables migrating some of the VMs which are not in active state (e.g. stopped). Cold migration can also be used for migrating active VM, although VM is shut down and hence unaccessible while migrating.

Change-Id: I89ad0a04d41282431c9773f6ae7feb41573368e3
Closes-Bug: #1564297
2016-05-24 13:26:45 +02:00
Jenkins
4e3caaa157 Merge "Fixed flaky tempest test" 2016-05-24 09:25:15 +00:00
Vincent Françoise
8c6bf734af Add goal_name & strategy_name in /audit_templates
In this changeset, I added both the 'goal_name' and the 'strategy_name'
field.

Change-Id: Ic164df84d4e23ec75b2b2f4b358cf827d0ad7fa5
Related-Bug: #1573582
2016-05-24 11:09:48 +02:00
Vincent Françoise
277a749ca0 Fix lazy translation issue with watcher-db-manage
In this changeset, I fix the issue caused by the use of lazy
translations within the 'watcher-db-manage purge' subcommand.
This is caused by the PrettyTable dependency which performs
addition operations to format its tables and the __add__ magic
method is not supported by oslo_i18n._message.Message objects.

Change-Id: Idd590e882c697957cfaf1849c3d51b52797230f6
Closes-Bug: #1584652
2016-05-23 14:35:17 +02:00
Vincent Françoise
8401b5e479 Fixed flaky tempest test
In this changeset, I fixed the test_create_audit_with_no_state
tempest test which was randomly failing because of a race condition.

Change-Id: Ibda49944c79fcd406fa81870dbbff6064b5dc4fa
2016-05-23 14:32:44 +02:00
Vincent Françoise
78689fbe3b Removed telemetry tag from tempest tests
Since telemetry was removed from tempest, this changeset removes the
telemetry tags from the watcher integration tests

Change-Id: I6229ee23740c3d92a66fc04c8de8b0ed25911022
2016-05-23 09:22:31 +00:00
OpenStack Proposal Bot
22abaa9c3a Updated from global requirements
Change-Id: I2506d35432748691fb53f8540aac43d1656a67a3
2016-05-21 15:53:55 +00:00
Alexander Chadin
fb82131d85 Fix for statistic_aggregation
This patch set fixes aggregate parameter for statistic_aggregation
function so we may use it with any aggregate function.

Change-Id: If586d656aadd3d098a1610a97a2f315e70351de5
Closes-Bug: #1583610
2016-05-19 16:41:07 +03:00
Jenkins
f6f5079adb Merge "Watcher DB class diagram" 2016-05-18 12:36:17 +00:00
Jenkins
f045f5d816 Merge "Updated from global requirements" 2016-05-13 07:06:35 +00:00
Jenkins
b77541deb2 Merge "[nova_helper] get keypair name by every admin users" 2016-05-13 06:33:42 +00:00
OpenStack Proposal Bot
3b9d72439c Updated from global requirements
Change-Id: I0f4fe97bdfa872074964a10535db868354d926da
2016-05-11 17:29:55 +00:00
Jenkins
89aa2d54df Merge "Added .pot file" 2016-05-11 15:39:13 +00:00
Jenkins
86f4cee588 Merge "Remove [watcher_goals] config section" 2016-05-11 15:32:36 +00:00
Jenkins
04ac509821 Merge "Remove watcher_goals section from devstack plugin" 2016-05-11 15:32:35 +00:00
Jenkins
4ba9d2cb73 Merge "Documentation update for get-goal-from-strategy" 2016-05-11 15:32:28 +00:00
Jenkins
a71c9be860 Merge "Updated purge to now include goals and strategies" 2016-05-11 15:32:22 +00:00
Jenkins
c2cb1a1f8e Merge "Syncer now syncs stale audit templates" 2016-05-11 15:32:17 +00:00
Jenkins
79bdcf7baf Merge "Add strategy_id & goal_id fields in audit template" 2016-05-11 15:32:10 +00:00
Jenkins
de1b1a9938 Merge "Refactored Strategy selector to select from DB" 2016-05-11 15:32:07 +00:00
Jenkins
031ebdecde Merge "Added /strategies endpoint in Watcher API" 2016-05-11 15:32:01 +00:00
Jenkins
daabe671c7 Merge "Add Goal in BaseStrategy + Goal API reads from DB" 2016-05-11 15:31:58 +00:00
Jenkins
26bc3d139d Merge "DB sync for Strategies" 2016-05-11 15:31:39 +00:00
Jenkins
4d2536b9b2 Merge "Added Strategy model" 2016-05-11 15:30:46 +00:00
Jenkins
4388780e66 Merge "Added Goal object + goal syncing" 2016-05-11 15:29:21 +00:00
Jenkins
d03a9197b0 Merge "Added Goal model into Watcher DB" 2016-05-11 15:28:28 +00:00
Jean-Emile DARTOIS
43f5ab18ba Fix documentation watcher sql database
This changeset fixes the issue with the parameter watcher-db-manage

Change-Id: I668edd85e3ea40c2a309caacbf68cf35bfd680f7
Closes-Bug: #1580617
2016-05-11 15:58:46 +02:00
Vincent Françoise
209176c3d7 Watcher DB class diagram
In this changeset, I added a class diagram reprensenting the
database schema of Watcher.

Change-Id: I2257010d0040a3f40279ec9db2967f0e69384b62
2016-05-11 15:52:54 +02:00
Vincent Françoise
1a21867735 Added .pot file
In this changeset, I just generate the .pot file for all the new
translations that were added during the implementation of this BP

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I2192508afda037510f8f91092c5cfde0115dae1d
2016-05-11 15:48:09 +02:00
Vincent Françoise
5f6a97148f Remove [watcher_goals] config section
In this changeset, I remove the now unused [watcher_goals] section.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I91e4e1ac3a58bb6f3e30b11449cf1a6eb18cd0ca
2016-05-11 15:48:09 +02:00
Vincent Françoise
e6b23a0856 Remove watcher_goals section from devstack plugin
In this changeset, I removed the now useless [watcher_goals] section
from the devstack plugin.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Iaa986f426dc47f6cbd04e74f16b67670e3563967
2016-05-11 15:48:09 +02:00
Vincent Françoise
f9a1b9d3ce Documentation update for get-goal-from-strategy
In this changeset, I updated the Watcher documentation to reflect
the changes that are introduced by this blueprint.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I40be39624097365220bf7d94cbe177bbf5bbe0ed
2016-05-11 15:48:02 +02:00
Vincent Françoise
ff611544fb Updated purge to now include goals and strategies
In this changeset, I updated the purge script to now take into
account the registered goals and strategies.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I2f1d58bb812fa45bc4bc6467760a071d8612e6a4
2016-05-11 15:31:02 +02:00
Vincent Françoise
18e5c7d844 Syncer now syncs stale audit templates
In this changeset, I introduce the syncing of audit templates.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ie394c12fe51f73eff95465fd5140d82ebd212599
2016-05-11 15:31:02 +02:00
Vincent Françoise
2966b93777 Add strategy_id & goal_id fields in audit template
In this changeset, I updated the 'goal_id' field into the AuditTemplate
to now become a mandatory foreign key towards the Goal model. I also
added the 'strategy_id' field into the AuditTemplate model to be an
optional foreign key onto the Strategy model.

This changeset also includes an update of the /audit_template
Watcher API endpoint to reflect the previous changes.

As this changeset changes the API, this should be merged alongside the
related changeset from python-watcherclient.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ic0573d036d1bbd7820f8eb963e47912d6b3ed1a9
2016-05-11 15:31:02 +02:00
Vincent Françoise
e67b532110 Refactored Strategy selector to select from DB
In this changeset, I refactored the strategy selector to now
look into the Watcher DB instead of looking into the configuration
file.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I2bcb63542f6237f26796a3e5a781c8b62820cf6f
2016-05-11 15:31:01 +02:00
Vincent Françoise
81765b9aa5 Added /strategies endpoint in Watcher API
In this changeset, I added the /strategies endpoint to the Watcher
API service.
This also includes the related Tempest tests.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I1b70836e0df2082ab0016ecc207e89fdcb0fc8b9
2016-05-11 15:31:01 +02:00
Vincent Françoise
673642e436 Add Goal in BaseStrategy + Goal API reads from DB
In this changeset, I changed the Strategy base class to add new
abstract class methods. I also added an abstract strategy class
per Goal type (dummy, server consolidation, thermal optimization).

This changeset also includes an update of the /goals Watcher API
endpoint to now use the new Goal model (DB entries) instead of
reading from the configuration file.

Partially Implements: blueprint get-goal-from-strategy
Change-Id: Iecfed58c72f3f9df4e9d27e50a3a274a1fc0a75f
2016-05-11 15:31:00 +02:00
Jenkins
1026a896e2 Merge "Log "https" if using SSL" 2016-05-11 13:24:43 +00:00
Vincent Françoise
a3ac26870a DB sync for Strategies
In this changeset, I added the ability to synchronize the strategies
into the Wather DB so that it can later be served through the Watcher
API.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ifeaa1f6e1f4ff7d7efc1b221cf57797a49dc5bc5
2016-05-11 15:19:40 +02:00
Vincent Françoise
192d8e262c Added Strategy model
In this changeset, I add the Strategy model as well as the DB
functionalities we need to manipulate strategies.

This changeset implies a DB schema update.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I438a8788844fbc514edfe1e9e3136f46ba5a82f2
2016-05-11 15:19:40 +02:00
Vincent Françoise
3b5ef15db6 Added Goal object + goal syncing
In this changeset, I added the Goal object into Watcher along with
a sync module that is responsible for syncing the goals with the
Watcher DB.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ia3a2032dd9023d668c6f32ebbce44f8c1d77b0a3
2016-05-11 15:19:40 +02:00
Vincent Françoise
be9058f3e3 Added Goal model into Watcher DB
In this changeset, I added the Goal model into Watcher.
This implies a change into the Watcher DB schema

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I5b5b0ffc7cff8affb59f17743e1af0e1277c2878
2016-05-11 15:19:40 +02:00
Jenkins
91951f3b01 Merge "Refactored DE and Applier to use oslo.service" 2016-05-11 13:14:52 +00:00
Jenkins
57a2af2685 Merge "Refactored Watcher API service" 2016-05-11 13:14:29 +00:00
Yosef Hoffman
76e3d2e2f6 Log "https" if using SSL
When starting the Watcher API service, the URI it served to is shown in
a log message. In this log message (in watcher/cmd/api.py) take into
account the case where SSL has been enabled with CONF.api.enable_ssl_api
set to True and format this log message accordingly.

Change-Id: I98541810139d9d4319ac89f21a5e0bc25454ee62
Closes-Bug: #1580044
2016-05-10 11:44:56 -04:00
Jenkins
bd5a969a26 Merge "Remove using of UUID field in POST methods of Watcher API" 2016-04-29 02:57:34 +00:00
zhangguoqing
d61bf5f053 [nova_helper] get keypair name by every admin users
Since the bug #1182965 has been fixed, allow admin users
to view any keypair.

Change-Id: I9cf948701515afd45e6720cfd15cfac6b5866aa5
2016-04-25 21:20:16 +08:00
Alexander Chadin
aaaf3f1c84 Remove using of UUID field in POST methods of Watcher API
This patch set removes the possibility of using UUID field
in POST methods of Watcher API.

Closes-Bug: #1572625

Change-Id: I88a8aa5346e937e3e9409b55da3316cbe1ed832a
2016-04-25 16:05:59 +03:00
Vincent Françoise
eb861f86ab Refactored DE and Applier to use oslo.service
In this PS, I have refactored the Decision Engine and the Applier
to use the oslo service utility.

Change-Id: If29158cc9b5e5e50f6c69d67c232cceeb07084f2
Closes-Bug: #1541850
2016-04-22 10:33:21 +02:00
Vincent Françoise
a9e7251d0d Refactored Watcher API service
This patchset introduces the use of oslo.service to run the
Watcher API service.

Change-Id: I6c38a3c1a2b4dc47388876e4c0ba61b7447690bd
Related-Bug: #1541850
2016-04-22 10:33:21 +02:00
OpenStack Proposal Bot
4ff373197c Updated from global requirements
Change-Id: I04865b9e63d6fc805802b6057ba9750116849c98
2016-04-19 12:30:29 +00:00
Jenkins
87087e9add Merge "Removed unused 'alarm' field" 2016-04-19 08:02:21 +00:00
Larry Rensing
408d6d4650 Removed unused 'alarm' field
The 'alarm' field is currently unused, so it has been removed.

Change-Id: I02fa15b06ed49dbc5dd63de54a9cde601413983c
Closes-Bug: #1550261
2016-04-18 14:12:12 +00:00
Alexander Chadin
e52dc4f8aa Add parameters verification when Audit is being created
We have to check Audit Type and Audit State to make sure
these parameters are in valid status.

Also, we provide default states for the next attributes:

- 'audit_template' is required and should be either UUID or text field
- 'state' is readonly so it raises an error if submitted in POST
  and is set by default to PENDING
- 'deadline' is optional and should be a datetime
- 'type' is a required text field

Change-Id: I2a7e0deec0ee2040e86400b500bb0efd8eade564
Closes-Bug: #1532843
Closes-Bug: #1533210
2016-04-14 15:43:26 +03:00
Jenkins
0f14b7635d Merge "correct the available disk, memory calculating Source data are misused in outlet temperature strategy. This patch fixes it." 2016-04-12 07:08:15 +00:00
junjie huang
bb77641aad correct the available disk, memory calculating
Source data are misused in outlet temperature strategy. This patch
fixes it.

Change-Id: I8ad5c974d7674ddfe6c4c9e3a6e3029d34a400db
Closes-bug: #1569114
2016-04-11 17:53:54 +00:00
Vincent Françoise
77228a0b0a Upgrade Watcher Tempest tests for multinode
Change-Id: I4b84ba9814227776232c8ab883cdaaf411930ee6
2016-04-11 16:49:10 +02:00
Jenkins
1157a8db30 Merge "Fix for deleting audit template" 2016-04-08 18:58:07 +00:00
Jenkins
18354d1b4e Merge "Update .coveragerc to ignore abstract methods" 2016-04-08 18:57:11 +00:00
Larry Rensing
8387cd10de Update .coveragerc to ignore abstract methods
Due to importing modules rather than functions and decorators directly,
@abc.abstract and 'raise NotImplementedError' were added to the
.coveragerc file.  Since abstract methods are not testable, this will
give us a more accurate representation of our coverage.

Change-Id: Id5ed5e1f5e142d10f41ad18d20228399226ec20d
Co-Authored-By: Jin Li <jl7351@att.com>
Closes-Bug: #1563717
2016-04-08 16:59:57 +00:00
OpenStack Proposal Bot
0449bae747 Updated from global requirements
Change-Id: Ieead2a4c784c248bd6af821f5e1e84c5e6cd3b5a
2016-04-07 17:25:39 +00:00
Alexander Chadin
3e07844844 Fix for deleting audit template
We need to update sqlalchemy/api and sqlalchemy/models (and appropriate tests)
to support deleting audit templates and recreating them with the same names.

Change-Id: Icf54cf1ed989a3f2ad689e25be4474b16a3a3eb2
Related-Bug: #1510179
2016-04-07 11:27:53 +03:00
zhangguoqing
a52d92be87 Remove unused logging import and LOG global var
In some modules the global LOG is not used any more. And the import
of logging is not used. This patch removes the unused logging import
and LOG vars.

Change-Id: I794ee719d76f04e70154cf67f726152fbb1ba15a
2016-04-06 10:34:39 +08:00
OpenStack Proposal Bot
96683a6133 Updated from global requirements
Change-Id: Ib98e484bb216eb31b64931db735ced8d1de738a4
2016-04-05 13:44:07 +00:00
146 changed files with 7946 additions and 1666 deletions

View File

@@ -4,6 +4,7 @@ source = watcher
omit = watcher/tests/* omit = watcher/tests/*
[report] [report]
ignore_errors = True ignore_errors = True
exclude_lines = exclude_lines =
@abstract @abc.abstract
raise NotImplementedError

View File

@@ -42,7 +42,3 @@ LOGDAYS=2
[[post-config|$NOVA_CONF]] [[post-config|$NOVA_CONF]]
[DEFAULT] [DEFAULT]
compute_monitors=cpu.virt_driver compute_monitors=cpu.virt_driver
[[post-config|$WATCHER_CONF]]
[watcher_goals]
goals=BASIC_CONSOLIDATION:basic,DUMMY:dummy

View File

@@ -155,6 +155,7 @@ by the :ref:`Watcher API <archi_watcher_api_definition>` or the
- :ref:`Action plans <action_plan_definition>` - :ref:`Action plans <action_plan_definition>`
- :ref:`Actions <action_definition>` - :ref:`Actions <action_definition>`
- :ref:`Goals <goal_definition>` - :ref:`Goals <goal_definition>`
- :ref:`Strategies <strategy_definition>`
The Watcher domain being here "*optimization of some resources provided by an The Watcher domain being here "*optimization of some resources provided by an
OpenStack system*". OpenStack system*".
@@ -196,8 +197,6 @@ Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
which provides information about the past of the which provides information about the past of the
:ref:`Cluster <cluster_definition>` :ref:`Cluster <cluster_definition>`
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
given :ref:`Goal <goal_definition>` via the main Watcher configuration file.
.. _data_model: .. _data_model:
@@ -211,6 +210,14 @@ view (Goals, Audits, Action Plans, ...):
.. image:: ./images/functional_data_model.svg .. image:: ./images/functional_data_model.svg
:width: 100% :width: 100%
Here below is a class diagram representing the main objects in Watcher from a
database perspective:
.. image:: ./images/watcher_class_diagram.png
:width: 100%
.. _sequence_diagrams: .. _sequence_diagrams:
Sequence diagrams Sequence diagrams
@@ -230,13 +237,15 @@ following parameters:
- A name - A name
- A goal to achieve - A goal to achieve
- An optional strategy
.. image:: ./images/sequence_create_audit_template.png .. image:: ./images/sequence_create_audit_template.png
:width: 100% :width: 100%
The `Watcher API`_ just makes sure that the goal exists (i.e. it is declared The `Watcher API`_ makes sure that both the specified goal (mandatory) and
in the Watcher configuration file) and stores a new audit template in the its associated strategy (optional) are registered inside the :ref:`Watcher
:ref:`Watcher Database <watcher_database_definition>`. Database <watcher_database_definition>` before storing a new audit template in
the :ref:`Watcher Database <watcher_database_definition>`.
.. _sequence_diagrams_create_and_launch_audit: .. _sequence_diagrams_create_and_launch_audit:
@@ -260,12 +269,11 @@ the Audit in the
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads
the Audit parameters from the the Audit parameters from the
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the :ref:`Watcher Database <watcher_database_definition>`. It instantiates the
appropriate :ref:`Strategy <strategy_definition>` (using entry points) appropriate :ref:`strategy <strategy_definition>` (using entry points)
associated to the :ref:`Goal <goal_definition>` of the given both the :ref:`goal <goal_definition>` and the strategy associated to the
:ref:`Audit <audit_definition>` (it uses the information of the Watcher parent :ref:`audit template <audit_template_definition>` of the :ref:`Audit
configuration file to find the mapping between the <audit_definition>`. If no strategy is associated to the audit template, the
:ref:`Goal <goal_definition>` and the :ref:`Strategy <strategy_definition>` strategy is dynamically selected by the Decision Engine.
python class).
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This

View File

@@ -182,8 +182,6 @@ The configuration file is organized into the following sections:
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients * ``[watcher_clients_auth]`` - Keystone auth configuration for clients
* ``[watcher_applier]`` - Watcher Applier module configuration * ``[watcher_applier]`` - Watcher Applier module configuration
* ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration * ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration
* ``[watcher_goals]`` - Goals mapping configuration
* ``[watcher_strategies]`` - Strategy configuration
* ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration * ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration
* ``[ceilometer_client]`` - Ceilometer client configuration * ``[ceilometer_client]`` - Ceilometer client configuration
* ``[cinder_client]`` - Cinder client configuration * ``[cinder_client]`` - Cinder client configuration

52
doc/source/deploy/gmr.rst Normal file
View File

@@ -0,0 +1,52 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
.. _watcher_gmr:
=======================
Guru Meditation Reports
=======================
Watcher contains a mechanism whereby developers and system administrators can
generate a report about the state of a running Watcher service. This report
is called a *Guru Meditation Report* (*GMR* for short).
Generating a GMR
================
A *GMR* can be generated by sending the *USR2* signal to any Watcher process
with support (see below). The *GMR* will then be outputted as standard error
for that particular process.
For example, suppose that ``watcher-api`` has process id ``8675``, and was run
with ``2>/var/log/watcher/watcher-api-err.log``. Then, ``kill -USR2 8675``
will trigger the Guru Meditation report to be printed to
``/var/log/watcher/watcher-api-err.log``.
Structure of a GMR
==================
The *GMR* is designed to be extensible; any particular service may add its
own sections. However, the base *GMR* consists of several sections:
Package
Shows information about the package to which this process belongs, including
version informations.
Threads
Shows stack traces and thread ids for each of the threads within this
process.
Green Threads
Shows stack traces for each of the green threads within this process (green
threads don't have thread ids).
Configuration
Lists all the configuration options currently accessible via the CONF object
for the current process.
Plugins
Lists all the plugins currently accessible by the Watcher service.

View File

@@ -32,17 +32,17 @@ This guide assumes you have a working installation of Watcher. If you get
Please refer to the `installation guide`_. Please refer to the `installation guide`_.
In order to use Watcher, you have to configure your credentials suitable for In order to use Watcher, you have to configure your credentials suitable for
watcher command-line tools. watcher command-line tools.
If you need help on a specific command, you can use:
.. code:: bash You can interact with Watcher either by using our dedicated `Watcher CLI`_
named ``watcher``, or by using the `OpenStack CLI`_ ``openstack``.
$ watcher help COMMAND
If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon
plugin installation guide`_. plugin installation guide`_.
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient .. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
.. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html .. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html
.. _`OpenStack CLI`: http://docs.openstack.org/developer/python-openstackclient/man/openstack.html
.. _`Watcher CLI`: https://factory.b-com.com/www/watcher/doc/python-watcherclient/index.html
Seeing what the Watcher CLI can do ? Seeing what the Watcher CLI can do ?
------------------------------------ ------------------------------------
@@ -51,23 +51,66 @@ watcher binary without options.
.. code:: bash .. code:: bash
$ watcher $ watcher help
or::
$ openstack help optimize
How do I run an audit of my cluster ? How do I run an audit of my cluster ?
------------------------------------- -------------------------------------
First, you need to create an :ref:`audit template <audit_template_definition>`. First, you need to find the :ref:`goal <goal_definition>` you want to achieve:
An :ref:`audit template <audit_template_definition>` defines an optimization
:ref:`goal <goal_definition>` to achieve (i.e. the settings of your audit).
This goal should be declared in the Watcher service configuration file
**/etc/watcher/watcher.conf**.
.. code:: bash .. code:: bash
$ watcher audit-template-create my_first_audit DUMMY $ watcher goal list
If you get "*You must provide a username via either --os-username or via or::
env[OS_USERNAME]*" you may have to verify your credentials.
$ openstack optimize goal list
.. note::
If you get "*You must provide a username via either --os-username or via
env[OS_USERNAME]*" you may have to verify your credentials.
Then, you can create an :ref:`audit template <audit_template_definition>`.
An :ref:`audit template <audit_template_definition>` defines an optimization
:ref:`goal <goal_definition>` to achieve (i.e. the settings of your audit).
.. code:: bash
$ watcher audittemplate create my_first_audit_template <your_goal>
or::
$ openstack optimize audittemplate create my_first_audit_template <your_goal>
Although optional, you may want to actually set a specific strategy for your
audit template. If so, you may can search of its UUID or name using the
following command:
.. code:: bash
$ watcher strategy list --goal-uuid <your_goal_uuid>
or::
$ openstack optimize strategy list --goal-uuid <your_goal_uuid>
The command to create your audit template would then be:
.. code:: bash
$ watcher audittemplate create my_first_audit_template <your_goal> \
--strategy <your_strategy>
or::
$ openstack optimize audittemplate create my_first_audit_template <your_goal> \
--strategy <your_strategy>
Then, you can create an audit. An audit is a request for optimizing your Then, you can create an audit. An audit is a request for optimizing your
cluster depending on the specified :ref:`goal <goal_definition>`. cluster depending on the specified :ref:`goal <goal_definition>`.
@@ -76,19 +119,26 @@ You can launch an audit on your cluster by referencing the
:ref:`audit template <audit_template_definition>` (i.e. the settings of your :ref:`audit template <audit_template_definition>` (i.e. the settings of your
audit) that you want to use. audit) that you want to use.
- Get the :ref:`audit template <audit_template_definition>` UUID: - Get the :ref:`audit template <audit_template_definition>` UUID or name:
.. code:: bash .. code:: bash
$ watcher audit-template-list $ watcher audittemplate list
or::
$ openstack optimize audittemplate list
- Start an audit based on this :ref:`audit template - Start an audit based on this :ref:`audit template
<audit_template_definition>` settings: <audit_template_definition>` settings:
.. code:: bash .. code:: bash
$ watcher audit-create -a <your_audit_template_uuid> $ watcher audit create -a <your_audit_template>
or::
$ openstack optimize audit create -a <your_audit_template>
Watcher service will compute an :ref:`Action Plan <action_plan_definition>` Watcher service will compute an :ref:`Action Plan <action_plan_definition>`
composed of a list of potential optimization :ref:`actions <action_definition>` composed of a list of potential optimization :ref:`actions <action_definition>`
@@ -102,15 +152,22 @@ configuration file.
.. code:: bash .. code:: bash
$ watcher action-plan-list --audit <the_audit_uuid> $ watcher actionplan list --audit <the_audit_uuid>
or::
$ openstack optimize actionplan list --audit <the_audit_uuid>
- Have a look on the list of optimization :ref:`actions <action_definition>` - Have a look on the list of optimization :ref:`actions <action_definition>`
contained in this new :ref:`action plan <action_plan_definition>`: contained in this new :ref:`action plan <action_plan_definition>`:
.. code:: bash .. code:: bash
$ watcher action-list --action-plan <the_action_plan_uuid> $ watcher action list --action-plan <the_action_plan_uuid>
or::
$ openstack optimize action list --action-plan <the_action_plan_uuid>
Once you have learned how to create an :ref:`Action Plan Once you have learned how to create an :ref:`Action Plan
<action_plan_definition>`, it's time to go further by applying it to your <action_plan_definition>`, it's time to go further by applying it to your
@@ -120,18 +177,30 @@ cluster:
.. code:: bash .. code:: bash
$ watcher action-plan-start <the_action_plan_uuid> $ watcher actionplan start <the_action_plan_uuid>
or::
$ openstack optimize actionplan start <the_action_plan_uuid>
You can follow the states of the :ref:`actions <action_definition>` by You can follow the states of the :ref:`actions <action_definition>` by
periodically calling: periodically calling:
.. code:: bash .. code:: bash
$ watcher action-list $ watcher action list
or::
$ openstack optimize action list
You can also obtain more detailed information about a specific action: You can also obtain more detailed information about a specific action:
.. code:: bash .. code:: bash
$ watcher action-show <the_action_uuid> $ watcher action show <the_action_uuid>
or::
$ openstack optimize action show <the_action_uuid>

View File

@@ -205,7 +205,7 @@ place:
$ workon watcher $ workon watcher
(watcher) $ watcher-db-manage --create_schema (watcher) $ watcher-db-manage create_schema
Running Watcher services Running Watcher services

View File

@@ -55,7 +55,7 @@ Here is an example showing how you can write a plugin called ``DummyAction``:
from watcher.applier.actions import base from watcher.applier.actions import base
class DummyAction(baseBaseAction): class DummyAction(base.BaseAction):
@property @property
def schema(self): def schema(self):
@@ -90,11 +90,53 @@ Input validation
As you can see in the previous example, we are using `Voluptuous`_ to validate As you can see in the previous example, we are using `Voluptuous`_ to validate
the input parameters of an action. So if you want to learn more about how to the input parameters of an action. So if you want to learn more about how to
work with `Voluptuous`_, you can have a look at their `documentation`_ here: work with `Voluptuous`_, you can have a look at their `documentation`_:
.. _Voluptuous: https://github.com/alecthomas/voluptuous .. _Voluptuous: https://github.com/alecthomas/voluptuous
.. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md .. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md
Define configuration parameters
===============================
At this point, you have a fully functional action. However, in more complex
implementation, you may want to define some configuration options so one can
tune the action to its needs. To do so, you can implement the
:py:meth:`~.Loadable.get_config_opts` class method as followed:
.. code-block:: python
from oslo_config import cfg
class DummyAction(base.BaseAction):
# [...]
def execute(self):
assert self.config.test_opt == 0
def get_config_opts(self):
return [
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
# Some more options ...
]
The configuration options defined within this class method will be included
within the global ``watcher.conf`` configuration file under a section named by
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
configuration would have to be modified as followed:
.. code-block:: ini
[watcher_actions.dummy]
# Option used for testing.
test_opt = test_value
Then, the configuration options you define within this method will then be
injected in each instantiated object via the ``config`` parameter of the
:py:meth:`~.BaseAction.__init__` method.
Abstract Plugin Class Abstract Plugin Class
===================== =====================
@@ -103,6 +145,7 @@ should implement:
.. autoclass:: watcher.applier.actions.base.BaseAction .. autoclass:: watcher.applier.actions.base.BaseAction
:members: :members:
:special-members: __init__
:noindex: :noindex:
.. py:attribute:: schema .. py:attribute:: schema

View File

@@ -69,6 +69,49 @@ examples, have a look at the implementation of planners already provided by
Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner
plugins can be found :ref:`here <watcher_planners>`. plugins can be found :ref:`here <watcher_planners>`.
Define configuration parameters
===============================
At this point, you have a fully functional planner. However, in more complex
implementation, you may want to define some configuration options so one can
tune the planner to its needs. To do so, you can implement the
:py:meth:`~.Loadable.get_config_opts` class method as followed:
.. code-block:: python
from oslo_config import cfg
class DummyPlanner(base.BasePlanner):
# [...]
def schedule(self, context, audit_uuid, solution):
assert self.config.test_opt == 0
# [...]
def get_config_opts(self):
return [
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
# Some more options ...
]
The configuration options defined within this class method will be included
within the global ``watcher.conf`` configuration file under a section named by
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
configuration would have to be modified as followed:
.. code-block:: ini
[watcher_planners.dummy]
# Option used for testing.
test_opt = test_value
Then, the configuration options you define within this method will then be
injected in each instantiated object via the ``config`` parameter of the
:py:meth:`~.BasePlanner.__init__` method.
Abstract Plugin Class Abstract Plugin Class
===================== =====================
@@ -77,6 +120,7 @@ should implement:
.. autoclass:: watcher.decision_engine.planner.base.BasePlanner .. autoclass:: watcher.decision_engine.planner.base.BasePlanner
:members: :members:
:special-members: __init__
:noindex: :noindex:

View File

@@ -15,7 +15,9 @@ plugin interface which gives anyone the ability to integrate an external
strategy in order to make use of placement algorithms. strategy in order to make use of placement algorithms.
This section gives some guidelines on how to implement and integrate custom This section gives some guidelines on how to implement and integrate custom
strategies with Watcher. strategies with Watcher. If you wish to create a third-party package for your
plugin, you can refer to our :ref:`documentation for third-party package
creation <plugin-base_setup>`.
Pre-requisites Pre-requisites
@@ -26,64 +28,217 @@ configured so that it would provide you all the metrics you need to be able to
use your strategy. use your strategy.
Creating a new plugin Create a new plugin
===================== ===================
First of all you have to: In order to create a new strategy, you have to:
- Extend :py:class:`~.BaseStrategy` - Extend the :py:class:`~.UnclassifiedStrategy` class
- Implement its :py:meth:`~.BaseStrategy.execute` method - Implement its :py:meth:`~.BaseStrategy.get_name` class method to return the
**unique** ID of the new strategy you want to create. This unique ID should
be the same as the name of :ref:`the entry point we will declare later on
<strategy_plugin_add_entrypoint>`.
- Implement its :py:meth:`~.BaseStrategy.get_display_name` class method to
return the translated display name of the strategy you want to create.
Note: Do not use a variable to return the translated string so it can be
automatically collected by the translation tool.
- Implement its :py:meth:`~.BaseStrategy.get_translatable_display_name`
class method to return the translation key (actually the english display
name) of your new strategy. The value return should be the same as the
string translated in :py:meth:`~.BaseStrategy.get_display_name`.
- Implement its :py:meth:`~.BaseStrategy.execute` method to return the
solution you computed within your strategy.
Here is an example showing how you can write a plugin called ``DummyStrategy``: Here is an example showing how you can write a plugin called ``NewStrategy``:
.. code-block:: python .. code-block:: python
import uuid import abc
class DummyStrategy(BaseStrategy): import six
DEFAULT_NAME = "dummy" from watcher._i18n import _
DEFAULT_DESCRIPTION = "Dummy Strategy" from watcher.decision_engine.strategy.strategies import base
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
super(DummyStrategy, self).__init__(name, description)
def execute(self, model): class NewStrategy(base.UnclassifiedStrategy):
migration_type = 'live'
src_hypervisor = 'compute-host-1' def __init__(self, osc=None):
dst_hypervisor = 'compute-host-2' super(NewStrategy, self).__init__(osc)
instance_id = uuid.uuid4()
parameters = {'migration_type': migration_type, def execute(self, original_model):
'src_hypervisor': src_hypervisor, self.solution.add_action(action_type="nop",
'dst_hypervisor': dst_hypervisor}
self.solution.add_action(action_type="migration",
resource_id=instance_id,
input_parameters=parameters) input_parameters=parameters)
# Do some more stuff here ... # Do some more stuff here ...
return self.solution return self.solution
@classmethod
def get_name(cls):
return "new_strategy"
@classmethod
def get_display_name(cls):
return _("New strategy")
@classmethod
def get_translatable_display_name(cls):
return "New strategy"
As you can see in the above example, the :py:meth:`~.BaseStrategy.execute` As you can see in the above example, the :py:meth:`~.BaseStrategy.execute`
method returns a :py:class:`~.BaseSolution` instance as required. This solution method returns a :py:class:`~.BaseSolution` instance as required. This solution
is what wraps the abstract set of actions the strategy recommends to you. This is what wraps the abstract set of actions the strategy recommends to you. This
solution is then processed by a :ref:`planner <planner_definition>` to produce solution is then processed by a :ref:`planner <planner_definition>` to produce
an action plan which shall contain the sequenced flow of actions to be an action plan which contains the sequenced flow of actions to be
executed by the :ref:`Watcher Applier <watcher_applier_definition>`. executed by the :ref:`Watcher Applier <watcher_applier_definition>`.
Please note that your strategy class will be instantiated without any Please note that your strategy class will expect to find the same constructor
parameter. Therefore, you should make sure not to make any of them required in signature as BaseStrategy to instantiate you strategy. Therefore, you should
your ``__init__`` method. ensure that your ``__init__`` signature is identical to the
:py:class:`~.BaseStrategy` one.
Create a new goal
=================
As stated before, the ``NewStrategy`` class extends a class called
:py:class:`~.UnclassifiedStrategy`. This class actually implements a set of
abstract methods which are defined within the :py:class:`~.BaseStrategy` parent
class.
Once you are confident in your strategy plugin, the next step is now to
classify your goal by assigning it a proper goal. To do so, you can either
reuse existing goals defined in Watcher. As of now, four goal-oriented abstract
classes are defined in Watcher:
- :py:class:`~.UnclassifiedStrategy` which is the one I mentioned up until now.
- :py:class:`~.DummyBaseStrategy` which is used by :py:class:`~.DummyStrategy`
for testing purposes.
- :py:class:`~.ServerConsolidationBaseStrategy`
- :py:class:`~.ThermalOptimizationBaseStrategy`
If none of the above actually correspond to the goal your new strategy
achieves, you can define a brand new one. To do so, you need to:
- Extend the :py:class:`~.BaseStrategy` class to make your new goal-oriented
strategy abstract class :
- Implement its :py:meth:`~.BaseStrategy.get_goal_name` class method to
return the **unique** ID of the goal you want to achieve.
- Implement its :py:meth:`~.BaseStrategy.get_goal_display_name` class method
to return the translated display name of the goal you want to achieve.
Note: Do not use a variable to return the translated string so it can be
automatically collected by the translation tool.
- Implement its :py:meth:`~.BaseStrategy.get_translatable_goal_display_name`
class method to return the goal translation key (actually the english
display name). The value return should be the same as the string translated
in :py:meth:`~.BaseStrategy.get_goal_display_name`.
Here is an example showing how you can define a new ``NEW_GOAL`` goal and
modify your ``NewStrategy`` plugin so it now achieves the latter:
.. code-block:: python
import abc
import six
from watcher._i18n import _
from watcher.decision_engine.strategy.strategies import base
@six.add_metaclass(abc.ABCMeta)
class NewGoalBaseStrategy(base.BaseStrategy):
@classmethod
def get_goal_name(cls):
return "NEW_GOAL"
@classmethod
def get_goal_display_name(cls):
return _("New goal")
@classmethod
def get_translatable_goal_display_name(cls):
return "New goal"
class NewStrategy(NewGoalBaseStrategy):
def __init__(self, config, osc=None):
super(NewStrategy, self).__init__(config, osc)
def execute(self, original_model):
self.solution.add_action(action_type="nop",
input_parameters=parameters)
# Do some more stuff here ...
return self.solution
@classmethod
def get_name(cls):
return "new_strategy"
@classmethod
def get_display_name(cls):
return _("New strategy")
@classmethod
def get_translatable_display_name(cls):
return "New strategy"
Define configuration parameters
===============================
At this point, you have a fully functional strategy. However, in more complex
implementation, you may want to define some configuration options so one can
tune the strategy to its needs. To do so, you can implement the
:py:meth:`~.Loadable.get_config_opts` class method as followed:
.. code-block:: python
from oslo_config import cfg
class NewStrategy(NewGoalBaseStrategy):
# [...]
def execute(self, original_model):
assert self.config.test_opt == 0
# [...]
def get_config_opts(self):
return [
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
# Some more options ...
]
The configuration options defined within this class method will be included
within the global ``watcher.conf`` configuration file under a section named by
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
configuration would have to be modified as followed:
.. code-block:: ini
[watcher_strategies.new_strategy]
# Option used for testing.
test_opt = test_value
Then, the configuration options you define within this method will then be
injected in each instantiated object via the ``config`` parameter of the
:py:meth:`~.BaseStrategy.__init__` method.
Abstract Plugin Class Abstract Plugin Class
===================== =====================
Here below is the abstract :py:class:`~.BaseStrategy` class that every single Here below is the abstract :py:class:`~.BaseStrategy` class:
strategy should implement:
.. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy .. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy
:members: :members:
:special-members: __init__
:noindex: :noindex:
.. _strategy_plugin_add_entrypoint:
Add a new entry point Add a new entry point
===================== =====================
@@ -93,7 +248,9 @@ strategy must be registered as a named entry point under the
``watcher_strategies`` entry point of your ``setup.py`` file. If you are using ``watcher_strategies`` entry point of your ``setup.py`` file. If you are using
pbr_, this entry point should be placed in your ``setup.cfg`` file. pbr_, this entry point should be placed in your ``setup.cfg`` file.
The name you give to your entry point has to be unique. The name you give to your entry point has to be unique and should be the same
as the value returned by the :py:meth:`~.BaseStrategy.get_id` class method of
your strategy.
Here below is how you would proceed to register ``DummyStrategy`` using pbr_: Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
@@ -101,7 +258,7 @@ Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
[entry_points] [entry_points]
watcher_strategies = watcher_strategies =
dummy = thirdparty.dummy:DummyStrategy dummy_strategy = thirdparty.dummy:DummyStrategy
To get a better understanding on how to implement a more advanced strategy, To get a better understanding on how to implement a more advanced strategy,
@@ -117,16 +274,10 @@ plugins when it is restarted. If a Python package containing a custom plugin is
installed within the same environment as Watcher, Watcher will automatically installed within the same environment as Watcher, Watcher will automatically
make that plugin available for use. make that plugin available for use.
At this point, Watcher will use your new strategy if you reference it in the At this point, Watcher will scan and register inside the :ref:`Watcher Database
``goals`` under the ``[watcher_goals]`` section of your ``watcher.conf`` <watcher_database_definition>` all the strategies (alongside the goals they
configuration file. For example, if you want to use a ``dummy`` strategy you should satisfy) you implemented upon restarting the :ref:`Watcher Decision
just installed, you would have to associate it to a goal like this: Engine <watcher_decision_engine_definition>`.
.. code-block:: ini
[watcher_goals]
goals = BALANCE_LOAD:basic,MINIMIZE_ENERGY_CONSUMPTION:dummy
You should take care when installing strategy plugins. By their very nature, You should take care when installing strategy plugins. By their very nature,
there are no guarantees that utilizing them as is will be supported, as there are no guarantees that utilizing them as is will be supported, as
@@ -148,7 +299,6 @@ 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 here_. The Ceilosca project is a good example of how to create your own
pluggable backend. pluggable backend.
Finally, if your strategy requires new metrics not covered by Ceilometer, you Finally, if your strategy requires new metrics not covered by Ceilometer, you
can add them through a Ceilometer `plugin`_. can add them through a Ceilometer `plugin`_.
@@ -191,7 +341,7 @@ Read usage metrics using the Watcher Cluster History Helper
Here below is the abstract ``BaseClusterHistory`` class of the Helper. Here below is the abstract ``BaseClusterHistory`` class of the Helper.
.. autoclass:: watcher.metrics_engine.cluster_history.api.BaseClusterHistory .. autoclass:: watcher.metrics_engine.cluster_history.base.BaseClusterHistory
:members: :members:
:noindex: :noindex:

View File

@@ -9,6 +9,10 @@
Available Plugins Available Plugins
================= =================
In this section we present all the plugins that are shipped along with Watcher.
If you want to know which plugins your Watcher services have access to, you can
use the :ref:`Guru Meditation Reports <watcher_gmr>` to display them.
.. _watcher_strategies: .. _watcher_strategies:
Strategies Strategies

View File

@@ -99,14 +99,14 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
Cluster Data Model Cluster Data Model
================== ==================
.. watcher-term:: watcher.metrics_engine.cluster_model_collector.api .. watcher-term:: watcher.metrics_engine.cluster_model_collector.base
.. _cluster_history_definition: .. _cluster_history_definition:
Cluster History Cluster History
=============== ===============
.. watcher-term:: watcher.metrics_engine.cluster_history.api .. watcher-term:: watcher.metrics_engine.cluster_history.base
.. _controller_node_definition: .. _controller_node_definition:
@@ -223,8 +223,8 @@ measure of how much of the :ref:`Goal <goal_definition>` has been achieved in
respect with constraints and :ref:`SLAs <sla_definition>` defined by the respect with constraints and :ref:`SLAs <sla_definition>` defined by the
:ref:`Customer <customer_definition>`. :ref:`Customer <customer_definition>`.
The way efficacy is evaluated will depend on the The way efficacy is evaluated will depend on the :ref:`Goal <goal_definition>`
:ref:`Goal <goal_definition>` to achieve. to achieve.
Of course, the efficacy will be relevant only as long as the Of course, the efficacy will be relevant only as long as the
:ref:`Action Plan <action_plan_definition>` is relevant :ref:`Action Plan <action_plan_definition>` is relevant
@@ -323,7 +323,7 @@ Solution
Strategy Strategy
======== ========
.. watcher-term:: watcher.decision_engine.strategy.strategies.base .. watcher-term:: watcher.api.controllers.v1.strategy
.. _watcher_applier_definition: .. _watcher_applier_definition:

View File

@@ -0,0 +1,14 @@
plantuml
========
To build an image from a source file, you have to upload the plantuml JAR file
available on http://plantuml.com/download.html.
After, just run this command to build your image:
.. code-block:: shell
$ cd doc/source/images
$ java -jar /path/to/plantuml.jar doc/source/image_src/plantuml/my_image.txt
$ ls doc/source/images/
my_image.png

View File

@@ -3,7 +3,7 @@
actor Administrator actor Administrator
Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid> Administrator -> "Watcher CLI" : watcher audit create -a <audit_template>
"Watcher CLI" -> "Watcher API" : POST audit(parameters) "Watcher CLI" -> "Watcher API" : POST audit(parameters)
"Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING) "Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING)
@@ -14,7 +14,7 @@ Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid>
Administrator <-- "Watcher CLI" : new audit uuid Administrator <-- "Watcher CLI" : new audit uuid
"Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid) "Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid)
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid) "AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid) (status=ONGOING)
ref over "Watcher Decision Engine" ref over "Watcher Decision Engine"
Trigger audit in the Trigger audit in the

View File

@@ -2,15 +2,21 @@
actor Administrator actor Administrator
Administrator -> "Watcher CLI" : watcher audit-template-create <name> <goal> Administrator -> "Watcher CLI" : watcher audittemplate create <name> <goal> \
[--strategy-uuid <strategy>]
"Watcher CLI" -> "Watcher API" : POST audit_template(parameters) "Watcher CLI" -> "Watcher API" : POST audit_template(parameters)
"Watcher API" -> "Watcher API" : make sure goal exist in configuration "Watcher API" -> "Watcher Database" : Request if goal exists in database
"Watcher API" -> "Watcher Database" : create new audit_template in database "Watcher API" <-- "Watcher Database" : OK
"Watcher API" <-- "Watcher Database" : new audit template uuid "Watcher API" -> "Watcher Database" : Request if strategy exists in database (if provided)
"Watcher CLI" <-- "Watcher API" : return new audit template URL in HTTP Location Header "Watcher API" <-- "Watcher Database" : OK
Administrator <-- "Watcher CLI" : new audit template uuid
"Watcher API" -> "Watcher Database" : Create new audit_template in database
"Watcher API" <-- "Watcher Database" : New audit template UUID
"Watcher CLI" <-- "Watcher API" : Return new audit template URL in HTTP Location Header
Administrator <-- "Watcher CLI" : New audit template UUID
@enduml @enduml

View File

@@ -2,10 +2,10 @@
actor Administrator actor Administrator
Administrator -> "Watcher CLI" : watcher action-plan-start <action_plan_uuid> Administrator -> "Watcher CLI" : watcher actionplan start <action_plan_uuid>
"Watcher CLI" -> "Watcher API" : PATCH action_plan(state=TRIGGERED) "Watcher CLI" -> "Watcher API" : PATCH action_plan(state=PENDING)
"Watcher API" -> "Watcher Database" : action_plan.state=TRIGGERED "Watcher API" -> "Watcher Database" : action_plan.state=PENDING
"Watcher CLI" <-- "Watcher API" : HTTP 200 "Watcher CLI" <-- "Watcher API" : HTTP 200

View File

@@ -0,0 +1,87 @@
@startuml
abstract class Base {
// Timestamp mixin
DateTime created_at
DateTime updated_at
// Soft Delete mixin
DateTime deleted_at
Integer deleted // default = 0
}
class Strategy {
**Integer id** // primary_key
String uuid // length = 36
String name // length = 63, nullable = false
String display_name // length = 63, nullable = false
<i>Integer goal_id</i> // ForeignKey('goals.id'), nullable = false
}
class Goal {
**Integer id** // primary_key
String uuid // length = 36
String name // length = 63, nullable = false
String display_name // length = 63, nullable=False
}
class AuditTemplate {
**Integer id** // primary_key
String uuid // length = 36
String name // length = 63, nullable = true
String description // length = 255, nullable = true
Integer host_aggregate // nullable = true
<i>Integer goal_id</i> // ForeignKey('goals.id'), nullable = false
<i>Integer strategy_id</i> // ForeignKey('strategies.id'), nullable = true
JsonString extra
String version // length = 15, nullable = true
}
class Audit {
**Integer id** // primary_key
String uuid // length = 36
String type // length = 20
String state // length = 20, nullable = true
DateTime deadline // nullable = true
<i>Integer audit_template_id</i> // ForeignKey('audit_templates.id') \
nullable = false
}
class Action {
**Integer id** // primary_key
String uuid // length = 36, nullable = false
<i>Integer action_plan_id</i> // ForeignKey('action_plans.id'), nullable = false
String action_type // length = 255, nullable = false
JsonString input_parameters // nullable = true
String state // length = 20, nullable = true
String next // length = 36, nullable = true
}
class ActionPlan {
**Integer id** // primary_key
String uuid // length = 36
Integer first_action_id //
<i>Integer audit_id</i> // ForeignKey('audits.id'), nullable = true
String state // length = 20, nullable = true
}
"Base" <|-- "Strategy"
"Base" <|-- "Goal"
"Base" <|-- "AuditTemplate"
"Base" <|-- "Audit"
"Base" <|-- "Action"
"Base" <|-- "ActionPlan"
"Goal" <.. "Strategy" : Foreign Key
"Goal" <.. "AuditTemplate" : Foreign Key
"Strategy" <.. "AuditTemplate" : Foreign Key
"AuditTemplate" <.. "Audit" : Foreign Key
"ActionPlan" <.. "Action" : Foreign Key
"Audit" <.. "ActionPlan" : Foreign Key
@enduml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -90,6 +90,7 @@ Introduction
deploy/installation deploy/installation
deploy/user-guide deploy/user-guide
deploy/gmr
Watcher Manual Pages Watcher Manual Pages
==================== ====================

View File

@@ -5,31 +5,34 @@
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
jsonpatch>=1.1 # BSD jsonpatch>=1.1 # BSD
keystoneauth1>=2.1.0 # Apache-2.0 keystoneauth1>=2.1.0 # Apache-2.0
keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0 keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
oslo.config>=3.7.0 # Apache-2.0 oslo.concurrency>=3.8.0 # Apache-2.0
oslo.context>=0.2.0 # Apache-2.0 oslo.cache>=1.5.0 # Apache-2.0
oslo.config>=3.9.0 # Apache-2.0
oslo.context>=2.2.0 # Apache-2.0
oslo.db>=4.1.0 # Apache-2.0 oslo.db>=4.1.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0 oslo.log>=1.14.0 # Apache-2.0
oslo.messaging>=4.0.0 # Apache-2.0 oslo.messaging>=4.5.0 # Apache-2.0
oslo.policy>=0.5.0 # Apache-2.0 oslo.policy>=0.5.0 # Apache-2.0
oslo.service>=1.0.0 # Apache-2.0 oslo.reports>=0.6.0 # Apache-2.0
oslo.service>=1.10.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0
PasteDeploy>=1.5.0 # MIT PasteDeploy>=1.5.0 # MIT
pbr>=1.6 # Apache-2.0 pbr>=1.6 # Apache-2.0
pecan>=1.0.0 # BSD pecan>=1.0.0 # BSD
PrettyTable<0.8,>=0.7 # BSD PrettyTable<0.8,>=0.7 # BSD
voluptuous>=0.8.6 # BSD License voluptuous>=0.8.9 # BSD License
python-ceilometerclient>=2.2.1 # Apache-2.0 python-ceilometerclient>=2.2.1 # Apache-2.0
python-cinderclient>=1.3.1 # Apache-2.0 python-cinderclient!=1.7.0,>=1.6.0 # Apache-2.0
python-glanceclient>=2.0.0 # Apache-2.0 python-glanceclient>=2.0.0 # Apache-2.0
python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
python-neutronclient!=4.1.0,>=2.6.0 # Apache-2.0 python-neutronclient>=4.2.0 # Apache-2.0
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
python-openstackclient>=2.1.0 # Apache-2.0 python-openstackclient>=2.1.0 # Apache-2.0
six>=1.9.0 # MIT six>=1.9.0 # MIT
SQLAlchemy<1.1.0,>=1.0.10 # MIT SQLAlchemy<1.1.0,>=1.0.10 # MIT
stevedore>=1.5.0 # Apache-2.0 stevedore>=1.10.0 # Apache-2.0
taskflow>=1.26.0 # Apache-2.0 taskflow>=1.26.0 # Apache-2.0
WebOb>=1.2.3 # MIT WebOb>=1.2.3 # MIT
WSME>=0.8 # MIT WSME>=0.8 # MIT

View File

@@ -50,6 +50,8 @@ watcher_strategies =
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
workload_stabilization = watcher.decision_engine.strategy.strategies.workload_stabilization:WorkloadStabilization
workload_balance = watcher.decision_engine.strategy.strategies.workload_balance:WorkloadBalance
watcher_actions = watcher_actions =
migrate = watcher.applier.actions.migration:Migrate migrate = watcher.applier.actions.migration:Migrate

View File

@@ -7,7 +7,7 @@ discover # BSD
doc8 # Apache-2.0 doc8 # Apache-2.0
freezegun # Apache-2.0 freezegun # Apache-2.0
hacking<0.11,>=0.10.2 hacking<0.11,>=0.10.2
mock>=1.2 # BSD mock>=2.0 # BSD
oslotest>=1.10.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0
os-testr>=0.4.1 # Apache-2.0 os-testr>=0.4.1 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD python-subunit>=0.0.18 # Apache-2.0/BSD

View File

@@ -15,6 +15,7 @@
# limitations under the License. # limitations under the License.
# #
import oslo_i18n import oslo_i18n
from oslo_i18n import _lazy
# The domain is the name of the App which is used to generate the folder # The domain is the name of the App which is used to generate the folder
# containing the translation files (i.e. the .pot file and the various locales) # containing the translation files (i.e. the .pot file and the various locales)
@@ -42,5 +43,9 @@ _LE = _translators.log_error
_LC = _translators.log_critical _LC = _translators.log_critical
def lazy_translation_enabled():
return _lazy.USE_LAZY
def get_available_languages(): def get_available_languages():
return oslo_i18n.get_available_languages(DOMAIN) return oslo_i18n.get_available_languages(DOMAIN)

View File

@@ -19,24 +19,38 @@
from oslo_config import cfg from oslo_config import cfg
import pecan import pecan
from watcher._i18n import _
from watcher.api import acl from watcher.api import acl
from watcher.api import config as api_config from watcher.api import config as api_config
from watcher.api import middleware from watcher.api import middleware
from watcher.decision_engine.strategy.selection import default \
as strategy_selector
# Register options for the service # Register options for the service
API_SERVICE_OPTS = [ API_SERVICE_OPTS = [
cfg.IntOpt('port', cfg.PortOpt('port',
default=9322, default=9322,
help='The port for the watcher API server'), help=_('The port for the watcher API server')),
cfg.StrOpt('host', cfg.StrOpt('host',
default='0.0.0.0', default='0.0.0.0',
help='The listen IP for the watcher API server'), help=_('The listen IP for the watcher API server')),
cfg.IntOpt('max_limit', cfg.IntOpt('max_limit',
default=1000, default=1000,
help='The maximum number of items returned in a single ' help=_('The maximum number of items returned in a single '
'response from a collection resource.') 'response from a collection resource')),
cfg.IntOpt('workers',
min=1,
help=_('Number of workers for Watcher API service. '
'The default is equal to the number of CPUs available '
'if that can be determined, else a default worker '
'count of 1 is returned.')),
cfg.BoolOpt('enable_ssl_api',
default=False,
help=_("Enable the integrated stand-alone API to service "
"requests via HTTPS instead of HTTP. If there is a "
"front-end service performing HTTPS offloading from "
"the service, this option should be False; note, you "
"will want to change public API endpoint to represent "
"SSL termination URL with 'public_endpoint' option.")),
] ]
CONF = cfg.CONF CONF = cfg.CONF
@@ -45,7 +59,6 @@ opt_group = cfg.OptGroup(name='api',
CONF.register_group(opt_group) CONF.register_group(opt_group)
CONF.register_opts(API_SERVICE_OPTS, opt_group) CONF.register_opts(API_SERVICE_OPTS, opt_group)
CONF.register_opts(strategy_selector.WATCHER_GOALS_OPTS)
def get_pecan_config(): def get_pecan_config():
@@ -68,3 +81,12 @@ def setup_app(config=None):
) )
return acl.install(app, CONF, config.app.acl_public_routes) return acl.install(app, CONF, config.app.acl_public_routes)
class VersionSelectorApplication(object):
def __init__(self):
pc = get_pecan_config()
self.v1 = setup_app(config=pc)
def __call__(self, environ, start_response):
return self.v1(environ, start_response)

View File

@@ -34,6 +34,7 @@ from watcher.api.controllers.v1 import action_plan
from watcher.api.controllers.v1 import audit from watcher.api.controllers.v1 import audit
from watcher.api.controllers.v1 import audit_template from watcher.api.controllers.v1 import audit_template
from watcher.api.controllers.v1 import goal from watcher.api.controllers.v1 import goal
from watcher.api.controllers.v1 import strategy
class APIBase(wtypes.Base): class APIBase(wtypes.Base):
@@ -157,6 +158,7 @@ class Controller(rest.RestController):
actions = action.ActionsController() actions = action.ActionsController()
action_plans = action_plan.ActionPlansController() action_plans = action_plan.ActionPlansController()
goals = goal.GoalsController() goals = goal.GoalsController()
strategies = strategy.StrategiesController()
@wsme_pecan.wsexpose(V1) @wsme_pecan.wsexpose(V1)
def get(self): def get(self):

View File

@@ -119,7 +119,7 @@ class Action(base.APIBase):
self.action_next_uuid = None self.action_next_uuid = None
# raise e # raise e
uuid = types.uuid uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action""" """Unique UUID for this action"""
action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid, action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid,
@@ -130,9 +130,6 @@ class Action(base.APIBase):
state = wtypes.text state = wtypes.text
"""This audit state""" """This audit state"""
alarm = types.uuid
"""An alarm UUID related to this action"""
action_type = wtypes.text action_type = wtypes.text
"""Action type""" """Action type"""
@@ -194,7 +191,6 @@ class Action(base.APIBase):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
description='action description', description='action description',
state='PENDING', state='PENDING',
alarm=None,
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
deleted_at=None, deleted_at=None,
updated_at=datetime.datetime.utcnow()) updated_at=datetime.datetime.utcnow())

View File

@@ -143,7 +143,7 @@ class ActionPlan(base.APIBase):
except exception.ActionNotFound: except exception.ActionNotFound:
self._first_action_uuid = None self._first_action_uuid = None
uuid = types.uuid uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action plan""" """Unique UUID for this action plan"""
first_action_uuid = wsme.wsproperty( first_action_uuid = wsme.wsproperty(

View File

@@ -49,6 +49,28 @@ from watcher.decision_engine import rpcapi
from watcher import objects from watcher import objects
class AuditPostType(wtypes.Base):
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=True)
type = wtypes.wsattr(wtypes.text, mandatory=True)
deadline = wtypes.wsattr(datetime.datetime, mandatory=False)
state = wsme.wsattr(wtypes.text, readonly=True,
default=objects.audit.State.PENDING)
def as_audit(self):
audit_type_values = [val.value for val in objects.audit.AuditType]
if self.type not in audit_type_values:
raise exception.AuditTypeNotFound(audit_type=self.type)
return Audit(
audit_template_id=self.audit_template_uuid,
type=self.type,
deadline=self.deadline)
class AuditPatchType(types.JsonPatchType): class AuditPatchType(types.JsonPatchType):
@staticmethod @staticmethod
@@ -325,12 +347,13 @@ class AuditsController(rest.RestController):
audit_uuid) audit_uuid)
return Audit.convert_with_links(rpc_audit) return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=Audit, status_code=201) @wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201)
def post(self, audit): def post(self, audit_p):
"""Create a new audit. """Create a new audit.
:param audit: a audit within the request body. :param audit_p: a audit within the request body.
""" """
audit = audit_p.as_audit()
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted

View File

@@ -50,42 +50,162 @@ provided as a list of key-value pairs.
import datetime import datetime
from oslo_config import cfg
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
from wsme import types as wtypes from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from watcher._i18n import _
from watcher.api.controllers import base from watcher.api.controllers import base
from watcher.api.controllers import link from watcher.api.controllers import link
from watcher.api.controllers.v1 import collection from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import context as context_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import utils as common_utils from watcher.common import utils as common_utils
from watcher import objects from watcher import objects
class AuditTemplatePostType(wtypes.Base):
_ctx = context_utils.make_context()
name = wtypes.wsattr(wtypes.text, mandatory=True)
"""Name of this audit template"""
description = wtypes.wsattr(wtypes.text, mandatory=False)
"""Short description of this audit template"""
deadline = wsme.wsattr(datetime.datetime, mandatory=False)
"""deadline of the audit template"""
host_aggregate = wsme.wsattr(wtypes.IntegerType(minimum=1),
mandatory=False)
"""ID of the Nova host aggregate targeted by the audit template"""
extra = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False)
"""The metadata of the audit template"""
goal = wtypes.wsattr(wtypes.text, mandatory=True)
"""Goal UUID or name of the audit template"""
strategy = wtypes.wsattr(wtypes.text, mandatory=False)
"""Strategy UUID or name of the audit template"""
version = wtypes.text
"""Internal version of the audit template"""
def as_audit_template(self):
return AuditTemplate(
name=self.name,
description=self.description,
deadline=self.deadline,
host_aggregate=self.host_aggregate,
extra=self.extra,
goal_id=self.goal, # Dirty trick ...
goal=self.goal,
strategy_id=self.strategy, # Dirty trick ...
strategy_uuid=self.strategy,
version=self.version,
)
@staticmethod
def validate(audit_template):
available_goals = objects.Goal.list(AuditTemplatePostType._ctx)
available_goal_uuids_map = {g.uuid: g for g in available_goals}
available_goal_names_map = {g.name: g for g in available_goals}
if audit_template.goal in available_goal_uuids_map:
goal = available_goal_uuids_map[audit_template.goal]
elif audit_template.goal in available_goal_names_map:
goal = available_goal_names_map[audit_template.goal]
else:
raise exception.InvalidGoal(goal=audit_template.goal)
if audit_template.strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePostType._ctx)
available_strategies_map = {
s.uuid: s for s in available_strategies}
if audit_template.strategy not in available_strategies_map:
raise exception.InvalidStrategy(
strategy=audit_template.strategy)
strategy = available_strategies_map[audit_template.strategy]
# Check that the strategy we indicate is actually related to the
# specified goal
if strategy.goal_id != goal.id:
choices = ["'%s' (%s)" % (s.uuid, s.name)
for s in available_strategies]
raise exception.InvalidStrategy(
message=_(
"'%(strategy)s' strategy does relate to the "
"'%(goal)s' goal. Possible choices: %(choices)s")
% dict(strategy=strategy.name, goal=goal.name,
choices=", ".join(choices)))
audit_template.strategy = strategy.uuid
# We force the UUID so that we do not need to query the DB with the
# name afterwards
audit_template.goal = goal.uuid
return audit_template
class AuditTemplatePatchType(types.JsonPatchType): class AuditTemplatePatchType(types.JsonPatchType):
_ctx = context_utils.make_context()
@staticmethod @staticmethod
def mandatory_attrs(): def mandatory_attrs():
return [] return []
@staticmethod @staticmethod
def validate(patch): def validate(patch):
if patch.path == "/goal": if patch.path == "/goal" and patch.op != "remove":
AuditTemplatePatchType._validate_goal(patch) AuditTemplatePatchType._validate_goal(patch)
elif patch.path == "/goal" and patch.op == "remove":
raise exception.OperationNotPermitted(
_("Cannot remove 'goal' attribute "
"from an audit template"))
if patch.path == "/strategy":
AuditTemplatePatchType._validate_strategy(patch)
return types.JsonPatchType.validate(patch) return types.JsonPatchType.validate(patch)
@staticmethod @staticmethod
def _validate_goal(patch): def _validate_goal(patch):
serialized_patch = {'path': patch.path, 'op': patch.op} patch.path = "/goal_id"
if patch.value is not wsme.Unset: goal = patch.value
serialized_patch['value'] = patch.value
new_goal = patch.value if goal:
if new_goal and new_goal not in cfg.CONF.watcher_goals.goals.keys(): available_goals = objects.Goal.list(
raise exception.InvalidGoal(goal=new_goal) AuditTemplatePatchType._ctx)
available_goal_uuids_map = {g.uuid: g for g in available_goals}
available_goal_names_map = {g.name: g for g in available_goals}
if goal in available_goal_uuids_map:
patch.value = available_goal_uuids_map[goal].id
elif goal in available_goal_names_map:
patch.value = available_goal_names_map[goal].id
else:
raise exception.InvalidGoal(goal=goal)
@staticmethod
def _validate_strategy(patch):
patch.path = "/strategy_id"
strategy = patch.value
if strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePatchType._ctx)
available_strategy_uuids_map = {
s.uuid: s for s in available_strategies}
available_strategy_names_map = {
s.name: s for s in available_strategies}
if strategy in available_strategy_uuids_map:
patch.value = available_strategy_uuids_map[strategy].id
elif strategy in available_strategy_names_map:
patch.value = available_strategy_names_map[strategy].id
else:
raise exception.InvalidStrategy(strategy=strategy)
class AuditTemplate(base.APIBase): class AuditTemplate(base.APIBase):
@@ -95,7 +215,90 @@ class AuditTemplate(base.APIBase):
between the internal object model and the API representation of an between the internal object model and the API representation of an
audit template. audit template.
""" """
uuid = types.uuid
_goal_uuid = None
_goal_name = None
_strategy_uuid = None
_strategy_name = None
def _get_goal(self, value):
if value == wtypes.Unset:
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
goal = objects.Goal.get(
pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(
pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
self.goal_id = goal.id
return goal
def _get_strategy(self, value):
if value == wtypes.Unset:
return None
strategy = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
strategy = objects.Strategy.get(
pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
except exception.StrategyNotFound:
pass
if strategy:
self.strategy_id = strategy.id
return strategy
def _get_goal_uuid(self):
return self._goal_uuid
def _set_goal_uuid(self, value):
if value and self._goal_uuid != value:
self._goal_uuid = None
goal = self._get_goal(value)
if goal:
self._goal_uuid = goal.uuid
def _get_strategy_uuid(self):
return self._strategy_uuid
def _set_strategy_uuid(self, value):
if value and self._strategy_uuid != value:
self._strategy_uuid = None
strategy = self._get_strategy(value)
if strategy:
self._strategy_uuid = strategy.uuid
def _get_goal_name(self):
return self._goal_name
def _set_goal_name(self, value):
if value and self._goal_name != value:
self._goal_name = None
goal = self._get_goal(value)
if goal:
self._goal_name = goal.name
def _get_strategy_name(self):
return self._strategy_name
def _set_strategy_name(self, value):
if value and self._strategy_name != value:
self._strategy_name = None
strategy = self._get_strategy(value)
if strategy:
self._strategy_name = strategy.name
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this audit template""" """Unique UUID for this audit template"""
name = wtypes.text name = wtypes.text
@@ -113,8 +316,21 @@ class AuditTemplate(base.APIBase):
extra = {wtypes.text: types.jsontype} extra = {wtypes.text: types.jsontype}
"""The metadata of the audit template""" """The metadata of the audit template"""
goal = wtypes.text goal_uuid = wsme.wsproperty(
"""Goal type of the audit template""" wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
"""Goal UUID the audit template refers to"""
goal_name = wsme.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
"""The name of the goal this audit template refers to"""
strategy_uuid = wsme.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
"""Strategy UUID the audit template refers to"""
strategy_name = wsme.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
"""The name of the strategy this audit template refers to"""
version = wtypes.text version = wtypes.text
"""Internal version of the audit template""" """Internal version of the audit template"""
@@ -127,20 +343,43 @@ class AuditTemplate(base.APIBase):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(AuditTemplate, self).__init__() super(AuditTemplate, self).__init__()
self.fields = [] self.fields = []
for field in objects.AuditTemplate.fields: fields = list(objects.AuditTemplate.fields)
for k in fields:
# Skip fields we do not expose. # Skip fields we do not expose.
if not hasattr(self, field): if not hasattr(self, k):
continue continue
self.fields.append(field) self.fields.append(k)
setattr(self, field, kwargs.get(field, wtypes.Unset)) setattr(self, k, kwargs.get(k, wtypes.Unset))
self.fields.append('goal_id')
self.fields.append('strategy_id')
# goal_uuid & strategy_uuid are not part of
# objects.AuditTemplate.fields because they're API-only attributes.
self.fields.append('goal_uuid')
self.fields.append('goal_name')
self.fields.append('strategy_uuid')
self.fields.append('strategy_name')
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'strategy_uuid',
kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_name',
kwargs.get('strategy_id', wtypes.Unset))
@staticmethod @staticmethod
def _convert_with_links(audit_template, url, expand=True): def _convert_with_links(audit_template, url, expand=True):
if not expand: if not expand:
audit_template.unset_fields_except(['uuid', 'name', audit_template.unset_fields_except(
'host_aggregate', 'goal']) ['uuid', 'name', 'host_aggregate', 'goal_uuid', 'goal_name',
'strategy_uuid', 'strategy_name'])
# The numeric ID should not be exposed to
# the user, it's internal only.
audit_template.goal_id = wtypes.Unset
audit_template.strategy_id = wtypes.Unset
audit_template.links = [link.Link.make_link('self', url, audit_template.links = [link.Link.make_link('self', url,
'audit_templates', 'audit_templates',
@@ -148,8 +387,7 @@ class AuditTemplate(base.APIBase):
link.Link.make_link('bookmark', url, link.Link.make_link('bookmark', url,
'audit_templates', 'audit_templates',
audit_template.uuid, audit_template.uuid,
bookmark=True) bookmark=True)]
]
return audit_template return audit_template
@classmethod @classmethod
@@ -164,19 +402,14 @@ class AuditTemplate(base.APIBase):
name='My Audit Template', name='My Audit Template',
description='Description of my audit template', description='Description of my audit template',
host_aggregate=5, host_aggregate=5,
goal='DUMMY', goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
extra={'automatic': True}, extra={'automatic': True},
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
deleted_at=None, deleted_at=None,
updated_at=datetime.datetime.utcnow()) updated_at=datetime.datetime.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9322', expand) return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@staticmethod
def validate(audit_template):
if audit_template.goal not in cfg.CONF.watcher_goals.goals.keys():
raise exception.InvalidGoal(audit_template.goal)
return audit_template
class AuditTemplateCollection(collection.Collection): class AuditTemplateCollection(collection.Collection):
"""API representation of a collection of audit templates.""" """API representation of a collection of audit templates."""
@@ -191,12 +424,12 @@ class AuditTemplateCollection(collection.Collection):
@staticmethod @staticmethod
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False, def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
**kwargs): **kwargs):
collection = AuditTemplateCollection() at_collection = AuditTemplateCollection()
collection.audit_templates = \ at_collection.audit_templates = [
[AuditTemplate.convert_with_links(p, expand) AuditTemplate.convert_with_links(p, expand)
for p in rpc_audit_templates] for p in rpc_audit_templates]
collection.next = collection.get_next(limit, url=url, **kwargs) at_collection.next = at_collection.get_next(limit, url=url, **kwargs)
return collection return at_collection
@classmethod @classmethod
def sample(cls): def sample(cls):
@@ -222,7 +455,8 @@ class AuditTemplatesController(rest.RestController):
sort_key, sort_dir, expand=False, sort_key, sort_dir, expand=False,
resource_url=None): resource_url=None):
api_utils.validate_search_filters( api_utils.validate_search_filters(
filters, objects.audit_template.AuditTemplate.fields.keys()) filters, list(objects.audit_template.AuditTemplate.fields.keys()) +
["goal_uuid", "goal_name", "strategy_uuid", "strategy_name"])
limit = api_utils.validate_limit(limit) limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir) api_utils.validate_sort_dir(sort_dir)
@@ -246,30 +480,43 @@ class AuditTemplatesController(rest.RestController):
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) sort_dir=sort_dir)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, @wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text) types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None, def get_all(self, goal=None, strategy=None, marker=None,
sort_key='id', sort_dir='asc'): limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of audit templates. """Retrieve a list of audit templates.
:param goal: goal name to filter by (case sensitive) :param goal: goal UUID or name to filter by
:param strategy: strategy UUID or name to filter by
:param marker: pagination marker for large data sets. :param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
""" """
filters = api_utils.as_filters_dict(goal=goal) filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
if strategy:
if common_utils.is_uuid_like(strategy):
filters['strategy_uuid'] = strategy
else:
filters['strategy_name'] = strategy
return self._get_audit_templates_collection( return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir) filters, marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, types.uuid, int, @wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
wtypes.text, wtypes.text) types.uuid, int, wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None, def detail(self, goal=None, strategy=None, marker=None,
sort_key='id', sort_dir='asc'): limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of audit templates with detail. """Retrieve a list of audit templates with detail.
:param goal: goal name to filter by (case sensitive) :param goal: goal UUID or name to filter by
:param strategy: strategy UUID or name to filter by
:param marker: pagination marker for large data sets. :param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
@@ -280,7 +527,18 @@ class AuditTemplatesController(rest.RestController):
if parent != "audit_templates": if parent != "audit_templates":
raise exception.HTTPNotFound raise exception.HTTPNotFound
filters = api_utils.as_filters_dict(goal=goal) filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
if strategy:
if common_utils.is_uuid_like(strategy):
filters['strategy_uuid'] = strategy
else:
filters['strategy_name'] = strategy
expand = True expand = True
resource_url = '/'.join(['audit_templates', 'detail']) resource_url = '/'.join(['audit_templates', 'detail'])
@@ -308,19 +566,21 @@ class AuditTemplatesController(rest.RestController):
return AuditTemplate.convert_with_links(rpc_audit_template) return AuditTemplate.convert_with_links(rpc_audit_template)
@wsme.validate(types.uuid, AuditTemplate) @wsme.validate(types.uuid, AuditTemplatePostType)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplate, status_code=201) @wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
def post(self, audit_template): status_code=201)
def post(self, audit_template_postdata):
"""Create a new audit template. """Create a new audit template.
:param audit template: a audit template within the request body. :param audit_template_postdata: the audit template POST data
from the request body.
""" """
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
audit_template_dict = audit_template.as_dict()
context = pecan.request.context context = pecan.request.context
audit_template = audit_template_postdata.as_audit_template()
audit_template_dict = audit_template.as_dict()
new_audit_template = objects.AuditTemplate(context, new_audit_template = objects.AuditTemplate(context,
**audit_template_dict) **audit_template_dict)
new_audit_template.create(context) new_audit_template.create(context)

View File

@@ -44,7 +44,7 @@ class Collection(base.APIBase):
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
'args': q_args, 'limit': limit, 'args': q_args, 'limit': limit,
'marker': self.collection[-1].uuid} 'marker': getattr(self.collection[-1], "uuid")}
return link.Link.make_link('next', pecan.request.host_url, return link.Link.make_link('next', pecan.request.host_url,
resource_url, next_args).href resource_url, next_args).href

View File

@@ -46,61 +46,64 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import utils as common_utils
from watcher import objects
CONF = cfg.CONF CONF = cfg.CONF
class Goal(base.APIBase): class Goal(base.APIBase):
"""API representation of a action. """API representation of a goal.
This class enforces type checking and value constraints, and converts This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a action. between the internal object model and the API representation of a goal.
""" """
uuid = types.uuid
"""Unique UUID for this goal"""
name = wtypes.text name = wtypes.text
"""Name of the goal""" """Name of the goal"""
strategy = wtypes.text display_name = wtypes.text
"""The strategy associated with the goal""" """Localized name of the goal"""
uuid = types.uuid
"""Unused field"""
links = wsme.wsattr([link.Link], readonly=True) links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated action links""" """A list containing a self link and associated audit template links"""
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Goal, self).__init__() super(Goal, self).__init__()
self.fields = [] self.fields = []
self.fields.append('uuid')
self.fields.append('name') self.fields.append('name')
self.fields.append('strategy') self.fields.append('display_name')
setattr(self, 'name', kwargs.get('name', setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset))
wtypes.Unset)) setattr(self, 'name', kwargs.get('name', wtypes.Unset))
setattr(self, 'strategy', kwargs.get('strategy', setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset))
wtypes.Unset))
@staticmethod @staticmethod
def _convert_with_links(goal, url, expand=True): def _convert_with_links(goal, url, expand=True):
if not expand: if not expand:
goal.unset_fields_except(['name', 'strategy']) goal.unset_fields_except(['uuid', 'name', 'display_name'])
goal.links = [link.Link.make_link('self', url, goal.links = [link.Link.make_link('self', url,
'goals', goal.name), 'goals', goal.uuid),
link.Link.make_link('bookmark', url, link.Link.make_link('bookmark', url,
'goals', goal.name, 'goals', goal.uuid,
bookmark=True)] bookmark=True)]
return goal return goal
@classmethod @classmethod
def convert_with_links(cls, goal, expand=True): def convert_with_links(cls, goal, expand=True):
goal = Goal(**goal) goal = Goal(**goal.as_dict())
return cls._convert_with_links(goal, pecan.request.host_url, expand) return cls._convert_with_links(goal, pecan.request.host_url, expand)
@classmethod @classmethod
def sample(cls, expand=True): def sample(cls, expand=True):
sample = cls(name='27e3153e-d5bf-4b7e-b517-fb518e17f34c', sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
strategy='action description') name='DUMMY',
display_name='Dummy strategy')
return cls._convert_with_links(sample, 'http://localhost:9322', expand) return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -117,27 +120,28 @@ class GoalCollection(collection.Collection):
@staticmethod @staticmethod
def convert_with_links(goals, limit, url=None, expand=False, def convert_with_links(goals, limit, url=None, expand=False,
**kwargs): **kwargs):
goal_collection = GoalCollection()
collection = GoalCollection() goal_collection.goals = [
collection.goals = [Goal.convert_with_links(g, expand) for g in goals] Goal.convert_with_links(g, expand) for g in goals]
if 'sort_key' in kwargs: if 'sort_key' in kwargs:
reverse = False reverse = False
if kwargs['sort_key'] == 'strategy': if kwargs['sort_key'] == 'strategy':
if 'sort_dir' in kwargs: if 'sort_dir' in kwargs:
reverse = True if kwargs['sort_dir'] == 'desc' else False reverse = True if kwargs['sort_dir'] == 'desc' else False
collection.goals = sorted( goal_collection.goals = sorted(
collection.goals, goal_collection.goals,
key=lambda goal: goal.name, key=lambda goal: goal.uuid,
reverse=reverse) reverse=reverse)
collection.next = collection.get_next(limit, url=url, **kwargs) goal_collection.next = goal_collection.get_next(
return collection limit, url=url, **kwargs)
return goal_collection
@classmethod @classmethod
def sample(cls): def sample(cls):
sample = cls() sample = cls()
sample.actions = [Goal.sample(expand=False)] sample.goals = [Goal.sample(expand=False)]
return sample return sample
@@ -154,51 +158,49 @@ class GoalsController(rest.RestController):
'detail': ['GET'], 'detail': ['GET'],
} }
def _get_goals_collection(self, limit, def _get_goals_collection(self, marker, limit, sort_key, sort_dir,
sort_key, sort_dir, expand=False, expand=False, resource_url=None):
resource_url=None, goal_name=None):
limit = api_utils.validate_limit(limit) limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir) api_utils.validate_sort_dir(sort_dir)
goals = [] sort_db_key = (sort_key if sort_key in objects.Goal.fields.keys()
else None)
if not goal_name and goal_name in CONF.watcher_goals.goals.keys(): marker_obj = None
goals.append({'name': goal_name, 'strategy': goals[goal_name]}) if marker:
else: marker_obj = objects.Goal.get_by_uuid(
for name, strategy in CONF.watcher_goals.goals.items(): pecan.request.context, marker)
goals.append({'name': name, 'strategy': strategy})
return GoalCollection.convert_with_links(goals[:limit], limit, goals = objects.Goal.list(pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
return GoalCollection.convert_with_links(goals, limit,
url=resource_url, url=resource_url,
expand=expand, expand=expand,
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) sort_dir=sort_dir)
@wsme_pecan.wsexpose(GoalCollection, int, wtypes.text, wtypes.text) @wsme_pecan.wsexpose(GoalCollection, wtypes.text,
def get_all(self, limit=None, int, wtypes.text, wtypes.text)
sort_key='name', sort_dir='asc'): def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of goals. """Retrieve a list of goals.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
to get only actions for that goal.
""" """
return self._get_goals_collection(limit, sort_key, sort_dir) return self._get_goals_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int, @wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
wtypes.text, wtypes.text) wtypes.text, wtypes.text)
def detail(self, goal_name=None, limit=None, def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
sort_key='name', sort_dir='asc'): """Retrieve a list of goals with detail.
"""Retrieve a list of actions with detail.
:param goal_name: name of a goal, to get only goals for that :param marker: pagination marker for large data sets.
action.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
to get only goals for that goal.
""" """
# NOTE(lucasagomes): /detail should only work agaist collections # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
@@ -206,21 +208,23 @@ class GoalsController(rest.RestController):
raise exception.HTTPNotFound raise exception.HTTPNotFound
expand = True expand = True
resource_url = '/'.join(['goals', 'detail']) resource_url = '/'.join(['goals', 'detail'])
return self._get_goals_collection(limit, sort_key, sort_dir, return self._get_goals_collection(marker, limit, sort_key, sort_dir,
expand, resource_url, goal_name) expand, resource_url)
@wsme_pecan.wsexpose(Goal, wtypes.text) @wsme_pecan.wsexpose(Goal, wtypes.text)
def get_one(self, goal_name): def get_one(self, goal):
"""Retrieve information about the given goal. """Retrieve information about the given goal.
:param goal_name: name of the goal. :param goal: UUID or name of the goal.
""" """
if self.from_goals: if self.from_goals:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
goals = CONF.watcher_goals.goals if common_utils.is_uuid_like(goal):
goal = {} get_goal_func = objects.Goal.get_by_uuid
if goal_name in goals.keys(): else:
goal = {'name': goal_name, 'strategy': goals[goal_name]} get_goal_func = objects.Goal.get_by_name
return Goal.convert_with_links(goal) rpc_goal = get_goal_func(pecan.request.context, goal)
return Goal.convert_with_links(rpc_goal)

View File

@@ -0,0 +1,281 @@
# -*- 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.
# 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.
"""
A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is
able to find a :ref:`Solution <solution_definition>` for a given
:ref:`Goal <goal_definition>`.
There may be several potential strategies which are able to achieve the same
:ref:`Goal <goal_definition>`. This is why it is possible to configure which
specific :ref:`Strategy <strategy_definition>` should be used for each goal.
Some strategies may provide better optimization results but may take more time
to find an optimal :ref:`Solution <solution_definition>`.
"""
from oslo_config import cfg
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from watcher.api.controllers import base
from watcher.api.controllers import link
from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import utils as common_utils
from watcher import objects
CONF = cfg.CONF
class Strategy(base.APIBase):
"""API representation of a strategy.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a strategy.
"""
_goal_uuid = None
def _get_goal(self, value):
if value == wtypes.Unset:
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
self.goal_id = goal.id
return goal
def _get_goal_uuid(self):
return self._goal_uuid
def _set_goal_uuid(self, value):
if value and self._goal_uuid != value:
self._goal_uuid = None
goal = self._get_goal(value)
if goal:
self._goal_uuid = goal.uuid
uuid = types.uuid
"""Unique UUID for this strategy"""
name = wtypes.text
"""Name of the strategy"""
display_name = wtypes.text
"""Localized name of the strategy"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated goal links"""
goal_uuid = wsme.wsproperty(wtypes.text, _get_goal_uuid, _set_goal_uuid,
mandatory=True)
"""The UUID of the goal this audit refers to"""
def __init__(self, **kwargs):
super(Strategy, self).__init__()
self.fields = []
self.fields.append('uuid')
self.fields.append('name')
self.fields.append('display_name')
self.fields.append('goal_uuid')
setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset))
setattr(self, 'name', kwargs.get('name', wtypes.Unset))
setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset))
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
@staticmethod
def _convert_with_links(strategy, url, expand=True):
if not expand:
strategy.unset_fields_except(
['uuid', 'name', 'display_name', 'goal_uuid'])
strategy.links = [
link.Link.make_link('self', url, 'strategies', strategy.uuid),
link.Link.make_link('bookmark', url, 'strategies', strategy.uuid,
bookmark=True)]
return strategy
@classmethod
def convert_with_links(cls, strategy, expand=True):
strategy = Strategy(**strategy.as_dict())
return cls._convert_with_links(
strategy, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='DUMMY',
display_name='Dummy strategy')
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
class StrategyCollection(collection.Collection):
"""API representation of a collection of strategies."""
strategies = [Strategy]
"""A list containing strategies objects"""
def __init__(self, **kwargs):
super(StrategyCollection, self).__init__()
self._type = 'strategies'
@staticmethod
def convert_with_links(strategies, limit, url=None, expand=False,
**kwargs):
strategy_collection = StrategyCollection()
strategy_collection.strategies = [
Strategy.convert_with_links(g, expand) for g in strategies]
if 'sort_key' in kwargs:
reverse = False
if kwargs['sort_key'] == 'strategy':
if 'sort_dir' in kwargs:
reverse = True if kwargs['sort_dir'] == 'desc' else False
strategy_collection.strategies = sorted(
strategy_collection.strategies,
key=lambda strategy: strategy.uuid,
reverse=reverse)
strategy_collection.next = strategy_collection.get_next(
limit, url=url, **kwargs)
return strategy_collection
@classmethod
def sample(cls):
sample = cls()
sample.strategies = [Strategy.sample(expand=False)]
return sample
class StrategiesController(rest.RestController):
"""REST controller for Strategies."""
def __init__(self):
super(StrategiesController, self).__init__()
from_strategies = False
"""A flag to indicate if the requests to this controller are coming
from the top-level resource Strategies."""
_custom_actions = {
'detail': ['GET'],
}
def _get_strategies_collection(self, filters, marker, limit, sort_key,
sort_dir, expand=False, resource_url=None):
api_utils.validate_search_filters(
filters, list(objects.strategy.Strategy.fields.keys()) +
["goal_uuid", "goal_name"])
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
sort_db_key = (sort_key if sort_key in objects.Strategy.fields.keys()
else None)
marker_obj = None
if marker:
marker_obj = objects.Strategy.get_by_uuid(
pecan.request.context, marker)
strategies = objects.Strategy.list(
pecan.request.context, limit, marker_obj, filters=filters,
sort_key=sort_db_key, sort_dir=sort_dir)
return StrategyCollection.convert_with_links(
strategies, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text,
int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of strategies.
:param goal: goal UUID or name to filter by.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, int,
wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of strategies with detail.
:param goal: goal UUID or name to filter by.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "strategies":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['strategies', 'detail'])
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir, expand, resource_url)
@wsme_pecan.wsexpose(Strategy, wtypes.text)
def get_one(self, strategy):
"""Retrieve information about the given strategy.
:param strategy: UUID or name of the strategy.
"""
if self.from_strategies:
raise exception.OperationNotPermitted
if common_utils.is_uuid_like(strategy):
get_strategy_func = objects.Strategy.get_by_uuid
else:
get_strategy_func = objects.Strategy.get_by_name
rpc_strategy = get_strategy_func(pecan.request.context, strategy)
return Strategy.convert_with_links(rpc_strategy)

View File

@@ -31,11 +31,6 @@ class UuidOrNameType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'uuid_or_name' name = 'uuid_or_name'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -55,11 +50,6 @@ class NameType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'name' name = 'name'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -79,11 +69,6 @@ class UuidType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'uuid' name = 'uuid'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -103,11 +88,6 @@ class BooleanType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'boolean' name = 'boolean'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -129,11 +109,6 @@ class JsonType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'json' name = 'json'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
def __str__(self): def __str__(self):
# These are the json serializable native types # These are the json serializable native types

View File

@@ -23,18 +23,26 @@ import abc
import six import six
from watcher.common import clients from watcher.common import clients
from watcher.common.loader import loadable
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseAction(object): class BaseAction(loadable.Loadable):
# NOTE(jed) by convention we decided # NOTE(jed) by convention we decided
# that the attribute "resource_id" is the unique id of # that the attribute "resource_id" is the unique id of
# the resource to which the Action applies to allow us to use it in the # the resource to which the Action applies to allow us to use it in the
# watcher dashboard and will be nested in input_parameters # watcher dashboard and will be nested in input_parameters
RESOURCE_ID = 'resource_id' RESOURCE_ID = 'resource_id'
def __init__(self, osc=None): def __init__(self, config, osc=None):
""":param osc: an OpenStackClients instance""" """Constructor
:param config: A mapping containing the configuration of this action
:type config: dict
:param osc: an OpenStackClients instance, defaults to None
:type osc: :py:class:`~.OpenStackClients` instance, optional
"""
super(BaseAction, self).__init__(config)
self._input_parameters = {} self._input_parameters = {}
self._osc = osc self._osc = osc
@@ -56,6 +64,15 @@ class BaseAction(object):
def resource_id(self): def resource_id(self):
return self.input_parameters[self.RESOURCE_ID] return self.input_parameters[self.RESOURCE_ID]
@classmethod
def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
return []
@abc.abstractmethod @abc.abstractmethod
def execute(self): def execute(self):
"""Executes the main logic of the action """Executes the main logic of the action

View File

@@ -17,12 +17,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_log import log
from watcher.common.loader import default from watcher.common.loader import default
LOG = log.getLogger(__name__)
class DefaultActionLoader(default.DefaultLoader): class DefaultActionLoader(default.DefaultLoader):
def __init__(self): def __init__(self):

View File

@@ -31,27 +31,24 @@ LOG = log.getLogger(__name__)
class Migrate(base.BaseAction): class Migrate(base.BaseAction):
"""Live-Migrates a server to a destination nova-compute host """Migrates a server to a destination nova-compute host
This action will allow you to migrate a server to another compute This action will allow you to migrate a server to another compute
destination host. As of now, only live migration can be performed using destination host.
this action. Migration type 'live' can only be used for migrating active VMs.
.. If either host uses shared storage, you can use ``live`` Migration type 'cold' can be used for migrating non-active VMs
.. as ``migration_type``. If both source and destination hosts provide as well active VMs, which will be shut down while migrating.
.. local disks, you can set the block_migration parameter to True (not
.. supported for yet).
The action schema is:: The action schema is::
schema = Schema({ schema = Schema({
'resource_id': str, # should be a UUID 'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live" only 'migration_type': str, # choices -> "live", "cold"
'dst_hypervisor': str, 'dst_hypervisor': str,
'src_hypervisor': str, 'src_hypervisor': str,
}) })
The `resource_id` is the UUID of the server to migrate. Only live migration The `resource_id` is the UUID of the server to migrate.
is supported.
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
source and the destination compute hostname (list of available compute source and the destination compute hostname (list of available compute
hosts is returned by this command: ``nova service-list --binary hosts is returned by this command: ``nova service-list --binary
@@ -61,6 +58,7 @@ class Migrate(base.BaseAction):
# input parameters constants # input parameters constants
MIGRATION_TYPE = 'migration_type' MIGRATION_TYPE = 'migration_type'
LIVE_MIGRATION = 'live' LIVE_MIGRATION = 'live'
COLD_MIGRATION = 'cold'
DST_HYPERVISOR = 'dst_hypervisor' DST_HYPERVISOR = 'dst_hypervisor'
SRC_HYPERVISOR = 'src_hypervisor' SRC_HYPERVISOR = 'src_hypervisor'
@@ -77,7 +75,8 @@ class Migrate(base.BaseAction):
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id, voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
voluptuous.Required(self.MIGRATION_TYPE, voluptuous.Required(self.MIGRATION_TYPE,
default=self.LIVE_MIGRATION): default=self.LIVE_MIGRATION):
voluptuous.Any(*[self.LIVE_MIGRATION]), voluptuous.Any(*[self.LIVE_MIGRATION,
self.COLD_MIGRATION]),
voluptuous.Required(self.DST_HYPERVISOR): voluptuous.Required(self.DST_HYPERVISOR):
voluptuous.All(voluptuous.Any(*six.string_types), voluptuous.All(voluptuous.Any(*six.string_types),
voluptuous.Length(min=1)), voluptuous.Length(min=1)),
@@ -127,14 +126,30 @@ class Migrate(base.BaseAction):
return result return result
def _cold_migrate_instance(self, nova, destination):
result = None
try:
result = nova.watcher_non_live_migrate_instance(
instance_id=self.instance_uuid,
dest_hostname=destination)
except Exception as exc:
LOG.exception(exc)
LOG.critical(_LC("Unexpected error occured. Migration failed for"
"instance %s. Leaving instance on previous "
"host."), self.instance_uuid)
return result
def migrate(self, destination): def migrate(self, destination):
nova = nova_helper.NovaHelper(osc=self.osc) nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Migrate instance %s to %s", self.instance_uuid, LOG.debug("Migrate instance %s to %s", self.instance_uuid,
destination) destination)
instance = nova.find_instance(self.instance_uuid) instance = nova.find_instance(self.instance_uuid)
if instance: if instance:
if self.migration_type == 'live': if self.migration_type == self.LIVE_MIGRATION:
return self._live_migrate_instance(nova, destination) return self._live_migrate_instance(nova, destination)
elif self.migration_type == self.COLD_MIGRATION:
return self._cold_migrate_instance(nova, destination)
else: else:
raise exception.Invalid( raise exception.Invalid(
message=(_('Migration of type %(migration_type)s is not ' message=(_('Migration of type %(migration_type)s is not '

View File

@@ -21,7 +21,6 @@ from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.applier.messaging import trigger from watcher.applier.messaging import trigger
from watcher.common.messaging import messaging_core
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@@ -63,17 +62,15 @@ CONF.register_group(opt_group)
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group) CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
class ApplierManager(messaging_core.MessagingCore): class ApplierManager(object):
def __init__(self):
super(ApplierManager, self).__init__(
CONF.watcher_applier.publisher_id,
CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.status_topic,
api_version=self.API_VERSION,
)
self.conductor_topic_handler.add_endpoint(
trigger.TriggerActionPlan(self))
def join(self): API_VERSION = '1.0'
self.conductor_topic_handler.join()
self.status_topic_handler.join() conductor_endpoints = [trigger.TriggerActionPlan]
status_endpoints = []
def __init__(self):
self.publisher_id = CONF.watcher_applier.publisher_id
self.conductor_topic = CONF.watcher_applier.conductor_topic
self.status_topic = CONF.watcher_applier.status_topic
self.api_version = self.API_VERSION

View File

@@ -18,48 +18,43 @@
# #
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
import oslo_messaging as om
from watcher.applier.manager import APPLIER_MANAGER_OPTS from watcher.applier import manager
from watcher.applier.manager import opt_group
from watcher.common import exception from watcher.common import exception
from watcher.common.messaging import messaging_core
from watcher.common.messaging import notification_handler as notification from watcher.common.messaging import notification_handler as notification
from watcher.common import service
from watcher.common import utils from watcher.common import utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
CONF.register_group(opt_group) CONF.register_group(manager.opt_group)
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group) CONF.register_opts(manager.APPLIER_MANAGER_OPTS, manager.opt_group)
class ApplierAPI(messaging_core.MessagingCore): class ApplierAPI(service.Service):
def __init__(self): def __init__(self):
super(ApplierAPI, self).__init__( super(ApplierAPI, self).__init__(ApplierAPIManager)
CONF.watcher_applier.publisher_id,
CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.status_topic,
api_version=self.API_VERSION,
)
self.handler = notification.NotificationHandler(self.publisher_id)
self.handler.register_observer(self)
self.status_topic_handler.add_endpoint(self.handler)
transport = om.get_transport(CONF)
target = om.Target(
topic=CONF.watcher_applier.conductor_topic,
version=self.API_VERSION,
)
self.client = om.RPCClient(transport, target,
serializer=self.serializer)
def launch_action_plan(self, context, action_plan_uuid=None): def launch_action_plan(self, context, action_plan_uuid=None):
if not utils.is_uuid_like(action_plan_uuid): if not utils.is_uuid_like(action_plan_uuid):
raise exception.InvalidUuidOrName(name=action_plan_uuid) raise exception.InvalidUuidOrName(name=action_plan_uuid)
return self.client.call( return self.conductor_client.call(
context.to_dict(), 'launch_action_plan', context.to_dict(), 'launch_action_plan',
action_plan_uuid=action_plan_uuid) action_plan_uuid=action_plan_uuid)
class ApplierAPIManager(object):
API_VERSION = '1.0'
conductor_endpoints = []
status_endpoints = [notification.NotificationHandler]
def __init__(self):
self.publisher_id = CONF.watcher_applier.publisher_id
self.conductor_topic = CONF.watcher_applier.conductor_topic
self.status_topic = CONF.watcher_applier.status_topic
self.api_version = self.API_VERSION

View File

@@ -23,18 +23,38 @@ import six
from watcher.applier.actions import factory from watcher.applier.actions import factory
from watcher.applier.messaging import event_types from watcher.applier.messaging import event_types
from watcher.common import clients from watcher.common import clients
from watcher.common.loader import loadable
from watcher.common.messaging.events import event from watcher.common.messaging.events import event
from watcher import objects from watcher import objects
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseWorkFlowEngine(object): class BaseWorkFlowEngine(loadable.Loadable):
def __init__(self, context=None, applier_manager=None):
def __init__(self, config, context=None, applier_manager=None):
"""Constructor
:param config: A mapping containing the configuration of this
workflow engine
:type config: dict
:param osc: an OpenStackClients object, defaults to None
:type osc: :py:class:`~.OpenStackClients` instance, optional
"""
super(BaseWorkFlowEngine, self).__init__(config)
self._context = context self._context = context
self._applier_manager = applier_manager self._applier_manager = applier_manager
self._action_factory = factory.ActionFactory() self._action_factory = factory.ActionFactory()
self._osc = None self._osc = None
@classmethod
def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
return []
@property @property
def context(self): def context(self):
return self._context return self._context

View File

@@ -17,12 +17,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_log import log
from watcher.common.loader import default from watcher.common.loader import default
LOG = log.getLogger(__name__)
class DefaultWorkFlowEngineLoader(default.DefaultLoader): class DefaultWorkFlowEngineLoader(default.DefaultLoader):
def __init__(self): def __init__(self):

View File

@@ -17,19 +17,14 @@
"""Starter script for the Watcher API service.""" """Starter script for the Watcher API service."""
import logging as std_logging
import os
import sys import sys
from wsgiref import simple_server
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from watcher._i18n import _ from watcher._i18n import _LI
from watcher.api import app as api_app
from watcher.common import service from watcher.common import service
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@@ -37,22 +32,20 @@ CONF = cfg.CONF
def main(): def main():
service.prepare_service(sys.argv) service.prepare_service(sys.argv)
app = api_app.setup_app()
# Create the WSGI server and start it
host, port = cfg.CONF.api.host, cfg.CONF.api.port host, port = cfg.CONF.api.host, cfg.CONF.api.port
srv = simple_server.make_server(host, port, app) protocol = "http" if not CONF.api.enable_ssl_api else "https"
# Build and start the WSGI app
LOG.info(_('Starting server in PID %s') % os.getpid()) server = service.WSGIService(
LOG.debug("Watcher configuration:") 'watcher-api', CONF.api.enable_ssl_api)
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
if host == '0.0.0.0': if host == '0.0.0.0':
LOG.info(_('serving on 0.0.0.0:%(port)s, ' LOG.info(_LI('serving on 0.0.0.0:%(port)s, '
'view at http://127.0.0.1:%(port)s') % 'view at %(protocol)s://127.0.0.1:%(port)s') %
dict(port=port)) dict(protocol=protocol, port=port))
else: else:
LOG.info(_('serving on http://%(host)s:%(port)s') % LOG.info(_LI('serving on %(protocol)s://%(host)s:%(port)s') %
dict(host=host, port=port)) dict(protocol=protocol, host=host, port=port))
srv.serve_forever() launcher = service.process_launcher()
launcher.launch_service(server, workers=server.workers)
launcher.wait()

View File

@@ -17,29 +17,26 @@
"""Starter script for the Applier service.""" """Starter script for the Applier service."""
import logging as std_logging
import os import os
import sys import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import service
from watcher import _i18n from watcher._i18n import _LI
from watcher.applier import manager from watcher.applier import manager
from watcher.common import service from watcher.common import service as watcher_service
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
_LI = _i18n._LI
def main(): def main():
service.prepare_service(sys.argv) watcher_service.prepare_service(sys.argv)
LOG.info(_LI('Starting server in PID %s') % os.getpid()) LOG.info(_LI('Starting Watcher Applier service in PID %s'), os.getpid())
LOG.debug("Configuration:")
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
server = manager.ApplierManager() applier_service = watcher_service.Service(manager.ApplierManager)
server.connect() launcher = service.launch(CONF, applier_service)
server.join() launcher.wait()

View File

@@ -17,30 +17,31 @@
"""Starter script for the Decision Engine manager service.""" """Starter script for the Decision Engine manager service."""
import logging as std_logging
import os import os
import sys import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import service
from watcher import _i18n from watcher._i18n import _LI
from watcher.common import service from watcher.common import service as watcher_service
from watcher.decision_engine import manager from watcher.decision_engine import manager
from watcher.decision_engine import sync
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
_LI = _i18n._LI
def main(): def main():
service.prepare_service(sys.argv) watcher_service.prepare_service(sys.argv)
LOG.info(_LI('Starting server in PID %s') % os.getpid()) LOG.info(_LI('Starting Watcher Decision Engine service in PID %s'),
LOG.debug("Configuration:") os.getpid())
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
server = manager.DecisionEngineManager() syncer = sync.Syncer()
server.connect() syncer.sync()
server.join()
de_service = watcher_service.Service(manager.DecisionEngineManager)
launcher = service.launch(CONF, de_service)
launcher.wait()

View File

@@ -123,7 +123,7 @@ class CeilometerHelper(object):
item_value = None item_value = None
if statistic: if statistic:
item_value = statistic[-1]._info.get('aggregate').get('avg') item_value = statistic[-1]._info.get('aggregate').get(aggregate)
return item_value return item_value
def get_last_sample_values(self, resource_id, meter_name, limit=1): def get_last_sample_values(self, resource_id, meter_name, limit=1):

View File

@@ -147,11 +147,15 @@ class ResourceNotFound(ObjectNotFound):
class InvalidIdentity(Invalid): class InvalidIdentity(Invalid):
msg_fmt = _("Expected an uuid or int but received %(identity)s") msg_fmt = _("Expected a uuid or int but received %(identity)s")
class InvalidGoal(Invalid): class InvalidGoal(Invalid):
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file") msg_fmt = _("Goal %(goal)s is invalid")
class InvalidStrategy(Invalid):
msg_fmt = _("Strategy %(strategy)s is invalid")
class InvalidUUID(Invalid): class InvalidUUID(Invalid):
@@ -166,12 +170,28 @@ class InvalidUuidOrName(Invalid):
msg_fmt = _("Expected a logical name or uuid but received %(name)s") msg_fmt = _("Expected a logical name or uuid but received %(name)s")
class GoalNotFound(ResourceNotFound):
msg_fmt = _("Goal %(goal)s could not be found")
class GoalAlreadyExists(Conflict):
msg_fmt = _("A goal with UUID %(uuid)s already exists")
class StrategyNotFound(ResourceNotFound):
msg_fmt = _("Strategy %(strategy)s could not be found")
class StrategyAlreadyExists(Conflict):
msg_fmt = _("A strategy with UUID %(uuid)s already exists")
class AuditTemplateNotFound(ResourceNotFound): class AuditTemplateNotFound(ResourceNotFound):
msg_fmt = _("AuditTemplate %(audit_template)s could not be found") msg_fmt = _("AuditTemplate %(audit_template)s could not be found")
class AuditTemplateAlreadyExists(Conflict): class AuditTemplateAlreadyExists(Conflict):
msg_fmt = _("An audit_template with UUID %(uuid)s or name %(name)s " msg_fmt = _("An audit_template with UUID or name %(audit_template)s "
"already exists") "already exists")
@@ -180,6 +200,10 @@ class AuditTemplateReferenced(Invalid):
"multiple audit") "multiple audit")
class AuditTypeNotFound(Invalid):
msg_fmt = _("Audit type %(audit_type)s could not be found")
class AuditNotFound(ResourceNotFound): class AuditNotFound(ResourceNotFound):
msg_fmt = _("Audit %(audit)s could not be found") msg_fmt = _("Audit %(audit)s could not be found")
@@ -267,7 +291,19 @@ class MetricCollectorNotDefined(WatcherException):
class ClusterStateNotDefined(WatcherException): class ClusterStateNotDefined(WatcherException):
msg_fmt = _("the cluster state is not defined") msg_fmt = _("The cluster state is not defined")
class NoAvailableStrategyForGoal(WatcherException):
msg_fmt = _("No strategy could be found to achieve the '%(goal)s' goal.")
class NoMetricValuesForVM(WatcherException):
msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.")
class NoSuchMetricForHost(WatcherException):
msg_fmt = _("No %(metric)s metric for %(host)s found.")
# Model # Model

View File

@@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com # Copyright (c) 2016 b<>com
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -16,33 +16,82 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_config import cfg
from oslo_log import log from oslo_log import log
from stevedore.driver import DriverManager from stevedore import driver as drivermanager
from stevedore import ExtensionManager from stevedore import extension as extensionmanager
from watcher.common import exception from watcher.common import exception
from watcher.common.loader.base import BaseLoader from watcher.common.loader import base
from watcher.common import utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class DefaultLoader(BaseLoader): class DefaultLoader(base.BaseLoader):
def __init__(self, namespace):
def __init__(self, namespace, conf=cfg.CONF):
"""Entry point loader for Watcher using Stevedore
:param namespace: namespace of the entry point(s) to load or list
:type namespace: str
:param conf: ConfigOpts instance, defaults to cfg.CONF
"""
super(DefaultLoader, self).__init__() super(DefaultLoader, self).__init__()
self.namespace = namespace self.namespace = namespace
self.conf = conf
def load(self, name, **kwargs): def load(self, name, **kwargs):
try: try:
LOG.debug("Loading in namespace %s => %s ", self.namespace, name) LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
driver_manager = DriverManager(namespace=self.namespace, driver_manager = drivermanager.DriverManager(
name=name) namespace=self.namespace,
loaded = driver_manager.driver name=name,
invoke_on_load=False,
)
driver_cls = driver_manager.driver
config = self._load_plugin_config(name, driver_cls)
driver = driver_cls(config, **kwargs)
except Exception as exc: except Exception as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.LoadingError(name=name) raise exception.LoadingError(name=name)
return loaded(**kwargs) return driver
def _reload_config(self):
self.conf()
def get_entry_name(self, name):
return ".".join([self.namespace, name])
def _load_plugin_config(self, name, driver_cls):
"""Load the config of the plugin"""
config = utils.Struct()
config_opts = driver_cls.get_config_opts()
if not config_opts:
return config
group_name = self.get_entry_name(name)
self.conf.register_opts(config_opts, group=group_name)
# Finalise the opt import by re-checking the configuration
# against the provided config files
self._reload_config()
config_group = self.conf.get(group_name)
if not config_group:
raise exception.LoadingError(name=name)
config.update({
name: value for name, value in config_group.items()
})
return config
def list_available(self): def list_available(self):
extension_manager = ExtensionManager(namespace=self.namespace) extension_manager = extensionmanager.ExtensionManager(
namespace=self.namespace)
return {ext.name: ext.plugin for ext in extension_manager.extensions} return {ext.name: ext.plugin for ext in extension_manager.extensions}

View File

@@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com # Copyright (c) 2016 b<>com
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -13,16 +13,29 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import abc import abc
import six import six
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class FakeLoadable(object): class Loadable(object):
@classmethod """Generic interface for dynamically loading a driver/entry point.
def namespace(cls):
return "TESTING" This defines the contract in order to let the loader manager inject
the configuration parameters during the loading.
"""
def __init__(self, config):
self.config = config
@classmethod @classmethod
def get_name(cls): @abc.abstractmethod
return 'fake' def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
raise NotImplementedError

View File

@@ -1,122 +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_config import cfg
from oslo_log import log
import oslo_messaging as om
from watcher.common.messaging.events import event_dispatcher as dispatcher
from watcher.common.messaging import messaging_handler
from watcher.common import rpc
from watcher.objects import base
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class MessagingCore(dispatcher.EventDispatcher):
API_VERSION = '1.0'
def __init__(self, publisher_id, conductor_topic, status_topic,
api_version=API_VERSION):
super(MessagingCore, self).__init__()
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self.publisher_id = publisher_id
self.api_version = api_version
self.conductor_topic = conductor_topic
self.status_topic = status_topic
self.conductor_topic_handler = self.build_topic_handler(
conductor_topic)
self.status_topic_handler = self.build_topic_handler(status_topic)
self._conductor_client = None
self._status_client = None
@property
def conductor_client(self):
if self._conductor_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.conductor_topic,
version=self.API_VERSION,
)
self._conductor_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._conductor_client
@conductor_client.setter
def conductor_client(self, c):
self.conductor_client = c
@property
def status_client(self):
if self._status_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.status_topic,
version=self.API_VERSION,
)
self._status_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._status_client
@status_client.setter
def status_client(self, c):
self.status_client = c
def build_topic_handler(self, topic_name):
return messaging_handler.MessagingHandler(
self.publisher_id, topic_name, self,
self.api_version, self.serializer)
def connect(self):
LOG.debug("Connecting to '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.start()
self.status_topic_handler.start()
def disconnect(self):
LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.stop()
self.status_topic_handler.stop()
def publish_control(self, event, payload):
return self.conductor_topic_handler.publish_event(event, payload)
def publish_status(self, event, payload, request_id=None):
return self.status_topic_handler.publish_event(
event, payload, request_id)
def get_version(self):
return self.api_version
def check_api_version(self, context):
api_manager_version = self.conductor_client.call(
context.to_dict(), 'check_api_version',
api_version=self.api_version)
return api_manager_version
def response(self, evt, ctx, message):
payload = {
'request_id': ctx['request_id'],
'msg': message
}
self.publish_status(evt, payload)

View File

@@ -38,7 +38,7 @@ CONF = cfg.CONF
class MessagingHandler(threading.Thread): class MessagingHandler(threading.Thread):
def __init__(self, publisher_id, topic_name, endpoint, version, def __init__(self, publisher_id, topic_name, endpoints, version,
serializer=None): serializer=None):
super(MessagingHandler, self).__init__() super(MessagingHandler, self).__init__()
self.publisher_id = publisher_id self.publisher_id = publisher_id
@@ -50,10 +50,10 @@ class MessagingHandler(threading.Thread):
self.__server = None self.__server = None
self.__notifier = None self.__notifier = None
self.__transport = None self.__transport = None
self.add_endpoint(endpoint) self.add_endpoints(endpoints)
def add_endpoint(self, endpoint): def add_endpoints(self, endpoints):
self.__endpoints.append(endpoint) self.__endpoints.extend(endpoints)
def remove_endpoint(self, endpoint): def remove_endpoint(self, endpoint):
if endpoint in self.__endpoints: if endpoint in self.__endpoints:

View File

@@ -15,14 +15,12 @@
# limitations under the License. # limitations under the License.
import eventlet import eventlet
from oslo_log import log
import oslo_messaging as messaging import oslo_messaging as messaging
from watcher.common.messaging.utils import observable from watcher.common.messaging.utils import observable
eventlet.monkey_patch() eventlet.monkey_patch()
LOG = log.getLogger(__name__)
class NotificationHandler(observable.Observable): class NotificationHandler(observable.Observable):

View File

@@ -87,7 +87,6 @@ class NovaHelper(object):
return False return False
else: else:
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host") host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
# https://bugs.launchpad.net/nova/+bug/1182965
LOG.debug( LOG.debug(
"Instance %s found on host '%s'." % (instance_id, host_name)) "Instance %s found on host '%s'." % (instance_id, host_name))
@@ -532,16 +531,12 @@ class NovaHelper(object):
"Trying to create new instance '%s' " "Trying to create new instance '%s' "
"from image '%s' with flavor '%s' ..." % ( "from image '%s' with flavor '%s' ..." % (
inst_name, image_id, flavor_name)) inst_name, image_id, flavor_name))
# TODO(jed) wait feature
# Allow admin users to view any keypair try:
# https://bugs.launchpad.net/nova/+bug/1182965 self.nova.keypairs.findall(name=keypair_name)
if not self.nova.keypairs.findall(name=keypair_name): except nvexceptions.NotFound:
LOG.debug("Key pair '%s' not found with user '%s'" % ( LOG.debug("Key pair '%s' not found " % keypair_name)
keypair_name, self.user))
return return
else:
LOG.debug("Key pair '%s' found with user '%s'" % (
keypair_name, self.user))
try: try:
image = self.nova.images.get(image_id) image = self.nova.images.get(image_id)

View File

@@ -15,108 +15,46 @@
# under the License. # under the License.
import logging import logging
import signal
import socket import socket
from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_log import _options from oslo_log import _options
from oslo_log import log from oslo_log import log
import oslo_messaging as messaging import oslo_messaging as om
from oslo_reports import guru_meditation_report as gmr
from oslo_reports import opts as gmr_opts
from oslo_service import service from oslo_service import service
from oslo_utils import importutils from oslo_service import wsgi
from watcher._i18n import _LE from watcher._i18n import _
from watcher._i18n import _LI from watcher.api import app
from watcher.common import config from watcher.common import config
from watcher.common import context from watcher.common.messaging.events import event_dispatcher as dispatcher
from watcher.common.messaging import messaging_handler
from watcher.common import rpc from watcher.common import rpc
from watcher.objects import base as objects_base from watcher.objects import base
from watcher import opts
from watcher import version
service_opts = [ service_opts = [
cfg.IntOpt('periodic_interval', cfg.IntOpt('periodic_interval',
default=60, default=60,
help='Seconds between running periodic tasks.'), help=_('Seconds between running periodic tasks.')),
cfg.StrOpt('host', cfg.StrOpt('host',
default=socket.getfqdn(), default=socket.getfqdn(),
help='Name of this node. This can be an opaque identifier. ' help=_('Name of this node. This can be an opaque identifier. '
'It is not necessarily a hostname, FQDN, or IP address. ' 'It is not necessarily a hostname, FQDN, or IP address. '
'However, the node name must be valid within ' 'However, the node name must be valid within '
'an AMQP key, and if using ZeroMQ, a valid ' 'an AMQP key, and if using ZeroMQ, a valid '
'hostname, FQDN, or IP address.'), 'hostname, FQDN, or IP address.')),
] ]
cfg.CONF.register_opts(service_opts) cfg.CONF.register_opts(service_opts)
CONF = cfg.CONF
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class RPCService(service.Service):
def __init__(self, host, manager_module, manager_class):
super(RPCService, self).__init__()
self.host = host
manager_module = importutils.try_import(manager_module)
manager_class = getattr(manager_module, manager_class)
self.manager = manager_class(host, manager_module.MANAGER_TOPIC)
self.topic = self.manager.topic
self.rpcserver = None
self.deregister = True
def start(self):
super(RPCService, self).start()
admin_context = context.RequestContext('admin', 'admin', is_admin=True)
target = messaging.Target(topic=self.topic, server=self.host)
endpoints = [self.manager]
serializer = objects_base.IronicObjectSerializer()
self.rpcserver = rpc.get_server(target, endpoints, serializer)
self.rpcserver.start()
self.handle_signal()
self.manager.init_host()
self.tg.add_dynamic_timer(
self.manager.periodic_tasks,
periodic_interval_max=cfg.CONF.periodic_interval,
context=admin_context)
LOG.info(_LI('Created RPC server for service %(service)s on host '
'%(host)s.'),
{'service': self.topic, 'host': self.host})
def stop(self):
try:
self.rpcserver.stop()
self.rpcserver.wait()
except Exception as e:
LOG.exception(_LE('Service error occurred when stopping the '
'RPC server. Error: %s'), e)
try:
self.manager.del_host(deregister=self.deregister)
except Exception as e:
LOG.exception(_LE('Service error occurred when cleaning up '
'the RPC manager. Error: %s'), e)
super(RPCService, self).stop(graceful=True)
LOG.info(_LI('Stopped RPC server for service %(service)s on host '
'%(host)s.'),
{'service': self.topic, 'host': self.host})
def _handle_signal(self):
LOG.info(_LI('Got signal SIGUSR1. Not deregistering on next shutdown '
'of service %(service)s on host %(host)s.'),
{'service': self.topic, 'host': self.host})
self.deregister = False
def handle_signal(self):
"""Add a signal handler for SIGUSR1.
The handler ensures that the manager is not deregistered when it is
shutdown.
"""
signal.signal(signal.SIGUSR1, self._handle_signal)
_DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO', _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
'oslo.messaging=INFO', 'sqlalchemy=WARN', 'oslo.messaging=INFO', 'sqlalchemy=WARN',
'keystoneclient=INFO', 'stevedore=INFO', 'keystoneclient=INFO', 'stevedore=INFO',
@@ -125,10 +63,165 @@ _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
'glanceclient=WARN', 'watcher.openstack.common=WARN'] 'glanceclient=WARN', 'watcher.openstack.common=WARN']
def prepare_service(argv=[], conf=cfg.CONF): class WSGIService(service.ServiceBase):
"""Provides ability to launch Watcher API from wsgi app."""
def __init__(self, name, use_ssl=False):
"""Initialize, but do not start the WSGI server.
:param name: The name of the WSGI server given to the loader.
:param use_ssl: Wraps the socket in an SSL context if True.
"""
self.name = name
self.app = app.VersionSelectorApplication()
self.workers = (CONF.api.workers or
processutils.get_worker_count())
self.server = wsgi.Server(CONF, name, self.app,
host=CONF.api.host,
port=CONF.api.port,
use_ssl=use_ssl,
logger_name=name)
def start(self):
"""Start serving this service using loaded configuration"""
self.server.start()
def stop(self):
"""Stop serving this API"""
self.server.stop()
def wait(self):
"""Wait for the service to stop serving this API"""
self.server.wait()
def reset(self):
"""Reset server greenpool size to default"""
self.server.reset()
class Service(service.ServiceBase, dispatcher.EventDispatcher):
API_VERSION = '1.0'
def __init__(self, manager_class):
super(Service, self).__init__()
self.manager = manager_class()
self.publisher_id = self.manager.publisher_id
self.api_version = self.manager.API_VERSION
self.conductor_topic = self.manager.conductor_topic
self.status_topic = self.manager.status_topic
self.conductor_endpoints = [
ep(self) for ep in self.manager.conductor_endpoints
]
self.status_endpoints = [
ep(self.publisher_id) for ep in self.manager.status_endpoints
]
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self.conductor_topic_handler = self.build_topic_handler(
self.conductor_topic, self.conductor_endpoints)
self.status_topic_handler = self.build_topic_handler(
self.status_topic, self.status_endpoints)
self._conductor_client = None
self._status_client = None
@property
def conductor_client(self):
if self._conductor_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.conductor_topic,
version=self.API_VERSION,
)
self._conductor_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._conductor_client
@conductor_client.setter
def conductor_client(self, c):
self.conductor_client = c
@property
def status_client(self):
if self._status_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.status_topic,
version=self.API_VERSION,
)
self._status_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._status_client
@status_client.setter
def status_client(self, c):
self.status_client = c
def build_topic_handler(self, topic_name, endpoints=()):
return messaging_handler.MessagingHandler(
self.publisher_id, topic_name, [self.manager] + list(endpoints),
self.api_version, self.serializer)
def start(self):
LOG.debug("Connecting to '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.start()
self.status_topic_handler.start()
def stop(self):
LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.stop()
self.status_topic_handler.stop()
def reset(self):
"""Reset a service in case it received a SIGHUP."""
def wait(self):
"""Wait for service to complete."""
def publish_control(self, event, payload):
return self.conductor_topic_handler.publish_event(event, payload)
def publish_status(self, event, payload, request_id=None):
return self.status_topic_handler.publish_event(
event, payload, request_id)
def get_version(self):
return self.api_version
def check_api_version(self, context):
api_manager_version = self.conductor_client.call(
context.to_dict(), 'check_api_version',
api_version=self.api_version)
return api_manager_version
def response(self, evt, ctx, message):
payload = {
'request_id': ctx['request_id'],
'msg': message
}
self.publish_status(evt, payload)
def process_launcher(conf=cfg.CONF):
return service.ProcessLauncher(conf)
def prepare_service(argv=(), conf=cfg.CONF):
log.register_options(conf) log.register_options(conf)
gmr_opts.set_defaults(conf)
config.parse_args(argv) config.parse_args(argv)
cfg.set_defaults(_options.log_opts, cfg.set_defaults(_options.log_opts,
default_log_levels=_DEFAULT_LOG_LEVELS) default_log_levels=_DEFAULT_LOG_LEVELS)
log.setup(conf, 'python-watcher') log.setup(conf, 'python-watcher')
conf.log_opt_values(LOG, logging.DEBUG) conf.log_opt_values(LOG, logging.DEBUG)
gmr.TextGuruMeditation.register_section(_('Plugins'), opts.show_plugins)
gmr.TextGuruMeditation.setup_autorun(version)

View File

@@ -41,6 +41,29 @@ CONF.register_opts(UTILS_OPTS)
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class Struct(dict):
"""Specialized dict where you access an item like an attribute
>>> struct = Struct()
>>> struct['a'] = 1
>>> struct.b = 2
>>> assert struct.a == 1
>>> assert struct['b'] == 2
"""
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
try:
self[name] = value
except KeyError:
raise AttributeError(name)
def safe_rstrip(value, chars=None): def safe_rstrip(value, chars=None):
"""Removes trailing characters from a string if that does not make it empty """Removes trailing characters from a string if that does not make it empty
@@ -95,6 +118,14 @@ def is_hostname_safe(hostname):
:returns: True if valid. False if not. :returns: True if valid. False if not.
""" """
m = '^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$' m = r'^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$'
return (isinstance(hostname, six.string_types) and return (isinstance(hostname, six.string_types) and
(re.match(m, hostname) is not None)) (re.match(m, hostname) is not None))
def get_cls_import_path(cls):
"""Return the import path of a given class"""
module = cls.__module__
if module is None or module == str.__module__:
return cls.__name__
return module + '.' + cls.__name__

View File

@@ -34,6 +34,192 @@ def get_instance():
class BaseConnection(object): class BaseConnection(object):
"""Base class for storage system connections.""" """Base class for storage system connections."""
@abc.abstractmethod
def get_goal_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching goals.
Return a list of the specified columns for all goals that
match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of goals to return.
:param marker: the last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_goal(self, values):
"""Create a new goal.
:param values: A dict containing several items used to identify
and track the goal. For example:
::
{
'uuid': utils.generate_uuid(),
'name': 'DUMMY',
'display_name': 'Dummy',
}
:returns: A goal
:raises: :py:class:`~.GoalAlreadyExists`
"""
@abc.abstractmethod
def get_goal_by_id(self, context, goal_id):
"""Return a goal given its ID.
:param context: The security context
:param goal_id: The ID of a goal
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_goal_by_uuid(self, context, goal_uuid):
"""Return a goal given its UUID.
:param context: The security context
:param goal_uuid: The UUID of a goal
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_goal_by_name(self, context, goal_name):
"""Return a goal given its name.
:param context: The security context
:param goal_name: The name of a goal
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def destroy_goal(self, goal_uuid):
"""Destroy a goal.
:param goal_uuid: The UUID of a goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def update_goal(self, goal_uuid, values):
"""Update properties of a goal.
:param goal_uuid: The UUID of a goal
:param values: A dict containing several items used to identify
and track the goal. For example:
::
{
'uuid': utils.generate_uuid(),
'name': 'DUMMY',
'display_name': 'Dummy',
}
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
:raises: :py:class:`~.Invalid`
"""
@abc.abstractmethod
def get_strategy_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching strategies.
Return a list of the specified columns for all strategies that
match the specified filters.
:param context: The security context
:param columns: List of column names to return.
Defaults to 'id' column when columns == None.
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of strategies to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_strategy(self, values):
"""Create a new strategy.
:param values: A dict containing items used to identify
and track the strategy. For example:
::
{
'id': 1,
'uuid': utils.generate_uuid(),
'name': 'my_strategy',
'display_name': 'My strategy',
'goal_uuid': utils.generate_uuid(),
}
:returns: A strategy
:raises: :py:class:`~.StrategyAlreadyExists`
"""
@abc.abstractmethod
def get_strategy_by_id(self, context, strategy_id):
"""Return a strategy given its ID.
:param context: The security context
:param strategy_id: The ID of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_strategy_by_uuid(self, context, strategy_uuid):
"""Return a strategy given its UUID.
:param context: The security context
:param strategy_uuid: The UUID of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_strategy_by_name(self, context, strategy_name):
"""Return a strategy given its name.
:param context: The security context
:param strategy_name: The name of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def destroy_strategy(self, strategy_uuid):
"""Destroy a strategy.
:param strategy_uuid: The UUID of a strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def update_strategy(self, strategy_uuid, values):
"""Update properties of a strategy.
:param strategy_uuid: The UUID of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
:raises: :py:class:`~.Invalid`
"""
@abc.abstractmethod @abc.abstractmethod
def get_audit_template_list(self, context, columns=None, filters=None, def get_audit_template_list(self, context, columns=None, filters=None,
limit=None, marker=None, sort_key=None, limit=None, marker=None, sort_key=None,
@@ -75,7 +261,7 @@ class BaseConnection(object):
'extra': {'automatic': True} 'extra': {'automatic': True}
} }
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateAlreadyExists :raises: :py:class:`~.AuditTemplateAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -85,7 +271,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_template_id: The id of an audit template. :param audit_template_id: The id of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -95,7 +281,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_template_uuid: The uuid of an audit template. :param audit_template_uuid: The uuid of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
def get_audit_template_by_name(self, context, audit_template_name): def get_audit_template_by_name(self, context, audit_template_name):
@@ -104,7 +290,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_template_name: The name of an audit template. :param audit_template_name: The name of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -112,7 +298,7 @@ class BaseConnection(object):
"""Destroy an audit_template. """Destroy an audit_template.
:param audit_template_id: The id or uuid of an audit template. :param audit_template_id: The id or uuid of an audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -121,8 +307,8 @@ class BaseConnection(object):
:param audit_template_id: The id or uuid of an audit template. :param audit_template_id: The id or uuid of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -130,7 +316,7 @@ class BaseConnection(object):
"""Soft delete an audit_template. """Soft delete an audit_template.
:param audit_template_id: The id or uuid of an audit template. :param audit_template_id: The id or uuid of an audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -171,7 +357,7 @@ class BaseConnection(object):
'deadline': None 'deadline': None
} }
:returns: An audit. :returns: An audit.
:raises: AuditAlreadyExists :raises: :py:class:`~.AuditAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -181,7 +367,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_id: The id of an audit. :param audit_id: The id of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -191,7 +377,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_uuid: The uuid of an audit. :param audit_uuid: The uuid of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -199,7 +385,7 @@ class BaseConnection(object):
"""Destroy an audit and all associated action plans. """Destroy an audit and all associated action plans.
:param audit_id: The id or uuid of an audit. :param audit_id: The id or uuid of an audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -208,8 +394,8 @@ class BaseConnection(object):
:param audit_id: The id or uuid of an audit. :param audit_id: The id or uuid of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """
def soft_delete_audit(self, audit_id): def soft_delete_audit(self, audit_id):
@@ -217,7 +403,7 @@ class BaseConnection(object):
:param audit_id: The id or uuid of an audit. :param audit_id: The id or uuid of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -259,7 +445,7 @@ class BaseConnection(object):
'aggregate': 'nova aggregate name or uuid' 'aggregate': 'nova aggregate name or uuid'
} }
:returns: A action. :returns: A action.
:raises: ActionAlreadyExists :raises: :py:class:`~.ActionAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -269,7 +455,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_id: The id of a action. :param action_id: The id of a action.
:returns: A action. :returns: A action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -279,7 +465,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_uuid: The uuid of a action. :param action_uuid: The uuid of a action.
:returns: A action. :returns: A action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -287,8 +473,8 @@ class BaseConnection(object):
"""Destroy a action and all associated interfaces. """Destroy a action and all associated interfaces.
:param action_id: The id or uuid of a action. :param action_id: The id or uuid of a action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
:raises: ActionReferenced :raises: :py:class:`~.ActionReferenced`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -297,9 +483,9 @@ class BaseConnection(object):
:param action_id: The id or uuid of a action. :param action_id: The id or uuid of a action.
:returns: A action. :returns: A action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
:raises: ActionReferenced :raises: :py:class:`~.ActionReferenced`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -332,7 +518,7 @@ class BaseConnection(object):
:param values: A dict containing several items used to identify :param values: A dict containing several items used to identify
and track the action plan. and track the action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanAlreadyExists :raises: :py:class:`~.ActionPlanAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -342,7 +528,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_plan_id: The id of an action plan. :param action_plan_id: The id of an action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -352,7 +538,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_plan__uuid: The uuid of an action plan. :param action_plan__uuid: The uuid of an action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -360,8 +546,8 @@ class BaseConnection(object):
"""Destroy an action plan and all associated interfaces. """Destroy an action plan and all associated interfaces.
:param action_plan_id: The id or uuid of a action plan. :param action_plan_id: The id or uuid of a action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
:raises: ActionPlanReferenced :raises: :py:class:`~.ActionPlanReferenced`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -370,7 +556,7 @@ class BaseConnection(object):
:param action_plan_id: The id or uuid of an action plan. :param action_plan_id: The id or uuid of an action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
:raises: ActionPlanReferenced :raises: :py:class:`~.ActionPlanReferenced`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """

View File

@@ -28,6 +28,7 @@ import prettytable as ptable
from six.moves import input from six.moves import input
from watcher._i18n import _, _LI from watcher._i18n import _, _LI
from watcher._i18n import lazy_translation_enabled
from watcher.common import context from watcher.common import context
from watcher.common import exception from watcher.common import exception
from watcher.common import utils from watcher.common import utils
@@ -47,6 +48,8 @@ class WatcherObjectsMap(object):
# This is for generating the .pot translations # This is for generating the .pot translations
keymap = collections.OrderedDict([ keymap = collections.OrderedDict([
("goals", _("Goals")),
("strategies", _("Strategies")),
("audit_templates", _("Audit Templates")), ("audit_templates", _("Audit Templates")),
("audits", _("Audits")), ("audits", _("Audits")),
("action_plans", _("Action Plans")), ("action_plans", _("Action Plans")),
@@ -54,11 +57,11 @@ class WatcherObjectsMap(object):
]) ])
def __init__(self): def __init__(self):
for attr_name in self.__class__.keys(): for attr_name in self.keys():
setattr(self, attr_name, []) setattr(self, attr_name, [])
def values(self): def values(self):
return (getattr(self, key) for key in self.__class__.keys()) return (getattr(self, key) for key in self.keys())
@classmethod @classmethod
def keys(cls): def keys(cls):
@@ -98,8 +101,13 @@ class WatcherObjectsMap(object):
def get_count_table(self): def get_count_table(self):
headers = list(self.keymap.values()) headers = list(self.keymap.values())
headers.append(_("Total")) # We also add a total count headers.append(_("Total")) # We also add a total count
translated_headers = [
h.translate() if lazy_translation_enabled() else h
for h in headers
]
counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)] counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)]
table = ptable.PrettyTable(field_names=headers) table = ptable.PrettyTable(field_names=translated_headers)
table.add_row(counters) table.add_row(counters)
return table.get_string() return table.get_string()
@@ -143,9 +151,9 @@ class PurgeCommand(object):
query_func = None query_func = None
if not utils.is_uuid_like(uuid_or_name): if not utils.is_uuid_like(uuid_or_name):
query_func = objects.audit_template.AuditTemplate.get_by_name query_func = objects.AuditTemplate.get_by_name
else: else:
query_func = objects.audit_template.AuditTemplate.get_by_uuid query_func = objects.AuditTemplate.get_by_uuid
try: try:
audit_template = query_func(cls.ctx, uuid_or_name) audit_template = query_func(cls.ctx, uuid_or_name)
@@ -159,45 +167,64 @@ class PurgeCommand(object):
return audit_template.uuid return audit_template.uuid
def _find_goals(self, filters=None):
return objects.Goal.list(self.ctx, filters=filters)
def _find_strategies(self, filters=None):
return objects.Strategy.list(self.ctx, filters=filters)
def _find_audit_templates(self, filters=None): def _find_audit_templates(self, filters=None):
return objects.audit_template.AuditTemplate.list( return objects.AuditTemplate.list(self.ctx, filters=filters)
self.ctx, filters=filters)
def _find_audits(self, filters=None): def _find_audits(self, filters=None):
return objects.audit.Audit.list(self.ctx, filters=filters) return objects.Audit.list(self.ctx, filters=filters)
def _find_action_plans(self, filters=None): def _find_action_plans(self, filters=None):
return objects.action_plan.ActionPlan.list(self.ctx, filters=filters) return objects.ActionPlan.list(self.ctx, filters=filters)
def _find_actions(self, filters=None): def _find_actions(self, filters=None):
return objects.action.Action.list(self.ctx, filters=filters) return objects.Action.list(self.ctx, filters=filters)
def _find_orphans(self): def _find_orphans(self):
orphans = WatcherObjectsMap() orphans = WatcherObjectsMap()
filters = dict(deleted=False) filters = dict(deleted=False)
audit_templates = objects.audit_template.AuditTemplate.list( goals = objects.Goal.list(self.ctx, filters=filters)
self.ctx, filters=filters) strategies = objects.Strategy.list(self.ctx, filters=filters)
audits = objects.audit.Audit.list(self.ctx, filters=filters) audit_templates = objects.AuditTemplate.list(self.ctx, filters=filters)
action_plans = objects.action_plan.ActionPlan.list( audits = objects.Audit.list(self.ctx, filters=filters)
self.ctx, filters=filters) action_plans = objects.ActionPlan.list(self.ctx, filters=filters)
actions = objects.action.Action.list(self.ctx, filters=filters) actions = objects.Action.list(self.ctx, filters=filters)
audit_template_ids = set(at.id for at in audit_templates) goal_ids = set(g.id for g in goals)
orphans.strategies = [
strategy for strategy in strategies
if strategy.goal_id not in goal_ids]
strategy_ids = [s.id for s in (s for s in strategies
if s not in orphans.strategies)]
orphans.audit_templates = [
audit_template for audit_template in audit_templates
if audit_template.goal_id not in goal_ids or
(audit_template.strategy_id and
audit_template.strategy_id not in strategy_ids)]
audit_template_ids = [at.id for at in audit_templates
if at not in orphans.audit_templates]
orphans.audits = [ orphans.audits = [
audit for audit in audits audit for audit in audits
if audit.audit_template_id not in audit_template_ids] if audit.audit_template_id not in audit_template_ids]
# Objects with orphan parents are themselves orphans # Objects with orphan parents are themselves orphans
audit_ids = [audit.id for audit in (a for a in audits audit_ids = [audit.id for audit in audits
if a not in orphans.audits)] if audit not in orphans.audits]
orphans.action_plans = [ orphans.action_plans = [
ap for ap in action_plans ap for ap in action_plans
if ap.audit_id not in audit_ids] if ap.audit_id not in audit_ids]
# Objects with orphan parents are themselves orphans # Objects with orphan parents are themselves orphans
action_plan_ids = [ap.id for ap in (a for a in action_plans action_plan_ids = [ap.id for ap in action_plans
if a not in orphans.action_plans)] if ap not in orphans.action_plans]
orphans.actions = [ orphans.actions = [
action for action in actions action for action in actions
if action.action_plan_id not in action_plan_ids] if action.action_plan_id not in action_plan_ids]
@@ -209,18 +236,21 @@ class PurgeCommand(object):
def _find_soft_deleted_objects(self): def _find_soft_deleted_objects(self):
to_be_deleted = WatcherObjectsMap() to_be_deleted = WatcherObjectsMap()
expiry_date = self.get_expiry_date() expiry_date = self.get_expiry_date()
filters = dict(deleted=True) filters = dict(deleted=True)
if self.uuid: if self.uuid:
filters["uuid"] = self.uuid filters["uuid"] = self.uuid
if expiry_date: if expiry_date:
filters.update(dict(deleted_at__lt=expiry_date)) filters.update(dict(deleted_at__lt=expiry_date))
to_be_deleted.goals.extend(self._find_goals(filters))
to_be_deleted.strategies.extend(self._find_strategies(filters))
to_be_deleted.audit_templates.extend( to_be_deleted.audit_templates.extend(
self._find_audit_templates(filters)) self._find_audit_templates(filters))
to_be_deleted.audits.extend(self._find_audits(filters)) to_be_deleted.audits.extend(self._find_audits(filters))
to_be_deleted.action_plans.extend(self._find_action_plans(filters)) to_be_deleted.action_plans.extend(
self._find_action_plans(filters))
to_be_deleted.actions.extend(self._find_actions(filters)) to_be_deleted.actions.extend(self._find_actions(filters))
soft_deleted_objs = self._find_related_objects( soft_deleted_objs = self._find_related_objects(
@@ -233,6 +263,23 @@ class PurgeCommand(object):
def _find_related_objects(self, objects_map, base_filters=None): def _find_related_objects(self, objects_map, base_filters=None):
base_filters = base_filters or {} base_filters = base_filters or {}
for goal in objects_map.goals:
filters = {}
filters.update(base_filters)
filters.update(dict(goal_id=goal.id))
related_objs = WatcherObjectsMap()
related_objs.strategies = self._find_strategies(filters)
related_objs.audit_templates = self._find_audit_templates(filters)
objects_map += related_objs
for strategy in objects_map.strategies:
filters = {}
filters.update(base_filters)
filters.update(dict(strategy_id=strategy.id))
related_objs = WatcherObjectsMap()
related_objs.audit_templates = self._find_audit_templates(filters)
objects_map += related_objs
for audit_template in objects_map.audit_templates: for audit_template in objects_map.audit_templates:
filters = {} filters = {}
filters.update(base_filters) filters.update(base_filters)
@@ -282,21 +329,48 @@ class PurgeCommand(object):
return self._delete_up_to_max return self._delete_up_to_max
def _aggregate_objects(self): def _aggregate_objects(self):
"""Objects aggregated on a 'per audit template' basis""" """Objects aggregated on a 'per goal' basis"""
# todo: aggregate orphans as well # todo: aggregate orphans as well
aggregate = [] aggregate = []
for audit_template in self._objects_map.audit_templates: for goal in self._objects_map.goals:
related_objs = WatcherObjectsMap() related_objs = WatcherObjectsMap()
related_objs.audit_templates = [audit_template]
# goals
related_objs.goals = [goal]
# strategies
goal_ids = [goal.id]
related_objs.strategies = [
strategy for strategy in self._objects_map.strategies
if strategy.goal_id in goal_ids
]
# audit templates
strategy_ids = [
strategy.id for strategy in related_objs.strategies]
related_objs.audit_templates = [
at for at in self._objects_map.audit_templates
if at.goal_id in goal_ids or
(at.strategy_id and at.strategy_id in strategy_ids)
]
# audits
audit_template_ids = [
audit_template.id
for audit_template in related_objs.audit_templates]
related_objs.audits = [ related_objs.audits = [
audit for audit in self._objects_map.audits audit for audit in self._objects_map.audits
if audit.audit_template_id == audit_template.id if audit.audit_template_id in audit_template_ids
] ]
# action plans
audit_ids = [audit.id for audit in related_objs.audits] audit_ids = [audit.id for audit in related_objs.audits]
related_objs.action_plans = [ related_objs.action_plans = [
action_plan for action_plan in self._objects_map.action_plans action_plan for action_plan in self._objects_map.action_plans
if action_plan.audit_id in audit_ids if action_plan.audit_id in audit_ids
] ]
# actions
action_plan_ids = [ action_plan_ids = [
action_plan.id for action_plan in related_objs.action_plans action_plan.id for action_plan in related_objs.action_plans
] ]

View File

@@ -21,7 +21,6 @@ from oslo_config import cfg
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import session as db_session from oslo_db.sqlalchemy import session as db_session
from oslo_db.sqlalchemy import utils as db_utils from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log
from sqlalchemy.orm import exc from sqlalchemy.orm import exc
from watcher import _i18n from watcher import _i18n
@@ -35,7 +34,6 @@ from watcher.objects import audit as audit_objects
from watcher.objects import utils as objutils from watcher.objects import utils as objutils
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__)
_ = _i18n._ _ = _i18n._
_FACADE = None _FACADE = None
@@ -184,26 +182,131 @@ class Connection(api.BaseConnection):
return query return query
def _add_audit_templates_filters(self, query, filters): def __add_simple_filter(self, query, model, fieldname, value):
if filters is None: return query.filter(getattr(model, fieldname) == value)
filters = []
if 'uuid' in filters: def __add_join_filter(self, query, model, join_model, fieldname, value):
query = query.filter_by(uuid=filters['uuid']) query = query.join(join_model)
if 'name' in filters: return self.__add_simple_filter(query, join_model, fieldname, value)
query = query.filter_by(name=filters['name'])
if 'host_aggregate' in filters:
query = query.filter_by(host_aggregate=filters['host_aggregate'])
if 'goal' in filters:
query = query.filter_by(goal=filters['goal'])
query = self.__add_soft_delete_mixin_filters( def _add_filters(self, query, model, filters=None,
query, filters, models.AuditTemplate) plain_fields=None, join_fieldmap=None):
query = self.__add_timestamp_mixin_filters( """Generic way to add filters to a Watcher model
query, filters, models.AuditTemplate)
:param query: a :py:class:`sqlalchemy.orm.query.Query` instance
:param model: the model class the filters should relate to
:param filters: dict with the following structure {"fieldname": value}
:param plain_fields: a :py:class:`sqlalchemy.orm.query.Query` instance
:param join_fieldmap: a :py:class:`sqlalchemy.orm.query.Query` instance
"""
filters = filters or {}
plain_fields = plain_fields or ()
join_fieldmap = join_fieldmap or {}
for fieldname, value in filters.items():
if fieldname in plain_fields:
query = self.__add_simple_filter(
query, model, fieldname, value)
elif fieldname in join_fieldmap:
join_field, join_model = join_fieldmap[fieldname]
query = self.__add_join_filter(
query, model, join_model, join_field, value)
query = self.__add_soft_delete_mixin_filters(query, filters, model)
query = self.__add_timestamp_mixin_filters(query, filters, model)
return query return query
def _get(self, context, model, fieldname, value):
query = model_query(model)
query = query.filter(getattr(model, fieldname) == value)
if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None))
try:
obj = query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=value)
return obj
def _update(self, model, id_, values):
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
ref.update(values)
return ref
def _soft_delete(self, model, id_):
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
query.soft_delete()
def _destroy(self, model, id_):
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
query.delete()
def _add_goals_filters(self, query, filters):
if filters is None:
filters = {}
plain_fields = ['uuid', 'name', 'display_name']
return self._add_filters(
query=query, model=models.Goal, filters=filters,
plain_fields=plain_fields)
def _add_strategies_filters(self, query, filters):
plain_fields = ['uuid', 'name', 'display_name', 'goal_id']
join_fieldmap = {
'goal_uuid': ("uuid", models.Goal),
'goal_name': ("name", models.Goal)
}
return self._add_filters(
query=query, model=models.Strategy, filters=filters,
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
def _add_audit_templates_filters(self, query, filters):
if filters is None:
filters = {}
plain_fields = ['uuid', 'name', 'host_aggregate',
'goal_id', 'strategy_id']
join_fieldmap = {
'goal_uuid': ("uuid", models.Goal),
'goal_name': ("name", models.Goal),
'strategy_uuid': ("uuid", models.Strategy),
'strategy_name': ("name", models.Strategy),
}
return self._add_filters(
query=query, model=models.AuditTemplate, filters=filters,
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
def _add_audits_filters(self, query, filters): def _add_audits_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = []
@@ -283,8 +386,6 @@ class Connection(api.BaseConnection):
if 'state' in filters: if 'state' in filters:
query = query.filter_by(state=filters['state']) query = query.filter_by(state=filters['state'])
if 'alarm' in filters:
query = query.filter_by(alarm=filters['alarm'])
query = self.__add_soft_delete_mixin_filters( query = self.__add_soft_delete_mixin_filters(
query, filters, models.Action) query, filters, models.Action)
@@ -293,6 +394,138 @@ class Connection(api.BaseConnection):
return query return query
# ### GOALS ### #
def get_goal_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
query = model_query(models.Goal)
query = self._add_goals_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
return _paginate_query(models.Goal, limit, marker,
sort_key, sort_dir, query)
def create_goal(self, values):
# ensure defaults are present for new goals
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
goal = models.Goal()
goal.update(values)
try:
goal.save()
except db_exc.DBDuplicateEntry:
raise exception.GoalAlreadyExists(uuid=values['uuid'])
return goal
def _get_goal(self, context, fieldname, value):
try:
return self._get(context, model=models.Goal,
fieldname=fieldname, value=value)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=value)
def get_goal_by_id(self, context, goal_id):
return self._get_goal(context, fieldname="id", value=goal_id)
def get_goal_by_uuid(self, context, goal_uuid):
return self._get_goal(context, fieldname="uuid", value=goal_uuid)
def get_goal_by_name(self, context, goal_name):
return self._get_goal(context, fieldname="name", value=goal_name)
def destroy_goal(self, goal_id):
try:
return self._destroy(models.Goal, goal_id)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=goal_id)
def update_goal(self, goal_id, values):
if 'uuid' in values:
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing Goal."))
try:
return self._update(models.Goal, goal_id, values)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=goal_id)
def soft_delete_goal(self, goal_id):
try:
self._soft_delete(models.Goal, goal_id)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=goal_id)
# ### STRATEGIES ### #
def get_strategy_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
query = model_query(models.Strategy)
query = self._add_strategies_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
return _paginate_query(models.Strategy, limit, marker,
sort_key, sort_dir, query)
def create_strategy(self, values):
# ensure defaults are present for new strategies
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
strategy = models.Strategy()
strategy.update(values)
try:
strategy.save()
except db_exc.DBDuplicateEntry:
raise exception.StrategyAlreadyExists(uuid=values['uuid'])
return strategy
def _get_strategy(self, context, fieldname, value):
try:
return self._get(context, model=models.Strategy,
fieldname=fieldname, value=value)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=value)
def get_strategy_by_id(self, context, strategy_id):
return self._get_strategy(context, fieldname="id", value=strategy_id)
def get_strategy_by_uuid(self, context, strategy_uuid):
return self._get_strategy(
context, fieldname="uuid", value=strategy_uuid)
def get_strategy_by_name(self, context, strategy_name):
return self._get_strategy(
context, fieldname="name", value=strategy_name)
def destroy_strategy(self, strategy_id):
try:
return self._destroy(models.Strategy, strategy_id)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=strategy_id)
def update_strategy(self, strategy_id, values):
if 'uuid' in values:
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing Strategy."))
try:
return self._update(models.Strategy, strategy_id, values)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=strategy_id)
def soft_delete_strategy(self, strategy_id):
try:
self._soft_delete(models.Strategy, strategy_id)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=strategy_id)
# ### AUDIT TEMPLATES ### #
def get_audit_template_list(self, context, filters=None, limit=None, def get_audit_template_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None): marker=None, sort_key=None, sort_dir=None):
@@ -308,110 +541,70 @@ class Connection(api.BaseConnection):
if not values.get('uuid'): if not values.get('uuid'):
values['uuid'] = utils.generate_uuid() values['uuid'] = utils.generate_uuid()
query = model_query(models.AuditTemplate)
query = query.filter_by(name=values.get('name'),
deleted_at=None)
if len(query.all()) > 0:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
audit_template = models.AuditTemplate() audit_template = models.AuditTemplate()
audit_template.update(values) audit_template.update(values)
try: try:
audit_template.save() audit_template.save()
except db_exc.DBDuplicateEntry: except db_exc.DBDuplicateEntry:
raise exception.AuditTemplateAlreadyExists(uuid=values['uuid'], raise exception.AuditTemplateAlreadyExists(
name=values['name']) audit_template=values['name'])
return audit_template return audit_template
def get_audit_template_by_id(self, context, audit_template_id): def _get_audit_template(self, context, fieldname, value):
query = model_query(models.AuditTemplate)
query = query.filter_by(id=audit_template_id)
try: try:
audit_template = query.one() return self._get(context, model=models.AuditTemplate,
if not context.show_deleted: fieldname=fieldname, value=value)
if audit_template.deleted_at is not None: except exception.ResourceNotFound:
raise exception.AuditTemplateNotFound( raise exception.AuditTemplateNotFound(audit_template=value)
audit_template=audit_template_id)
return audit_template def get_audit_template_by_id(self, context, audit_template_id):
except exc.NoResultFound: return self._get_audit_template(
raise exception.AuditTemplateNotFound( context, fieldname="id", value=audit_template_id)
audit_template=audit_template_id)
def get_audit_template_by_uuid(self, context, audit_template_uuid): def get_audit_template_by_uuid(self, context, audit_template_uuid):
query = model_query(models.AuditTemplate) return self._get_audit_template(
query = query.filter_by(uuid=audit_template_uuid) context, fieldname="uuid", value=audit_template_uuid)
try:
audit_template = query.one()
if not context.show_deleted:
if audit_template.deleted_at is not None:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_uuid)
return audit_template
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_uuid)
def get_audit_template_by_name(self, context, audit_template_name): def get_audit_template_by_name(self, context, audit_template_name):
query = model_query(models.AuditTemplate) return self._get_audit_template(
query = query.filter_by(name=audit_template_name) context, fieldname="name", value=audit_template_name)
try:
audit_template = query.one()
if not context.show_deleted:
if audit_template.deleted_at is not None:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_name)
return audit_template
except exc.MultipleResultsFound:
raise exception.Conflict(
_('Multiple audit templates exist with the same name.'
' Please use the audit template uuid instead'))
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_name)
def destroy_audit_template(self, audit_template_id): def destroy_audit_template(self, audit_template_id):
session = get_session() try:
with session.begin(): return self._destroy(models.AuditTemplate, audit_template_id)
query = model_query(models.AuditTemplate, session=session) except exception.ResourceNotFound:
query = add_identity_filter(query, audit_template_id) raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
try:
query.one()
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(node=audit_template_id)
query.delete()
def update_audit_template(self, audit_template_id, values): def update_audit_template(self, audit_template_id, values):
if 'uuid' in values: if 'uuid' in values:
raise exception.Invalid( raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing " message=_("Cannot overwrite UUID for an existing "
"Audit Template.")) "Audit Template."))
try:
return self._do_update_audit_template(audit_template_id, values) return self._update(
models.AuditTemplate, audit_template_id, values)
def _do_update_audit_template(self, audit_template_id, values): except exception.ResourceNotFound:
session = get_session() raise exception.AuditTemplateNotFound(
with session.begin(): audit_template=audit_template_id)
query = model_query(models.AuditTemplate, session=session)
query = add_identity_filter(query, audit_template_id)
try:
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
ref.update(values)
return ref
def soft_delete_audit_template(self, audit_template_id): def soft_delete_audit_template(self, audit_template_id):
session = get_session() try:
with session.begin(): self._soft_delete(models.AuditTemplate, audit_template_id)
query = model_query(models.AuditTemplate, session=session) except exception.ResourceNotFound:
query = add_identity_filter(query, audit_template_id) raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
try: # ### AUDITS ### #
query.one()
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(node=audit_template_id)
query.soft_delete()
def get_audit_list(self, context, filters=None, limit=None, marker=None, def get_audit_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None): sort_key=None, sort_dir=None):
@@ -519,10 +712,12 @@ class Connection(api.BaseConnection):
try: try:
query.one() query.one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.AuditNotFound(node=audit_id) raise exception.AuditNotFound(audit=audit_id)
query.soft_delete() query.soft_delete()
# ### ACTIONS ### #
def get_action_list(self, context, filters=None, limit=None, marker=None, def get_action_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None): sort_key=None, sort_dir=None):
query = model_query(models.Action) query = model_query(models.Action)
@@ -612,10 +807,12 @@ class Connection(api.BaseConnection):
try: try:
query.one() query.one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ActionNotFound(node=action_id) raise exception.ActionNotFound(action=action_id)
query.soft_delete() query.soft_delete()
# ### ACTION PLANS ### #
def get_action_plan_list( def get_action_plan_list(
self, context, columns=None, filters=None, limit=None, self, context, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None): marker=None, sort_key=None, sort_dir=None):
@@ -723,6 +920,6 @@ class Connection(api.BaseConnection):
try: try:
query.one() query.one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ActionPlanNotFound(node=action_plan_id) raise exception.ActionPlanNotFound(action_plan=action_plan_id)
query.soft_delete() query.soft_delete()

View File

@@ -110,13 +110,41 @@ class WatcherBase(models.SoftDeleteMixin,
Base = declarative_base(cls=WatcherBase) Base = declarative_base(cls=WatcherBase)
class Strategy(Base):
"""Represents a strategy."""
__tablename__ = 'strategies'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_strategies0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(63), nullable=False)
display_name = Column(String(63), nullable=False)
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
class Goal(Base):
"""Represents a goal."""
__tablename__ = 'goals'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_goals0uuid'),
table_args(),
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(63), nullable=False)
display_name = Column(String(63), nullable=False)
class AuditTemplate(Base): class AuditTemplate(Base):
"""Represents an audit template.""" """Represents an audit template."""
__tablename__ = 'audit_templates' __tablename__ = 'audit_templates'
__table_args__ = ( __table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'), schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
schema.UniqueConstraint('name', name='uniq_audit_templates0name'),
table_args() table_args()
) )
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@@ -124,7 +152,8 @@ class AuditTemplate(Base):
name = Column(String(63), nullable=True) name = Column(String(63), nullable=True)
description = Column(String(255), nullable=True) description = Column(String(255), nullable=True)
host_aggregate = Column(Integer, nullable=True) host_aggregate = Column(Integer, nullable=True)
goal = Column(String(63), nullable=True) goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
extra = Column(JSONEncodedDict) extra = Column(JSONEncodedDict)
version = Column(String(15), nullable=True) version = Column(String(15), nullable=True)
@@ -162,8 +191,6 @@ class Action(Base):
action_type = Column(String(255), nullable=False) action_type = Column(String(255), nullable=False)
input_parameters = Column(JSONEncodedDict, nullable=True) input_parameters = Column(JSONEncodedDict, nullable=True)
state = Column(String(20), nullable=True) state = Column(String(20), nullable=True)
# todo(jed) remove parameter alarm
alarm = Column(String(36))
next = Column(String(36), nullable=True) next = Column(String(36), nullable=True)
@@ -178,9 +205,6 @@ class ActionPlan(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
uuid = Column(String(36)) uuid = Column(String(36))
first_action_id = Column(Integer) first_action_id = Column(Integer)
# first_action_id = Column(Integer, ForeignKeyConstraint(
# ['first_action_id'], ['actions.id'], name='fk_first_action_id'),
# nullable=True)
audit_id = Column(Integer, ForeignKey('audits.id'), audit_id = Column(Integer, ForeignKey('audits.id'),
nullable=True) nullable=True)
state = Column(String(20), nullable=True) state = Column(String(20), nullable=True)

View File

@@ -38,13 +38,10 @@ See :doc:`../architecture` for more details on this component.
""" """
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log
from watcher.common.messaging import messaging_core
from watcher.decision_engine.messaging import audit_endpoint from watcher.decision_engine.messaging import audit_endpoint
LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
WATCHER_DECISION_ENGINE_OPTS = [ WATCHER_DECISION_ENGINE_OPTS = [
@@ -78,18 +75,15 @@ CONF.register_group(decision_engine_opt_group)
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group) CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
class DecisionEngineManager(messaging_core.MessagingCore): class DecisionEngineManager(object):
def __init__(self):
super(DecisionEngineManager, self).__init__(
CONF.watcher_decision_engine.publisher_id,
CONF.watcher_decision_engine.conductor_topic,
CONF.watcher_decision_engine.status_topic,
api_version=self.API_VERSION)
endpoint = audit_endpoint.AuditEndpoint(
self,
max_workers=CONF.watcher_decision_engine.max_workers)
self.conductor_topic_handler.add_endpoint(endpoint)
def join(self): API_VERSION = '1.0'
self.conductor_topic_handler.join()
self.status_topic_handler.join() conductor_endpoints = [audit_endpoint.AuditEndpoint]
status_endpoints = []
def __init__(self):
self.publisher_id = CONF.watcher_decision_engine.publisher_id
self.conductor_topic = CONF.watcher_decision_engine.conductor_topic
self.status_topic = CONF.watcher_decision_engine.status_topic
self.api_version = self.API_VERSION

View File

@@ -18,17 +18,20 @@
# #
from concurrent import futures from concurrent import futures
from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.decision_engine.audit import default from watcher.decision_engine.audit import default
CONF = cfg.CONF
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class AuditEndpoint(object): class AuditEndpoint(object):
def __init__(self, messaging, max_workers): def __init__(self, messaging):
self._messaging = messaging self._messaging = messaging
self._executor = futures.ThreadPoolExecutor(max_workers=max_workers) self._executor = futures.ThreadPoolExecutor(
max_workers=CONF.watcher_decision_engine.max_workers)
@property @property
def executor(self): def executor(self):

View File

@@ -13,16 +13,12 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from oslo_log import log
from watcher._i18n import _ from watcher._i18n import _
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.model import hypervisor from watcher.decision_engine.model import hypervisor
from watcher.decision_engine.model import mapping from watcher.decision_engine.model import mapping
from watcher.decision_engine.model import vm from watcher.decision_engine.model import vm
LOG = log.getLogger(__name__)
class ModelRoot(object): class ModelRoot(object):
def __init__(self): def __init__(self):

View File

@@ -47,12 +47,24 @@ See :doc:`../architecture` for more details on this component.
import abc import abc
import six import six
from watcher.common.loader import loadable
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BasePlanner(object): class BasePlanner(loadable.Loadable):
@classmethod
def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
return []
@abc.abstractmethod @abc.abstractmethod
def schedule(self, context, audit_uuid, solution): def schedule(self, context, audit_uuid, solution):
"""The planner receives a solution to schedule """The planner receives a solution to schedule
:param solution: A solution provided by a strategy for scheduling :param solution: A solution provided by a strategy for scheduling
:type solution: :py:class:`~.BaseSolution` subclass instance :type solution: :py:class:`~.BaseSolution` subclass instance
@@ -60,7 +72,7 @@ class BasePlanner(object):
:type audit_uuid: str :type audit_uuid: str
:return: Action plan with an ordered sequence of actions such that all :return: Action plan with an ordered sequence of actions such that all
security, dependency, and performance requirements are met. security, dependency, and performance requirements are met.
:rtype: :py:class:`watcher.objects.action_plan.ActionPlan` instance :rtype: :py:class:`watcher.objects.ActionPlan` instance
""" """
# example: directed acyclic graph # example: directed acyclic graph
raise NotImplementedError() raise NotImplementedError()

View File

@@ -53,7 +53,6 @@ class DefaultPlanner(base.BasePlanner):
'action_type': action_type, 'action_type': action_type,
'input_parameters': input_parameters, 'input_parameters': input_parameters,
'state': objects.action.State.PENDING, 'state': objects.action.State.PENDING,
'alarm': None,
'next': None, 'next': None,
} }
return action return action

View File

@@ -17,14 +17,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_log import log from watcher.common.loader import default
from watcher.common.loader.default import DefaultLoader
LOG = log.getLogger(__name__)
class DefaultPlannerLoader(DefaultLoader): class DefaultPlannerLoader(default.DefaultLoader):
def __init__(self): def __init__(self):
super(DefaultPlannerLoader, self).__init__( super(DefaultPlannerLoader, self).__init__(
namespace='watcher_planners') namespace='watcher_planners')

View File

@@ -18,35 +18,25 @@
# #
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log
from watcher.common import exception from watcher.common import exception
from watcher.common.messaging import messaging_core
from watcher.common.messaging import notification_handler from watcher.common.messaging import notification_handler
from watcher.common import service
from watcher.common import utils from watcher.common import utils
from watcher.decision_engine.manager import decision_engine_opt_group from watcher.decision_engine import manager
from watcher.decision_engine.manager import WATCHER_DECISION_ENGINE_OPTS
LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
CONF.register_group(decision_engine_opt_group) CONF.register_group(manager.decision_engine_opt_group)
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group) CONF.register_opts(manager.WATCHER_DECISION_ENGINE_OPTS,
manager.decision_engine_opt_group)
class DecisionEngineAPI(messaging_core.MessagingCore): class DecisionEngineAPI(service.Service):
def __init__(self): def __init__(self):
super(DecisionEngineAPI, self).__init__( super(DecisionEngineAPI, self).__init__(DecisionEngineAPIManager)
CONF.watcher_decision_engine.publisher_id,
CONF.watcher_decision_engine.conductor_topic,
CONF.watcher_decision_engine.status_topic,
api_version=self.API_VERSION,
)
self.handler = notification_handler.NotificationHandler(
self.publisher_id)
self.status_topic_handler.add_endpoint(self.handler)
def trigger_audit(self, context, audit_uuid=None): def trigger_audit(self, context, audit_uuid=None):
if not utils.is_uuid_like(audit_uuid): if not utils.is_uuid_like(audit_uuid):
@@ -54,3 +44,17 @@ class DecisionEngineAPI(messaging_core.MessagingCore):
return self.conductor_client.call( return self.conductor_client.call(
context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid) context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid)
class DecisionEngineAPIManager(object):
API_VERSION = '1.0'
conductor_endpoints = []
status_endpoints = [notification_handler.NotificationHandler]
def __init__(self):
self.publisher_id = CONF.watcher_decision_engine.publisher_id
self.conductor_topic = CONF.watcher_decision_engine.conductor_topic
self.status_topic = CONF.watcher_decision_engine.status_topic
self.api_version = self.API_VERSION

View File

@@ -16,14 +16,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
from oslo_log import log
from watcher.applier.actions import base as baction from watcher.applier.actions import base as baction
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.solution import base from watcher.decision_engine.solution import base
LOG = log.getLogger(__name__)
class DefaultSolution(base.BaseSolution): class DefaultSolution(base.BaseSolution):
def __init__(self): def __init__(self):

View File

@@ -26,29 +26,24 @@ LOG = log.getLogger(__name__)
class DefaultStrategyContext(base.BaseStrategyContext): class DefaultStrategyContext(base.BaseStrategyContext):
def __init__(self): def __init__(self):
super(DefaultStrategyContext, self).__init__() super(DefaultStrategyContext, self).__init__()
LOG.debug("Initializing Strategy Context") LOG.debug("Initializing Strategy Context")
self._strategy_selector = default.DefaultStrategySelector()
self._collector_manager = manager.CollectorManager() self._collector_manager = manager.CollectorManager()
@property @property
def collector(self): def collector(self):
return self._collector_manager return self._collector_manager
@property
def strategy_selector(self):
return self._strategy_selector
def execute_strategy(self, audit_uuid, request_context): def execute_strategy(self, audit_uuid, request_context):
audit = objects.Audit.get_by_uuid(request_context, audit_uuid) audit = objects.Audit.get_by_uuid(request_context, audit_uuid)
# Retrieve the Audit Template # Retrieve the Audit Template
audit_template = objects.\ audit_template = objects.AuditTemplate.get_by_id(
AuditTemplate.get_by_id(request_context, audit.audit_template_id) request_context, audit.audit_template_id)
osc = clients.OpenStackClients() osc = clients.OpenStackClients()
# todo(jed) retrieve in audit_template parameters (threshold,...) # todo(jed) retrieve in audit_template parameters (threshold,...)
# todo(jed) create ActionPlan # todo(jed) create ActionPlan
collector_manager = self.collector.get_cluster_model_collector(osc=osc) collector_manager = self.collector.get_cluster_model_collector(osc=osc)
@@ -56,8 +51,13 @@ class DefaultStrategyContext(base.BaseStrategyContext):
# todo(jed) remove call to get_latest_cluster_data_model # todo(jed) remove call to get_latest_cluster_data_model
cluster_data_model = collector_manager.get_latest_cluster_data_model() cluster_data_model = collector_manager.get_latest_cluster_data_model()
selected_strategy = self.strategy_selector.define_from_goal( strategy_selector = default.DefaultStrategySelector(
audit_template.goal, osc=osc) goal_name=objects.Goal.get_by_id(
request_context, audit_template.goal_id).name,
strategy_name=None,
osc=osc)
selected_strategy = strategy_selector.select()
# todo(jed) add parameters and remove cluster_data_model # todo(jed) add parameters and remove cluster_data_model
return selected_strategy.execute(cluster_data_model) return selected_strategy.execute(cluster_data_model)

View File

@@ -19,12 +19,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_log import log
from watcher.common.loader import default from watcher.common.loader import default
LOG = log.getLogger(__name__)
class DefaultStrategyLoader(default.DefaultLoader): class DefaultStrategyLoader(default.DefaultLoader):
def __init__(self): def __init__(self):

View File

@@ -22,6 +22,7 @@ import six
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseSelector(object): class BaseSelector(object):
@abc.abstractmethod @abc.abstractmethod
def define_from_goal(self, goal_name): def select(self):
raise NotImplementedError() raise NotImplementedError()

View File

@@ -25,38 +25,51 @@ from watcher.decision_engine.strategy.selection import base
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
default_goals = {'DUMMY': 'dummy'}
WATCHER_GOALS_OPTS = [
cfg.DictOpt(
'goals',
default=default_goals,
required=True,
help='Goals used for the optimization. '
'Maps each goal to an associated strategy (for example: '
'BASIC_CONSOLIDATION:basic, MY_GOAL:my_strategy_1)'),
]
goals_opt_group = cfg.OptGroup(name='watcher_goals',
title='Goals available for the optimization')
CONF.register_group(goals_opt_group)
CONF.register_opts(WATCHER_GOALS_OPTS, goals_opt_group)
class DefaultStrategySelector(base.BaseSelector): class DefaultStrategySelector(base.BaseSelector):
def __init__(self): def __init__(self, goal_name, strategy_name=None, osc=None):
"""Default strategy selector
:param goal_name: Name of the goal
:param strategy_name: Name of the strategy
:param osc: an OpenStackClients instance
"""
super(DefaultStrategySelector, self).__init__() super(DefaultStrategySelector, self).__init__()
self.goal_name = goal_name
self.strategy_name = strategy_name
self.osc = osc
self.strategy_loader = default.DefaultStrategyLoader() self.strategy_loader = default.DefaultStrategyLoader()
def define_from_goal(self, goal_name, osc=None): def select(self):
""":param osc: an OpenStackClients instance""" """Selects a strategy
:raises: :py:class:`~.LoadingError` if it failed to load a strategy
:returns: A :py:class:`~.BaseStrategy` instance
"""
strategy_to_load = None strategy_to_load = None
try: try:
strategy_to_load = CONF.watcher_goals.goals[goal_name] if self.strategy_name:
return self.strategy_loader.load(strategy_to_load, osc=osc) strategy_to_load = self.strategy_name
except KeyError as exc: else:
available_strategies = self.strategy_loader.list_available()
available_strategies_for_goal = list(
key for key, strat in available_strategies.items()
if strat.get_goal_name() == self.goal_name)
if not available_strategies_for_goal:
raise exception.NoAvailableStrategyForGoal(
goal=self.goal_name)
# TODO(v-francoise): We should do some more work here to select
# a strategy out of a given goal instead of just choosing the
# 1st one
strategy_to_load = available_strategies_for_goal[0]
return self.strategy_loader.load(strategy_to_load, osc=self.osc)
except exception.NoAvailableStrategyForGoal:
raise
except Exception as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.WatcherException( raise exception.LoadingError(
_("Incorrect mapping: could not find " _("Could not load any strategy for goal %(goal)s"),
"associated strategy for '%s'") % goal_name goal=self.goal_name)
)

View File

@@ -18,14 +18,16 @@
from watcher.decision_engine.strategy.strategies import basic_consolidation from watcher.decision_engine.strategy.strategies import basic_consolidation
from watcher.decision_engine.strategy.strategies import dummy_strategy from watcher.decision_engine.strategy.strategies import dummy_strategy
from watcher.decision_engine.strategy.strategies import outlet_temp_control from watcher.decision_engine.strategy.strategies import outlet_temp_control
from watcher.decision_engine.strategy.strategies \ from watcher.decision_engine.strategy.strategies import \
import vm_workload_consolidation vm_workload_consolidation
from watcher.decision_engine.strategy.strategies import workload_stabilization
BasicConsolidation = basic_consolidation.BasicConsolidation BasicConsolidation = basic_consolidation.BasicConsolidation
OutletTempControl = outlet_temp_control.OutletTempControl OutletTempControl = outlet_temp_control.OutletTempControl
DummyStrategy = dummy_strategy.DummyStrategy DummyStrategy = dummy_strategy.DummyStrategy
VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation
WorkloadStabilization = workload_stabilization.WorkloadStabilization
__all__ = ("BasicConsolidation", "OutletTempControl",
__all__ = (BasicConsolidation, OutletTempControl, DummyStrategy, "DummyStrategy", "VMWorkloadConsolidation",
VMWorkloadConsolidation) "WorkloadStabilization")

View File

@@ -37,29 +37,28 @@ which are dynamically loaded by Watcher at launch time.
""" """
import abc import abc
from oslo_log import log
import six import six
from watcher._i18n import _
from watcher.common import clients from watcher.common import clients
from watcher.common.loader import loadable
from watcher.decision_engine.solution import default from watcher.decision_engine.solution import default
from watcher.decision_engine.strategy.common import level from watcher.decision_engine.strategy.common import level
LOG = log.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseStrategy(object): class BaseStrategy(loadable.Loadable):
"""A base class for all the strategies """A base class for all the strategies
A Strategy is an algorithm implementation which is able to find a A Strategy is an algorithm implementation which is able to find a
Solution for a given Goal. Solution for a given Goal.
""" """
def __init__(self, name=None, description=None, osc=None): def __init__(self, config, osc=None):
""":param osc: an OpenStackClients instance""" """:param osc: an OpenStackClients instance"""
self._name = name super(BaseStrategy, self).__init__(config)
self.description = description self._name = self.get_name()
self._display_name = self.get_display_name()
# default strategy level # default strategy level
self._strategy_level = level.StrategyLevel.conservative self._strategy_level = level.StrategyLevel.conservative
self._cluster_state_collector = None self._cluster_state_collector = None
@@ -67,6 +66,55 @@ class BaseStrategy(object):
self._solution = default.DefaultSolution() self._solution = default.DefaultSolution()
self._osc = osc self._osc = osc
@classmethod
@abc.abstractmethod
def get_name(cls):
"""The name of the strategy"""
raise NotImplementedError()
@classmethod
@abc.abstractmethod
def get_display_name(cls):
"""The goal display name for the strategy"""
raise NotImplementedError()
@classmethod
@abc.abstractmethod
def get_translatable_display_name(cls):
"""The translatable msgid of the strategy"""
# Note(v-francoise): Defined here to be used as the translation key for
# other services
raise NotImplementedError()
@classmethod
@abc.abstractmethod
def get_goal_name(cls):
"""The goal name for the strategy"""
raise NotImplementedError()
@classmethod
@abc.abstractmethod
def get_goal_display_name(cls):
"""The translated display name related to the goal of the strategy"""
raise NotImplementedError()
@classmethod
@abc.abstractmethod
def get_translatable_goal_display_name(cls):
"""The translatable msgid related to the goal of the strategy"""
# Note(v-francoise): Defined here to be used as the translation key for
# other services
raise NotImplementedError()
@classmethod
def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
return []
@abc.abstractmethod @abc.abstractmethod
def execute(self, original_model): def execute(self, original_model):
"""Execute a strategy """Execute a strategy
@@ -92,12 +140,12 @@ class BaseStrategy(object):
self._solution = s self._solution = s
@property @property
def name(self): def id(self):
return self._name return self._name
@name.setter @property
def name(self, n): def display_name(self):
self._name = n return self._display_name
@property @property
def strategy_level(self): def strategy_level(self):
@@ -114,3 +162,90 @@ class BaseStrategy(object):
@state_collector.setter @state_collector.setter
def state_collector(self, s): def state_collector(self, s):
self._cluster_state_collector = s self._cluster_state_collector = s
@six.add_metaclass(abc.ABCMeta)
class DummyBaseStrategy(BaseStrategy):
@classmethod
def get_goal_name(cls):
return "DUMMY"
@classmethod
def get_goal_display_name(cls):
return _("Dummy goal")
@classmethod
def get_translatable_goal_display_name(cls):
return "Dummy goal"
@six.add_metaclass(abc.ABCMeta)
class UnclassifiedStrategy(BaseStrategy):
"""This base class is used to ease the development of new strategies
The goal defined within this strategy can be used to simplify the
documentation explaining how to implement a new strategy plugin by
ommitting the need for the strategy developer to define a goal straight
away.
"""
@classmethod
def get_goal_name(cls):
return "UNCLASSIFIED"
@classmethod
def get_goal_display_name(cls):
return _("Unclassified")
@classmethod
def get_translatable_goal_display_name(cls):
return "Unclassified"
@six.add_metaclass(abc.ABCMeta)
class ServerConsolidationBaseStrategy(BaseStrategy):
@classmethod
def get_goal_name(cls):
return "SERVER_CONSOLIDATION"
@classmethod
def get_goal_display_name(cls):
return _("Server consolidation")
@classmethod
def get_translatable_goal_display_name(cls):
return "Server consolidation"
@six.add_metaclass(abc.ABCMeta)
class ThermalOptimizationBaseStrategy(BaseStrategy):
@classmethod
def get_goal_name(cls):
return "THERMAL_OPTIMIZATION"
@classmethod
def get_goal_display_name(cls):
return _("Thermal optimization")
@classmethod
def get_translatable_goal_display_name(cls):
return "Thermal optimization"
@six.add_metaclass(abc.ABCMeta)
class WorkloadStabilizationBaseStrategy(BaseStrategy):
@classmethod
def get_goal_name(cls):
return "WORKLOAD_BALANCING"
@classmethod
def get_goal_display_name(cls):
return _("Workload balancing")
@classmethod
def get_translatable_goal_display_name(cls):
return "Workload balancing"

View File

@@ -29,7 +29,7 @@ order to both minimize energy consumption and comply to the various SLAs.
from oslo_log import log from oslo_log import log
from watcher._i18n import _LE, _LI, _LW from watcher._i18n import _, _LE, _LI, _LW
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.model import hypervisor_state as hyper_state from watcher.decision_engine.model import hypervisor_state as hyper_state
from watcher.decision_engine.model import resource from watcher.decision_engine.model import resource
@@ -41,7 +41,7 @@ from watcher.metrics_engine.cluster_history import ceilometer as \
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class BasicConsolidation(base.BaseStrategy): class BasicConsolidation(base.ServerConsolidationBaseStrategy):
"""Basic offline consolidation using live migration """Basic offline consolidation using live migration
*Description* *Description*
@@ -65,26 +65,20 @@ class BasicConsolidation(base.BaseStrategy):
<None> <None>
""" """
DEFAULT_NAME = "basic"
DEFAULT_DESCRIPTION = "Basic offline consolidation"
HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent' HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent'
INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util' INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util'
MIGRATION = "migrate" MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state" CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, def __init__(self, config=None, osc=None):
osc=None):
"""Basic offline Consolidation using live migration """Basic offline Consolidation using live migration
:param name: The name of the strategy (Default: "basic") :param config: A mapping containing the configuration of this strategy
:param description: The description of the strategy :type config: dict
(Default: "Basic offline consolidation") :param osc: :py:class:`~.OpenStackClients` instance
:param osc: An :py:class:`~watcher.common.clients.OpenStackClients`
instance
""" """
super(BasicConsolidation, self).__init__(name, description, osc) super(BasicConsolidation, self).__init__(config, osc)
# set default value for the number of released nodes # set default value for the number of released nodes
self.number_of_released_nodes = 0 self.number_of_released_nodes = 0
@@ -114,6 +108,18 @@ class BasicConsolidation(base.BaseStrategy):
# TODO(jed) bound migration attempts (80 %) # TODO(jed) bound migration attempts (80 %)
self.bound_migration = 0.80 self.bound_migration = 0.80
@classmethod
def get_name(cls):
return "basic"
@classmethod
def get_display_name(cls):
return _("Basic offline consolidation")
@classmethod
def get_translatable_display_name(cls):
return "Basic offline consolidation"
@property @property
def ceilometer(self): def ceilometer(self):
if self._ceilometer is None: if self._ceilometer is None:

View File

@@ -18,12 +18,13 @@
# #
from oslo_log import log from oslo_log import log
from watcher._i18n import _
from watcher.decision_engine.strategy.strategies import base from watcher.decision_engine.strategy.strategies import base
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class DummyStrategy(base.BaseStrategy): class DummyStrategy(base.DummyBaseStrategy):
"""Dummy strategy used for integration testing via Tempest """Dummy strategy used for integration testing via Tempest
*Description* *Description*
@@ -44,15 +45,17 @@ class DummyStrategy(base.BaseStrategy):
<None> <None>
""" """
DEFAULT_NAME = "dummy"
DEFAULT_DESCRIPTION = "Dummy Strategy"
NOP = "nop" NOP = "nop"
SLEEP = "sleep" SLEEP = "sleep"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, def __init__(self, config=None, osc=None):
osc=None): """Dummy Strategy implemented for demo and testing purposes
super(DummyStrategy, self).__init__(name, description, osc)
:param config: A mapping containing the configuration of this strategy
:type config: dict
:param osc: :py:class:`~.OpenStackClients` instance
"""
super(DummyStrategy, self).__init__(config, osc)
def execute(self, original_model): def execute(self, original_model):
LOG.debug("Executing Dummy strategy") LOG.debug("Executing Dummy strategy")
@@ -67,3 +70,15 @@ class DummyStrategy(base.BaseStrategy):
self.solution.add_action(action_type=self.SLEEP, self.solution.add_action(action_type=self.SLEEP,
input_parameters={'duration': 5.0}) input_parameters={'duration': 5.0})
return self.solution return self.solution
@classmethod
def get_name(cls):
return "dummy"
@classmethod
def get_display_name(cls):
return _("Dummy strategy")
@classmethod
def get_translatable_display_name(cls):
return "Dummy strategy"

View File

@@ -30,7 +30,7 @@ telemetries to measure thermal/workload status of server.
from oslo_log import log from oslo_log import log
from watcher._i18n import _LE from watcher._i18n import _, _LE
from watcher.common import exception as wexc from watcher.common import exception as wexc
from watcher.decision_engine.model import resource from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm_state from watcher.decision_engine.model import vm_state
@@ -41,7 +41,7 @@ from watcher.metrics_engine.cluster_history import ceilometer as ceil
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class OutletTempControl(base.BaseStrategy): class OutletTempControl(base.ThermalOptimizationBaseStrategy):
"""[PoC] Outlet temperature control using live migration """[PoC] Outlet temperature control using live migration
*Description* *Description*
@@ -71,8 +71,6 @@ class OutletTempControl(base.BaseStrategy):
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/approved/outlet-temperature-based-strategy.rst https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/approved/outlet-temperature-based-strategy.rst
""" # noqa """ # noqa
DEFAULT_NAME = "outlet_temp_control"
DEFAULT_DESCRIPTION = "outlet temperature based migration strategy"
# The meter to report outlet temperature in ceilometer # The meter to report outlet temperature in ceilometer
METER_NAME = "hardware.ipmi.node.outlet_temperature" METER_NAME = "hardware.ipmi.node.outlet_temperature"
# Unit: degree C # Unit: degree C
@@ -80,15 +78,15 @@ class OutletTempControl(base.BaseStrategy):
MIGRATION = "migrate" MIGRATION = "migrate"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, def __init__(self, config=None, osc=None):
osc=None):
"""Outlet temperature control using live migration """Outlet temperature control using live migration
:param name: the name of the strategy :param config: A mapping containing the configuration of this strategy
:param description: a description of the strategy :type config: dict
:param osc: an OpenStackClients object :param osc: an OpenStackClients object, defaults to None
:type osc: :py:class:`~.OpenStackClients` instance, optional
""" """
super(OutletTempControl, self).__init__(name, description, osc) super(OutletTempControl, self).__init__(config, osc)
# the migration plan will be triggered when the outlet temperature # the migration plan will be triggered when the outlet temperature
# reaches threshold # reaches threshold
# TODO(zhenzanz): Threshold should be configurable for each audit # TODO(zhenzanz): Threshold should be configurable for each audit
@@ -96,6 +94,18 @@ class OutletTempControl(base.BaseStrategy):
self._meter = self.METER_NAME self._meter = self.METER_NAME
self._ceilometer = None self._ceilometer = None
@classmethod
def get_name(cls):
return "outlet_temperature"
@classmethod
def get_display_name(cls):
return _("Outlet temperature based strategy")
@classmethod
def get_translatable_display_name(cls):
return "Outlet temperature based strategy"
@property @property
def ceilometer(self): def ceilometer(self):
if self._ceilometer is None: if self._ceilometer is None:
@@ -202,8 +212,8 @@ class OutletTempControl(base.BaseStrategy):
cluster_data_model, host, cpu_capacity, memory_capacity, cluster_data_model, host, cpu_capacity, memory_capacity,
disk_capacity) disk_capacity)
cores_available = cpu_capacity.get_capacity(host) - cores_used cores_available = cpu_capacity.get_capacity(host) - cores_used
disk_available = disk_capacity.get_capacity(host) - mem_used disk_available = disk_capacity.get_capacity(host) - disk_used
mem_available = memory_capacity.get_capacity(host) - disk_used mem_available = memory_capacity.get_capacity(host) - mem_used
if cores_available >= required_cores \ if cores_available >= required_cores \
and disk_available >= required_disk \ and disk_available >= required_disk \
and mem_available >= required_memory: and mem_available >= required_memory:

View File

@@ -22,7 +22,7 @@ from copy import deepcopy
from oslo_log import log from oslo_log import log
import six import six
from watcher._i18n import _LE, _LI from watcher._i18n import _, _LE, _LI
from watcher.common import exception from watcher.common import exception
from watcher.decision_engine.model import hypervisor_state as hyper_state from watcher.decision_engine.model import hypervisor_state as hyper_state
from watcher.decision_engine.model import resource from watcher.decision_engine.model import resource
@@ -34,9 +34,11 @@ from watcher.metrics_engine.cluster_history import ceilometer \
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class VMWorkloadConsolidation(base.BaseStrategy): class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
"""VM Workload Consolidation Strategy. """VM Workload Consolidation Strategy.
*Description*
A load consolidation strategy based on heuristic first-fit A load consolidation strategy based on heuristic first-fit
algorithm which focuses on measured CPU utilization and tries to algorithm which focuses on measured CPU utilization and tries to
minimize hosts which have too much or too little load respecting minimize hosts which have too much or too little load respecting
@@ -44,10 +46,11 @@ class VMWorkloadConsolidation(base.BaseStrategy):
This strategy produces a solution resulting in more efficient This strategy produces a solution resulting in more efficient
utilization of cluster resources using following four phases: utilization of cluster resources using following four phases:
* Offload phase - handling over-utilized resources
* Consolidation phase - handling under-utilized resources * Offload phase - handling over-utilized resources
* Solution optimization - reducing number of migrations * Consolidation phase - handling under-utilized resources
* Deactivation of unused hypervisors * Solution optimization - reducing number of migrations
* Deactivation of unused hypervisors
A capacity coefficients (cc) might be used to adjust optimization A capacity coefficients (cc) might be used to adjust optimization
thresholds. Different resources may require different coefficient thresholds. Different resources may require different coefficient
@@ -56,7 +59,7 @@ class VMWorkloadConsolidation(base.BaseStrategy):
If the cc equals 1 the full resource capacity may be used, cc If the cc equals 1 the full resource capacity may be used, cc
values lower than 1 will lead to resource under utilization and values lower than 1 will lead to resource under utilization and
values higher than 1 will lead to resource overbooking. values higher than 1 will lead to resource overbooking.
e.g. If targeted utilization is 80% of hypervisor capacity, e.g. If targeted utilization is 80 percent of hypervisor capacity,
the coefficient in the consolidation phase will be 0.8, but the coefficient in the consolidation phase will be 0.8, but
may any lower value in the offloading phase. The lower it gets may any lower value in the offloading phase. The lower it gets
the cluster will appear more released (distributed) for the the cluster will appear more released (distributed) for the
@@ -67,19 +70,39 @@ class VMWorkloadConsolidation(base.BaseStrategy):
correctly on all hypervisors within the cluster. correctly on all hypervisors within the cluster.
This strategy assumes it is possible to live migrate any VM from This strategy assumes it is possible to live migrate any VM from
an active hypervisor to any other active hypervisor. an active hypervisor to any other active hypervisor.
"""
DEFAULT_NAME = 'vm_workload_consolidation' *Requirements*
DEFAULT_DESCRIPTION = 'VM Workload Consolidation Strategy'
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, * You must have at least 2 physical compute nodes to run this strategy.
osc=None):
super(VMWorkloadConsolidation, self).__init__(name, description, osc) *Limitations*
<None>
*Spec URL*
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/implemented/zhaw-load-consolidation.rst
""" # noqa
def __init__(self, config=None, osc=None):
super(VMWorkloadConsolidation, self).__init__(config, osc)
self._ceilometer = None self._ceilometer = None
self.number_of_migrations = 0 self.number_of_migrations = 0
self.number_of_released_hypervisors = 0 self.number_of_released_hypervisors = 0
self.ceilometer_vm_data_cache = dict() self.ceilometer_vm_data_cache = dict()
@classmethod
def get_name(cls):
return "vm_workload_consolidation"
@classmethod
def get_display_name(cls):
return _("VM Workload Consolidation Strategy")
@classmethod
def get_translatable_display_name(cls):
return "VM Workload Consolidation Strategy"
@property @property
def ceilometer(self): def ceilometer(self):
if self._ceilometer is None: if self._ceilometer is None:
@@ -375,12 +398,13 @@ class VMWorkloadConsolidation(base.BaseStrategy):
This is done by eliminating unnecessary or circular set of migrations This is done by eliminating unnecessary or circular set of migrations
which can be replaced by a more efficient solution. which can be replaced by a more efficient solution.
e.g.: e.g.:
* A->B, B->C => replace migrations A->B, B->C with
a single migration A->C as both solution result in * A->B, B->C => replace migrations A->B, B->C with
VM running on hypervisor C which can be achieved with a single migration A->C as both solution result in
one migration instead of two. VM running on hypervisor C which can be achieved with
* A->B, B->A => remove A->B and B->A as they do not result one migration instead of two.
in a new VM placement. * 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 :param model: model_root object
""" """
@@ -480,10 +504,11 @@ class VMWorkloadConsolidation(base.BaseStrategy):
This strategy produces a solution resulting in more This strategy produces a solution resulting in more
efficient utilization of cluster resources using following efficient utilization of cluster resources using following
four phases: four phases:
* Offload phase - handling over-utilized resources
* Consolidation phase - handling under-utilized resources * Offload phase - handling over-utilized resources
* Solution optimization - reducing number of migrations * Consolidation phase - handling under-utilized resources
* Deactivation of unused hypervisors * Solution optimization - reducing number of migrations
* Deactivation of unused hypervisors
:param original_model: root_model object :param original_model: root_model object
""" """

View File

@@ -0,0 +1,324 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 Intel Corp
#
# Authors: Junjie-Huang <junjie.huang@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_log import log
from watcher._i18n import _LE, _LI, _LW
from watcher.common import exception as wexc
from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm_state
from watcher.decision_engine.strategy.strategies import base
from watcher.metrics_engine.cluster_history import ceilometer as ceil
LOG = log.getLogger(__name__)
class WorkloadBalance(base.BaseStrategy):
"""[PoC]Workload balance using live migration
*Description*
It is a migration strategy based on the VM workload of physical
servers. It generates solutions to move a workload whenever a server's
CPU utilization % is higher than the specified threshold.
The VM to be moved should make the host close to average workload
of all hypervisors.
*Requirements*
* Hardware: compute node should use the same physical CPUs
* Software: Ceilometer component ceilometer-agent-compute running
in each compute node, and Ceilometer API can report such telemetry
"cpu_util" successfully.
* You must have at least 2 physical compute nodes to run this strategy
*Limitations*
- This is a proof of concept that is not meant to be used in production
- We cannot forecast how many servers should be migrated. This is the
reason why we only plan a single virtual machine migration at a time.
So it's better to use this algorithm with `CONTINUOUS` audits.
- It assume that live migrations are possible
"""
# The meter to report CPU utilization % of VM in ceilometer
METER_NAME = "cpu_util"
# Unit: %, value range is [0 , 100]
# TODO(Junjie): make it configurable
THRESHOLD = 25.0
# choose 300 seconds as the default duration of meter aggregation
# TODO(Junjie): make it configurable
PERIOD = 300
MIGRATION = "migrate"
def __init__(self, osc=None):
"""Using live migration
:param osc: an OpenStackClients object
"""
super(WorkloadBalance, self).__init__(osc)
# the migration plan will be triggered when the CPU utlization %
# reaches threshold
# TODO(Junjie): Threshold should be configurable for each audit
self.threshold = self.THRESHOLD
self._meter = self.METER_NAME
self._ceilometer = None
self._period = self.PERIOD
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
return self._ceilometer
@ceilometer.setter
def ceilometer(self, c):
self._ceilometer = c
@classmethod
def get_name(cls):
return "workload_balance"
@classmethod
def get_display_name(cls):
return _("workload balance migration strategy")
@classmethod
def get_translatable_display_name(cls):
return "workload balance migration strategy"
@classmethod
def get_goal_name(cls):
return "WORKLOAD_OPTIMIZATION"
@classmethod
def get_goal_display_name(cls):
return _("Workload optimization")
@classmethod
def get_translatable_goal_display_name(cls):
return "Workload optimization"
def calculate_used_resource(self, model, hypervisor, cap_cores, cap_mem,
cap_disk):
'''calculate the used vcpus, memory and disk based on VM flavors'''
vms = model.get_mapping().get_node_vms(hypervisor)
vcpus_used = 0
memory_mb_used = 0
disk_gb_used = 0
for vm_id in vms:
vm = model.get_vm_from_id(vm_id)
vcpus_used += cap_cores.get_capacity(vm)
memory_mb_used += cap_mem.get_capacity(vm)
disk_gb_used += cap_disk.get_capacity(vm)
return vcpus_used, memory_mb_used, disk_gb_used
def choose_vm_to_migrate(self, model, hosts, avg_workload, workload_cache):
"""pick up an active vm instance to migrate from provided hosts
:param model: it's the origin_model passed from 'execute' function
:param hosts: the array of dict which contains hypervisor object
:param avg_workload: the average workload value of all hypervisors
:param workload_cache: the map contains vm to workload mapping
"""
for hvmap in hosts:
source_hypervisor = hvmap['hv']
source_vms = model.get_mapping().get_node_vms(source_hypervisor)
if source_vms:
delta_workload = hvmap['workload'] - avg_workload
min_delta = 1000000
instance_id = None
for vm_id in source_vms:
try:
# select the first active VM to migrate
vm = model.get_vm_from_id(vm_id)
if vm.state != vm_state.VMState.ACTIVE.value:
LOG.debug("VM not active, skipped: %s",
vm.uuid)
continue
current_delta = delta_workload - workload_cache[vm_id]
if 0 <= current_delta < min_delta:
min_delta = current_delta
instance_id = vm_id
except wexc.InstanceNotFound:
LOG.error(_LE("VM not found Error: %s"), vm_id)
if instance_id:
return source_hypervisor, model.get_vm_from_id(instance_id)
else:
LOG.info(_LI("VM not found from hypervisor: %s"),
source_hypervisor.uuid)
def filter_destination_hosts(self, model, hosts, vm_to_migrate,
avg_workload, workload_cache):
'''Only return hosts with sufficient available resources'''
cap_cores = model.get_resource_from_id(resource.ResourceType.cpu_cores)
cap_disk = model.get_resource_from_id(resource.ResourceType.disk)
cap_mem = model.get_resource_from_id(resource.ResourceType.memory)
required_cores = cap_cores.get_capacity(vm_to_migrate)
required_disk = cap_disk.get_capacity(vm_to_migrate)
required_mem = cap_mem.get_capacity(vm_to_migrate)
# filter hypervisors without enough resource
destination_hosts = []
src_vm_workload = workload_cache[vm_to_migrate.uuid]
for hvmap in hosts:
host = hvmap['hv']
workload = hvmap['workload']
# calculate the available resources
cores_used, mem_used, disk_used = self.calculate_used_resource(
model, 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
if (cores_available >= required_cores and
disk_available >= required_disk and
mem_available >= required_mem and
(src_vm_workload + workload) < self.threshold / 100 *
cap_cores.get_capacity(host)):
destination_hosts.append(hvmap)
return destination_hosts
def group_hosts_by_cpu_util(self, model):
"""Calculate the workloads of each hypervisor
try to find out the hypervisors which have reached threshold
and the hypervisors which are under threshold.
and also calculate the average workload value of all hypervisors.
and also generate the VM workload map.
"""
hypervisors = model.get_all_hypervisors()
cluster_size = len(hypervisors)
if not hypervisors:
raise wexc.ClusterEmpty()
# get cpu cores capacity of hypervisors and vms
cap_cores = model.get_resource_from_id(resource.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 hypervisor_id in hypervisors:
hypervisor = model.get_hypervisor_from_id(hypervisor_id)
vms = model.get_mapping().get_node_vms(hypervisor)
hypervisor_workload = 0.0
for vm_id in vms:
vm = model.get_vm_from_id(vm_id)
try:
cpu_util = self.ceilometer.statistic_aggregation(
resource_id=vm_id,
meter_name=self._meter,
period=self._period,
aggregate='avg')
except Exception as e:
LOG.error(_LE("Can not get cpu_util: %s"), e.message)
continue
if cpu_util is None:
LOG.debug("%s: cpu_util is None", vm_id)
continue
vm_cores = cap_cores.get_capacity(vm)
workload_cache[vm_id] = cpu_util * vm_cores / 100
hypervisor_workload += workload_cache[vm_id]
LOG.debug("%s: cpu_util %f", vm_id, cpu_util)
hypervisor_cores = cap_cores.get_capacity(hypervisor)
hy_cpu_util = hypervisor_workload / hypervisor_cores * 100
cluster_workload += hypervisor_workload
hvmap = {'hv': hypervisor, "cpu_util": hy_cpu_util, 'workload':
hypervisor_workload}
if hy_cpu_util >= self.threshold:
# mark the hypervisor to release resources
overload_hosts.append(hvmap)
else:
nonoverload_hosts.append(hvmap)
avg_workload = cluster_workload / cluster_size
return overload_hosts, nonoverload_hosts, avg_workload, workload_cache
def execute(self, origin_model):
LOG.info(_LI("Initializing Workload Balance Strategy"))
if origin_model is None:
raise wexc.ClusterStateNotDefined()
current_model = origin_model
src_hypervisors, target_hypervisors, avg_workload, workload_cache = (
self.group_hosts_by_cpu_util(current_model))
if not src_hypervisors:
LOG.debug("No hosts require optimization")
return self.solution
if not target_hypervisors:
LOG.warning(_LW("No hosts current have CPU utilization under %s "
"percent, therefore there are no possible target "
"hosts for any migration"),
self.threshold)
return self.solution
# choose the server with largest cpu_util
src_hypervisors = sorted(src_hypervisors,
reverse=True,
key=lambda x: (x[self.METER_NAME]))
vm_to_migrate = self.choose_vm_to_migrate(current_model,
src_hypervisors,
avg_workload,
workload_cache)
if not vm_to_migrate:
return self.solution
source_hypervisor, vm_src = vm_to_migrate
# find the hosts that have enough resource for the VM to be migrated
destination_hosts = self.filter_destination_hosts(current_model,
target_hypervisors,
vm_src,
avg_workload,
workload_cache)
# sort the filtered result by workload
# pick up the lowest one as dest server
if not destination_hosts:
# for instance.
LOG.warning(_LW("No proper target host could be found, it might "
"be because of there's no enough CPU/Memory/DISK"))
return self.solution
destination_hosts = sorted(destination_hosts,
key=lambda x: (x["cpu_util"]))
# always use the host with lowerest CPU utilization
mig_dst_hypervisor = destination_hosts[0]['hv']
# generate solution to migrate the vm to the dest server,
if current_model.get_mapping().migrate_vm(vm_src,
source_hypervisor,
mig_dst_hypervisor):
parameters = {'migration_type': 'live',
'src_hypervisor': source_hypervisor.uuid,
'dst_hypervisor': mig_dst_hypervisor.uuid}
self.solution.add_action(action_type=self.MIGRATION,
resource_id=vm_src.uuid,
input_parameters=parameters)
self.solution.model = current_model
return self.solution

View File

@@ -0,0 +1,414 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 Servionica LLC
#
# 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 copy import deepcopy
import itertools
import math
import random
import oslo_cache
from oslo_config import cfg
from oslo_log import log
from watcher._i18n import _LI, _
from watcher.common import exception
from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm_state
from watcher.decision_engine.strategy.strategies import base
from watcher.metrics_engine.cluster_history import ceilometer as \
ceilometer_cluster_history
LOG = log.getLogger(__name__)
metrics = ['cpu_util', 'memory.resident']
thresholds_dict = {'cpu_util': 0.2, 'memory.resident': 0.2}
weights_dict = {'cpu_util_weight': 1.0, 'memory.resident_weight': 1.0}
vm_host_measures = {'cpu_util': 'hardware.cpu.util',
'memory.resident': 'hardware.memory.used'}
ws_opts = [
cfg.ListOpt('metrics',
default=metrics,
required=True,
help='Metrics used as rates of cluster loads.'),
cfg.DictOpt('thresholds',
default=thresholds_dict,
help=''),
cfg.DictOpt('weights',
default=weights_dict,
help='These weights used to calculate '
'common standard deviation. Name of weight '
'contains meter name and _weight suffix.'),
cfg.StrOpt('host_choice',
default='retry',
required=True,
help="Method of host's choice."),
cfg.IntOpt('retry_count',
default=1,
required=True,
help='Count of random returned hosts.'),
]
CONF = cfg.CONF
CONF.register_opts(ws_opts, 'watcher_strategies.workload_stabilization')
def _set_memoize(conf):
oslo_cache.configure(conf)
region = oslo_cache.create_region()
configured_region = oslo_cache.configure_cache_region(conf, region)
return oslo_cache.core.get_memoization_decorator(conf,
configured_region,
'cache')
class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
"""Workload Stabilization control using live migration
*Description*
This is workload stabilization strategy based on standard deviation
algorithm. The goal is to determine if there is an overload in a cluster
and respond to it by migrating VMs to stabilize the cluster.
*Requirements*
* Software: Ceilometer component ceilometer-compute running
in each compute host, and Ceilometer API can report such telemetries
``memory.resident`` and ``cpu_util`` successfully.
* You must have at least 2 physical compute nodes to run this strategy.
*Limitations*
- It assume that live migrations are possible
- Load on the system is sufficiently stable.
*Spec URL*
https://review.openstack.org/#/c/286153/
"""
MIGRATION = "migrate"
MEMOIZE = _set_memoize(CONF)
def __init__(self, osc=None):
super(WorkloadStabilization, self).__init__(osc)
self._ceilometer = None
self._nova = None
self.weights = CONF['watcher_strategies.workload_stabilization']\
.weights
self.metrics = CONF['watcher_strategies.workload_stabilization']\
.metrics
self.thresholds = CONF['watcher_strategies.workload_stabilization']\
.thresholds
self.host_choice = CONF['watcher_strategies.workload_stabilization']\
.host_choice
@classmethod
def get_name(cls):
return "WORKLOAD_BALANCING"
@classmethod
def get_display_name(cls):
return _("Workload balancing")
@classmethod
def get_translatable_display_name(cls):
return "Workload balancing"
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = (ceilometer_cluster_history.
CeilometerClusterHistory(osc=self.osc))
return self._ceilometer
@property
def nova(self):
if self._nova is None:
self._nova = self.osc.nova()
return self._nova
@nova.setter
def nova(self, n):
self._nova = n
@ceilometer.setter
def ceilometer(self, c):
self._ceilometer = c
def transform_vm_cpu(self, vm_load, host_vcpus):
"""This method transforms vm cpu utilization to overall host cpu utilization.
:param vm_load: dict that contains vm uuid and utilization info.
:param host_vcpus: int
:return: float value
"""
return vm_load['cpu_util'] * (vm_load['vcpus'] / float(host_vcpus))
@MEMOIZE
def get_vm_load(self, vm_uuid, current_model):
"""Gathering vm load through ceilometer statistic.
:param vm_uuid: vm for which statistic is gathered.
:param current_model: the cluster model
:return: dict
"""
LOG.debug(_LI('get_vm_load started'))
vm_vcpus = current_model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(
current_model.get_vm_from_id(vm_uuid))
vm_load = {'uuid': vm_uuid, 'vcpus': vm_vcpus}
for meter in self.metrics:
avg_meter = self.ceilometer.statistic_aggregation(
resource_id=vm_uuid,
meter_name=meter,
period="120",
aggregate='min'
)
if avg_meter is None:
raise exception.NoMetricValuesForVM(resource_id=vm_uuid,
metric_name=meter)
vm_load[meter] = avg_meter
return vm_load
def normalize_hosts_load(self, hosts, current_model):
normalized_hosts = deepcopy(hosts)
for host in normalized_hosts:
if 'cpu_util' in normalized_hosts[host]:
normalized_hosts[host]['cpu_util'] /= float(100)
if 'memory.resident' in normalized_hosts[host]:
h_memory = current_model.get_resource_from_id(
resource.ResourceType.memory).get_capacity(
current_model.get_hypervisor_from_id(host))
normalized_hosts[host]['memory.resident'] /= float(h_memory)
return normalized_hosts
def get_hosts_load(self, current_model):
"""Get load of every host by gathering vms load"""
hosts_load = {}
for hypervisor_id in current_model.get_all_hypervisors():
hosts_load[hypervisor_id] = {}
host_vcpus = current_model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(
current_model.get_hypervisor_from_id(hypervisor_id))
hosts_load[hypervisor_id]['vcpus'] = host_vcpus
for metric in self.metrics:
avg_meter = self.ceilometer.statistic_aggregation(
resource_id=hypervisor_id,
meter_name=vm_host_measures[metric],
period="60",
aggregate='avg'
)
if avg_meter is None:
raise exception.NoSuchMetricForHost(
metric=vm_host_measures[metric],
host=hypervisor_id)
hosts_load[hypervisor_id][metric] = avg_meter
return hosts_load
def get_sd(self, hosts, meter_name):
"""Get standard deviation among hosts by specified meter"""
mean = 0
variaton = 0
for host_id in hosts:
mean += hosts[host_id][meter_name]
mean /= len(hosts)
for host_id in hosts:
variaton += (hosts[host_id][meter_name] - mean) ** 2
variaton /= len(hosts)
sd = math.sqrt(variaton)
return sd
def calculate_weighted_sd(self, sd_case):
"""Calculate common standard deviation among meters on host"""
weighted_sd = 0
for metric, value in zip(self.metrics, sd_case):
try:
weighted_sd += value * float(self.weights[metric + '_weight'])
except KeyError as exc:
LOG.exception(exc)
raise exception.WatcherException(
_("Incorrect mapping: could not find associated weight"
" for %s in weight dict.") % metric)
return weighted_sd
def calculate_migration_case(self, hosts, vm_id, src_hp_id, dst_hp_id,
current_model):
"""Calculate migration case
Return list of standard deviation values, that appearing in case of
migration of vm from source host to destination host
:param hosts: hosts with their workload
:param vm_id: the virtual machine
:param src_hp_id: the source hypervisor id
:param dst_hp_id: the destination hypervisor id
:param current_model: the cluster model
:return: list of standard deviation values
"""
migration_case = []
new_hosts = deepcopy(hosts)
vm_load = self.get_vm_load(vm_id, current_model)
d_host_vcpus = new_hosts[dst_hp_id]['vcpus']
s_host_vcpus = new_hosts[src_hp_id]['vcpus']
for metric in self.metrics:
if metric is 'cpu_util':
new_hosts[src_hp_id][metric] -= self.transform_vm_cpu(
vm_load,
s_host_vcpus)
new_hosts[dst_hp_id][metric] += self.transform_vm_cpu(
vm_load,
d_host_vcpus)
else:
new_hosts[src_hp_id][metric] -= vm_load[metric]
new_hosts[dst_hp_id][metric] += vm_load[metric]
normalized_hosts = self.normalize_hosts_load(new_hosts, current_model)
for metric in self.metrics:
migration_case.append(self.get_sd(normalized_hosts, metric))
migration_case.append(new_hosts)
return migration_case
def simulate_migrations(self, current_model, hosts):
"""Make sorted list of pairs vm:dst_host"""
def yield_hypervisors(hypervisors):
ct = CONF['watcher_strategies.workload_stabilization'].retry_count
if self.host_choice == 'cycle':
for i in itertools.cycle(hypervisors):
yield [i]
if self.host_choice == 'retry':
while True:
yield random.sample(hypervisors, ct)
if self.host_choice == 'fullsearch':
while True:
yield hypervisors
vm_host_map = []
for source_hp_id in current_model.get_all_hypervisors():
hypervisors = list(current_model.get_all_hypervisors())
hypervisors.remove(source_hp_id)
hypervisor_list = yield_hypervisors(hypervisors)
vms_id = current_model.get_mapping(). \
get_node_vms_from_id(source_hp_id)
for vm_id in vms_id:
min_sd_case = {'value': len(self.metrics)}
vm = current_model.get_vm_from_id(vm_id)
if vm.state not in [vm_state.VMState.ACTIVE.value,
vm_state.VMState.PAUSED.value]:
continue
for dst_hp_id in next(hypervisor_list):
sd_case = self.calculate_migration_case(hosts, vm_id,
source_hp_id,
dst_hp_id,
current_model)
weighted_sd = self.calculate_weighted_sd(sd_case[:-1])
if weighted_sd < min_sd_case['value']:
min_sd_case = {'host': dst_hp_id, 'value': weighted_sd,
's_host': source_hp_id, 'vm': vm_id}
vm_host_map.append(min_sd_case)
break
return sorted(vm_host_map, key=lambda x: x['value'])
def check_threshold(self, current_model):
"""Check if cluster is needed in balancing"""
hosts_load = self.get_hosts_load(current_model)
normalized_load = self.normalize_hosts_load(hosts_load, current_model)
for metric in self.metrics:
metric_sd = self.get_sd(normalized_load, metric)
if metric_sd > float(self.thresholds[metric]):
return self.simulate_migrations(current_model, hosts_load)
def add_migration(self,
resource_id,
migration_type,
src_hypervisor,
dst_hypervisor):
parameters = {'migration_type': migration_type,
'src_hypervisor': src_hypervisor,
'dst_hypervisor': dst_hypervisor}
self.solution.add_action(action_type=self.MIGRATION,
resource_id=resource_id,
input_parameters=parameters)
def create_migration_vm(self, current_model, mig_vm, mig_src_hypervisor,
mig_dst_hypervisor):
"""Create migration VM """
if current_model.get_mapping().migrate_vm(
mig_vm, mig_src_hypervisor, mig_dst_hypervisor):
self.add_migration(mig_vm.uuid, 'live',
mig_src_hypervisor.uuid,
mig_dst_hypervisor.uuid)
def migrate(self, current_model, vm_uuid, src_host, dst_host):
mig_vm = current_model.get_vm_from_id(vm_uuid)
mig_src_hypervisor = current_model.get_hypervisor_from_id(src_host)
mig_dst_hypervisor = current_model.get_hypervisor_from_id(dst_host)
self.create_migration_vm(current_model, mig_vm, mig_src_hypervisor,
mig_dst_hypervisor)
def fill_solution(self, current_model):
self.solution.model = current_model
self.solution.efficacy = 100
return self.solution
def execute(self, orign_model):
LOG.info(_LI("Initializing Workload Stabilization"))
current_model = orign_model
if orign_model is None:
raise exception.ClusterStateNotDefined()
migration = self.check_threshold(current_model)
if migration:
hosts_load = self.get_hosts_load(current_model)
min_sd = 1
balanced = False
for vm_host in migration:
dst_hp_disk = current_model.get_resource_from_id(
resource.ResourceType.disk).get_capacity(
current_model.get_hypervisor_from_id(vm_host['host']))
vm_disk = current_model.get_resource_from_id(
resource.ResourceType.disk).get_capacity(
current_model.get_vm_from_id(vm_host['vm']))
if vm_disk > dst_hp_disk:
continue
vm_load = self.calculate_migration_case(hosts_load,
vm_host['vm'],
vm_host['s_host'],
vm_host['host'],
current_model)
weighted_sd = self.calculate_weighted_sd(vm_load[:-1])
if weighted_sd < min_sd:
min_sd = weighted_sd
hosts_load = vm_load[-1]
self.migrate(current_model, vm_host['vm'],
vm_host['s_host'], vm_host['host'])
for metric, value in zip(self.metrics, vm_load[:-1]):
if value < float(self.thresholds[metric]):
balanced = True
break
if balanced:
break
return self.fill_solution(current_model)

View File

@@ -0,0 +1,301 @@
# -*- 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.
# 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
from oslo_log import log
from watcher._i18n import _LE, _LI
from watcher.common import context
from watcher.decision_engine.strategy.loading import default
from watcher import objects
LOG = log.getLogger(__name__)
GoalMapping = collections.namedtuple('GoalMapping', ['name', 'display_name'])
StrategyMapping = collections.namedtuple(
'StrategyMapping', ['name', 'goal_name', 'display_name'])
class Syncer(object):
"""Syncs all available goals and strategies with the Watcher DB"""
def __init__(self):
self.ctx = context.make_context()
self.discovered_map = None
self._available_goals = None
self._available_goals_map = None
self._available_strategies = None
self._available_strategies_map = None
# This goal mapping maps stale goal IDs to the synced goal
self.goal_mapping = dict()
# This strategy mapping maps stale strategy IDs to the synced goal
self.strategy_mapping = dict()
self.stale_audit_templates_map = {}
@property
def available_goals(self):
if self._available_goals is None:
self._available_goals = objects.Goal.list(self.ctx)
return self._available_goals
@property
def available_strategies(self):
if self._available_strategies is None:
self._available_strategies = objects.Strategy.list(self.ctx)
return self._available_strategies
@property
def available_goals_map(self):
if self._available_goals_map is None:
self._available_goals_map = {
GoalMapping(
name=g.name, display_name=g.display_name): g
for g in self.available_goals
}
return self._available_goals_map
@property
def available_strategies_map(self):
if self._available_strategies_map is None:
goals_map = {g.id: g.name for g in self.available_goals}
self._available_strategies_map = {
StrategyMapping(
name=s.name, goal_name=goals_map[s.goal_id],
display_name=s.display_name): s
for s in self.available_strategies
}
return self._available_strategies_map
def sync(self):
self.discovered_map = self._discover()
goals_map = self.discovered_map["goals"]
strategies_map = self.discovered_map["strategies"]
for goal_name, goal_map in goals_map.items():
if goal_map in self.available_goals_map:
LOG.info(_LI("Goal %s already exists"), goal_name)
continue
self.goal_mapping.update(self._sync_goal(goal_map))
for strategy_name, strategy_map in strategies_map.items():
if (strategy_map in self.available_strategies_map and
strategy_map.goal_name not in
[g.name for g in self.goal_mapping.values()]):
LOG.info(_LI("Strategy %s already exists"), strategy_name)
continue
self.strategy_mapping.update(self._sync_strategy(strategy_map))
self._sync_audit_templates()
def _sync_goal(self, goal_map):
goal_name = goal_map.name
goal_display_name = goal_map.display_name
goal_mapping = dict()
# Goals that are matching by name with the given discovered goal name
matching_goals = [g for g in self.available_goals
if g.name == goal_name]
stale_goals = self._soft_delete_stale_goals(goal_map, matching_goals)
if stale_goals or not matching_goals:
goal = objects.Goal(self.ctx)
goal.name = goal_name
goal.display_name = goal_display_name
goal.create()
LOG.info(_LI("Goal %s created"), goal_name)
# Updating the internal states
self.available_goals_map[goal] = goal_map
# Map the old goal IDs to the new (equivalent) goal
for matching_goal in matching_goals:
goal_mapping[matching_goal.id] = goal
return goal_mapping
def _sync_strategy(self, strategy_map):
strategy_name = strategy_map.name
strategy_display_name = strategy_map.display_name
goal_name = strategy_map.goal_name
strategy_mapping = dict()
# Strategies that are matching by name with the given
# discovered strategy name
matching_strategies = [s for s in self.available_strategies
if s.name == strategy_name]
stale_strategies = self._soft_delete_stale_strategies(
strategy_map, matching_strategies)
if stale_strategies or not matching_strategies:
strategy = objects.Strategy(self.ctx)
strategy.name = strategy_name
strategy.display_name = strategy_display_name
strategy.goal_id = objects.Goal.get_by_name(self.ctx, goal_name).id
strategy.create()
LOG.info(_LI("Strategy %s created"), strategy_name)
# Updating the internal states
self.available_strategies_map[strategy] = strategy_map
# Map the old strategy IDs to the new (equivalent) strategy
for matching_strategy in matching_strategies:
strategy_mapping[matching_strategy.id] = strategy
return strategy_mapping
def _sync_audit_templates(self):
# First we find audit templates that are stale because their associated
# goal or strategy has been modified and we update them in-memory
self._find_stale_audit_templates_due_to_goal()
self._find_stale_audit_templates_due_to_strategy()
# Then we handle the case where an audit template became
# stale because its related goal does not exist anymore.
self._soft_delete_removed_goals()
# Then we handle the case where an audit template became
# stale because its related strategy does not exist anymore.
self._soft_delete_removed_strategies()
# Finally, we save into the DB the updated stale audit templates
for stale_audit_template in self.stale_audit_templates_map.values():
stale_audit_template.save()
LOG.info(_LI("Audit Template '%s' synced"),
stale_audit_template.name)
def _find_stale_audit_templates_due_to_goal(self):
for goal_id, synced_goal in self.goal_mapping.items():
filters = {"goal_id": goal_id}
stale_audit_templates = objects.AuditTemplate.list(
self.ctx, filters=filters)
# Update the goal ID for the stale audit templates (w/o saving)
for audit_template in stale_audit_templates:
if audit_template.id not in self.stale_audit_templates_map:
audit_template.goal_id = synced_goal.id
self.stale_audit_templates_map[audit_template.id] = (
audit_template)
else:
self.stale_audit_templates_map[
audit_template.id].goal_id = synced_goal.id
def _find_stale_audit_templates_due_to_strategy(self):
for strategy_id, synced_strategy in self.strategy_mapping.items():
filters = {"strategy_id": strategy_id}
stale_audit_templates = objects.AuditTemplate.list(
self.ctx, filters=filters)
# Update strategy IDs for all stale audit templates (w/o saving)
for audit_template in stale_audit_templates:
if audit_template.id not in self.stale_audit_templates_map:
audit_template.strategy_id = synced_strategy.id
self.stale_audit_templates_map[audit_template.id] = (
audit_template)
else:
self.stale_audit_templates_map[
audit_template.id].strategy_id = synced_strategy.id
def _soft_delete_removed_goals(self):
removed_goals = [
g for g in self.available_goals
if g.name not in self.discovered_map['goals']]
for removed_goal in removed_goals:
removed_goal.soft_delete()
filters = {"goal_id": removed_goal.id}
invalid_ats = objects.AuditTemplate.list(self.ctx, filters=filters)
for at in invalid_ats:
LOG.warning(
_LE("Audit Template '%(audit_template)s' references a "
"goal that does not exist"),
audit_template=at.uuid)
def _soft_delete_removed_strategies(self):
removed_strategies = [
s for s in self.available_strategies
if s.name not in self.discovered_map['strategies']]
for removed_strategy in removed_strategies:
removed_strategy.soft_delete()
filters = {"strategy_id": removed_strategy.id}
invalid_ats = objects.AuditTemplate.list(self.ctx, filters=filters)
for at in invalid_ats:
LOG.info(
_LI("Audit Template '%(audit_template)s' references a "
"strategy that does not exist"),
audit_template=at.uuid)
# In this case we can reset the strategy ID to None
# so the audit template can still achieve the same goal
# but with a different strategy
if at.id not in self.stale_audit_templates_map:
at.strategy_id = None
self.stale_audit_templates_map[at.id] = at
else:
self.stale_audit_templates_map[at.id].strategy_id = None
def _discover(self):
strategies_map = {}
goals_map = {}
discovered_map = {"goals": goals_map, "strategies": strategies_map}
strategy_loader = default.DefaultStrategyLoader()
implemented_strategies = strategy_loader.list_available()
for _, strategy_cls in implemented_strategies.items():
goals_map[strategy_cls.get_goal_name()] = GoalMapping(
name=strategy_cls.get_goal_name(),
display_name=strategy_cls.get_translatable_goal_display_name())
strategies_map[strategy_cls.get_name()] = StrategyMapping(
name=strategy_cls.get_name(),
goal_name=strategy_cls.get_goal_name(),
display_name=strategy_cls.get_translatable_display_name())
return discovered_map
def _soft_delete_stale_goals(self, goal_map, matching_goals):
goal_name = goal_map.name
goal_display_name = goal_map.display_name
stale_goals = []
for matching_goal in matching_goals:
if (matching_goal.display_name == goal_display_name and
matching_goal.strategy_id not in self.strategy_mapping):
LOG.info(_LI("Goal %s unchanged"), goal_name)
else:
LOG.info(_LI("Goal %s modified"), goal_name)
matching_goal.soft_delete()
stale_goals.append(matching_goal)
return stale_goals
def _soft_delete_stale_strategies(self, strategy_map, matching_strategies):
strategy_name = strategy_map.name
strategy_display_name = strategy_map.display_name
stale_strategies = []
for matching_strategy in matching_strategies:
if (matching_strategy.display_name == strategy_display_name and
matching_strategy.goal_id not in self.goal_mapping):
LOG.info(_LI("Strategy %s unchanged"), strategy_name)
else:
LOG.info(_LI("Strategy %s modified"), strategy_name)
matching_strategy.soft_delete()
stale_strategies.append(matching_strategy)
return stale_strategies

View File

@@ -7,16 +7,46 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: python-watcher 0.25.1.dev3\n" "Project-Id-Version: python-watcher 0.26.1.dev33\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-03-30 10:10+0200\n" "POT-Creation-Date: 2016-05-11 15:31+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.2.0\n" "Generated-By: Babel 2.3.4\n"
#: watcher/api/app.py:31
msgid "The port for the watcher API server"
msgstr ""
#: watcher/api/app.py:34
msgid "The listen IP for the watcher API server"
msgstr ""
#: watcher/api/app.py:37
msgid ""
"The maximum number of items returned in a single response from a "
"collection resource"
msgstr ""
#: watcher/api/app.py:41
msgid ""
"Number of workers for Watcher API service. The default is equal to the "
"number of CPUs available if that can be determined, else a default worker"
" count of 1 is returned."
msgstr ""
#: watcher/api/app.py:48
msgid ""
"Enable the integrated stand-alone API to service requests via HTTPS "
"instead of HTTP. If there is a front-end service performing HTTPS "
"offloading from the service, this option should be False; note, you will "
"want to change public API endpoint to represent SSL termination URL with "
"'public_endpoint' option."
msgstr ""
#: watcher/api/controllers/v1/action.py:364 #: watcher/api/controllers/v1/action.py:364
msgid "Cannot create an action directly" msgid "Cannot create an action directly"
@@ -30,41 +60,52 @@ msgstr ""
msgid "Cannot delete an action directly" msgid "Cannot delete an action directly"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/action_plan.py:102 #: watcher/api/controllers/v1/action_plan.py:87
#, python-format #, python-format
msgid "Invalid state: %(state)s" msgid "Invalid state: %(state)s"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/action_plan.py:422 #: watcher/api/controllers/v1/action_plan.py:407
#, python-format #, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)" msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/audit.py:359 #: watcher/api/controllers/v1/audit.py:362
msgid "The audit template UUID or name specified is invalid" msgid "The audit template UUID or name specified is invalid"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/types.py:148 #: watcher/api/controllers/v1/audit_template.py:138
#, python-format
msgid ""
"'%(strategy)s' strategy does relate to the '%(goal)s' goal. Possible "
"choices: %(choices)s"
msgstr ""
#: watcher/api/controllers/v1/audit_template.py:160
msgid "Cannot remove 'goal_uuid' attribute from an audit template"
msgstr ""
#: watcher/api/controllers/v1/types.py:123
#, python-format #, python-format
msgid "%s is not JSON serializable" msgid "%s is not JSON serializable"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/types.py:184 #: watcher/api/controllers/v1/types.py:159
#, python-format #, python-format
msgid "Wrong type. Expected '%(type)s', got '%(value)s'" msgid "Wrong type. Expected '%(type)s', got '%(value)s'"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/types.py:223 #: watcher/api/controllers/v1/types.py:198
#, python-format #, python-format
msgid "'%s' is an internal attribute and can not be updated" msgid "'%s' is an internal attribute and can not be updated"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/types.py:227 #: watcher/api/controllers/v1/types.py:202
#, python-format #, python-format
msgid "'%s' is a mandatory attribute and can not be removed" msgid "'%s' is a mandatory attribute and can not be removed"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/types.py:232 #: watcher/api/controllers/v1/types.py:207
msgid "'add' and 'replace' operations needs value" msgid "'add' and 'replace' operations needs value"
msgstr "" msgstr ""
@@ -135,22 +176,26 @@ msgstr ""
msgid "Oops! We need disaster recover plan" msgid "Oops! We need disaster recover plan"
msgstr "" msgstr ""
#: watcher/cmd/api.py:46 watcher/cmd/applier.py:39 #: watcher/cmd/api.py:46
#: watcher/cmd/decisionengine.py:40
#, python-format
msgid "Starting server in PID %s"
msgstr ""
#: watcher/cmd/api.py:51
#, python-format #, python-format
msgid "serving on 0.0.0.0:%(port)s, view at http://127.0.0.1:%(port)s" msgid "serving on 0.0.0.0:%(port)s, view at http://127.0.0.1:%(port)s"
msgstr "" msgstr ""
#: watcher/cmd/api.py:55 #: watcher/cmd/api.py:50
#, python-format #, python-format
msgid "serving on http://%(host)s:%(port)s" msgid "serving on http://%(host)s:%(port)s"
msgstr "" msgstr ""
#: watcher/cmd/applier.py:41
#, python-format
msgid "Starting Watcher Applier service in PID %s"
msgstr ""
#: watcher/cmd/decisionengine.py:42
#, python-format
msgid "Starting Watcher Decision Engine service in PID %s"
msgstr ""
#: watcher/common/clients.py:29 #: watcher/common/clients.py:29
msgid "Version of Nova API to use in novaclient." msgid "Version of Nova API to use in novaclient."
msgstr "" msgstr ""
@@ -212,189 +257,209 @@ msgstr ""
#: watcher/common/exception.py:150 #: watcher/common/exception.py:150
#, python-format #, python-format
msgid "Expected an uuid or int but received %(identity)s" msgid "Expected a uuid or int but received %(identity)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:154 #: watcher/common/exception.py:154
#, python-format #, python-format
msgid "Goal %(goal)s is not defined in Watcher configuration file" msgid "Goal %(goal)s is invalid"
msgstr "" msgstr ""
#: watcher/common/exception.py:158 #: watcher/common/exception.py:158
#, python-format #, python-format
msgid "Expected a uuid but received %(uuid)s" msgid "Strategy %(strategy)s is invalid"
msgstr "" msgstr ""
#: watcher/common/exception.py:162 #: watcher/common/exception.py:162
#, python-format #, python-format
msgid "Expected a logical name but received %(name)s" msgid "Expected a uuid but received %(uuid)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:166 #: watcher/common/exception.py:166
#, python-format #, python-format
msgid "Expected a logical name or uuid but received %(name)s" msgid "Expected a logical name but received %(name)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:170 #: watcher/common/exception.py:170
#, python-format #, python-format
msgid "AuditTemplate %(audit_template)s could not be found" msgid "Expected a logical name or uuid but received %(name)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:174 #: watcher/common/exception.py:174
#, python-format #, python-format
msgid "An audit_template with UUID %(uuid)s or name %(name)s already exists" msgid "Goal %(goal)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:179 #: watcher/common/exception.py:178
#, python-format
msgid "A goal with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:182
#, python-format
msgid "Strategy %(strategy)s could not be found"
msgstr ""
#: watcher/common/exception.py:186
#, python-format
msgid "A strategy with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:190
#, python-format
msgid "AuditTemplate %(audit_template)s could not be found"
msgstr ""
#: watcher/common/exception.py:194
#, python-format
msgid "An audit_template with UUID or name %(audit_template)s already exists"
msgstr ""
#: watcher/common/exception.py:199
#, python-format #, python-format
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit" msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
msgstr "" msgstr ""
#: watcher/common/exception.py:184 #: watcher/common/exception.py:204
#, python-format
msgid "Audit type %(audit_type)s could not be found"
msgstr ""
#: watcher/common/exception.py:208
#, python-format #, python-format
msgid "Audit %(audit)s could not be found" msgid "Audit %(audit)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:188 #: watcher/common/exception.py:212
#, python-format #, python-format
msgid "An audit with UUID %(uuid)s already exists" msgid "An audit with UUID %(uuid)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:192 #: watcher/common/exception.py:216
#, python-format #, python-format
msgid "Audit %(audit)s is referenced by one or multiple action plans" msgid "Audit %(audit)s is referenced by one or multiple action plans"
msgstr "" msgstr ""
#: watcher/common/exception.py:197 #: watcher/common/exception.py:221
#, python-format #, python-format
msgid "ActionPlan %(action_plan)s could not be found" msgid "ActionPlan %(action_plan)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:201 #: watcher/common/exception.py:225
#, python-format #, python-format
msgid "An action plan with UUID %(uuid)s already exists" msgid "An action plan with UUID %(uuid)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:205 #: watcher/common/exception.py:229
#, python-format #, python-format
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions" msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
msgstr "" msgstr ""
#: watcher/common/exception.py:210 #: watcher/common/exception.py:234
#, python-format #, python-format
msgid "Action %(action)s could not be found" msgid "Action %(action)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:214 #: watcher/common/exception.py:238
#, python-format #, python-format
msgid "An action with UUID %(uuid)s already exists" msgid "An action with UUID %(uuid)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:218 #: watcher/common/exception.py:242
#, python-format #, python-format
msgid "Action plan %(action_plan)s is referenced by one or multiple goals" msgid "Action plan %(action_plan)s is referenced by one or multiple goals"
msgstr "" msgstr ""
#: watcher/common/exception.py:223 #: watcher/common/exception.py:247
msgid "Filtering actions on both audit and action-plan is prohibited" msgid "Filtering actions on both audit and action-plan is prohibited"
msgstr "" msgstr ""
#: watcher/common/exception.py:232 #: watcher/common/exception.py:256
#, python-format #, python-format
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s" msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:238 #: watcher/common/exception.py:262
#, python-format #, python-format
msgid "Workflow execution error: %(error)s" msgid "Workflow execution error: %(error)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:242 #: watcher/common/exception.py:266
msgid "Illegal argument" msgid "Illegal argument"
msgstr "" msgstr ""
#: watcher/common/exception.py:246 #: watcher/common/exception.py:270
msgid "No such metric" msgid "No such metric"
msgstr "" msgstr ""
#: watcher/common/exception.py:250 #: watcher/common/exception.py:274
msgid "No rows were returned" msgid "No rows were returned"
msgstr "" msgstr ""
#: watcher/common/exception.py:254 #: watcher/common/exception.py:278
#, python-format #, python-format
msgid "%(client)s connection failed. Reason: %(reason)s" msgid "%(client)s connection failed. Reason: %(reason)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:258 #: watcher/common/exception.py:282
msgid "'Keystone API endpoint is missing''" msgid "'Keystone API endpoint is missing''"
msgstr "" msgstr ""
#: watcher/common/exception.py:262 #: watcher/common/exception.py:286
msgid "The list of hypervisor(s) in the cluster is empty" msgid "The list of hypervisor(s) in the cluster is empty"
msgstr "" msgstr ""
#: watcher/common/exception.py:266 #: watcher/common/exception.py:290
msgid "The metrics resource collector is not defined" msgid "The metrics resource collector is not defined"
msgstr "" msgstr ""
#: watcher/common/exception.py:270 #: watcher/common/exception.py:294
msgid "the cluster state is not defined" msgid "The cluster state is not defined"
msgstr "" msgstr ""
#: watcher/common/exception.py:276 #: watcher/common/exception.py:298
#, python-format
msgid "No strategy could be found to achieve the '%(goal)s' goal."
msgstr ""
#: watcher/common/exception.py:304
#, python-format #, python-format
msgid "The instance '%(name)s' is not found" msgid "The instance '%(name)s' is not found"
msgstr "" msgstr ""
#: watcher/common/exception.py:280 #: watcher/common/exception.py:308
msgid "The hypervisor is not found" msgid "The hypervisor is not found"
msgstr "" msgstr ""
#: watcher/common/exception.py:284 #: watcher/common/exception.py:312
#, python-format #, python-format
msgid "Error loading plugin '%(name)s'" msgid "Error loading plugin '%(name)s'"
msgstr "" msgstr ""
#: watcher/common/exception.py:288 #: watcher/common/exception.py:316
#, python-format #, python-format
msgid "The identifier '%(name)s' is a reserved word" msgid "The identifier '%(name)s' is a reserved word"
msgstr "" msgstr ""
#: watcher/common/exception.py:292 #: watcher/common/exception.py:320
#, python-format #, python-format
msgid "The %(name)s resource %(id)s is not soft deleted" msgid "The %(name)s resource %(id)s is not soft deleted"
msgstr "" msgstr ""
#: watcher/common/exception.py:296 #: watcher/common/exception.py:324
msgid "Limit should be positive" msgid "Limit should be positive"
msgstr "" msgstr ""
#: watcher/common/service.py:83 #: watcher/common/service.py:40
#, python-format msgid "Seconds between running periodic tasks."
msgid "Created RPC server for service %(service)s on host %(host)s."
msgstr "" msgstr ""
#: watcher/common/service.py:92 #: watcher/common/service.py:43
#, python-format
msgid "Service error occurred when stopping the RPC server. Error: %s"
msgstr ""
#: watcher/common/service.py:97
#, python-format
msgid "Service error occurred when cleaning up the RPC manager. Error: %s"
msgstr ""
#: watcher/common/service.py:101
#, python-format
msgid "Stopped RPC server for service %(service)s on host %(host)s."
msgstr ""
#: watcher/common/service.py:106
#, python-format
msgid "" msgid ""
"Got signal SIGUSR1. Not deregistering on next shutdown of service " "Name of this node. This can be an opaque identifier. It is not "
"%(service)s on host %(host)s." "necessarily a hostname, FQDN, or IP address. However, the node name must "
"be valid within an AMQP key, and if using ZeroMQ, a valid hostname, FQDN,"
" or IP address."
msgstr "" msgstr ""
#: watcher/common/utils.py:53 #: watcher/common/utils.py:53
@@ -413,101 +478,111 @@ msgid "Messaging configuration error"
msgstr "" msgstr ""
#: watcher/db/purge.py:50 #: watcher/db/purge.py:50
msgid "Audit Templates" msgid "Goals"
msgstr "" msgstr ""
#: watcher/db/purge.py:51 #: watcher/db/purge.py:51
msgid "Audits" msgid "Strategies"
msgstr "" msgstr ""
#: watcher/db/purge.py:52 #: watcher/db/purge.py:52
msgid "Action Plans" msgid "Audit Templates"
msgstr "" msgstr ""
#: watcher/db/purge.py:53 #: watcher/db/purge.py:53
msgid "Audits"
msgstr ""
#: watcher/db/purge.py:54
msgid "Action Plans"
msgstr ""
#: watcher/db/purge.py:55
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
#: watcher/db/purge.py:100 #: watcher/db/purge.py:102
msgid "Total" msgid "Total"
msgstr "" msgstr ""
#: watcher/db/purge.py:158 #: watcher/db/purge.py:160
msgid "Audit Template" msgid "Audit Template"
msgstr "" msgstr ""
#: watcher/db/purge.py:206 #: watcher/db/purge.py:227
#, python-format #, python-format
msgid "" msgid ""
"Orphans found:\n" "Orphans found:\n"
"%s" "%s"
msgstr "" msgstr ""
#: watcher/db/purge.py:265 #: watcher/db/purge.py:306
#, python-format #, python-format
msgid "There are %(count)d objects set for deletion. Continue? [y/N]" msgid "There are %(count)d objects set for deletion. Continue? [y/N]"
msgstr "" msgstr ""
#: watcher/db/purge.py:272 #: watcher/db/purge.py:313
#, python-format #, python-format
msgid "" msgid ""
"The number of objects (%(num)s) to delete from the database exceeds the " "The number of objects (%(num)s) to delete from the database exceeds the "
"maximum number of objects (%(max_number)s) specified." "maximum number of objects (%(max_number)s) specified."
msgstr "" msgstr ""
#: watcher/db/purge.py:277 #: watcher/db/purge.py:318
msgid "Do you want to delete objects up to the specified maximum number? [y/N]" msgid "Do you want to delete objects up to the specified maximum number? [y/N]"
msgstr "" msgstr ""
#: watcher/db/purge.py:340 #: watcher/db/purge.py:408
msgid "Deleting..." msgid "Deleting..."
msgstr "" msgstr ""
#: watcher/db/purge.py:346 #: watcher/db/purge.py:414
msgid "Starting purge command" msgid "Starting purge command"
msgstr "" msgstr ""
#: watcher/db/purge.py:356 #: watcher/db/purge.py:424
msgid " (orphans excluded)" msgid " (orphans excluded)"
msgstr "" msgstr ""
#: watcher/db/purge.py:357 #: watcher/db/purge.py:425
msgid " (may include orphans)" msgid " (may include orphans)"
msgstr "" msgstr ""
#: watcher/db/purge.py:360 watcher/db/purge.py:361 #: watcher/db/purge.py:428 watcher/db/purge.py:429
#, python-format #, python-format
msgid "Purge results summary%s:" msgid "Purge results summary%s:"
msgstr "" msgstr ""
#: watcher/db/purge.py:364 #: watcher/db/purge.py:432
#, python-format #, python-format
msgid "Here below is a table containing the objects that can be purged%s:" msgid "Here below is a table containing the objects that can be purged%s:"
msgstr "" msgstr ""
#: watcher/db/purge.py:369 #: watcher/db/purge.py:437
msgid "Purge process completed" msgid "Purge process completed"
msgstr "" msgstr ""
#: watcher/db/sqlalchemy/api.py:362 #: watcher/db/sqlalchemy/api.py:443
msgid "" msgid "Cannot overwrite UUID for an existing Goal."
"Multiple audit templates exist with the same name. Please use the audit "
"template uuid instead"
msgstr "" msgstr ""
#: watcher/db/sqlalchemy/api.py:384 #: watcher/db/sqlalchemy/api.py:509
msgid "Cannot overwrite UUID for an existing Strategy."
msgstr ""
#: watcher/db/sqlalchemy/api.py:586
msgid "Cannot overwrite UUID for an existing Audit Template." msgid "Cannot overwrite UUID for an existing Audit Template."
msgstr "" msgstr ""
#: watcher/db/sqlalchemy/api.py:495 #: watcher/db/sqlalchemy/api.py:683
msgid "Cannot overwrite UUID for an existing Audit." msgid "Cannot overwrite UUID for an existing Audit."
msgstr "" msgstr ""
#: watcher/db/sqlalchemy/api.py:588 #: watcher/db/sqlalchemy/api.py:778
msgid "Cannot overwrite UUID for an existing Action." msgid "Cannot overwrite UUID for an existing Action."
msgstr "" msgstr ""
#: watcher/db/sqlalchemy/api.py:699 #: watcher/db/sqlalchemy/api.py:891
msgid "Cannot overwrite UUID for an existing Action Plan." msgid "Cannot overwrite UUID for an existing Action Plan."
msgstr "" msgstr ""
@@ -517,68 +592,157 @@ msgid ""
"instead" "instead"
msgstr "" msgstr ""
#: watcher/decision_engine/model/model_root.py:37 #: watcher/decision_engine/sync.py:94
#: watcher/decision_engine/model/model_root.py:42 #, python-format
msgid "Goal %s already exists"
msgstr ""
#: watcher/decision_engine/sync.py:103
#, python-format
msgid "Strategy %s already exists"
msgstr ""
#: watcher/decision_engine/sync.py:125
#, python-format
msgid "Goal %s created"
msgstr ""
#: watcher/decision_engine/sync.py:154
#, python-format
msgid "Strategy %s created"
msgstr ""
#: watcher/decision_engine/sync.py:180
#, python-format
msgid "Audit Template '%s' synced"
msgstr ""
#: watcher/decision_engine/sync.py:225
#, python-format
msgid "Audit Template '%(audit_template)s' references a goal that does not exist"
msgstr ""
#: watcher/decision_engine/sync.py:240
#, python-format
msgid ""
"Audit Template '%(audit_template)s' references a strategy that does not "
"exist"
msgstr ""
#: watcher/decision_engine/sync.py:279
#, python-format
msgid "Goal %s unchanged"
msgstr ""
#: watcher/decision_engine/sync.py:281
#, python-format
msgid "Goal %s modified"
msgstr ""
#: watcher/decision_engine/sync.py:295
#, python-format
msgid "Strategy %s unchanged"
msgstr ""
#: watcher/decision_engine/sync.py:297
#, python-format
msgid "Strategy %s modified"
msgstr ""
#: watcher/decision_engine/model/model_root.py:33
#: watcher/decision_engine/model/model_root.py:38
msgid "'obj' argument type is not valid" msgid "'obj' argument type is not valid"
msgstr "" msgstr ""
#: watcher/decision_engine/planner/default.py:79 #: watcher/decision_engine/planner/default.py:78
msgid "The action plan is empty" msgid "The action plan is empty"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/selection/default.py:60 #: watcher/decision_engine/strategy/selection/default.py:74
#, python-format #, python-format
msgid "Incorrect mapping: could not find associated strategy for '%s'" msgid "Could not load any strategy for goal %(goal)s"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:288 #: watcher/decision_engine/strategy/strategies/base.py:165
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:335 msgid "Dummy goal"
msgstr ""
#: watcher/decision_engine/strategy/strategies/base.py:188
msgid "Unclassified"
msgstr ""
#: watcher/decision_engine/strategy/strategies/base.py:204
msgid "Server consolidation"
msgstr ""
#: watcher/decision_engine/strategy/strategies/base.py:220
msgid "Thermal optimization"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:119
msgid "Basic offline consolidation"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:296
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:343
#, python-format #, python-format
msgid "No values returned by %(resource_id)s for %(metric_name)s" msgid "No values returned by %(resource_id)s for %(metric_name)s"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:448 #: watcher/decision_engine/strategy/strategies/basic_consolidation.py:456
msgid "Initializing Sercon Consolidation" msgid "Initializing Sercon Consolidation"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:492 #: watcher/decision_engine/strategy/strategies/basic_consolidation.py:500
msgid "The workloads of the compute nodes of the cluster is zero" msgid "The workloads of the compute nodes of the cluster is zero"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:147 #: watcher/decision_engine/strategy/strategies/dummy_strategy.py:74
msgid "Dummy strategy"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:102
msgid "Outlet temperature based strategy"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:156
#, python-format #, python-format
msgid "%s: no outlet temp data" msgid "%s: no outlet temp data"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:172 #: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:181
#, python-format #, python-format
msgid "VM not active, skipped: %s" msgid "VM not active, skipped: %s"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:230 #: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:239
msgid "No hosts under outlet temp threshold found" msgid "No hosts under outlet temp threshold found"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:253 #: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:262
msgid "No proper target host could be found" msgid "No proper target host could be found"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:104 #: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:100
msgid "VM Workload Consolidation Strategy"
msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:128
#, python-format #, python-format
msgid "Unexpexted resource state type, state=%(state)s, state_type=%(st)s." msgid "Unexpexted resource state type, state=%(state)s, state_type=%(st)s."
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:156 #: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:180
#, python-format #, python-format
msgid "Cannot live migrate: vm_uuid=%(vm_uuid)s, state=%(vm_state)s." msgid "Cannot live migrate: vm_uuid=%(vm_uuid)s, state=%(vm_state)s."
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:240 #: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:264
#, python-format #, python-format
msgid "No values returned by %(resource_id)s for memory.usage or disk.root.size" msgid "No values returned by %(resource_id)s for memory.usage or disk.root.size"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:489 #: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:515
msgid "Executing Smart Strategy" msgid "Executing Smart Strategy"
msgstr "" msgstr ""

View File

@@ -19,13 +19,11 @@
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log
from watcher.common import ceilometer_helper from watcher.common import ceilometer_helper
from watcher.metrics_engine.cluster_history import base from watcher.metrics_engine.cluster_history import base
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__)
class CeilometerClusterHistory(base.BaseClusterHistory): class CeilometerClusterHistory(base.BaseClusterHistory):

View File

@@ -18,13 +18,11 @@
# #
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log
from watcher.common import nova_helper from watcher.common import nova_helper
from watcher.metrics_engine.cluster_model_collector import nova as cnova from watcher.metrics_engine.cluster_model_collector import nova as cnova
LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF

View File

@@ -17,10 +17,15 @@ from watcher.objects import action
from watcher.objects import action_plan from watcher.objects import action_plan
from watcher.objects import audit from watcher.objects import audit
from watcher.objects import audit_template from watcher.objects import audit_template
from watcher.objects import goal
from watcher.objects import strategy
Audit = audit.Audit Audit = audit.Audit
AuditTemplate = audit_template.AuditTemplate AuditTemplate = audit_template.AuditTemplate
Action = action.Action Action = action.Action
ActionPlan = action_plan.ActionPlan ActionPlan = action_plan.ActionPlan
Goal = goal.Goal
Strategy = strategy.Strategy
__all__ = (Audit, AuditTemplate, Action, ActionPlan) __all__ = ("Audit", "AuditTemplate", "Action",
"ActionPlan", "Goal", "Strategy")

View File

@@ -44,8 +44,6 @@ class Action(base.WatcherObject):
'action_type': obj_utils.str_or_none, 'action_type': obj_utils.str_or_none,
'input_parameters': obj_utils.dict_or_none, 'input_parameters': obj_utils.dict_or_none,
'state': obj_utils.str_or_none, 'state': obj_utils.str_or_none,
# todo(jed) remove parameter alarm
'alarm': obj_utils.str_or_none,
'next': obj_utils.int_or_none, 'next': obj_utils.int_or_none,
} }

View File

@@ -48,6 +48,8 @@ be one of the following:
:ref:`Administrator <administrator_definition>` :ref:`Administrator <administrator_definition>`
""" """
import enum
from watcher.common import exception from watcher.common import exception
from watcher.common import utils from watcher.common import utils
from watcher.db import api as dbapi from watcher.db import api as dbapi
@@ -65,7 +67,7 @@ class State(object):
PENDING = 'PENDING' PENDING = 'PENDING'
class AuditType(object): class AuditType(enum.Enum):
ONESHOT = 'ONESHOT' ONESHOT = 'ONESHOT'
CONTINUOUS = 'CONTINUOUS' CONTINUOUS = 'CONTINUOUS'

View File

@@ -65,7 +65,8 @@ class AuditTemplate(base.WatcherObject):
'uuid': obj_utils.str_or_none, 'uuid': obj_utils.str_or_none,
'name': obj_utils.str_or_none, 'name': obj_utils.str_or_none,
'description': obj_utils.str_or_none, 'description': obj_utils.str_or_none,
'goal': obj_utils.str_or_none, 'goal_id': obj_utils.int_or_none,
'strategy_id': obj_utils.int_or_none,
'host_aggregate': obj_utils.int_or_none, 'host_aggregate': obj_utils.int_or_none,
'extra': obj_utils.dict_or_none, 'extra': obj_utils.dict_or_none,
'version': obj_utils.str_or_none, 'version': obj_utils.str_or_none,
@@ -83,8 +84,7 @@ class AuditTemplate(base.WatcherObject):
@staticmethod @staticmethod
def _from_db_object_list(db_objects, cls, context): def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects.""" """Converts a list of database entities to a list of formal objects."""
return \ return [AuditTemplate._from_db_object(cls(context), obj)
[AuditTemplate._from_db_object(cls(context), obj)
for obj in db_objects] for obj in db_objects]
@classmethod @classmethod

View File

@@ -343,7 +343,7 @@ class WatcherObject(object):
@property @property
def obj_fields(self): def obj_fields(self):
return self.fields.keys() + self.obj_extra_fields return list(self.fields.keys()) + self.obj_extra_fields
# dictish syntactic sugar # dictish syntactic sugar
def iteritems(self): def iteritems(self):

184
watcher/objects/goal.py Normal file
View File

@@ -0,0 +1,184 @@
# -*- encoding: utf-8 -*-
# Copyright 2013 IBM Corp.
#
# 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 exception
from watcher.common import utils
from watcher.db import api as dbapi
from watcher.objects import base
from watcher.objects import utils as obj_utils
class Goal(base.WatcherObject):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'id': int,
'uuid': obj_utils.str_or_none,
'name': obj_utils.str_or_none,
'display_name': obj_utils.str_or_none,
}
@staticmethod
def _from_db_object(goal, db_goal):
"""Converts a database entity to a formal object."""
for field in goal.fields:
goal[field] = db_goal[field]
goal.obj_reset_changes()
return goal
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [cls._from_db_object(cls(context), obj) for obj in db_objects]
@classmethod
def get(cls, context, goal_id):
"""Find a goal based on its id or uuid
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Goal(context)
:param goal_id: the id *or* uuid of a goal.
:returns: a :class:`Goal` object.
"""
if utils.is_int_like(goal_id):
return cls.get_by_id(context, goal_id)
elif utils.is_uuid_like(goal_id):
return cls.get_by_uuid(context, goal_id)
else:
raise exception.InvalidIdentity(identity=goal_id)
@classmethod
def get_by_id(cls, context, goal_id):
"""Find a goal based on its integer id
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Goal(context)
:param goal_id: the id *or* uuid of a goal.
:returns: a :class:`Goal` object.
"""
db_goal = cls.dbapi.get_goal_by_id(context, goal_id)
goal = cls._from_db_object(cls(context), db_goal)
return goal
@classmethod
def get_by_uuid(cls, context, uuid):
"""Find a goal based on uuid
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Goal(context)
:param uuid: the uuid of a goal.
:returns: a :class:`Goal` object.
"""
db_goal = cls.dbapi.get_goal_by_uuid(context, uuid)
goal = cls._from_db_object(cls(context), db_goal)
return goal
@classmethod
def get_by_name(cls, context, name):
"""Find a goal based on name
:param name: the name of a goal.
:param context: Security context
:returns: a :class:`Goal` object.
"""
db_goal = cls.dbapi.get_goal_by_name(context, name)
goal = cls._from_db_object(cls(context), db_goal)
return goal
@classmethod
def list(cls, context, limit=None, marker=None, filters=None,
sort_key=None, sort_dir=None):
"""Return a list of :class:`Goal` objects.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Goal(context)
:param filters: dict mapping the filter key to a value.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:returns: a list of :class:`Goal` object.
"""
db_goals = cls.dbapi.get_goal_list(
context,
filters=filters,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
return cls._from_db_object_list(db_goals, cls, context)
def create(self):
"""Create a :class:`Goal` record in the DB."""
values = self.obj_get_changes()
db_goal = self.dbapi.create_goal(values)
self._from_db_object(self, db_goal)
def destroy(self):
"""Delete the :class:`Goal` from the DB."""
self.dbapi.destroy_goal(self.id)
self.obj_reset_changes()
def save(self):
"""Save updates to this :class:`Goal`.
Updates will be made column by column based on the result
of self.what_changed().
"""
updates = self.obj_get_changes()
self.dbapi.update_goal(self.id, updates)
self.obj_reset_changes()
def refresh(self):
"""Loads updates for this :class:`Goal`.
Loads a goal with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded goal column by column, if there are any updates.
"""
current = self.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if (hasattr(self, base.get_attrname(field)) and
self[field] != current[field]):
self[field] = current[field]
def soft_delete(self):
"""Soft Delete the :class:`Goal` from the DB."""
self.dbapi.soft_delete_goal(self.uuid)

222
watcher/objects/strategy.py Normal file
View File

@@ -0,0 +1,222 @@
# -*- 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.
# 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 exception
from watcher.common import utils
from watcher.db import api as dbapi
from watcher.objects import base
from watcher.objects import utils as obj_utils
class Strategy(base.WatcherObject):
dbapi = dbapi.get_instance()
fields = {
'id': int,
'uuid': obj_utils.str_or_none,
'name': obj_utils.str_or_none,
'display_name': obj_utils.str_or_none,
'goal_id': obj_utils.int_or_none,
}
@staticmethod
def _from_db_object(strategy, db_strategy):
"""Converts a database entity to a formal object."""
for field in strategy.fields:
strategy[field] = db_strategy[field]
strategy.obj_reset_changes()
return strategy
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [Strategy._from_db_object(cls(context), obj)
for obj in db_objects]
@classmethod
def get(cls, context, strategy_id):
"""Find a strategy based on its id or uuid
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
:param strategy_id: the id *or* uuid of a strategy.
:returns: a :class:`Strategy` object.
"""
if utils.is_int_like(strategy_id):
return cls.get_by_id(context, strategy_id)
elif utils.is_uuid_like(strategy_id):
return cls.get_by_uuid(context, strategy_id)
else:
raise exception.InvalidIdentity(identity=strategy_id)
@classmethod
def get_by_id(cls, context, strategy_id):
"""Find a strategy based on its integer id
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
:param strategy_id: the id of a strategy.
:returns: a :class:`Strategy` object.
"""
db_strategy = cls.dbapi.get_strategy_by_id(context, strategy_id)
strategy = Strategy._from_db_object(cls(context), db_strategy)
return strategy
@classmethod
def get_by_uuid(cls, context, uuid):
"""Find a strategy based on uuid
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
:param uuid: the uuid of a strategy.
:returns: a :class:`Strategy` object.
"""
db_strategy = cls.dbapi.get_strategy_by_uuid(context, uuid)
strategy = cls._from_db_object(cls(context), db_strategy)
return strategy
@classmethod
def get_by_name(cls, context, name):
"""Find a strategy based on name
:param name: the name of a strategy.
:param context: Security context
:returns: a :class:`Strategy` object.
"""
db_strategy = cls.dbapi.get_strategy_by_name(context, name)
strategy = cls._from_db_object(cls(context), db_strategy)
return strategy
@classmethod
def list(cls, context, limit=None, marker=None, filters=None,
sort_key=None, sort_dir=None):
"""Return a list of :class:`Strategy` objects.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
:param filters: dict mapping the filter key to a value.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:returns: a list of :class:`Strategy` object.
"""
db_strategies = cls.dbapi.get_strategy_list(
context,
filters=filters,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
return Strategy._from_db_object_list(db_strategies, cls, context)
def create(self, context=None):
"""Create a :class:`Strategy` record in the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
"""
values = self.obj_get_changes()
db_strategy = self.dbapi.create_strategy(values)
self._from_db_object(self, db_strategy)
def destroy(self, context=None):
"""Delete the :class:`Strategy` from the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
"""
self.dbapi.destroy_strategy(self.id)
self.obj_reset_changes()
def save(self, context=None):
"""Save updates to this :class:`Strategy`.
Updates will be made column by column based on the result
of self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
"""
updates = self.obj_get_changes()
self.dbapi.update_strategy(self.id, updates)
self.obj_reset_changes()
def refresh(self, context=None):
"""Loads updates for this :class:`Strategy`.
Loads a strategy with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded strategy column by column, if there are any updates.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
"""
current = self.__class__.get_by_id(self._context, strategy_id=self.id)
for field in self.fields:
if (hasattr(self, base.get_attrname(field)) and
self[field] != current[field]):
self[field] = current[field]
def soft_delete(self, context=None):
"""Soft Delete the :class:`Strategy` from the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Strategy(context)
"""
self.dbapi.soft_delete_strategy(self.id)

View File

@@ -16,20 +16,32 @@
# limitations under the License. # limitations under the License.
from keystoneauth1 import loading as ka_loading from keystoneauth1 import loading as ka_loading
import prettytable as ptable
import watcher.api.app import watcher.api.app
from watcher.applier.actions.loading import default as action_loader
from watcher.applier import manager as applier_manager from watcher.applier import manager as applier_manager
from watcher.applier.workflow_engine.loading import default as \
workflow_engine_loader
from watcher.common import clients from watcher.common import clients
from watcher.common import utils
from watcher.decision_engine import manager as decision_engine_manger from watcher.decision_engine import manager as decision_engine_manger
from watcher.decision_engine.planner.loading import default as planner_loader
from watcher.decision_engine.planner import manager as planner_manager from watcher.decision_engine.planner import manager as planner_manager
from watcher.decision_engine.strategy.selection import default \ from watcher.decision_engine.strategy.loading import default as strategy_loader
as strategy_selector
PLUGIN_LOADERS = (
action_loader.DefaultActionLoader,
planner_loader.DefaultPlannerLoader,
strategy_loader.DefaultStrategyLoader,
workflow_engine_loader.DefaultWorkFlowEngineLoader,
)
def list_opts(): def list_opts():
return [ watcher_opts = [
('api', watcher.api.app.API_SERVICE_OPTS), ('api', watcher.api.app.API_SERVICE_OPTS),
('watcher_goals', strategy_selector.WATCHER_GOALS_OPTS),
('watcher_decision_engine', ('watcher_decision_engine',
decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS), decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS),
('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS), ('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
@@ -44,3 +56,45 @@ def list_opts():
ka_loading.get_auth_plugin_conf_options('password') + ka_loading.get_auth_plugin_conf_options('password') +
ka_loading.get_session_conf_options())) ka_loading.get_session_conf_options()))
] ]
watcher_opts += list_plugin_opts()
return watcher_opts
def list_plugin_opts():
plugins_opts = []
for plugin_loader_cls in PLUGIN_LOADERS:
plugin_loader = plugin_loader_cls()
plugins_map = plugin_loader.list_available()
for plugin_name, plugin_cls in plugins_map.items():
plugin_opts = plugin_cls.get_config_opts()
if plugin_opts:
plugins_opts.append(
(plugin_loader.get_entry_name(plugin_name), plugin_opts))
return plugins_opts
def _show_plugins_ascii_table(rows):
headers = ["Namespace", "Plugin name", "Import path"]
table = ptable.PrettyTable(field_names=headers)
for row in rows:
table.add_row(row)
return table.get_string()
def show_plugins():
rows = []
for plugin_loader_cls in PLUGIN_LOADERS:
plugin_loader = plugin_loader_cls()
plugins_map = plugin_loader.list_available()
rows += [
(plugin_loader.get_entry_name(plugin_name),
plugin_name,
utils.get_cls_import_path(plugin_cls))
for plugin_name, plugin_cls in plugins_map.items()]
return _show_plugins_ascii_table(rows)

View File

@@ -11,6 +11,7 @@
# limitations under the License. # limitations under the License.
import datetime import datetime
import itertools
import mock import mock
from oslo_config import cfg from oslo_config import cfg
@@ -19,44 +20,80 @@ from six.moves.urllib import parse as urlparse
from wsme import types as wtypes from wsme import types as wtypes
from watcher.api.controllers.v1 import audit_template as api_audit_template from watcher.api.controllers.v1 import audit_template as api_audit_template
from watcher.common import exception
from watcher.common import utils from watcher.common import utils
from watcher.db import api as db_api
from watcher import objects from watcher import objects
from watcher.tests.api import base as api_base from watcher.tests.api import base as api_base
from watcher.tests.api import utils as api_utils from watcher.tests.api import utils as api_utils
from watcher.tests import base from watcher.tests import base
from watcher.tests.db import utils as db_utils
from watcher.tests.objects import utils as obj_utils from watcher.tests.objects import utils as obj_utils
def post_get_test_audit_template(**kw):
audit_template = api_utils.audit_template_post_data(**kw)
goal = db_utils.get_test_goal()
strategy = db_utils.get_test_strategy(goal_id=goal['id'])
del audit_template['uuid']
del audit_template['goal_id']
del audit_template['strategy_id']
audit_template['goal'] = kw.get('goal', goal['uuid'])
audit_template['strategy'] = kw.get('strategy', strategy['uuid'])
return audit_template
class TestAuditTemplateObject(base.TestCase): class TestAuditTemplateObject(base.TestCase):
def test_audit_template_init(self): def test_audit_template_init(self):
audit_template_dict = api_utils.audit_template_post_data() audit_template_dict = post_get_test_audit_template()
del audit_template_dict['name'] del audit_template_dict['name']
audit_template = api_audit_template.AuditTemplate( audit_template = api_audit_template.AuditTemplate(
**audit_template_dict) **audit_template_dict)
self.assertEqual(wtypes.Unset, audit_template.name) self.assertEqual(wtypes.Unset, audit_template.name)
class TestListAuditTemplate(api_base.FunctionalTest): class FunctionalTestWithSetup(api_base.FunctionalTest):
def setUp(self):
super(FunctionalTestWithSetup, self).setUp()
self.fake_goal1 = obj_utils.get_test_goal(
self.context, id=1, uuid=utils.generate_uuid(), name="DUMMY_1")
self.fake_goal2 = obj_utils.get_test_goal(
self.context, id=2, uuid=utils.generate_uuid(), name="DUMMY_2")
self.fake_goal1.create()
self.fake_goal2.create()
self.fake_strategy1 = obj_utils.get_test_strategy(
self.context, id=1, uuid=utils.generate_uuid(), name="STRATEGY_1",
goal_id=self.fake_goal1.id)
self.fake_strategy2 = obj_utils.get_test_strategy(
self.context, id=2, uuid=utils.generate_uuid(), name="STRATEGY_2",
goal_id=self.fake_goal2.id)
self.fake_strategy1.create()
self.fake_strategy2.create()
class TestListAuditTemplate(FunctionalTestWithSetup):
def test_empty(self): def test_empty(self):
response = self.get_json('/audit_templates') response = self.get_json('/audit_templates')
self.assertEqual([], response['audit_templates']) self.assertEqual([], response['audit_templates'])
def _assert_audit_template_fields(self, audit_template): def _assert_audit_template_fields(self, audit_template):
audit_template_fields = ['name', 'goal', 'host_aggregate'] audit_template_fields = ['name', 'goal_uuid', 'goal_name',
'strategy_uuid', 'strategy_name',
'host_aggregate']
for field in audit_template_fields: for field in audit_template_fields:
self.assertIn(field, audit_template) self.assertIn(field, audit_template)
def test_one(self): def test_one(self):
audit_template = obj_utils.create_test_audit_template(self.context) audit_template = obj_utils.create_test_audit_template(
self.context, strategy_id=self.fake_strategy1.id)
response = self.get_json('/audit_templates') response = self.get_json('/audit_templates')
self.assertEqual(audit_template.uuid, self.assertEqual(audit_template.uuid,
response['audit_templates'][0]["uuid"]) response['audit_templates'][0]["uuid"])
self._assert_audit_template_fields(response['audit_templates'][0]) self._assert_audit_template_fields(response['audit_templates'][0])
def test_one_soft_deleted(self): def test_get_one_soft_deleted_ok(self):
audit_template = obj_utils.create_test_audit_template(self.context) audit_template = obj_utils.create_test_audit_template(self.context)
audit_template.soft_delete() audit_template.soft_delete()
response = self.get_json('/audit_templates', response = self.get_json('/audit_templates',
@@ -124,53 +161,57 @@ class TestListAuditTemplate(api_base.FunctionalTest):
def test_many(self): def test_many(self):
audit_template_list = [] audit_template_list = []
for id_ in range(5): for id_ in range(1, 6):
audit_template = obj_utils.create_test_audit_template( audit_template = obj_utils.create_test_audit_template(
self.context, id=id_, self.context, id=id_,
uuid=utils.generate_uuid(), uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_)) name='My Audit Template {0}'.format(id_))
audit_template_list.append(audit_template.uuid) audit_template_list.append(audit_template)
response = self.get_json('/audit_templates') response = self.get_json('/audit_templates')
self.assertEqual(len(audit_template_list), self.assertEqual(len(audit_template_list),
len(response['audit_templates'])) len(response['audit_templates']))
uuids = [s['uuid'] for s in response['audit_templates']] uuids = [s['uuid'] for s in response['audit_templates']]
self.assertEqual(sorted(audit_template_list), sorted(uuids)) self.assertEqual(
sorted([at.uuid for at in audit_template_list]),
sorted(uuids))
def test_many_without_soft_deleted(self): def test_many_without_soft_deleted(self):
audit_template_list = [] audit_template_list = []
for id_ in [1, 2, 3]: for id_ in range(1, 6):
audit_template = obj_utils.create_test_audit_template( audit_template = obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(), self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_)) name='My Audit Template {0}'.format(id_))
audit_template_list.append(audit_template.uuid) audit_template_list.append(audit_template)
for id_ in [4, 5]:
audit_template = obj_utils.create_test_audit_template( # We soft delete the ones with ID 4 and 5
self.context, id=id_, uuid=utils.generate_uuid(), [at.soft_delete() for at in audit_template_list[3:]]
name='My Audit Template {0}'.format(id_))
audit_template.soft_delete()
response = self.get_json('/audit_templates') response = self.get_json('/audit_templates')
self.assertEqual(3, len(response['audit_templates'])) self.assertEqual(3, len(response['audit_templates']))
uuids = [s['uuid'] for s in response['audit_templates']] uuids = [s['uuid'] for s in response['audit_templates']]
self.assertEqual(sorted(audit_template_list), sorted(uuids)) self.assertEqual(
sorted([at.uuid for at in audit_template_list[:3]]),
sorted(uuids))
def test_many_with_soft_deleted(self): def test_many_with_soft_deleted(self):
audit_template_list = [] audit_template_list = []
for id_ in [1, 2, 3]: for id_ in range(1, 6):
audit_template = obj_utils.create_test_audit_template( audit_template = obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(), self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_)) name='My Audit Template {0}'.format(id_))
audit_template_list.append(audit_template.uuid) audit_template_list.append(audit_template)
for id_ in [4, 5]:
audit_template = obj_utils.create_test_audit_template( # We soft delete the ones with ID 4 and 5
self.context, id=id_, uuid=utils.generate_uuid(), [at.soft_delete() for at in audit_template_list[3:]]
name='My Audit Template {0}'.format(id_))
audit_template.soft_delete()
audit_template_list.append(audit_template.uuid)
response = self.get_json('/audit_templates', response = self.get_json('/audit_templates',
headers={'X-Show-Deleted': 'True'}) headers={'X-Show-Deleted': 'True'})
self.assertEqual(5, len(response['audit_templates'])) self.assertEqual(5, len(response['audit_templates']))
uuids = [s['uuid'] for s in response['audit_templates']] uuids = [s['uuid'] for s in response['audit_templates']]
self.assertEqual(sorted(audit_template_list), sorted(uuids)) self.assertEqual(
sorted([at.uuid for at in audit_template_list]),
sorted(uuids))
def test_links(self): def test_links(self):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()
@@ -207,88 +248,110 @@ class TestListAuditTemplate(api_base.FunctionalTest):
next_marker = response['audit_templates'][-1]['uuid'] next_marker = response['audit_templates'][-1]['uuid']
self.assertIn(next_marker, response['next']) self.assertIn(next_marker, response['next'])
def test_filter_by_goal(self): def test_filter_by_goal_uuid(self):
cfg.CONF.set_override('goals', {"DUMMY": "DUMMY", "BASIC": "BASIC"}, for id_, goal_id in enumerate(itertools.chain.from_iterable([
group='watcher_goals', enforce_type=True) itertools.repeat(self.fake_goal1.id, 3),
itertools.repeat(self.fake_goal2.id, 2)]), 1):
for id_ in range(2):
obj_utils.create_test_audit_template( obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(), self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_), name='My Audit Template {0}'.format(id_),
goal="DUMMY") goal_id=goal_id)
for id_ in range(2, 5): response = self.get_json(
'/audit_templates?goal=%s' % self.fake_goal2.uuid)
self.assertEqual(2, len(response['audit_templates']))
def test_filter_by_goal_name(self):
for id_, goal_id in enumerate(itertools.chain.from_iterable([
itertools.repeat(self.fake_goal1.id, 3),
itertools.repeat(self.fake_goal2.id, 2)]), 1):
obj_utils.create_test_audit_template( obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(), self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_), name='My Audit Template {0}'.format(id_),
goal="BASIC") goal_id=goal_id)
response = self.get_json('/audit_templates?goal=BASIC') response = self.get_json(
self.assertEqual(3, len(response['audit_templates'])) '/audit_templates?goal=%s' % self.fake_goal2.name)
self.assertEqual(2, len(response['audit_templates']))
def test_filter_by_strategy_uuid(self):
for id_, strategy_id in enumerate(itertools.chain.from_iterable([
itertools.repeat(self.fake_strategy1.id, 3),
itertools.repeat(self.fake_strategy2.id, 2)]), 1):
obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_),
strategy_id=strategy_id)
response = self.get_json(
'/audit_templates?strategy=%s' % self.fake_strategy2.uuid)
self.assertEqual(2, len(response['audit_templates']))
def test_filter_by_strategy_name(self):
for id_, strategy_id in enumerate(itertools.chain.from_iterable([
itertools.repeat(self.fake_strategy1.id, 3),
itertools.repeat(self.fake_strategy2.id, 2)]), 1):
obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_),
strategy_id=strategy_id)
response = self.get_json(
'/audit_templates?strategy=%s' % self.fake_strategy2.name)
self.assertEqual(2, len(response['audit_templates']))
class TestPatch(api_base.FunctionalTest): class TestPatch(FunctionalTestWithSetup):
def setUp(self): def setUp(self):
super(TestPatch, self).setUp() super(TestPatch, self).setUp()
self.audit_template = obj_utils.create_test_audit_template( self.audit_template = obj_utils.create_test_audit_template(
self.context) self.context, strategy_id=None)
p = mock.patch.object(db_api.BaseConnection, 'update_audit_template')
self.mock_audit_template_update = p.start()
self.mock_audit_template_update.side_effect = \
self._simulate_rpc_audit_template_update
cfg.CONF.set_override('goals', {"DUMMY": "DUMMY", "BASIC": "BASIC"},
group='watcher_goals', enforce_type=True)
self.addCleanup(p.stop)
def _simulate_rpc_audit_template_update(self, audit_template): @mock.patch.object(timeutils, 'utcnow')
audit_template.save() def test_replace_goal_uuid(self, mock_utcnow):
return audit_template
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
new_goal = "BASIC" new_goal_uuid = self.fake_goal2.uuid
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
self.assertNotEqual(new_goal, response['goal']) self.assertNotEqual(new_goal_uuid, response['goal_uuid'])
response = self.patch_json( response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'value': new_goal, [{'path': '/goal', 'value': new_goal_uuid,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
self.assertEqual(new_goal, response['goal']) self.assertEqual(new_goal_uuid, response['goal_uuid'])
return_updated_at = timeutils.parse_isotime( return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None) response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at) self.assertEqual(test_time, return_updated_at)
@mock.patch('oslo_utils.timeutils.utcnow') @mock.patch.object(timeutils, 'utcnow')
def test_replace_ok_by_name(self, mock_utcnow): def test_replace_goal_uuid_by_name(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
new_goal = 'BASIC' new_goal_uuid = self.fake_goal2.uuid
response = self.get_json(urlparse.quote( response = self.get_json(urlparse.quote(
'/audit_templates/%s' % self.audit_template.name)) '/audit_templates/%s' % self.audit_template.name))
self.assertNotEqual(new_goal, response['goal']) self.assertNotEqual(new_goal_uuid, response['goal_uuid'])
response = self.patch_json( response = self.patch_json(
'/audit_templates/%s' % self.audit_template.name, '/audit_templates/%s' % self.audit_template.name,
[{'path': '/goal', 'value': new_goal, [{'path': '/goal', 'value': new_goal_uuid,
'op': 'replace'}]) 'op': 'replace'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.name) '/audit_templates/%s' % self.audit_template.name)
self.assertEqual(new_goal, response['goal']) self.assertEqual(new_goal_uuid, response['goal_uuid'])
return_updated_at = timeutils.parse_isotime( return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None) response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at) self.assertEqual(test_time, return_updated_at)
@@ -296,8 +359,8 @@ class TestPatch(api_base.FunctionalTest):
def test_replace_non_existent_audit_template(self): def test_replace_non_existent_audit_template(self):
response = self.patch_json( response = self.patch_json(
'/audit_templates/%s' % utils.generate_uuid(), '/audit_templates/%s' % utils.generate_uuid(),
[{'path': '/goal', 'value': 'DUMMY', [{'path': '/goal', 'value': self.fake_goal1.uuid,
'op': 'replace'}], 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
@@ -311,23 +374,61 @@ class TestPatch(api_base.FunctionalTest):
) as cn_mock: ) as cn_mock:
response = self.patch_json( response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'value': 'INVALID_GOAL', [{'path': '/goal', 'value': utils.generate_uuid(),
'op': 'replace'}], 'op': 'replace'}],
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(400, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_add_ok(self): def test_add_goal_uuid(self):
new_goal = 'DUMMY'
response = self.patch_json( response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'value': new_goal, 'op': 'add'}]) [{'path': '/goal',
'value': self.fake_goal2.uuid,
'op': 'add'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
self.assertEqual(new_goal, response['goal']) self.assertEqual(self.fake_goal2.uuid, response['goal_uuid'])
def test_add_strategy_uuid(self):
response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/strategy',
'value': self.fake_strategy1.uuid,
'op': 'add'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
self.assertEqual(self.fake_strategy1.uuid, response['strategy_uuid'])
def test_replace_strategy_uuid(self):
response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/strategy',
'value': self.fake_strategy2['uuid'],
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
self.assertEqual(
self.fake_strategy2['uuid'], response['strategy_uuid'])
def test_replace_invalid_strategy(self):
response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/strategy',
'value': utils.generate_uuid(), # Does not exist
'op': 'replace'}], expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_add_non_existent_property(self): def test_add_non_existent_property(self):
response = self.patch_json( response = self.patch_json(
@@ -338,20 +439,34 @@ class TestPatch(api_base.FunctionalTest):
self.assertEqual(400, response.status_int) self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_remove_ok(self): def test_remove_strategy(self):
audit_template = obj_utils.create_test_audit_template(
self.context, uuid=utils.generate_uuid(),
name="AT_%s" % utils.generate_uuid(),
goal_id=self.fake_goal1.id,
strategy_id=self.fake_strategy1.id)
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % audit_template.uuid)
self.assertIsNotNone(response['goal']) self.assertIsNotNone(response['strategy_uuid'])
response = self.patch_json( response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid, '/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'op': 'remove'}]) [{'path': '/strategy', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
def test_remove_goal(self):
response = self.get_json( response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid) '/audit_templates/%s' % self.audit_template.uuid)
self.assertIsNone(response['goal']) self.assertIsNotNone(response['goal_uuid'])
response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(403, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_remove_uuid(self): def test_remove_uuid(self):
response = self.patch_json( response = self.patch_json(
@@ -372,23 +487,13 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
class TestPost(api_base.FunctionalTest): class TestPost(FunctionalTestWithSetup):
def setUp(self): @mock.patch.object(timeutils, 'utcnow')
super(TestPost, self).setUp()
p = mock.patch.object(db_api.BaseConnection, 'create_audit_template')
self.mock_create_audit_template = p.start()
self.mock_create_audit_template.side_effect = (
self._simulate_rpc_audit_template_create)
self.addCleanup(p.stop)
def _simulate_rpc_audit_template_create(self, audit_template):
audit_template.create()
return audit_template
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_audit_template(self, mock_utcnow): def test_create_audit_template(self, mock_utcnow):
audit_template_dict = api_utils.audit_template_post_data() audit_template_dict = post_get_test_audit_template(
goal=self.fake_goal1.uuid,
strategy=self.fake_strategy1.uuid)
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
@@ -398,39 +503,40 @@ class TestPost(api_base.FunctionalTest):
# Check location header # Check location header
self.assertIsNotNone(response.location) self.assertIsNotNone(response.location)
expected_location = \ expected_location = \
'/v1/audit_templates/%s' % audit_template_dict['uuid'] '/v1/audit_templates/%s' % response.json['uuid']
self.assertEqual(urlparse.urlparse(response.location).path, self.assertEqual(urlparse.urlparse(response.location).path,
expected_location) expected_location)
self.assertEqual(audit_template_dict['uuid'], response.json['uuid']) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
self.assertNotIn('updated_at', response.json.keys) self.assertNotIn('updated_at', response.json.keys)
self.assertNotIn('deleted_at', response.json.keys) self.assertNotIn('deleted_at', response.json.keys)
self.assertEqual(self.fake_goal1.uuid, response.json['goal_uuid'])
self.assertEqual(self.fake_strategy1.uuid,
response.json['strategy_uuid'])
return_created_at = timeutils.parse_isotime( return_created_at = timeutils.parse_isotime(
response.json['created_at']).replace(tzinfo=None) response.json['created_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_created_at) self.assertEqual(test_time, return_created_at)
def test_create_audit_template_doesnt_contain_id(self): def test_create_audit_template_does_autogenerate_id(self):
audit_template_dict = post_get_test_audit_template(
goal=self.fake_goal1.uuid, strategy=None)
with mock.patch.object( with mock.patch.object(
self.dbapi, self.dbapi,
'create_audit_template', 'create_audit_template',
wraps=self.dbapi.create_audit_template wraps=self.dbapi.create_audit_template
) as cn_mock: ) as cn_mock:
audit_template_dict = api_utils.audit_template_post_data(
goal='DUMMY')
response = self.post_json('/audit_templates', audit_template_dict) response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual(audit_template_dict['goal'], self.assertEqual(audit_template_dict['goal'],
response.json['goal']) response.json['goal_uuid'])
cn_mock.assert_called_once_with(mock.ANY) # Check that 'id' is not in first arg of positional args
# Check that 'id' is not in first arg of positional args self.assertNotIn('id', cn_mock.call_args[0][0])
self.assertNotIn('id', cn_mock.call_args[0][0])
def test_create_audit_template_generate_uuid(self): def test_create_audit_template_generate_uuid(self):
audit_template_dict = api_utils.audit_template_post_data() audit_template_dict = post_get_test_audit_template(
del audit_template_dict['uuid'] goal=self.fake_goal1.uuid, strategy=None)
response = self.post_json('/audit_templates', audit_template_dict) response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(201, response.status_int)
self.assertEqual(audit_template_dict['goal'], response.json['goal'])
self.assertTrue(utils.is_uuid_like(response.json['uuid'])) self.assertTrue(utils.is_uuid_like(response.json['uuid']))
def test_create_audit_template_with_invalid_goal(self): def test_create_audit_template_with_invalid_goal(self):
@@ -439,13 +545,54 @@ class TestPost(api_base.FunctionalTest):
'create_audit_template', 'create_audit_template',
wraps=self.dbapi.create_audit_template wraps=self.dbapi.create_audit_template
) as cn_mock: ) as cn_mock:
audit_template_dict = api_utils.audit_template_post_data( audit_template_dict = post_get_test_audit_template(
goal='INVALID_GOAL') goal_uuid=utils.generate_uuid())
response = self.post_json('/audit_templates', response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True) audit_template_dict, expect_errors=True)
self.assertEqual(400, response.status_int) self.assertEqual(400, response.status_int)
assert not cn_mock.called assert not cn_mock.called
def test_create_audit_template_with_invalid_strategy(self):
with mock.patch.object(
self.dbapi,
'create_audit_template',
wraps=self.dbapi.create_audit_template
) as cn_mock:
audit_template_dict = post_get_test_audit_template(
goal_uuid=self.fake_goal1['uuid'],
strategy_uuid=utils.generate_uuid())
response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_create_audit_template_with_unrelated_strategy(self):
with mock.patch.object(
self.dbapi,
'create_audit_template',
wraps=self.dbapi.create_audit_template
) as cn_mock:
audit_template_dict = post_get_test_audit_template(
goal_uuid=self.fake_goal1['uuid'],
strategy=self.fake_strategy2['uuid'])
response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_create_audit_template_with_uuid(self):
with mock.patch.object(
self.dbapi,
'create_audit_template',
wraps=self.dbapi.create_audit_template
) as cn_mock:
audit_template_dict = post_get_test_audit_template()
response = self.post_json('/audit_templates', audit_template_dict,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
class TestDelete(api_base.FunctionalTest): class TestDelete(api_base.FunctionalTest):
@@ -453,54 +600,52 @@ class TestDelete(api_base.FunctionalTest):
super(TestDelete, self).setUp() super(TestDelete, self).setUp()
self.audit_template = obj_utils.create_test_audit_template( self.audit_template = obj_utils.create_test_audit_template(
self.context) self.context)
p = mock.patch.object(db_api.BaseConnection, 'update_audit_template')
self.mock_audit_template_update = p.start()
self.mock_audit_template_update.side_effect = \
self._simulate_rpc_audit_template_update
self.addCleanup(p.stop)
def _simulate_rpc_audit_template_update(self, audit_template): @mock.patch.object(timeutils, 'utcnow')
audit_template.save()
return audit_template
@mock.patch('oslo_utils.timeutils.utcnow')
def test_delete_audit_template_by_uuid(self, mock_utcnow): def test_delete_audit_template_by_uuid(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
self.delete('/audit_templates/%s' % self.audit_template.uuid)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid,
expect_errors=True)
# self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
self.context.show_deleted = True
audit_template = objects.AuditTemplate.get_by_uuid(
self.context, self.audit_template.uuid)
return_deleted_at = timeutils.strtime(audit_template['deleted_at'])
self.assertEqual(timeutils.strtime(test_time), return_deleted_at)
@mock.patch('oslo_utils.timeutils.utcnow')
def test_delete_audit_template_by_name(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
self.delete(urlparse.quote('/audit_templates/%s' % self.delete(urlparse.quote('/audit_templates/%s' %
self.audit_template.name)) self.audit_template.uuid))
response = self.get_json(urlparse.quote( response = self.get_json(
'/audit_templates/%s' % self.audit_template.name), urlparse.quote('/audit_templates/%s' % self.audit_template.uuid),
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
self.context.show_deleted = True self.assertRaises(exception.AuditTemplateNotFound,
audit_template = objects.AuditTemplate.get_by_name( objects.AuditTemplate.get_by_uuid,
self.context, self.audit_template.name) self.context,
self.audit_template.uuid)
return_deleted_at = timeutils.strtime(audit_template['deleted_at']) self.context.show_deleted = True
self.assertEqual(timeutils.strtime(test_time), return_deleted_at) at = objects.AuditTemplate.get_by_uuid(self.context,
self.audit_template.uuid)
self.assertEqual(self.audit_template.name, at.name)
@mock.patch.object(timeutils, 'utcnow')
def test_delete_audit_template_by_name(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
self.delete(urlparse.quote('/audit_templates/%s' %
self.audit_template.name))
response = self.get_json(
urlparse.quote('/audit_templates/%s' % self.audit_template.name),
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
self.assertRaises(exception.AuditTemplateNotFound,
objects.AuditTemplate.get_by_name,
self.context,
self.audit_template.name)
self.context.show_deleted = True
at = objects.AuditTemplate.get_by_name(self.context,
self.audit_template.name)
self.assertEqual(self.audit_template.uuid, at.uuid)
def test_delete_audit_template_not_found(self): def test_delete_audit_template_not_found(self):
uuid = utils.generate_uuid() uuid = utils.generate_uuid()

View File

@@ -432,17 +432,18 @@ class TestPost(api_base.FunctionalTest):
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
audit_dict = post_get_test_audit() audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
del audit_dict['uuid']
del audit_dict['state']
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int) self.assertEqual(201, response.status_int)
# Check location header # Check location header
self.assertIsNotNone(response.location) self.assertIsNotNone(response.location)
expected_location = '/v1/audits/%s' % audit_dict['uuid'] expected_location = '/v1/audits/%s' % response.json['uuid']
self.assertEqual(urlparse.urlparse(response.location).path, self.assertEqual(urlparse.urlparse(response.location).path,
expected_location) expected_location)
self.assertEqual(audit_dict['uuid'], response.json['uuid'])
self.assertEqual(objects.audit.State.PENDING, self.assertEqual(objects.audit.State.PENDING,
response.json['state']) response.json['state'])
self.assertNotIn('updated_at', response.json.keys) self.assertNotIn('updated_at', response.json.keys)
@@ -451,12 +452,29 @@ class TestPost(api_base.FunctionalTest):
response.json['created_at']).replace(tzinfo=None) response.json['created_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_created_at) self.assertEqual(test_time, return_created_at)
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_audit_with_state_not_allowed(self, mock_utcnow,
mock_trigger_audit):
mock_trigger_audit.return_value = mock.ANY
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
audit_dict = post_get_test_audit(state=objects.audit.State.SUCCEEDED)
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@mock.patch('oslo_utils.timeutils.utcnow') @mock.patch('oslo_utils.timeutils.utcnow')
def test_create_audit_invalid_audit_template_uuid(self, mock_utcnow): def test_create_audit_invalid_audit_template_uuid(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
audit_dict = post_get_test_audit() audit_dict = post_get_test_audit()
del audit_dict['uuid']
del audit_dict['state']
# Make the audit template UUID some garbage value # Make the audit template UUID some garbage value
audit_dict['audit_template_uuid'] = ( audit_dict['audit_template_uuid'] = (
'01234567-8910-1112-1314-151617181920') '01234567-8910-1112-1314-151617181920')
@@ -473,11 +491,14 @@ class TestPost(api_base.FunctionalTest):
def test_create_audit_doesnt_contain_id(self, mock_trigger_audit): def test_create_audit_doesnt_contain_id(self, mock_trigger_audit):
mock_trigger_audit.return_value = mock.ANY mock_trigger_audit.return_value = mock.ANY
audit_dict = post_get_test_audit(state='ONGOING') audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
state = audit_dict['state']
del audit_dict['uuid']
del audit_dict['state']
with mock.patch.object(self.dbapi, 'create_audit', with mock.patch.object(self.dbapi, 'create_audit',
wraps=self.dbapi.create_audit) as cn_mock: wraps=self.dbapi.create_audit) as cn_mock:
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual(audit_dict['state'], response.json['state']) self.assertEqual(state, response.json['state'])
cn_mock.assert_called_once_with(mock.ANY) cn_mock.assert_called_once_with(mock.ANY)
# Check that 'id' is not in first arg of positional args # Check that 'id' is not in first arg of positional args
self.assertNotIn('id', cn_mock.call_args[0][0]) self.assertNotIn('id', cn_mock.call_args[0][0])
@@ -488,6 +509,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict = post_get_test_audit() audit_dict = post_get_test_audit()
del audit_dict['uuid'] del audit_dict['uuid']
del audit_dict['state']
response = self.post_json('/audits', audit_dict) response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
@@ -499,9 +521,21 @@ class TestPost(api_base.FunctionalTest):
def test_create_audit_trigger_decision_engine(self): def test_create_audit_trigger_decision_engine(self):
with mock.patch.object(deapi.DecisionEngineAPI, with mock.patch.object(deapi.DecisionEngineAPI,
'trigger_audit') as de_mock: 'trigger_audit') as de_mock:
audit_dict = post_get_test_audit(state='ONGOING') audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
self.post_json('/audits', audit_dict) del audit_dict['uuid']
de_mock.assert_called_once_with(mock.ANY, audit_dict['uuid']) del audit_dict['state']
response = self.post_json('/audits', audit_dict)
de_mock.assert_called_once_with(mock.ANY, response.json['uuid'])
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
def test_create_audit_with_uuid(self, mock_trigger_audit):
mock_trigger_audit.return_value = mock.ANY
audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
assert not mock_trigger_audit.called
# class TestDelete(api_base.FunctionalTest): # class TestDelete(api_base.FunctionalTest):

View File

@@ -11,60 +11,109 @@
# limitations under the License. # limitations under the License.
from oslo_config import cfg from oslo_config import cfg
from watcher.tests.api import base as api_base from six.moves.urllib import parse as urlparse
CONF = cfg.CONF from watcher.common import utils
from watcher.tests.api import base as api_base
from watcher.tests.objects import utils as obj_utils
class TestListGoal(api_base.FunctionalTest): class TestListGoal(api_base.FunctionalTest):
def setUp(self):
super(TestListGoal, self).setUp()
# Override the default to get enough goals to test limit on query
cfg.CONF.set_override(
"goals", {
"DUMMY_1": "dummy", "DUMMY_2": "dummy",
"DUMMY_3": "dummy", "DUMMY_4": "dummy",
},
group='watcher_goals', enforce_type=True)
def _assert_goal_fields(self, goal): def _assert_goal_fields(self, goal):
goal_fields = ['name', 'strategy'] goal_fields = ['uuid', 'name', 'display_name']
for field in goal_fields: for field in goal_fields:
self.assertIn(field, goal) self.assertIn(field, goal)
def test_one(self): def test_one(self):
goal = obj_utils.create_test_goal(self.context)
response = self.get_json('/goals') response = self.get_json('/goals')
self.assertEqual(goal.uuid, response['goals'][0]["uuid"])
self._assert_goal_fields(response['goals'][0]) self._assert_goal_fields(response['goals'][0])
def test_get_one(self): def test_get_one_by_uuid(self):
goal_name = list(CONF.watcher_goals.goals.keys())[0] goal = obj_utils.create_test_goal(self.context)
response = self.get_json('/goals/%s' % goal_name) response = self.get_json('/goals/%s' % goal.uuid)
self.assertEqual(goal_name, response['name']) self.assertEqual(goal.uuid, response["uuid"])
self.assertEqual(goal.name, response["name"])
self._assert_goal_fields(response) self._assert_goal_fields(response)
def test_get_one_by_name(self):
goal = obj_utils.create_test_goal(self.context)
response = self.get_json(urlparse.quote(
'/goals/%s' % goal['name']))
self.assertEqual(goal.uuid, response['uuid'])
self._assert_goal_fields(response)
def test_get_one_soft_deleted(self):
goal = obj_utils.create_test_goal(self.context)
goal.soft_delete()
response = self.get_json(
'/goals/%s' % goal['uuid'],
headers={'X-Show-Deleted': 'True'})
self.assertEqual(goal.uuid, response['uuid'])
self._assert_goal_fields(response)
response = self.get_json(
'/goals/%s' % goal['uuid'],
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_detail(self): def test_detail(self):
goal_name = list(CONF.watcher_goals.goals.keys())[0] goal = obj_utils.create_test_goal(self.context)
response = self.get_json('/goals/detail') response = self.get_json('/goals/detail')
self.assertEqual(goal_name, response['goals'][0]["name"]) self.assertEqual(goal.uuid, response['goals'][0]["uuid"])
self._assert_goal_fields(response['goals'][0]) self._assert_goal_fields(response['goals'][0])
def test_detail_against_single(self): def test_detail_against_single(self):
goal_name = list(CONF.watcher_goals.goals.keys())[0] goal = obj_utils.create_test_goal(self.context)
response = self.get_json('/goals/%s/detail' % goal_name, response = self.get_json('/goals/%s/detail' % goal.uuid,
expect_errors=True) expect_errors=True)
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
def test_many(self): def test_many(self):
goal_list = []
for idx in range(1, 6):
goal = obj_utils.create_test_goal(
self.context, id=idx,
uuid=utils.generate_uuid(),
name='GOAL_{0}'.format(idx))
goal_list.append(goal.uuid)
response = self.get_json('/goals') response = self.get_json('/goals')
self.assertEqual(len(CONF.watcher_goals.goals), self.assertTrue(len(response['goals']) > 2)
len(response['goals']))
def test_many_without_soft_deleted(self):
goal_list = []
for id_ in [1, 2, 3]:
goal = obj_utils.create_test_goal(
self.context, id=id_, uuid=utils.generate_uuid(),
name='GOAL_{0}'.format(id_))
goal_list.append(goal.uuid)
for id_ in [4, 5]:
goal = obj_utils.create_test_goal(
self.context, id=id_, uuid=utils.generate_uuid(),
name='GOAL_{0}'.format(id_))
goal.soft_delete()
response = self.get_json('/goals')
self.assertEqual(3, len(response['goals']))
uuids = [s['uuid'] for s in response['goals']]
self.assertEqual(sorted(goal_list), sorted(uuids))
def test_goals_collection_links(self): def test_goals_collection_links(self):
for idx in range(1, 6):
obj_utils.create_test_goal(
self.context, id=idx,
uuid=utils.generate_uuid(),
name='GOAL_{0}'.format(idx))
response = self.get_json('/goals/?limit=2') response = self.get_json('/goals/?limit=2')
self.assertEqual(2, len(response['goals'])) self.assertEqual(2, len(response['goals']))
def test_goals_collection_links_default_limit(self): def test_goals_collection_links_default_limit(self):
for idx in range(1, 6):
obj_utils.create_test_goal(
self.context, id=idx,
uuid=utils.generate_uuid(),
name='GOAL_{0}'.format(idx))
cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True) cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
response = self.get_json('/goals') response = self.get_json('/goals')
self.assertEqual(3, len(response['goals'])) self.assertEqual(3, len(response['goals']))

View File

@@ -0,0 +1,189 @@
# 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 six.moves.urllib import parse as urlparse
from watcher.common import utils
from watcher.tests.api import base as api_base
from watcher.tests.objects import utils as obj_utils
class TestListStrategy(api_base.FunctionalTest):
def _assert_strategy_fields(self, strategy):
strategy_fields = ['uuid', 'name', 'display_name', 'goal_uuid']
for field in strategy_fields:
self.assertIn(field, strategy)
def test_one(self):
strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json('/strategies')
self.assertEqual(strategy.uuid, response['strategies'][0]["uuid"])
self._assert_strategy_fields(response['strategies'][0])
def test_get_one_by_uuid(self):
strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json('/strategies/%s' % strategy.uuid)
self.assertEqual(strategy.uuid, response["uuid"])
self.assertEqual(strategy.name, response["name"])
self._assert_strategy_fields(response)
def test_get_one_by_name(self):
strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json(urlparse.quote(
'/strategies/%s' % strategy['name']))
self.assertEqual(strategy.uuid, response['uuid'])
self._assert_strategy_fields(response)
def test_get_one_soft_deleted(self):
strategy = obj_utils.create_test_strategy(self.context)
strategy.soft_delete()
response = self.get_json(
'/strategies/%s' % strategy['uuid'],
headers={'X-Show-Deleted': 'True'})
self.assertEqual(strategy.uuid, response['uuid'])
self._assert_strategy_fields(response)
response = self.get_json(
'/strategies/%s' % strategy['uuid'],
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_detail(self):
obj_utils.create_test_goal(self.context)
strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json('/strategies/detail')
self.assertEqual(strategy.uuid, response['strategies'][0]["uuid"])
self._assert_strategy_fields(response['strategies'][0])
for strategy in response['strategies']:
self.assertTrue(
all(val is not None for key, val in strategy.items()
if key in ['uuid', 'name', 'display_name', 'goal_uuid']))
def test_detail_against_single(self):
strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json('/strategies/%s/detail' % strategy.uuid,
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_many(self):
obj_utils.create_test_goal(self.context)
strategy_list = []
for idx in range(1, 6):
strategy = obj_utils.create_test_strategy(
self.context, id=idx,
uuid=utils.generate_uuid(),
name='STRATEGY_{0}'.format(idx))
strategy_list.append(strategy.uuid)
response = self.get_json('/strategies')
self.assertEqual(5, len(response['strategies']))
for strategy in response['strategies']:
self.assertTrue(
all(val is not None for key, val in strategy.items()
if key in ['uuid', 'name', 'display_name', 'goal_uuid']))
def test_many_without_soft_deleted(self):
strategy_list = []
for id_ in [1, 2, 3]:
strategy = obj_utils.create_test_strategy(
self.context, id=id_, uuid=utils.generate_uuid(),
name='STRATEGY_{0}'.format(id_))
strategy_list.append(strategy.uuid)
for id_ in [4, 5]:
strategy = obj_utils.create_test_strategy(
self.context, id=id_, uuid=utils.generate_uuid(),
name='STRATEGY_{0}'.format(id_))
strategy.soft_delete()
response = self.get_json('/strategies')
self.assertEqual(3, len(response['strategies']))
uuids = [s['uuid'] for s in response['strategies']]
self.assertEqual(sorted(strategy_list), sorted(uuids))
def test_strategies_collection_links(self):
for idx in range(1, 6):
obj_utils.create_test_strategy(
self.context, id=idx,
uuid=utils.generate_uuid(),
name='STRATEGY_{0}'.format(idx))
response = self.get_json('/strategies/?limit=2')
self.assertEqual(2, len(response['strategies']))
def test_strategies_collection_links_default_limit(self):
for idx in range(1, 6):
obj_utils.create_test_strategy(
self.context, id=idx,
uuid=utils.generate_uuid(),
name='STRATEGY_{0}'.format(idx))
cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
response = self.get_json('/strategies')
self.assertEqual(3, len(response['strategies']))
def test_filter_by_goal_uuid(self):
goal1 = obj_utils.create_test_goal(
self.context,
id=1,
uuid=utils.generate_uuid(),
name='My_Goal 1')
goal2 = obj_utils.create_test_goal(
self.context,
id=2,
uuid=utils.generate_uuid(),
name='My Goal 2')
for id_ in range(1, 3):
obj_utils.create_test_strategy(
self.context, id=id_,
uuid=utils.generate_uuid(),
goal_id=goal1['id'])
for id_ in range(3, 5):
obj_utils.create_test_strategy(
self.context, id=id_,
uuid=utils.generate_uuid(),
goal_id=goal2['id'])
response = self.get_json('/strategies/?goal=%s' % goal1['uuid'])
strategies = response['strategies']
self.assertEqual(2, len(strategies))
for strategy in strategies:
self.assertEqual(goal1['uuid'], strategy['goal_uuid'])
def test_filter_by_goal_name(self):
goal1 = obj_utils.create_test_goal(
self.context,
id=1,
uuid=utils.generate_uuid(),
name='My_Goal 1')
goal2 = obj_utils.create_test_goal(
self.context,
id=2,
uuid=utils.generate_uuid(),
name='My Goal 2')
for id_ in range(1, 3):
obj_utils.create_test_strategy(
self.context, id=id_,
uuid=utils.generate_uuid(),
goal_id=goal1['id'])
for id_ in range(3, 5):
obj_utils.create_test_strategy(
self.context, id=id_,
uuid=utils.generate_uuid(),
goal_id=goal2['id'])
response = self.get_json('/strategies/?goal=%s' % goal1['name'])
strategies = response['strategies']
self.assertEqual(2, len(strategies))
for strategy in strategies:
self.assertEqual(goal1['uuid'], strategy['goal_uuid'])

View File

@@ -54,7 +54,8 @@ class TestChangeNovaServiceState(base.TestCase):
baction.BaseAction.RESOURCE_ID: "compute-1", baction.BaseAction.RESOURCE_ID: "compute-1",
"state": hstate.HypervisorState.ENABLED.value, "state": hstate.HypervisorState.ENABLED.value,
} }
self.action = change_nova_service_state.ChangeNovaServiceState() self.action = change_nova_service_state.ChangeNovaServiceState(
mock.Mock())
self.action.input_parameters = self.input_parameters self.action.input_parameters = self.input_parameters
def test_parameters_down(self): def test_parameters_down(self):

View File

@@ -58,9 +58,18 @@ class TestMigration(base.TestCase):
"dst_hypervisor": "hypervisor2-hostname", "dst_hypervisor": "hypervisor2-hostname",
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID, baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
} }
self.action = migration.Migrate() self.action = migration.Migrate(mock.Mock())
self.action.input_parameters = self.input_parameters self.action.input_parameters = self.input_parameters
self.input_parameters_cold = {
"migration_type": "cold",
"src_hypervisor": "hypervisor1-hostname",
"dst_hypervisor": "hypervisor2-hostname",
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
}
self.action_cold = migration.Migrate(mock.Mock())
self.action_cold.input_parameters = self.input_parameters_cold
def test_parameters(self): def test_parameters(self):
params = {baction.BaseAction.RESOURCE_ID: params = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID, self.INSTANCE_UUID,
@@ -70,6 +79,15 @@ class TestMigration(base.TestCase):
self.action.input_parameters = params self.action.input_parameters = params
self.assertEqual(True, self.action.validate_parameters()) self.assertEqual(True, self.action.validate_parameters())
def test_parameters_cold(self):
params = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
self.action.MIGRATION_TYPE: 'cold',
self.action.DST_HYPERVISOR: 'compute-2',
self.action.SRC_HYPERVISOR: 'compute-3'}
self.action_cold.input_parameters = params
self.assertEqual(True, self.action_cold.validate_parameters())
def test_parameters_exception_empty_fields(self): def test_parameters_exception_empty_fields(self):
parameters = {baction.BaseAction.RESOURCE_ID: None, parameters = {baction.BaseAction.RESOURCE_ID: None,
'migration_type': None, 'migration_type': None,
@@ -87,7 +105,7 @@ class TestMigration(base.TestCase):
def test_parameters_exception_migration_type(self): def test_parameters_exception_migration_type(self):
parameters = {baction.BaseAction.RESOURCE_ID: parameters = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID, self.INSTANCE_UUID,
'migration_type': 'cold', 'migration_type': 'unknown',
'src_hypervisor': 'compute-2', 'src_hypervisor': 'compute-2',
'dst_hypervisor': 'compute-3'} 'dst_hypervisor': 'compute-3'}
self.action.input_parameters = parameters self.action.input_parameters = parameters
@@ -154,6 +172,13 @@ class TestMigration(base.TestCase):
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID) self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"]) self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
def test_execute_cold_migration_invalid_instance(self):
self.m_helper.find_instance.return_value = None
exc = self.assertRaises(
exception.InstanceNotFound, self.action_cold.execute)
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
def test_execute_live_migration(self): def test_execute_live_migration(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID self.m_helper.find_instance.return_value = self.INSTANCE_UUID
@@ -166,6 +191,20 @@ class TestMigration(base.TestCase):
instance_id=self.INSTANCE_UUID, instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname") dest_hostname="hypervisor2-hostname")
def test_execute_cold_migration(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
try:
self.action_cold.execute()
except Exception as exc:
self.fail(exc)
self.m_helper.watcher_non_live_migrate_instance.\
assert_called_once_with(
instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname"
)
def test_revert_live_migration(self): def test_revert_live_migration(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID self.m_helper.find_instance.return_value = self.INSTANCE_UUID
@@ -177,6 +216,18 @@ class TestMigration(base.TestCase):
dest_hostname="hypervisor1-hostname" dest_hostname="hypervisor1-hostname"
) )
def test_revert_cold_migration(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
self.action_cold.revert()
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
self.m_helper.watcher_non_live_migrate_instance.\
assert_called_once_with(
instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor1-hostname"
)
def test_live_migrate_non_shared_storage_instance(self): def test_live_migrate_non_shared_storage_instance(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID self.m_helper.find_instance.return_value = self.INSTANCE_UUID

View File

@@ -14,6 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
import mock
import voluptuous import voluptuous
from watcher.applier.actions import sleep from watcher.applier.actions import sleep
@@ -23,7 +25,7 @@ from watcher.tests import base
class TestSleep(base.TestCase): class TestSleep(base.TestCase):
def setUp(self): def setUp(self):
super(TestSleep, self).setUp() super(TestSleep, self).setUp()
self.s = sleep.Sleep() self.s = sleep.Sleep(mock.Mock())
def test_parameters_duration(self): def test_parameters_duration(self):
self.s.input_parameters = {self.s.DURATION: 1.0} self.s.input_parameters = {self.s.DURATION: 1.0}

View File

@@ -18,22 +18,22 @@
# #
from mock import patch from mock import patch
from threading import Thread
from watcher.applier.manager import ApplierManager from watcher.applier import manager as applier_manager
from watcher.common.messaging.messaging_core import MessagingCore from watcher.common.messaging import messaging_handler
from watcher.common import service
from watcher.tests import base from watcher.tests import base
class TestApplierManager(base.TestCase): class TestApplierManager(base.TestCase):
def setUp(self): def setUp(self):
super(TestApplierManager, self).setUp() super(TestApplierManager, self).setUp()
self.applier = ApplierManager() self.applier = service.Service(applier_manager.ApplierManager)
@patch.object(MessagingCore, "connect") @patch.object(messaging_handler.MessagingHandler, "stop")
@patch.object(Thread, "join") @patch.object(messaging_handler.MessagingHandler, "start")
def test_connect(self, m_messaging, m_thread): def test_start(self, m_messaging_start, m_messaging_stop):
self.applier.connect() self.applier.start()
self.applier.join() self.applier.stop()
self.assertEqual(2, m_messaging.call_count) self.assertEqual(2, m_messaging_start.call_count)
self.assertEqual(1, m_thread.call_count) self.assertEqual(2, m_messaging_stop.call_count)

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