Compare commits

..

78 Commits

Author SHA1 Message Date
junjie huang
8ebc898924 outlet Temperature based migration strategy
It implements one of the algorithm of Intel thermal POC.
It extends the BaseStrategy class, getting the Outlet
metrics of servers via Ceilometer, and generates solutions
when the outlet Temperature is greater than threshold.
current threshold is hard-coded, will make it configurable
in the next patches.

Implements: blueprint outlet-temperature-based-strategy

Change-Id: I248147329d34eddf408652205a077895be572010
Co-Authored-By: Zhenzan Zhou <zhenzan.zhou@intel.com>
2015-12-28 21:29:08 +00:00
Jenkins
660c782626 Merge "Move Audit-template management in DefaultStrategyContext" 2015-12-23 08:29:36 +00:00
Jean-Emile DARTOIS
dfaba80252 Move Audit-template management in DefaultStrategyContext
This aim of this patchset is to move the management of the
Audit-Template into StrategyContext
in order to prepare to pass parameters to strategies
but also to prepare to add more dynamic Actions management

Partially implements: blueprint glossary-related-refactoring

Change-Id: I13ee063da947113ce349855aa331a22f40567051
2015-12-22 17:52:51 +01:00
Darren Shaw
9dccb29bf4 Move glossary.rst to root folder of doc
Move glossary.rst from /doc/dev to /doc/ as glossary is not only
related to dev. Updated index.rst.

Change-Id: I2e5251bdb0b94ef03727dea26bc43866544d2fd3
Closes-Bug: #1527130
2015-12-21 13:14:06 -06:00
Jenkins
853145f4d1 Merge "Remove string concatenation in favor of string formatting" 2015-12-21 10:13:56 +00:00
Jenkins
764f5c7681 Merge "Add Creative Commons Attribution header to documentation" 2015-12-21 10:10:36 +00:00
Jenkins
b81b767567 Merge "Rename NovaWrapper to NovaClient" 2015-12-21 10:06:53 +00:00
Jenkins
764e31a7a1 Merge "Remove useless event factory" 2015-12-21 10:06:21 +00:00
Steve Wilkerson
1c49d07912 Remove string concatenation in favor of string formatting
Some of the modules still utilized string concatenation
instead of using formatting.  In order to align with other
modules in the project, I refactored these modules to use string
formatting instead.

Change-Id: I708392e1d03b6331a134419aa0ae9dc02a05c31b
Closes-Bug: 1522738
2015-12-21 11:04:00 +01:00
Jean-Emile DARTOIS
18549dc182 Remove useless event factory
Change-Id: I8442a26ebfcfe3c17c378b283ffbbc0810b7a067
2015-12-21 10:31:15 +01:00
Jenkins
9f222221a7 Merge "Change default strategy to DummyStrategy" 2015-12-21 09:29:29 +00:00
Jean-Emile DARTOIS
1a64383a68 Rename NovaWrapper to NovaClient
This patchset aim to rename the nova client class and move it with
the other openstack clients in the folder openstack/common.

Change-Id: Ie8aab199922985f42ad85e6688f0727b24f53ffd
2015-12-21 10:26:20 +01:00
Vincent Françoise
ac07f35dc7 i18n - Make string translatable
Since internationalization should be enabled in Watcher, this
patchset refactors the Watcher codebase to wrap previously
untranslatable strings with i18n translation functions so we can
import them for translation into the .pot template file.

Partially Implements: blueprint support-translation
Change-Id: I425967a60b5a7957f753894e5d2ba0d2c5009d1d
2015-12-21 10:08:59 +01:00
Jean-Emile DARTOIS
010bc61cc9 Change default strategy to DummyStrategy
The aim of this patchset is to change the default strategy
and set invoke_on_load to False.

Change-Id: I0e374993614f465b11a22e33008f7026642154ee
2015-12-21 09:55:53 +01:00
Jenkins
3dd02ee895 Merge "Code refactoring - StrategyContext and Auditendpoint" 2015-12-21 08:40:04 +00:00
Steve Wilkerson
a4bbe7f893 Add Creative Commons Attribution header to documentation
Changed the header in the documentation to reflect that the docs
are covered under the CCBY 3.0 license.

Change-Id: I29f3f1a2491c28b1a4ee12a7b97a7ab00c919867
Closes-Bug: 1526331
2015-12-20 01:51:00 -06:00
Jean-Emile DARTOIS
4c3073efb4 Code refactoring - StrategyContext and Auditendpoint
This patchset aim to remove useless code in StrategyContext
and AuditEndPoint.
This patchset also add a parameter for strategy context to define the
numbers of thread of execute the strategies.

DocImpact
Change-Id: I83e87165b03b42fe6b863921502a300bd94d2982
2015-12-18 14:25:07 +00:00
Jenkins
642226812f Merge "Remove *.pyc files before running tox tests" 2015-12-18 12:52:31 +00:00
Jenkins
8f2ca2518f Merge "'admin_user' opt (and others) imported twice" 2015-12-18 12:48:56 +00:00
Jenkins
1698eb31f3 Merge "Fix generation of watcher config file" 2015-12-17 18:34:26 +00:00
Jenkins
5fb74e677f Merge "Rename command to audit" 2015-12-17 13:21:39 +00:00
Gábor Antal
35a5ba1cd0 Remove *.pyc files before running tox tests
Modified tox.ini to remove any .pyc file before running tests.
Also, now it removes all the __pycache__ folders which was also
in the bug report.

Change-Id: I2f60751b155f8098b746339c12406b934bcdbcf9
Closes-Bug: #1525852
2015-12-17 11:59:54 +01:00
Jean-Emile DARTOIS
c0a85be7b8 Add missing parameter in prepare_service for api
Although the bug #1526888 (prepare_service() - duplicate function) got merged
https://review.openstack.org/#/c/258007/ we didn't see that the file in
(watcher/cmd/api.py) api didn't have sys.argv parameter.

Change-Id: I458b4ff169684131a20ef6ac090b1c918f08d427
Closes-Bug: 1526888
2015-12-16 17:58:18 +01:00
Jean-Emile DARTOIS
633b360652 Fix generation of watcher config file
Although the refactoring of the applier got merged we have an issue
to generate the config. This pathset fix that

Change-Id: Iafce7d0136b2ad813102c3e3caeebafa215363c8
Closes-Bug: 1526851
2015-12-16 16:09:12 +00:00
Jenkins
d3160bf007 Merge "Removed duplicated function prepare_service()" 2015-12-16 10:41:32 +00:00
Jean-Emile DARTOIS
69152f2449 Rename command to audit
This patchset is there to change the code structure.

Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to rename the folder command to audit

Partially implements: blueprint glossary-related-refactoring

Change-Id: I76616fb58d5e79a7dc209b80e882d216850d18a4
2015-12-16 10:23:44 +01:00
Jenkins
37cd75ffe3 Merge "Include terminology definition from docstring" 2015-12-16 09:08:25 +00:00
Gábor Antal
c7fd5e8b21 'admin_user' opt (and others) imported twice
"admin_user", "admin_tenant_name", "admin_password" and "auth_uri"
are options which are imported from the "keystone_authtoken" group,
itself being part of the "keystonemiddleware" 3rd-party library.

The problem is that these options are imported at 2 different
locations in the codebase:

- watcher/applier/manager.py
- watcher/common/keystone.py

Removed one from the applier.

Change-Id: Ie7075883018ec69f6a4e8190f50a9ea949ab9745
Closes-Bug: #1526259
2015-12-15 20:10:02 +01:00
Gábor Antal
f0b58f8c27 Removed duplicated function prepare_service()
The prepare_service() function is defined both in watcher/service.py
and in watcher/common/service.py.

These 2 needed to be merged into a single one
to avoid code duplication.

At the same time, the watcher/service.py only contains this function,
so I removed that file.

Change-Id: I0c935dfcd011bee9597315752dae8668221c53f9
Closes-Bug: #1525842
2015-12-15 19:14:52 +01:00
Jenkins
22dd6d42c3 Merge "Internationalization (i18n) - Enable French locale" 2015-12-15 17:49:50 +00:00
Vincent Françoise
bd29e2e79f Internationalization (i18n) - Enable French locale
Our project should now enable its internationalization.
This patchset add the french locale to the project but also
refactors the codebase to following the oslo_i18n recommendations.

DocImpact
Implements: blueprint support-translation

Change-Id: I0e4fbf05d16afb5e25bac78438c640f147c754b1
2015-12-15 17:57:10 +01:00
Vincent Françoise
c2eb112184 Include terminology definition from docstring
As of now, the glossary defined in our documentation reflects the
current state of the codebase. In order to avoid any discrepancy
between the codebase and each definition, the objective here is to
gather both in a single place and link it into the rst documentation
via a custom directive.
Also re-aligned the requirements with liberty for doc.

DocImpact

Change-Id: I9ca50f8d3c32b4690ee240e13dec0cb7eeedcc2c
2015-12-15 11:38:42 +01:00
Jean-Emile DARTOIS
57cecb27f5 Remove pragma no cover from code
Add exclude_lines in the report section of .coveragerc to ignore
abstract in test coverage

Change-Id: I7863a8ba7e20358fb7cdf3cc7e4d83871a5104ef
2015-12-15 10:14:40 +01:00
Jenkins
7c72d6f912 Merge "Some tests are ignored" 2015-12-15 07:57:58 +00:00
Jean-Emile DARTOIS
62570525ad Tidy up - Watcher Decision Engine package
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change the code structure by adding the folder
"strategies" and "loading".

Partially implements: blueprint glossary-related-refactoring

Change-Id: I56fb24ee6762b3186eccde5983233e17bb227cc1
2015-12-14 14:33:56 +01:00
Jenkins
92940ba9e2 Merge "Typo in ClusteStateNotDefined" 2015-12-14 10:56:51 +00:00
Jenkins
7c8ce453ed Merge "Add Apache license header to all rst documentation" 2015-12-14 10:43:37 +00:00
Gábor Antal
33ea5f96f8 Typo in ClusteStateNotDefined
ClusteStateNotDefined has a typo, it should be ClusterStateNotDefine

Change-Id: I727301786d47db847215d73722051e59d340f1c2
Closes-Bug: #1525818
2015-12-14 11:41:29 +01:00
Jenkins
916f4d0c08 Merge "Tidy up - Rename Base" 2015-12-14 10:27:47 +00:00
Jenkins
3fb5defc16 Merge "Removed H404, H405, H305 ignore in pep8" 2015-12-14 10:12:37 +00:00
Jean-Emile DARTOIS
b01f4bead4 Some tests are ignored
Testr is using the prefix _test for discover tests.

Change-Id: I963c25f1d331273dd0d28a374be894246413530b
2015-12-14 10:31:06 +01:00
Jenkins
8516d629c2 Merge "Added unit tests on nova wrapper" 2015-12-14 09:21:54 +00:00
Jean-Emile DARTOIS
35a1f0a657 Tidy up - Rename Base
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset add missing Base in class name

Partially implements: blueprint glossary-related-refactoring

Change-Id: I95a3e41fbd5fcd90a99d81c9cf278940f50c7732
2015-12-11 14:45:50 +01:00
Vincent Françoise
d934971458 Refactored Watcher codebase to add py34 support
Even though Watcher was mentioning python 3.4 as supported, it
really wasn't the case as all the unit tests were not passing in this
version of Python.

This patchset fixes all the failing tests in Python 3.4 while
keeping Watcher Python 2.7 compatible.

DocImpact
BugImpact
Change-Id: Ie74acc08ef0a2899349a4b419728c89e416a18cb
2015-12-11 13:24:02 +00:00
Vincent Françoise
d92f85574d Added unit tests on nova wrapper
As the coverage on the nova wrapper is actually very low, this
patchset is here to raise it a bit since it is a quite central
piece of code.

Change-Id: Ie7879c74c8d322d5031953827c339bb11d9085c1
2015-12-11 10:47:03 +01:00
Jenkins
b1fe7a5f3d Merge "Remove references to removed watcher/openstack directory" 2015-12-11 08:42:46 +00:00
Jenkins
8a5eb4b6a1 Merge "Remove unreachable code in basic_consolidation.py" 2015-12-11 08:29:57 +00:00
Jenkins
91c14e4eda Merge "Removed unnecessary code from basic_consolidation" 2015-12-11 08:07:28 +00:00
Gábor Antal
1613bd6904 Removed H404, H405, H305 ignore in pep8
In the file tox.ini we have some pep8 rules disabled.
We should remove H404,H405,H305 from the ignore list.

Removed them from the ignore list, and got some errors.
I restructured the comments, and now with H404, H405, H305 enabled,
pep8 works without any failures.

Change-Id: Ic2aeb2a8bd47e92fbd2bb0f43fd00d44b6c220ca
Closes-Bug: #1523841
2015-12-10 19:35:52 +01:00
Gábor Antal
ba4f5569d1 Removed unnecessary code from basic_consolidation
In basic_consolidation.py has a method, called calculate_score_vm().
This method used to get vm's id as a parameter, but it was replaced
and now it gets the vm as a parameter. So we don't need to get the
vm from our vm's id, as we already have the vm.

Change-Id: I96af7fbdbe85eda8d4fc44b4b162e8ba9d4967fa
2015-12-10 18:55:16 +01:00
Gábor Antal
a62553a6a5 Remove unreachable code in basic_consolidation.py
In basic_consolidation.py there is a method called calculate_score_vm()
which had two return statements following each other. The second one
(which equals the first one) is unreachable as the first one returns.

Change-Id: Ia4877c22188fae6217e07597a2dd939633414349
Closes-Bug: #1524911
2015-12-10 18:20:35 +01:00
Jean-Emile DARTOIS
d5ba40530f Rename Mapper to Mapping
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change mapper to mapping

Partially implements: blueprint glossary-related-refactoring

Change-Id: Ieaca42431322ce40d87de147ac0b46a1f446f390
2015-12-10 17:58:58 +01:00
Jean-Emile DARTOIS
f98e96da42 Tidy up - Primitive
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change
Primitive to Primitives.
Add BasePrimitive.

Partially implements: blueprint glossary-related-refactoring

Change-Id: I839bddd12b5320b338b2f207d74963afa23de522
2015-12-10 17:37:13 +01:00
Jenkins
64747cad1f Merge "Rename Command to Action" 2015-12-10 16:33:04 +00:00
Jenkins
5f87e82bac Merge "Removed py33, pypy support" 2015-12-10 16:31:43 +00:00
Jean-Emile DARTOIS
ff89e942ca Remove references to removed watcher/openstack directory
Since we removed openstack/common this is not useful.

Change-Id: Ifb8e1cdacd1879874be7496a8bc4bc8349085456
2015-12-10 16:13:43 +00:00
Gábor Antal
7faa501fb7 Removed py33, pypy support
As Openstack Liberty now targets Python 2.7 and 3.4,
we should drop Python 3.3 support from our setup.cfg

Change-Id: I8c8f9222fcf1111e99a943976aae9c5b427f7ee4
Closes-Bug: #1523983
2015-12-10 15:53:09 +01:00
Jenkins
ab8d242c1f Merge "Remove alembic revision of watcher db" 2015-12-10 08:35:04 +00:00
Jenkins
bbd26cafae Merge "Update the glossary to lay down Watcher terminology" 2015-12-10 08:35:01 +00:00
Jenkins
0042356245 Merge "Rename command to action_plan" 2015-12-10 08:31:21 +00:00
Jean-Emile DARTOIS
e1d4026c7c Remove alembic revision of watcher db
This code is not useful because we have only one version 
of watcher db currently and this alembic revision is out
of date compared to the initial schema as of mitaka-1.

Change-Id: Id21c665ff7a600a716e80d112e131a0e13687b41
2015-12-09 16:20:51 +00:00
Jenkins
df692a8215 Merge "Rename efficiency to efficacy" 2015-12-09 15:43:43 +00:00
Darren Shaw
f9323889d6 Add Apache license header to all rst documentation
This patch has the Apache license header found in
doc/source/dev/glossary.rst added to every .rst file

Change-Id: Icc20e7baf7d3cd0f116c371d54ef03c7c8401778
Closes-Bug: #1523986
2015-12-09 08:34:53 -06:00
Jenkins
0a44b2972e Merge "Rename Meta-Action to Action" 2015-12-09 10:59:14 +00:00
Jean-Emile DARTOIS
c5c16ac055 Rename Command to Action
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change Command to Action

Partially implements: blueprint glossary-related-refactoring

Change-Id: Id38e133fc8b789319c7db5712d3820ecca1bd51d
2015-12-09 09:35:30 +00:00
Jenkins
3016d3da11 Merge "Add a checker for the documentation" 2015-12-09 08:14:45 +00:00
Jean-Emile DARTOIS
daa560111c Update the glossary to lay down Watcher terminology
The Primitive used by the applier is not defined in the glossary.
This patchset start to fix this gap

Change-Id: Ie3fe153fc4c396da5c83425ccdd540910ad33e49
2015-12-09 09:10:14 +01:00
Jenkins
16705f68da Merge "Removed unused enum" 2015-12-08 23:14:03 +00:00
Jean-Emile DARTOIS
109a980c29 Rename command to action_plan
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change command to action_plan

Partially implements: blueprint glossary-related-refactoring

Change-Id: I19a70adeca347ce747a2221b5fc31658139c95a2
2015-12-08 15:26:44 +01:00
Vincent Françoise
531373cb84 Removed unused enum
This StrategyState Enum is not used anywhere in the codebase (and
shouldn't be), so I removed it.

Change-Id: I0b5d3102b4d08856dccd751313fdd097937d8ccf
2015-12-08 14:22:22 +01:00
Jean-Emile DARTOIS
087c4d49ed Rename Meta-Action to Action
Some Python class and packages need to be renamed
for a better compliance with the shared terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change Meta-Action to Action

Partially implements: blueprint glossary-related-refactoring

Change-Id: Ie67b800332179be93718030ddd7a36ddc76a544c
2015-12-08 11:53:29 +01:00
Jean-Emile DARTOIS
022b15dc1e Add a checker for the documentation
This patchset add some unit tests on the documentation:
Checking wrapping
Checking trailing spaces
Checking no_cr

Change-Id: I3fa56d3e7dd3218dcd398e6750bdd2fb3a8e75b4
2015-12-08 11:28:45 +01:00
Jean-Emile DARTOIS
454f70a19f Rename efficiency to efficacy
Some Python class and packages need to be renamed
for a better compliance with the shared Terminology
which provides a better understanding of Watcher
objects and components by every contributor.

This patchset is there to change efficiency to efficacy

Partially implements: blueprint glossary-related-refactoring

Change-Id: I4c84192d49a147e0fd406da35e2805143b902331
2015-12-08 09:18:48 +00:00
Steve Wilkerson
98f05a52a8 Fix Watcher Applier variables in CamelCase
PEP8 standards call for instance variable names to be written
in the same manner as methods, with underscores separating words.
This fix addresses an instance variable  in the DeployPhase class.

As the instance variable seems public, the Java-ish getter and
setter mentioned in the bug report have been removed as well.

Change-Id: I8835315c8cae64665d3ccb321c4066e37a22c467
Closes-Bug: 1522489
2015-12-07 10:40:57 -06:00
Jenkins
5ff9f28a83 Merge "Remove duplicate setup in Watcher API main()" 2015-12-07 10:35:55 +00:00
Darren Shaw
4a88220ffe Remove duplicate setup in Watcher API main()
main() duplicates code for oslo_log setup, which is first
called within prepare_services(). This patches removes
the duplication call in main()

Change-Id: I712d3733218c18b2eb02d4bf26e54b29ef72a988
Closes-Bug: #1522781
2015-12-04 20:39:42 -06:00
Jean-Emile DARTOIS
4c2d0e6345 Cleanup deprecated documentation
Since vincent mahe created a great
documentation available in doc/source
this one is deprecated

Change-Id: I81ff633771570f28a38e3d277718f0a97a9d5090
2015-12-04 18:21:47 +01:00
vmahe
7710b1670e Provide detailed information on architecture
Added new global architecture diagram using Dia tool
and a more detailed description of each component.
Exported this diagram in SVG format.
The source code of the diagram is stored in
/doc/images_src/ folder.
Moved also the architecture.rst file from the /dev
folder to the root folder of the Watcher doc.

Change-Id: I74379390178673dcb6a043967b31e38ca9560bb3
2015-12-04 17:33:05 +01:00
181 changed files with 3657 additions and 2079 deletions

View File

@@ -1,7 +1,9 @@
[run]
branch = True
source = watcher
omit = watcher/tests/*,watcher/openstack/*
omit = watcher/tests/*
[report]
ignore_errors = True
exclude_lines =
@abstract

View File

@@ -1,3 +1,9 @@
..
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 Style Commandments
==========================

View File

@@ -1,3 +1,9 @@
..
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
=======

182
doc/source/architecture.rst Normal file
View File

@@ -0,0 +1,182 @@
..
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/
.. _architecture:
===================
System Architecture
===================
This page presents the current technical Architecture of the Watcher system.
.. _architecture_overview:
Overview
========
Below you will find a diagram, showing the main components of Watcher:
.. image:: ./images/architecture.svg
:width: 100%
.. _components_definition:
Components
==========
.. _amqp_bus_definition:
AMQP Bus
--------
The AMQP message bus handles asynchronous communications between the different
Watcher components.
.. _cluster_history_db_definition:
Cluster History Database
------------------------
This component stores the data related to the
:ref:`Cluster History <cluster_history_definition>`.
It can potentially rely on any appropriate storage system (InfluxDB, OpenTSDB,
MongoDB,...) but will probably be more performant when using
`Time Series Databases <https://en.wikipedia.org/wiki/Time_series_database>`_
which are optimized for handling time series data, which are arrays of numbers
indexed by time (a datetime or a datetime range).
.. _watcher_api_definition:
Watcher API
-----------
This component implements the REST API provided by the Watcher system to the
external world.
It enables the :ref:`Administrator <administrator_definition>` of a
:ref:`Cluster <cluster_definition>` to control and monitor the Watcher system
via any interaction mechanism connected to this API:
- :ref:`CLI <watcher_cli_definition>`
- Horizon plugin
- Python SDK
You can also read the detailed description of `Watcher API`_.
.. _watcher_applier_definition:
Watcher Applier
---------------
This component is in charge of executing the
:ref:`Action Plan <action_plan_definition>` built by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
It connects to the :ref:`message bus <amqp_bus_definition>` and launches the
:ref:`Action Plan <action_plan_definition>` whenever a triggering message is
received on a dedicated AMQP queue.
The triggering message contains the Action Plan UUID.
It then gets the detailed information about the
:ref:`Action Plan <action_plan_definition>` from the
:ref:`Watcher Database <watcher_database_definition>` which contains the list
of :ref:`Actions <action_definition>` to launch.
It then loops on each :ref:`Action <action_definition>`, gets the associated
class and calls the execute() method of this class.
Most of the time, this method will first request a token to the Keystone API
and if it is allowed, sends a request to the REST API of the OpenStack service
which handles this kind of :ref:`atomic Action <action_definition>`.
Note that as soon as :ref:`Watcher Applier <watcher_applier_definition>` starts
handling a given :ref:`Action <action_definition>` from the list, a
notification message is sent on the :ref:`message bus <amqp_bus_definition>`
indicating that the state of the action has changed to **ONGOING**.
If the :ref:`Action <action_definition>` is successful,
the :ref:`Watcher Applier <watcher_applier_definition>` sends a notification
message on :ref:`the bus <amqp_bus_definition>` informing the other components
of this.
If the :ref:`Action <action_definition>` fails, the
:ref:`Watcher Applier <watcher_applier_definition>` tries to rollback to the
previous state of the :ref:`Managed resource <managed_resource_definition>`
(i.e. before the command was sent to the underlying OpenStack service).
.. _watcher_cli_definition:
Watcher CLI
-----------
The watcher command-line interface (CLI) can be used to interact with the
Watcher system in order to control it or to know its current status.
Please, read `the detailed documentation about Watcher CLI <https://factory.b-com.com/www/watcher/doc/python-watcherclient/>`_
.. _watcher_database_definition:
Watcher Database
----------------
This database stores all the Watcher domain objects which can be requested
by the :ref:`Watcher API <watcher_api_definition>` or the
:ref:`Watcher CLI <watcher_cli_definition>`:
- :ref:`Audit templates <audit_template_definition>`
- :ref:`Audits <audit_definition>`
- :ref:`Action plans <action_plan_definition>`
- :ref:`Actions <action_definition>`
- :ref:`Goals <goal_definition>`
The Watcher domain being here "*optimization of some resources provided by an
OpenStack system*".
.. _watcher_decision_engine_definition:
Watcher Decision Engine
-----------------------
This component is responsible for computing a set of potential optimization
:ref:`Actions <action_definition>` in order to fulfill
the :ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
It first reads the parameters of the :ref:`Audit <audit_definition>` from the
associated :ref:`Audit Template <audit_template_definition>` and knows the
:ref:`Goal <goal_definition>` to achieve.
It then selects the most appropriate :ref:`Strategy <strategy_definition>`
depending on how Watcher was configured for this :ref:`Goal <goal_definition>`.
The :ref:`Strategy <strategy_definition>` is then dynamically loaded (via
`stevedore <https://github.com/openstack/stevedore/>`_). The
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` calls the
**execute()** method of the :ref:`Strategy <strategy_definition>` class which
generates a set of :ref:`Actions <action_definition>`.
These :ref:`Actions <action_definition>` are scheduled in time by the
:ref:`Watcher Planner <watcher_planner_definition>` (i.e., it generates an
:ref:`Action Plan <action_plan_definition>`).
In order to compute the potential :ref:`Solution <solution_definition>` for the
Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
- the current state of the
:ref:`Managed resources <managed_resource_definition>`
(e.g., the data stored in the Nova database)
- the data stored in the
:ref:`Cluster History Database <cluster_history_db_definition>`
which provides information about the past of the
: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.
.. _Watcher API: webapi/v1.html

View File

@@ -1,3 +1,9 @@
..
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-db-manage:
=============
@@ -54,7 +60,8 @@ Usage
=====
Options for the various :ref:`commands <db-manage_cmds>` for
:command:`watcher-db-manage` are listed when the :option:`-h` or :option:`--help`
:command:`watcher-db-manage` are listed when the :option:`-h` or
:option:`--help`
option is used after the command.
For example::
@@ -81,8 +88,9 @@ If no configuration file is specified with the :option:`--config-file` option,
Command Options
===============
:command:`watcher-db-manage` is given a command that tells the utility what actions
to perform. These commands can take arguments. Several commands are available:
:command:`watcher-db-manage` is given a command that tells the utility
what actions to perform.
These commands can take arguments. Several commands are available:
.. _create_schema:

View File

@@ -28,7 +28,8 @@ extensions = [
'sphinxcontrib.httpdomain',
'sphinxcontrib.pecanwsme.rest',
'wsmeext.sphinxext',
'oslosphinx'
'oslosphinx',
'watcher.doc',
]
wsme_protocols = ['restjson']

View File

@@ -1,4 +1,8 @@
..
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/
===================
Configuring Watcher

View File

@@ -1,3 +1,9 @@
..
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/
==================
Installing Watcher
==================

View File

@@ -1,4 +1,10 @@
.. _user-guide:
..
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/
.. _user-guide:
=================================
Welcome to the Watcher User Guide

View File

@@ -1,9 +0,0 @@
.. _architecture:
===================
System Architecture
===================
Please go to `Wiki Watcher Architecture <https://wiki.openstack.org/wiki/WatcherArchitecture>`_
.. _API service: ../webapi/v1.html

View File

@@ -1,3 +1,9 @@
..
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/
.. _contributing:
======================
@@ -44,7 +50,7 @@ Bug tracker
Mailing list (prefix subjects with ``[watcher]`` for faster responses)
http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
Wiki
http://wiki.openstack.org/Watcher

View File

@@ -1,4 +1,8 @@
..
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/
============================================
Setting up a Watcher development environment

View File

@@ -1,3 +1,9 @@
..
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 plugins
===============
@@ -45,7 +51,7 @@ Here is an example showing how you can write a plugin called ``DummyStrategy``:
def execute(self, model):
self.solution.add_change_request(
Migrate(vm=my_vm, source_hypervisor=src, dest_hypervisor=dest)
Migrate(vm=my_vm, src_hypervisor=src, dest_hypervisor=dest)
)
# Do some more stuff here ...
return self.solution

View File

@@ -1,15 +1,8 @@
..
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view 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.
https://creativecommons.org/licenses/by/3.0/
==========
Glossary
@@ -41,7 +34,8 @@ of the OpenStack :ref:`Cluster <cluster_definition>` such as:
- Changing the current state of an hypervisor (enable or disable) with Nova
In most cases, an :ref:`Action <action_definition>` triggers some concrete
commands on an existing OpenStack module (Nova, Neutron, Cinder, Ironic, etc.).
commands on an existing OpenStack module (Nova, Neutron, Cinder, Ironic, etc.)
via a :ref:`Primitive <primitive_definition>`.
An :ref:`Action <action_definition>` has a life-cycle and its current state may
be one of the following:
@@ -71,23 +65,27 @@ An :ref:`Action Plan <action_plan_definition>` is a flow of
a given :ref:`Goal <goal_definition>`.
An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
:ref:`Audit <audit_definition>` is successful which implies that the :ref:`Strategy <strategy_definition>`
:ref:`Audit <audit_definition>` is successful which implies that the
:ref:`Strategy <strategy_definition>`
which was used has found a :ref:`Solution <solution_definition>` to achieve the
:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`.
In the default implementation of Watcher, an :ref:`Action Plan <action_plan_definition>`
In the default implementation of Watcher, an
:ref:`Action Plan <action_plan_definition>`
is only composed of successive :ref:`Actions <action_definition>`
(i.e., a Workflow of :ref:`Actions <action_definition>` belonging to a unique
branch).
However, Watcher provides abstract interfaces for many of its components,
allowing other implementations to generate and handle more complex :ref:`Action Plan(s) <action_plan_definition>`
allowing other implementations to generate and handle more complex
:ref:`Action Plan(s) <action_plan_definition>`
composed of two types of Action Item(s):
- simple :ref:`Actions <action_definition>`: atomic tasks, which means it
can not be split into smaller tasks or commands from an OpenStack point of
view.
- composite Actions: which are composed of several simple :ref:`Actions <action_definition>`
- composite Actions: which are composed of several simple
:ref:`Actions <action_definition>`
ordered in sequential and/or parallel flows.
An :ref:`Action Plan <action_plan_definition>` may be described using
@@ -134,7 +132,8 @@ is a role for users which allows them to run any Watcher commands, such as:
- Launch an :ref:`Audit <audit_definition>`
- Get the :ref:`Action Plan <action_plan_definition>`
- Launch a recommended :ref:`Action Plan <action_plan_definition>` manually
- Archive previous :ref:`Audits <audit_definition>` and :ref:`Action Plans <action_plan_definition>`
- Archive previous :ref:`Audits <audit_definition>` and
:ref:`Action Plans <action_plan_definition>`
The :ref:`Administrator <administrator_definition>` is also allowed to modify
@@ -163,7 +162,8 @@ be one of the following:
event handling mechanism) and is in the queue for being processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
processed by the :ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
successfully (note that it may not necessarily produce a
:ref:`Solution <solution_definition>`).
@@ -191,7 +191,8 @@ An :ref:`Audit Template <audit_template_definition>` contains at least the
It may also contain some error handling settings indicating whether:
- :ref:`Watcher Applier <watcher_applier_definition>` stops the entire operation
- :ref:`Watcher Applier <watcher_applier_definition>` stops the
entire operation
- :ref:`Watcher Applier <watcher_applier_definition>` performs a rollback
and how many retries should be attempted before failure occurs (also the latter
@@ -241,10 +242,12 @@ Cluster Data Model
==================
A :ref:`Cluster Data Model <cluster_data_model_definition>` is a logical
representation of the current state and topology of the :ref:`Cluster <cluster_definition>`
representation of the current state and topology of the
:ref:`Cluster <cluster_definition>`
:ref:`Managed resources <managed_resource_definition>`.
It is represented as a set of :ref:`Managed resources <managed_resource_definition>`
It is represented as a set of
:ref:`Managed resources <managed_resource_definition>`
(which may be a simple tree or a flat list of key-value pairs)
which enables Watcher :ref:`Strategies <strategy_definition>` to know the
current relationships between the different
@@ -253,7 +256,8 @@ current relationships between the different
and enables the :ref:`Strategy <strategy_definition>` to request information
such as:
- What compute nodes are in a given :ref:`Availability Zone <availability_zone_definition>`
- What compute nodes are in a given
:ref:`Availability Zone <availability_zone_definition>`
or a given :ref:`Host Aggregate <host_aggregates_definition>` ?
- What :ref:`Instances <instance_definition>` are hosted on a given compute
node ?
@@ -263,16 +267,19 @@ such as:
- What is the available bandwidth on a given network link ?
- What is the current space available on a given virtual disk of a given
:ref:`Instance <instance_definition>` ?
- What is the current state of a given :ref:`Instance <instance_definition>` ?
- What is the current state of a given :ref:`Instance <instance_definition>`?
- ...
In a word, this data model enables the :ref:`Strategy <strategy_definition>`
to know:
- the current topology of the :ref:`Cluster <cluster_definition>`
- the current capacity for each :ref:`Managed resource <managed_resource_definition>`
- the current amount of used/free space for each :ref:`Managed resource <managed_resource_definition>`
- the current state of each :ref:`Managed resources <managed_resource_definition>`
- the current capacity for each
:ref:`Managed resource <managed_resource_definition>`
- the current amount of used/free space for each
:ref:`Managed resource <managed_resource_definition>`
- the current state of each
:ref:`Managed resources <managed_resource_definition>`
In the Watcher project, we aim at providing a generic and very basic
:ref:`Cluster Data Model <cluster_data_model_definition>` for each
@@ -295,7 +302,8 @@ to:
:ref:`Cluster Data Model <cluster_data_model_definition>`
(the proposed data model acts as a pivot data model)
There may be various :ref:`generic and basic Cluster Data Models <cluster_data_model_definition>`
There may be various
:ref:`generic and basic Cluster Data Models <cluster_data_model_definition>`
proposed in Watcher helpers, each of them being adapted to achieving a given
:ref:`Goal <goal_definition>`:
@@ -311,8 +319,10 @@ proposed in Watcher helpers, each of them being adapted to achieving a given
Note however that a developer can use his/her own
:ref:`Cluster Data Model <cluster_data_model_definition>` if the proposed data
model does not fit his/her needs as long as the :ref:`Strategy <strategy_definition>`
is able to produce a :ref:`Solution <solution_definition>` for the requested :ref:`Goal <goal_definition>`.
model does not fit his/her needs as long as the
:ref:`Strategy <strategy_definition>` is able to produce a
:ref:`Solution <solution_definition>` for the requested
:ref:`Goal <goal_definition>`.
For example, a developer could rely on the Nova Data Model to optimize some
compute resources.
@@ -335,16 +345,21 @@ history may be used by any :ref:`Strategy <strategy_definition>` in order to
find the most optimal :ref:`Solution <solution_definition>` during an
:ref:`Audit <audit_definition>`.
In the Watcher project, a generic :ref:`Cluster History <cluster_history_definition>`
In the Watcher project, a generic
:ref:`Cluster History <cluster_history_definition>`
API is proposed with some helper classes in order to :
- share a common measurement (events or metrics) naming based on what is
defined in Ceilometer. See `the full list of available measurements <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
defined in Ceilometer.
See `the full list of available measurements <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
- share common meter types (Cumulative, Delta, Gauge) based on what is
defined in Ceilometer. See `the full list of meter types <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
defined in Ceilometer.
See `the full list of meter types <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
- simplify the development of a new :ref:`Strategy <strategy_definition>`
- avoid duplicating the same code in several :ref:`Strategies <strategy_definition>`
- have a better consistency between the different :ref:`Strategies <strategy_definition>`
- avoid duplicating the same code in several
:ref:`Strategies <strategy_definition>`
- have a better consistency between the different
:ref:`Strategies <strategy_definition>`
- avoid any strong coupling with any external metrics/events storage system
(the proposed API and measurement naming system acts as a pivot format)
@@ -369,8 +384,8 @@ services:
- Cinder scheduler: for volumes management
- Glance controller: for image management
- Neutron controller: for network management
- Nova controller: for global compute resources management with services such as
nova-scheduler, nova-conductor and nova-network
- Nova controller: for global compute resources management with services
such as nova-scheduler, nova-conductor and nova-network.
In many configurations, Watcher will reside on a controller node even if it
can potentially be hosted on a dedicated machine.
@@ -388,7 +403,8 @@ Customer
========
A :ref:`Customer <customer_definition>` is the person or company which
subscribes to the cloud provider offering. A customer may have several :ref:`Project(s) <project_definition>`
subscribes to the cloud provider offering. A customer may have several
:ref:`Project(s) <project_definition>`
hosted on the same :ref:`Cluster <cluster_definition>` or dispatched on
different clusters.
@@ -484,11 +500,12 @@ 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
:ref:`Customer <customer_definition>`.
The way efficiency is evaluated will depend on the :ref:`Goal <goal_definition>`
to achieve.
The way efficiency is evaluated will depend on the
:ref:`Goal <goal_definition>` to achieve.
Of course, the efficiency will be relevant only as long as the :ref:`Action Plan <action_plan_definition>`
is relevant (i.e., the current state of the :ref:`Cluster <cluster_definition>`
Of course, the efficiency will be relevant only as long as the
:ref:`Action Plan <action_plan_definition>` is relevant
(i.e., the current state of the :ref:`Cluster <cluster_definition>`
has not changed in a way that a new :ref:`Audit <audit_definition>` would need
to be launched).
@@ -521,6 +538,24 @@ specific domain.
Please, read `the official OpenStack definition of a Project <http://docs.openstack.org/glossary/content/glossary.html>`_.
.. _primitive_definition
Primitive
=========
A :ref:`Primitive <primitive_definition>` is the component that carries out a
certain type of atomic :ref:`Actions <action_definition>` on a given
:ref:`Managed resource <managed_resource_definition>` (nova, swift, neutron,
glance,..). A :ref:`Primitive <primitive_definition>` is a part of the
:ref:`Watcher Applier <watcher_applier_definition>` module.
For example, there can be a :ref:`Primitive <primitive_definition>` which is
responsible for creating a snapshot of a given instance on a Nova compute node.
This :ref:`Primitive <primitive_definition>` knows exactly how to send
the appropriate commands to Nova for this type of
:ref:`Actions <action_definition>`.
.. _sla_definition:
SLA
@@ -551,21 +586,21 @@ which provides a good definition.
SLA violation
=============
A :ref:`SLA violation <sla_violation_definition>` happens when a :ref:`SLA <sla_definition>`
defined with a given :ref:`Customer <customer_definition>` could not be
respected by the cloud provider within the timeframe defined by the official
contract document.
A :ref:`SLA violation <sla_violation_definition>` happens when a
:ref:`SLA <sla_definition>` defined with a given
:ref:`Customer <customer_definition>` could not be respected by the
cloud provider within the timeframe defined by the official contract document.
.. _slo_definition:
SLO
===
A Service Level Objective (SLO) is a key element of a :ref:`SLA <sla_definition>`
between a service provider and a :ref:`Customer <customer_definition>`. SLOs
are agreed as a means of measuring the performance of the Service Provider and
are outlined as a way of avoiding disputes between the two parties based on
misunderstanding.
A Service Level Objective (SLO) is a key element of a
:ref:`SLA <sla_definition>` between a service provider and a
:ref:`Customer <customer_definition>`. SLOs are agreed as a means of measuring
the performance of the Service Provider and are outlined as a way of avoiding
disputes between the two parties based on misunderstanding.
You can also read `the Wikipedia page for SLO <https://en.wikipedia.org/wiki/Service_level_objective>`_
which provides a good definition.
@@ -575,9 +610,10 @@ which provides a good definition.
Solution
========
A :ref:`Solution <solution_definition>` is a set of :ref:`Actions <action_definition>`
generated by a :ref:`Strategy <strategy_definition>` (i.e., an algorithm) in
order to achieve the :ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
A :ref:`Solution <solution_definition>` is a set of
:ref:`Actions <action_definition>` generated by a
:ref:`Strategy <strategy_definition>` (i.e., an algorithm) in order to achieve
the :ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
A :ref:`Solution <solution_definition>` is different from an
:ref:`Action Plan <action_plan_definition>` because it contains the
@@ -593,8 +629,8 @@ applied.
Two approaches to dealing with this can be envisaged:
- **fully automated mode**: only the :ref:`Solution <solution_definition>` with
the highest ranking (i.e., the highest
- **fully automated mode**: only the :ref:`Solution <solution_definition>`
with the highest ranking (i.e., the highest
:ref:`Optimization Efficiency <efficiency_definition>`)
will be sent to the :ref:`Watcher Planner <watcher_planner_definition>` and
translated into concrete :ref:`Actions <action_definition>`.
@@ -610,11 +646,13 @@ Strategy
========
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>`.
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 :ref:`Goal <goal_definition>`.
specific :ref:`Strategy <strategy_definition>` should be used for each
:ref:`Goal <goal_definition>`.
Some strategies may provide better optimization results but may take more time
to find an optimal :ref:`Solution <solution_definition>`.
@@ -628,8 +666,9 @@ provided as well.
Watcher Applier
===============
This component is in charge of executing the :ref:`Action Plan <action_plan_definition>`
built by the :ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
This component is in charge of executing the
:ref:`Action Plan <action_plan_definition>` built by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
See :doc:`architecture` for more details on this component.
@@ -658,8 +697,8 @@ Watcher Decision Engine
=======================
This component is responsible for computing a set of potential optimization
:ref:`Actions <action_definition>` in order to fulfill the :ref:`Goal <goal_definition>`
of an :ref:`Audit <audit_definition>`.
:ref:`Actions <action_definition>` in order to fulfill the
:ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
It first reads the parameters of the :ref:`Audit <audit_definition>` from the
associated :ref:`Audit Template <audit_template_definition>` and knows the

Binary file not shown.

View File

@@ -0,0 +1,275 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
<svg width="60cm" height="27cm" viewBox="-181 -239 1196 535" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<rect style="fill: #b3d6c6" x="-169.614" y="-237" width="1173" height="532"/>
<path style="fill: #b3d6c6" d="M -169.614,-237 A 10,10 0 0 0 -179.614,-227 L -169.614,-227 z"/>
<path style="fill: #b3d6c6" d="M 1013.39,-227 A 10,10 0 0 0 1003.39,-237 L 1003.39,-227 z"/>
<rect style="fill: #b3d6c6" x="-179.614" y="-227" width="1193" height="512"/>
<path style="fill: #b3d6c6" d="M -179.614,285 A 10,10 0 0 0 -169.614,295 L -169.614,285 z"/>
<path style="fill: #b3d6c6" d="M 1003.39,295 A 10,10 0 0 0 1013.39,285 L 1003.39,285 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" x1="-169.614" y1="-237" x2="1003.39" y2="-237"/>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" x1="-169.614" y1="295" x2="1003.39" y2="295"/>
<path style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" d="M -169.614,-237 A 10,10 0 0 0 -179.614,-227"/>
<path style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" d="M 1013.39,-227 A 10,10 0 0 0 1003.39,-237"/>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" x1="-179.614" y1="-227" x2="-179.614" y2="285"/>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" x1="1013.39" y1="-227" x2="1013.39" y2="285"/>
<path style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" d="M -179.614,285 A 10,10 0 0 0 -169.614,295"/>
<path style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ffffff" d="M 1003.39,295 A 10,10 0 0 0 1013.39,285"/>
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="416.886" y="32.9002">
<tspan x="416.886" y="32.9002"></tspan>
</text>
</g>
<g>
<rect style="fill: #00cd78" x="244.1" y="-158" width="82.287" height="260"/>
<path style="fill: #00cd78" d="M 244.1,-158 A 10,10 0 0 0 234.1,-148 L 244.1,-148 z"/>
<path style="fill: #00cd78" d="M 336.387,-148 A 10,10 0 0 0 326.387,-158 L 326.387,-148 z"/>
<rect style="fill: #00cd78" x="234.1" y="-148" width="102.287" height="240"/>
<path style="fill: #00cd78" d="M 234.1,92 A 10,10 0 0 0 244.1,102 L 244.1,92 z"/>
<path style="fill: #00cd78" d="M 326.387,102 A 10,10 0 0 0 336.387,92 L 326.387,92 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="244.1" y1="-158" x2="326.387" y2="-158"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="244.1" y1="102" x2="326.387" y2="102"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 244.1,-158 A 10,10 0 0 0 234.1,-148"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 336.387,-148 A 10,10 0 0 0 326.387,-158"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="234.1" y1="-148" x2="234.1" y2="92"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="336.387" y1="-148" x2="336.387" y2="92"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 234.1,92 A 10,10 0 0 0 244.1,102"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 326.387,102 A 10,10 0 0 0 336.387,92"/>
<text font-size="12.8" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="285.243" y="-32.1">
<tspan x="285.243" y="-32.1">AMQP</tspan>
<tspan x="285.243" y="-16.1">bus</tspan>
</text>
</g>
<g>
<rect style="fill: #ffb400" x="407" y="-0.832" width="162.3" height="89"/>
<path style="fill: #ffb400" d="M 407,-0.832 A 10,10 0 0 0 397,9.168 L 407,9.168 z"/>
<path style="fill: #ffb400" d="M 579.3,9.168 A 10,10 0 0 0 569.3,-0.832 L 569.3,9.168 z"/>
<rect style="fill: #ffb400" x="397" y="9.168" width="182.3" height="69"/>
<path style="fill: #ffb400" d="M 397,78.168 A 10,10 0 0 0 407,88.168 L 407,78.168 z"/>
<path style="fill: #ffb400" d="M 569.3,88.168 A 10,10 0 0 0 579.3,78.168 L 569.3,78.168 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="-0.832" x2="569.3" y2="-0.832"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="88.168" x2="569.3" y2="88.168"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 407,-0.832 A 10,10 0 0 0 397,9.168"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 579.3,9.168 A 10,10 0 0 0 569.3,-0.832"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="397" y1="9.168" x2="397" y2="78.168"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="579.3" y1="9.168" x2="579.3" y2="78.168"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 397,78.168 A 10,10 0 0 0 407,88.168"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 569.3,88.168 A 10,10 0 0 0 579.3,78.168"/>
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="488.15" y="39.1291">
<tspan x="488.15" y="39.1291">Watcher</tspan>
<tspan x="488.15" y="56.768">Decision Engine</tspan>
</text>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="341.176" y1="21" x2="387.264" y2="21.3451"/>
<polygon style="fill: #000000" points="394.764,21.4013 384.727,26.3262 387.264,21.3451 384.802,16.3265 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,21.4013 384.727,26.3262 387.264,21.3451 384.802,16.3265 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 6; stroke: #000000" x1="349.122" y1="65.5706" x2="387.264" y2="65.8474"/>
<polygon style="fill: #000000" points="341.622,65.5162 351.658,60.5889 349.122,65.5706 351.585,70.5886 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="341.622,65.5162 351.658,60.5889 349.122,65.5706 351.585,70.5886 "/>
<polygon style="fill: #000000" points="394.764,65.9018 384.728,70.8291 387.264,65.8474 384.801,60.8294 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,65.9018 384.728,70.8291 387.264,65.8474 384.801,60.8294 "/>
</g>
<g>
<rect style="fill: #ffb400" x="407" y="-159" width="160" height="89"/>
<path style="fill: #ffb400" d="M 407,-159 A 10,10 0 0 0 397,-149 L 407,-149 z"/>
<path style="fill: #ffb400" d="M 577,-149 A 10,10 0 0 0 567,-159 L 567,-149 z"/>
<rect style="fill: #ffb400" x="397" y="-149" width="180" height="69"/>
<path style="fill: #ffb400" d="M 397,-80 A 10,10 0 0 0 407,-70 L 407,-80 z"/>
<path style="fill: #ffb400" d="M 567,-70 A 10,10 0 0 0 577,-80 L 567,-80 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="-159" x2="567" y2="-159"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="-70" x2="567" y2="-70"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 407,-159 A 10,10 0 0 0 397,-149"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 577,-149 A 10,10 0 0 0 567,-159"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="397" y1="-149" x2="397" y2="-80"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="577" y1="-149" x2="577" y2="-80"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 397,-80 A 10,10 0 0 0 407,-70"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 567,-70 A 10,10 0 0 0 577,-80"/>
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="487" y="-119.039">
<tspan x="487" y="-119.039">Watcher</tspan>
<tspan x="487" y="-101.4">Actions Applier</tspan>
</text>
</g>
<g>
<rect style="fill: #ffb400" x="63.086" y="-136.5" width="70.7" height="157"/>
<path style="fill: #ffb400" d="M 63.086,-136.5 A 10,10 0 0 0 53.086,-126.5 L 63.086,-126.5 z"/>
<path style="fill: #ffb400" d="M 143.786,-126.5 A 10,10 0 0 0 133.786,-136.5 L 133.786,-126.5 z"/>
<rect style="fill: #ffb400" x="53.086" y="-126.5" width="90.7" height="137"/>
<path style="fill: #ffb400" d="M 53.086,10.5 A 10,10 0 0 0 63.086,20.5 L 63.086,10.5 z"/>
<path style="fill: #ffb400" d="M 133.786,20.5 A 10,10 0 0 0 143.786,10.5 L 133.786,10.5 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="63.086" y1="-136.5" x2="133.786" y2="-136.5"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="63.086" y1="20.5" x2="133.786" y2="20.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 63.086,-136.5 A 10,10 0 0 0 53.086,-126.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 143.786,-126.5 A 10,10 0 0 0 133.786,-136.5"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="53.086" y1="-126.5" x2="53.086" y2="10.5"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="143.786" y1="-126.5" x2="143.786" y2="10.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 53.086,10.5 A 10,10 0 0 0 63.086,20.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 133.786,20.5 A 10,10 0 0 0 143.786,10.5"/>
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="98.436" y="-62.5389">
<tspan x="98.436" y="-62.5389">Watcher</tspan>
<tspan x="98.436" y="-44.9">API</tspan>
</text>
</g>
<g>
<rect style="fill: #ffb400" x="-138.614" y="-124.5" width="105" height="69.1"/>
<path style="fill: #ffb400" d="M -138.614,-124.5 A 10,10 0 0 0 -148.614,-114.5 L -138.614,-114.5 z"/>
<path style="fill: #ffb400" d="M -23.614,-114.5 A 10,10 0 0 0 -33.614,-124.5 L -33.614,-114.5 z"/>
<rect style="fill: #ffb400" x="-148.614" y="-114.5" width="125" height="49.1"/>
<path style="fill: #ffb400" d="M -148.614,-65.4 A 10,10 0 0 0 -138.614,-55.4 L -138.614,-65.4 z"/>
<path style="fill: #ffb400" d="M -33.614,-55.4 A 10,10 0 0 0 -23.614,-65.4 L -33.614,-65.4 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-138.614" y1="-124.5" x2="-33.614" y2="-124.5"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-138.614" y1="-55.4" x2="-33.614" y2="-55.4"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -138.614,-124.5 A 10,10 0 0 0 -148.614,-114.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -23.614,-114.5 A 10,10 0 0 0 -33.614,-124.5"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-148.614" y1="-114.5" x2="-148.614" y2="-65.4"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-23.614" y1="-114.5" x2="-23.614" y2="-65.4"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -148.614,-65.4 A 10,10 0 0 0 -138.614,-55.4"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -33.614,-55.4 A 10,10 0 0 0 -23.614,-65.4"/>
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="-86.114" y="-94.4889">
<tspan x="-86.114" y="-94.4889">Watcher</tspan>
<tspan x="-86.114" y="-76.85">CLI</tspan>
</text>
</g>
<g>
<rect style="fill: #ffffff" x="-155.614" y="-29.5" width="117" height="56"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="-155.614" y="-29.5" width="117" height="56"/>
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="-97.114" y="2.4">
<tspan x="-97.114" y="2.4">Horizon</tspan>
</text>
</g>
<g>
<rect style="fill: #ffb400" x="-111.614" y="12.6" width="90" height="59.9"/>
<path style="fill: #ffb400" d="M -111.614,12.6 A 10,10 0 0 0 -121.614,22.6 L -111.614,22.6 z"/>
<path style="fill: #ffb400" d="M -11.614,22.6 A 10,10 0 0 0 -21.614,12.6 L -21.614,22.6 z"/>
<rect style="fill: #ffb400" x="-121.614" y="22.6" width="110" height="39.9"/>
<path style="fill: #ffb400" d="M -121.614,62.5 A 10,10 0 0 0 -111.614,72.5 L -111.614,62.5 z"/>
<path style="fill: #ffb400" d="M -21.614,72.5 A 10,10 0 0 0 -11.614,62.5 L -21.614,62.5 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-111.614" y1="12.6" x2="-21.614" y2="12.6"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-111.614" y1="72.5" x2="-21.614" y2="72.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -111.614,12.6 A 10,10 0 0 0 -121.614,22.6"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -11.614,22.6 A 10,10 0 0 0 -21.614,12.6"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-121.614" y1="22.6" x2="-121.614" y2="62.5"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="-11.614" y1="22.6" x2="-11.614" y2="62.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -121.614,62.5 A 10,10 0 0 0 -111.614,72.5"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M -21.614,72.5 A 10,10 0 0 0 -11.614,62.5"/>
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="-66.614" y="38.0111">
<tspan x="-66.614" y="38.0111">Watcher</tspan>
<tspan x="-66.614" y="55.65">plugin</tspan>
</text>
</g>
<g>
<rect style="fill: #ffffff" x="639.986" y="-66.4" width="117" height="56"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="639.986" y="-66.4" width="117" height="56"/>
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="698.486" y="-34.5">
<tspan x="698.486" y="-34.5">Nova</tspan>
</text>
</g>
<g>
<rect style="fill: #ffffff" x="822.786" y="122.5" width="167.6" height="59.501"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="822.786" y="122.5" width="167.6" height="59.501"/>
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="906.586" y="156.151">
<tspan x="906.586" y="156.151">MariaDB Database</tspan>
</text>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="149.386" y1="-56.9998" x2="216.651" y2="-56.1259"/>
<polygon style="fill: #000000" points="224.15,-56.0284 214.086,-51.1588 216.651,-56.1259 214.216,-61.1579 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="224.15,-56.0284 214.086,-51.1588 216.651,-56.1259 214.216,-61.1579 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="341.294" y1="-128.975" x2="381.652" y2="-128.189"/>
<polygon style="fill: #000000" points="389.15,-128.043 379.055,-123.238 381.652,-128.189 379.25,-133.236 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="389.15,-128.043 379.055,-123.238 381.652,-128.189 379.25,-133.236 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 6; stroke: #000000" x1="346.121" y1="-92.8795" x2="387.265" y2="-92.3705"/>
<polygon style="fill: #000000" points="338.622,-92.9723 348.683,-97.8482 346.121,-92.8795 348.559,-87.849 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="338.622,-92.9723 348.683,-97.8482 346.121,-92.8795 348.559,-87.849 "/>
<polygon style="fill: #000000" points="394.764,-92.2777 384.703,-87.4018 387.265,-92.3705 384.827,-97.401 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,-92.2777 384.703,-87.4018 387.265,-92.3705 384.827,-97.401 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="577" y1="-114.5" x2="633.053" y2="-59.2355"/>
<polygon style="fill: #000000" points="638.394,-53.9699 627.762,-57.4302 633.053,-59.2355 634.783,-64.5512 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="638.394,-53.9699 627.762,-57.4302 633.053,-59.2355 634.783,-64.5512 "/>
</g>
<g>
<rect style="fill: #ffffff" x="654.61" y="121.001" width="137.55" height="38"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="654.61" y="121.001" width="137.55" height="38"/>
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="723.385" y="143.901">
<tspan x="723.385" y="143.901">Ceilometer API</tspan>
</text>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="584.386" y1="53.5" x2="833.65" y2="54.4624"/>
<polygon style="fill: #000000" points="841.15,54.4914 831.131,59.4527 833.65,54.4624 831.169,49.4528 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="841.15,54.4914 831.131,59.4527 833.65,54.4624 831.169,49.4528 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="-23.614" y1="-89.95" x2="44.0985" y2="-61.7438"/>
<polygon style="fill: #000000" points="51.0219,-58.8598 39.8681,-58.0896 44.0985,-61.7438 43.7134,-67.3207 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="51.0219,-58.8598 39.8681,-58.0896 44.0985,-61.7438 43.7134,-67.3207 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="-11.614" y1="42.55" x2="46.0184" y2="-12.0538"/>
<polygon style="fill: #000000" points="51.4628,-17.2121 47.6424,-6.70471 46.0184,-12.0538 40.7647,-13.9639 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="51.4628,-17.2121 47.6424,-6.70471 46.0184,-12.0538 40.7647,-13.9639 "/>
</g>
<g>
<rect style="fill: #ffa200" x="506.36" y="72.1008" width="92.025" height="49.9"/>
<path style="fill: #ffa200" d="M 506.36,72.1008 A 10,10 0 0 0 496.36,82.1008 L 506.36,82.1008 z"/>
<path style="fill: #ffa200" d="M 608.385,82.1008 A 10,10 0 0 0 598.385,72.1008 L 598.385,82.1008 z"/>
<rect style="fill: #ffa200" x="496.36" y="82.1008" width="112.025" height="29.9"/>
<path style="fill: #ffa200" d="M 496.36,112.001 A 10,10 0 0 0 506.36,122.001 L 506.36,112.001 z"/>
<path style="fill: #ffa200" d="M 598.385,122.001 A 10,10 0 0 0 608.385,112.001 L 598.385,112.001 z"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="506.36" y1="72.1008" x2="598.385" y2="72.1008"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="506.36" y1="122.001" x2="598.385" y2="122.001"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 506.36,72.1008 A 10,10 0 0 0 496.36,82.1008"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 608.385,82.1008 A 10,10 0 0 0 598.385,72.1008"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="496.36" y1="82.1008" x2="496.36" y2="112.001"/>
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="608.385" y1="82.1008" x2="608.385" y2="112.001"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 496.36,112.001 A 10,10 0 0 0 506.36,122.001"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 598.385,122.001 A 10,10 0 0 0 608.385,112.001"/>
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="552.372" y="101.331">
<tspan x="552.372" y="101.331">Strategy</tspan>
</text>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="611.386" y1="115.526" x2="646.095" y2="136.896"/>
<polygon style="fill: #000000" points="652.482,140.829 641.345,139.843 646.095,136.896 646.588,131.328 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="652.482,140.829 641.345,139.843 646.095,136.896 646.588,131.328 "/>
</g>
<g>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="577,-136.75 874.386,-136.75 874.386,3.76393 "/>
<polygon style="fill: #000000" points="874.386,11.2639 869.386,1.26393 874.386,3.76393 879.386,1.26393 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="874.386,11.2639 869.386,1.26393 874.386,3.76393 879.386,1.26393 "/>
</g>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="579.3" y1="21.418" x2="632.216" y2="-18.5335"/>
<polygon style="fill: #000000" points="638.201,-23.0527 633.233,-13.0367 632.216,-18.5335 627.208,-21.0175 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="638.201,-23.0527 633.233,-13.0367 632.216,-18.5335 627.208,-21.0175 "/>
</g>
<g>
<path style="fill: #ffb400" d="M 844 38.3333 C 865.877,23.0833 876.816,18 898.693,18 C 920.57,18 931.509,23.0833 953.386,38.3333 L 953.386,119.667 C 931.509,134.917 920.57,140 898.693,140 C 876.816,140 865.877,134.917 844,119.667 L 844,38.3333z"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 844 38.3333 C 865.877,23.0833 876.816,18 898.693,18 C 920.57,18 931.509,23.0833 953.386,38.3333 L 953.386,119.667 C 931.509,134.917 920.57,140 898.693,140 C 876.816,140 865.877,134.917 844,119.667 L 844,38.3333"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 844 38.3333 C 865.877,53.5833 876.816,58.6667 898.693,58.6667 C 920.57,58.6667 931.509,53.5833 953.386,38.3333"/>
<text font-size="12.8" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="898.693" y="93.1667">
<tspan x="898.693" y="93.1667">Watcher DB</tspan>
</text>
</g>
<g>
<path style="fill: #ff5050" d="M 651.176 172.933 C 680.618,157.683 695.339,152.6 724.78,152.6 C 754.222,152.6 768.943,157.683 798.385,172.933 L 798.385,254.267 C 768.943,269.517 754.222,274.6 724.78,274.6 C 695.339,274.6 680.618,269.517 651.176,254.267 L 651.176,172.933z"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 651.176 172.933 C 680.618,157.683 695.339,152.6 724.78,152.6 C 754.222,152.6 768.943,157.683 798.385,172.933 L 798.385,254.267 C 768.943,269.517 754.222,274.6 724.78,274.6 C 695.339,274.6 680.618,269.517 651.176,254.267 L 651.176,172.933"/>
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 651.176 172.933 C 680.618,188.183 695.339,193.267 724.78,193.267 C 754.222,193.267 768.943,188.183 798.385,172.933"/>
<text font-size="12.8" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="724.78" y="227.767">
<tspan x="724.78" y="227.767">Cluster History DB</tspan>
</text>
</g>
<g>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="96.386,-142.999 96.386,-205.998 899.386,-205.998 899.386,5.26493 "/>
<polygon style="fill: #000000" points="899.386,12.7649 894.386,2.76493 899.386,5.26493 904.386,2.76493 "/>
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="899.386,12.7649 894.386,2.76493 899.386,5.26493 904.386,2.76493 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,3 +1,9 @@
..
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/
============================================
Welcome to Watcher's developer documentation
============================================
@@ -21,8 +27,8 @@ Introduction
.. toctree::
:maxdepth: 1
dev/glossary
dev/architecture
glossary
architecture
dev/environment
dev/contributing
dev/plugins

View File

@@ -1,3 +1,9 @@
..
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/
========
Usage
========

View File

@@ -1,3 +1,9 @@
..
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/
=====================
RESTful Web API (v1)
=====================

View File

@@ -157,13 +157,17 @@
# timeout exception when timeout expired. (integer value)
#rpc_poll_timeout = 1
# Shows whether zmq-messaging uses broker or not. (boolean value)
#zmq_use_broker = true
# Configures zmq-messaging to use broker or not. (boolean value)
#zmq_use_broker = false
# Minimal port number for random ports range. (integer value)
# Minimal port number for random ports range. (port value)
# Minimum value: 1
# Maximum value: 65535
#rpc_zmq_min_port = 49152
# Maximal port number for random ports range. (integer value)
# Minimum value: 1
# Maximum value: 65536
#rpc_zmq_max_port = 65536
# Number of retries to find free port number before fail with
@@ -173,7 +177,9 @@
# Host to locate redis. (string value)
#host = 127.0.0.1
# Use this port to connect to redis host. (integer value)
# Use this port to connect to redis host. (port value)
# Minimum value: 1
# Maximum value: 65535
#port = 6379
# Password for Redis server (optional). (string value)
@@ -185,16 +191,19 @@
# The Drivers(s) to handle sending notifications. Possible values are
# messaging, messagingv2, routing, log, test, noop (multi valued)
#notification_driver =
# Deprecated group/name - [DEFAULT]/notification_driver
#driver =
# A URL representing the messaging driver to use for notifications. If
# not set, we fall back to the same configuration used for RPC.
# (string value)
#notification_transport_url = <None>
# Deprecated group/name - [DEFAULT]/notification_transport_url
#transport_url = <None>
# AMQP topic used for OpenStack notifications. (list value)
# Deprecated group/name - [rpc_notifier2]/topics
#notification_topics = notifications
# Deprecated group/name - [DEFAULT]/notification_topics
#topics = notifications
# Seconds to wait for a response from a call. (integer value)
#rpc_response_timeout = 60
@@ -205,7 +214,7 @@
#transport_url = <None>
# The messaging driver to use, defaults to rabbit. Other drivers
# include qpid and zmq. (string value)
# include amqp and zmq. (string value)
#rpc_backend = rabbit
# The default exchange under which topics are scoped. May be
@@ -508,6 +517,14 @@
# Service tenant name. (string value)
#admin_tenant_name = admin
# Authentication type to load (unknown value)
# Deprecated group/name - [DEFAULT]/auth_plugin
#auth_type = <None>
# Config Section from which to load plugin specific options (unknown
# value)
#auth_section = <None>
[matchmaker_redis]
@@ -518,7 +535,9 @@
# Host to locate redis. (string value)
#host = 127.0.0.1
# Use this port to connect to redis host. (integer value)
# Use this port to connect to redis host. (port value)
# Minimum value: 1
# Maximum value: 65535
#port = 6379
# Password for Redis server (optional). (string value)
@@ -599,82 +618,6 @@
#password =
[oslo_messaging_qpid]
#
# From oslo.messaging
#
# Use durable queues in AMQP. (boolean value)
# Deprecated group/name - [DEFAULT]/amqp_durable_queues
# Deprecated group/name - [DEFAULT]/rabbit_durable_queues
#amqp_durable_queues = false
# Auto-delete queues in AMQP. (boolean value)
# Deprecated group/name - [DEFAULT]/amqp_auto_delete
#amqp_auto_delete = false
# Send a single AMQP reply to call message. The current behaviour
# since oslo-incubator is to send two AMQP replies - first one with
# the payload, a second one to ensure the other have finish to send
# the payload. We are going to remove it in the N release, but we must
# keep backward compatible at the same time. This option provides such
# compatibility - it defaults to False in Liberty and can be turned on
# for early adopters with a new installations or for testing. Please
# note, that this option will be removed in the Mitaka release.
# (boolean value)
#send_single_reply = false
# Qpid broker hostname. (string value)
# Deprecated group/name - [DEFAULT]/qpid_hostname
#qpid_hostname = localhost
# Qpid broker port. (integer value)
# Deprecated group/name - [DEFAULT]/qpid_port
#qpid_port = 5672
# Qpid HA cluster host:port pairs. (list value)
# Deprecated group/name - [DEFAULT]/qpid_hosts
#qpid_hosts = $qpid_hostname:$qpid_port
# Username for Qpid connection. (string value)
# Deprecated group/name - [DEFAULT]/qpid_username
#qpid_username =
# Password for Qpid connection. (string value)
# Deprecated group/name - [DEFAULT]/qpid_password
#qpid_password =
# Space separated list of SASL mechanisms to use for auth. (string
# value)
# Deprecated group/name - [DEFAULT]/qpid_sasl_mechanisms
#qpid_sasl_mechanisms =
# Seconds between connection keepalive heartbeats. (integer value)
# Deprecated group/name - [DEFAULT]/qpid_heartbeat
#qpid_heartbeat = 60
# Transport to use, either 'tcp' or 'ssl'. (string value)
# Deprecated group/name - [DEFAULT]/qpid_protocol
#qpid_protocol = tcp
# Whether to disable the Nagle algorithm. (boolean value)
# Deprecated group/name - [DEFAULT]/qpid_tcp_nodelay
#qpid_tcp_nodelay = true
# The number of prefetched messages held by receiver. (integer value)
# Deprecated group/name - [DEFAULT]/qpid_receiver_capacity
#qpid_receiver_capacity = 1
# The qpid topology version to use. Version 1 is what was originally
# used by impl_qpid. Version 2 includes some backwards-incompatible
# changes that allow broker federation to work. Users should update
# to version 2 when they are able to take everything down, as it
# requires a clean break. (integer value)
# Deprecated group/name - [DEFAULT]/qpid_topology_version
#qpid_topology_version = 1
[oslo_messaging_rabbit]
#
@@ -725,18 +668,26 @@
# Deprecated group/name - [DEFAULT]/kombu_reconnect_delay
#kombu_reconnect_delay = 1.0
# How long to wait before considering a reconnect attempt to have
# failed. This value should not be longer than rpc_response_timeout.
# How long to wait a missing client beforce abandoning to send it its
# replies. This value should not be longer than rpc_response_timeout.
# (integer value)
#kombu_reconnect_timeout = 60
# Deprecated group/name - [DEFAULT]/kombu_reconnect_timeout
#kombu_missing_consumer_retry_timeout = 5
# Determines how the next RabbitMQ node is chosen in case the one we
# are currently connected to becomes unavailable. Takes effect only if
# more than one RabbitMQ node is provided in config. (string value)
# Allowed values: round-robin, shuffle
#kombu_failover_strategy = round-robin
# The RabbitMQ broker address where a single node is used. (string
# value)
# Deprecated group/name - [DEFAULT]/rabbit_host
#rabbit_host = localhost
# The RabbitMQ broker port where a single node is used. (integer
# value)
# The RabbitMQ broker port where a single node is used. (port value)
# Minimum value: 1
# Maximum value: 65535
# Deprecated group/name - [DEFAULT]/rabbit_port
#rabbit_port = 5672
@@ -837,6 +788,10 @@
# value)
#publisher_id = watcher.decision.api
# The maximum number of threads that can be used to execute strategies
# (integer value)
#max_workers = 2
[watcher_goals]

View File

@@ -16,7 +16,6 @@ classifier =
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
[files]
@@ -43,12 +42,14 @@ watcher.database.migration_backend =
sqlalchemy = watcher.db.sqlalchemy.migration
watcher_strategies =
dummy = watcher.decision_engine.strategy.dummy_strategy:DummyStrategy
basic = watcher.decision_engine.strategy.basic_consolidation:BasicConsolidation
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
fresh_env = 1
all_files = 1
[upload_sphinx]

View File

@@ -2,19 +2,19 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking<0.11,>=0.10
coverage>=3.6
discover
python-subunit>=0.0.18
hacking>=0.10.2,<0.11
mock>=1.2
oslotest>=1.10.0 # Apache-2.0
python-subunit>=0.0.18
testrepository>=0.0.18
testscenarios>=0.4
testtools>=1.4.0
mock>=1.2
# Doc requirements
oslosphinx>=2.5.0 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
oslosphinx>=2.5.0 # Apache-2.0
sphinxcontrib-pecanwsme>=0.8
# For PyPI distribution

17
tox.ini
View File

@@ -1,16 +1,20 @@
[tox]
minversion = 1.6
envlist = py33,py34,py27,pypy,pep8
envlist = py34,py27,pep8
skipsdist = True
[testenv]
usedevelop = True
whitelist_externals = find
install_command = pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'
commands =
find . -type f -name "*.pyc" -delete
find . -type d -name "__pycache__" -delete
python setup.py testr --slowest --testr-args='{posargs}'
[testenv:pep8]
commands = flake8
@@ -19,7 +23,7 @@ commands = flake8
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --omit="watcher/tests/*,watcher/openstack/*" --testr-args='{posargs}'
commands = python setup.py testr --coverage --omit="watcher/tests/*" --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
@@ -41,9 +45,9 @@ commands =
# E123, E125 skipped as they are invalid PEP-8.
show-source=True
ignore=E123,E125,H404,H405,H305
ignore=E123,E125
builtins= _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
[testenv:pypi]
commands =
@@ -52,3 +56,6 @@ commands =
[testenv:wheel]
commands = python setup.py bdist_wheel
[hacking]
import_exceptions = watcher._i18n

View File

@@ -16,11 +16,31 @@
#
import oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='watcher')
oslo_i18n.enable_lazy()
# 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)
DOMAIN = "watcher"
_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
# The primary translation function using the well-known name "_"
_ = _translators.primary
# The contextual translation function using the name "_C"
_C = _translators.contextual_form
# The plural translation function using the name "_P"
_P = _translators.plural_form
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
def get_available_languages():
return oslo_i18n.get_available_languages(DOMAIN)

View File

@@ -1,6 +0,0 @@
# Watcher API
This component implements the REST API provided by the Watcher system to the external world. It enables a cluster administrator to control and monitor the Watcher system via any interaction mechanism connected to this API :
* CLI
* Horizon plugin
* Python SDK

View File

@@ -22,7 +22,7 @@ import pecan
from watcher.api import acl
from watcher.api import config as api_config
from watcher.api import middleware
from watcher.decision_engine.strategy.selector import default \
from watcher.decision_engine.strategy.selection import default \
as strategy_selector
# Register options for the service

View File

@@ -199,7 +199,7 @@ class ActionCollection(collection.Collection):
reverse = True if kwargs['sort_dir'] == 'desc' else False
collection.actions = sorted(
collection.actions,
key=lambda action: action.next_uuid,
key=lambda action: action.next_uuid or '',
reverse=reverse)
collection.next = collection.get_next(limit, url=url, **kwargs)
@@ -229,7 +229,6 @@ class ActionsController(rest.RestController):
sort_key, sort_dir, expand=False,
resource_url=None,
action_plan_uuid=None, audit_uuid=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)

View File

@@ -21,8 +21,8 @@ import six
import wsme
from wsme import types as wtypes
from watcher._i18n import _
from watcher.common import exception
from watcher.common.i18n import _
from watcher.common import utils
@@ -181,8 +181,9 @@ class MultiType(wtypes.UserType):
return value
else:
raise ValueError(
_("Wrong type. Expected '%(type)s', got '%(value)s'")
% {'type': self.types, 'value': type(value)})
_("Wrong type. Expected '%(type)s', got '%(value)s'"),
type=self.types, value=type(value)
)
class JsonPatchType(wtypes.Base):
@@ -217,7 +218,7 @@ class JsonPatchType(wtypes.Base):
@staticmethod
def validate(patch):
_path = '/' + patch.path.split('/')[1]
_path = '/{0}'.format(patch.path.split('/')[1])
if _path in patch.internal_attrs():
msg = _("'%s' is an internal attribute and can not be updated")
raise wsme.exc.ClientSideError(msg % patch.path)

View File

@@ -17,7 +17,7 @@ import jsonpatch
from oslo_config import cfg
import wsme
from watcher.common.i18n import _
from watcher._i18n import _
CONF = cfg.CONF
@@ -28,10 +28,18 @@ JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
def validate_limit(limit):
if limit is not None and limit <= 0:
if limit is None:
return CONF.api.max_limit
if limit <= 0:
# Case where we don't a valid limit value
raise wsme.exc.ClientSideError(_("Limit must be positive"))
return min(CONF.api.max_limit, limit) or CONF.api.max_limit
if limit and not CONF.api.max_limit:
# Case where we don't have an upper limit
return limit
return min(CONF.api.max_limit, limit)
def validate_sort_dir(sort_dir):

View File

@@ -19,8 +19,8 @@ from oslo_log import log
from keystonemiddleware import auth_token
from watcher._i18n import _
from watcher.common import exception
from watcher.common.i18n import _
from watcher.common import utils
LOG = log.getLogger(__name__)
@@ -40,10 +40,9 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
for route_tpl in public_api_routes]
except re.error as e:
msg = _('Cannot compile public API routes: %s') % e
LOG.error(msg)
raise exception.ConfigInvalid(error_msg=msg)
LOG.exception(e)
raise exception.ConfigInvalid(
error_msg=_('Cannot compile public API routes'))
super(AuthTokenMiddleware, self).__init__(app, conf)

View File

@@ -13,7 +13,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Middleware to replace the plain text message body of an error
response with one formatted so the client can parse it.
@@ -24,12 +23,12 @@ Based on pecan.middleware.errordocument
import json
from xml import etree as et
from oslo_log import log
import six
import webob
from oslo_log import log
from watcher.common.i18n import _
from watcher.common.i18n import _LE
from watcher._i18n import _
from watcher._i18n import _LE
LOG = log.getLogger(__name__)
@@ -69,23 +68,28 @@ class ParsableErrorMiddleware(object):
app_iter = self.app(environ, replacement_start_response)
if (state['status_code'] // 100) not in (2, 3):
req = webob.Request(environ)
if (req.accept.best_match(['application/json', 'application/xml'])
== 'application/xml'):
if (req.accept.best_match(['application/json', 'application/xml']
) == 'application/xml'
):
try:
# simple check xml is valid
body = [et.ElementTree.tostring(
et.ElementTree.fromstring('<error_message>'
+ '\n'.join(app_iter)
+ '</error_message>'))]
et.ElementTree.Element('error_message',
text='\n'.join(app_iter)))]
except et.ElementTree.ParseError as err:
LOG.error(_LE('Error parsing HTTP response: %s'), err)
body = ['<error_message>%s' % state['status_code']
+ '</error_message>']
body = [et.ElementTree.tostring(
et.ElementTree.Element('error_message',
text=state['status_code']))]
state['headers'].append(('Content-Type', 'application/xml'))
else:
if six.PY3:
app_iter = [i.decode('utf-8') for i in app_iter]
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
if six.PY3:
body = [item.encode('utf-8') for item in body]
state['headers'].append(('Content-Type', 'application/json'))
state['headers'].append(('Content-Length', len(body[0])))
state['headers'].append(('Content-Length', str(len(body[0]))))
else:
body = app_iter
return body

View File

@@ -1,11 +0,0 @@
# Watcher Actions Applier
This component is in charge of executing the plan of actions built by the Watcher Actions Planner.
For each action of the workflow, this component may call directly the component responsible for this kind of action (Example : Nova API for an instance migration) or via some publish/subscribe pattern on the message bus.
It notifies continuously of the current progress of the Action Plan (and atomic Actions), sending status messages on the bus. Those events may be used by the CEP to trigger new actions.
This component is also connected to the Watcher MySQL database in order to:
* get the description of the action plan to execute
* persist its current state so that if it is restarted, it can restore each Action plan context and restart from the last known safe point of each ongoing workflow.

View File

@@ -22,8 +22,7 @@ import six
@six.add_metaclass(abc.ABCMeta)
class ApplierCommand(object):
class BaseActionPlanHandler(object):
@abc.abstractmethod
def execute(self):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()

View File

@@ -18,8 +18,8 @@
#
from oslo_log import log
from watcher.applier.action_plan.base import BaseActionPlanHandler
from watcher.applier.default import DefaultApplier
from watcher.applier.messaging.applier_command import ApplierCommand
from watcher.applier.messaging.events import Events
from watcher.common.messaging.events.event import Event
from watcher.objects.action_plan import ActionPlan
@@ -28,23 +28,23 @@ from watcher.objects.action_plan import Status
LOG = log.getLogger(__name__)
class LaunchActionPlanCommand(ApplierCommand):
class DefaultActionPlanHandler(BaseActionPlanHandler):
def __init__(self, context, manager_applier, action_plan_uuid):
super(LaunchActionPlanCommand, self).__init__()
super(DefaultActionPlanHandler, self).__init__()
self.ctx = context
self.action_plan_uuid = action_plan_uuid
self.manager_applier = manager_applier
def notify(self, uuid, event_type, status):
def notify(self, uuid, event_type, state):
action_plan = ActionPlan.get_by_uuid(self.ctx, uuid)
action_plan.state = status
action_plan.state = state
action_plan.save()
event = Event()
event.set_type(event_type)
event.set_data({})
event.type = event_type
event.data = {}
payload = {'action_plan__uuid': uuid,
'action_plan_status': status}
self.manager_applier.topic_status.publish_event(event.get_type().name,
'action_plan_state': state}
self.manager_applier.topic_status.publish_event(event.type.name,
payload)
def execute(self):

View File

@@ -22,8 +22,7 @@ import six
@six.add_metaclass(abc.ABCMeta)
class Applier(object):
class BaseApplier(object):
@abc.abstractmethod
def execute(self, action_plan_uuid):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()

View File

@@ -16,18 +16,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from watcher.applier.base import Applier
from watcher.applier.execution.executor import CommandExecutor
from watcher.applier.base import BaseApplier
from watcher.applier.execution.executor import ActionPlanExecutor
from watcher.objects import Action
from watcher.objects import ActionPlan
class DefaultApplier(Applier):
class DefaultApplier(BaseApplier):
def __init__(self, manager_applier, context):
super(DefaultApplier, self).__init__()
self.manager_applier = manager_applier
self.context = context
self.executor = CommandExecutor(manager_applier, context)
self.executor = ActionPlanExecutor(manager_applier, context)
def execute(self, action_plan_uuid):
action_plan = ActionPlan.get_by_uuid(self.context, action_plan_uuid)

View File

@@ -24,25 +24,31 @@ LOG = log.getLogger(__name__)
class DeployPhase(object):
def __init__(self, executor):
# todo(jed) oslo_conf 10 secondes
self.maxTimeout = 100000
self.commands = []
self.executor = executor
self._max_timeout = 100000
self._actions = []
self._executor = executor
def set_max_time(self, mt):
self.maxTimeout = mt
@property
def actions(self):
return self._actions
def get_max_time(self):
return self.maxTimeout
@property
def max_timeout(self):
return self._max_timeout
@max_timeout.setter
def max_timeout(self, m):
self._max_timeout = m
def populate(self, action):
self.commands.append(action)
self._actions.append(action)
def execute_primitive(self, primitive):
futur = primitive.execute(primitive)
return futur.result(self.get_max_time())
future = primitive.execute(primitive)
return future.result(self.max_timeout)
def rollback(self):
reverted = sorted(self.commands, reverse=True)
reverted = sorted(self.actions, reverse=True)
for primitive in reverted:
try:
self.execute_primitive(primitive)

View File

@@ -17,10 +17,8 @@
# limitations under the License.
#
from oslo_log import log
from watcher.applier.mapper.default import DefaultCommandMapper
from watcher.applier.execution.deploy_phase import DeployPhase
from watcher.applier.mapping.default import DefaultActionMapper
from watcher.applier.messaging.events import Events
from watcher.common.messaging.events.event import Event
from watcher.objects import Action
@@ -29,26 +27,26 @@ from watcher.objects.action_plan import Status
LOG = log.getLogger(__name__)
class CommandExecutor(object):
class ActionPlanExecutor(object):
def __init__(self, manager_applier, context):
self.manager_applier = manager_applier
self.context = context
self.deploy = DeployPhase(self)
self.mapper = DefaultCommandMapper()
self.mapper = DefaultActionMapper()
def get_primitive(self, action):
return self.mapper.build_primitive_command(action)
return self.mapper.build_primitive_from_action(action)
def notify(self, action, state):
db_action = Action.get_by_uuid(self.context, action.uuid)
db_action.state = state
db_action.save()
event = Event()
event.set_type(Events.LAUNCH_ACTION)
event.set_data({})
event.type = Events.LAUNCH_ACTION
event.data = {}
payload = {'action_uuid': action.uuid,
'action_status': state}
self.manager_applier.topic_status.publish_event(event.get_type().name,
'action_state': state}
self.manager_applier.topic_status.publish_event(event.type.name,
payload)
def execute(self, actions):

View File

@@ -23,9 +23,6 @@ from oslo_log import log
from watcher.applier.messaging.trigger import TriggerActionPlan
from watcher.common.messaging.messaging_core import MessagingCore
from watcher.common.messaging.notification_handler import NotificationHandler
from watcher.decision_engine.messaging.events import Events
LOG = log.getLogger(__name__)
CONF = cfg.CONF
@@ -57,19 +54,8 @@ opt_group = cfg.OptGroup(name='watcher_applier',
CONF.register_group(opt_group)
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('admin_password', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
class ApplierManager(MessagingCore):
# todo(jed) need workflow
def __init__(self):
super(ApplierManager, self).__init__(
CONF.watcher_applier.publisher_id,
@@ -79,24 +65,9 @@ class ApplierManager(MessagingCore):
)
# shared executor of the workflow
self.executor = ThreadPoolExecutor(max_workers=1)
self.handler = NotificationHandler(self.publisher_id)
self.handler.register_observer(self)
self.add_event_listener(Events.ALL, self.event_receive)
# trigger action_plan
self.topic_control.add_endpoint(TriggerActionPlan(self))
def join(self):
self.topic_control.join()
self.topic_status.join()
def event_receive(self, event):
try:
request_id = event.get_request_id()
event_type = event.get_type()
data = event.get_data()
LOG.debug("request id => %s" % request_id)
LOG.debug("type_event => %s" % str(event_type))
LOG.debug("data => %s" % str(data))
except Exception as e:
LOG.error("evt %s" % e.message)
raise e

View File

@@ -22,8 +22,12 @@ import six
@six.add_metaclass(abc.ABCMeta)
class CommandMapper(object):
class BaseActionMapper(object):
@abc.abstractmethod
def build_primitive_command(self, action):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
def build_primitive_from_action(self, action):
"""Transform an action to a primitive
:type action: watcher.decision_engine.action.BaseAction
:return: the associated Primitive
"""
raise NotImplementedError()

View File

@@ -17,31 +17,31 @@
# limitations under the License.
#
from watcher.applier.mapper.base import CommandMapper
from watcher.applier.primitive.hypervisor_state import HypervisorStateCommand
from watcher.applier.primitive.migration import MigrateCommand
from watcher.applier.primitive.nop import NopCommand
from watcher.applier.primitive.power_state import PowerStateCommand
from watcher.applier.mapping.base import BaseActionMapper
from watcher.applier.primitives.change_nova_service_state import \
ChangeNovaServiceState
from watcher.applier.primitives.migration import Migrate
from watcher.applier.primitives.nop import Nop
from watcher.applier.primitives.power_state import ChangePowerState
from watcher.common.exception import ActionNotFound
from watcher.decision_engine.planner.default import Primitives
class DefaultCommandMapper(CommandMapper):
def build_primitive_command(self, action):
class DefaultActionMapper(BaseActionMapper):
def build_primitive_from_action(self, action):
if action.action_type == Primitives.COLD_MIGRATE.value:
return MigrateCommand(action.applies_to, Primitives.COLD_MIGRATE,
action.src,
action.dst)
return Migrate(action.applies_to, Primitives.COLD_MIGRATE,
action.src,
action.dst)
elif action.action_type == Primitives.LIVE_MIGRATE.value:
return MigrateCommand(action.applies_to, Primitives.COLD_MIGRATE,
action.src,
action.dst)
return Migrate(action.applies_to, Primitives.COLD_MIGRATE,
action.src,
action.dst)
elif action.action_type == Primitives.HYPERVISOR_STATE.value:
return HypervisorStateCommand(action.applies_to, action.parameter)
return ChangeNovaServiceState(action.applies_to, action.parameter)
elif action.action_type == Primitives.POWER_STATE.value:
return PowerStateCommand()
return ChangePowerState()
elif action.action_type == Primitives.NOP.value:
return NopCommand()
return Nop()
else:
raise ActionNotFound()

View File

@@ -18,7 +18,7 @@
#
from oslo_log import log
from watcher.applier.messaging.launcher import LaunchActionPlanCommand
from watcher.applier.action_plan.default import DefaultActionPlanHandler
LOG = log.getLogger(__name__)
@@ -29,12 +29,12 @@ class TriggerActionPlan(object):
def do_launch_action_plan(self, context, action_plan_uuid):
try:
cmd = LaunchActionPlanCommand(context,
self.manager_applier,
action_plan_uuid)
cmd = DefaultActionPlanHandler(context,
self.manager_applier,
action_plan_uuid)
cmd.execute()
except Exception as e:
LOG.error("do_launch_action_plan " + unicode(e))
LOG.exception(e)
def launch_action_plan(self, context, action_plan_uuid):
LOG.debug("Trigger ActionPlan %s" % action_plan_uuid)

View File

@@ -1,61 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from oslo_config import cfg
from watcher.applier.primitive.base import PrimitiveCommand
from watcher.applier.primitive.wrapper.nova_wrapper import NovaWrapper
from watcher.applier.promise import Promise
from watcher.common.keystone import KeystoneClient
from watcher.decision_engine.model.hypervisor_state import HypervisorState
CONF = cfg.CONF
class HypervisorStateCommand(PrimitiveCommand):
def __init__(self, host, status):
self.host = host
self.status = status
def nova_manage_service(self, status):
keystone = KeystoneClient()
wrapper = NovaWrapper(keystone.get_credentials(),
session=keystone.get_session())
if status is True:
return wrapper.enable_service_nova_compute(self.host)
else:
return wrapper.disable_service_nova_compute(self.host)
@Promise
def execute(self):
if self.status == HypervisorState.OFFLINE.value:
state = False
elif self.status == HypervisorState.ONLINE.value:
state = True
return self.nova_manage_service(state)
@Promise
def undo(self):
if self.status == HypervisorState.OFFLINE.value:
state = True
elif self.status == HypervisorState.ONLINE.value:
state = False
return self.nova_manage_service(state)

View File

@@ -22,15 +22,13 @@ from watcher.applier.promise import Promise
@six.add_metaclass(abc.ABCMeta)
class PrimitiveCommand(object):
class BasePrimitive(object):
@Promise
@abc.abstractmethod
def execute(self):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()
@Promise
@abc.abstractmethod
def undo(self):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()

View File

@@ -0,0 +1,83 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from oslo_config import cfg
from watcher._i18n import _
from watcher.applier.primitives.base import BasePrimitive
from watcher.applier.promise import Promise
from watcher.common.exception import IllegalArgumentException
from watcher.common.keystone import KeystoneClient
from watcher.common.nova import NovaClient
from watcher.decision_engine.model.hypervisor_state import HypervisorState
CONF = cfg.CONF
class ChangeNovaServiceState(BasePrimitive):
def __init__(self, host, state):
"""This class allows us to change the state of nova-compute service.
:param host: the uuid of the host
:param state: (enabled/disabled)
"""
super(BasePrimitive, self).__init__()
self._host = host
self._state = state
@property
def host(self):
return self._host
@property
def state(self):
return self._state
@Promise
def execute(self):
target_state = None
if self.state == HypervisorState.OFFLINE.value:
target_state = False
elif self.status == HypervisorState.ONLINE.value:
target_state = True
return self.nova_manage_service(target_state)
@Promise
def undo(self):
target_state = None
if self.state == HypervisorState.OFFLINE.value:
target_state = True
elif self.state == HypervisorState.ONLINE.value:
target_state = False
return self.nova_manage_service(target_state)
def nova_manage_service(self, state):
if state is None:
raise IllegalArgumentException(
_("The target state is not defined"))
keystone = KeystoneClient()
wrapper = NovaClient(keystone.get_credentials(),
session=keystone.get_session())
if state is True:
return wrapper.enable_service_nova_compute(self.host)
else:
return wrapper.disable_service_nova_compute(self.host)

View File

@@ -21,20 +21,21 @@ from keystoneclient.auth.identity import v3
from keystoneclient import session
from oslo_config import cfg
from watcher.applier.primitive.base import PrimitiveCommand
from watcher.applier.primitive.wrapper.nova_wrapper import NovaWrapper
from watcher.applier.primitives.base import BasePrimitive
from watcher.applier.promise import Promise
from watcher.common.keystone import KeystoneClient
from watcher.common.nova import NovaClient
from watcher.decision_engine.planner.default import Primitives
CONF = cfg.CONF
class MigrateCommand(PrimitiveCommand):
class Migrate(BasePrimitive):
def __init__(self, vm_uuid=None,
migration_type=None,
source_hypervisor=None,
destination_hypervisor=None):
super(BasePrimitive, self).__init__()
self.instance_uuid = vm_uuid
self.migration_type = migration_type
self.source_hypervisor = source_hypervisor
@@ -42,8 +43,8 @@ class MigrateCommand(PrimitiveCommand):
def migrate(self, destination):
keystone = KeystoneClient()
wrapper = NovaWrapper(keystone.get_credentials(),
session=keystone.get_session())
wrapper = NovaClient(keystone.get_credentials(),
session=keystone.get_session())
instance = wrapper.find_instance(self.instance_uuid)
if instance:
project_id = getattr(instance, "tenant_id")
@@ -64,7 +65,7 @@ class MigrateCommand(PrimitiveCommand):
project_domain_name=creds2[
'project_domain_name'])
sess2 = session.Session(auth=auth2)
wrapper2 = NovaWrapper(creds2, session=sess2)
wrapper2 = NovaClient(creds2, session=sess2)
# todo(jed) remove Primitves
if self.migration_type is Primitives.COLD_MIGRATE:

View File

@@ -19,16 +19,15 @@
from oslo_log import log
from watcher.applier.primitive.base import PrimitiveCommand
from watcher.applier.primitives.base import BasePrimitive
from watcher.applier.promise import Promise
LOG = log.getLogger(__name__)
class NopCommand(PrimitiveCommand):
def __init__(self):
pass
class Nop(BasePrimitive):
@Promise
def execute(self):

View File

@@ -16,20 +16,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from watcher.applier.primitive.base import PrimitiveCommand
from watcher.applier.primitives.base import BasePrimitive
from watcher.applier.promise import Promise
class PowerStateCommand(PrimitiveCommand):
def __init__(self):
pass
class ChangePowerState(BasePrimitive):
@Promise
def execute(self):
pass
raise NotImplementedError # pragma:no cover
@Promise
def undo(self):
# TODO(jde): migrate VM from target_hypervisor
# to current_hypervisor in model
return True
raise NotImplementedError # pragma:no cover

View File

@@ -20,7 +20,6 @@ from oslo_config import cfg
from oslo_log import log
import oslo_messaging as om
from watcher.applier.manager import APPLIER_MANAGER_OPTS
from watcher.applier.manager import opt_group
from watcher.common import exception
@@ -69,5 +68,5 @@ class ApplierAPI(MessagingCore):
try:
pass
except Exception as e:
LOG.error("evt %s" % e.message)
raise e
LOG.exception(e)
raise

View File

@@ -19,14 +19,15 @@
import logging as std_logging
import os
import sys
from wsgiref import simple_server
from oslo_config import cfg
from oslo_log import log as logging
from watcher._i18n import _
from watcher.api import app as api_app
from watcher.common.i18n import _
from watcher import service
from watcher.common import service
LOG = logging.getLogger(__name__)
@@ -34,7 +35,7 @@ CONF = cfg.CONF
def main():
service.prepare_service()
service.prepare_service(sys.argv)
app = api_app.setup_app()
@@ -42,7 +43,6 @@ def main():
host, port = cfg.CONF.api.host, cfg.CONF.api.port
srv = simple_server.make_server(host, port, app)
logging.setup(CONF, 'watcher')
LOG.info(_('Starting server in PID %s') % os.getpid())
LOG.debug("Watcher configuration:")
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)

View File

@@ -24,13 +24,13 @@ import sys
from oslo_config import cfg
from oslo_log import log as logging
from watcher import _i18n
from watcher.applier.manager import ApplierManager
from watcher.common import service
from watcher import i18n
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
_LI = i18n._LI
_LI = _i18n._LI
def main():

View File

@@ -23,15 +23,15 @@ import sys
from oslo_config import cfg
from oslo_log import log as logging
from watcher import _i18n
from watcher.common import service
from watcher.decision_engine.manager import DecisionEngineManager
from watcher import i18n
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
_LI = i18n._LI
_LI = _i18n._LI
def main():

View File

@@ -19,6 +19,7 @@
from ceilometerclient import client
from ceilometerclient.exc import HTTPUnauthorized
from watcher.common import keystone
@@ -44,6 +45,7 @@ class CeilometerClient(object):
def build_query(self, user_id=None, tenant_id=None, resource_id=None,
user_ids=None, tenant_ids=None, resource_ids=None):
"""Returns query built from given parameters.
This query can be then used for querying resources, meters and
statistics.
:Parameters:
@@ -54,6 +56,7 @@ class CeilometerClient(object):
- `tenant_ids`: list of tenant_ids
- `resource_ids`: list of resource_ids
"""
user_ids = user_ids or []
tenant_ids = tenant_ids or []
resource_ids = resource_ids or []
@@ -109,7 +112,8 @@ class CeilometerClient(object):
meter_name,
period,
aggregate='avg'):
"""
"""Representing a statistic aggregate by operators
:param resource_id: id
:param meter_name: meter names of which we want the statistics
:param period: `period`: In seconds. If no period is given, only one
@@ -119,7 +123,6 @@ class CeilometerClient(object):
:param aggregate:
:return:
"""
"""Representing a statistic aggregate by operators"""
query = self.build_query(resource_id=resource_id)
statistic = self.query_retry(f=self.cmclient.statistics.list,

View File

@@ -24,11 +24,9 @@ SHOULD include dedicated exception logging.
from oslo_config import cfg
from oslo_log import log as logging
import six
from watcher.common.i18n import _
from watcher.common.i18n import _LE
from watcher._i18n import _, _LE
LOG = logging.getLogger(__name__)
@@ -43,8 +41,8 @@ CONF.register_opts(exc_log_opts)
def _cleanse_dict(original):
"""Strip all admin_password, new_pass, rescue_pass keys from a dict."""
return dict((k, v) for k, v in original.iteritems() if "_pass" not in k)
"""Strip all admin_password, new_pass, rescue_pass keys from a dict"""
return dict((k, v) for k, v in six.iteritems(original) if "_pass" not in k)
class WatcherException(Exception):
@@ -55,7 +53,7 @@ class WatcherException(Exception):
with the keyword arguments provided to the constructor.
"""
message = _("An unknown exception occurred.")
message = _("An unknown exception occurred")
code = 500
headers = {}
safe = False
@@ -77,8 +75,8 @@ class WatcherException(Exception):
# kwargs doesn't match a variable in the message
# log the issue and the kwargs
LOG.exception(_LE('Exception in string format operation'))
for name, value in kwargs.iteritems():
LOG.error("%s: %s" % (name, value))
for name, value in six.iteritems(kwargs):
LOG.error("%s: %s", name, value)
if CONF.fatal_exception_format_errors:
raise e
@@ -89,7 +87,7 @@ class WatcherException(Exception):
super(WatcherException, self).__init__(message)
def __str__(self):
"""Encode to utf-8 then wsme api can consume it as well."""
"""Encode to utf-8 then wsme api can consume it as well"""
if not six.PY3:
return unicode(self.args[0]).encode('utf-8')
else:
@@ -106,39 +104,39 @@ class WatcherException(Exception):
class NotAuthorized(WatcherException):
message = _("Not authorized.")
message = _("Not authorized")
code = 403
class OperationNotPermitted(NotAuthorized):
message = _("Operation not permitted.")
message = _("Operation not permitted")
class Invalid(WatcherException):
message = _("Unacceptable parameters.")
message = _("Unacceptable parameters")
code = 400
class ObjectNotFound(WatcherException):
message = _("The %(name)s %(id)s could not be found.")
message = _("The %(name)s %(id)s could not be found")
class Conflict(WatcherException):
message = _('Conflict.')
message = _('Conflict')
code = 409
class ResourceNotFound(ObjectNotFound):
message = _("The %(name)s resource %(id)s could not be found.")
message = _("The %(name)s resource %(id)s could not be found")
code = 404
class InvalidIdentity(Invalid):
message = _("Expected an uuid or int but received %(identity)s.")
message = _("Expected an uuid or int but received %(identity)s")
class InvalidGoal(Invalid):
message = _("Goal %(goal)s is not defined in Watcher configuration file.")
message = _("Goal %(goal)s is not defined in Watcher configuration file")
# Cannot be templated as the error syntax varies.
@@ -148,73 +146,73 @@ class InvalidParameterValue(Invalid):
class InvalidUUID(Invalid):
message = _("Expected a uuid but received %(uuid)s.")
message = _("Expected a uuid but received %(uuid)s")
class InvalidName(Invalid):
message = _("Expected a logical name but received %(name)s.")
message = _("Expected a logical name but received %(name)s")
class InvalidUuidOrName(Invalid):
message = _("Expected a logical name or uuid but received %(name)s.")
message = _("Expected a logical name or uuid but received %(name)s")
class AuditTemplateNotFound(ResourceNotFound):
message = _("AuditTemplate %(audit_template)s could not be found.")
message = _("AuditTemplate %(audit_template)s could not be found")
class AuditTemplateAlreadyExists(Conflict):
message = _("An audit_template with UUID %(uuid)s or name %(name)s "
"already exists.")
"already exists")
class AuditTemplateReferenced(Invalid):
message = _("AuditTemplate %(audit_template)s is referenced by one or "
"multiple audit.")
"multiple audit")
class AuditNotFound(ResourceNotFound):
message = _("Audit %(audit)s could not be found.")
message = _("Audit %(audit)s could not be found")
class AuditAlreadyExists(Conflict):
message = _("An audit with UUID %(uuid)s already exists.")
message = _("An audit with UUID %(uuid)s already exists")
class AuditReferenced(Invalid):
message = _("Audit %(audit)s is referenced by one or multiple action "
"plans.")
"plans")
class ActionPlanNotFound(ResourceNotFound):
message = _("ActionPlan %(action plan)s could not be found.")
message = _("ActionPlan %(action plan)s could not be found")
class ActionPlanAlreadyExists(Conflict):
message = _("An action plan with UUID %(uuid)s already exists.")
message = _("An action plan with UUID %(uuid)s already exists")
class ActionPlanReferenced(Invalid):
message = _("Action Plan %(action_plan)s is referenced by one or "
"multiple actions.")
"multiple actions")
class ActionNotFound(ResourceNotFound):
message = _("Action %(action)s could not be found.")
message = _("Action %(action)s could not be found")
class ActionAlreadyExists(Conflict):
message = _("An action with UUID %(uuid)s already exists.")
message = _("An action with UUID %(uuid)s already exists")
class ActionReferenced(Invalid):
message = _("Action plan %(action_plan)s is referenced by one or "
"multiple goals.")
"multiple goals")
class ActionFilterCombinationProhibited(Invalid):
message = _("Filtering actions on both audit and action-plan is "
"prohibited.")
"prohibited")
class HTTPNotFound(ResourceNotFound):
@@ -231,9 +229,8 @@ class PatchError(Invalid):
class BaseException(Exception):
def __init__(self, desc=""):
if (not isinstance(desc, basestring)):
raise IllegalArgumentException(
"Description must be an instance of str")
if (not isinstance(desc, six.string_types)):
raise ValueError(_("Description must be an instance of str"))
desc = desc.strip()
@@ -243,7 +240,7 @@ class BaseException(Exception):
return self._desc
def get_message(self):
return "An exception occurred without a description."
return _("An exception occurred without a description")
def __str__(self):
return self.get_message()
@@ -251,10 +248,8 @@ class BaseException(Exception):
class IllegalArgumentException(BaseException):
def __init__(self, desc):
BaseException.__init__(self, desc)
if self._desc == "":
raise IllegalArgumentException("Description cannot be empty")
desc = desc or _("Description cannot be empty")
super(IllegalArgumentException, self).__init__(desc)
def get_message(self):
return self._desc
@@ -262,10 +257,8 @@ class IllegalArgumentException(BaseException):
class NoSuchMetric(BaseException):
def __init__(self, desc):
BaseException.__init__(self, desc)
if self._desc == "":
raise NoSuchMetric("No such metric")
desc = desc or _("No such metric")
super(NoSuchMetric, self).__init__(desc)
def get_message(self):
return self._desc
@@ -273,10 +266,8 @@ class NoSuchMetric(BaseException):
class NoDataFound(BaseException):
def __init__(self, desc):
BaseException.__init__(self, desc)
if self._desc == "":
raise NoSuchMetric("no rows were returned")
desc = desc or _("No rows were returned")
super(NoDataFound, self).__init__(desc)
def get_message(self):
return self._desc
@@ -287,26 +278,26 @@ class KeystoneFailure(WatcherException):
class ClusterEmpty(WatcherException):
message = _("The list of hypervisor(s) in the cluster is empty.'")
message = _("The list of hypervisor(s) in the cluster is empty")
class MetricCollectorNotDefined(WatcherException):
message = _("The metrics resource collector is not defined.'")
message = _("The metrics resource collector is not defined")
class ClusteStateNotDefined(WatcherException):
class ClusterStateNotDefined(WatcherException):
message = _("the cluster state is not defined")
# Model
class VMNotFound(WatcherException):
message = _("The VM could not be found.")
message = _("The VM could not be found")
class HypervisorNotFound(WatcherException):
message = _("The hypervisor could not be found.")
message = _("The hypervisor could not be found")
class MetaActionNotFound(WatcherException):
message = _("The Meta-Action could not be found.")
message = _("The Meta-Action could not be found")

View File

@@ -17,16 +17,15 @@
# limitations under the License.
#
from oslo_config import cfg
from oslo_log import log
from urlparse import urljoin
from urlparse import urlparse
from keystoneclient.auth.identity import generic
from keystoneclient import session as keystone_session
from oslo_config import cfg
from oslo_log import log
from six.moves.urllib.parse import urljoin
from six.moves.urllib.parse import urlparse
from watcher.common.exception import KeystoneFailure
from watcher._i18n import _
from watcher.common import exception
LOG = log.getLogger(__name__)
@@ -54,16 +53,17 @@ class KeystoneClient(object):
self._token = None
def get_endpoint(self, **kwargs):
kc = self._get_ksclient()
kc = self.get_ksclient()
if not kc.has_service_catalog():
raise KeystoneFailure('No Keystone service catalog '
'loaded')
raise exception.KeystoneFailure(
_('No Keystone service catalog loaded')
)
attr = None
filter_value = None
if kwargs.get('region_name'):
attr = 'region'
filter_value = kwargs.get('region_name')
return self._get_ksclient().service_catalog.url_for(
return kc.service_catalog.url_for(
service_type=kwargs.get('service_type') or 'metering',
attr=attr,
filter_value=filter_value,
@@ -73,23 +73,25 @@ class KeystoneClient(object):
return auth_version == 'v3.0' or '/v3' in urlparse(auth_url).path
def get_keystone_url(self, auth_url, auth_version):
"""Gives an http/https url to contact keystone.
"""
"""Gives an http/https url to contact keystone."""
api_v3 = self._is_apiv3(auth_url, auth_version)
api_version = 'v3' if api_v3 else 'v2.0'
# NOTE(lucasagomes): Get rid of the trailing '/' otherwise urljoin()
# fails to override the version in the URL
return urljoin(auth_url.rstrip('/'), api_version)
def _get_ksclient(self):
"""Get an endpoint and auth token from Keystone.
"""
ks_args = self.get_credentials()
def get_ksclient(self, creds=None):
"""Get an endpoint and auth token from Keystone."""
auth_version = CONF.keystone_authtoken.auth_version
auth_url = CONF.keystone_authtoken.auth_uri
api_version = self._is_apiv3(auth_url, auth_version)
api_v3 = self._is_apiv3(auth_url, auth_version)
if creds is None:
ks_args = self._get_credentials(api_v3)
else:
ks_args = creds
if api_version:
if api_v3:
from keystoneclient.v3 import client
else:
from keystoneclient.v2_0 import client
@@ -99,17 +101,29 @@ class KeystoneClient(object):
return client.Client(**ks_args)
def get_credentials(self):
creds = \
{'auth_url': CONF.keystone_authtoken.auth_uri,
'username': CONF.keystone_authtoken.admin_user,
'password': CONF.keystone_authtoken.admin_password,
'project_name': CONF.keystone_authtoken.admin_tenant_name,
'user_domain_name': "default",
'project_domain_name': "default"}
def _get_credentials(self, api_v3):
if api_v3:
creds = \
{'auth_url': CONF.keystone_authtoken.auth_uri,
'username': CONF.keystone_authtoken.admin_user,
'password': CONF.keystone_authtoken.admin_password,
'project_name': CONF.keystone_authtoken.admin_tenant_name,
'user_domain_name': "default",
'project_domain_name': "default"}
else:
creds = \
{'auth_url': CONF.keystone_authtoken.auth_uri,
'username': CONF.keystone_authtoken.admin_user,
'password': CONF.keystone_authtoken.admin_password,
'tenant_name': CONF.keystone_authtoken.admin_tenant_name}
LOG.debug(creds)
return creds
def get_credentials(self):
api_v3 = self._is_apiv3(CONF.keystone_authtoken.auth_uri,
CONF.keystone_authtoken.auth_version)
return self._get_credentials(api_v3)
def get_session(self):
creds = self.get_credentials()
self._auth = generic.Password(**creds)

View File

@@ -29,20 +29,26 @@ class Event(object):
self._data = data
self._request_id = request_id
def get_type(self):
@property
def type(self):
return self._type
def set_type(self, type):
@type.setter
def type(self, type):
self._type = type
def get_data(self):
@property
def data(self):
return self._data
def set_data(self, data):
@data.setter
def data(self, data):
self._data = data
def set_request_id(self, id):
self._request_id = id
def get_request_id(self):
@property
def request_id(self):
return self._request_id
@request_id.setter
def request_id(self, id):
self._request_id = id

View File

@@ -39,7 +39,7 @@ class EventDispatcher(object):
return False
def dispatch_event(self, event):
LOG.debug("dispatch evt : %s" % str(event.get_type()))
LOG.debug("dispatch evt : %s" % str(event.type))
"""
Dispatch an instance of Event class
"""
@@ -49,8 +49,8 @@ class EventDispatcher(object):
listener(event)
# Dispatch the event to all the associated listeners
if event.get_type() in self._events.keys():
listeners = self._events[event.get_type()]
if event.type in self._events.keys():
listeners = self._events[event.type]
for listener in listeners:
listener(event)

View File

@@ -15,14 +15,15 @@
# limitations under the License.
import socket
import threading
import eventlet
from oslo_config import cfg
from oslo_log import log
import oslo_messaging as om
from threading import Thread
from watcher.common.rpc import JsonPayloadSerializer
from watcher.common.rpc import RequestContextSerializer
from watcher.common import rpc
from watcher._i18n import _LE, _LW
# NOTE:
# Ubuntu 14.04 forces librabbitmq when kombu is used
@@ -35,7 +36,7 @@ LOG = log.getLogger(__name__)
CONF = cfg.CONF
class MessagingHandler(Thread):
class MessagingHandler(threading.Thread):
def __init__(self, publisher_id, topic_watcher, endpoint, version,
serializer=None):
@@ -67,7 +68,7 @@ class MessagingHandler(Thread):
return self.__transport
def build_notifier(self):
serializer = RequestContextSerializer(JsonPayloadSerializer())
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
return om.Notifier(
self.__transport,
publisher_id=self.publisher_id,
@@ -93,11 +94,11 @@ class MessagingHandler(Thread):
)
self.__server = self.build_server(target)
else:
LOG.warn("you have no defined endpoint, "
"so you can only publish events")
LOG.warn(
_LW("No endpoint defined; can only publish events"))
except Exception as e:
LOG.exception(e)
LOG.error("configure : %s" % str(e.message))
LOG.error(_LE("Messaging configuration error"))
def run(self):
LOG.debug("configure MessagingHandler for %s" % self.topic_watcher)
@@ -107,7 +108,7 @@ class MessagingHandler(Thread):
self.__server.start()
def stop(self):
LOG.debug('Stop up server')
LOG.debug('Stopped server')
self.__server.wait()
self.__server.stop()

View File

@@ -25,15 +25,16 @@ from oslo_log import log
import cinderclient.exceptions as ciexceptions
import cinderclient.v2.client as ciclient
import glanceclient.v2.client as glclient
import keystoneclient.v3.client as ksclient
import neutronclient.neutron.client as netclient
import novaclient.client as nvclient
import novaclient.exceptions as nvexceptions
from watcher.common import keystone
LOG = log.getLogger(__name__)
class NovaWrapper(object):
class NovaClient(object):
NOVA_CLIENT_API_VERSION = "2"
def __init__(self, creds, session):
@@ -43,7 +44,7 @@ class NovaWrapper(object):
self.cinder = None
self.nova = nvclient.Client(self.NOVA_CLIENT_API_VERSION,
session=session)
self.keystone = ksclient.Client(**creds)
self.keystone = keystone.KeystoneClient().get_ksclient(creds)
self.glance = None
def get_hypervisors_list(self):
@@ -64,21 +65,21 @@ class NovaWrapper(object):
keep_original_image_name=True):
"""This method migrates a given instance
using an image of this instance and creating a new instance
from this image. It saves some configuration information
about the original instance : security group, list of networks
,list of attached volumes, floating IP, ...
in order to apply the same settings to the new instance.
At the end of the process the original instance is deleted.
It returns True if the migration was successful,
False otherwise.
using an image of this instance and creating a new instance
from this image. It saves some configuration information
about the original instance : security group, list of networks,
list of attached volumes, floating IP, ...
in order to apply the same settings to the new instance.
At the end of the process the original instance is deleted.
It returns True if the migration was successful,
False otherwise.
:param instance_id: the unique id of the instance to migrate.
:param keep_original_image_name: flag indicating whether the
image name from which the original instance was built must be
used as the name of the intermediate image used for migration.
If this flag is False, a temporary image name is built
"""
:param instance_id: the unique id of the instance to migrate.
:param keep_original_image_name: flag indicating whether the
image name from which the original instance was built must be
used as the name of the intermediate image used for migration.
If this flag is False, a temporary image name is built
"""
new_image_name = ""
@@ -275,12 +276,14 @@ class NovaWrapper(object):
return True
def built_in_non_live_migrate_instance(self, instance_id, hypervisor_id):
"""This method uses the Nova built-in non-live migrate()
action to migrate a given instance.
It returns True if the migration was successful, False otherwise.
"""This method does a live migration of a given instance
:param instance_id: the unique id of the instance to migrate.
"""
This method uses the Nova built-in non-live migrate()
action to migrate a given instance.
It returns True if the migration was successful, False otherwise.
:param instance_id: the unique id of the instance to migrate.
"""
LOG.debug(
"Trying a Nova built-in non-live "
@@ -326,15 +329,18 @@ class NovaWrapper(object):
def live_migrate_instance(self, instance_id, dest_hostname,
block_migration=True, retry=120):
"""This method uses the Nova built-in live_migrate()
action to do a live migration of a given instance.
It returns True if the migration was successful,
False otherwise.
"""This method does a live migration of a given instance
:param instance_id: the unique id of the instance to migrate.
:param dest_hostname: the name of the destination compute node.
:param block_migration: No shared storage is required.
"""
This method uses the Nova built-in live_migrate()
action to do a live migration of a given instance.
It returns True if the migration was successful,
False otherwise.
:param instance_id: the unique id of the instance to migrate.
:param dest_hostname: the name of the destination compute node.
:param block_migration: No shared storage is required.
"""
LOG.debug("Trying a live migrate of instance %s to host '%s'" % (
instance_id, dest_hostname))
@@ -429,16 +435,17 @@ class NovaWrapper(object):
def create_image_from_instance(self, instance_id, image_name,
metadata={"reason": "instance_migrate"}):
"""This method creates a new image from a given instance.
It waits for this image to be in 'active' state before returning.
It returns the unique UUID of the created image if successful,
None otherwise
:param instance_id: the uniqueid of
the instance to backup as an image.
:param image_name: the name of the image to create.
:param metadata: a dictionary containing the list of
key-value pairs to associate to the image as metadata.
"""
It waits for this image to be in 'active' state before returning.
It returns the unique UUID of the created image if successful,
None otherwise
:param instance_id: the uniqueid of
the instance to backup as an image.
:param image_name: the name of the image to create.
:param metadata: a dictionary containing the list of
key-value pairs to associate to the image as metadata.
"""
if self.glance is None:
glance_endpoint = self.keystone. \
service_catalog.url_for(service_type='image',
@@ -495,8 +502,8 @@ class NovaWrapper(object):
def delete_instance(self, instance_id):
"""This method deletes a given instance.
:param instance_id: the unique id of the instance to delete.
"""
:param instance_id: the unique id of the instance to delete.
"""
LOG.debug("Trying to remove instance %s ..." % instance_id)
@@ -513,8 +520,8 @@ class NovaWrapper(object):
def stop_instance(self, instance_id):
"""This method stops a given instance.
:param instance_id: the unique id of the instance to stop.
"""
:param instance_id: the unique id of the instance to stop.
"""
LOG.debug("Trying to stop instance %s ..." % instance_id)
@@ -533,14 +540,17 @@ class NovaWrapper(object):
return False
def wait_for_vm_state(self, server, vm_state, retry, sleep):
"""Waits for server to be in vm_state which can be one of the following :
active, stopped
"""Waits for server to be in a specific vm_state
The vm_state can be one of the following :
active, stopped
:param server: server object.
:param vm_state: for which state we are waiting for
:param retry: how many times to retry
:param sleep: seconds to sleep between the retries
"""
:param server: server object.
:param vm_state: for which state we are waiting for
:param retry: how many times to retry
:param sleep: seconds to sleep between the retries
"""
if not server:
return False
@@ -551,15 +561,18 @@ class NovaWrapper(object):
return getattr(server, 'OS-EXT-STS:vm_state') == vm_state
def wait_for_instance_status(self, instance, status_list, retry, sleep):
"""Waits for instance to be in status which can be one of the following
: BUILD, ACTIVE, ERROR, VERIFY_RESIZE, SHUTOFF
"""Waits for instance to be in a specific status
The status can be one of the following
: BUILD, ACTIVE, ERROR, VERIFY_RESIZE, SHUTOFF
:param instance: instance object.
:param status_list: tuple containing the list of
status we are waiting for
:param retry: how many times to retry
:param sleep: seconds to sleep between the retries
"""
:param instance: instance object.
:param status_list: tuple containing the list of
status we are waiting for
:param retry: how many times to retry
:param sleep: seconds to sleep between the retries
"""
if not instance:
return False
@@ -577,11 +590,12 @@ class NovaWrapper(object):
network_names_list=["demo-net"], keypair_name="mykeys",
create_new_floating_ip=True,
block_device_mapping_v2=None):
"""This method creates a new instance.
It also creates, if requested, a new floating IP and associates
it with the new instance
It returns the unique id of the created instance.
"""
"""This method creates a new instance
It also creates, if requested, a new floating IP and associates
it with the new instance
It returns the unique id of the created instance.
"""
LOG.debug(
"Trying to create new instance '%s' "

View File

@@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
import signal
import socket
@@ -24,10 +25,10 @@ import oslo_messaging as messaging
from oslo_service import service
from oslo_utils import importutils
from watcher._i18n import _LE
from watcher._i18n import _LI
from watcher.common import config
from watcher.common import context
from watcher.common.i18n import _LE
from watcher.common.i18n import _LI
from watcher.common import rpc
from watcher.objects import base as objects_base
@@ -124,9 +125,10 @@ _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
'glanceclient=WARN', 'watcher.openstack.common=WARN']
def prepare_service(argv=[]):
log.register_options(cfg.CONF)
def prepare_service(argv=[], conf=cfg.CONF):
log.register_options(conf)
config.parse_args(argv)
cfg.set_defaults(_options.log_opts,
default_log_levels=_DEFAULT_LOG_LEVELS)
log.setup(cfg.CONF, 'python-watcher')
log.setup(conf, 'python-watcher')
conf.log_opt_values(LOG, logging.DEBUG)

View File

@@ -24,7 +24,7 @@ import six
import uuid
from watcher.common.i18n import _LW
from watcher._i18n import _LW
UTILS_OPTS = [
cfg.StrOpt('rootwrap_config',

View File

@@ -1,3 +1,9 @@
..
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/
Tempest Field Guide to Infrastructure Optimization API tests
============================================================

View File

@@ -93,14 +93,13 @@ class BaseInfraOptimTest(test.BaseTestCase):
@classmethod
@creates('audit_template')
def create_audit_template(cls, description=None, expect_errors=False):
"""
Wrapper utility for creating test audit_template.
"""Wrapper utility for creating test audit_template.
:param description: A description of the audit template.
if not supplied, a random value will be generated.
:return: Created audit template.
"""
description = description or data_utils.rand_name(
'test-audit_template')
resp, body = cls.client.create_audit_template(description=description)
@@ -108,12 +107,10 @@ class BaseInfraOptimTest(test.BaseTestCase):
@classmethod
def delete_audit_template(cls, audit_template_id):
"""
Deletes a audit_template having the specified UUID.
"""Deletes a audit_template having the specified UUID.
:param uuid: The unique identifier of the audit_template.
:return: Server response.
"""
resp, body = cls.client.delete_audit_template(audit_template_id)

View File

@@ -11,6 +11,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from tempest_lib import exceptions as lib_exc
from tempest.api.infra_optim.admin import base
@@ -27,7 +29,7 @@ class TestAuditTemplate(base.BaseInfraOptimTest):
def _assertExpected(self, expected, actual):
# Check if not expected keys/values exists in actual response body
for key, value in expected.iteritems():
for key, value in six.iteritems(expected):
if key not in ('created_at', 'updated_at', 'deleted_at'):
self.assertIn(key, actual)
self.assertEqual(value, actual[key])

View File

@@ -1,3 +1,9 @@
..
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/
.. _cli_field_guide:
Tempest Field Guide to CLI tests

View File

@@ -40,10 +40,7 @@ def handle_errors(f):
class InfraOptimClient(service_client.ServiceClient):
"""
Base Tempest REST client for Watcher API.
"""
"""Base Tempest REST client for Watcher API."""
uri_prefix = ''
@@ -58,14 +55,13 @@ class InfraOptimClient(service_client.ServiceClient):
return json.loads(object_str)
def _get_uri(self, resource_name, uuid=None, permanent=False):
"""
Get URI for a specific resource or object.
"""Get URI for a specific resource or object.
:param resource_name: The name of the REST resource, e.g., 'audits'.
:param uuid: The unique identifier of an object in UUID format.
:return: Relative URI for the resource or object.
"""
prefix = self.uri_prefix if not permanent else ''
return '{pref}/{res}{uuid}'.format(pref=prefix,
@@ -73,16 +69,15 @@ class InfraOptimClient(service_client.ServiceClient):
uuid='/%s' % uuid if uuid else '')
def _make_patch(self, allowed_attributes, **kw):
"""
Create a JSON patch according to RFC 6902.
"""Create a JSON patch according to RFC 6902.
:param allowed_attributes: An iterable object that contains a set of
allowed attributes for an object.
:param **kw: Attributes and new values for them.
:return: A JSON path that sets values of the specified attributes to
the new ones.
"""
def get_change(kw, path='/'):
for name, value in six.iteritems(kw):
if isinstance(value, dict):
@@ -103,15 +98,14 @@ class InfraOptimClient(service_client.ServiceClient):
return patch
def _list_request(self, resource, permanent=False, **kwargs):
"""
Get the list of objects of the specified type.
"""Get the list of objects of the specified type.
:param resource: The name of the REST resource, e.g., 'audits'.
"param **kw: Parameters for the request.
:return: A tuple with the server response and deserialized JSON list
of objects
"""
uri = self._get_uri(resource, permanent=permanent)
if kwargs:
uri += "?%s" % urllib.urlencode(kwargs)
@@ -122,13 +116,12 @@ class InfraOptimClient(service_client.ServiceClient):
return resp, self.deserialize(body)
def _show_request(self, resource, uuid, permanent=False, **kwargs):
"""
Gets a specific object of the specified type.
"""Gets a specific object of the specified type.
:param uuid: Unique identifier of the object in UUID format.
:return: Serialized object as a dictionary.
"""
if 'uri' in kwargs:
uri = kwargs['uri']
else:
@@ -139,16 +132,15 @@ class InfraOptimClient(service_client.ServiceClient):
return resp, self.deserialize(body)
def _create_request(self, resource, object_dict):
"""
Create an object of the specified type.
"""Create an object of the specified type.
:param resource: The name of the REST resource, e.g., 'audits'.
:param object_dict: A Python dict that represents an object of the
specified type.
:return: A tuple with the server response and the deserialized created
object.
"""
body = self.serialize(object_dict)
uri = self._get_uri(resource)
@@ -158,14 +150,13 @@ class InfraOptimClient(service_client.ServiceClient):
return resp, self.deserialize(body)
def _delete_request(self, resource, uuid):
"""
Delete specified object.
"""Delete specified object.
:param resource: The name of the REST resource, e.g., 'audits'.
:param uuid: The unique identifier of an object in UUID format.
:return: A tuple with the server response and the response body.
"""
uri = self._get_uri(resource, uuid)
resp, body = self.delete(uri)
@@ -173,15 +164,14 @@ class InfraOptimClient(service_client.ServiceClient):
return resp, body
def _patch_request(self, resource, uuid, patch_object):
"""
Update specified object with JSON-patch.
"""Update specified object with JSON-patch.
:param resource: The name of the REST resource, e.g., 'audits'.
:param uuid: The unique identifier of an object in UUID format.
:return: A tuple with the server response and the serialized patched
object.
"""
uri = self._get_uri(resource, uuid)
patch_body = json.dumps(patch_object)
@@ -197,20 +187,17 @@ class InfraOptimClient(service_client.ServiceClient):
@handle_errors
def get_version_description(self, version='v1'):
"""
Retrieves the description of the API.
"""Retrieves the description of the API.
:param version: The version of the API. Default: 'v1'.
:return: Serialized description of API resources.
"""
return self._list_request(version, permanent=True)
def _put_request(self, resource, put_object):
"""
Update specified object with JSON-patch.
"""Update specified object with JSON-patch."""
"""
uri = self._get_uri(resource)
put_body = json.dumps(put_object)

View File

@@ -14,9 +14,7 @@ from tempest.services.infra_optim import base
class InfraOptimClientJSON(base.InfraOptimClient):
"""
Base Tempest REST client for Watcher API v1.
"""
"""Base Tempest REST client for Watcher API v1."""
version = '1'
uri_prefix = 'v1'
@@ -40,45 +38,41 @@ class InfraOptimClientJSON(base.InfraOptimClient):
@base.handle_errors
def show_audit_template(self, uuid):
"""
Gets a specific audit template.
"""Gets a specific audit template.
:param uuid: Unique identifier of the audit template in UUID format.
:return: Serialized audit template as a dictionary.
"""
return self._show_request('audit_templates', uuid)
@base.handle_errors
def show_audit_template_by_host_agregate(self, host_agregate_id):
"""
Gets an audit template associated with given host agregate ID.
"""Gets an audit template associated with given host agregate ID.
:param uuid: Unique identifier of the audit_template in UUID format.
:return: Serialized audit_template as a dictionary.
"""
uri = '/audit_templates/detail?host_agregate=%s' % host_agregate_id
return self._show_request('audit_templates', uuid=None, uri=uri)
@base.handle_errors
def show_audit_template_by_goal(self, goal):
"""
Gets an audit template associated with given goal.
"""Gets an audit template associated with given goal.
:param uuid: Unique identifier of the audit_template in UUID format.
:return: Serialized audit_template as a dictionary.
"""
uri = '/audit_templates/detail?goal=%s' % goal
return self._show_request('audit_templates', uuid=None, uri=uri)
@base.handle_errors
def create_audit_template(self, **kwargs):
"""
Creates an audit template with the specified parameters.
"""Creates an audit template with the specified parameters.
:param name: The name of the audit template. Default: My Audit Template
:param description: The description of the audit template.
@@ -91,8 +85,8 @@ class InfraOptimClientJSON(base.InfraOptimClient):
Default: {}
:return: A tuple with the server response and the created audit
template.
"""
audit_template = {
'name': kwargs.get('name', 'My Audit Template'),
'description': kwargs.get('description', 'AT Description'),
@@ -127,25 +121,22 @@ class InfraOptimClientJSON(base.InfraOptimClient):
@base.handle_errors
def delete_audit_template(self, uuid):
"""
Deletes an audit template having the specified UUID.
"""Deletes an audit template having the specified UUID.
:param uuid: The unique identifier of the audit template.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('audit_templates', uuid)
@base.handle_errors
def update_audit_template(self, uuid, patch):
"""
Update the specified audit template.
"""Update the specified audit template.
:param uuid: The unique identifier of the audit template.
:param patch: List of dicts representing json patches.
:return: A tuple with the server response and the updated audit
template.
"""
return self._patch_request('audit_templates', uuid, patch)

View File

@@ -1,15 +0,0 @@
# Watcher Database
This database stores all the watcher business objects which can be requested by the Watcher API :
* Audit templates
* Audits
* Action plans
* Actions history
* Watcher settings :
* metrics/events collector endpoints for each type of metric
* manual/automatic mode
* Business Objects states
It may be any relational database or a key-value database.
Business objects are read/created/updated/deleted from/to the Watcher database using a common Python package which provides a high-level Service API.

View File

@@ -16,12 +16,10 @@ Base classes for storage engines
"""
import abc
from oslo_config import cfg
from oslo_db import api as db_api
import six
_BACKEND_MAPPING = {'sqlalchemy': 'watcher.db.sqlalchemy.api'}
IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING,
lazy=True)
@@ -33,13 +31,9 @@ def get_instance():
@six.add_metaclass(abc.ABCMeta)
class Connection(object):
class BaseConnection(object):
"""Base class for storage system connections."""
@abc.abstractmethod
def __init__(self):
"""Constructor."""
@abc.abstractmethod
def get_audit_template_list(self, context, columns=None, filters=None,
limit=None, marker=None, sort_key=None,
@@ -130,6 +124,7 @@ class Connection(object):
:raises: AuditTemplateNotFound
:raises: InvalidParameterValue
"""
@abc.abstractmethod
def soft_delete_audit_template(self, audit_template_id):
"""Soft delete an audit_template.
@@ -308,7 +303,7 @@ class Connection(object):
@abc.abstractmethod
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):
"""Get specific columns for matching action plans.

View File

@@ -1,90 +0,0 @@
"""Initial revision
Revision ID: 414bf1d36e7d
Revises: None
Create Date: 2015-04-08 15:05:50.942578
"""
# revision identifiers, used by Alembic.
revision = '414bf1d36e7d'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('audit_templates',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=63), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('host_aggregate', sa.Integer(), nullable=True),
sa.Column('goal', sa.String(length=63), nullable=True),
sa.Column('extra', watcher.db.sqlalchemy.models.JSONEncodedDict(), nullable=True),
sa.Column('version', sa.String(length=15), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', name='uniq_audit_templates0name'),
sa.UniqueConstraint('uuid', name='uniq_audit_templates0uuid')
)
op.create_table('audits',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('type', sa.String(length=20), nullable=True),
sa.Column('state', sa.String(length=20), nullable=True),
sa.Column('deadline', sa.DateTime(), nullable=True),
sa.Column('audit_template_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['audit_template_id'], ['audit_templates.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_audits0uuid')
)
op.create_table('action_plans',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('first_action_id', sa.Integer(), nullable=True),
sa.Column('audit_id', sa.Integer(), nullable=True),
sa.Column('state', sa.String(length=20), nullable=True),
sa.ForeignKeyConstraint(['audit_id'], ['audits.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_action_plans0uuid')
)
op.create_table('actions',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('action_plan_id', sa.Integer(), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('state', sa.String(length=20), nullable=True),
sa.Column('alarm', sa.String(length=36), nullable=True),
sa.Column('next', sa.String(length=36), nullable=True),
sa.ForeignKeyConstraint(['action_plan_id'], ['action_plans.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_actions0uuid')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('actions')
op.drop_table('action_plans')
op.drop_table('audits')
op.drop_table('audit_templates')
### end Alembic commands ###

View File

@@ -22,19 +22,18 @@ from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import session as db_session
from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log
from sqlalchemy.orm.exc import MultipleResultsFound
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm import exc
from watcher import _i18n
from watcher.common import exception
from watcher.common import utils
from watcher.db import api
from watcher.db.sqlalchemy import models
from watcher import i18n
from watcher.objects.audit import AuditStatus
from watcher.objects import audit as audit_objects
CONF = cfg.CONF
LOG = log.getLogger(__name__)
_ = i18n._
_ = _i18n._
_FACADE = None
@@ -102,7 +101,7 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None,
return query.all()
class Connection(api.Connection):
class Connection(api.BaseConnection):
"""SqlAlchemy connection."""
def __init__(self):
@@ -223,7 +222,7 @@ class Connection(api.Connection):
raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
return audit_template
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
@@ -238,7 +237,7 @@ class Connection(api.Connection):
raise exception.AuditTemplateNotFound(
audit_template=audit_template_uuid)
return audit_template
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_uuid)
@@ -252,11 +251,11 @@ class Connection(api.Connection):
raise exception.AuditTemplateNotFound(
audit_template=audit_template_name)
return audit_template
except MultipleResultsFound:
except exc.MultipleResultsFound:
raise exception.Conflict(
'Multiple audit templates exist with same name.'
' Please use the audit template uuid instead.')
except NoResultFound:
_('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)
@@ -268,7 +267,7 @@ class Connection(api.Connection):
try:
query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(node=audit_template_id)
query.delete()
@@ -287,7 +286,7 @@ class Connection(api.Connection):
query = add_identity_filter(query, audit_template_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
@@ -302,7 +301,7 @@ class Connection(api.Connection):
try:
query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(node=audit_template_id)
query.soft_delete()
@@ -323,7 +322,7 @@ class Connection(api.Connection):
values['uuid'] = utils.generate_uuid()
if values.get('state') is None:
values['state'] = AuditStatus.PENDING
values['state'] = audit_objects.AuditStatus.PENDING
audit = models.Audit()
audit.update(values)
@@ -343,7 +342,7 @@ class Connection(api.Connection):
if audit.state == 'DELETED':
raise exception.AuditNotFound(audit=audit_id)
return audit
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditNotFound(audit=audit_id)
def get_audit_by_uuid(self, context, audit_uuid):
@@ -356,7 +355,7 @@ class Connection(api.Connection):
if audit.state == 'DELETED':
raise exception.AuditNotFound(audit=audit_uuid)
return audit
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditNotFound(audit=audit_uuid)
def destroy_audit(self, audit_id):
@@ -374,7 +373,7 @@ class Connection(api.Connection):
try:
audit_ref = query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditNotFound(audit=audit_id)
if is_audit_referenced(session, audit_ref['id']):
@@ -396,7 +395,7 @@ class Connection(api.Connection):
query = add_identity_filter(query, audit_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditNotFound(audit=audit_id)
ref.update(values)
@@ -410,7 +409,7 @@ class Connection(api.Connection):
try:
query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.AuditNotFound(node=audit_id)
query.soft_delete()
@@ -447,7 +446,7 @@ class Connection(api.Connection):
raise exception.ActionNotFound(
action=action_id)
return action
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionNotFound(action=action_id)
def get_action_by_uuid(self, context, action_uuid):
@@ -460,7 +459,7 @@ class Connection(api.Connection):
raise exception.ActionNotFound(
action=action_uuid)
return action
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionNotFound(action=action_uuid)
def destroy_action(self, action_id):
@@ -487,7 +486,7 @@ class Connection(api.Connection):
query = add_identity_filter(query, action_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionNotFound(action=action_id)
ref.update(values)
@@ -501,7 +500,7 @@ class Connection(api.Connection):
try:
query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionNotFound(node=action_id)
query.soft_delete()
@@ -541,7 +540,7 @@ class Connection(api.Connection):
raise exception.ActionPlanNotFound(
action_plan=action_plan_id)
return action_plan
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
def get_action_plan_by_uuid(self, context, action_plan__uuid):
@@ -555,7 +554,7 @@ class Connection(api.Connection):
raise exception.ActionPlanNotFound(
action_plan=action_plan__uuid)
return action_plan
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan__uuid)
def destroy_action_plan(self, action_plan_id):
@@ -573,7 +572,7 @@ class Connection(api.Connection):
try:
action_plan_ref = query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
if is_action_plan_referenced(session, action_plan_ref['id']):
@@ -596,7 +595,7 @@ class Connection(api.Connection):
query = add_identity_filter(query, action_plan_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
ref.update(values)
@@ -610,7 +609,7 @@ class Connection(api.Connection):
try:
query.one()
except NoResultFound:
except exc.NoResultFound:
raise exception.ActionPlanNotFound(node=action_plan_id)
query.soft_delete()

View File

@@ -21,6 +21,7 @@ from alembic import config as alembic_config
import alembic.migration as alembic_migration
from oslo_db import exception as db_exc
from watcher._i18n import _
from watcher.db.sqlalchemy import api as sqla_api
from watcher.db.sqlalchemy import models
@@ -68,8 +69,9 @@ def create_schema(config=None, engine=None):
# schema, it will only add the new tables, but leave
# existing as is. So we should avoid of this situation.
if version(engine=engine) is not None:
raise db_exc.DbMigrationError("DB schema is already under version"
" control. Use upgrade() instead")
raise db_exc.DbMigrationError(
_("Watcher database schema is already under version control; "
"use upgrade() instead"))
models.Base.metadata.create_all(engine)
stamp('head', config=config)

View File

@@ -33,14 +33,14 @@ from sqlalchemy.types import TypeDecorator, TEXT
from watcher.common import paths
sql_opts = [
cfg.StrOpt('mysql_engine',
default='InnoDB',
help='MySQL engine to use.')
]
_DEFAULT_SQL_CONNECTION = 'sqlite:///' + paths.state_path_def('watcher.sqlite')
_DEFAULT_SQL_CONNECTION = 'sqlite:///{0}'.format(
paths.state_path_def('watcher.sqlite'))
cfg.CONF.register_opts(sql_opts, 'database')
db_options.set_defaults(cfg.CONF, _DEFAULT_SQL_CONNECTION, 'watcher.sqlite')

View File

@@ -1,63 +0,0 @@
# Watcher Decision Engine
This component is responsible for computing a list of potential optimization actions in order to fulfill the goals of an audit.
It uses the following input data :
* current, previous and predicted state of the cluster (hosts, instances, network, ...)
* evolution of metrics within a time frame
It first selects the most appropriate optimization strategy depending on several factors :
* the optimization goals that must be fulfilled (servers consolidation, energy consumption, license optimization, ...)
* the deadline that was provided by the Openstack cluster admin for delivering an action plan
* the "aggressivity" level regarding potential optimization actions :
* is it allowed to do a lot of instance migrations ?
* is it allowed to consume a lot of bandwidth on the admin network ?
* is it allowed to violate initial placement constraint such as affinity/anti-affinity, region, ... ?
The strategy is then executed and generates a list of Meta-Actions in order to fulfill the goals of the Audit.
A Meta-Action is a generic optimization task which is independent from the target cluster implementation (Openstack, ...). For example, an instance migration is a Meta-Action which corresponds, in the Openstack context, to a set of technical actions on the Nova, Cinder and Neutron components.
Using Meta-Actions instead of technical actions brings two advantages in Watcher :
* a loose coupling between the Watcher Decision Engine and the Watcher Applier
* a simplification of the optimization algorithms which don't need to know the underlying technical cluster implementation
Beyond that, the Meta-Actions which are computed by the optimization strategy are not necessarily ordered in time (it depends on the selected Strategy). Therefore, the Actions Planner module of Decision Engine reorganizes the list of Meta-Actions into an ordered sequence of technical actions (migrations, ...) such that all security, dependency, and performance requirements are met. An ordered sequence of technical actions is called an "Action Plan".
The Decision Engine saves the generated Action Plan in the Watcher Database. This Action Plan is loaded later by the Watcher Actions Applier.
Like every Watcher component, the Decision Engine notifies its current status (learning phase, current status of each Audit, ...) on the message/notification bus.
## Watcher Compute Node Profiler
This module of the Decision Engine is responsible for profiling a new compute node. When a new compute node is added to the cluster, it automatically triggers test scripts in order to extract profiling information such as :
* the maximum I/O available on each disk
* the evolution of energy consumption for a given workload
It stores those information in the Watcher database. They may be used by any optimization strategy that needs to rely on real metrics about a given physical machine and not only theoretical metrics.
## Watcher Metrics Predictor
This module of the Decision Engine is able to compute some predicted metric values according to previously acquired metrics.
For instance, it may be able to predict the future CPU in the next 5 minutes for a given instance given the previous CPU load during the last 2 hours (relying on some neural network algorithm or any other machine learning system).
This component pushes the new predicted metrics to the CEP in order to trigger new actions if needed.
## Watcher Cluster State Collector
This module of the Decision Engine provides a high level API for requesting status information from the Nova API.
A DSL will be provided in order to ease the development of new optimization strategies.
Example of high level requests that may be provided :
* get the difference between current cluster state and cluster state yesterday at the same time
* get the state evolution in time of a group of instances from 9 AM to 10 AM for every day of the week
* ...
## Watcher Actions Planner
This module of the Decision Engine translates Meta-Actions into technical actions on the Openstack modules (Nova, Cinder, ...) and builds an appropriate workflow which defines how-to schedule in time those different technical actions and for each action what are the pre-requisite conditions.
Today, the Action Plan is just a simple chain of sequential actions but in later versions, we intend to rely on more complex workflow models description formats, such as [BPMN 2.0](http://www.bpmn.org/), which enable a complete definition of activity diagrams containing sequential and parallel tasks.

View File

@@ -17,13 +17,13 @@
# limitations under the License.
#
import abc
import six
from watcher.decision_engine.strategy.level import StrategyLevel
import six
from watcher.decision_engine.strategy.common.level import StrategyLevel
@six.add_metaclass(abc.ABCMeta)
class MetaAction(object):
class BaseAction(object):
def __init__(self):
self._level = StrategyLevel.conservative
self._priority = 0
@@ -43,6 +43,3 @@ class MetaAction(object):
@priority.setter
def priority(self, p):
self._priority = p
def __str__(self):
return " "

View File

@@ -16,18 +16,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from watcher.decision_engine.meta_action.base import MetaAction
from watcher.decision_engine.actions.base import BaseAction
from watcher.decision_engine.model.hypervisor_state import HypervisorState
class ChangeHypervisorState(MetaAction):
class ChangeHypervisorState(BaseAction):
def __init__(self, target):
MetaAction.__init__(self)
'''The target host to change the power
'''The target host to change the state
:param target:
:return:
:param target: the target hypervisor uuid
'''
super(ChangeHypervisorState, self).__init__()
self._target = target
self._state = HypervisorState.ONLINE
@@ -48,5 +48,5 @@ class ChangeHypervisorState(MetaAction):
self._target = p
def __str__(self):
return "{0} {1} ChangeHypervisorState => {2}".format(
MetaAction.__str__(self), self.target, self.state)
return "{} ChangeHypervisorState => {}".format(self.target,
self.state)

View File

@@ -0,0 +1,72 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from enum import Enum
from watcher.decision_engine.actions.base import BaseAction
class MigrationType(Enum):
# Total migration time and downtime depend on memory dirtying speed
pre_copy = 0
# Postcopy transfer a page only once reliability
post_copy = 1
class Migrate(BaseAction):
def __init__(self, vm, src_hypervisor, dest_hypervisor):
"""Request to migrate a virtual machine from a host to another
:param vm: the virtual machine uuid to migrate
:param src_hypervisor: uuid
:param dest_hypervisor: uuid
"""
super(Migrate, self).__init__()
self._reserved_disk_iops = 0
self._remaining_dirty_pages = 0
self._vm = vm
self._migration_type = MigrationType.pre_copy
self._src_hypervisor = src_hypervisor
self._dest_hypervisor = dest_hypervisor
@property
def migration_type(self):
return self._migration_type
@migration_type.setter
def migration_type(self, type):
self._migration_type = type
@property
def vm(self):
return self._vm
@property
def src_hypervisor(self):
return self._src_hypervisor
@property
def dest_hypervisor(self):
return self._dest_hypervisor
def __str__(self):
return "Migrate {} from {} to {}".format(
self.vm,
self.src_hypervisor,
self.dest_hypervisor)

View File

@@ -17,12 +17,10 @@
# limitations under the License.
#
from enum import Enum
from watcher.decision_engine.actions.base import BaseAction
class StrategyState(Enum):
INIT = 1,
READY = 2,
RUNNING = 3,
TERMINATED = 4,
ERROR = 5
class Nop(BaseAction):
def __str__(self):
return "Nop"

View File

@@ -16,18 +16,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from watcher.decision_engine.meta_action.base import MetaAction
from watcher.decision_engine.actions.base import BaseAction
from watcher.decision_engine.model.power_state import PowerState
class ChangePowerState(MetaAction):
class ChangePowerState(BaseAction):
def __init__(self, target):
MetaAction.__init__(self)
"""The target host to change the power
:param target:
:return:
"""
super(ChangePowerState, self).__init__()
self._target = target
self._power_state = PowerState.g0
@@ -44,10 +44,9 @@ class ChangePowerState(MetaAction):
return self._target
@target.setter
def target(self, p):
self._target = p
def target(self, t):
self._target = t
def __str__(self):
return "{0} ChangePowerState {1} => {2} ".format(
MetaAction.__str__(self),
self.target, self.powerstate)
return "ChangePowerState {} => {} ".format(self.target,
self.powerstate)

View File

@@ -21,8 +21,7 @@ import six
@six.add_metaclass(abc.ABCMeta)
class BaseDecisionEngineCommand(object):
class BaseAuditHandler(object):
@abc.abstractmethod
def execute(self):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()

View File

@@ -0,0 +1,85 @@
# -*- 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_log import log
from watcher.common.messaging.events.event import Event
from watcher.decision_engine.audit.base import \
BaseAuditHandler
from watcher.decision_engine.messaging.events import Events
from watcher.decision_engine.planner.default import DefaultPlanner
from watcher.decision_engine.strategy.context.default import \
DefaultStrategyContext
from watcher.objects.audit import Audit
from watcher.objects.audit import AuditStatus
LOG = log.getLogger(__name__)
class DefaultAuditHandler(BaseAuditHandler):
def __init__(self, messaging):
super(DefaultAuditHandler, self).__init__()
self._messaging = messaging
self._strategy_context = DefaultStrategyContext()
@property
def messaging(self):
return self._messaging
@property
def strategy_context(self):
return self._strategy_context
def notify(self, audit_uuid, event_type, status):
event = Event()
event.type = event_type
event.data = {}
payload = {'audit_uuid': audit_uuid,
'audit_status': status}
self.messaging.topic_status.publish_event(event.type.name,
payload)
def update_audit_state(self, request_context, audit_uuid, state):
LOG.debug("Update audit state:{0} ".format(state))
audit = Audit.get_by_uuid(request_context, audit_uuid)
audit.state = state
audit.save()
self.notify(audit_uuid, Events.TRIGGER_AUDIT, state)
return audit
def execute(self, audit_uuid, request_context):
try:
LOG.debug("Trigger audit %s" % audit_uuid)
# change state of the audit to ONGOING
audit = self.update_audit_state(request_context, audit_uuid,
AuditStatus.ONGOING)
# execute the strategy
solution = self.strategy_context.execute_strategy(audit_uuid,
request_context)
# schedule the actions and create in the watcher db the ActionPlan
planner = DefaultPlanner()
planner.schedule(request_context, audit.id, solution)
# change state of the audit to SUCCEEDED
self.update_audit_state(request_context, audit_uuid,
AuditStatus.SUCCEEDED)
except Exception as e:
LOG.exception(e)
self.update_audit_state(request_context, audit_uuid,
AuditStatus.FAILED)

View File

@@ -1,83 +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_log import log
from watcher.common.messaging.events.event import Event
from watcher.decision_engine.messaging.command.base import \
BaseDecisionEngineCommand
from watcher.decision_engine.messaging.events import Events
from watcher.decision_engine.planner.default import DefaultPlanner
from watcher.decision_engine.strategy.context.default import StrategyContext
from watcher.objects.audit import Audit
from watcher.objects.audit import AuditStatus
from watcher.objects.audit_template import AuditTemplate
LOG = log.getLogger(__name__)
class TriggerAuditCommand(BaseDecisionEngineCommand):
def __init__(self, messaging, model_collector):
self.messaging = messaging
self.model_collector = model_collector
self.strategy_context = StrategyContext()
def notify(self, audit_uuid, event_type, status):
event = Event()
event.set_type(event_type)
event.set_data({})
payload = {'audit_uuid': audit_uuid,
'audit_status': status}
self.messaging.topic_status.publish_event(event.get_type().name,
payload)
def update_audit(self, request_context, audit_uuid, state):
LOG.debug("update audit {0} ".format(state))
audit = Audit.get_by_uuid(request_context, audit_uuid)
audit.state = state
audit.save()
self.notify(audit_uuid, Events.TRIGGER_AUDIT, state)
return audit
def execute(self, audit_uuid, request_context):
try:
LOG.debug("Execute TriggerAuditCommand ")
# 1 - change status to ONGOING
audit = self.update_audit(request_context, audit_uuid,
AuditStatus.ONGOING)
# 3 - Retrieve cluster-data-model
cluster = self.model_collector.get_latest_cluster_data_model()
# 4 - Select appropriate strategy
audit_template = AuditTemplate.get_by_id(request_context,
audit.audit_template_id)
self.strategy_context.set_goal(audit_template.goal)
# 5 - compute change requests
solution = self.strategy_context.execute_strategy(cluster)
# 6 - create an action plan
planner = DefaultPlanner()
planner.schedule(request_context, audit.id, solution)
# 7 - change status to SUCCEEDED and notify
self.update_audit(request_context, audit_uuid,
AuditStatus.SUCCEEDED)
except Exception as e:
self.update_audit(request_context, audit_uuid, AuditStatus.FAILED)
LOG.error("Execute audit command {0} ".format(unicode(e)))

View File

@@ -1,27 +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.
class EventConsumerFactory(object):
def factory(self, type):
"""Factory so as to create
:param type:
:return:
"""
# return eval(type + "()")
raise AssertionError()

View File

@@ -16,17 +16,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from concurrent.futures import ThreadPoolExecutor
from oslo_config import cfg
from oslo_log import log
from watcher.common.messaging.messaging_core import MessagingCore
from watcher.common.messaging.notification_handler import NotificationHandler
from watcher.decision_engine.event.consumer_factory import EventConsumerFactory
from watcher.decision_engine.messaging.audit_endpoint import AuditEndpoint
from watcher.decision_engine.messaging.events import Events
from watcher.decision_engine.strategy.context.default import StrategyContext
LOG = log.getLogger(__name__)
CONF = cfg.CONF
@@ -47,49 +43,33 @@ WATCHER_DECISION_ENGINE_OPTS = [
cfg.StrOpt('publisher_id',
default='watcher.decision.api',
help='The identifier used by watcher '
'module on the message broker')
'module on the message broker'),
cfg.IntOpt('max_workers',
default=2,
required=True,
help='The maximum number of threads that can be used to '
'execute strategies',
),
]
decision_engine_opt_group = cfg.OptGroup(
name='watcher_decision_engine',
title='Defines the parameters of the module decision engine')
decision_engine_opt_group = cfg.OptGroup(name='watcher_decision_engine',
title='Defines the parameters of '
'the module decision engine')
CONF.register_group(decision_engine_opt_group)
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
class DecisionEngineManager(MessagingCore):
def __init__(self):
super(DecisionEngineManager, self).__init__(
CONF.watcher_decision_engine.publisher_id,
CONF.watcher_decision_engine.topic_control,
CONF.watcher_decision_engine.topic_status,
api_version=self.API_VERSION,
)
self.handler = NotificationHandler(self.publisher_id)
self.handler.register_observer(self)
self.add_event_listener(Events.ALL, self.event_receive)
# todo(jed) oslo_conf
self.executor = ThreadPoolExecutor(max_workers=2)
self.topic_control.add_endpoint(AuditEndpoint(self))
self.context = StrategyContext(self)
api_version=self.API_VERSION)
endpoint = AuditEndpoint(self,
max_workers=CONF.watcher_decision_engine.
max_workers)
self.topic_control.add_endpoint(endpoint)
def join(self):
self.topic_control.join()
self.topic_status.join()
# TODO(ebe): Producer / consumer
def event_receive(self, event):
try:
request_id = event.get_request_id()
event_type = event.get_type()
data = event.get_data()
LOG.debug("request id => %s" % event.get_request_id())
LOG.debug("type_event => %s" % str(event.get_type()))
LOG.debug("data => %s" % str(data))
event_consumer = EventConsumerFactory().factory(event_type)
event_consumer.messaging = self
event_consumer.execute(request_id, data)
except Exception as e:
LOG.error("evt %s" % e.message)
raise e

View File

@@ -16,29 +16,35 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from concurrent.futures import ThreadPoolExecutor
from oslo_log import log
from watcher.decision_engine.command.audit import TriggerAuditCommand
from watcher.metrics_engine.cluster_model_collector.manager import \
CollectorManager
from watcher.decision_engine.audit.default import DefaultAuditHandler
LOG = log.getLogger(__name__)
class AuditEndpoint(object):
def __init__(self, de):
self.de = de
self.manager = CollectorManager()
def __init__(self, messaging, max_workers):
self._messaging = messaging
self._executor = ThreadPoolExecutor(max_workers=max_workers)
@property
def executor(self):
return self._executor
@property
def messaging(self):
return self._messaging
def do_trigger_audit(self, context, audit_uuid):
model_collector = self.manager.get_cluster_model_collector()
audit = TriggerAuditCommand(self.de, model_collector)
audit = DefaultAuditHandler(self.messaging)
audit.execute(audit_uuid, context)
def trigger_audit(self, context, audit_uuid):
LOG.debug("Trigger audit %s" % audit_uuid)
self.de.executor.submit(self.do_trigger_audit,
context,
audit_uuid)
self.executor.submit(self.do_trigger_audit,
context,
audit_uuid)
return audit_uuid

View File

@@ -1,39 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class BaseEventConsumer(object):
def __init__(self):
self._messaging = None
@property
def messaging(self):
return self._messaging
@messaging.setter
def messaging(self, e):
self._messaging = e
@abc.abstractmethod
def execute(self, request_id, context, data):
raise NotImplementedError('Not implemented ...') # pragma:no cover

View File

@@ -1,76 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from enum import Enum
from watcher.decision_engine.meta_action.base import MetaAction
class MigrationType(Enum):
# Total migration time and downtime depend on memory dirtying speed
pre_copy = 0
# Postcopy transfer a page only once reliability
post_copy = 1
class Migrate(MetaAction):
def __init__(self, vm, source_hypervisor, dest_hypervisor):
MetaAction.__init__(self)
"""Request Migrate
:param bandwidth the bandwidth reserved for the migration
:param vm: the virtual machine to migrate
:param source_hypervisor:
:param dest_hypervisor:
:return:
"""
self.bandwidth = 0
self.reservedDiskIOPS = 0
self.remainingDirtyPages = 0
self.vm = vm
self.migration_type = MigrationType.pre_copy
self.source_hypervisor = source_hypervisor
self.dest_hypervisor = dest_hypervisor
def set_migration_type(self, type):
self.migration_type = type
def set_bandwidth(self, bw):
"""Set the bandwidth reserved for the migration
:param bw: bandwidth
"""
self.bandwidth = bw
def get_bandwidth(self):
return self.bandwidth
def get_vm(self):
return self.vm
def get_source_hypervisor(self):
return self.source_hypervisor
def get_dest_hypervisor(self):
return self.dest_hypervisor
def __str__(self):
return "{0} Migrate {1} from {2} to {3}".format(
MetaAction.__str__(self), self.vm,
self.source_hypervisor,
self.dest_hypervisor)

View File

@@ -1,25 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from watcher.decision_engine.meta_action.base import MetaAction
class Nop(MetaAction):
def __str__(self):
return "{0} Nop".format(MetaAction.__str__(self))

View File

@@ -32,6 +32,7 @@ class Mapping(object):
:param hypervisor: the hypervisor
:param vm: the virtual machine or instance
"""
try:
self.lock.acquire()
@@ -55,13 +56,15 @@ class Mapping(object):
:param hypervisor: the hypervisor
:param vm: the virtual machine or instance
"""
self.unmap_from_id(hypervisor.uuid, vm.uuid)
def unmap_from_id(self, node_uuid, vm_uuid):
"""
"""Remove the instance (by id) from the hypervisor (by id)
:rtype : object
"""
try:
self.lock.acquire()
if str(node_uuid) in self._mapping_hypervisors:
@@ -91,6 +94,7 @@ class Mapping(object):
:param vm: the uuid of the instance
:return: hypervisor
"""
return self.model.get_hypervisor_from_id(
self.get_mapping_vm()[str(vm_uuid)])
@@ -117,6 +121,7 @@ class Mapping(object):
:param dest_hypervisor:
:return:
"""
if src_hypervisor == dest_hypervisor:
return False
# unmap

View File

@@ -15,12 +15,11 @@
# limitations under the License.
from oslo_log import log
from watcher.common.exception import HypervisorNotFound
from watcher.common.exception import IllegalArgumentException
from watcher.common.exception import VMNotFound
from watcher.decision_engine.model.hypervisor import Hypervisor
from watcher.decision_engine.model.mapping import Mapping
from watcher.decision_engine.model.vm import VM
from watcher._i18n import _
from watcher.common import exception
from watcher.decision_engine.model import hypervisor
from watcher.decision_engine.model import mapping
from watcher.decision_engine.model import vm
LOG = log.getLogger(__name__)
@@ -29,18 +28,18 @@ class ModelRoot(object):
def __init__(self):
self._hypervisors = {}
self._vms = {}
self.mapping = Mapping(self)
self.mapping = mapping.Mapping(self)
self.resource = {}
def assert_hypervisor(self, hypervisor):
if not isinstance(hypervisor, Hypervisor):
raise IllegalArgumentException(
"Hypervisor must be an instance of hypervisor")
def assert_hypervisor(self, obj):
if not isinstance(obj, hypervisor.Hypervisor):
raise exception.IllegalArgumentException(
_("'obj' argument type is not valid"))
def assert_vm(self, vm):
if not isinstance(vm, VM):
raise IllegalArgumentException(
"VM must be an instance of VM")
def assert_vm(self, obj):
if not isinstance(obj, vm.VM):
raise exception.IllegalArgumentException(
_("'obj' argument type is not valid"))
def add_hypervisor(self, hypervisor):
self.assert_hypervisor(hypervisor)
@@ -49,7 +48,7 @@ class ModelRoot(object):
def remove_hypervisor(self, hypervisor):
self.assert_hypervisor(hypervisor)
if str(hypervisor.uuid) not in self._hypervisors.keys():
raise HypervisorNotFound(hypervisor.uuid)
raise exception.HypervisorNotFound(hypervisor.uuid)
else:
del self._hypervisors[hypervisor.uuid]
@@ -62,12 +61,12 @@ class ModelRoot(object):
def get_hypervisor_from_id(self, hypervisor_uuid):
if str(hypervisor_uuid) not in self._hypervisors.keys():
raise HypervisorNotFound(hypervisor_uuid)
raise exception.HypervisorNotFound(hypervisor_uuid)
return self._hypervisors[str(hypervisor_uuid)]
def get_vm_from_id(self, uuid):
if str(uuid) not in self._vms.keys():
raise VMNotFound(uuid)
raise exception.VMNotFound(uuid)
return self._vms[str(uuid)]
def get_all_vms(self):

View File

@@ -21,7 +21,7 @@ import six
@six.add_metaclass(abc.ABCMeta)
class Planner(object):
class BasePlanner(object):
@abc.abstractmethod
def schedule(self, context, audit_uuid, solution):
"""The planner receives a solution to schedule
@@ -33,5 +33,4 @@ class Planner(object):
and performance requirements are met.
"""
# example: directed acyclic graph
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()

View File

@@ -19,20 +19,17 @@
from oslo_log import log
from enum import Enum
from watcher.common.exception import MetaActionNotFound
from watcher._i18n import _LW
from watcher.common import exception
from watcher.common import utils
from watcher.decision_engine.planner.base import Planner
from watcher.decision_engine.actions import hypervisor_state
from watcher.decision_engine.actions import migration
from watcher.decision_engine.actions import nop
from watcher.decision_engine.actions import power_state
from watcher.decision_engine.planner import base
from watcher import objects
from watcher.decision_engine.meta_action.hypervisor_state import \
ChangeHypervisorState
from watcher.decision_engine.meta_action.migrate import Migrate
from watcher.decision_engine.meta_action.nop import Nop
from watcher.decision_engine.meta_action.power_state import ChangePowerState
from watcher.objects.action import Status as AStatus
from watcher.objects.action_plan import Status as APStatus
LOG = log.getLogger(__name__)
@@ -57,7 +54,7 @@ priority_primitives = {
}
class DefaultPlanner(Planner):
class DefaultPlanner(base.BasePlanner):
def create_action(self, action_plan_id, action_type, applies_to=None,
src=None,
dst=None,
@@ -74,7 +71,7 @@ class DefaultPlanner(Planner):
'dst': dst,
'parameter': parameter,
'description': description,
'state': AStatus.PENDING,
'state': objects.action.Status.PENDING,
'alarm': None,
'next': None,
}
@@ -84,24 +81,24 @@ class DefaultPlanner(Planner):
LOG.debug('Create an action plan for the audit uuid')
action_plan = self._create_action_plan(context, audit_id)
actions = list(solution.meta_actions)
actions = list(solution.actions)
to_schedule = []
for action in actions:
if isinstance(action, Migrate):
if isinstance(action, migration.Migrate):
# TODO(jed) type
primitive = self.create_action(action_plan.id,
Primitives.LIVE_MIGRATE.value,
action.get_vm().uuid,
action.get_source_hypervisor().
action.vm.uuid,
action.src_hypervisor.
uuid,
action.get_dest_hypervisor().
action.dest_hypervisor.
uuid,
description="{0}".format(
action)
)
elif isinstance(action, ChangePowerState):
elif isinstance(action, power_state.ChangePowerState):
primitive = self.create_action(action_plan_id=action_plan.id,
action_type=Primitives.
POWER_STATE.value,
@@ -111,7 +108,7 @@ class DefaultPlanner(Planner):
value,
description="{0}".format(
action))
elif isinstance(action, ChangeHypervisorState):
elif isinstance(action, hypervisor_state.ChangeHypervisorState):
primitive = self.create_action(action_plan_id=action_plan.id,
action_type=Primitives.
HYPERVISOR_STATE.value,
@@ -120,21 +117,21 @@ class DefaultPlanner(Planner):
value,
description="{0}".format(
action))
elif isinstance(action, Nop):
elif isinstance(action, nop.Nop):
primitive = self.create_action(action_plan_id=action_plan.id,
action_type=Primitives.
NOP.value,
description="{0}".format(
action))
else:
raise MetaActionNotFound()
raise exception.MetaActionNotFound()
priority = priority_primitives[primitive['action_type']]
to_schedule.append((priority, primitive))
# scheduling
scheduled = sorted(to_schedule, reverse=False, key=lambda x: (x[0]))
if len(scheduled) == 0:
LOG.warning("The ActionPlan is empty")
LOG.warning(_LW("The action plan is empty"))
action_plan.first_action_id = None
action_plan.save()
else:
@@ -157,7 +154,7 @@ class DefaultPlanner(Planner):
'uuid': utils.generate_uuid(),
'audit_id': audit_id,
'first_action_id': None,
'state': APStatus.RECOMMENDED
'state': objects.action_plan.Status.RECOMMENDED
}
new_action_plan = objects.ActionPlan(context, **action_plan_dict)

View File

@@ -23,13 +23,10 @@ import oslo_messaging as om
from watcher.common import exception
from watcher.common.messaging.messaging_core import MessagingCore
from watcher.common.messaging.notification_handler import NotificationHandler
from watcher.common import utils
from watcher.decision_engine.event.consumer_factory import EventConsumerFactory
from watcher.decision_engine.manager import decision_engine_opt_group
from watcher.decision_engine.manager import WATCHER_DECISION_ENGINE_OPTS
from watcher.decision_engine.messaging.events import Events
LOG = log.getLogger(__name__)
CONF = cfg.CONF
@@ -47,17 +44,12 @@ class DecisionEngineAPI(MessagingCore):
CONF.watcher_decision_engine.topic_status,
api_version=self.API_VERSION,
)
self.handler = NotificationHandler(self.publisher_id)
self.handler.register_observer(self)
self.add_event_listener(Events.ALL, self.event_receive)
self.topic_status.add_endpoint(self.handler)
transport = om.get_transport(CONF)
target = om.Target(
topic=CONF.watcher_decision_engine.topic_control,
version=self.API_VERSION,
)
self.client = om.RPCClient(transport, target,
serializer=self.serializer)
@@ -67,20 +59,3 @@ class DecisionEngineAPI(MessagingCore):
return self.client.call(
context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid)
# TODO(ebe): Producteur / consommateur implementer
def event_receive(self, event):
try:
request_id = event.get_request_id()
event_type = event.get_type()
data = event.get_data()
LOG.debug("request id => %s" % event.get_request_id())
LOG.debug("type_event => %s" % str(event.get_type()))
LOG.debug("data => %s" % str(data))
event_consumer = EventConsumerFactory.factory(event_type)
event_consumer.execute(request_id, self.context, data)
except Exception as e:
LOG.error("evt %s" % e.message)
raise e

View File

@@ -21,19 +21,19 @@ import six
@six.add_metaclass(abc.ABCMeta)
class Solution(object):
class BaseSolution(object):
def __init__(self):
self._origin = None
self._model = None
self._efficiency = 0
self._efficacy = 0
@property
def efficiency(self):
return self._efficiency
def efficacy(self):
return self._efficacy
@efficiency.setter
def efficiency(self, e):
self._efficiency = e
@efficacy.setter
def efficacy(self, e):
self._efficacy = e
@property
def model(self):
@@ -53,10 +53,8 @@ class Solution(object):
@abc.abstractmethod
def add_change_request(self, r):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
raise NotImplementedError()
@abc.abstractproperty
def meta_actions(self):
raise NotImplementedError(
"Should have implemented this") # pragma:no cover
def actions(self):
raise NotImplementedError()

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