Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ebc898924 | ||
|
|
660c782626 | ||
|
|
dfaba80252 | ||
|
|
9dccb29bf4 | ||
|
|
853145f4d1 | ||
|
|
764f5c7681 | ||
|
|
b81b767567 | ||
|
|
764e31a7a1 | ||
|
|
1c49d07912 | ||
|
|
18549dc182 | ||
|
|
9f222221a7 | ||
|
|
1a64383a68 | ||
|
|
ac07f35dc7 | ||
|
|
010bc61cc9 | ||
|
|
3dd02ee895 | ||
|
|
a4bbe7f893 | ||
|
|
4c3073efb4 | ||
|
|
642226812f | ||
|
|
8f2ca2518f | ||
|
|
1698eb31f3 | ||
|
|
5fb74e677f | ||
|
|
35a5ba1cd0 | ||
|
|
c0a85be7b8 | ||
|
|
633b360652 | ||
|
|
d3160bf007 | ||
|
|
69152f2449 | ||
|
|
37cd75ffe3 | ||
|
|
c7fd5e8b21 | ||
|
|
f0b58f8c27 | ||
|
|
22dd6d42c3 | ||
|
|
bd29e2e79f | ||
|
|
c2eb112184 | ||
|
|
57cecb27f5 | ||
|
|
7c72d6f912 | ||
|
|
62570525ad | ||
|
|
92940ba9e2 | ||
|
|
7c8ce453ed | ||
|
|
33ea5f96f8 | ||
|
|
916f4d0c08 | ||
|
|
3fb5defc16 | ||
|
|
b01f4bead4 | ||
|
|
8516d629c2 | ||
|
|
35a1f0a657 | ||
|
|
d934971458 | ||
|
|
d92f85574d | ||
|
|
b1fe7a5f3d | ||
|
|
8a5eb4b6a1 | ||
|
|
91c14e4eda | ||
|
|
1613bd6904 | ||
|
|
ba4f5569d1 | ||
|
|
a62553a6a5 | ||
|
|
d5ba40530f | ||
|
|
f98e96da42 | ||
|
|
64747cad1f | ||
|
|
5f87e82bac | ||
|
|
ff89e942ca | ||
|
|
7faa501fb7 | ||
|
|
ab8d242c1f | ||
|
|
bbd26cafae | ||
|
|
0042356245 | ||
|
|
e1d4026c7c | ||
|
|
df692a8215 | ||
|
|
f9323889d6 | ||
|
|
0a44b2972e | ||
|
|
c5c16ac055 | ||
|
|
3016d3da11 | ||
|
|
daa560111c | ||
|
|
16705f68da | ||
|
|
109a980c29 | ||
|
|
531373cb84 | ||
|
|
087c4d49ed | ||
|
|
022b15dc1e | ||
|
|
454f70a19f | ||
|
|
98f05a52a8 | ||
|
|
5ff9f28a83 | ||
|
|
4a88220ffe | ||
|
|
4c2d0e6345 | ||
|
|
7710b1670e |
@@ -1,7 +1,9 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = watcher
|
||||
omit = watcher/tests/*,watcher/openstack/*
|
||||
omit = watcher/tests/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
exclude_lines =
|
||||
@abstract
|
||||
|
||||
@@ -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
|
||||
==========================
|
||||
|
||||
@@ -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
182
doc/source/architecture.rst
Normal 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
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ extensions = [
|
||||
'sphinxcontrib.httpdomain',
|
||||
'sphinxcontrib.pecanwsme.rest',
|
||||
'wsmeext.sphinxext',
|
||||
'oslosphinx'
|
||||
'oslosphinx',
|
||||
'watcher.doc',
|
||||
]
|
||||
|
||||
wsme_protocols = ['restjson']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
==================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.. _architecture:
|
||||
|
||||
===================
|
||||
System Architecture
|
||||
===================
|
||||
|
||||
Please go to `Wiki Watcher Architecture <https://wiki.openstack.org/wiki/WatcherArchitecture>`_
|
||||
|
||||
.. _API service: ../webapi/v1.html
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
BIN
doc/source/image_src/architecture.dia
Normal file
BIN
doc/source/image_src/architecture.dia
Normal file
Binary file not shown.
275
doc/source/images/architecture.svg
Normal file
275
doc/source/images/architecture.svg
Normal 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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
========
|
||||
|
||||
@@ -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)
|
||||
=====================
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
17
tox.ini
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
83
watcher/applier/primitives/change_nova_service_state.py
Normal file
83
watcher/applier/primitives/change_nova_service_state.py
Normal 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)
|
||||
@@ -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:
|
||||
@@ -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):
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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' "
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
============================================================
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 ###
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
@@ -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 " "
|
||||
@@ -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)
|
||||
72
watcher/decision_engine/actions/migration.py
Normal file
72
watcher/decision_engine/actions/migration.py
Normal 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)
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
85
watcher/decision_engine/audit/default.py
Normal file
85
watcher/decision_engine/audit/default.py
Normal 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)
|
||||
@@ -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)))
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user