Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d398b4d22 | ||
|
|
585fbeb9ee | ||
|
|
e9f237dc80 | ||
|
|
38f6700144 | ||
|
|
30bdf29002 | ||
|
|
e6f147d81d | ||
|
|
5aa6b16238 | ||
|
|
dcb5c1f9fc | ||
|
|
4ba01cbbcf | ||
|
|
d91d72d2c2 | ||
|
|
083bc2bed4 | ||
|
|
9d3671af37 | ||
|
|
3b88e37680 | ||
|
|
6eee64502f | ||
|
|
b9231f65cc | ||
|
|
b9b505a518 | ||
|
|
388ef9f11c | ||
|
|
4e3caaa157 | ||
|
|
8c6bf734af | ||
|
|
277a749ca0 | ||
|
|
8401b5e479 | ||
|
|
78689fbe3b | ||
|
|
22abaa9c3a | ||
|
|
fb82131d85 | ||
|
|
f6f5079adb | ||
|
|
f045f5d816 | ||
|
|
b77541deb2 | ||
|
|
3b9d72439c | ||
|
|
89aa2d54df | ||
|
|
86f4cee588 | ||
|
|
04ac509821 | ||
|
|
4ba9d2cb73 | ||
|
|
a71c9be860 | ||
|
|
c2cb1a1f8e | ||
|
|
79bdcf7baf | ||
|
|
de1b1a9938 | ||
|
|
031ebdecde | ||
|
|
daabe671c7 | ||
|
|
26bc3d139d | ||
|
|
4d2536b9b2 | ||
|
|
4388780e66 | ||
|
|
d03a9197b0 | ||
|
|
43f5ab18ba | ||
|
|
209176c3d7 | ||
|
|
1a21867735 | ||
|
|
5f6a97148f | ||
|
|
e6b23a0856 | ||
|
|
f9a1b9d3ce | ||
|
|
ff611544fb | ||
|
|
18e5c7d844 | ||
|
|
2966b93777 | ||
|
|
e67b532110 | ||
|
|
81765b9aa5 | ||
|
|
673642e436 | ||
|
|
1026a896e2 | ||
|
|
a3ac26870a | ||
|
|
192d8e262c | ||
|
|
3b5ef15db6 | ||
|
|
be9058f3e3 | ||
|
|
91951f3b01 | ||
|
|
57a2af2685 | ||
|
|
76e3d2e2f6 | ||
|
|
bd5a969a26 | ||
|
|
d61bf5f053 | ||
|
|
aaaf3f1c84 | ||
|
|
eb861f86ab | ||
|
|
a9e7251d0d | ||
|
|
4ff373197c | ||
|
|
87087e9add | ||
|
|
408d6d4650 | ||
|
|
e52dc4f8aa | ||
|
|
0f14b7635d | ||
|
|
bb77641aad | ||
|
|
77228a0b0a | ||
|
|
1157a8db30 | ||
|
|
18354d1b4e | ||
|
|
8387cd10de | ||
|
|
0449bae747 | ||
|
|
3e07844844 | ||
|
|
a52d92be87 | ||
|
|
96683a6133 |
@@ -4,6 +4,7 @@ source = watcher
|
|||||||
omit = watcher/tests/*
|
omit = watcher/tests/*
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
ignore_errors = True
|
ignore_errors = True
|
||||||
exclude_lines =
|
exclude_lines =
|
||||||
@abstract
|
@abc.abstract
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -42,7 +42,3 @@ LOGDAYS=2
|
|||||||
[[post-config|$NOVA_CONF]]
|
[[post-config|$NOVA_CONF]]
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
compute_monitors=cpu.virt_driver
|
compute_monitors=cpu.virt_driver
|
||||||
|
|
||||||
[[post-config|$WATCHER_CONF]]
|
|
||||||
[watcher_goals]
|
|
||||||
goals=BASIC_CONSOLIDATION:basic,DUMMY:dummy
|
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ by the :ref:`Watcher API <archi_watcher_api_definition>` or the
|
|||||||
- :ref:`Action plans <action_plan_definition>`
|
- :ref:`Action plans <action_plan_definition>`
|
||||||
- :ref:`Actions <action_definition>`
|
- :ref:`Actions <action_definition>`
|
||||||
- :ref:`Goals <goal_definition>`
|
- :ref:`Goals <goal_definition>`
|
||||||
|
- :ref:`Strategies <strategy_definition>`
|
||||||
|
|
||||||
The Watcher domain being here "*optimization of some resources provided by an
|
The Watcher domain being here "*optimization of some resources provided by an
|
||||||
OpenStack system*".
|
OpenStack system*".
|
||||||
@@ -196,8 +197,6 @@ Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
|
|||||||
which provides information about the past of the
|
which provides information about the past of the
|
||||||
:ref:`Cluster <cluster_definition>`
|
:ref:`Cluster <cluster_definition>`
|
||||||
|
|
||||||
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
|
|
||||||
given :ref:`Goal <goal_definition>` via the main Watcher configuration file.
|
|
||||||
|
|
||||||
.. _data_model:
|
.. _data_model:
|
||||||
|
|
||||||
@@ -211,6 +210,14 @@ view (Goals, Audits, Action Plans, ...):
|
|||||||
.. image:: ./images/functional_data_model.svg
|
.. image:: ./images/functional_data_model.svg
|
||||||
:width: 100%
|
:width: 100%
|
||||||
|
|
||||||
|
Here below is a class diagram representing the main objects in Watcher from a
|
||||||
|
database perspective:
|
||||||
|
|
||||||
|
.. image:: ./images/watcher_class_diagram.png
|
||||||
|
:width: 100%
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. _sequence_diagrams:
|
.. _sequence_diagrams:
|
||||||
|
|
||||||
Sequence diagrams
|
Sequence diagrams
|
||||||
@@ -230,13 +237,15 @@ following parameters:
|
|||||||
|
|
||||||
- A name
|
- A name
|
||||||
- A goal to achieve
|
- A goal to achieve
|
||||||
|
- An optional strategy
|
||||||
|
|
||||||
.. image:: ./images/sequence_create_audit_template.png
|
.. image:: ./images/sequence_create_audit_template.png
|
||||||
:width: 100%
|
:width: 100%
|
||||||
|
|
||||||
The `Watcher API`_ just makes sure that the goal exists (i.e. it is declared
|
The `Watcher API`_ makes sure that both the specified goal (mandatory) and
|
||||||
in the Watcher configuration file) and stores a new audit template in the
|
its associated strategy (optional) are registered inside the :ref:`Watcher
|
||||||
:ref:`Watcher Database <watcher_database_definition>`.
|
Database <watcher_database_definition>` before storing a new audit template in
|
||||||
|
the :ref:`Watcher Database <watcher_database_definition>`.
|
||||||
|
|
||||||
.. _sequence_diagrams_create_and_launch_audit:
|
.. _sequence_diagrams_create_and_launch_audit:
|
||||||
|
|
||||||
@@ -260,12 +269,11 @@ the Audit in the
|
|||||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads
|
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads
|
||||||
the Audit parameters from the
|
the Audit parameters from the
|
||||||
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the
|
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the
|
||||||
appropriate :ref:`Strategy <strategy_definition>` (using entry points)
|
appropriate :ref:`strategy <strategy_definition>` (using entry points)
|
||||||
associated to the :ref:`Goal <goal_definition>` of the
|
given both the :ref:`goal <goal_definition>` and the strategy associated to the
|
||||||
:ref:`Audit <audit_definition>` (it uses the information of the Watcher
|
parent :ref:`audit template <audit_template_definition>` of the :ref:`Audit
|
||||||
configuration file to find the mapping between the
|
<audit_definition>`. If no strategy is associated to the audit template, the
|
||||||
:ref:`Goal <goal_definition>` and the :ref:`Strategy <strategy_definition>`
|
strategy is dynamically selected by the Decision Engine.
|
||||||
python class).
|
|
||||||
|
|
||||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
|
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
|
||||||
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This
|
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ The configuration file is organized into the following sections:
|
|||||||
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients
|
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients
|
||||||
* ``[watcher_applier]`` - Watcher Applier module configuration
|
* ``[watcher_applier]`` - Watcher Applier module configuration
|
||||||
* ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration
|
* ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration
|
||||||
* ``[watcher_goals]`` - Goals mapping configuration
|
|
||||||
* ``[watcher_strategies]`` - Strategy configuration
|
|
||||||
* ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration
|
* ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration
|
||||||
* ``[ceilometer_client]`` - Ceilometer client configuration
|
* ``[ceilometer_client]`` - Ceilometer client configuration
|
||||||
* ``[cinder_client]`` - Cinder client configuration
|
* ``[cinder_client]`` - Cinder client configuration
|
||||||
|
|||||||
52
doc/source/deploy/gmr.rst
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
..
|
||||||
|
Except where otherwise noted, this document is licensed under Creative
|
||||||
|
Commons Attribution 3.0 License. You can view the license at:
|
||||||
|
|
||||||
|
https://creativecommons.org/licenses/by/3.0/
|
||||||
|
|
||||||
|
.. _watcher_gmr:
|
||||||
|
|
||||||
|
=======================
|
||||||
|
Guru Meditation Reports
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Watcher contains a mechanism whereby developers and system administrators can
|
||||||
|
generate a report about the state of a running Watcher service. This report
|
||||||
|
is called a *Guru Meditation Report* (*GMR* for short).
|
||||||
|
|
||||||
|
Generating a GMR
|
||||||
|
================
|
||||||
|
|
||||||
|
A *GMR* can be generated by sending the *USR2* signal to any Watcher process
|
||||||
|
with support (see below). The *GMR* will then be outputted as standard error
|
||||||
|
for that particular process.
|
||||||
|
|
||||||
|
For example, suppose that ``watcher-api`` has process id ``8675``, and was run
|
||||||
|
with ``2>/var/log/watcher/watcher-api-err.log``. Then, ``kill -USR2 8675``
|
||||||
|
will trigger the Guru Meditation report to be printed to
|
||||||
|
``/var/log/watcher/watcher-api-err.log``.
|
||||||
|
|
||||||
|
Structure of a GMR
|
||||||
|
==================
|
||||||
|
|
||||||
|
The *GMR* is designed to be extensible; any particular service may add its
|
||||||
|
own sections. However, the base *GMR* consists of several sections:
|
||||||
|
|
||||||
|
Package
|
||||||
|
Shows information about the package to which this process belongs, including
|
||||||
|
version informations.
|
||||||
|
|
||||||
|
Threads
|
||||||
|
Shows stack traces and thread ids for each of the threads within this
|
||||||
|
process.
|
||||||
|
|
||||||
|
Green Threads
|
||||||
|
Shows stack traces for each of the green threads within this process (green
|
||||||
|
threads don't have thread ids).
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
Lists all the configuration options currently accessible via the CONF object
|
||||||
|
for the current process.
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
Lists all the plugins currently accessible by the Watcher service.
|
||||||
@@ -32,17 +32,17 @@ This guide assumes you have a working installation of Watcher. If you get
|
|||||||
Please refer to the `installation guide`_.
|
Please refer to the `installation guide`_.
|
||||||
In order to use Watcher, you have to configure your credentials suitable for
|
In order to use Watcher, you have to configure your credentials suitable for
|
||||||
watcher command-line tools.
|
watcher command-line tools.
|
||||||
If you need help on a specific command, you can use:
|
|
||||||
|
|
||||||
.. code:: bash
|
You can interact with Watcher either by using our dedicated `Watcher CLI`_
|
||||||
|
named ``watcher``, or by using the `OpenStack CLI`_ ``openstack``.
|
||||||
$ watcher help COMMAND
|
|
||||||
|
|
||||||
If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon
|
If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon
|
||||||
plugin installation guide`_.
|
plugin installation guide`_.
|
||||||
|
|
||||||
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
|
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
|
||||||
.. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html
|
.. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html
|
||||||
|
.. _`OpenStack CLI`: http://docs.openstack.org/developer/python-openstackclient/man/openstack.html
|
||||||
|
.. _`Watcher CLI`: https://factory.b-com.com/www/watcher/doc/python-watcherclient/index.html
|
||||||
|
|
||||||
Seeing what the Watcher CLI can do ?
|
Seeing what the Watcher CLI can do ?
|
||||||
------------------------------------
|
------------------------------------
|
||||||
@@ -51,23 +51,66 @@ watcher binary without options.
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher
|
$ watcher help
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack help optimize
|
||||||
|
|
||||||
How do I run an audit of my cluster ?
|
How do I run an audit of my cluster ?
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
First, you need to create an :ref:`audit template <audit_template_definition>`.
|
First, you need to find the :ref:`goal <goal_definition>` you want to achieve:
|
||||||
An :ref:`audit template <audit_template_definition>` defines an optimization
|
|
||||||
:ref:`goal <goal_definition>` to achieve (i.e. the settings of your audit).
|
|
||||||
This goal should be declared in the Watcher service configuration file
|
|
||||||
**/etc/watcher/watcher.conf**.
|
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher audit-template-create my_first_audit DUMMY
|
$ watcher goal list
|
||||||
|
|
||||||
If you get "*You must provide a username via either --os-username or via
|
or::
|
||||||
env[OS_USERNAME]*" you may have to verify your credentials.
|
|
||||||
|
$ openstack optimize goal list
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you get "*You must provide a username via either --os-username or via
|
||||||
|
env[OS_USERNAME]*" you may have to verify your credentials.
|
||||||
|
|
||||||
|
Then, you can create an :ref:`audit template <audit_template_definition>`.
|
||||||
|
An :ref:`audit template <audit_template_definition>` defines an optimization
|
||||||
|
:ref:`goal <goal_definition>` to achieve (i.e. the settings of your audit).
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
$ watcher audittemplate create my_first_audit_template <your_goal>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize audittemplate create my_first_audit_template <your_goal>
|
||||||
|
|
||||||
|
Although optional, you may want to actually set a specific strategy for your
|
||||||
|
audit template. If so, you may can search of its UUID or name using the
|
||||||
|
following command:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
$ watcher strategy list --goal-uuid <your_goal_uuid>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize strategy list --goal-uuid <your_goal_uuid>
|
||||||
|
|
||||||
|
|
||||||
|
The command to create your audit template would then be:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
$ watcher audittemplate create my_first_audit_template <your_goal> \
|
||||||
|
--strategy <your_strategy>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize audittemplate create my_first_audit_template <your_goal> \
|
||||||
|
--strategy <your_strategy>
|
||||||
|
|
||||||
Then, you can create an audit. An audit is a request for optimizing your
|
Then, you can create an audit. An audit is a request for optimizing your
|
||||||
cluster depending on the specified :ref:`goal <goal_definition>`.
|
cluster depending on the specified :ref:`goal <goal_definition>`.
|
||||||
@@ -76,19 +119,26 @@ You can launch an audit on your cluster by referencing the
|
|||||||
:ref:`audit template <audit_template_definition>` (i.e. the settings of your
|
:ref:`audit template <audit_template_definition>` (i.e. the settings of your
|
||||||
audit) that you want to use.
|
audit) that you want to use.
|
||||||
|
|
||||||
- Get the :ref:`audit template <audit_template_definition>` UUID:
|
- Get the :ref:`audit template <audit_template_definition>` UUID or name:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher audit-template-list
|
$ watcher audittemplate list
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize audittemplate list
|
||||||
|
|
||||||
- Start an audit based on this :ref:`audit template
|
- Start an audit based on this :ref:`audit template
|
||||||
<audit_template_definition>` settings:
|
<audit_template_definition>` settings:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher audit-create -a <your_audit_template_uuid>
|
$ watcher audit create -a <your_audit_template>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize audit create -a <your_audit_template>
|
||||||
|
|
||||||
Watcher service will compute an :ref:`Action Plan <action_plan_definition>`
|
Watcher service will compute an :ref:`Action Plan <action_plan_definition>`
|
||||||
composed of a list of potential optimization :ref:`actions <action_definition>`
|
composed of a list of potential optimization :ref:`actions <action_definition>`
|
||||||
@@ -102,15 +152,22 @@ configuration file.
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher action-plan-list --audit <the_audit_uuid>
|
$ watcher actionplan list --audit <the_audit_uuid>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize actionplan list --audit <the_audit_uuid>
|
||||||
|
|
||||||
- Have a look on the list of optimization :ref:`actions <action_definition>`
|
- Have a look on the list of optimization :ref:`actions <action_definition>`
|
||||||
contained in this new :ref:`action plan <action_plan_definition>`:
|
contained in this new :ref:`action plan <action_plan_definition>`:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher action-list --action-plan <the_action_plan_uuid>
|
$ watcher action list --action-plan <the_action_plan_uuid>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize action list --action-plan <the_action_plan_uuid>
|
||||||
|
|
||||||
Once you have learned how to create an :ref:`Action Plan
|
Once you have learned how to create an :ref:`Action Plan
|
||||||
<action_plan_definition>`, it's time to go further by applying it to your
|
<action_plan_definition>`, it's time to go further by applying it to your
|
||||||
@@ -120,18 +177,30 @@ cluster:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher action-plan-start <the_action_plan_uuid>
|
$ watcher actionplan start <the_action_plan_uuid>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize actionplan start <the_action_plan_uuid>
|
||||||
|
|
||||||
You can follow the states of the :ref:`actions <action_definition>` by
|
You can follow the states of the :ref:`actions <action_definition>` by
|
||||||
periodically calling:
|
periodically calling:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher action-list
|
$ watcher action list
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize action list
|
||||||
|
|
||||||
You can also obtain more detailed information about a specific action:
|
You can also obtain more detailed information about a specific action:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher action-show <the_action_uuid>
|
$ watcher action show <the_action_uuid>
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
$ openstack optimize action show <the_action_uuid>
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ place:
|
|||||||
|
|
||||||
$ workon watcher
|
$ workon watcher
|
||||||
|
|
||||||
(watcher) $ watcher-db-manage --create_schema
|
(watcher) $ watcher-db-manage create_schema
|
||||||
|
|
||||||
|
|
||||||
Running Watcher services
|
Running Watcher services
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Here is an example showing how you can write a plugin called ``DummyAction``:
|
|||||||
from watcher.applier.actions import base
|
from watcher.applier.actions import base
|
||||||
|
|
||||||
|
|
||||||
class DummyAction(baseBaseAction):
|
class DummyAction(base.BaseAction):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema(self):
|
def schema(self):
|
||||||
@@ -90,11 +90,53 @@ Input validation
|
|||||||
|
|
||||||
As you can see in the previous example, we are using `Voluptuous`_ to validate
|
As you can see in the previous example, we are using `Voluptuous`_ to validate
|
||||||
the input parameters of an action. So if you want to learn more about how to
|
the input parameters of an action. So if you want to learn more about how to
|
||||||
work with `Voluptuous`_, you can have a look at their `documentation`_ here:
|
work with `Voluptuous`_, you can have a look at their `documentation`_:
|
||||||
|
|
||||||
.. _Voluptuous: https://github.com/alecthomas/voluptuous
|
.. _Voluptuous: https://github.com/alecthomas/voluptuous
|
||||||
.. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md
|
.. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md
|
||||||
|
|
||||||
|
|
||||||
|
Define configuration parameters
|
||||||
|
===============================
|
||||||
|
|
||||||
|
At this point, you have a fully functional action. However, in more complex
|
||||||
|
implementation, you may want to define some configuration options so one can
|
||||||
|
tune the action to its needs. To do so, you can implement the
|
||||||
|
:py:meth:`~.Loadable.get_config_opts` class method as followed:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
class DummyAction(base.BaseAction):
|
||||||
|
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
assert self.config.test_opt == 0
|
||||||
|
|
||||||
|
def get_config_opts(self):
|
||||||
|
return [
|
||||||
|
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
|
||||||
|
# Some more options ...
|
||||||
|
]
|
||||||
|
|
||||||
|
The configuration options defined within this class method will be included
|
||||||
|
within the global ``watcher.conf`` configuration file under a section named by
|
||||||
|
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
|
||||||
|
configuration would have to be modified as followed:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[watcher_actions.dummy]
|
||||||
|
# Option used for testing.
|
||||||
|
test_opt = test_value
|
||||||
|
|
||||||
|
Then, the configuration options you define within this method will then be
|
||||||
|
injected in each instantiated object via the ``config`` parameter of the
|
||||||
|
:py:meth:`~.BaseAction.__init__` method.
|
||||||
|
|
||||||
|
|
||||||
Abstract Plugin Class
|
Abstract Plugin Class
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@@ -103,6 +145,7 @@ should implement:
|
|||||||
|
|
||||||
.. autoclass:: watcher.applier.actions.base.BaseAction
|
.. autoclass:: watcher.applier.actions.base.BaseAction
|
||||||
:members:
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
.. py:attribute:: schema
|
.. py:attribute:: schema
|
||||||
|
|||||||
@@ -69,6 +69,49 @@ examples, have a look at the implementation of planners already provided by
|
|||||||
Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner
|
Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner
|
||||||
plugins can be found :ref:`here <watcher_planners>`.
|
plugins can be found :ref:`here <watcher_planners>`.
|
||||||
|
|
||||||
|
|
||||||
|
Define configuration parameters
|
||||||
|
===============================
|
||||||
|
|
||||||
|
At this point, you have a fully functional planner. However, in more complex
|
||||||
|
implementation, you may want to define some configuration options so one can
|
||||||
|
tune the planner to its needs. To do so, you can implement the
|
||||||
|
:py:meth:`~.Loadable.get_config_opts` class method as followed:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
class DummyPlanner(base.BasePlanner):
|
||||||
|
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
def schedule(self, context, audit_uuid, solution):
|
||||||
|
assert self.config.test_opt == 0
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
def get_config_opts(self):
|
||||||
|
return [
|
||||||
|
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
|
||||||
|
# Some more options ...
|
||||||
|
]
|
||||||
|
|
||||||
|
The configuration options defined within this class method will be included
|
||||||
|
within the global ``watcher.conf`` configuration file under a section named by
|
||||||
|
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
|
||||||
|
configuration would have to be modified as followed:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[watcher_planners.dummy]
|
||||||
|
# Option used for testing.
|
||||||
|
test_opt = test_value
|
||||||
|
|
||||||
|
Then, the configuration options you define within this method will then be
|
||||||
|
injected in each instantiated object via the ``config`` parameter of the
|
||||||
|
:py:meth:`~.BasePlanner.__init__` method.
|
||||||
|
|
||||||
|
|
||||||
Abstract Plugin Class
|
Abstract Plugin Class
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@@ -77,6 +120,7 @@ should implement:
|
|||||||
|
|
||||||
.. autoclass:: watcher.decision_engine.planner.base.BasePlanner
|
.. autoclass:: watcher.decision_engine.planner.base.BasePlanner
|
||||||
:members:
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ plugin interface which gives anyone the ability to integrate an external
|
|||||||
strategy in order to make use of placement algorithms.
|
strategy in order to make use of placement algorithms.
|
||||||
|
|
||||||
This section gives some guidelines on how to implement and integrate custom
|
This section gives some guidelines on how to implement and integrate custom
|
||||||
strategies with Watcher.
|
strategies with Watcher. If you wish to create a third-party package for your
|
||||||
|
plugin, you can refer to our :ref:`documentation for third-party package
|
||||||
|
creation <plugin-base_setup>`.
|
||||||
|
|
||||||
|
|
||||||
Pre-requisites
|
Pre-requisites
|
||||||
@@ -26,64 +28,217 @@ configured so that it would provide you all the metrics you need to be able to
|
|||||||
use your strategy.
|
use your strategy.
|
||||||
|
|
||||||
|
|
||||||
Creating a new plugin
|
Create a new plugin
|
||||||
=====================
|
===================
|
||||||
|
|
||||||
First of all you have to:
|
In order to create a new strategy, you have to:
|
||||||
|
|
||||||
- Extend :py:class:`~.BaseStrategy`
|
- Extend the :py:class:`~.UnclassifiedStrategy` class
|
||||||
- Implement its :py:meth:`~.BaseStrategy.execute` method
|
- Implement its :py:meth:`~.BaseStrategy.get_name` class method to return the
|
||||||
|
**unique** ID of the new strategy you want to create. This unique ID should
|
||||||
|
be the same as the name of :ref:`the entry point we will declare later on
|
||||||
|
<strategy_plugin_add_entrypoint>`.
|
||||||
|
- Implement its :py:meth:`~.BaseStrategy.get_display_name` class method to
|
||||||
|
return the translated display name of the strategy you want to create.
|
||||||
|
Note: Do not use a variable to return the translated string so it can be
|
||||||
|
automatically collected by the translation tool.
|
||||||
|
- Implement its :py:meth:`~.BaseStrategy.get_translatable_display_name`
|
||||||
|
class method to return the translation key (actually the english display
|
||||||
|
name) of your new strategy. The value return should be the same as the
|
||||||
|
string translated in :py:meth:`~.BaseStrategy.get_display_name`.
|
||||||
|
- Implement its :py:meth:`~.BaseStrategy.execute` method to return the
|
||||||
|
solution you computed within your strategy.
|
||||||
|
|
||||||
Here is an example showing how you can write a plugin called ``DummyStrategy``:
|
Here is an example showing how you can write a plugin called ``NewStrategy``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import uuid
|
import abc
|
||||||
|
|
||||||
class DummyStrategy(BaseStrategy):
|
import six
|
||||||
|
|
||||||
DEFAULT_NAME = "dummy"
|
from watcher._i18n import _
|
||||||
DEFAULT_DESCRIPTION = "Dummy Strategy"
|
from watcher.decision_engine.strategy.strategies import base
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
|
||||||
super(DummyStrategy, self).__init__(name, description)
|
|
||||||
|
|
||||||
def execute(self, model):
|
class NewStrategy(base.UnclassifiedStrategy):
|
||||||
migration_type = 'live'
|
|
||||||
src_hypervisor = 'compute-host-1'
|
def __init__(self, osc=None):
|
||||||
dst_hypervisor = 'compute-host-2'
|
super(NewStrategy, self).__init__(osc)
|
||||||
instance_id = uuid.uuid4()
|
|
||||||
parameters = {'migration_type': migration_type,
|
def execute(self, original_model):
|
||||||
'src_hypervisor': src_hypervisor,
|
self.solution.add_action(action_type="nop",
|
||||||
'dst_hypervisor': dst_hypervisor}
|
|
||||||
self.solution.add_action(action_type="migration",
|
|
||||||
resource_id=instance_id,
|
|
||||||
input_parameters=parameters)
|
input_parameters=parameters)
|
||||||
# Do some more stuff here ...
|
# Do some more stuff here ...
|
||||||
return self.solution
|
return self.solution
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "new_strategy"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("New strategy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "New strategy"
|
||||||
|
|
||||||
|
|
||||||
As you can see in the above example, the :py:meth:`~.BaseStrategy.execute`
|
As you can see in the above example, the :py:meth:`~.BaseStrategy.execute`
|
||||||
method returns a :py:class:`~.BaseSolution` instance as required. This solution
|
method returns a :py:class:`~.BaseSolution` instance as required. This solution
|
||||||
is what wraps the abstract set of actions the strategy recommends to you. This
|
is what wraps the abstract set of actions the strategy recommends to you. This
|
||||||
solution is then processed by a :ref:`planner <planner_definition>` to produce
|
solution is then processed by a :ref:`planner <planner_definition>` to produce
|
||||||
an action plan which shall contain the sequenced flow of actions to be
|
an action plan which contains the sequenced flow of actions to be
|
||||||
executed by the :ref:`Watcher Applier <watcher_applier_definition>`.
|
executed by the :ref:`Watcher Applier <watcher_applier_definition>`.
|
||||||
|
|
||||||
Please note that your strategy class will be instantiated without any
|
Please note that your strategy class will expect to find the same constructor
|
||||||
parameter. Therefore, you should make sure not to make any of them required in
|
signature as BaseStrategy to instantiate you strategy. Therefore, you should
|
||||||
your ``__init__`` method.
|
ensure that your ``__init__`` signature is identical to the
|
||||||
|
:py:class:`~.BaseStrategy` one.
|
||||||
|
|
||||||
|
|
||||||
|
Create a new goal
|
||||||
|
=================
|
||||||
|
|
||||||
|
As stated before, the ``NewStrategy`` class extends a class called
|
||||||
|
:py:class:`~.UnclassifiedStrategy`. This class actually implements a set of
|
||||||
|
abstract methods which are defined within the :py:class:`~.BaseStrategy` parent
|
||||||
|
class.
|
||||||
|
|
||||||
|
Once you are confident in your strategy plugin, the next step is now to
|
||||||
|
classify your goal by assigning it a proper goal. To do so, you can either
|
||||||
|
reuse existing goals defined in Watcher. As of now, four goal-oriented abstract
|
||||||
|
classes are defined in Watcher:
|
||||||
|
|
||||||
|
- :py:class:`~.UnclassifiedStrategy` which is the one I mentioned up until now.
|
||||||
|
- :py:class:`~.DummyBaseStrategy` which is used by :py:class:`~.DummyStrategy`
|
||||||
|
for testing purposes.
|
||||||
|
- :py:class:`~.ServerConsolidationBaseStrategy`
|
||||||
|
- :py:class:`~.ThermalOptimizationBaseStrategy`
|
||||||
|
|
||||||
|
If none of the above actually correspond to the goal your new strategy
|
||||||
|
achieves, you can define a brand new one. To do so, you need to:
|
||||||
|
|
||||||
|
- Extend the :py:class:`~.BaseStrategy` class to make your new goal-oriented
|
||||||
|
strategy abstract class :
|
||||||
|
- Implement its :py:meth:`~.BaseStrategy.get_goal_name` class method to
|
||||||
|
return the **unique** ID of the goal you want to achieve.
|
||||||
|
- Implement its :py:meth:`~.BaseStrategy.get_goal_display_name` class method
|
||||||
|
to return the translated display name of the goal you want to achieve.
|
||||||
|
Note: Do not use a variable to return the translated string so it can be
|
||||||
|
automatically collected by the translation tool.
|
||||||
|
- Implement its :py:meth:`~.BaseStrategy.get_translatable_goal_display_name`
|
||||||
|
class method to return the goal translation key (actually the english
|
||||||
|
display name). The value return should be the same as the string translated
|
||||||
|
in :py:meth:`~.BaseStrategy.get_goal_display_name`.
|
||||||
|
|
||||||
|
Here is an example showing how you can define a new ``NEW_GOAL`` goal and
|
||||||
|
modify your ``NewStrategy`` plugin so it now achieves the latter:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
|
from watcher.decision_engine.strategy.strategies import base
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class NewGoalBaseStrategy(base.BaseStrategy):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "NEW_GOAL"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("New goal")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "New goal"
|
||||||
|
|
||||||
|
|
||||||
|
class NewStrategy(NewGoalBaseStrategy):
|
||||||
|
|
||||||
|
def __init__(self, config, osc=None):
|
||||||
|
super(NewStrategy, self).__init__(config, osc)
|
||||||
|
|
||||||
|
def execute(self, original_model):
|
||||||
|
self.solution.add_action(action_type="nop",
|
||||||
|
input_parameters=parameters)
|
||||||
|
# Do some more stuff here ...
|
||||||
|
return self.solution
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "new_strategy"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("New strategy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "New strategy"
|
||||||
|
|
||||||
|
|
||||||
|
Define configuration parameters
|
||||||
|
===============================
|
||||||
|
|
||||||
|
At this point, you have a fully functional strategy. However, in more complex
|
||||||
|
implementation, you may want to define some configuration options so one can
|
||||||
|
tune the strategy to its needs. To do so, you can implement the
|
||||||
|
:py:meth:`~.Loadable.get_config_opts` class method as followed:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
class NewStrategy(NewGoalBaseStrategy):
|
||||||
|
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
def execute(self, original_model):
|
||||||
|
assert self.config.test_opt == 0
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
def get_config_opts(self):
|
||||||
|
return [
|
||||||
|
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
|
||||||
|
# Some more options ...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
The configuration options defined within this class method will be included
|
||||||
|
within the global ``watcher.conf`` configuration file under a section named by
|
||||||
|
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
|
||||||
|
configuration would have to be modified as followed:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[watcher_strategies.new_strategy]
|
||||||
|
# Option used for testing.
|
||||||
|
test_opt = test_value
|
||||||
|
|
||||||
|
Then, the configuration options you define within this method will then be
|
||||||
|
injected in each instantiated object via the ``config`` parameter of the
|
||||||
|
:py:meth:`~.BaseStrategy.__init__` method.
|
||||||
|
|
||||||
|
|
||||||
Abstract Plugin Class
|
Abstract Plugin Class
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
Here below is the abstract :py:class:`~.BaseStrategy` class that every single
|
Here below is the abstract :py:class:`~.BaseStrategy` class:
|
||||||
strategy should implement:
|
|
||||||
|
|
||||||
.. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy
|
.. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy
|
||||||
:members:
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
.. _strategy_plugin_add_entrypoint:
|
||||||
|
|
||||||
Add a new entry point
|
Add a new entry point
|
||||||
=====================
|
=====================
|
||||||
@@ -93,7 +248,9 @@ strategy must be registered as a named entry point under the
|
|||||||
``watcher_strategies`` entry point of your ``setup.py`` file. If you are using
|
``watcher_strategies`` entry point of your ``setup.py`` file. If you are using
|
||||||
pbr_, this entry point should be placed in your ``setup.cfg`` file.
|
pbr_, this entry point should be placed in your ``setup.cfg`` file.
|
||||||
|
|
||||||
The name you give to your entry point has to be unique.
|
The name you give to your entry point has to be unique and should be the same
|
||||||
|
as the value returned by the :py:meth:`~.BaseStrategy.get_id` class method of
|
||||||
|
your strategy.
|
||||||
|
|
||||||
Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
|
Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
|
||||||
|
|
||||||
@@ -101,7 +258,7 @@ Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
|
|||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
watcher_strategies =
|
watcher_strategies =
|
||||||
dummy = thirdparty.dummy:DummyStrategy
|
dummy_strategy = thirdparty.dummy:DummyStrategy
|
||||||
|
|
||||||
|
|
||||||
To get a better understanding on how to implement a more advanced strategy,
|
To get a better understanding on how to implement a more advanced strategy,
|
||||||
@@ -117,16 +274,10 @@ plugins when it is restarted. If a Python package containing a custom plugin is
|
|||||||
installed within the same environment as Watcher, Watcher will automatically
|
installed within the same environment as Watcher, Watcher will automatically
|
||||||
make that plugin available for use.
|
make that plugin available for use.
|
||||||
|
|
||||||
At this point, Watcher will use your new strategy if you reference it in the
|
At this point, Watcher will scan and register inside the :ref:`Watcher Database
|
||||||
``goals`` under the ``[watcher_goals]`` section of your ``watcher.conf``
|
<watcher_database_definition>` all the strategies (alongside the goals they
|
||||||
configuration file. For example, if you want to use a ``dummy`` strategy you
|
should satisfy) you implemented upon restarting the :ref:`Watcher Decision
|
||||||
just installed, you would have to associate it to a goal like this:
|
Engine <watcher_decision_engine_definition>`.
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[watcher_goals]
|
|
||||||
goals = BALANCE_LOAD:basic,MINIMIZE_ENERGY_CONSUMPTION:dummy
|
|
||||||
|
|
||||||
|
|
||||||
You should take care when installing strategy plugins. By their very nature,
|
You should take care when installing strategy plugins. By their very nature,
|
||||||
there are no guarantees that utilizing them as is will be supported, as
|
there are no guarantees that utilizing them as is will be supported, as
|
||||||
@@ -148,7 +299,6 @@ for various types of backends. A list of the available backends is located
|
|||||||
here_. The Ceilosca project is a good example of how to create your own
|
here_. The Ceilosca project is a good example of how to create your own
|
||||||
pluggable backend.
|
pluggable backend.
|
||||||
|
|
||||||
|
|
||||||
Finally, if your strategy requires new metrics not covered by Ceilometer, you
|
Finally, if your strategy requires new metrics not covered by Ceilometer, you
|
||||||
can add them through a Ceilometer `plugin`_.
|
can add them through a Ceilometer `plugin`_.
|
||||||
|
|
||||||
@@ -191,7 +341,7 @@ Read usage metrics using the Watcher Cluster History Helper
|
|||||||
|
|
||||||
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
|
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
|
||||||
|
|
||||||
.. autoclass:: watcher.metrics_engine.cluster_history.api.BaseClusterHistory
|
.. autoclass:: watcher.metrics_engine.cluster_history.base.BaseClusterHistory
|
||||||
:members:
|
:members:
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
Available Plugins
|
Available Plugins
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
In this section we present all the plugins that are shipped along with Watcher.
|
||||||
|
If you want to know which plugins your Watcher services have access to, you can
|
||||||
|
use the :ref:`Guru Meditation Reports <watcher_gmr>` to display them.
|
||||||
|
|
||||||
.. _watcher_strategies:
|
.. _watcher_strategies:
|
||||||
|
|
||||||
Strategies
|
Strategies
|
||||||
|
|||||||
@@ -99,14 +99,14 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
|
|||||||
Cluster Data Model
|
Cluster Data Model
|
||||||
==================
|
==================
|
||||||
|
|
||||||
.. watcher-term:: watcher.metrics_engine.cluster_model_collector.api
|
.. watcher-term:: watcher.metrics_engine.cluster_model_collector.base
|
||||||
|
|
||||||
.. _cluster_history_definition:
|
.. _cluster_history_definition:
|
||||||
|
|
||||||
Cluster History
|
Cluster History
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. watcher-term:: watcher.metrics_engine.cluster_history.api
|
.. watcher-term:: watcher.metrics_engine.cluster_history.base
|
||||||
|
|
||||||
.. _controller_node_definition:
|
.. _controller_node_definition:
|
||||||
|
|
||||||
@@ -223,8 +223,8 @@ measure of how much of the :ref:`Goal <goal_definition>` has been achieved in
|
|||||||
respect with constraints and :ref:`SLAs <sla_definition>` defined by the
|
respect with constraints and :ref:`SLAs <sla_definition>` defined by the
|
||||||
:ref:`Customer <customer_definition>`.
|
:ref:`Customer <customer_definition>`.
|
||||||
|
|
||||||
The way efficacy is evaluated will depend on the
|
The way efficacy is evaluated will depend on the :ref:`Goal <goal_definition>`
|
||||||
:ref:`Goal <goal_definition>` to achieve.
|
to achieve.
|
||||||
|
|
||||||
Of course, the efficacy will be relevant only as long as the
|
Of course, the efficacy will be relevant only as long as the
|
||||||
:ref:`Action Plan <action_plan_definition>` is relevant
|
:ref:`Action Plan <action_plan_definition>` is relevant
|
||||||
@@ -323,7 +323,7 @@ Solution
|
|||||||
Strategy
|
Strategy
|
||||||
========
|
========
|
||||||
|
|
||||||
.. watcher-term:: watcher.decision_engine.strategy.strategies.base
|
.. watcher-term:: watcher.api.controllers.v1.strategy
|
||||||
|
|
||||||
.. _watcher_applier_definition:
|
.. _watcher_applier_definition:
|
||||||
|
|
||||||
|
|||||||
14
doc/source/image_src/plantuml/README.rst
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
plantuml
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
To build an image from a source file, you have to upload the plantuml JAR file
|
||||||
|
available on http://plantuml.com/download.html.
|
||||||
|
After, just run this command to build your image:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$ cd doc/source/images
|
||||||
|
$ java -jar /path/to/plantuml.jar doc/source/image_src/plantuml/my_image.txt
|
||||||
|
$ ls doc/source/images/
|
||||||
|
my_image.png
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
actor Administrator
|
actor Administrator
|
||||||
|
|
||||||
Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid>
|
Administrator -> "Watcher CLI" : watcher audit create -a <audit_template>
|
||||||
|
|
||||||
"Watcher CLI" -> "Watcher API" : POST audit(parameters)
|
"Watcher CLI" -> "Watcher API" : POST audit(parameters)
|
||||||
"Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING)
|
"Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING)
|
||||||
@@ -14,7 +14,7 @@ Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid>
|
|||||||
Administrator <-- "Watcher CLI" : new audit uuid
|
Administrator <-- "Watcher CLI" : new audit uuid
|
||||||
|
|
||||||
"Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid)
|
"Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid)
|
||||||
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid)
|
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid) (status=ONGOING)
|
||||||
|
|
||||||
ref over "Watcher Decision Engine"
|
ref over "Watcher Decision Engine"
|
||||||
Trigger audit in the
|
Trigger audit in the
|
||||||
|
|||||||
@@ -2,15 +2,21 @@
|
|||||||
|
|
||||||
actor Administrator
|
actor Administrator
|
||||||
|
|
||||||
Administrator -> "Watcher CLI" : watcher audit-template-create <name> <goal>
|
Administrator -> "Watcher CLI" : watcher audittemplate create <name> <goal> \
|
||||||
|
[--strategy-uuid <strategy>]
|
||||||
"Watcher CLI" -> "Watcher API" : POST audit_template(parameters)
|
"Watcher CLI" -> "Watcher API" : POST audit_template(parameters)
|
||||||
|
|
||||||
"Watcher API" -> "Watcher API" : make sure goal exist in configuration
|
"Watcher API" -> "Watcher Database" : Request if goal exists in database
|
||||||
"Watcher API" -> "Watcher Database" : create new audit_template in database
|
"Watcher API" <-- "Watcher Database" : OK
|
||||||
|
|
||||||
"Watcher API" <-- "Watcher Database" : new audit template uuid
|
"Watcher API" -> "Watcher Database" : Request if strategy exists in database (if provided)
|
||||||
"Watcher CLI" <-- "Watcher API" : return new audit template URL in HTTP Location Header
|
"Watcher API" <-- "Watcher Database" : OK
|
||||||
Administrator <-- "Watcher CLI" : new audit template uuid
|
|
||||||
|
"Watcher API" -> "Watcher Database" : Create new audit_template in database
|
||||||
|
"Watcher API" <-- "Watcher Database" : New audit template UUID
|
||||||
|
|
||||||
|
"Watcher CLI" <-- "Watcher API" : Return new audit template URL in HTTP Location Header
|
||||||
|
Administrator <-- "Watcher CLI" : New audit template UUID
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
actor Administrator
|
actor Administrator
|
||||||
|
|
||||||
Administrator -> "Watcher CLI" : watcher action-plan-start <action_plan_uuid>
|
Administrator -> "Watcher CLI" : watcher actionplan start <action_plan_uuid>
|
||||||
|
|
||||||
"Watcher CLI" -> "Watcher API" : PATCH action_plan(state=TRIGGERED)
|
"Watcher CLI" -> "Watcher API" : PATCH action_plan(state=PENDING)
|
||||||
"Watcher API" -> "Watcher Database" : action_plan.state=TRIGGERED
|
"Watcher API" -> "Watcher Database" : action_plan.state=PENDING
|
||||||
|
|
||||||
"Watcher CLI" <-- "Watcher API" : HTTP 200
|
"Watcher CLI" <-- "Watcher API" : HTTP 200
|
||||||
|
|
||||||
|
|||||||
87
doc/source/image_src/plantuml/watcher_class_diagram.txt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
abstract class Base {
|
||||||
|
// Timestamp mixin
|
||||||
|
DateTime created_at
|
||||||
|
DateTime updated_at
|
||||||
|
|
||||||
|
// Soft Delete mixin
|
||||||
|
DateTime deleted_at
|
||||||
|
Integer deleted // default = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class Strategy {
|
||||||
|
**Integer id** // primary_key
|
||||||
|
String uuid // length = 36
|
||||||
|
String name // length = 63, nullable = false
|
||||||
|
String display_name // length = 63, nullable = false
|
||||||
|
<i>Integer goal_id</i> // ForeignKey('goals.id'), nullable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Goal {
|
||||||
|
**Integer id** // primary_key
|
||||||
|
String uuid // length = 36
|
||||||
|
String name // length = 63, nullable = false
|
||||||
|
String display_name // length = 63, nullable=False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AuditTemplate {
|
||||||
|
**Integer id** // primary_key
|
||||||
|
String uuid // length = 36
|
||||||
|
String name // length = 63, nullable = true
|
||||||
|
String description // length = 255, nullable = true
|
||||||
|
Integer host_aggregate // nullable = true
|
||||||
|
<i>Integer goal_id</i> // ForeignKey('goals.id'), nullable = false
|
||||||
|
<i>Integer strategy_id</i> // ForeignKey('strategies.id'), nullable = true
|
||||||
|
JsonString extra
|
||||||
|
String version // length = 15, nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Audit {
|
||||||
|
**Integer id** // primary_key
|
||||||
|
String uuid // length = 36
|
||||||
|
String type // length = 20
|
||||||
|
String state // length = 20, nullable = true
|
||||||
|
DateTime deadline // nullable = true
|
||||||
|
<i>Integer audit_template_id</i> // ForeignKey('audit_templates.id') \
|
||||||
|
nullable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Action {
|
||||||
|
**Integer id** // primary_key
|
||||||
|
String uuid // length = 36, nullable = false
|
||||||
|
<i>Integer action_plan_id</i> // ForeignKey('action_plans.id'), nullable = false
|
||||||
|
String action_type // length = 255, nullable = false
|
||||||
|
JsonString input_parameters // nullable = true
|
||||||
|
String state // length = 20, nullable = true
|
||||||
|
String next // length = 36, nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ActionPlan {
|
||||||
|
**Integer id** // primary_key
|
||||||
|
String uuid // length = 36
|
||||||
|
Integer first_action_id //
|
||||||
|
<i>Integer audit_id</i> // ForeignKey('audits.id'), nullable = true
|
||||||
|
String state // length = 20, nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
"Base" <|-- "Strategy"
|
||||||
|
"Base" <|-- "Goal"
|
||||||
|
"Base" <|-- "AuditTemplate"
|
||||||
|
"Base" <|-- "Audit"
|
||||||
|
"Base" <|-- "Action"
|
||||||
|
"Base" <|-- "ActionPlan"
|
||||||
|
|
||||||
|
"Goal" <.. "Strategy" : Foreign Key
|
||||||
|
"Goal" <.. "AuditTemplate" : Foreign Key
|
||||||
|
"Strategy" <.. "AuditTemplate" : Foreign Key
|
||||||
|
"AuditTemplate" <.. "Audit" : Foreign Key
|
||||||
|
"ActionPlan" <.. "Action" : Foreign Key
|
||||||
|
"Audit" <.. "ActionPlan" : Foreign Key
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
BIN
doc/source/images/watcher_class_diagram.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
@@ -90,6 +90,7 @@ Introduction
|
|||||||
|
|
||||||
deploy/installation
|
deploy/installation
|
||||||
deploy/user-guide
|
deploy/user-guide
|
||||||
|
deploy/gmr
|
||||||
|
|
||||||
Watcher Manual Pages
|
Watcher Manual Pages
|
||||||
====================
|
====================
|
||||||
|
|||||||
@@ -5,31 +5,34 @@
|
|||||||
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
||||||
jsonpatch>=1.1 # BSD
|
jsonpatch>=1.1 # BSD
|
||||||
keystoneauth1>=2.1.0 # Apache-2.0
|
keystoneauth1>=2.1.0 # Apache-2.0
|
||||||
keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0
|
keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
|
||||||
oslo.config>=3.7.0 # Apache-2.0
|
oslo.concurrency>=3.8.0 # Apache-2.0
|
||||||
oslo.context>=0.2.0 # Apache-2.0
|
oslo.cache>=1.5.0 # Apache-2.0
|
||||||
|
oslo.config>=3.9.0 # Apache-2.0
|
||||||
|
oslo.context>=2.2.0 # Apache-2.0
|
||||||
oslo.db>=4.1.0 # Apache-2.0
|
oslo.db>=4.1.0 # Apache-2.0
|
||||||
oslo.i18n>=2.1.0 # Apache-2.0
|
oslo.i18n>=2.1.0 # Apache-2.0
|
||||||
oslo.log>=1.14.0 # Apache-2.0
|
oslo.log>=1.14.0 # Apache-2.0
|
||||||
oslo.messaging>=4.0.0 # Apache-2.0
|
oslo.messaging>=4.5.0 # Apache-2.0
|
||||||
oslo.policy>=0.5.0 # Apache-2.0
|
oslo.policy>=0.5.0 # Apache-2.0
|
||||||
oslo.service>=1.0.0 # Apache-2.0
|
oslo.reports>=0.6.0 # Apache-2.0
|
||||||
|
oslo.service>=1.10.0 # Apache-2.0
|
||||||
oslo.utils>=3.5.0 # Apache-2.0
|
oslo.utils>=3.5.0 # Apache-2.0
|
||||||
PasteDeploy>=1.5.0 # MIT
|
PasteDeploy>=1.5.0 # MIT
|
||||||
pbr>=1.6 # Apache-2.0
|
pbr>=1.6 # Apache-2.0
|
||||||
pecan>=1.0.0 # BSD
|
pecan>=1.0.0 # BSD
|
||||||
PrettyTable<0.8,>=0.7 # BSD
|
PrettyTable<0.8,>=0.7 # BSD
|
||||||
voluptuous>=0.8.6 # BSD License
|
voluptuous>=0.8.9 # BSD License
|
||||||
python-ceilometerclient>=2.2.1 # Apache-2.0
|
python-ceilometerclient>=2.2.1 # Apache-2.0
|
||||||
python-cinderclient>=1.3.1 # Apache-2.0
|
python-cinderclient!=1.7.0,>=1.6.0 # Apache-2.0
|
||||||
python-glanceclient>=2.0.0 # Apache-2.0
|
python-glanceclient>=2.0.0 # Apache-2.0
|
||||||
python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0
|
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
|
||||||
python-neutronclient!=4.1.0,>=2.6.0 # Apache-2.0
|
python-neutronclient>=4.2.0 # Apache-2.0
|
||||||
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
|
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
|
||||||
python-openstackclient>=2.1.0 # Apache-2.0
|
python-openstackclient>=2.1.0 # Apache-2.0
|
||||||
six>=1.9.0 # MIT
|
six>=1.9.0 # MIT
|
||||||
SQLAlchemy<1.1.0,>=1.0.10 # MIT
|
SQLAlchemy<1.1.0,>=1.0.10 # MIT
|
||||||
stevedore>=1.5.0 # Apache-2.0
|
stevedore>=1.10.0 # Apache-2.0
|
||||||
taskflow>=1.26.0 # Apache-2.0
|
taskflow>=1.26.0 # Apache-2.0
|
||||||
WebOb>=1.2.3 # MIT
|
WebOb>=1.2.3 # MIT
|
||||||
WSME>=0.8 # MIT
|
WSME>=0.8 # MIT
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ watcher_strategies =
|
|||||||
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
||||||
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
||||||
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
|
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
|
||||||
|
workload_stabilization = watcher.decision_engine.strategy.strategies.workload_stabilization:WorkloadStabilization
|
||||||
|
workload_balance = watcher.decision_engine.strategy.strategies.workload_balance:WorkloadBalance
|
||||||
|
|
||||||
watcher_actions =
|
watcher_actions =
|
||||||
migrate = watcher.applier.actions.migration:Migrate
|
migrate = watcher.applier.actions.migration:Migrate
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ discover # BSD
|
|||||||
doc8 # Apache-2.0
|
doc8 # Apache-2.0
|
||||||
freezegun # Apache-2.0
|
freezegun # Apache-2.0
|
||||||
hacking<0.11,>=0.10.2
|
hacking<0.11,>=0.10.2
|
||||||
mock>=1.2 # BSD
|
mock>=2.0 # BSD
|
||||||
oslotest>=1.10.0 # Apache-2.0
|
oslotest>=1.10.0 # Apache-2.0
|
||||||
os-testr>=0.4.1 # Apache-2.0
|
os-testr>=0.4.1 # Apache-2.0
|
||||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
import oslo_i18n
|
import oslo_i18n
|
||||||
|
from oslo_i18n import _lazy
|
||||||
|
|
||||||
# The domain is the name of the App which is used to generate the folder
|
# The domain is the name of the App which is used to generate the folder
|
||||||
# containing the translation files (i.e. the .pot file and the various locales)
|
# containing the translation files (i.e. the .pot file and the various locales)
|
||||||
@@ -42,5 +43,9 @@ _LE = _translators.log_error
|
|||||||
_LC = _translators.log_critical
|
_LC = _translators.log_critical
|
||||||
|
|
||||||
|
|
||||||
|
def lazy_translation_enabled():
|
||||||
|
return _lazy.USE_LAZY
|
||||||
|
|
||||||
|
|
||||||
def get_available_languages():
|
def get_available_languages():
|
||||||
return oslo_i18n.get_available_languages(DOMAIN)
|
return oslo_i18n.get_available_languages(DOMAIN)
|
||||||
|
|||||||
@@ -19,24 +19,38 @@
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.api import acl
|
from watcher.api import acl
|
||||||
from watcher.api import config as api_config
|
from watcher.api import config as api_config
|
||||||
from watcher.api import middleware
|
from watcher.api import middleware
|
||||||
from watcher.decision_engine.strategy.selection import default \
|
|
||||||
as strategy_selector
|
|
||||||
|
|
||||||
# Register options for the service
|
# Register options for the service
|
||||||
API_SERVICE_OPTS = [
|
API_SERVICE_OPTS = [
|
||||||
cfg.IntOpt('port',
|
cfg.PortOpt('port',
|
||||||
default=9322,
|
default=9322,
|
||||||
help='The port for the watcher API server'),
|
help=_('The port for the watcher API server')),
|
||||||
cfg.StrOpt('host',
|
cfg.StrOpt('host',
|
||||||
default='0.0.0.0',
|
default='0.0.0.0',
|
||||||
help='The listen IP for the watcher API server'),
|
help=_('The listen IP for the watcher API server')),
|
||||||
cfg.IntOpt('max_limit',
|
cfg.IntOpt('max_limit',
|
||||||
default=1000,
|
default=1000,
|
||||||
help='The maximum number of items returned in a single '
|
help=_('The maximum number of items returned in a single '
|
||||||
'response from a collection resource.')
|
'response from a collection resource')),
|
||||||
|
cfg.IntOpt('workers',
|
||||||
|
min=1,
|
||||||
|
help=_('Number of workers for Watcher API service. '
|
||||||
|
'The default is equal to the number of CPUs available '
|
||||||
|
'if that can be determined, else a default worker '
|
||||||
|
'count of 1 is returned.')),
|
||||||
|
|
||||||
|
cfg.BoolOpt('enable_ssl_api',
|
||||||
|
default=False,
|
||||||
|
help=_("Enable the integrated stand-alone API to service "
|
||||||
|
"requests via HTTPS instead of HTTP. If there is a "
|
||||||
|
"front-end service performing HTTPS offloading from "
|
||||||
|
"the service, this option should be False; note, you "
|
||||||
|
"will want to change public API endpoint to represent "
|
||||||
|
"SSL termination URL with 'public_endpoint' option.")),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@@ -45,7 +59,6 @@ opt_group = cfg.OptGroup(name='api',
|
|||||||
|
|
||||||
CONF.register_group(opt_group)
|
CONF.register_group(opt_group)
|
||||||
CONF.register_opts(API_SERVICE_OPTS, opt_group)
|
CONF.register_opts(API_SERVICE_OPTS, opt_group)
|
||||||
CONF.register_opts(strategy_selector.WATCHER_GOALS_OPTS)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pecan_config():
|
def get_pecan_config():
|
||||||
@@ -68,3 +81,12 @@ def setup_app(config=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return acl.install(app, CONF, config.app.acl_public_routes)
|
return acl.install(app, CONF, config.app.acl_public_routes)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionSelectorApplication(object):
|
||||||
|
def __init__(self):
|
||||||
|
pc = get_pecan_config()
|
||||||
|
self.v1 = setup_app(config=pc)
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
return self.v1(environ, start_response)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from watcher.api.controllers.v1 import action_plan
|
|||||||
from watcher.api.controllers.v1 import audit
|
from watcher.api.controllers.v1 import audit
|
||||||
from watcher.api.controllers.v1 import audit_template
|
from watcher.api.controllers.v1 import audit_template
|
||||||
from watcher.api.controllers.v1 import goal
|
from watcher.api.controllers.v1 import goal
|
||||||
|
from watcher.api.controllers.v1 import strategy
|
||||||
|
|
||||||
|
|
||||||
class APIBase(wtypes.Base):
|
class APIBase(wtypes.Base):
|
||||||
@@ -157,6 +158,7 @@ class Controller(rest.RestController):
|
|||||||
actions = action.ActionsController()
|
actions = action.ActionsController()
|
||||||
action_plans = action_plan.ActionPlansController()
|
action_plans = action_plan.ActionPlansController()
|
||||||
goals = goal.GoalsController()
|
goals = goal.GoalsController()
|
||||||
|
strategies = strategy.StrategiesController()
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(V1)
|
@wsme_pecan.wsexpose(V1)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class Action(base.APIBase):
|
|||||||
self.action_next_uuid = None
|
self.action_next_uuid = None
|
||||||
# raise e
|
# raise e
|
||||||
|
|
||||||
uuid = types.uuid
|
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||||
"""Unique UUID for this action"""
|
"""Unique UUID for this action"""
|
||||||
|
|
||||||
action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid,
|
action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid,
|
||||||
@@ -130,9 +130,6 @@ class Action(base.APIBase):
|
|||||||
state = wtypes.text
|
state = wtypes.text
|
||||||
"""This audit state"""
|
"""This audit state"""
|
||||||
|
|
||||||
alarm = types.uuid
|
|
||||||
"""An alarm UUID related to this action"""
|
|
||||||
|
|
||||||
action_type = wtypes.text
|
action_type = wtypes.text
|
||||||
"""Action type"""
|
"""Action type"""
|
||||||
|
|
||||||
@@ -194,7 +191,6 @@ class Action(base.APIBase):
|
|||||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||||
description='action description',
|
description='action description',
|
||||||
state='PENDING',
|
state='PENDING',
|
||||||
alarm=None,
|
|
||||||
created_at=datetime.datetime.utcnow(),
|
created_at=datetime.datetime.utcnow(),
|
||||||
deleted_at=None,
|
deleted_at=None,
|
||||||
updated_at=datetime.datetime.utcnow())
|
updated_at=datetime.datetime.utcnow())
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class ActionPlan(base.APIBase):
|
|||||||
except exception.ActionNotFound:
|
except exception.ActionNotFound:
|
||||||
self._first_action_uuid = None
|
self._first_action_uuid = None
|
||||||
|
|
||||||
uuid = types.uuid
|
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||||
"""Unique UUID for this action plan"""
|
"""Unique UUID for this action plan"""
|
||||||
|
|
||||||
first_action_uuid = wsme.wsproperty(
|
first_action_uuid = wsme.wsproperty(
|
||||||
|
|||||||
@@ -49,6 +49,28 @@ from watcher.decision_engine import rpcapi
|
|||||||
from watcher import objects
|
from watcher import objects
|
||||||
|
|
||||||
|
|
||||||
|
class AuditPostType(wtypes.Base):
|
||||||
|
|
||||||
|
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=True)
|
||||||
|
|
||||||
|
type = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||||
|
|
||||||
|
deadline = wtypes.wsattr(datetime.datetime, mandatory=False)
|
||||||
|
|
||||||
|
state = wsme.wsattr(wtypes.text, readonly=True,
|
||||||
|
default=objects.audit.State.PENDING)
|
||||||
|
|
||||||
|
def as_audit(self):
|
||||||
|
audit_type_values = [val.value for val in objects.audit.AuditType]
|
||||||
|
if self.type not in audit_type_values:
|
||||||
|
raise exception.AuditTypeNotFound(audit_type=self.type)
|
||||||
|
|
||||||
|
return Audit(
|
||||||
|
audit_template_id=self.audit_template_uuid,
|
||||||
|
type=self.type,
|
||||||
|
deadline=self.deadline)
|
||||||
|
|
||||||
|
|
||||||
class AuditPatchType(types.JsonPatchType):
|
class AuditPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -325,12 +347,13 @@ class AuditsController(rest.RestController):
|
|||||||
audit_uuid)
|
audit_uuid)
|
||||||
return Audit.convert_with_links(rpc_audit)
|
return Audit.convert_with_links(rpc_audit)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(Audit, body=Audit, status_code=201)
|
@wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201)
|
||||||
def post(self, audit):
|
def post(self, audit_p):
|
||||||
"""Create a new audit.
|
"""Create a new audit.
|
||||||
|
|
||||||
:param audit: a audit within the request body.
|
:param audit_p: a audit within the request body.
|
||||||
"""
|
"""
|
||||||
|
audit = audit_p.as_audit()
|
||||||
if self.from_audits:
|
if self.from_audits:
|
||||||
raise exception.OperationNotPermitted
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
|||||||
@@ -50,42 +50,162 @@ provided as a list of key-value pairs.
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
import wsmeext.pecan as wsme_pecan
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.api.controllers import base
|
from watcher.api.controllers import base
|
||||||
from watcher.api.controllers import link
|
from watcher.api.controllers import link
|
||||||
from watcher.api.controllers.v1 import collection
|
from watcher.api.controllers.v1 import collection
|
||||||
from watcher.api.controllers.v1 import types
|
from watcher.api.controllers.v1 import types
|
||||||
from watcher.api.controllers.v1 import utils as api_utils
|
from watcher.api.controllers.v1 import utils as api_utils
|
||||||
|
from watcher.common import context as context_utils
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common import utils as common_utils
|
from watcher.common import utils as common_utils
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
|
|
||||||
|
|
||||||
|
class AuditTemplatePostType(wtypes.Base):
|
||||||
|
_ctx = context_utils.make_context()
|
||||||
|
|
||||||
|
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||||
|
"""Name of this audit template"""
|
||||||
|
|
||||||
|
description = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||||
|
"""Short description of this audit template"""
|
||||||
|
|
||||||
|
deadline = wsme.wsattr(datetime.datetime, mandatory=False)
|
||||||
|
"""deadline of the audit template"""
|
||||||
|
|
||||||
|
host_aggregate = wsme.wsattr(wtypes.IntegerType(minimum=1),
|
||||||
|
mandatory=False)
|
||||||
|
"""ID of the Nova host aggregate targeted by the audit template"""
|
||||||
|
|
||||||
|
extra = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False)
|
||||||
|
"""The metadata of the audit template"""
|
||||||
|
|
||||||
|
goal = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||||
|
"""Goal UUID or name of the audit template"""
|
||||||
|
|
||||||
|
strategy = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||||
|
"""Strategy UUID or name of the audit template"""
|
||||||
|
|
||||||
|
version = wtypes.text
|
||||||
|
"""Internal version of the audit template"""
|
||||||
|
|
||||||
|
def as_audit_template(self):
|
||||||
|
return AuditTemplate(
|
||||||
|
name=self.name,
|
||||||
|
description=self.description,
|
||||||
|
deadline=self.deadline,
|
||||||
|
host_aggregate=self.host_aggregate,
|
||||||
|
extra=self.extra,
|
||||||
|
goal_id=self.goal, # Dirty trick ...
|
||||||
|
goal=self.goal,
|
||||||
|
strategy_id=self.strategy, # Dirty trick ...
|
||||||
|
strategy_uuid=self.strategy,
|
||||||
|
version=self.version,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(audit_template):
|
||||||
|
available_goals = objects.Goal.list(AuditTemplatePostType._ctx)
|
||||||
|
available_goal_uuids_map = {g.uuid: g for g in available_goals}
|
||||||
|
available_goal_names_map = {g.name: g for g in available_goals}
|
||||||
|
if audit_template.goal in available_goal_uuids_map:
|
||||||
|
goal = available_goal_uuids_map[audit_template.goal]
|
||||||
|
elif audit_template.goal in available_goal_names_map:
|
||||||
|
goal = available_goal_names_map[audit_template.goal]
|
||||||
|
else:
|
||||||
|
raise exception.InvalidGoal(goal=audit_template.goal)
|
||||||
|
|
||||||
|
if audit_template.strategy:
|
||||||
|
available_strategies = objects.Strategy.list(
|
||||||
|
AuditTemplatePostType._ctx)
|
||||||
|
available_strategies_map = {
|
||||||
|
s.uuid: s for s in available_strategies}
|
||||||
|
if audit_template.strategy not in available_strategies_map:
|
||||||
|
raise exception.InvalidStrategy(
|
||||||
|
strategy=audit_template.strategy)
|
||||||
|
|
||||||
|
strategy = available_strategies_map[audit_template.strategy]
|
||||||
|
# Check that the strategy we indicate is actually related to the
|
||||||
|
# specified goal
|
||||||
|
if strategy.goal_id != goal.id:
|
||||||
|
choices = ["'%s' (%s)" % (s.uuid, s.name)
|
||||||
|
for s in available_strategies]
|
||||||
|
raise exception.InvalidStrategy(
|
||||||
|
message=_(
|
||||||
|
"'%(strategy)s' strategy does relate to the "
|
||||||
|
"'%(goal)s' goal. Possible choices: %(choices)s")
|
||||||
|
% dict(strategy=strategy.name, goal=goal.name,
|
||||||
|
choices=", ".join(choices)))
|
||||||
|
audit_template.strategy = strategy.uuid
|
||||||
|
|
||||||
|
# We force the UUID so that we do not need to query the DB with the
|
||||||
|
# name afterwards
|
||||||
|
audit_template.goal = goal.uuid
|
||||||
|
|
||||||
|
return audit_template
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplatePatchType(types.JsonPatchType):
|
class AuditTemplatePatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
_ctx = context_utils.make_context()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mandatory_attrs():
|
def mandatory_attrs():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(patch):
|
def validate(patch):
|
||||||
if patch.path == "/goal":
|
if patch.path == "/goal" and patch.op != "remove":
|
||||||
AuditTemplatePatchType._validate_goal(patch)
|
AuditTemplatePatchType._validate_goal(patch)
|
||||||
|
elif patch.path == "/goal" and patch.op == "remove":
|
||||||
|
raise exception.OperationNotPermitted(
|
||||||
|
_("Cannot remove 'goal' attribute "
|
||||||
|
"from an audit template"))
|
||||||
|
if patch.path == "/strategy":
|
||||||
|
AuditTemplatePatchType._validate_strategy(patch)
|
||||||
return types.JsonPatchType.validate(patch)
|
return types.JsonPatchType.validate(patch)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _validate_goal(patch):
|
def _validate_goal(patch):
|
||||||
serialized_patch = {'path': patch.path, 'op': patch.op}
|
patch.path = "/goal_id"
|
||||||
if patch.value is not wsme.Unset:
|
goal = patch.value
|
||||||
serialized_patch['value'] = patch.value
|
|
||||||
new_goal = patch.value
|
if goal:
|
||||||
if new_goal and new_goal not in cfg.CONF.watcher_goals.goals.keys():
|
available_goals = objects.Goal.list(
|
||||||
raise exception.InvalidGoal(goal=new_goal)
|
AuditTemplatePatchType._ctx)
|
||||||
|
available_goal_uuids_map = {g.uuid: g for g in available_goals}
|
||||||
|
available_goal_names_map = {g.name: g for g in available_goals}
|
||||||
|
if goal in available_goal_uuids_map:
|
||||||
|
patch.value = available_goal_uuids_map[goal].id
|
||||||
|
elif goal in available_goal_names_map:
|
||||||
|
patch.value = available_goal_names_map[goal].id
|
||||||
|
else:
|
||||||
|
raise exception.InvalidGoal(goal=goal)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_strategy(patch):
|
||||||
|
patch.path = "/strategy_id"
|
||||||
|
strategy = patch.value
|
||||||
|
if strategy:
|
||||||
|
available_strategies = objects.Strategy.list(
|
||||||
|
AuditTemplatePatchType._ctx)
|
||||||
|
available_strategy_uuids_map = {
|
||||||
|
s.uuid: s for s in available_strategies}
|
||||||
|
available_strategy_names_map = {
|
||||||
|
s.name: s for s in available_strategies}
|
||||||
|
if strategy in available_strategy_uuids_map:
|
||||||
|
patch.value = available_strategy_uuids_map[strategy].id
|
||||||
|
elif strategy in available_strategy_names_map:
|
||||||
|
patch.value = available_strategy_names_map[strategy].id
|
||||||
|
else:
|
||||||
|
raise exception.InvalidStrategy(strategy=strategy)
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplate(base.APIBase):
|
class AuditTemplate(base.APIBase):
|
||||||
@@ -95,7 +215,90 @@ class AuditTemplate(base.APIBase):
|
|||||||
between the internal object model and the API representation of an
|
between the internal object model and the API representation of an
|
||||||
audit template.
|
audit template.
|
||||||
"""
|
"""
|
||||||
uuid = types.uuid
|
|
||||||
|
_goal_uuid = None
|
||||||
|
_goal_name = None
|
||||||
|
|
||||||
|
_strategy_uuid = None
|
||||||
|
_strategy_name = None
|
||||||
|
|
||||||
|
def _get_goal(self, value):
|
||||||
|
if value == wtypes.Unset:
|
||||||
|
return None
|
||||||
|
goal = None
|
||||||
|
try:
|
||||||
|
if (common_utils.is_uuid_like(value) or
|
||||||
|
common_utils.is_int_like(value)):
|
||||||
|
goal = objects.Goal.get(
|
||||||
|
pecan.request.context, value)
|
||||||
|
else:
|
||||||
|
goal = objects.Goal.get_by_name(
|
||||||
|
pecan.request.context, value)
|
||||||
|
except exception.GoalNotFound:
|
||||||
|
pass
|
||||||
|
if goal:
|
||||||
|
self.goal_id = goal.id
|
||||||
|
return goal
|
||||||
|
|
||||||
|
def _get_strategy(self, value):
|
||||||
|
if value == wtypes.Unset:
|
||||||
|
return None
|
||||||
|
strategy = None
|
||||||
|
try:
|
||||||
|
if (common_utils.is_uuid_like(value) or
|
||||||
|
common_utils.is_int_like(value)):
|
||||||
|
strategy = objects.Strategy.get(
|
||||||
|
pecan.request.context, value)
|
||||||
|
else:
|
||||||
|
strategy = objects.Strategy.get_by_name(
|
||||||
|
pecan.request.context, value)
|
||||||
|
except exception.StrategyNotFound:
|
||||||
|
pass
|
||||||
|
if strategy:
|
||||||
|
self.strategy_id = strategy.id
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
def _get_goal_uuid(self):
|
||||||
|
return self._goal_uuid
|
||||||
|
|
||||||
|
def _set_goal_uuid(self, value):
|
||||||
|
if value and self._goal_uuid != value:
|
||||||
|
self._goal_uuid = None
|
||||||
|
goal = self._get_goal(value)
|
||||||
|
if goal:
|
||||||
|
self._goal_uuid = goal.uuid
|
||||||
|
|
||||||
|
def _get_strategy_uuid(self):
|
||||||
|
return self._strategy_uuid
|
||||||
|
|
||||||
|
def _set_strategy_uuid(self, value):
|
||||||
|
if value and self._strategy_uuid != value:
|
||||||
|
self._strategy_uuid = None
|
||||||
|
strategy = self._get_strategy(value)
|
||||||
|
if strategy:
|
||||||
|
self._strategy_uuid = strategy.uuid
|
||||||
|
|
||||||
|
def _get_goal_name(self):
|
||||||
|
return self._goal_name
|
||||||
|
|
||||||
|
def _set_goal_name(self, value):
|
||||||
|
if value and self._goal_name != value:
|
||||||
|
self._goal_name = None
|
||||||
|
goal = self._get_goal(value)
|
||||||
|
if goal:
|
||||||
|
self._goal_name = goal.name
|
||||||
|
|
||||||
|
def _get_strategy_name(self):
|
||||||
|
return self._strategy_name
|
||||||
|
|
||||||
|
def _set_strategy_name(self, value):
|
||||||
|
if value and self._strategy_name != value:
|
||||||
|
self._strategy_name = None
|
||||||
|
strategy = self._get_strategy(value)
|
||||||
|
if strategy:
|
||||||
|
self._strategy_name = strategy.name
|
||||||
|
|
||||||
|
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||||
"""Unique UUID for this audit template"""
|
"""Unique UUID for this audit template"""
|
||||||
|
|
||||||
name = wtypes.text
|
name = wtypes.text
|
||||||
@@ -113,8 +316,21 @@ class AuditTemplate(base.APIBase):
|
|||||||
extra = {wtypes.text: types.jsontype}
|
extra = {wtypes.text: types.jsontype}
|
||||||
"""The metadata of the audit template"""
|
"""The metadata of the audit template"""
|
||||||
|
|
||||||
goal = wtypes.text
|
goal_uuid = wsme.wsproperty(
|
||||||
"""Goal type of the audit template"""
|
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
|
||||||
|
"""Goal UUID the audit template refers to"""
|
||||||
|
|
||||||
|
goal_name = wsme.wsproperty(
|
||||||
|
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
|
||||||
|
"""The name of the goal this audit template refers to"""
|
||||||
|
|
||||||
|
strategy_uuid = wsme.wsproperty(
|
||||||
|
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
|
||||||
|
"""Strategy UUID the audit template refers to"""
|
||||||
|
|
||||||
|
strategy_name = wsme.wsproperty(
|
||||||
|
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
|
||||||
|
"""The name of the strategy this audit template refers to"""
|
||||||
|
|
||||||
version = wtypes.text
|
version = wtypes.text
|
||||||
"""Internal version of the audit template"""
|
"""Internal version of the audit template"""
|
||||||
@@ -127,20 +343,43 @@ class AuditTemplate(base.APIBase):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(AuditTemplate, self).__init__()
|
super(AuditTemplate, self).__init__()
|
||||||
|
|
||||||
self.fields = []
|
self.fields = []
|
||||||
for field in objects.AuditTemplate.fields:
|
fields = list(objects.AuditTemplate.fields)
|
||||||
|
|
||||||
|
for k in fields:
|
||||||
# Skip fields we do not expose.
|
# Skip fields we do not expose.
|
||||||
if not hasattr(self, field):
|
if not hasattr(self, k):
|
||||||
continue
|
continue
|
||||||
self.fields.append(field)
|
self.fields.append(k)
|
||||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
setattr(self, k, kwargs.get(k, wtypes.Unset))
|
||||||
|
|
||||||
|
self.fields.append('goal_id')
|
||||||
|
self.fields.append('strategy_id')
|
||||||
|
|
||||||
|
# goal_uuid & strategy_uuid are not part of
|
||||||
|
# objects.AuditTemplate.fields because they're API-only attributes.
|
||||||
|
self.fields.append('goal_uuid')
|
||||||
|
self.fields.append('goal_name')
|
||||||
|
self.fields.append('strategy_uuid')
|
||||||
|
self.fields.append('strategy_name')
|
||||||
|
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
|
||||||
|
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
|
||||||
|
setattr(self, 'strategy_uuid',
|
||||||
|
kwargs.get('strategy_id', wtypes.Unset))
|
||||||
|
setattr(self, 'strategy_name',
|
||||||
|
kwargs.get('strategy_id', wtypes.Unset))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(audit_template, url, expand=True):
|
def _convert_with_links(audit_template, url, expand=True):
|
||||||
if not expand:
|
if not expand:
|
||||||
audit_template.unset_fields_except(['uuid', 'name',
|
audit_template.unset_fields_except(
|
||||||
'host_aggregate', 'goal'])
|
['uuid', 'name', 'host_aggregate', 'goal_uuid', 'goal_name',
|
||||||
|
'strategy_uuid', 'strategy_name'])
|
||||||
|
|
||||||
|
# The numeric ID should not be exposed to
|
||||||
|
# the user, it's internal only.
|
||||||
|
audit_template.goal_id = wtypes.Unset
|
||||||
|
audit_template.strategy_id = wtypes.Unset
|
||||||
|
|
||||||
audit_template.links = [link.Link.make_link('self', url,
|
audit_template.links = [link.Link.make_link('self', url,
|
||||||
'audit_templates',
|
'audit_templates',
|
||||||
@@ -148,8 +387,7 @@ class AuditTemplate(base.APIBase):
|
|||||||
link.Link.make_link('bookmark', url,
|
link.Link.make_link('bookmark', url,
|
||||||
'audit_templates',
|
'audit_templates',
|
||||||
audit_template.uuid,
|
audit_template.uuid,
|
||||||
bookmark=True)
|
bookmark=True)]
|
||||||
]
|
|
||||||
return audit_template
|
return audit_template
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -164,19 +402,14 @@ class AuditTemplate(base.APIBase):
|
|||||||
name='My Audit Template',
|
name='My Audit Template',
|
||||||
description='Description of my audit template',
|
description='Description of my audit template',
|
||||||
host_aggregate=5,
|
host_aggregate=5,
|
||||||
goal='DUMMY',
|
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
|
||||||
|
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
|
||||||
extra={'automatic': True},
|
extra={'automatic': True},
|
||||||
created_at=datetime.datetime.utcnow(),
|
created_at=datetime.datetime.utcnow(),
|
||||||
deleted_at=None,
|
deleted_at=None,
|
||||||
updated_at=datetime.datetime.utcnow())
|
updated_at=datetime.datetime.utcnow())
|
||||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate(audit_template):
|
|
||||||
if audit_template.goal not in cfg.CONF.watcher_goals.goals.keys():
|
|
||||||
raise exception.InvalidGoal(audit_template.goal)
|
|
||||||
return audit_template
|
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplateCollection(collection.Collection):
|
class AuditTemplateCollection(collection.Collection):
|
||||||
"""API representation of a collection of audit templates."""
|
"""API representation of a collection of audit templates."""
|
||||||
@@ -191,12 +424,12 @@ class AuditTemplateCollection(collection.Collection):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
|
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
collection = AuditTemplateCollection()
|
at_collection = AuditTemplateCollection()
|
||||||
collection.audit_templates = \
|
at_collection.audit_templates = [
|
||||||
[AuditTemplate.convert_with_links(p, expand)
|
AuditTemplate.convert_with_links(p, expand)
|
||||||
for p in rpc_audit_templates]
|
for p in rpc_audit_templates]
|
||||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
at_collection.next = at_collection.get_next(limit, url=url, **kwargs)
|
||||||
return collection
|
return at_collection
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sample(cls):
|
def sample(cls):
|
||||||
@@ -222,7 +455,8 @@ class AuditTemplatesController(rest.RestController):
|
|||||||
sort_key, sort_dir, expand=False,
|
sort_key, sort_dir, expand=False,
|
||||||
resource_url=None):
|
resource_url=None):
|
||||||
api_utils.validate_search_filters(
|
api_utils.validate_search_filters(
|
||||||
filters, objects.audit_template.AuditTemplate.fields.keys())
|
filters, list(objects.audit_template.AuditTemplate.fields.keys()) +
|
||||||
|
["goal_uuid", "goal_name", "strategy_uuid", "strategy_name"])
|
||||||
limit = api_utils.validate_limit(limit)
|
limit = api_utils.validate_limit(limit)
|
||||||
api_utils.validate_sort_dir(sort_dir)
|
api_utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
@@ -246,30 +480,43 @@ class AuditTemplatesController(rest.RestController):
|
|||||||
sort_key=sort_key,
|
sort_key=sort_key,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text,
|
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
|
||||||
types.uuid, int, wtypes.text, wtypes.text)
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
def get_all(self, goal=None, marker=None, limit=None,
|
def get_all(self, goal=None, strategy=None, marker=None,
|
||||||
sort_key='id', sort_dir='asc'):
|
limit=None, sort_key='id', sort_dir='asc'):
|
||||||
"""Retrieve a list of audit templates.
|
"""Retrieve a list of audit templates.
|
||||||
|
|
||||||
:param goal: goal name to filter by (case sensitive)
|
:param goal: goal UUID or name to filter by
|
||||||
|
:param strategy: strategy UUID or name to filter by
|
||||||
:param marker: pagination marker for large data sets.
|
:param marker: pagination marker for large data sets.
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
:param sort_key: column to sort results by. Default: id.
|
:param sort_key: column to sort results by. Default: id.
|
||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
"""
|
"""
|
||||||
filters = api_utils.as_filters_dict(goal=goal)
|
filters = {}
|
||||||
|
if goal:
|
||||||
|
if common_utils.is_uuid_like(goal):
|
||||||
|
filters['goal_uuid'] = goal
|
||||||
|
else:
|
||||||
|
filters['goal_name'] = goal
|
||||||
|
|
||||||
|
if strategy:
|
||||||
|
if common_utils.is_uuid_like(strategy):
|
||||||
|
filters['strategy_uuid'] = strategy
|
||||||
|
else:
|
||||||
|
filters['strategy_name'] = strategy
|
||||||
|
|
||||||
return self._get_audit_templates_collection(
|
return self._get_audit_templates_collection(
|
||||||
filters, marker, limit, sort_key, sort_dir)
|
filters, marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, types.uuid, int,
|
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
|
||||||
wtypes.text, wtypes.text)
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
def detail(self, goal=None, marker=None, limit=None,
|
def detail(self, goal=None, strategy=None, marker=None,
|
||||||
sort_key='id', sort_dir='asc'):
|
limit=None, sort_key='id', sort_dir='asc'):
|
||||||
"""Retrieve a list of audit templates with detail.
|
"""Retrieve a list of audit templates with detail.
|
||||||
|
|
||||||
:param goal: goal name to filter by (case sensitive)
|
:param goal: goal UUID or name to filter by
|
||||||
|
:param strategy: strategy UUID or name to filter by
|
||||||
:param marker: pagination marker for large data sets.
|
:param marker: pagination marker for large data sets.
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
:param sort_key: column to sort results by. Default: id.
|
:param sort_key: column to sort results by. Default: id.
|
||||||
@@ -280,7 +527,18 @@ class AuditTemplatesController(rest.RestController):
|
|||||||
if parent != "audit_templates":
|
if parent != "audit_templates":
|
||||||
raise exception.HTTPNotFound
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
filters = api_utils.as_filters_dict(goal=goal)
|
filters = {}
|
||||||
|
if goal:
|
||||||
|
if common_utils.is_uuid_like(goal):
|
||||||
|
filters['goal_uuid'] = goal
|
||||||
|
else:
|
||||||
|
filters['goal_name'] = goal
|
||||||
|
|
||||||
|
if strategy:
|
||||||
|
if common_utils.is_uuid_like(strategy):
|
||||||
|
filters['strategy_uuid'] = strategy
|
||||||
|
else:
|
||||||
|
filters['strategy_name'] = strategy
|
||||||
|
|
||||||
expand = True
|
expand = True
|
||||||
resource_url = '/'.join(['audit_templates', 'detail'])
|
resource_url = '/'.join(['audit_templates', 'detail'])
|
||||||
@@ -308,19 +566,21 @@ class AuditTemplatesController(rest.RestController):
|
|||||||
|
|
||||||
return AuditTemplate.convert_with_links(rpc_audit_template)
|
return AuditTemplate.convert_with_links(rpc_audit_template)
|
||||||
|
|
||||||
@wsme.validate(types.uuid, AuditTemplate)
|
@wsme.validate(types.uuid, AuditTemplatePostType)
|
||||||
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplate, status_code=201)
|
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
|
||||||
def post(self, audit_template):
|
status_code=201)
|
||||||
|
def post(self, audit_template_postdata):
|
||||||
"""Create a new audit template.
|
"""Create a new audit template.
|
||||||
|
|
||||||
:param audit template: a audit template within the request body.
|
:param audit_template_postdata: the audit template POST data
|
||||||
|
from the request body.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.from_audit_templates:
|
if self.from_audit_templates:
|
||||||
raise exception.OperationNotPermitted
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
audit_template_dict = audit_template.as_dict()
|
|
||||||
context = pecan.request.context
|
context = pecan.request.context
|
||||||
|
audit_template = audit_template_postdata.as_audit_template()
|
||||||
|
audit_template_dict = audit_template.as_dict()
|
||||||
new_audit_template = objects.AuditTemplate(context,
|
new_audit_template = objects.AuditTemplate(context,
|
||||||
**audit_template_dict)
|
**audit_template_dict)
|
||||||
new_audit_template.create(context)
|
new_audit_template.create(context)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Collection(base.APIBase):
|
|||||||
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
|
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
|
||||||
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
|
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
|
||||||
'args': q_args, 'limit': limit,
|
'args': q_args, 'limit': limit,
|
||||||
'marker': self.collection[-1].uuid}
|
'marker': getattr(self.collection[-1], "uuid")}
|
||||||
|
|
||||||
return link.Link.make_link('next', pecan.request.host_url,
|
return link.Link.make_link('next', pecan.request.host_url,
|
||||||
resource_url, next_args).href
|
resource_url, next_args).href
|
||||||
|
|||||||
@@ -46,61 +46,64 @@ from watcher.api.controllers.v1 import collection
|
|||||||
from watcher.api.controllers.v1 import types
|
from watcher.api.controllers.v1 import types
|
||||||
from watcher.api.controllers.v1 import utils as api_utils
|
from watcher.api.controllers.v1 import utils as api_utils
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
|
from watcher.common import utils as common_utils
|
||||||
|
from watcher import objects
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class Goal(base.APIBase):
|
class Goal(base.APIBase):
|
||||||
"""API representation of a action.
|
"""API representation of a goal.
|
||||||
|
|
||||||
This class enforces type checking and value constraints, and converts
|
This class enforces type checking and value constraints, and converts
|
||||||
between the internal object model and the API representation of a action.
|
between the internal object model and the API representation of a goal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"""Unique UUID for this goal"""
|
||||||
|
|
||||||
name = wtypes.text
|
name = wtypes.text
|
||||||
"""Name of the goal"""
|
"""Name of the goal"""
|
||||||
|
|
||||||
strategy = wtypes.text
|
display_name = wtypes.text
|
||||||
"""The strategy associated with the goal"""
|
"""Localized name of the goal"""
|
||||||
|
|
||||||
uuid = types.uuid
|
|
||||||
"""Unused field"""
|
|
||||||
|
|
||||||
links = wsme.wsattr([link.Link], readonly=True)
|
links = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""A list containing a self link and associated action links"""
|
"""A list containing a self link and associated audit template links"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Goal, self).__init__()
|
super(Goal, self).__init__()
|
||||||
|
|
||||||
self.fields = []
|
self.fields = []
|
||||||
|
self.fields.append('uuid')
|
||||||
self.fields.append('name')
|
self.fields.append('name')
|
||||||
self.fields.append('strategy')
|
self.fields.append('display_name')
|
||||||
setattr(self, 'name', kwargs.get('name',
|
setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset))
|
||||||
wtypes.Unset))
|
setattr(self, 'name', kwargs.get('name', wtypes.Unset))
|
||||||
setattr(self, 'strategy', kwargs.get('strategy',
|
setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset))
|
||||||
wtypes.Unset))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(goal, url, expand=True):
|
def _convert_with_links(goal, url, expand=True):
|
||||||
if not expand:
|
if not expand:
|
||||||
goal.unset_fields_except(['name', 'strategy'])
|
goal.unset_fields_except(['uuid', 'name', 'display_name'])
|
||||||
|
|
||||||
goal.links = [link.Link.make_link('self', url,
|
goal.links = [link.Link.make_link('self', url,
|
||||||
'goals', goal.name),
|
'goals', goal.uuid),
|
||||||
link.Link.make_link('bookmark', url,
|
link.Link.make_link('bookmark', url,
|
||||||
'goals', goal.name,
|
'goals', goal.uuid,
|
||||||
bookmark=True)]
|
bookmark=True)]
|
||||||
return goal
|
return goal
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_with_links(cls, goal, expand=True):
|
def convert_with_links(cls, goal, expand=True):
|
||||||
goal = Goal(**goal)
|
goal = Goal(**goal.as_dict())
|
||||||
return cls._convert_with_links(goal, pecan.request.host_url, expand)
|
return cls._convert_with_links(goal, pecan.request.host_url, expand)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sample(cls, expand=True):
|
def sample(cls, expand=True):
|
||||||
sample = cls(name='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||||
strategy='action description')
|
name='DUMMY',
|
||||||
|
display_name='Dummy strategy')
|
||||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||||
|
|
||||||
|
|
||||||
@@ -117,27 +120,28 @@ class GoalCollection(collection.Collection):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_with_links(goals, limit, url=None, expand=False,
|
def convert_with_links(goals, limit, url=None, expand=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
|
goal_collection = GoalCollection()
|
||||||
collection = GoalCollection()
|
goal_collection.goals = [
|
||||||
collection.goals = [Goal.convert_with_links(g, expand) for g in goals]
|
Goal.convert_with_links(g, expand) for g in goals]
|
||||||
|
|
||||||
if 'sort_key' in kwargs:
|
if 'sort_key' in kwargs:
|
||||||
reverse = False
|
reverse = False
|
||||||
if kwargs['sort_key'] == 'strategy':
|
if kwargs['sort_key'] == 'strategy':
|
||||||
if 'sort_dir' in kwargs:
|
if 'sort_dir' in kwargs:
|
||||||
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
||||||
collection.goals = sorted(
|
goal_collection.goals = sorted(
|
||||||
collection.goals,
|
goal_collection.goals,
|
||||||
key=lambda goal: goal.name,
|
key=lambda goal: goal.uuid,
|
||||||
reverse=reverse)
|
reverse=reverse)
|
||||||
|
|
||||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
goal_collection.next = goal_collection.get_next(
|
||||||
return collection
|
limit, url=url, **kwargs)
|
||||||
|
return goal_collection
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sample(cls):
|
def sample(cls):
|
||||||
sample = cls()
|
sample = cls()
|
||||||
sample.actions = [Goal.sample(expand=False)]
|
sample.goals = [Goal.sample(expand=False)]
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
|
|
||||||
@@ -154,51 +158,49 @@ class GoalsController(rest.RestController):
|
|||||||
'detail': ['GET'],
|
'detail': ['GET'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_goals_collection(self, limit,
|
def _get_goals_collection(self, marker, limit, sort_key, sort_dir,
|
||||||
sort_key, sort_dir, expand=False,
|
expand=False, resource_url=None):
|
||||||
resource_url=None, goal_name=None):
|
|
||||||
|
|
||||||
limit = api_utils.validate_limit(limit)
|
limit = api_utils.validate_limit(limit)
|
||||||
api_utils.validate_sort_dir(sort_dir)
|
api_utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
goals = []
|
sort_db_key = (sort_key if sort_key in objects.Goal.fields.keys()
|
||||||
|
else None)
|
||||||
|
|
||||||
if not goal_name and goal_name in CONF.watcher_goals.goals.keys():
|
marker_obj = None
|
||||||
goals.append({'name': goal_name, 'strategy': goals[goal_name]})
|
if marker:
|
||||||
else:
|
marker_obj = objects.Goal.get_by_uuid(
|
||||||
for name, strategy in CONF.watcher_goals.goals.items():
|
pecan.request.context, marker)
|
||||||
goals.append({'name': name, 'strategy': strategy})
|
|
||||||
|
|
||||||
return GoalCollection.convert_with_links(goals[:limit], limit,
|
goals = objects.Goal.list(pecan.request.context, limit, marker_obj,
|
||||||
|
sort_key=sort_db_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return GoalCollection.convert_with_links(goals, limit,
|
||||||
url=resource_url,
|
url=resource_url,
|
||||||
expand=expand,
|
expand=expand,
|
||||||
sort_key=sort_key,
|
sort_key=sort_key,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(GoalCollection, int, wtypes.text, wtypes.text)
|
@wsme_pecan.wsexpose(GoalCollection, wtypes.text,
|
||||||
def get_all(self, limit=None,
|
int, wtypes.text, wtypes.text)
|
||||||
sort_key='name', sort_dir='asc'):
|
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
"""Retrieve a list of goals.
|
"""Retrieve a list of goals.
|
||||||
|
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
:param sort_key: column to sort results by. Default: id.
|
:param sort_key: column to sort results by. Default: id.
|
||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
to get only actions for that goal.
|
|
||||||
"""
|
"""
|
||||||
return self._get_goals_collection(limit, sort_key, sort_dir)
|
return self._get_goals_collection(marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
|
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
|
||||||
wtypes.text, wtypes.text)
|
wtypes.text, wtypes.text)
|
||||||
def detail(self, goal_name=None, limit=None,
|
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
sort_key='name', sort_dir='asc'):
|
"""Retrieve a list of goals with detail.
|
||||||
"""Retrieve a list of actions with detail.
|
|
||||||
|
|
||||||
:param goal_name: name of a goal, to get only goals for that
|
:param marker: pagination marker for large data sets.
|
||||||
action.
|
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
:param sort_key: column to sort results by. Default: id.
|
:param sort_key: column to sort results by. Default: id.
|
||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
to get only goals for that goal.
|
|
||||||
"""
|
"""
|
||||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||||
parent = pecan.request.path.split('/')[:-1][-1]
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
@@ -206,21 +208,23 @@ class GoalsController(rest.RestController):
|
|||||||
raise exception.HTTPNotFound
|
raise exception.HTTPNotFound
|
||||||
expand = True
|
expand = True
|
||||||
resource_url = '/'.join(['goals', 'detail'])
|
resource_url = '/'.join(['goals', 'detail'])
|
||||||
return self._get_goals_collection(limit, sort_key, sort_dir,
|
return self._get_goals_collection(marker, limit, sort_key, sort_dir,
|
||||||
expand, resource_url, goal_name)
|
expand, resource_url)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(Goal, wtypes.text)
|
@wsme_pecan.wsexpose(Goal, wtypes.text)
|
||||||
def get_one(self, goal_name):
|
def get_one(self, goal):
|
||||||
"""Retrieve information about the given goal.
|
"""Retrieve information about the given goal.
|
||||||
|
|
||||||
:param goal_name: name of the goal.
|
:param goal: UUID or name of the goal.
|
||||||
"""
|
"""
|
||||||
if self.from_goals:
|
if self.from_goals:
|
||||||
raise exception.OperationNotPermitted
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
goals = CONF.watcher_goals.goals
|
if common_utils.is_uuid_like(goal):
|
||||||
goal = {}
|
get_goal_func = objects.Goal.get_by_uuid
|
||||||
if goal_name in goals.keys():
|
else:
|
||||||
goal = {'name': goal_name, 'strategy': goals[goal_name]}
|
get_goal_func = objects.Goal.get_by_name
|
||||||
|
|
||||||
return Goal.convert_with_links(goal)
|
rpc_goal = get_goal_func(pecan.request.context, goal)
|
||||||
|
|
||||||
|
return Goal.convert_with_links(rpc_goal)
|
||||||
|
|||||||
281
watcher/api/controllers/v1/strategy.py
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2016 b<>com
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is
|
||||||
|
able to find a :ref:`Solution <solution_definition>` for a given
|
||||||
|
:ref:`Goal <goal_definition>`.
|
||||||
|
|
||||||
|
There may be several potential strategies which are able to achieve the same
|
||||||
|
:ref:`Goal <goal_definition>`. This is why it is possible to configure which
|
||||||
|
specific :ref:`Strategy <strategy_definition>` should be used for each goal.
|
||||||
|
|
||||||
|
Some strategies may provide better optimization results but may take more time
|
||||||
|
to find an optimal :ref:`Solution <solution_definition>`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from watcher.api.controllers import base
|
||||||
|
from watcher.api.controllers import link
|
||||||
|
from watcher.api.controllers.v1 import collection
|
||||||
|
from watcher.api.controllers.v1 import types
|
||||||
|
from watcher.api.controllers.v1 import utils as api_utils
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.common import utils as common_utils
|
||||||
|
from watcher import objects
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class Strategy(base.APIBase):
|
||||||
|
"""API representation of a strategy.
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of a strategy.
|
||||||
|
"""
|
||||||
|
_goal_uuid = None
|
||||||
|
|
||||||
|
def _get_goal(self, value):
|
||||||
|
if value == wtypes.Unset:
|
||||||
|
return None
|
||||||
|
goal = None
|
||||||
|
try:
|
||||||
|
if (common_utils.is_uuid_like(value) or
|
||||||
|
common_utils.is_int_like(value)):
|
||||||
|
goal = objects.Goal.get(pecan.request.context, value)
|
||||||
|
else:
|
||||||
|
goal = objects.Goal.get_by_name(pecan.request.context, value)
|
||||||
|
except exception.GoalNotFound:
|
||||||
|
pass
|
||||||
|
if goal:
|
||||||
|
self.goal_id = goal.id
|
||||||
|
return goal
|
||||||
|
|
||||||
|
def _get_goal_uuid(self):
|
||||||
|
return self._goal_uuid
|
||||||
|
|
||||||
|
def _set_goal_uuid(self, value):
|
||||||
|
if value and self._goal_uuid != value:
|
||||||
|
self._goal_uuid = None
|
||||||
|
goal = self._get_goal(value)
|
||||||
|
if goal:
|
||||||
|
self._goal_uuid = goal.uuid
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"""Unique UUID for this strategy"""
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"""Name of the strategy"""
|
||||||
|
|
||||||
|
display_name = wtypes.text
|
||||||
|
"""Localized name of the strategy"""
|
||||||
|
|
||||||
|
links = wsme.wsattr([link.Link], readonly=True)
|
||||||
|
"""A list containing a self link and associated goal links"""
|
||||||
|
|
||||||
|
goal_uuid = wsme.wsproperty(wtypes.text, _get_goal_uuid, _set_goal_uuid,
|
||||||
|
mandatory=True)
|
||||||
|
"""The UUID of the goal this audit refers to"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Strategy, self).__init__()
|
||||||
|
|
||||||
|
self.fields = []
|
||||||
|
self.fields.append('uuid')
|
||||||
|
self.fields.append('name')
|
||||||
|
self.fields.append('display_name')
|
||||||
|
self.fields.append('goal_uuid')
|
||||||
|
setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset))
|
||||||
|
setattr(self, 'name', kwargs.get('name', wtypes.Unset))
|
||||||
|
setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset))
|
||||||
|
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _convert_with_links(strategy, url, expand=True):
|
||||||
|
if not expand:
|
||||||
|
strategy.unset_fields_except(
|
||||||
|
['uuid', 'name', 'display_name', 'goal_uuid'])
|
||||||
|
|
||||||
|
strategy.links = [
|
||||||
|
link.Link.make_link('self', url, 'strategies', strategy.uuid),
|
||||||
|
link.Link.make_link('bookmark', url, 'strategies', strategy.uuid,
|
||||||
|
bookmark=True)]
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, strategy, expand=True):
|
||||||
|
strategy = Strategy(**strategy.as_dict())
|
||||||
|
return cls._convert_with_links(
|
||||||
|
strategy, pecan.request.host_url, expand)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls, expand=True):
|
||||||
|
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||||
|
name='DUMMY',
|
||||||
|
display_name='Dummy strategy')
|
||||||
|
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of strategies."""
|
||||||
|
|
||||||
|
strategies = [Strategy]
|
||||||
|
"""A list containing strategies objects"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(StrategyCollection, self).__init__()
|
||||||
|
self._type = 'strategies'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_with_links(strategies, limit, url=None, expand=False,
|
||||||
|
**kwargs):
|
||||||
|
strategy_collection = StrategyCollection()
|
||||||
|
strategy_collection.strategies = [
|
||||||
|
Strategy.convert_with_links(g, expand) for g in strategies]
|
||||||
|
|
||||||
|
if 'sort_key' in kwargs:
|
||||||
|
reverse = False
|
||||||
|
if kwargs['sort_key'] == 'strategy':
|
||||||
|
if 'sort_dir' in kwargs:
|
||||||
|
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
||||||
|
strategy_collection.strategies = sorted(
|
||||||
|
strategy_collection.strategies,
|
||||||
|
key=lambda strategy: strategy.uuid,
|
||||||
|
reverse=reverse)
|
||||||
|
|
||||||
|
strategy_collection.next = strategy_collection.get_next(
|
||||||
|
limit, url=url, **kwargs)
|
||||||
|
return strategy_collection
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = cls()
|
||||||
|
sample.strategies = [Strategy.sample(expand=False)]
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
class StrategiesController(rest.RestController):
|
||||||
|
"""REST controller for Strategies."""
|
||||||
|
def __init__(self):
|
||||||
|
super(StrategiesController, self).__init__()
|
||||||
|
|
||||||
|
from_strategies = False
|
||||||
|
"""A flag to indicate if the requests to this controller are coming
|
||||||
|
from the top-level resource Strategies."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_strategies_collection(self, filters, marker, limit, sort_key,
|
||||||
|
sort_dir, expand=False, resource_url=None):
|
||||||
|
api_utils.validate_search_filters(
|
||||||
|
filters, list(objects.strategy.Strategy.fields.keys()) +
|
||||||
|
["goal_uuid", "goal_name"])
|
||||||
|
limit = api_utils.validate_limit(limit)
|
||||||
|
api_utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
sort_db_key = (sort_key if sort_key in objects.Strategy.fields.keys()
|
||||||
|
else None)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.Strategy.get_by_uuid(
|
||||||
|
pecan.request.context, marker)
|
||||||
|
|
||||||
|
strategies = objects.Strategy.list(
|
||||||
|
pecan.request.context, limit, marker_obj, filters=filters,
|
||||||
|
sort_key=sort_db_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return StrategyCollection.convert_with_links(
|
||||||
|
strategies, limit, url=resource_url, expand=expand,
|
||||||
|
sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text,
|
||||||
|
int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, goal=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of strategies.
|
||||||
|
|
||||||
|
:param goal: goal UUID or name to filter by.
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
|
:param limit: maximum number of resources to return in a single result.
|
||||||
|
:param sort_key: column to sort results by. Default: id.
|
||||||
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
|
"""
|
||||||
|
filters = {}
|
||||||
|
if goal:
|
||||||
|
if common_utils.is_uuid_like(goal):
|
||||||
|
filters['goal_uuid'] = goal
|
||||||
|
else:
|
||||||
|
filters['goal_name'] = goal
|
||||||
|
|
||||||
|
return self._get_strategies_collection(
|
||||||
|
filters, marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, goal=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of strategies with detail.
|
||||||
|
|
||||||
|
:param goal: goal UUID or name to filter by.
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
|
:param limit: maximum number of resources to return in a single result.
|
||||||
|
:param sort_key: column to sort results by. Default: id.
|
||||||
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
|
"""
|
||||||
|
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "strategies":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['strategies', 'detail'])
|
||||||
|
|
||||||
|
filters = {}
|
||||||
|
if goal:
|
||||||
|
if common_utils.is_uuid_like(goal):
|
||||||
|
filters['goal_uuid'] = goal
|
||||||
|
else:
|
||||||
|
filters['goal_name'] = goal
|
||||||
|
|
||||||
|
return self._get_strategies_collection(
|
||||||
|
filters, marker, limit, sort_key, sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(Strategy, wtypes.text)
|
||||||
|
def get_one(self, strategy):
|
||||||
|
"""Retrieve information about the given strategy.
|
||||||
|
|
||||||
|
:param strategy: UUID or name of the strategy.
|
||||||
|
"""
|
||||||
|
if self.from_strategies:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
if common_utils.is_uuid_like(strategy):
|
||||||
|
get_strategy_func = objects.Strategy.get_by_uuid
|
||||||
|
else:
|
||||||
|
get_strategy_func = objects.Strategy.get_by_name
|
||||||
|
|
||||||
|
rpc_strategy = get_strategy_func(pecan.request.context, strategy)
|
||||||
|
|
||||||
|
return Strategy.convert_with_links(rpc_strategy)
|
||||||
@@ -31,11 +31,6 @@ class UuidOrNameType(wtypes.UserType):
|
|||||||
|
|
||||||
basetype = wtypes.text
|
basetype = wtypes.text
|
||||||
name = 'uuid_or_name'
|
name = 'uuid_or_name'
|
||||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
|
||||||
# to get the name of the type by accessing it's __name__ attribute.
|
|
||||||
# Remove this __name__ attribute once it's fixed in WSME.
|
|
||||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
|
||||||
__name__ = name
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(value):
|
def validate(value):
|
||||||
@@ -55,11 +50,6 @@ class NameType(wtypes.UserType):
|
|||||||
|
|
||||||
basetype = wtypes.text
|
basetype = wtypes.text
|
||||||
name = 'name'
|
name = 'name'
|
||||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
|
||||||
# to get the name of the type by accessing it's __name__ attribute.
|
|
||||||
# Remove this __name__ attribute once it's fixed in WSME.
|
|
||||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
|
||||||
__name__ = name
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(value):
|
def validate(value):
|
||||||
@@ -79,11 +69,6 @@ class UuidType(wtypes.UserType):
|
|||||||
|
|
||||||
basetype = wtypes.text
|
basetype = wtypes.text
|
||||||
name = 'uuid'
|
name = 'uuid'
|
||||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
|
||||||
# to get the name of the type by accessing it's __name__ attribute.
|
|
||||||
# Remove this __name__ attribute once it's fixed in WSME.
|
|
||||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
|
||||||
__name__ = name
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(value):
|
def validate(value):
|
||||||
@@ -103,11 +88,6 @@ class BooleanType(wtypes.UserType):
|
|||||||
|
|
||||||
basetype = wtypes.text
|
basetype = wtypes.text
|
||||||
name = 'boolean'
|
name = 'boolean'
|
||||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
|
||||||
# to get the name of the type by accessing it's __name__ attribute.
|
|
||||||
# Remove this __name__ attribute once it's fixed in WSME.
|
|
||||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
|
||||||
__name__ = name
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(value):
|
def validate(value):
|
||||||
@@ -129,11 +109,6 @@ class JsonType(wtypes.UserType):
|
|||||||
|
|
||||||
basetype = wtypes.text
|
basetype = wtypes.text
|
||||||
name = 'json'
|
name = 'json'
|
||||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
|
||||||
# to get the name of the type by accessing it's __name__ attribute.
|
|
||||||
# Remove this __name__ attribute once it's fixed in WSME.
|
|
||||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
|
||||||
__name__ = name
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# These are the json serializable native types
|
# These are the json serializable native types
|
||||||
|
|||||||
@@ -23,18 +23,26 @@ import abc
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
from watcher.common import clients
|
from watcher.common import clients
|
||||||
|
from watcher.common.loader import loadable
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseAction(object):
|
class BaseAction(loadable.Loadable):
|
||||||
# NOTE(jed) by convention we decided
|
# NOTE(jed) by convention we decided
|
||||||
# that the attribute "resource_id" is the unique id of
|
# that the attribute "resource_id" is the unique id of
|
||||||
# the resource to which the Action applies to allow us to use it in the
|
# the resource to which the Action applies to allow us to use it in the
|
||||||
# watcher dashboard and will be nested in input_parameters
|
# watcher dashboard and will be nested in input_parameters
|
||||||
RESOURCE_ID = 'resource_id'
|
RESOURCE_ID = 'resource_id'
|
||||||
|
|
||||||
def __init__(self, osc=None):
|
def __init__(self, config, osc=None):
|
||||||
""":param osc: an OpenStackClients instance"""
|
"""Constructor
|
||||||
|
|
||||||
|
:param config: A mapping containing the configuration of this action
|
||||||
|
:type config: dict
|
||||||
|
:param osc: an OpenStackClients instance, defaults to None
|
||||||
|
:type osc: :py:class:`~.OpenStackClients` instance, optional
|
||||||
|
"""
|
||||||
|
super(BaseAction, self).__init__(config)
|
||||||
self._input_parameters = {}
|
self._input_parameters = {}
|
||||||
self._osc = osc
|
self._osc = osc
|
||||||
|
|
||||||
@@ -56,6 +64,15 @@ class BaseAction(object):
|
|||||||
def resource_id(self):
|
def resource_id(self):
|
||||||
return self.input_parameters[self.RESOURCE_ID]
|
return self.input_parameters[self.RESOURCE_ID]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_opts(cls):
|
||||||
|
"""Defines the configuration options to be associated to this loadable
|
||||||
|
|
||||||
|
:return: A list of configuration options relative to this Loadable
|
||||||
|
:rtype: list of :class:`oslo_config.cfg.Opt` instances
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def execute(self):
|
def execute(self):
|
||||||
"""Executes the main logic of the action
|
"""Executes the main logic of the action
|
||||||
|
|||||||
@@ -17,12 +17,8 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.common.loader import default
|
from watcher.common.loader import default
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultActionLoader(default.DefaultLoader):
|
class DefaultActionLoader(default.DefaultLoader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -31,27 +31,24 @@ LOG = log.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Migrate(base.BaseAction):
|
class Migrate(base.BaseAction):
|
||||||
"""Live-Migrates a server to a destination nova-compute host
|
"""Migrates a server to a destination nova-compute host
|
||||||
|
|
||||||
This action will allow you to migrate a server to another compute
|
This action will allow you to migrate a server to another compute
|
||||||
destination host. As of now, only live migration can be performed using
|
destination host.
|
||||||
this action.
|
Migration type 'live' can only be used for migrating active VMs.
|
||||||
.. If either host uses shared storage, you can use ``live``
|
Migration type 'cold' can be used for migrating non-active VMs
|
||||||
.. as ``migration_type``. If both source and destination hosts provide
|
as well active VMs, which will be shut down while migrating.
|
||||||
.. local disks, you can set the block_migration parameter to True (not
|
|
||||||
.. supported for yet).
|
|
||||||
|
|
||||||
The action schema is::
|
The action schema is::
|
||||||
|
|
||||||
schema = Schema({
|
schema = Schema({
|
||||||
'resource_id': str, # should be a UUID
|
'resource_id': str, # should be a UUID
|
||||||
'migration_type': str, # choices -> "live" only
|
'migration_type': str, # choices -> "live", "cold"
|
||||||
'dst_hypervisor': str,
|
'dst_hypervisor': str,
|
||||||
'src_hypervisor': str,
|
'src_hypervisor': str,
|
||||||
})
|
})
|
||||||
|
|
||||||
The `resource_id` is the UUID of the server to migrate. Only live migration
|
The `resource_id` is the UUID of the server to migrate.
|
||||||
is supported.
|
|
||||||
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
|
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
|
||||||
source and the destination compute hostname (list of available compute
|
source and the destination compute hostname (list of available compute
|
||||||
hosts is returned by this command: ``nova service-list --binary
|
hosts is returned by this command: ``nova service-list --binary
|
||||||
@@ -61,6 +58,7 @@ class Migrate(base.BaseAction):
|
|||||||
# input parameters constants
|
# input parameters constants
|
||||||
MIGRATION_TYPE = 'migration_type'
|
MIGRATION_TYPE = 'migration_type'
|
||||||
LIVE_MIGRATION = 'live'
|
LIVE_MIGRATION = 'live'
|
||||||
|
COLD_MIGRATION = 'cold'
|
||||||
DST_HYPERVISOR = 'dst_hypervisor'
|
DST_HYPERVISOR = 'dst_hypervisor'
|
||||||
SRC_HYPERVISOR = 'src_hypervisor'
|
SRC_HYPERVISOR = 'src_hypervisor'
|
||||||
|
|
||||||
@@ -77,7 +75,8 @@ class Migrate(base.BaseAction):
|
|||||||
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
||||||
voluptuous.Required(self.MIGRATION_TYPE,
|
voluptuous.Required(self.MIGRATION_TYPE,
|
||||||
default=self.LIVE_MIGRATION):
|
default=self.LIVE_MIGRATION):
|
||||||
voluptuous.Any(*[self.LIVE_MIGRATION]),
|
voluptuous.Any(*[self.LIVE_MIGRATION,
|
||||||
|
self.COLD_MIGRATION]),
|
||||||
voluptuous.Required(self.DST_HYPERVISOR):
|
voluptuous.Required(self.DST_HYPERVISOR):
|
||||||
voluptuous.All(voluptuous.Any(*six.string_types),
|
voluptuous.All(voluptuous.Any(*six.string_types),
|
||||||
voluptuous.Length(min=1)),
|
voluptuous.Length(min=1)),
|
||||||
@@ -127,14 +126,30 @@ class Migrate(base.BaseAction):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _cold_migrate_instance(self, nova, destination):
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
result = nova.watcher_non_live_migrate_instance(
|
||||||
|
instance_id=self.instance_uuid,
|
||||||
|
dest_hostname=destination)
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
LOG.critical(_LC("Unexpected error occured. Migration failed for"
|
||||||
|
"instance %s. Leaving instance on previous "
|
||||||
|
"host."), self.instance_uuid)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def migrate(self, destination):
|
def migrate(self, destination):
|
||||||
nova = nova_helper.NovaHelper(osc=self.osc)
|
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||||
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
|
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
|
||||||
destination)
|
destination)
|
||||||
instance = nova.find_instance(self.instance_uuid)
|
instance = nova.find_instance(self.instance_uuid)
|
||||||
if instance:
|
if instance:
|
||||||
if self.migration_type == 'live':
|
if self.migration_type == self.LIVE_MIGRATION:
|
||||||
return self._live_migrate_instance(nova, destination)
|
return self._live_migrate_instance(nova, destination)
|
||||||
|
elif self.migration_type == self.COLD_MIGRATION:
|
||||||
|
return self._cold_migrate_instance(nova, destination)
|
||||||
else:
|
else:
|
||||||
raise exception.Invalid(
|
raise exception.Invalid(
|
||||||
message=(_('Migration of type %(migration_type)s is not '
|
message=(_('Migration of type %(migration_type)s is not '
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from oslo_config import cfg
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.messaging import trigger
|
from watcher.applier.messaging import trigger
|
||||||
from watcher.common.messaging import messaging_core
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@@ -63,17 +62,15 @@ CONF.register_group(opt_group)
|
|||||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||||
|
|
||||||
|
|
||||||
class ApplierManager(messaging_core.MessagingCore):
|
class ApplierManager(object):
|
||||||
def __init__(self):
|
|
||||||
super(ApplierManager, self).__init__(
|
|
||||||
CONF.watcher_applier.publisher_id,
|
|
||||||
CONF.watcher_applier.conductor_topic,
|
|
||||||
CONF.watcher_applier.status_topic,
|
|
||||||
api_version=self.API_VERSION,
|
|
||||||
)
|
|
||||||
self.conductor_topic_handler.add_endpoint(
|
|
||||||
trigger.TriggerActionPlan(self))
|
|
||||||
|
|
||||||
def join(self):
|
API_VERSION = '1.0'
|
||||||
self.conductor_topic_handler.join()
|
|
||||||
self.status_topic_handler.join()
|
conductor_endpoints = [trigger.TriggerActionPlan]
|
||||||
|
status_endpoints = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.publisher_id = CONF.watcher_applier.publisher_id
|
||||||
|
self.conductor_topic = CONF.watcher_applier.conductor_topic
|
||||||
|
self.status_topic = CONF.watcher_applier.status_topic
|
||||||
|
self.api_version = self.API_VERSION
|
||||||
|
|||||||
@@ -18,48 +18,43 @@
|
|||||||
#
|
#
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
import oslo_messaging as om
|
|
||||||
|
|
||||||
from watcher.applier.manager import APPLIER_MANAGER_OPTS
|
from watcher.applier import manager
|
||||||
from watcher.applier.manager import opt_group
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common.messaging import messaging_core
|
|
||||||
from watcher.common.messaging import notification_handler as notification
|
from watcher.common.messaging import notification_handler as notification
|
||||||
|
from watcher.common import service
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_group(opt_group)
|
CONF.register_group(manager.opt_group)
|
||||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
CONF.register_opts(manager.APPLIER_MANAGER_OPTS, manager.opt_group)
|
||||||
|
|
||||||
|
|
||||||
class ApplierAPI(messaging_core.MessagingCore):
|
class ApplierAPI(service.Service):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ApplierAPI, self).__init__(
|
super(ApplierAPI, self).__init__(ApplierAPIManager)
|
||||||
CONF.watcher_applier.publisher_id,
|
|
||||||
CONF.watcher_applier.conductor_topic,
|
|
||||||
CONF.watcher_applier.status_topic,
|
|
||||||
api_version=self.API_VERSION,
|
|
||||||
)
|
|
||||||
self.handler = notification.NotificationHandler(self.publisher_id)
|
|
||||||
self.handler.register_observer(self)
|
|
||||||
self.status_topic_handler.add_endpoint(self.handler)
|
|
||||||
transport = om.get_transport(CONF)
|
|
||||||
|
|
||||||
target = om.Target(
|
|
||||||
topic=CONF.watcher_applier.conductor_topic,
|
|
||||||
version=self.API_VERSION,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client = om.RPCClient(transport, target,
|
|
||||||
serializer=self.serializer)
|
|
||||||
|
|
||||||
def launch_action_plan(self, context, action_plan_uuid=None):
|
def launch_action_plan(self, context, action_plan_uuid=None):
|
||||||
if not utils.is_uuid_like(action_plan_uuid):
|
if not utils.is_uuid_like(action_plan_uuid):
|
||||||
raise exception.InvalidUuidOrName(name=action_plan_uuid)
|
raise exception.InvalidUuidOrName(name=action_plan_uuid)
|
||||||
|
|
||||||
return self.client.call(
|
return self.conductor_client.call(
|
||||||
context.to_dict(), 'launch_action_plan',
|
context.to_dict(), 'launch_action_plan',
|
||||||
action_plan_uuid=action_plan_uuid)
|
action_plan_uuid=action_plan_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplierAPIManager(object):
|
||||||
|
|
||||||
|
API_VERSION = '1.0'
|
||||||
|
|
||||||
|
conductor_endpoints = []
|
||||||
|
status_endpoints = [notification.NotificationHandler]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.publisher_id = CONF.watcher_applier.publisher_id
|
||||||
|
self.conductor_topic = CONF.watcher_applier.conductor_topic
|
||||||
|
self.status_topic = CONF.watcher_applier.status_topic
|
||||||
|
self.api_version = self.API_VERSION
|
||||||
|
|||||||
@@ -23,18 +23,38 @@ import six
|
|||||||
from watcher.applier.actions import factory
|
from watcher.applier.actions import factory
|
||||||
from watcher.applier.messaging import event_types
|
from watcher.applier.messaging import event_types
|
||||||
from watcher.common import clients
|
from watcher.common import clients
|
||||||
|
from watcher.common.loader import loadable
|
||||||
from watcher.common.messaging.events import event
|
from watcher.common.messaging.events import event
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseWorkFlowEngine(object):
|
class BaseWorkFlowEngine(loadable.Loadable):
|
||||||
def __init__(self, context=None, applier_manager=None):
|
|
||||||
|
def __init__(self, config, context=None, applier_manager=None):
|
||||||
|
"""Constructor
|
||||||
|
|
||||||
|
:param config: A mapping containing the configuration of this
|
||||||
|
workflow engine
|
||||||
|
:type config: dict
|
||||||
|
:param osc: an OpenStackClients object, defaults to None
|
||||||
|
:type osc: :py:class:`~.OpenStackClients` instance, optional
|
||||||
|
"""
|
||||||
|
super(BaseWorkFlowEngine, self).__init__(config)
|
||||||
self._context = context
|
self._context = context
|
||||||
self._applier_manager = applier_manager
|
self._applier_manager = applier_manager
|
||||||
self._action_factory = factory.ActionFactory()
|
self._action_factory = factory.ActionFactory()
|
||||||
self._osc = None
|
self._osc = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_opts(cls):
|
||||||
|
"""Defines the configuration options to be associated to this loadable
|
||||||
|
|
||||||
|
:return: A list of configuration options relative to this Loadable
|
||||||
|
:rtype: list of :class:`oslo_config.cfg.Opt` instances
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def context(self):
|
def context(self):
|
||||||
return self._context
|
return self._context
|
||||||
|
|||||||
@@ -17,12 +17,8 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.common.loader import default
|
from watcher.common.loader import default
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultWorkFlowEngineLoader(default.DefaultLoader):
|
class DefaultWorkFlowEngineLoader(default.DefaultLoader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -17,19 +17,14 @@
|
|||||||
|
|
||||||
"""Starter script for the Watcher API service."""
|
"""Starter script for the Watcher API service."""
|
||||||
|
|
||||||
import logging as std_logging
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from wsgiref import simple_server
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from watcher._i18n import _
|
from watcher._i18n import _LI
|
||||||
from watcher.api import app as api_app
|
|
||||||
from watcher.common import service
|
from watcher.common import service
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@@ -37,22 +32,20 @@ CONF = cfg.CONF
|
|||||||
def main():
|
def main():
|
||||||
service.prepare_service(sys.argv)
|
service.prepare_service(sys.argv)
|
||||||
|
|
||||||
app = api_app.setup_app()
|
|
||||||
|
|
||||||
# Create the WSGI server and start it
|
|
||||||
host, port = cfg.CONF.api.host, cfg.CONF.api.port
|
host, port = cfg.CONF.api.host, cfg.CONF.api.port
|
||||||
srv = simple_server.make_server(host, port, app)
|
protocol = "http" if not CONF.api.enable_ssl_api else "https"
|
||||||
|
# Build and start the WSGI app
|
||||||
LOG.info(_('Starting server in PID %s') % os.getpid())
|
server = service.WSGIService(
|
||||||
LOG.debug("Watcher configuration:")
|
'watcher-api', CONF.api.enable_ssl_api)
|
||||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
|
||||||
|
|
||||||
if host == '0.0.0.0':
|
if host == '0.0.0.0':
|
||||||
LOG.info(_('serving on 0.0.0.0:%(port)s, '
|
LOG.info(_LI('serving on 0.0.0.0:%(port)s, '
|
||||||
'view at http://127.0.0.1:%(port)s') %
|
'view at %(protocol)s://127.0.0.1:%(port)s') %
|
||||||
dict(port=port))
|
dict(protocol=protocol, port=port))
|
||||||
else:
|
else:
|
||||||
LOG.info(_('serving on http://%(host)s:%(port)s') %
|
LOG.info(_LI('serving on %(protocol)s://%(host)s:%(port)s') %
|
||||||
dict(host=host, port=port))
|
dict(protocol=protocol, host=host, port=port))
|
||||||
|
|
||||||
srv.serve_forever()
|
launcher = service.process_launcher()
|
||||||
|
launcher.launch_service(server, workers=server.workers)
|
||||||
|
launcher.wait()
|
||||||
|
|||||||
@@ -17,29 +17,26 @@
|
|||||||
|
|
||||||
"""Starter script for the Applier service."""
|
"""Starter script for the Applier service."""
|
||||||
|
|
||||||
import logging as std_logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
from watcher import _i18n
|
from watcher._i18n import _LI
|
||||||
from watcher.applier import manager
|
from watcher.applier import manager
|
||||||
from watcher.common import service
|
from watcher.common import service as watcher_service
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
_LI = _i18n._LI
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
service.prepare_service(sys.argv)
|
watcher_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
LOG.info(_LI('Starting server in PID %s') % os.getpid())
|
LOG.info(_LI('Starting Watcher Applier service in PID %s'), os.getpid())
|
||||||
LOG.debug("Configuration:")
|
|
||||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
|
||||||
|
|
||||||
server = manager.ApplierManager()
|
applier_service = watcher_service.Service(manager.ApplierManager)
|
||||||
server.connect()
|
launcher = service.launch(CONF, applier_service)
|
||||||
server.join()
|
launcher.wait()
|
||||||
|
|||||||
@@ -17,30 +17,31 @@
|
|||||||
|
|
||||||
"""Starter script for the Decision Engine manager service."""
|
"""Starter script for the Decision Engine manager service."""
|
||||||
|
|
||||||
import logging as std_logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
from watcher import _i18n
|
from watcher._i18n import _LI
|
||||||
from watcher.common import service
|
from watcher.common import service as watcher_service
|
||||||
from watcher.decision_engine import manager
|
from watcher.decision_engine import manager
|
||||||
|
from watcher.decision_engine import sync
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
_LI = _i18n._LI
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
service.prepare_service(sys.argv)
|
watcher_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
LOG.info(_LI('Starting server in PID %s') % os.getpid())
|
LOG.info(_LI('Starting Watcher Decision Engine service in PID %s'),
|
||||||
LOG.debug("Configuration:")
|
os.getpid())
|
||||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
|
||||||
|
|
||||||
server = manager.DecisionEngineManager()
|
syncer = sync.Syncer()
|
||||||
server.connect()
|
syncer.sync()
|
||||||
server.join()
|
|
||||||
|
de_service = watcher_service.Service(manager.DecisionEngineManager)
|
||||||
|
launcher = service.launch(CONF, de_service)
|
||||||
|
launcher.wait()
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class CeilometerHelper(object):
|
|||||||
|
|
||||||
item_value = None
|
item_value = None
|
||||||
if statistic:
|
if statistic:
|
||||||
item_value = statistic[-1]._info.get('aggregate').get('avg')
|
item_value = statistic[-1]._info.get('aggregate').get(aggregate)
|
||||||
return item_value
|
return item_value
|
||||||
|
|
||||||
def get_last_sample_values(self, resource_id, meter_name, limit=1):
|
def get_last_sample_values(self, resource_id, meter_name, limit=1):
|
||||||
|
|||||||
@@ -147,11 +147,15 @@ class ResourceNotFound(ObjectNotFound):
|
|||||||
|
|
||||||
|
|
||||||
class InvalidIdentity(Invalid):
|
class InvalidIdentity(Invalid):
|
||||||
msg_fmt = _("Expected an uuid or int but received %(identity)s")
|
msg_fmt = _("Expected a uuid or int but received %(identity)s")
|
||||||
|
|
||||||
|
|
||||||
class InvalidGoal(Invalid):
|
class InvalidGoal(Invalid):
|
||||||
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file")
|
msg_fmt = _("Goal %(goal)s is invalid")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidStrategy(Invalid):
|
||||||
|
msg_fmt = _("Strategy %(strategy)s is invalid")
|
||||||
|
|
||||||
|
|
||||||
class InvalidUUID(Invalid):
|
class InvalidUUID(Invalid):
|
||||||
@@ -166,12 +170,28 @@ class InvalidUuidOrName(Invalid):
|
|||||||
msg_fmt = _("Expected a logical name or uuid but received %(name)s")
|
msg_fmt = _("Expected a logical name or uuid but received %(name)s")
|
||||||
|
|
||||||
|
|
||||||
|
class GoalNotFound(ResourceNotFound):
|
||||||
|
msg_fmt = _("Goal %(goal)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
|
class GoalAlreadyExists(Conflict):
|
||||||
|
msg_fmt = _("A goal with UUID %(uuid)s already exists")
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyNotFound(ResourceNotFound):
|
||||||
|
msg_fmt = _("Strategy %(strategy)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyAlreadyExists(Conflict):
|
||||||
|
msg_fmt = _("A strategy with UUID %(uuid)s already exists")
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplateNotFound(ResourceNotFound):
|
class AuditTemplateNotFound(ResourceNotFound):
|
||||||
msg_fmt = _("AuditTemplate %(audit_template)s could not be found")
|
msg_fmt = _("AuditTemplate %(audit_template)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplateAlreadyExists(Conflict):
|
class AuditTemplateAlreadyExists(Conflict):
|
||||||
msg_fmt = _("An audit_template with UUID %(uuid)s or name %(name)s "
|
msg_fmt = _("An audit_template with UUID or name %(audit_template)s "
|
||||||
"already exists")
|
"already exists")
|
||||||
|
|
||||||
|
|
||||||
@@ -180,6 +200,10 @@ class AuditTemplateReferenced(Invalid):
|
|||||||
"multiple audit")
|
"multiple audit")
|
||||||
|
|
||||||
|
|
||||||
|
class AuditTypeNotFound(Invalid):
|
||||||
|
msg_fmt = _("Audit type %(audit_type)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
class AuditNotFound(ResourceNotFound):
|
class AuditNotFound(ResourceNotFound):
|
||||||
msg_fmt = _("Audit %(audit)s could not be found")
|
msg_fmt = _("Audit %(audit)s could not be found")
|
||||||
|
|
||||||
@@ -267,7 +291,19 @@ class MetricCollectorNotDefined(WatcherException):
|
|||||||
|
|
||||||
|
|
||||||
class ClusterStateNotDefined(WatcherException):
|
class ClusterStateNotDefined(WatcherException):
|
||||||
msg_fmt = _("the cluster state is not defined")
|
msg_fmt = _("The cluster state is not defined")
|
||||||
|
|
||||||
|
|
||||||
|
class NoAvailableStrategyForGoal(WatcherException):
|
||||||
|
msg_fmt = _("No strategy could be found to achieve the '%(goal)s' goal.")
|
||||||
|
|
||||||
|
|
||||||
|
class NoMetricValuesForVM(WatcherException):
|
||||||
|
msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchMetricForHost(WatcherException):
|
||||||
|
msg_fmt = _("No %(metric)s metric for %(host)s found.")
|
||||||
|
|
||||||
|
|
||||||
# Model
|
# Model
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
# Copyright (c) 2015 b<>com
|
# Copyright (c) 2016 b<>com
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,33 +16,82 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from stevedore.driver import DriverManager
|
from stevedore import driver as drivermanager
|
||||||
from stevedore import ExtensionManager
|
from stevedore import extension as extensionmanager
|
||||||
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common.loader.base import BaseLoader
|
from watcher.common.loader import base
|
||||||
|
from watcher.common import utils
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DefaultLoader(BaseLoader):
|
class DefaultLoader(base.BaseLoader):
|
||||||
def __init__(self, namespace):
|
|
||||||
|
def __init__(self, namespace, conf=cfg.CONF):
|
||||||
|
"""Entry point loader for Watcher using Stevedore
|
||||||
|
|
||||||
|
:param namespace: namespace of the entry point(s) to load or list
|
||||||
|
:type namespace: str
|
||||||
|
:param conf: ConfigOpts instance, defaults to cfg.CONF
|
||||||
|
"""
|
||||||
super(DefaultLoader, self).__init__()
|
super(DefaultLoader, self).__init__()
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
def load(self, name, **kwargs):
|
def load(self, name, **kwargs):
|
||||||
try:
|
try:
|
||||||
LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
|
LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
|
||||||
driver_manager = DriverManager(namespace=self.namespace,
|
driver_manager = drivermanager.DriverManager(
|
||||||
name=name)
|
namespace=self.namespace,
|
||||||
loaded = driver_manager.driver
|
name=name,
|
||||||
|
invoke_on_load=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
driver_cls = driver_manager.driver
|
||||||
|
config = self._load_plugin_config(name, driver_cls)
|
||||||
|
|
||||||
|
driver = driver_cls(config, **kwargs)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOG.exception(exc)
|
LOG.exception(exc)
|
||||||
raise exception.LoadingError(name=name)
|
raise exception.LoadingError(name=name)
|
||||||
|
|
||||||
return loaded(**kwargs)
|
return driver
|
||||||
|
|
||||||
|
def _reload_config(self):
|
||||||
|
self.conf()
|
||||||
|
|
||||||
|
def get_entry_name(self, name):
|
||||||
|
return ".".join([self.namespace, name])
|
||||||
|
|
||||||
|
def _load_plugin_config(self, name, driver_cls):
|
||||||
|
"""Load the config of the plugin"""
|
||||||
|
config = utils.Struct()
|
||||||
|
config_opts = driver_cls.get_config_opts()
|
||||||
|
|
||||||
|
if not config_opts:
|
||||||
|
return config
|
||||||
|
|
||||||
|
group_name = self.get_entry_name(name)
|
||||||
|
self.conf.register_opts(config_opts, group=group_name)
|
||||||
|
|
||||||
|
# Finalise the opt import by re-checking the configuration
|
||||||
|
# against the provided config files
|
||||||
|
self._reload_config()
|
||||||
|
|
||||||
|
config_group = self.conf.get(group_name)
|
||||||
|
if not config_group:
|
||||||
|
raise exception.LoadingError(name=name)
|
||||||
|
|
||||||
|
config.update({
|
||||||
|
name: value for name, value in config_group.items()
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
def list_available(self):
|
def list_available(self):
|
||||||
extension_manager = ExtensionManager(namespace=self.namespace)
|
extension_manager = extensionmanager.ExtensionManager(
|
||||||
|
namespace=self.namespace)
|
||||||
return {ext.name: ext.plugin for ext in extension_manager.extensions}
|
return {ext.name: ext.plugin for ext in extension_manager.extensions}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
# Copyright (c) 2015 b<>com
|
# Copyright (c) 2016 b<>com
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -13,16 +13,29 @@
|
|||||||
# implied.
|
# implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class FakeLoadable(object):
|
class Loadable(object):
|
||||||
@classmethod
|
"""Generic interface for dynamically loading a driver/entry point.
|
||||||
def namespace(cls):
|
|
||||||
return "TESTING"
|
This defines the contract in order to let the loader manager inject
|
||||||
|
the configuration parameters during the loading.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_name(cls):
|
@abc.abstractmethod
|
||||||
return 'fake'
|
def get_config_opts(cls):
|
||||||
|
"""Defines the configuration options to be associated to this loadable
|
||||||
|
|
||||||
|
:return: A list of configuration options relative to this Loadable
|
||||||
|
:rtype: list of :class:`oslo_config.cfg.Opt` instances
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
# Copyright (c) 2015 b<>com
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log
|
|
||||||
import oslo_messaging as om
|
|
||||||
|
|
||||||
from watcher.common.messaging.events import event_dispatcher as dispatcher
|
|
||||||
from watcher.common.messaging import messaging_handler
|
|
||||||
from watcher.common import rpc
|
|
||||||
|
|
||||||
from watcher.objects import base
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
CONF = cfg.CONF
|
|
||||||
|
|
||||||
|
|
||||||
class MessagingCore(dispatcher.EventDispatcher):
|
|
||||||
|
|
||||||
API_VERSION = '1.0'
|
|
||||||
|
|
||||||
def __init__(self, publisher_id, conductor_topic, status_topic,
|
|
||||||
api_version=API_VERSION):
|
|
||||||
super(MessagingCore, self).__init__()
|
|
||||||
self.serializer = rpc.RequestContextSerializer(
|
|
||||||
base.WatcherObjectSerializer())
|
|
||||||
self.publisher_id = publisher_id
|
|
||||||
self.api_version = api_version
|
|
||||||
|
|
||||||
self.conductor_topic = conductor_topic
|
|
||||||
self.status_topic = status_topic
|
|
||||||
self.conductor_topic_handler = self.build_topic_handler(
|
|
||||||
conductor_topic)
|
|
||||||
self.status_topic_handler = self.build_topic_handler(status_topic)
|
|
||||||
|
|
||||||
self._conductor_client = None
|
|
||||||
self._status_client = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def conductor_client(self):
|
|
||||||
if self._conductor_client is None:
|
|
||||||
transport = om.get_transport(CONF)
|
|
||||||
target = om.Target(
|
|
||||||
topic=self.conductor_topic,
|
|
||||||
version=self.API_VERSION,
|
|
||||||
)
|
|
||||||
self._conductor_client = om.RPCClient(
|
|
||||||
transport, target, serializer=self.serializer)
|
|
||||||
return self._conductor_client
|
|
||||||
|
|
||||||
@conductor_client.setter
|
|
||||||
def conductor_client(self, c):
|
|
||||||
self.conductor_client = c
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status_client(self):
|
|
||||||
if self._status_client is None:
|
|
||||||
transport = om.get_transport(CONF)
|
|
||||||
target = om.Target(
|
|
||||||
topic=self.status_topic,
|
|
||||||
version=self.API_VERSION,
|
|
||||||
)
|
|
||||||
self._status_client = om.RPCClient(
|
|
||||||
transport, target, serializer=self.serializer)
|
|
||||||
return self._status_client
|
|
||||||
|
|
||||||
@status_client.setter
|
|
||||||
def status_client(self, c):
|
|
||||||
self.status_client = c
|
|
||||||
|
|
||||||
def build_topic_handler(self, topic_name):
|
|
||||||
return messaging_handler.MessagingHandler(
|
|
||||||
self.publisher_id, topic_name, self,
|
|
||||||
self.api_version, self.serializer)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
LOG.debug("Connecting to '%s' (%s)",
|
|
||||||
CONF.transport_url, CONF.rpc_backend)
|
|
||||||
self.conductor_topic_handler.start()
|
|
||||||
self.status_topic_handler.start()
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
LOG.debug("Disconnecting from '%s' (%s)",
|
|
||||||
CONF.transport_url, CONF.rpc_backend)
|
|
||||||
self.conductor_topic_handler.stop()
|
|
||||||
self.status_topic_handler.stop()
|
|
||||||
|
|
||||||
def publish_control(self, event, payload):
|
|
||||||
return self.conductor_topic_handler.publish_event(event, payload)
|
|
||||||
|
|
||||||
def publish_status(self, event, payload, request_id=None):
|
|
||||||
return self.status_topic_handler.publish_event(
|
|
||||||
event, payload, request_id)
|
|
||||||
|
|
||||||
def get_version(self):
|
|
||||||
return self.api_version
|
|
||||||
|
|
||||||
def check_api_version(self, context):
|
|
||||||
api_manager_version = self.conductor_client.call(
|
|
||||||
context.to_dict(), 'check_api_version',
|
|
||||||
api_version=self.api_version)
|
|
||||||
return api_manager_version
|
|
||||||
|
|
||||||
def response(self, evt, ctx, message):
|
|
||||||
payload = {
|
|
||||||
'request_id': ctx['request_id'],
|
|
||||||
'msg': message
|
|
||||||
}
|
|
||||||
self.publish_status(evt, payload)
|
|
||||||
@@ -38,7 +38,7 @@ CONF = cfg.CONF
|
|||||||
|
|
||||||
class MessagingHandler(threading.Thread):
|
class MessagingHandler(threading.Thread):
|
||||||
|
|
||||||
def __init__(self, publisher_id, topic_name, endpoint, version,
|
def __init__(self, publisher_id, topic_name, endpoints, version,
|
||||||
serializer=None):
|
serializer=None):
|
||||||
super(MessagingHandler, self).__init__()
|
super(MessagingHandler, self).__init__()
|
||||||
self.publisher_id = publisher_id
|
self.publisher_id = publisher_id
|
||||||
@@ -50,10 +50,10 @@ class MessagingHandler(threading.Thread):
|
|||||||
self.__server = None
|
self.__server = None
|
||||||
self.__notifier = None
|
self.__notifier = None
|
||||||
self.__transport = None
|
self.__transport = None
|
||||||
self.add_endpoint(endpoint)
|
self.add_endpoints(endpoints)
|
||||||
|
|
||||||
def add_endpoint(self, endpoint):
|
def add_endpoints(self, endpoints):
|
||||||
self.__endpoints.append(endpoint)
|
self.__endpoints.extend(endpoints)
|
||||||
|
|
||||||
def remove_endpoint(self, endpoint):
|
def remove_endpoint(self, endpoint):
|
||||||
if endpoint in self.__endpoints:
|
if endpoint in self.__endpoints:
|
||||||
|
|||||||
@@ -15,14 +15,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
from oslo_log import log
|
|
||||||
import oslo_messaging as messaging
|
import oslo_messaging as messaging
|
||||||
|
|
||||||
from watcher.common.messaging.utils import observable
|
from watcher.common.messaging.utils import observable
|
||||||
|
|
||||||
|
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationHandler(observable.Observable):
|
class NotificationHandler(observable.Observable):
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ class NovaHelper(object):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
|
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
|
||||||
# https://bugs.launchpad.net/nova/+bug/1182965
|
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||||
|
|
||||||
@@ -532,16 +531,12 @@ class NovaHelper(object):
|
|||||||
"Trying to create new instance '%s' "
|
"Trying to create new instance '%s' "
|
||||||
"from image '%s' with flavor '%s' ..." % (
|
"from image '%s' with flavor '%s' ..." % (
|
||||||
inst_name, image_id, flavor_name))
|
inst_name, image_id, flavor_name))
|
||||||
# TODO(jed) wait feature
|
|
||||||
# Allow admin users to view any keypair
|
try:
|
||||||
# https://bugs.launchpad.net/nova/+bug/1182965
|
self.nova.keypairs.findall(name=keypair_name)
|
||||||
if not self.nova.keypairs.findall(name=keypair_name):
|
except nvexceptions.NotFound:
|
||||||
LOG.debug("Key pair '%s' not found with user '%s'" % (
|
LOG.debug("Key pair '%s' not found " % keypair_name)
|
||||||
keypair_name, self.user))
|
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
LOG.debug("Key pair '%s' found with user '%s'" % (
|
|
||||||
keypair_name, self.user))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = self.nova.images.get(image_id)
|
image = self.nova.images.get(image_id)
|
||||||
|
|||||||
@@ -15,108 +15,46 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import signal
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import _options
|
from oslo_log import _options
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
import oslo_messaging as messaging
|
import oslo_messaging as om
|
||||||
|
from oslo_reports import guru_meditation_report as gmr
|
||||||
|
from oslo_reports import opts as gmr_opts
|
||||||
from oslo_service import service
|
from oslo_service import service
|
||||||
from oslo_utils import importutils
|
from oslo_service import wsgi
|
||||||
|
|
||||||
from watcher._i18n import _LE
|
from watcher._i18n import _
|
||||||
from watcher._i18n import _LI
|
from watcher.api import app
|
||||||
from watcher.common import config
|
from watcher.common import config
|
||||||
from watcher.common import context
|
from watcher.common.messaging.events import event_dispatcher as dispatcher
|
||||||
|
from watcher.common.messaging import messaging_handler
|
||||||
from watcher.common import rpc
|
from watcher.common import rpc
|
||||||
from watcher.objects import base as objects_base
|
from watcher.objects import base
|
||||||
|
from watcher import opts
|
||||||
|
from watcher import version
|
||||||
|
|
||||||
service_opts = [
|
service_opts = [
|
||||||
cfg.IntOpt('periodic_interval',
|
cfg.IntOpt('periodic_interval',
|
||||||
default=60,
|
default=60,
|
||||||
help='Seconds between running periodic tasks.'),
|
help=_('Seconds between running periodic tasks.')),
|
||||||
cfg.StrOpt('host',
|
cfg.StrOpt('host',
|
||||||
default=socket.getfqdn(),
|
default=socket.getfqdn(),
|
||||||
help='Name of this node. This can be an opaque identifier. '
|
help=_('Name of this node. This can be an opaque identifier. '
|
||||||
'It is not necessarily a hostname, FQDN, or IP address. '
|
'It is not necessarily a hostname, FQDN, or IP address. '
|
||||||
'However, the node name must be valid within '
|
'However, the node name must be valid within '
|
||||||
'an AMQP key, and if using ZeroMQ, a valid '
|
'an AMQP key, and if using ZeroMQ, a valid '
|
||||||
'hostname, FQDN, or IP address.'),
|
'hostname, FQDN, or IP address.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
cfg.CONF.register_opts(service_opts)
|
cfg.CONF.register_opts(service_opts)
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RPCService(service.Service):
|
|
||||||
|
|
||||||
def __init__(self, host, manager_module, manager_class):
|
|
||||||
super(RPCService, self).__init__()
|
|
||||||
self.host = host
|
|
||||||
manager_module = importutils.try_import(manager_module)
|
|
||||||
manager_class = getattr(manager_module, manager_class)
|
|
||||||
self.manager = manager_class(host, manager_module.MANAGER_TOPIC)
|
|
||||||
self.topic = self.manager.topic
|
|
||||||
self.rpcserver = None
|
|
||||||
self.deregister = True
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
super(RPCService, self).start()
|
|
||||||
admin_context = context.RequestContext('admin', 'admin', is_admin=True)
|
|
||||||
|
|
||||||
target = messaging.Target(topic=self.topic, server=self.host)
|
|
||||||
endpoints = [self.manager]
|
|
||||||
serializer = objects_base.IronicObjectSerializer()
|
|
||||||
self.rpcserver = rpc.get_server(target, endpoints, serializer)
|
|
||||||
self.rpcserver.start()
|
|
||||||
|
|
||||||
self.handle_signal()
|
|
||||||
self.manager.init_host()
|
|
||||||
self.tg.add_dynamic_timer(
|
|
||||||
self.manager.periodic_tasks,
|
|
||||||
periodic_interval_max=cfg.CONF.periodic_interval,
|
|
||||||
context=admin_context)
|
|
||||||
|
|
||||||
LOG.info(_LI('Created RPC server for service %(service)s on host '
|
|
||||||
'%(host)s.'),
|
|
||||||
{'service': self.topic, 'host': self.host})
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
try:
|
|
||||||
self.rpcserver.stop()
|
|
||||||
self.rpcserver.wait()
|
|
||||||
except Exception as e:
|
|
||||||
LOG.exception(_LE('Service error occurred when stopping the '
|
|
||||||
'RPC server. Error: %s'), e)
|
|
||||||
try:
|
|
||||||
self.manager.del_host(deregister=self.deregister)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.exception(_LE('Service error occurred when cleaning up '
|
|
||||||
'the RPC manager. Error: %s'), e)
|
|
||||||
|
|
||||||
super(RPCService, self).stop(graceful=True)
|
|
||||||
LOG.info(_LI('Stopped RPC server for service %(service)s on host '
|
|
||||||
'%(host)s.'),
|
|
||||||
{'service': self.topic, 'host': self.host})
|
|
||||||
|
|
||||||
def _handle_signal(self):
|
|
||||||
LOG.info(_LI('Got signal SIGUSR1. Not deregistering on next shutdown '
|
|
||||||
'of service %(service)s on host %(host)s.'),
|
|
||||||
{'service': self.topic, 'host': self.host})
|
|
||||||
self.deregister = False
|
|
||||||
|
|
||||||
def handle_signal(self):
|
|
||||||
"""Add a signal handler for SIGUSR1.
|
|
||||||
|
|
||||||
The handler ensures that the manager is not deregistered when it is
|
|
||||||
shutdown.
|
|
||||||
"""
|
|
||||||
signal.signal(signal.SIGUSR1, self._handle_signal)
|
|
||||||
|
|
||||||
|
|
||||||
_DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
|
_DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
|
||||||
'oslo.messaging=INFO', 'sqlalchemy=WARN',
|
'oslo.messaging=INFO', 'sqlalchemy=WARN',
|
||||||
'keystoneclient=INFO', 'stevedore=INFO',
|
'keystoneclient=INFO', 'stevedore=INFO',
|
||||||
@@ -125,10 +63,165 @@ _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
|
|||||||
'glanceclient=WARN', 'watcher.openstack.common=WARN']
|
'glanceclient=WARN', 'watcher.openstack.common=WARN']
|
||||||
|
|
||||||
|
|
||||||
def prepare_service(argv=[], conf=cfg.CONF):
|
class WSGIService(service.ServiceBase):
|
||||||
|
"""Provides ability to launch Watcher API from wsgi app."""
|
||||||
|
|
||||||
|
def __init__(self, name, use_ssl=False):
|
||||||
|
"""Initialize, but do not start the WSGI server.
|
||||||
|
|
||||||
|
:param name: The name of the WSGI server given to the loader.
|
||||||
|
:param use_ssl: Wraps the socket in an SSL context if True.
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.app = app.VersionSelectorApplication()
|
||||||
|
self.workers = (CONF.api.workers or
|
||||||
|
processutils.get_worker_count())
|
||||||
|
self.server = wsgi.Server(CONF, name, self.app,
|
||||||
|
host=CONF.api.host,
|
||||||
|
port=CONF.api.port,
|
||||||
|
use_ssl=use_ssl,
|
||||||
|
logger_name=name)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start serving this service using loaded configuration"""
|
||||||
|
self.server.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop serving this API"""
|
||||||
|
self.server.stop()
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait for the service to stop serving this API"""
|
||||||
|
self.server.wait()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset server greenpool size to default"""
|
||||||
|
self.server.reset()
|
||||||
|
|
||||||
|
|
||||||
|
class Service(service.ServiceBase, dispatcher.EventDispatcher):
|
||||||
|
|
||||||
|
API_VERSION = '1.0'
|
||||||
|
|
||||||
|
def __init__(self, manager_class):
|
||||||
|
super(Service, self).__init__()
|
||||||
|
self.manager = manager_class()
|
||||||
|
|
||||||
|
self.publisher_id = self.manager.publisher_id
|
||||||
|
self.api_version = self.manager.API_VERSION
|
||||||
|
self.conductor_topic = self.manager.conductor_topic
|
||||||
|
self.status_topic = self.manager.status_topic
|
||||||
|
|
||||||
|
self.conductor_endpoints = [
|
||||||
|
ep(self) for ep in self.manager.conductor_endpoints
|
||||||
|
]
|
||||||
|
self.status_endpoints = [
|
||||||
|
ep(self.publisher_id) for ep in self.manager.status_endpoints
|
||||||
|
]
|
||||||
|
|
||||||
|
self.serializer = rpc.RequestContextSerializer(
|
||||||
|
base.WatcherObjectSerializer())
|
||||||
|
|
||||||
|
self.conductor_topic_handler = self.build_topic_handler(
|
||||||
|
self.conductor_topic, self.conductor_endpoints)
|
||||||
|
self.status_topic_handler = self.build_topic_handler(
|
||||||
|
self.status_topic, self.status_endpoints)
|
||||||
|
|
||||||
|
self._conductor_client = None
|
||||||
|
self._status_client = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conductor_client(self):
|
||||||
|
if self._conductor_client is None:
|
||||||
|
transport = om.get_transport(CONF)
|
||||||
|
target = om.Target(
|
||||||
|
topic=self.conductor_topic,
|
||||||
|
version=self.API_VERSION,
|
||||||
|
)
|
||||||
|
self._conductor_client = om.RPCClient(
|
||||||
|
transport, target, serializer=self.serializer)
|
||||||
|
return self._conductor_client
|
||||||
|
|
||||||
|
@conductor_client.setter
|
||||||
|
def conductor_client(self, c):
|
||||||
|
self.conductor_client = c
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_client(self):
|
||||||
|
if self._status_client is None:
|
||||||
|
transport = om.get_transport(CONF)
|
||||||
|
target = om.Target(
|
||||||
|
topic=self.status_topic,
|
||||||
|
version=self.API_VERSION,
|
||||||
|
)
|
||||||
|
self._status_client = om.RPCClient(
|
||||||
|
transport, target, serializer=self.serializer)
|
||||||
|
return self._status_client
|
||||||
|
|
||||||
|
@status_client.setter
|
||||||
|
def status_client(self, c):
|
||||||
|
self.status_client = c
|
||||||
|
|
||||||
|
def build_topic_handler(self, topic_name, endpoints=()):
|
||||||
|
return messaging_handler.MessagingHandler(
|
||||||
|
self.publisher_id, topic_name, [self.manager] + list(endpoints),
|
||||||
|
self.api_version, self.serializer)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
LOG.debug("Connecting to '%s' (%s)",
|
||||||
|
CONF.transport_url, CONF.rpc_backend)
|
||||||
|
self.conductor_topic_handler.start()
|
||||||
|
self.status_topic_handler.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
LOG.debug("Disconnecting from '%s' (%s)",
|
||||||
|
CONF.transport_url, CONF.rpc_backend)
|
||||||
|
self.conductor_topic_handler.stop()
|
||||||
|
self.status_topic_handler.stop()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset a service in case it received a SIGHUP."""
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait for service to complete."""
|
||||||
|
|
||||||
|
def publish_control(self, event, payload):
|
||||||
|
return self.conductor_topic_handler.publish_event(event, payload)
|
||||||
|
|
||||||
|
def publish_status(self, event, payload, request_id=None):
|
||||||
|
return self.status_topic_handler.publish_event(
|
||||||
|
event, payload, request_id)
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
return self.api_version
|
||||||
|
|
||||||
|
def check_api_version(self, context):
|
||||||
|
api_manager_version = self.conductor_client.call(
|
||||||
|
context.to_dict(), 'check_api_version',
|
||||||
|
api_version=self.api_version)
|
||||||
|
return api_manager_version
|
||||||
|
|
||||||
|
def response(self, evt, ctx, message):
|
||||||
|
payload = {
|
||||||
|
'request_id': ctx['request_id'],
|
||||||
|
'msg': message
|
||||||
|
}
|
||||||
|
self.publish_status(evt, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def process_launcher(conf=cfg.CONF):
|
||||||
|
return service.ProcessLauncher(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_service(argv=(), conf=cfg.CONF):
|
||||||
log.register_options(conf)
|
log.register_options(conf)
|
||||||
|
gmr_opts.set_defaults(conf)
|
||||||
|
|
||||||
config.parse_args(argv)
|
config.parse_args(argv)
|
||||||
cfg.set_defaults(_options.log_opts,
|
cfg.set_defaults(_options.log_opts,
|
||||||
default_log_levels=_DEFAULT_LOG_LEVELS)
|
default_log_levels=_DEFAULT_LOG_LEVELS)
|
||||||
log.setup(conf, 'python-watcher')
|
log.setup(conf, 'python-watcher')
|
||||||
conf.log_opt_values(LOG, logging.DEBUG)
|
conf.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
|
||||||
|
gmr.TextGuruMeditation.register_section(_('Plugins'), opts.show_plugins)
|
||||||
|
gmr.TextGuruMeditation.setup_autorun(version)
|
||||||
|
|||||||
@@ -41,6 +41,29 @@ CONF.register_opts(UTILS_OPTS)
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Struct(dict):
|
||||||
|
"""Specialized dict where you access an item like an attribute
|
||||||
|
|
||||||
|
>>> struct = Struct()
|
||||||
|
>>> struct['a'] = 1
|
||||||
|
>>> struct.b = 2
|
||||||
|
>>> assert struct.a == 1
|
||||||
|
>>> assert struct['b'] == 2
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
try:
|
||||||
|
return self[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
try:
|
||||||
|
self[name] = value
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
|
||||||
def safe_rstrip(value, chars=None):
|
def safe_rstrip(value, chars=None):
|
||||||
"""Removes trailing characters from a string if that does not make it empty
|
"""Removes trailing characters from a string if that does not make it empty
|
||||||
|
|
||||||
@@ -95,6 +118,14 @@ def is_hostname_safe(hostname):
|
|||||||
:returns: True if valid. False if not.
|
:returns: True if valid. False if not.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
m = '^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$'
|
m = r'^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$'
|
||||||
return (isinstance(hostname, six.string_types) and
|
return (isinstance(hostname, six.string_types) and
|
||||||
(re.match(m, hostname) is not None))
|
(re.match(m, hostname) is not None))
|
||||||
|
|
||||||
|
|
||||||
|
def get_cls_import_path(cls):
|
||||||
|
"""Return the import path of a given class"""
|
||||||
|
module = cls.__module__
|
||||||
|
if module is None or module == str.__module__:
|
||||||
|
return cls.__name__
|
||||||
|
return module + '.' + cls.__name__
|
||||||
|
|||||||
@@ -34,6 +34,192 @@ def get_instance():
|
|||||||
class BaseConnection(object):
|
class BaseConnection(object):
|
||||||
"""Base class for storage system connections."""
|
"""Base class for storage system connections."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_goal_list(self, context, filters=None, limit=None,
|
||||||
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
"""Get specific columns for matching goals.
|
||||||
|
|
||||||
|
Return a list of the specified columns for all goals that
|
||||||
|
match the specified filters.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param filters: Filters to apply. Defaults to None.
|
||||||
|
|
||||||
|
:param limit: Maximum number of goals to return.
|
||||||
|
:param marker: the last item of the previous page; we return the next
|
||||||
|
result set.
|
||||||
|
:param sort_key: Attribute by which results should be sorted.
|
||||||
|
:param sort_dir: direction in which results should be sorted.
|
||||||
|
(asc, desc)
|
||||||
|
:returns: A list of tuples of the specified columns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_goal(self, values):
|
||||||
|
"""Create a new goal.
|
||||||
|
|
||||||
|
:param values: A dict containing several items used to identify
|
||||||
|
and track the goal. For example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'name': 'DUMMY',
|
||||||
|
'display_name': 'Dummy',
|
||||||
|
}
|
||||||
|
:returns: A goal
|
||||||
|
:raises: :py:class:`~.GoalAlreadyExists`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_goal_by_id(self, context, goal_id):
|
||||||
|
"""Return a goal given its ID.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param goal_id: The ID of a goal
|
||||||
|
:returns: A goal
|
||||||
|
:raises: :py:class:`~.GoalNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_goal_by_uuid(self, context, goal_uuid):
|
||||||
|
"""Return a goal given its UUID.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param goal_uuid: The UUID of a goal
|
||||||
|
:returns: A goal
|
||||||
|
:raises: :py:class:`~.GoalNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_goal_by_name(self, context, goal_name):
|
||||||
|
"""Return a goal given its name.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param goal_name: The name of a goal
|
||||||
|
:returns: A goal
|
||||||
|
:raises: :py:class:`~.GoalNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def destroy_goal(self, goal_uuid):
|
||||||
|
"""Destroy a goal.
|
||||||
|
|
||||||
|
:param goal_uuid: The UUID of a goal
|
||||||
|
:raises: :py:class:`~.GoalNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_goal(self, goal_uuid, values):
|
||||||
|
"""Update properties of a goal.
|
||||||
|
|
||||||
|
:param goal_uuid: The UUID of a goal
|
||||||
|
:param values: A dict containing several items used to identify
|
||||||
|
and track the goal. For example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'name': 'DUMMY',
|
||||||
|
'display_name': 'Dummy',
|
||||||
|
}
|
||||||
|
:returns: A goal
|
||||||
|
:raises: :py:class:`~.GoalNotFound`
|
||||||
|
:raises: :py:class:`~.Invalid`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_strategy_list(self, context, filters=None, limit=None,
|
||||||
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
"""Get specific columns for matching strategies.
|
||||||
|
|
||||||
|
Return a list of the specified columns for all strategies that
|
||||||
|
match the specified filters.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param columns: List of column names to return.
|
||||||
|
Defaults to 'id' column when columns == None.
|
||||||
|
:param filters: Filters to apply. Defaults to None.
|
||||||
|
|
||||||
|
:param limit: Maximum number of strategies to return.
|
||||||
|
:param marker: The last item of the previous page; we return the next
|
||||||
|
result set.
|
||||||
|
:param sort_key: Attribute by which results should be sorted.
|
||||||
|
:param sort_dir: Direction in which results should be sorted.
|
||||||
|
(asc, desc)
|
||||||
|
:returns: A list of tuples of the specified columns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_strategy(self, values):
|
||||||
|
"""Create a new strategy.
|
||||||
|
|
||||||
|
:param values: A dict containing items used to identify
|
||||||
|
and track the strategy. For example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'name': 'my_strategy',
|
||||||
|
'display_name': 'My strategy',
|
||||||
|
'goal_uuid': utils.generate_uuid(),
|
||||||
|
}
|
||||||
|
:returns: A strategy
|
||||||
|
:raises: :py:class:`~.StrategyAlreadyExists`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_strategy_by_id(self, context, strategy_id):
|
||||||
|
"""Return a strategy given its ID.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param strategy_id: The ID of a strategy
|
||||||
|
:returns: A strategy
|
||||||
|
:raises: :py:class:`~.StrategyNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_strategy_by_uuid(self, context, strategy_uuid):
|
||||||
|
"""Return a strategy given its UUID.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param strategy_uuid: The UUID of a strategy
|
||||||
|
:returns: A strategy
|
||||||
|
:raises: :py:class:`~.StrategyNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_strategy_by_name(self, context, strategy_name):
|
||||||
|
"""Return a strategy given its name.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param strategy_name: The name of a strategy
|
||||||
|
:returns: A strategy
|
||||||
|
:raises: :py:class:`~.StrategyNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def destroy_strategy(self, strategy_uuid):
|
||||||
|
"""Destroy a strategy.
|
||||||
|
|
||||||
|
:param strategy_uuid: The UUID of a strategy
|
||||||
|
:raises: :py:class:`~.StrategyNotFound`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_strategy(self, strategy_uuid, values):
|
||||||
|
"""Update properties of a strategy.
|
||||||
|
|
||||||
|
:param strategy_uuid: The UUID of a strategy
|
||||||
|
:returns: A strategy
|
||||||
|
:raises: :py:class:`~.StrategyNotFound`
|
||||||
|
:raises: :py:class:`~.Invalid`
|
||||||
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_audit_template_list(self, context, columns=None, filters=None,
|
def get_audit_template_list(self, context, columns=None, filters=None,
|
||||||
limit=None, marker=None, sort_key=None,
|
limit=None, marker=None, sort_key=None,
|
||||||
@@ -75,7 +261,7 @@ class BaseConnection(object):
|
|||||||
'extra': {'automatic': True}
|
'extra': {'automatic': True}
|
||||||
}
|
}
|
||||||
:returns: An audit template.
|
:returns: An audit template.
|
||||||
:raises: AuditTemplateAlreadyExists
|
:raises: :py:class:`~.AuditTemplateAlreadyExists`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -85,7 +271,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param audit_template_id: The id of an audit template.
|
:param audit_template_id: The id of an audit template.
|
||||||
:returns: An audit template.
|
:returns: An audit template.
|
||||||
:raises: AuditTemplateNotFound
|
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -95,7 +281,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param audit_template_uuid: The uuid of an audit template.
|
:param audit_template_uuid: The uuid of an audit template.
|
||||||
:returns: An audit template.
|
:returns: An audit template.
|
||||||
:raises: AuditTemplateNotFound
|
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_audit_template_by_name(self, context, audit_template_name):
|
def get_audit_template_by_name(self, context, audit_template_name):
|
||||||
@@ -104,7 +290,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param audit_template_name: The name of an audit template.
|
:param audit_template_name: The name of an audit template.
|
||||||
:returns: An audit template.
|
:returns: An audit template.
|
||||||
:raises: AuditTemplateNotFound
|
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -112,7 +298,7 @@ class BaseConnection(object):
|
|||||||
"""Destroy an audit_template.
|
"""Destroy an audit_template.
|
||||||
|
|
||||||
:param audit_template_id: The id or uuid of an audit template.
|
:param audit_template_id: The id or uuid of an audit template.
|
||||||
:raises: AuditTemplateNotFound
|
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -121,8 +307,8 @@ class BaseConnection(object):
|
|||||||
|
|
||||||
:param audit_template_id: The id or uuid of an audit template.
|
:param audit_template_id: The id or uuid of an audit template.
|
||||||
:returns: An audit template.
|
:returns: An audit template.
|
||||||
:raises: AuditTemplateNotFound
|
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||||
:raises: Invalid
|
:raises: :py:class:`~.Invalid`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -130,7 +316,7 @@ class BaseConnection(object):
|
|||||||
"""Soft delete an audit_template.
|
"""Soft delete an audit_template.
|
||||||
|
|
||||||
:param audit_template_id: The id or uuid of an audit template.
|
:param audit_template_id: The id or uuid of an audit template.
|
||||||
:raises: AuditTemplateNotFound
|
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -171,7 +357,7 @@ class BaseConnection(object):
|
|||||||
'deadline': None
|
'deadline': None
|
||||||
}
|
}
|
||||||
:returns: An audit.
|
:returns: An audit.
|
||||||
:raises: AuditAlreadyExists
|
:raises: :py:class:`~.AuditAlreadyExists`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -181,7 +367,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param audit_id: The id of an audit.
|
:param audit_id: The id of an audit.
|
||||||
:returns: An audit.
|
:returns: An audit.
|
||||||
:raises: AuditNotFound
|
:raises: :py:class:`~.AuditNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -191,7 +377,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param audit_uuid: The uuid of an audit.
|
:param audit_uuid: The uuid of an audit.
|
||||||
:returns: An audit.
|
:returns: An audit.
|
||||||
:raises: AuditNotFound
|
:raises: :py:class:`~.AuditNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -199,7 +385,7 @@ class BaseConnection(object):
|
|||||||
"""Destroy an audit and all associated action plans.
|
"""Destroy an audit and all associated action plans.
|
||||||
|
|
||||||
:param audit_id: The id or uuid of an audit.
|
:param audit_id: The id or uuid of an audit.
|
||||||
:raises: AuditNotFound
|
:raises: :py:class:`~.AuditNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -208,8 +394,8 @@ class BaseConnection(object):
|
|||||||
|
|
||||||
:param audit_id: The id or uuid of an audit.
|
:param audit_id: The id or uuid of an audit.
|
||||||
:returns: An audit.
|
:returns: An audit.
|
||||||
:raises: AuditNotFound
|
:raises: :py:class:`~.AuditNotFound`
|
||||||
:raises: Invalid
|
:raises: :py:class:`~.Invalid`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def soft_delete_audit(self, audit_id):
|
def soft_delete_audit(self, audit_id):
|
||||||
@@ -217,7 +403,7 @@ class BaseConnection(object):
|
|||||||
|
|
||||||
:param audit_id: The id or uuid of an audit.
|
:param audit_id: The id or uuid of an audit.
|
||||||
:returns: An audit.
|
:returns: An audit.
|
||||||
:raises: AuditNotFound
|
:raises: :py:class:`~.AuditNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -259,7 +445,7 @@ class BaseConnection(object):
|
|||||||
'aggregate': 'nova aggregate name or uuid'
|
'aggregate': 'nova aggregate name or uuid'
|
||||||
}
|
}
|
||||||
:returns: A action.
|
:returns: A action.
|
||||||
:raises: ActionAlreadyExists
|
:raises: :py:class:`~.ActionAlreadyExists`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -269,7 +455,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param action_id: The id of a action.
|
:param action_id: The id of a action.
|
||||||
:returns: A action.
|
:returns: A action.
|
||||||
:raises: ActionNotFound
|
:raises: :py:class:`~.ActionNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -279,7 +465,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param action_uuid: The uuid of a action.
|
:param action_uuid: The uuid of a action.
|
||||||
:returns: A action.
|
:returns: A action.
|
||||||
:raises: ActionNotFound
|
:raises: :py:class:`~.ActionNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -287,8 +473,8 @@ class BaseConnection(object):
|
|||||||
"""Destroy a action and all associated interfaces.
|
"""Destroy a action and all associated interfaces.
|
||||||
|
|
||||||
:param action_id: The id or uuid of a action.
|
:param action_id: The id or uuid of a action.
|
||||||
:raises: ActionNotFound
|
:raises: :py:class:`~.ActionNotFound`
|
||||||
:raises: ActionReferenced
|
:raises: :py:class:`~.ActionReferenced`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -297,9 +483,9 @@ class BaseConnection(object):
|
|||||||
|
|
||||||
:param action_id: The id or uuid of a action.
|
:param action_id: The id or uuid of a action.
|
||||||
:returns: A action.
|
:returns: A action.
|
||||||
:raises: ActionNotFound
|
:raises: :py:class:`~.ActionNotFound`
|
||||||
:raises: ActionReferenced
|
:raises: :py:class:`~.ActionReferenced`
|
||||||
:raises: Invalid
|
:raises: :py:class:`~.Invalid`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -332,7 +518,7 @@ class BaseConnection(object):
|
|||||||
:param values: A dict containing several items used to identify
|
:param values: A dict containing several items used to identify
|
||||||
and track the action plan.
|
and track the action plan.
|
||||||
:returns: An action plan.
|
:returns: An action plan.
|
||||||
:raises: ActionPlanAlreadyExists
|
:raises: :py:class:`~.ActionPlanAlreadyExists`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -342,7 +528,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param action_plan_id: The id of an action plan.
|
:param action_plan_id: The id of an action plan.
|
||||||
:returns: An action plan.
|
:returns: An action plan.
|
||||||
:raises: ActionPlanNotFound
|
:raises: :py:class:`~.ActionPlanNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -352,7 +538,7 @@ class BaseConnection(object):
|
|||||||
:param context: The security context
|
:param context: The security context
|
||||||
:param action_plan__uuid: The uuid of an action plan.
|
:param action_plan__uuid: The uuid of an action plan.
|
||||||
:returns: An action plan.
|
:returns: An action plan.
|
||||||
:raises: ActionPlanNotFound
|
:raises: :py:class:`~.ActionPlanNotFound`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -360,8 +546,8 @@ class BaseConnection(object):
|
|||||||
"""Destroy an action plan and all associated interfaces.
|
"""Destroy an action plan and all associated interfaces.
|
||||||
|
|
||||||
:param action_plan_id: The id or uuid of a action plan.
|
:param action_plan_id: The id or uuid of a action plan.
|
||||||
:raises: ActionPlanNotFound
|
:raises: :py:class:`~.ActionPlanNotFound`
|
||||||
:raises: ActionPlanReferenced
|
:raises: :py:class:`~.ActionPlanReferenced`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -370,7 +556,7 @@ class BaseConnection(object):
|
|||||||
|
|
||||||
:param action_plan_id: The id or uuid of an action plan.
|
:param action_plan_id: The id or uuid of an action plan.
|
||||||
:returns: An action plan.
|
:returns: An action plan.
|
||||||
:raises: ActionPlanNotFound
|
:raises: :py:class:`~.ActionPlanNotFound`
|
||||||
:raises: ActionPlanReferenced
|
:raises: :py:class:`~.ActionPlanReferenced`
|
||||||
:raises: Invalid
|
:raises: :py:class:`~.Invalid`
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import prettytable as ptable
|
|||||||
from six.moves import input
|
from six.moves import input
|
||||||
|
|
||||||
from watcher._i18n import _, _LI
|
from watcher._i18n import _, _LI
|
||||||
|
from watcher._i18n import lazy_translation_enabled
|
||||||
from watcher.common import context
|
from watcher.common import context
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
@@ -47,6 +48,8 @@ class WatcherObjectsMap(object):
|
|||||||
|
|
||||||
# This is for generating the .pot translations
|
# This is for generating the .pot translations
|
||||||
keymap = collections.OrderedDict([
|
keymap = collections.OrderedDict([
|
||||||
|
("goals", _("Goals")),
|
||||||
|
("strategies", _("Strategies")),
|
||||||
("audit_templates", _("Audit Templates")),
|
("audit_templates", _("Audit Templates")),
|
||||||
("audits", _("Audits")),
|
("audits", _("Audits")),
|
||||||
("action_plans", _("Action Plans")),
|
("action_plans", _("Action Plans")),
|
||||||
@@ -54,11 +57,11 @@ class WatcherObjectsMap(object):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
for attr_name in self.__class__.keys():
|
for attr_name in self.keys():
|
||||||
setattr(self, attr_name, [])
|
setattr(self, attr_name, [])
|
||||||
|
|
||||||
def values(self):
|
def values(self):
|
||||||
return (getattr(self, key) for key in self.__class__.keys())
|
return (getattr(self, key) for key in self.keys())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def keys(cls):
|
def keys(cls):
|
||||||
@@ -98,8 +101,13 @@ class WatcherObjectsMap(object):
|
|||||||
def get_count_table(self):
|
def get_count_table(self):
|
||||||
headers = list(self.keymap.values())
|
headers = list(self.keymap.values())
|
||||||
headers.append(_("Total")) # We also add a total count
|
headers.append(_("Total")) # We also add a total count
|
||||||
|
translated_headers = [
|
||||||
|
h.translate() if lazy_translation_enabled() else h
|
||||||
|
for h in headers
|
||||||
|
]
|
||||||
|
|
||||||
counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)]
|
counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)]
|
||||||
table = ptable.PrettyTable(field_names=headers)
|
table = ptable.PrettyTable(field_names=translated_headers)
|
||||||
table.add_row(counters)
|
table.add_row(counters)
|
||||||
return table.get_string()
|
return table.get_string()
|
||||||
|
|
||||||
@@ -143,9 +151,9 @@ class PurgeCommand(object):
|
|||||||
|
|
||||||
query_func = None
|
query_func = None
|
||||||
if not utils.is_uuid_like(uuid_or_name):
|
if not utils.is_uuid_like(uuid_or_name):
|
||||||
query_func = objects.audit_template.AuditTemplate.get_by_name
|
query_func = objects.AuditTemplate.get_by_name
|
||||||
else:
|
else:
|
||||||
query_func = objects.audit_template.AuditTemplate.get_by_uuid
|
query_func = objects.AuditTemplate.get_by_uuid
|
||||||
|
|
||||||
try:
|
try:
|
||||||
audit_template = query_func(cls.ctx, uuid_or_name)
|
audit_template = query_func(cls.ctx, uuid_or_name)
|
||||||
@@ -159,45 +167,64 @@ class PurgeCommand(object):
|
|||||||
|
|
||||||
return audit_template.uuid
|
return audit_template.uuid
|
||||||
|
|
||||||
|
def _find_goals(self, filters=None):
|
||||||
|
return objects.Goal.list(self.ctx, filters=filters)
|
||||||
|
|
||||||
|
def _find_strategies(self, filters=None):
|
||||||
|
return objects.Strategy.list(self.ctx, filters=filters)
|
||||||
|
|
||||||
def _find_audit_templates(self, filters=None):
|
def _find_audit_templates(self, filters=None):
|
||||||
return objects.audit_template.AuditTemplate.list(
|
return objects.AuditTemplate.list(self.ctx, filters=filters)
|
||||||
self.ctx, filters=filters)
|
|
||||||
|
|
||||||
def _find_audits(self, filters=None):
|
def _find_audits(self, filters=None):
|
||||||
return objects.audit.Audit.list(self.ctx, filters=filters)
|
return objects.Audit.list(self.ctx, filters=filters)
|
||||||
|
|
||||||
def _find_action_plans(self, filters=None):
|
def _find_action_plans(self, filters=None):
|
||||||
return objects.action_plan.ActionPlan.list(self.ctx, filters=filters)
|
return objects.ActionPlan.list(self.ctx, filters=filters)
|
||||||
|
|
||||||
def _find_actions(self, filters=None):
|
def _find_actions(self, filters=None):
|
||||||
return objects.action.Action.list(self.ctx, filters=filters)
|
return objects.Action.list(self.ctx, filters=filters)
|
||||||
|
|
||||||
def _find_orphans(self):
|
def _find_orphans(self):
|
||||||
orphans = WatcherObjectsMap()
|
orphans = WatcherObjectsMap()
|
||||||
|
|
||||||
filters = dict(deleted=False)
|
filters = dict(deleted=False)
|
||||||
audit_templates = objects.audit_template.AuditTemplate.list(
|
goals = objects.Goal.list(self.ctx, filters=filters)
|
||||||
self.ctx, filters=filters)
|
strategies = objects.Strategy.list(self.ctx, filters=filters)
|
||||||
audits = objects.audit.Audit.list(self.ctx, filters=filters)
|
audit_templates = objects.AuditTemplate.list(self.ctx, filters=filters)
|
||||||
action_plans = objects.action_plan.ActionPlan.list(
|
audits = objects.Audit.list(self.ctx, filters=filters)
|
||||||
self.ctx, filters=filters)
|
action_plans = objects.ActionPlan.list(self.ctx, filters=filters)
|
||||||
actions = objects.action.Action.list(self.ctx, filters=filters)
|
actions = objects.Action.list(self.ctx, filters=filters)
|
||||||
|
|
||||||
audit_template_ids = set(at.id for at in audit_templates)
|
goal_ids = set(g.id for g in goals)
|
||||||
|
orphans.strategies = [
|
||||||
|
strategy for strategy in strategies
|
||||||
|
if strategy.goal_id not in goal_ids]
|
||||||
|
|
||||||
|
strategy_ids = [s.id for s in (s for s in strategies
|
||||||
|
if s not in orphans.strategies)]
|
||||||
|
orphans.audit_templates = [
|
||||||
|
audit_template for audit_template in audit_templates
|
||||||
|
if audit_template.goal_id not in goal_ids or
|
||||||
|
(audit_template.strategy_id and
|
||||||
|
audit_template.strategy_id not in strategy_ids)]
|
||||||
|
|
||||||
|
audit_template_ids = [at.id for at in audit_templates
|
||||||
|
if at not in orphans.audit_templates]
|
||||||
orphans.audits = [
|
orphans.audits = [
|
||||||
audit for audit in audits
|
audit for audit in audits
|
||||||
if audit.audit_template_id not in audit_template_ids]
|
if audit.audit_template_id not in audit_template_ids]
|
||||||
|
|
||||||
# Objects with orphan parents are themselves orphans
|
# Objects with orphan parents are themselves orphans
|
||||||
audit_ids = [audit.id for audit in (a for a in audits
|
audit_ids = [audit.id for audit in audits
|
||||||
if a not in orphans.audits)]
|
if audit not in orphans.audits]
|
||||||
orphans.action_plans = [
|
orphans.action_plans = [
|
||||||
ap for ap in action_plans
|
ap for ap in action_plans
|
||||||
if ap.audit_id not in audit_ids]
|
if ap.audit_id not in audit_ids]
|
||||||
|
|
||||||
# Objects with orphan parents are themselves orphans
|
# Objects with orphan parents are themselves orphans
|
||||||
action_plan_ids = [ap.id for ap in (a for a in action_plans
|
action_plan_ids = [ap.id for ap in action_plans
|
||||||
if a not in orphans.action_plans)]
|
if ap not in orphans.action_plans]
|
||||||
orphans.actions = [
|
orphans.actions = [
|
||||||
action for action in actions
|
action for action in actions
|
||||||
if action.action_plan_id not in action_plan_ids]
|
if action.action_plan_id not in action_plan_ids]
|
||||||
@@ -209,18 +236,21 @@ class PurgeCommand(object):
|
|||||||
|
|
||||||
def _find_soft_deleted_objects(self):
|
def _find_soft_deleted_objects(self):
|
||||||
to_be_deleted = WatcherObjectsMap()
|
to_be_deleted = WatcherObjectsMap()
|
||||||
|
|
||||||
expiry_date = self.get_expiry_date()
|
expiry_date = self.get_expiry_date()
|
||||||
filters = dict(deleted=True)
|
filters = dict(deleted=True)
|
||||||
|
|
||||||
if self.uuid:
|
if self.uuid:
|
||||||
filters["uuid"] = self.uuid
|
filters["uuid"] = self.uuid
|
||||||
if expiry_date:
|
if expiry_date:
|
||||||
filters.update(dict(deleted_at__lt=expiry_date))
|
filters.update(dict(deleted_at__lt=expiry_date))
|
||||||
|
|
||||||
|
to_be_deleted.goals.extend(self._find_goals(filters))
|
||||||
|
to_be_deleted.strategies.extend(self._find_strategies(filters))
|
||||||
to_be_deleted.audit_templates.extend(
|
to_be_deleted.audit_templates.extend(
|
||||||
self._find_audit_templates(filters))
|
self._find_audit_templates(filters))
|
||||||
to_be_deleted.audits.extend(self._find_audits(filters))
|
to_be_deleted.audits.extend(self._find_audits(filters))
|
||||||
to_be_deleted.action_plans.extend(self._find_action_plans(filters))
|
to_be_deleted.action_plans.extend(
|
||||||
|
self._find_action_plans(filters))
|
||||||
to_be_deleted.actions.extend(self._find_actions(filters))
|
to_be_deleted.actions.extend(self._find_actions(filters))
|
||||||
|
|
||||||
soft_deleted_objs = self._find_related_objects(
|
soft_deleted_objs = self._find_related_objects(
|
||||||
@@ -233,6 +263,23 @@ class PurgeCommand(object):
|
|||||||
def _find_related_objects(self, objects_map, base_filters=None):
|
def _find_related_objects(self, objects_map, base_filters=None):
|
||||||
base_filters = base_filters or {}
|
base_filters = base_filters or {}
|
||||||
|
|
||||||
|
for goal in objects_map.goals:
|
||||||
|
filters = {}
|
||||||
|
filters.update(base_filters)
|
||||||
|
filters.update(dict(goal_id=goal.id))
|
||||||
|
related_objs = WatcherObjectsMap()
|
||||||
|
related_objs.strategies = self._find_strategies(filters)
|
||||||
|
related_objs.audit_templates = self._find_audit_templates(filters)
|
||||||
|
objects_map += related_objs
|
||||||
|
|
||||||
|
for strategy in objects_map.strategies:
|
||||||
|
filters = {}
|
||||||
|
filters.update(base_filters)
|
||||||
|
filters.update(dict(strategy_id=strategy.id))
|
||||||
|
related_objs = WatcherObjectsMap()
|
||||||
|
related_objs.audit_templates = self._find_audit_templates(filters)
|
||||||
|
objects_map += related_objs
|
||||||
|
|
||||||
for audit_template in objects_map.audit_templates:
|
for audit_template in objects_map.audit_templates:
|
||||||
filters = {}
|
filters = {}
|
||||||
filters.update(base_filters)
|
filters.update(base_filters)
|
||||||
@@ -282,21 +329,48 @@ class PurgeCommand(object):
|
|||||||
return self._delete_up_to_max
|
return self._delete_up_to_max
|
||||||
|
|
||||||
def _aggregate_objects(self):
|
def _aggregate_objects(self):
|
||||||
"""Objects aggregated on a 'per audit template' basis"""
|
"""Objects aggregated on a 'per goal' basis"""
|
||||||
# todo: aggregate orphans as well
|
# todo: aggregate orphans as well
|
||||||
aggregate = []
|
aggregate = []
|
||||||
for audit_template in self._objects_map.audit_templates:
|
for goal in self._objects_map.goals:
|
||||||
related_objs = WatcherObjectsMap()
|
related_objs = WatcherObjectsMap()
|
||||||
related_objs.audit_templates = [audit_template]
|
|
||||||
|
# goals
|
||||||
|
related_objs.goals = [goal]
|
||||||
|
|
||||||
|
# strategies
|
||||||
|
goal_ids = [goal.id]
|
||||||
|
related_objs.strategies = [
|
||||||
|
strategy for strategy in self._objects_map.strategies
|
||||||
|
if strategy.goal_id in goal_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
# audit templates
|
||||||
|
strategy_ids = [
|
||||||
|
strategy.id for strategy in related_objs.strategies]
|
||||||
|
related_objs.audit_templates = [
|
||||||
|
at for at in self._objects_map.audit_templates
|
||||||
|
if at.goal_id in goal_ids or
|
||||||
|
(at.strategy_id and at.strategy_id in strategy_ids)
|
||||||
|
]
|
||||||
|
|
||||||
|
# audits
|
||||||
|
audit_template_ids = [
|
||||||
|
audit_template.id
|
||||||
|
for audit_template in related_objs.audit_templates]
|
||||||
related_objs.audits = [
|
related_objs.audits = [
|
||||||
audit for audit in self._objects_map.audits
|
audit for audit in self._objects_map.audits
|
||||||
if audit.audit_template_id == audit_template.id
|
if audit.audit_template_id in audit_template_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# action plans
|
||||||
audit_ids = [audit.id for audit in related_objs.audits]
|
audit_ids = [audit.id for audit in related_objs.audits]
|
||||||
related_objs.action_plans = [
|
related_objs.action_plans = [
|
||||||
action_plan for action_plan in self._objects_map.action_plans
|
action_plan for action_plan in self._objects_map.action_plans
|
||||||
if action_plan.audit_id in audit_ids
|
if action_plan.audit_id in audit_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# actions
|
||||||
action_plan_ids = [
|
action_plan_ids = [
|
||||||
action_plan.id for action_plan in related_objs.action_plans
|
action_plan.id for action_plan in related_objs.action_plans
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from oslo_config import cfg
|
|||||||
from oslo_db import exception as db_exc
|
from oslo_db import exception as db_exc
|
||||||
from oslo_db.sqlalchemy import session as db_session
|
from oslo_db.sqlalchemy import session as db_session
|
||||||
from oslo_db.sqlalchemy import utils as db_utils
|
from oslo_db.sqlalchemy import utils as db_utils
|
||||||
from oslo_log import log
|
|
||||||
from sqlalchemy.orm import exc
|
from sqlalchemy.orm import exc
|
||||||
|
|
||||||
from watcher import _i18n
|
from watcher import _i18n
|
||||||
@@ -35,7 +34,6 @@ from watcher.objects import audit as audit_objects
|
|||||||
from watcher.objects import utils as objutils
|
from watcher.objects import utils as objutils
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
_ = _i18n._
|
_ = _i18n._
|
||||||
|
|
||||||
_FACADE = None
|
_FACADE = None
|
||||||
@@ -184,26 +182,131 @@ class Connection(api.BaseConnection):
|
|||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def _add_audit_templates_filters(self, query, filters):
|
def __add_simple_filter(self, query, model, fieldname, value):
|
||||||
if filters is None:
|
return query.filter(getattr(model, fieldname) == value)
|
||||||
filters = []
|
|
||||||
|
|
||||||
if 'uuid' in filters:
|
def __add_join_filter(self, query, model, join_model, fieldname, value):
|
||||||
query = query.filter_by(uuid=filters['uuid'])
|
query = query.join(join_model)
|
||||||
if 'name' in filters:
|
return self.__add_simple_filter(query, join_model, fieldname, value)
|
||||||
query = query.filter_by(name=filters['name'])
|
|
||||||
if 'host_aggregate' in filters:
|
|
||||||
query = query.filter_by(host_aggregate=filters['host_aggregate'])
|
|
||||||
if 'goal' in filters:
|
|
||||||
query = query.filter_by(goal=filters['goal'])
|
|
||||||
|
|
||||||
query = self.__add_soft_delete_mixin_filters(
|
def _add_filters(self, query, model, filters=None,
|
||||||
query, filters, models.AuditTemplate)
|
plain_fields=None, join_fieldmap=None):
|
||||||
query = self.__add_timestamp_mixin_filters(
|
"""Generic way to add filters to a Watcher model
|
||||||
query, filters, models.AuditTemplate)
|
|
||||||
|
:param query: a :py:class:`sqlalchemy.orm.query.Query` instance
|
||||||
|
:param model: the model class the filters should relate to
|
||||||
|
:param filters: dict with the following structure {"fieldname": value}
|
||||||
|
:param plain_fields: a :py:class:`sqlalchemy.orm.query.Query` instance
|
||||||
|
:param join_fieldmap: a :py:class:`sqlalchemy.orm.query.Query` instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
filters = filters or {}
|
||||||
|
plain_fields = plain_fields or ()
|
||||||
|
join_fieldmap = join_fieldmap or {}
|
||||||
|
|
||||||
|
for fieldname, value in filters.items():
|
||||||
|
if fieldname in plain_fields:
|
||||||
|
query = self.__add_simple_filter(
|
||||||
|
query, model, fieldname, value)
|
||||||
|
elif fieldname in join_fieldmap:
|
||||||
|
join_field, join_model = join_fieldmap[fieldname]
|
||||||
|
query = self.__add_join_filter(
|
||||||
|
query, model, join_model, join_field, value)
|
||||||
|
|
||||||
|
query = self.__add_soft_delete_mixin_filters(query, filters, model)
|
||||||
|
query = self.__add_timestamp_mixin_filters(query, filters, model)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
def _get(self, context, model, fieldname, value):
|
||||||
|
query = model_query(model)
|
||||||
|
query = query.filter(getattr(model, fieldname) == value)
|
||||||
|
if not context.show_deleted:
|
||||||
|
query = query.filter(model.deleted_at.is_(None))
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = query.one()
|
||||||
|
except exc.NoResultFound:
|
||||||
|
raise exception.ResourceNotFound(name=model.__name__, id=value)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def _update(self, model, id_, values):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(model, session=session)
|
||||||
|
query = add_identity_filter(query, id_)
|
||||||
|
try:
|
||||||
|
ref = query.with_lockmode('update').one()
|
||||||
|
except exc.NoResultFound:
|
||||||
|
raise exception.ResourceNotFound(name=model.__name__, id=id_)
|
||||||
|
|
||||||
|
ref.update(values)
|
||||||
|
return ref
|
||||||
|
|
||||||
|
def _soft_delete(self, model, id_):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(model, session=session)
|
||||||
|
query = add_identity_filter(query, id_)
|
||||||
|
try:
|
||||||
|
query.one()
|
||||||
|
except exc.NoResultFound:
|
||||||
|
raise exception.ResourceNotFound(name=model.__name__, id=id_)
|
||||||
|
|
||||||
|
query.soft_delete()
|
||||||
|
|
||||||
|
def _destroy(self, model, id_):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(model, session=session)
|
||||||
|
query = add_identity_filter(query, id_)
|
||||||
|
|
||||||
|
try:
|
||||||
|
query.one()
|
||||||
|
except exc.NoResultFound:
|
||||||
|
raise exception.ResourceNotFound(name=model.__name__, id=id_)
|
||||||
|
|
||||||
|
query.delete()
|
||||||
|
|
||||||
|
def _add_goals_filters(self, query, filters):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
plain_fields = ['uuid', 'name', 'display_name']
|
||||||
|
|
||||||
|
return self._add_filters(
|
||||||
|
query=query, model=models.Goal, filters=filters,
|
||||||
|
plain_fields=plain_fields)
|
||||||
|
|
||||||
|
def _add_strategies_filters(self, query, filters):
|
||||||
|
plain_fields = ['uuid', 'name', 'display_name', 'goal_id']
|
||||||
|
join_fieldmap = {
|
||||||
|
'goal_uuid': ("uuid", models.Goal),
|
||||||
|
'goal_name': ("name", models.Goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._add_filters(
|
||||||
|
query=query, model=models.Strategy, filters=filters,
|
||||||
|
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||||
|
|
||||||
|
def _add_audit_templates_filters(self, query, filters):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
plain_fields = ['uuid', 'name', 'host_aggregate',
|
||||||
|
'goal_id', 'strategy_id']
|
||||||
|
join_fieldmap = {
|
||||||
|
'goal_uuid': ("uuid", models.Goal),
|
||||||
|
'goal_name': ("name", models.Goal),
|
||||||
|
'strategy_uuid': ("uuid", models.Strategy),
|
||||||
|
'strategy_name': ("name", models.Strategy),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._add_filters(
|
||||||
|
query=query, model=models.AuditTemplate, filters=filters,
|
||||||
|
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||||
|
|
||||||
def _add_audits_filters(self, query, filters):
|
def _add_audits_filters(self, query, filters):
|
||||||
if filters is None:
|
if filters is None:
|
||||||
filters = []
|
filters = []
|
||||||
@@ -283,8 +386,6 @@ class Connection(api.BaseConnection):
|
|||||||
|
|
||||||
if 'state' in filters:
|
if 'state' in filters:
|
||||||
query = query.filter_by(state=filters['state'])
|
query = query.filter_by(state=filters['state'])
|
||||||
if 'alarm' in filters:
|
|
||||||
query = query.filter_by(alarm=filters['alarm'])
|
|
||||||
|
|
||||||
query = self.__add_soft_delete_mixin_filters(
|
query = self.__add_soft_delete_mixin_filters(
|
||||||
query, filters, models.Action)
|
query, filters, models.Action)
|
||||||
@@ -293,6 +394,138 @@ class Connection(api.BaseConnection):
|
|||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
# ### GOALS ### #
|
||||||
|
|
||||||
|
def get_goal_list(self, context, filters=None, limit=None,
|
||||||
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
|
||||||
|
query = model_query(models.Goal)
|
||||||
|
query = self._add_goals_filters(query, filters)
|
||||||
|
if not context.show_deleted:
|
||||||
|
query = query.filter_by(deleted_at=None)
|
||||||
|
return _paginate_query(models.Goal, limit, marker,
|
||||||
|
sort_key, sort_dir, query)
|
||||||
|
|
||||||
|
def create_goal(self, values):
|
||||||
|
# ensure defaults are present for new goals
|
||||||
|
if not values.get('uuid'):
|
||||||
|
values['uuid'] = utils.generate_uuid()
|
||||||
|
|
||||||
|
goal = models.Goal()
|
||||||
|
goal.update(values)
|
||||||
|
|
||||||
|
try:
|
||||||
|
goal.save()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.GoalAlreadyExists(uuid=values['uuid'])
|
||||||
|
return goal
|
||||||
|
|
||||||
|
def _get_goal(self, context, fieldname, value):
|
||||||
|
try:
|
||||||
|
return self._get(context, model=models.Goal,
|
||||||
|
fieldname=fieldname, value=value)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.GoalNotFound(goal=value)
|
||||||
|
|
||||||
|
def get_goal_by_id(self, context, goal_id):
|
||||||
|
return self._get_goal(context, fieldname="id", value=goal_id)
|
||||||
|
|
||||||
|
def get_goal_by_uuid(self, context, goal_uuid):
|
||||||
|
return self._get_goal(context, fieldname="uuid", value=goal_uuid)
|
||||||
|
|
||||||
|
def get_goal_by_name(self, context, goal_name):
|
||||||
|
return self._get_goal(context, fieldname="name", value=goal_name)
|
||||||
|
|
||||||
|
def destroy_goal(self, goal_id):
|
||||||
|
try:
|
||||||
|
return self._destroy(models.Goal, goal_id)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.GoalNotFound(goal=goal_id)
|
||||||
|
|
||||||
|
def update_goal(self, goal_id, values):
|
||||||
|
if 'uuid' in values:
|
||||||
|
raise exception.Invalid(
|
||||||
|
message=_("Cannot overwrite UUID for an existing Goal."))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._update(models.Goal, goal_id, values)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.GoalNotFound(goal=goal_id)
|
||||||
|
|
||||||
|
def soft_delete_goal(self, goal_id):
|
||||||
|
try:
|
||||||
|
self._soft_delete(models.Goal, goal_id)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.GoalNotFound(goal=goal_id)
|
||||||
|
|
||||||
|
# ### STRATEGIES ### #
|
||||||
|
|
||||||
|
def get_strategy_list(self, context, filters=None, limit=None,
|
||||||
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
|
||||||
|
query = model_query(models.Strategy)
|
||||||
|
query = self._add_strategies_filters(query, filters)
|
||||||
|
if not context.show_deleted:
|
||||||
|
query = query.filter_by(deleted_at=None)
|
||||||
|
return _paginate_query(models.Strategy, limit, marker,
|
||||||
|
sort_key, sort_dir, query)
|
||||||
|
|
||||||
|
def create_strategy(self, values):
|
||||||
|
# ensure defaults are present for new strategies
|
||||||
|
if not values.get('uuid'):
|
||||||
|
values['uuid'] = utils.generate_uuid()
|
||||||
|
|
||||||
|
strategy = models.Strategy()
|
||||||
|
strategy.update(values)
|
||||||
|
|
||||||
|
try:
|
||||||
|
strategy.save()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.StrategyAlreadyExists(uuid=values['uuid'])
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
def _get_strategy(self, context, fieldname, value):
|
||||||
|
try:
|
||||||
|
return self._get(context, model=models.Strategy,
|
||||||
|
fieldname=fieldname, value=value)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.StrategyNotFound(strategy=value)
|
||||||
|
|
||||||
|
def get_strategy_by_id(self, context, strategy_id):
|
||||||
|
return self._get_strategy(context, fieldname="id", value=strategy_id)
|
||||||
|
|
||||||
|
def get_strategy_by_uuid(self, context, strategy_uuid):
|
||||||
|
return self._get_strategy(
|
||||||
|
context, fieldname="uuid", value=strategy_uuid)
|
||||||
|
|
||||||
|
def get_strategy_by_name(self, context, strategy_name):
|
||||||
|
return self._get_strategy(
|
||||||
|
context, fieldname="name", value=strategy_name)
|
||||||
|
|
||||||
|
def destroy_strategy(self, strategy_id):
|
||||||
|
try:
|
||||||
|
return self._destroy(models.Strategy, strategy_id)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.StrategyNotFound(strategy=strategy_id)
|
||||||
|
|
||||||
|
def update_strategy(self, strategy_id, values):
|
||||||
|
if 'uuid' in values:
|
||||||
|
raise exception.Invalid(
|
||||||
|
message=_("Cannot overwrite UUID for an existing Strategy."))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._update(models.Strategy, strategy_id, values)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.StrategyNotFound(strategy=strategy_id)
|
||||||
|
|
||||||
|
def soft_delete_strategy(self, strategy_id):
|
||||||
|
try:
|
||||||
|
self._soft_delete(models.Strategy, strategy_id)
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
raise exception.StrategyNotFound(strategy=strategy_id)
|
||||||
|
|
||||||
|
# ### AUDIT TEMPLATES ### #
|
||||||
|
|
||||||
def get_audit_template_list(self, context, filters=None, limit=None,
|
def get_audit_template_list(self, context, filters=None, limit=None,
|
||||||
marker=None, sort_key=None, sort_dir=None):
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
|
||||||
@@ -308,110 +541,70 @@ class Connection(api.BaseConnection):
|
|||||||
if not values.get('uuid'):
|
if not values.get('uuid'):
|
||||||
values['uuid'] = utils.generate_uuid()
|
values['uuid'] = utils.generate_uuid()
|
||||||
|
|
||||||
|
query = model_query(models.AuditTemplate)
|
||||||
|
query = query.filter_by(name=values.get('name'),
|
||||||
|
deleted_at=None)
|
||||||
|
|
||||||
|
if len(query.all()) > 0:
|
||||||
|
raise exception.AuditTemplateAlreadyExists(
|
||||||
|
audit_template=values['name'])
|
||||||
|
|
||||||
audit_template = models.AuditTemplate()
|
audit_template = models.AuditTemplate()
|
||||||
audit_template.update(values)
|
audit_template.update(values)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
audit_template.save()
|
audit_template.save()
|
||||||
except db_exc.DBDuplicateEntry:
|
except db_exc.DBDuplicateEntry:
|
||||||
raise exception.AuditTemplateAlreadyExists(uuid=values['uuid'],
|
raise exception.AuditTemplateAlreadyExists(
|
||||||
name=values['name'])
|
audit_template=values['name'])
|
||||||
return audit_template
|
return audit_template
|
||||||
|
|
||||||
def get_audit_template_by_id(self, context, audit_template_id):
|
def _get_audit_template(self, context, fieldname, value):
|
||||||
query = model_query(models.AuditTemplate)
|
|
||||||
query = query.filter_by(id=audit_template_id)
|
|
||||||
try:
|
try:
|
||||||
audit_template = query.one()
|
return self._get(context, model=models.AuditTemplate,
|
||||||
if not context.show_deleted:
|
fieldname=fieldname, value=value)
|
||||||
if audit_template.deleted_at is not None:
|
except exception.ResourceNotFound:
|
||||||
raise exception.AuditTemplateNotFound(
|
raise exception.AuditTemplateNotFound(audit_template=value)
|
||||||
audit_template=audit_template_id)
|
|
||||||
return audit_template
|
def get_audit_template_by_id(self, context, audit_template_id):
|
||||||
except exc.NoResultFound:
|
return self._get_audit_template(
|
||||||
raise exception.AuditTemplateNotFound(
|
context, fieldname="id", value=audit_template_id)
|
||||||
audit_template=audit_template_id)
|
|
||||||
|
|
||||||
def get_audit_template_by_uuid(self, context, audit_template_uuid):
|
def get_audit_template_by_uuid(self, context, audit_template_uuid):
|
||||||
query = model_query(models.AuditTemplate)
|
return self._get_audit_template(
|
||||||
query = query.filter_by(uuid=audit_template_uuid)
|
context, fieldname="uuid", value=audit_template_uuid)
|
||||||
|
|
||||||
try:
|
|
||||||
audit_template = query.one()
|
|
||||||
if not context.show_deleted:
|
|
||||||
if audit_template.deleted_at is not None:
|
|
||||||
raise exception.AuditTemplateNotFound(
|
|
||||||
audit_template=audit_template_uuid)
|
|
||||||
return audit_template
|
|
||||||
except exc.NoResultFound:
|
|
||||||
raise exception.AuditTemplateNotFound(
|
|
||||||
audit_template=audit_template_uuid)
|
|
||||||
|
|
||||||
def get_audit_template_by_name(self, context, audit_template_name):
|
def get_audit_template_by_name(self, context, audit_template_name):
|
||||||
query = model_query(models.AuditTemplate)
|
return self._get_audit_template(
|
||||||
query = query.filter_by(name=audit_template_name)
|
context, fieldname="name", value=audit_template_name)
|
||||||
try:
|
|
||||||
audit_template = query.one()
|
|
||||||
if not context.show_deleted:
|
|
||||||
if audit_template.deleted_at is not None:
|
|
||||||
raise exception.AuditTemplateNotFound(
|
|
||||||
audit_template=audit_template_name)
|
|
||||||
return audit_template
|
|
||||||
except exc.MultipleResultsFound:
|
|
||||||
raise exception.Conflict(
|
|
||||||
_('Multiple audit templates exist with the same name.'
|
|
||||||
' Please use the audit template uuid instead'))
|
|
||||||
except exc.NoResultFound:
|
|
||||||
raise exception.AuditTemplateNotFound(
|
|
||||||
audit_template=audit_template_name)
|
|
||||||
|
|
||||||
def destroy_audit_template(self, audit_template_id):
|
def destroy_audit_template(self, audit_template_id):
|
||||||
session = get_session()
|
try:
|
||||||
with session.begin():
|
return self._destroy(models.AuditTemplate, audit_template_id)
|
||||||
query = model_query(models.AuditTemplate, session=session)
|
except exception.ResourceNotFound:
|
||||||
query = add_identity_filter(query, audit_template_id)
|
raise exception.AuditTemplateNotFound(
|
||||||
|
audit_template=audit_template_id)
|
||||||
try:
|
|
||||||
query.one()
|
|
||||||
except exc.NoResultFound:
|
|
||||||
raise exception.AuditTemplateNotFound(node=audit_template_id)
|
|
||||||
|
|
||||||
query.delete()
|
|
||||||
|
|
||||||
def update_audit_template(self, audit_template_id, values):
|
def update_audit_template(self, audit_template_id, values):
|
||||||
if 'uuid' in values:
|
if 'uuid' in values:
|
||||||
raise exception.Invalid(
|
raise exception.Invalid(
|
||||||
message=_("Cannot overwrite UUID for an existing "
|
message=_("Cannot overwrite UUID for an existing "
|
||||||
"Audit Template."))
|
"Audit Template."))
|
||||||
|
try:
|
||||||
return self._do_update_audit_template(audit_template_id, values)
|
return self._update(
|
||||||
|
models.AuditTemplate, audit_template_id, values)
|
||||||
def _do_update_audit_template(self, audit_template_id, values):
|
except exception.ResourceNotFound:
|
||||||
session = get_session()
|
raise exception.AuditTemplateNotFound(
|
||||||
with session.begin():
|
audit_template=audit_template_id)
|
||||||
query = model_query(models.AuditTemplate, session=session)
|
|
||||||
query = add_identity_filter(query, audit_template_id)
|
|
||||||
try:
|
|
||||||
ref = query.with_lockmode('update').one()
|
|
||||||
except exc.NoResultFound:
|
|
||||||
raise exception.AuditTemplateNotFound(
|
|
||||||
audit_template=audit_template_id)
|
|
||||||
|
|
||||||
ref.update(values)
|
|
||||||
return ref
|
|
||||||
|
|
||||||
def soft_delete_audit_template(self, audit_template_id):
|
def soft_delete_audit_template(self, audit_template_id):
|
||||||
session = get_session()
|
try:
|
||||||
with session.begin():
|
self._soft_delete(models.AuditTemplate, audit_template_id)
|
||||||
query = model_query(models.AuditTemplate, session=session)
|
except exception.ResourceNotFound:
|
||||||
query = add_identity_filter(query, audit_template_id)
|
raise exception.AuditTemplateNotFound(
|
||||||
|
audit_template=audit_template_id)
|
||||||
|
|
||||||
try:
|
# ### AUDITS ### #
|
||||||
query.one()
|
|
||||||
except exc.NoResultFound:
|
|
||||||
raise exception.AuditTemplateNotFound(node=audit_template_id)
|
|
||||||
|
|
||||||
query.soft_delete()
|
|
||||||
|
|
||||||
def get_audit_list(self, context, filters=None, limit=None, marker=None,
|
def get_audit_list(self, context, filters=None, limit=None, marker=None,
|
||||||
sort_key=None, sort_dir=None):
|
sort_key=None, sort_dir=None):
|
||||||
@@ -519,10 +712,12 @@ class Connection(api.BaseConnection):
|
|||||||
try:
|
try:
|
||||||
query.one()
|
query.one()
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
raise exception.AuditNotFound(node=audit_id)
|
raise exception.AuditNotFound(audit=audit_id)
|
||||||
|
|
||||||
query.soft_delete()
|
query.soft_delete()
|
||||||
|
|
||||||
|
# ### ACTIONS ### #
|
||||||
|
|
||||||
def get_action_list(self, context, filters=None, limit=None, marker=None,
|
def get_action_list(self, context, filters=None, limit=None, marker=None,
|
||||||
sort_key=None, sort_dir=None):
|
sort_key=None, sort_dir=None):
|
||||||
query = model_query(models.Action)
|
query = model_query(models.Action)
|
||||||
@@ -612,10 +807,12 @@ class Connection(api.BaseConnection):
|
|||||||
try:
|
try:
|
||||||
query.one()
|
query.one()
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
raise exception.ActionNotFound(node=action_id)
|
raise exception.ActionNotFound(action=action_id)
|
||||||
|
|
||||||
query.soft_delete()
|
query.soft_delete()
|
||||||
|
|
||||||
|
# ### ACTION PLANS ### #
|
||||||
|
|
||||||
def get_action_plan_list(
|
def get_action_plan_list(
|
||||||
self, context, columns=None, filters=None, limit=None,
|
self, context, columns=None, filters=None, limit=None,
|
||||||
marker=None, sort_key=None, sort_dir=None):
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
@@ -723,6 +920,6 @@ class Connection(api.BaseConnection):
|
|||||||
try:
|
try:
|
||||||
query.one()
|
query.one()
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
raise exception.ActionPlanNotFound(node=action_plan_id)
|
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
|
||||||
|
|
||||||
query.soft_delete()
|
query.soft_delete()
|
||||||
|
|||||||
@@ -110,13 +110,41 @@ class WatcherBase(models.SoftDeleteMixin,
|
|||||||
Base = declarative_base(cls=WatcherBase)
|
Base = declarative_base(cls=WatcherBase)
|
||||||
|
|
||||||
|
|
||||||
|
class Strategy(Base):
|
||||||
|
"""Represents a strategy."""
|
||||||
|
|
||||||
|
__tablename__ = 'strategies'
|
||||||
|
__table_args__ = (
|
||||||
|
schema.UniqueConstraint('uuid', name='uniq_strategies0uuid'),
|
||||||
|
table_args()
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
uuid = Column(String(36))
|
||||||
|
name = Column(String(63), nullable=False)
|
||||||
|
display_name = Column(String(63), nullable=False)
|
||||||
|
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(Base):
|
||||||
|
"""Represents a goal."""
|
||||||
|
|
||||||
|
__tablename__ = 'goals'
|
||||||
|
__table_args__ = (
|
||||||
|
schema.UniqueConstraint('uuid', name='uniq_goals0uuid'),
|
||||||
|
table_args(),
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
uuid = Column(String(36))
|
||||||
|
name = Column(String(63), nullable=False)
|
||||||
|
display_name = Column(String(63), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplate(Base):
|
class AuditTemplate(Base):
|
||||||
"""Represents an audit template."""
|
"""Represents an audit template."""
|
||||||
|
|
||||||
__tablename__ = 'audit_templates'
|
__tablename__ = 'audit_templates'
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
|
schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
|
||||||
schema.UniqueConstraint('name', name='uniq_audit_templates0name'),
|
|
||||||
table_args()
|
table_args()
|
||||||
)
|
)
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
@@ -124,7 +152,8 @@ class AuditTemplate(Base):
|
|||||||
name = Column(String(63), nullable=True)
|
name = Column(String(63), nullable=True)
|
||||||
description = Column(String(255), nullable=True)
|
description = Column(String(255), nullable=True)
|
||||||
host_aggregate = Column(Integer, nullable=True)
|
host_aggregate = Column(Integer, nullable=True)
|
||||||
goal = Column(String(63), nullable=True)
|
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
|
||||||
|
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
|
||||||
extra = Column(JSONEncodedDict)
|
extra = Column(JSONEncodedDict)
|
||||||
version = Column(String(15), nullable=True)
|
version = Column(String(15), nullable=True)
|
||||||
|
|
||||||
@@ -162,8 +191,6 @@ class Action(Base):
|
|||||||
action_type = Column(String(255), nullable=False)
|
action_type = Column(String(255), nullable=False)
|
||||||
input_parameters = Column(JSONEncodedDict, nullable=True)
|
input_parameters = Column(JSONEncodedDict, nullable=True)
|
||||||
state = Column(String(20), nullable=True)
|
state = Column(String(20), nullable=True)
|
||||||
# todo(jed) remove parameter alarm
|
|
||||||
alarm = Column(String(36))
|
|
||||||
next = Column(String(36), nullable=True)
|
next = Column(String(36), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,9 +205,6 @@ class ActionPlan(Base):
|
|||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
uuid = Column(String(36))
|
uuid = Column(String(36))
|
||||||
first_action_id = Column(Integer)
|
first_action_id = Column(Integer)
|
||||||
# first_action_id = Column(Integer, ForeignKeyConstraint(
|
|
||||||
# ['first_action_id'], ['actions.id'], name='fk_first_action_id'),
|
|
||||||
# nullable=True)
|
|
||||||
audit_id = Column(Integer, ForeignKey('audits.id'),
|
audit_id = Column(Integer, ForeignKey('audits.id'),
|
||||||
nullable=True)
|
nullable=True)
|
||||||
state = Column(String(20), nullable=True)
|
state = Column(String(20), nullable=True)
|
||||||
|
|||||||
@@ -38,13 +38,10 @@ See :doc:`../architecture` for more details on this component.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.common.messaging import messaging_core
|
|
||||||
from watcher.decision_engine.messaging import audit_endpoint
|
from watcher.decision_engine.messaging import audit_endpoint
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
WATCHER_DECISION_ENGINE_OPTS = [
|
WATCHER_DECISION_ENGINE_OPTS = [
|
||||||
@@ -78,18 +75,15 @@ CONF.register_group(decision_engine_opt_group)
|
|||||||
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
|
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
|
||||||
|
|
||||||
|
|
||||||
class DecisionEngineManager(messaging_core.MessagingCore):
|
class DecisionEngineManager(object):
|
||||||
def __init__(self):
|
|
||||||
super(DecisionEngineManager, self).__init__(
|
|
||||||
CONF.watcher_decision_engine.publisher_id,
|
|
||||||
CONF.watcher_decision_engine.conductor_topic,
|
|
||||||
CONF.watcher_decision_engine.status_topic,
|
|
||||||
api_version=self.API_VERSION)
|
|
||||||
endpoint = audit_endpoint.AuditEndpoint(
|
|
||||||
self,
|
|
||||||
max_workers=CONF.watcher_decision_engine.max_workers)
|
|
||||||
self.conductor_topic_handler.add_endpoint(endpoint)
|
|
||||||
|
|
||||||
def join(self):
|
API_VERSION = '1.0'
|
||||||
self.conductor_topic_handler.join()
|
|
||||||
self.status_topic_handler.join()
|
conductor_endpoints = [audit_endpoint.AuditEndpoint]
|
||||||
|
status_endpoints = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.publisher_id = CONF.watcher_decision_engine.publisher_id
|
||||||
|
self.conductor_topic = CONF.watcher_decision_engine.conductor_topic
|
||||||
|
self.status_topic = CONF.watcher_decision_engine.status_topic
|
||||||
|
self.api_version = self.API_VERSION
|
||||||
|
|||||||
@@ -18,17 +18,20 @@
|
|||||||
#
|
#
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.decision_engine.audit import default
|
from watcher.decision_engine.audit import default
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuditEndpoint(object):
|
class AuditEndpoint(object):
|
||||||
def __init__(self, messaging, max_workers):
|
def __init__(self, messaging):
|
||||||
self._messaging = messaging
|
self._messaging = messaging
|
||||||
self._executor = futures.ThreadPoolExecutor(max_workers=max_workers)
|
self._executor = futures.ThreadPoolExecutor(
|
||||||
|
max_workers=CONF.watcher_decision_engine.max_workers)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def executor(self):
|
def executor(self):
|
||||||
|
|||||||
@@ -13,16 +13,12 @@
|
|||||||
# implied.
|
# implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher._i18n import _
|
from watcher._i18n import _
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.decision_engine.model import hypervisor
|
from watcher.decision_engine.model import hypervisor
|
||||||
from watcher.decision_engine.model import mapping
|
from watcher.decision_engine.model import mapping
|
||||||
from watcher.decision_engine.model import vm
|
from watcher.decision_engine.model import vm
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ModelRoot(object):
|
class ModelRoot(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -47,12 +47,24 @@ See :doc:`../architecture` for more details on this component.
|
|||||||
import abc
|
import abc
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from watcher.common.loader import loadable
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BasePlanner(object):
|
class BasePlanner(loadable.Loadable):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_opts(cls):
|
||||||
|
"""Defines the configuration options to be associated to this loadable
|
||||||
|
|
||||||
|
:return: A list of configuration options relative to this Loadable
|
||||||
|
:rtype: list of :class:`oslo_config.cfg.Opt` instances
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def schedule(self, context, audit_uuid, solution):
|
def schedule(self, context, audit_uuid, solution):
|
||||||
"""The planner receives a solution to schedule
|
"""The planner receives a solution to schedule
|
||||||
|
|
||||||
:param solution: A solution provided by a strategy for scheduling
|
:param solution: A solution provided by a strategy for scheduling
|
||||||
:type solution: :py:class:`~.BaseSolution` subclass instance
|
:type solution: :py:class:`~.BaseSolution` subclass instance
|
||||||
@@ -60,7 +72,7 @@ class BasePlanner(object):
|
|||||||
:type audit_uuid: str
|
:type audit_uuid: str
|
||||||
:return: Action plan with an ordered sequence of actions such that all
|
:return: Action plan with an ordered sequence of actions such that all
|
||||||
security, dependency, and performance requirements are met.
|
security, dependency, and performance requirements are met.
|
||||||
:rtype: :py:class:`watcher.objects.action_plan.ActionPlan` instance
|
:rtype: :py:class:`watcher.objects.ActionPlan` instance
|
||||||
"""
|
"""
|
||||||
# example: directed acyclic graph
|
# example: directed acyclic graph
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class DefaultPlanner(base.BasePlanner):
|
|||||||
'action_type': action_type,
|
'action_type': action_type,
|
||||||
'input_parameters': input_parameters,
|
'input_parameters': input_parameters,
|
||||||
'state': objects.action.State.PENDING,
|
'state': objects.action.State.PENDING,
|
||||||
'alarm': None,
|
|
||||||
'next': None,
|
'next': None,
|
||||||
}
|
}
|
||||||
return action
|
return action
|
||||||
|
|||||||
@@ -17,14 +17,10 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from oslo_log import log
|
from watcher.common.loader import default
|
||||||
|
|
||||||
from watcher.common.loader.default import DefaultLoader
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultPlannerLoader(DefaultLoader):
|
class DefaultPlannerLoader(default.DefaultLoader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(DefaultPlannerLoader, self).__init__(
|
super(DefaultPlannerLoader, self).__init__(
|
||||||
namespace='watcher_planners')
|
namespace='watcher_planners')
|
||||||
|
|||||||
@@ -18,35 +18,25 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common.messaging import messaging_core
|
|
||||||
from watcher.common.messaging import notification_handler
|
from watcher.common.messaging import notification_handler
|
||||||
|
from watcher.common import service
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
from watcher.decision_engine.manager import decision_engine_opt_group
|
from watcher.decision_engine import manager
|
||||||
from watcher.decision_engine.manager import WATCHER_DECISION_ENGINE_OPTS
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
CONF.register_group(decision_engine_opt_group)
|
CONF.register_group(manager.decision_engine_opt_group)
|
||||||
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
|
CONF.register_opts(manager.WATCHER_DECISION_ENGINE_OPTS,
|
||||||
|
manager.decision_engine_opt_group)
|
||||||
|
|
||||||
|
|
||||||
class DecisionEngineAPI(messaging_core.MessagingCore):
|
class DecisionEngineAPI(service.Service):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(DecisionEngineAPI, self).__init__(
|
super(DecisionEngineAPI, self).__init__(DecisionEngineAPIManager)
|
||||||
CONF.watcher_decision_engine.publisher_id,
|
|
||||||
CONF.watcher_decision_engine.conductor_topic,
|
|
||||||
CONF.watcher_decision_engine.status_topic,
|
|
||||||
api_version=self.API_VERSION,
|
|
||||||
)
|
|
||||||
self.handler = notification_handler.NotificationHandler(
|
|
||||||
self.publisher_id)
|
|
||||||
self.status_topic_handler.add_endpoint(self.handler)
|
|
||||||
|
|
||||||
def trigger_audit(self, context, audit_uuid=None):
|
def trigger_audit(self, context, audit_uuid=None):
|
||||||
if not utils.is_uuid_like(audit_uuid):
|
if not utils.is_uuid_like(audit_uuid):
|
||||||
@@ -54,3 +44,17 @@ class DecisionEngineAPI(messaging_core.MessagingCore):
|
|||||||
|
|
||||||
return self.conductor_client.call(
|
return self.conductor_client.call(
|
||||||
context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid)
|
context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class DecisionEngineAPIManager(object):
|
||||||
|
|
||||||
|
API_VERSION = '1.0'
|
||||||
|
|
||||||
|
conductor_endpoints = []
|
||||||
|
status_endpoints = [notification_handler.NotificationHandler]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.publisher_id = CONF.watcher_decision_engine.publisher_id
|
||||||
|
self.conductor_topic = CONF.watcher_decision_engine.conductor_topic
|
||||||
|
self.status_topic = CONF.watcher_decision_engine.status_topic
|
||||||
|
self.api_version = self.API_VERSION
|
||||||
|
|||||||
@@ -16,14 +16,10 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.applier.actions import base as baction
|
from watcher.applier.actions import base as baction
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.decision_engine.solution import base
|
from watcher.decision_engine.solution import base
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultSolution(base.BaseSolution):
|
class DefaultSolution(base.BaseSolution):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -26,29 +26,24 @@ LOG = log.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DefaultStrategyContext(base.BaseStrategyContext):
|
class DefaultStrategyContext(base.BaseStrategyContext):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(DefaultStrategyContext, self).__init__()
|
super(DefaultStrategyContext, self).__init__()
|
||||||
LOG.debug("Initializing Strategy Context")
|
LOG.debug("Initializing Strategy Context")
|
||||||
self._strategy_selector = default.DefaultStrategySelector()
|
|
||||||
self._collector_manager = manager.CollectorManager()
|
self._collector_manager = manager.CollectorManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collector(self):
|
def collector(self):
|
||||||
return self._collector_manager
|
return self._collector_manager
|
||||||
|
|
||||||
@property
|
|
||||||
def strategy_selector(self):
|
|
||||||
return self._strategy_selector
|
|
||||||
|
|
||||||
def execute_strategy(self, audit_uuid, request_context):
|
def execute_strategy(self, audit_uuid, request_context):
|
||||||
audit = objects.Audit.get_by_uuid(request_context, audit_uuid)
|
audit = objects.Audit.get_by_uuid(request_context, audit_uuid)
|
||||||
|
|
||||||
# Retrieve the Audit Template
|
# Retrieve the Audit Template
|
||||||
audit_template = objects.\
|
audit_template = objects.AuditTemplate.get_by_id(
|
||||||
AuditTemplate.get_by_id(request_context, audit.audit_template_id)
|
request_context, audit.audit_template_id)
|
||||||
|
|
||||||
osc = clients.OpenStackClients()
|
osc = clients.OpenStackClients()
|
||||||
|
|
||||||
# todo(jed) retrieve in audit_template parameters (threshold,...)
|
# todo(jed) retrieve in audit_template parameters (threshold,...)
|
||||||
# todo(jed) create ActionPlan
|
# todo(jed) create ActionPlan
|
||||||
collector_manager = self.collector.get_cluster_model_collector(osc=osc)
|
collector_manager = self.collector.get_cluster_model_collector(osc=osc)
|
||||||
@@ -56,8 +51,13 @@ class DefaultStrategyContext(base.BaseStrategyContext):
|
|||||||
# todo(jed) remove call to get_latest_cluster_data_model
|
# todo(jed) remove call to get_latest_cluster_data_model
|
||||||
cluster_data_model = collector_manager.get_latest_cluster_data_model()
|
cluster_data_model = collector_manager.get_latest_cluster_data_model()
|
||||||
|
|
||||||
selected_strategy = self.strategy_selector.define_from_goal(
|
strategy_selector = default.DefaultStrategySelector(
|
||||||
audit_template.goal, osc=osc)
|
goal_name=objects.Goal.get_by_id(
|
||||||
|
request_context, audit_template.goal_id).name,
|
||||||
|
strategy_name=None,
|
||||||
|
osc=osc)
|
||||||
|
|
||||||
|
selected_strategy = strategy_selector.select()
|
||||||
|
|
||||||
# todo(jed) add parameters and remove cluster_data_model
|
# todo(jed) add parameters and remove cluster_data_model
|
||||||
return selected_strategy.execute(cluster_data_model)
|
return selected_strategy.execute(cluster_data_model)
|
||||||
|
|||||||
@@ -19,12 +19,9 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.common.loader import default
|
from watcher.common.loader import default
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultStrategyLoader(default.DefaultLoader):
|
class DefaultStrategyLoader(default.DefaultLoader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import six
|
|||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseSelector(object):
|
class BaseSelector(object):
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def define_from_goal(self, goal_name):
|
def select(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -25,38 +25,51 @@ from watcher.decision_engine.strategy.selection import base
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
default_goals = {'DUMMY': 'dummy'}
|
|
||||||
|
|
||||||
WATCHER_GOALS_OPTS = [
|
|
||||||
cfg.DictOpt(
|
|
||||||
'goals',
|
|
||||||
default=default_goals,
|
|
||||||
required=True,
|
|
||||||
help='Goals used for the optimization. '
|
|
||||||
'Maps each goal to an associated strategy (for example: '
|
|
||||||
'BASIC_CONSOLIDATION:basic, MY_GOAL:my_strategy_1)'),
|
|
||||||
]
|
|
||||||
goals_opt_group = cfg.OptGroup(name='watcher_goals',
|
|
||||||
title='Goals available for the optimization')
|
|
||||||
CONF.register_group(goals_opt_group)
|
|
||||||
CONF.register_opts(WATCHER_GOALS_OPTS, goals_opt_group)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultStrategySelector(base.BaseSelector):
|
class DefaultStrategySelector(base.BaseSelector):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, goal_name, strategy_name=None, osc=None):
|
||||||
|
"""Default strategy selector
|
||||||
|
|
||||||
|
:param goal_name: Name of the goal
|
||||||
|
:param strategy_name: Name of the strategy
|
||||||
|
:param osc: an OpenStackClients instance
|
||||||
|
"""
|
||||||
super(DefaultStrategySelector, self).__init__()
|
super(DefaultStrategySelector, self).__init__()
|
||||||
|
self.goal_name = goal_name
|
||||||
|
self.strategy_name = strategy_name
|
||||||
|
self.osc = osc
|
||||||
self.strategy_loader = default.DefaultStrategyLoader()
|
self.strategy_loader = default.DefaultStrategyLoader()
|
||||||
|
|
||||||
def define_from_goal(self, goal_name, osc=None):
|
def select(self):
|
||||||
""":param osc: an OpenStackClients instance"""
|
"""Selects a strategy
|
||||||
|
|
||||||
|
:raises: :py:class:`~.LoadingError` if it failed to load a strategy
|
||||||
|
:returns: A :py:class:`~.BaseStrategy` instance
|
||||||
|
"""
|
||||||
strategy_to_load = None
|
strategy_to_load = None
|
||||||
try:
|
try:
|
||||||
strategy_to_load = CONF.watcher_goals.goals[goal_name]
|
if self.strategy_name:
|
||||||
return self.strategy_loader.load(strategy_to_load, osc=osc)
|
strategy_to_load = self.strategy_name
|
||||||
except KeyError as exc:
|
else:
|
||||||
|
available_strategies = self.strategy_loader.list_available()
|
||||||
|
available_strategies_for_goal = list(
|
||||||
|
key for key, strat in available_strategies.items()
|
||||||
|
if strat.get_goal_name() == self.goal_name)
|
||||||
|
|
||||||
|
if not available_strategies_for_goal:
|
||||||
|
raise exception.NoAvailableStrategyForGoal(
|
||||||
|
goal=self.goal_name)
|
||||||
|
|
||||||
|
# TODO(v-francoise): We should do some more work here to select
|
||||||
|
# a strategy out of a given goal instead of just choosing the
|
||||||
|
# 1st one
|
||||||
|
strategy_to_load = available_strategies_for_goal[0]
|
||||||
|
return self.strategy_loader.load(strategy_to_load, osc=self.osc)
|
||||||
|
except exception.NoAvailableStrategyForGoal:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
LOG.exception(exc)
|
LOG.exception(exc)
|
||||||
raise exception.WatcherException(
|
raise exception.LoadingError(
|
||||||
_("Incorrect mapping: could not find "
|
_("Could not load any strategy for goal %(goal)s"),
|
||||||
"associated strategy for '%s'") % goal_name
|
goal=self.goal_name)
|
||||||
)
|
|
||||||
|
|||||||
@@ -18,14 +18,16 @@
|
|||||||
from watcher.decision_engine.strategy.strategies import basic_consolidation
|
from watcher.decision_engine.strategy.strategies import basic_consolidation
|
||||||
from watcher.decision_engine.strategy.strategies import dummy_strategy
|
from watcher.decision_engine.strategy.strategies import dummy_strategy
|
||||||
from watcher.decision_engine.strategy.strategies import outlet_temp_control
|
from watcher.decision_engine.strategy.strategies import outlet_temp_control
|
||||||
from watcher.decision_engine.strategy.strategies \
|
from watcher.decision_engine.strategy.strategies import \
|
||||||
import vm_workload_consolidation
|
vm_workload_consolidation
|
||||||
|
from watcher.decision_engine.strategy.strategies import workload_stabilization
|
||||||
|
|
||||||
BasicConsolidation = basic_consolidation.BasicConsolidation
|
BasicConsolidation = basic_consolidation.BasicConsolidation
|
||||||
OutletTempControl = outlet_temp_control.OutletTempControl
|
OutletTempControl = outlet_temp_control.OutletTempControl
|
||||||
DummyStrategy = dummy_strategy.DummyStrategy
|
DummyStrategy = dummy_strategy.DummyStrategy
|
||||||
VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation
|
VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation
|
||||||
|
WorkloadStabilization = workload_stabilization.WorkloadStabilization
|
||||||
|
|
||||||
|
__all__ = ("BasicConsolidation", "OutletTempControl",
|
||||||
__all__ = (BasicConsolidation, OutletTempControl, DummyStrategy,
|
"DummyStrategy", "VMWorkloadConsolidation",
|
||||||
VMWorkloadConsolidation)
|
"WorkloadStabilization")
|
||||||
|
|||||||
@@ -37,29 +37,28 @@ which are dynamically loaded by Watcher at launch time.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.common import clients
|
from watcher.common import clients
|
||||||
|
from watcher.common.loader import loadable
|
||||||
from watcher.decision_engine.solution import default
|
from watcher.decision_engine.solution import default
|
||||||
from watcher.decision_engine.strategy.common import level
|
from watcher.decision_engine.strategy.common import level
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseStrategy(object):
|
class BaseStrategy(loadable.Loadable):
|
||||||
"""A base class for all the strategies
|
"""A base class for all the strategies
|
||||||
|
|
||||||
A Strategy is an algorithm implementation which is able to find a
|
A Strategy is an algorithm implementation which is able to find a
|
||||||
Solution for a given Goal.
|
Solution for a given Goal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name=None, description=None, osc=None):
|
def __init__(self, config, osc=None):
|
||||||
""":param osc: an OpenStackClients instance"""
|
""":param osc: an OpenStackClients instance"""
|
||||||
self._name = name
|
super(BaseStrategy, self).__init__(config)
|
||||||
self.description = description
|
self._name = self.get_name()
|
||||||
|
self._display_name = self.get_display_name()
|
||||||
# default strategy level
|
# default strategy level
|
||||||
self._strategy_level = level.StrategyLevel.conservative
|
self._strategy_level = level.StrategyLevel.conservative
|
||||||
self._cluster_state_collector = None
|
self._cluster_state_collector = None
|
||||||
@@ -67,6 +66,55 @@ class BaseStrategy(object):
|
|||||||
self._solution = default.DefaultSolution()
|
self._solution = default.DefaultSolution()
|
||||||
self._osc = osc
|
self._osc = osc
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_name(cls):
|
||||||
|
"""The name of the strategy"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
"""The goal display name for the strategy"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
"""The translatable msgid of the strategy"""
|
||||||
|
# Note(v-francoise): Defined here to be used as the translation key for
|
||||||
|
# other services
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
"""The goal name for the strategy"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
"""The translated display name related to the goal of the strategy"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
"""The translatable msgid related to the goal of the strategy"""
|
||||||
|
# Note(v-francoise): Defined here to be used as the translation key for
|
||||||
|
# other services
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_opts(cls):
|
||||||
|
"""Defines the configuration options to be associated to this loadable
|
||||||
|
|
||||||
|
:return: A list of configuration options relative to this Loadable
|
||||||
|
:rtype: list of :class:`oslo_config.cfg.Opt` instances
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def execute(self, original_model):
|
def execute(self, original_model):
|
||||||
"""Execute a strategy
|
"""Execute a strategy
|
||||||
@@ -92,12 +140,12 @@ class BaseStrategy(object):
|
|||||||
self._solution = s
|
self._solution = s
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def id(self):
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@name.setter
|
@property
|
||||||
def name(self, n):
|
def display_name(self):
|
||||||
self._name = n
|
return self._display_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strategy_level(self):
|
def strategy_level(self):
|
||||||
@@ -114,3 +162,90 @@ class BaseStrategy(object):
|
|||||||
@state_collector.setter
|
@state_collector.setter
|
||||||
def state_collector(self, s):
|
def state_collector(self, s):
|
||||||
self._cluster_state_collector = s
|
self._cluster_state_collector = s
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class DummyBaseStrategy(BaseStrategy):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "DUMMY"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("Dummy goal")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "Dummy goal"
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class UnclassifiedStrategy(BaseStrategy):
|
||||||
|
"""This base class is used to ease the development of new strategies
|
||||||
|
|
||||||
|
The goal defined within this strategy can be used to simplify the
|
||||||
|
documentation explaining how to implement a new strategy plugin by
|
||||||
|
ommitting the need for the strategy developer to define a goal straight
|
||||||
|
away.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "UNCLASSIFIED"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("Unclassified")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "Unclassified"
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class ServerConsolidationBaseStrategy(BaseStrategy):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "SERVER_CONSOLIDATION"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("Server consolidation")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "Server consolidation"
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class ThermalOptimizationBaseStrategy(BaseStrategy):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "THERMAL_OPTIMIZATION"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("Thermal optimization")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "Thermal optimization"
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class WorkloadStabilizationBaseStrategy(BaseStrategy):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "WORKLOAD_BALANCING"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("Workload balancing")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "Workload balancing"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ order to both minimize energy consumption and comply to the various SLAs.
|
|||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher._i18n import _LE, _LI, _LW
|
from watcher._i18n import _, _LE, _LI, _LW
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.decision_engine.model import hypervisor_state as hyper_state
|
from watcher.decision_engine.model import hypervisor_state as hyper_state
|
||||||
from watcher.decision_engine.model import resource
|
from watcher.decision_engine.model import resource
|
||||||
@@ -41,7 +41,7 @@ from watcher.metrics_engine.cluster_history import ceilometer as \
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BasicConsolidation(base.BaseStrategy):
|
class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||||
"""Basic offline consolidation using live migration
|
"""Basic offline consolidation using live migration
|
||||||
|
|
||||||
*Description*
|
*Description*
|
||||||
@@ -65,26 +65,20 @@ class BasicConsolidation(base.BaseStrategy):
|
|||||||
<None>
|
<None>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_NAME = "basic"
|
|
||||||
DEFAULT_DESCRIPTION = "Basic offline consolidation"
|
|
||||||
|
|
||||||
HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent'
|
HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent'
|
||||||
INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util'
|
INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util'
|
||||||
|
|
||||||
MIGRATION = "migrate"
|
MIGRATION = "migrate"
|
||||||
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
|
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
|
def __init__(self, config=None, osc=None):
|
||||||
osc=None):
|
|
||||||
"""Basic offline Consolidation using live migration
|
"""Basic offline Consolidation using live migration
|
||||||
|
|
||||||
:param name: The name of the strategy (Default: "basic")
|
:param config: A mapping containing the configuration of this strategy
|
||||||
:param description: The description of the strategy
|
:type config: dict
|
||||||
(Default: "Basic offline consolidation")
|
:param osc: :py:class:`~.OpenStackClients` instance
|
||||||
:param osc: An :py:class:`~watcher.common.clients.OpenStackClients`
|
|
||||||
instance
|
|
||||||
"""
|
"""
|
||||||
super(BasicConsolidation, self).__init__(name, description, osc)
|
super(BasicConsolidation, self).__init__(config, osc)
|
||||||
|
|
||||||
# set default value for the number of released nodes
|
# set default value for the number of released nodes
|
||||||
self.number_of_released_nodes = 0
|
self.number_of_released_nodes = 0
|
||||||
@@ -114,6 +108,18 @@ class BasicConsolidation(base.BaseStrategy):
|
|||||||
# TODO(jed) bound migration attempts (80 %)
|
# TODO(jed) bound migration attempts (80 %)
|
||||||
self.bound_migration = 0.80
|
self.bound_migration = 0.80
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "basic"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("Basic offline consolidation")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "Basic offline consolidation"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ceilometer(self):
|
def ceilometer(self):
|
||||||
if self._ceilometer is None:
|
if self._ceilometer is None:
|
||||||
|
|||||||
@@ -18,12 +18,13 @@
|
|||||||
#
|
#
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.decision_engine.strategy.strategies import base
|
from watcher.decision_engine.strategy.strategies import base
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DummyStrategy(base.BaseStrategy):
|
class DummyStrategy(base.DummyBaseStrategy):
|
||||||
"""Dummy strategy used for integration testing via Tempest
|
"""Dummy strategy used for integration testing via Tempest
|
||||||
|
|
||||||
*Description*
|
*Description*
|
||||||
@@ -44,15 +45,17 @@ class DummyStrategy(base.BaseStrategy):
|
|||||||
<None>
|
<None>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_NAME = "dummy"
|
|
||||||
DEFAULT_DESCRIPTION = "Dummy Strategy"
|
|
||||||
|
|
||||||
NOP = "nop"
|
NOP = "nop"
|
||||||
SLEEP = "sleep"
|
SLEEP = "sleep"
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
|
def __init__(self, config=None, osc=None):
|
||||||
osc=None):
|
"""Dummy Strategy implemented for demo and testing purposes
|
||||||
super(DummyStrategy, self).__init__(name, description, osc)
|
|
||||||
|
:param config: A mapping containing the configuration of this strategy
|
||||||
|
:type config: dict
|
||||||
|
:param osc: :py:class:`~.OpenStackClients` instance
|
||||||
|
"""
|
||||||
|
super(DummyStrategy, self).__init__(config, osc)
|
||||||
|
|
||||||
def execute(self, original_model):
|
def execute(self, original_model):
|
||||||
LOG.debug("Executing Dummy strategy")
|
LOG.debug("Executing Dummy strategy")
|
||||||
@@ -67,3 +70,15 @@ class DummyStrategy(base.BaseStrategy):
|
|||||||
self.solution.add_action(action_type=self.SLEEP,
|
self.solution.add_action(action_type=self.SLEEP,
|
||||||
input_parameters={'duration': 5.0})
|
input_parameters={'duration': 5.0})
|
||||||
return self.solution
|
return self.solution
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "dummy"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("Dummy strategy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "Dummy strategy"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ telemetries to measure thermal/workload status of server.
|
|||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher._i18n import _LE
|
from watcher._i18n import _, _LE
|
||||||
from watcher.common import exception as wexc
|
from watcher.common import exception as wexc
|
||||||
from watcher.decision_engine.model import resource
|
from watcher.decision_engine.model import resource
|
||||||
from watcher.decision_engine.model import vm_state
|
from watcher.decision_engine.model import vm_state
|
||||||
@@ -41,7 +41,7 @@ from watcher.metrics_engine.cluster_history import ceilometer as ceil
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OutletTempControl(base.BaseStrategy):
|
class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||||
"""[PoC] Outlet temperature control using live migration
|
"""[PoC] Outlet temperature control using live migration
|
||||||
|
|
||||||
*Description*
|
*Description*
|
||||||
@@ -71,8 +71,6 @@ class OutletTempControl(base.BaseStrategy):
|
|||||||
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/approved/outlet-temperature-based-strategy.rst
|
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/approved/outlet-temperature-based-strategy.rst
|
||||||
""" # noqa
|
""" # noqa
|
||||||
|
|
||||||
DEFAULT_NAME = "outlet_temp_control"
|
|
||||||
DEFAULT_DESCRIPTION = "outlet temperature based migration strategy"
|
|
||||||
# The meter to report outlet temperature in ceilometer
|
# The meter to report outlet temperature in ceilometer
|
||||||
METER_NAME = "hardware.ipmi.node.outlet_temperature"
|
METER_NAME = "hardware.ipmi.node.outlet_temperature"
|
||||||
# Unit: degree C
|
# Unit: degree C
|
||||||
@@ -80,15 +78,15 @@ class OutletTempControl(base.BaseStrategy):
|
|||||||
|
|
||||||
MIGRATION = "migrate"
|
MIGRATION = "migrate"
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
|
def __init__(self, config=None, osc=None):
|
||||||
osc=None):
|
|
||||||
"""Outlet temperature control using live migration
|
"""Outlet temperature control using live migration
|
||||||
|
|
||||||
:param name: the name of the strategy
|
:param config: A mapping containing the configuration of this strategy
|
||||||
:param description: a description of the strategy
|
:type config: dict
|
||||||
:param osc: an OpenStackClients object
|
:param osc: an OpenStackClients object, defaults to None
|
||||||
|
:type osc: :py:class:`~.OpenStackClients` instance, optional
|
||||||
"""
|
"""
|
||||||
super(OutletTempControl, self).__init__(name, description, osc)
|
super(OutletTempControl, self).__init__(config, osc)
|
||||||
# the migration plan will be triggered when the outlet temperature
|
# the migration plan will be triggered when the outlet temperature
|
||||||
# reaches threshold
|
# reaches threshold
|
||||||
# TODO(zhenzanz): Threshold should be configurable for each audit
|
# TODO(zhenzanz): Threshold should be configurable for each audit
|
||||||
@@ -96,6 +94,18 @@ class OutletTempControl(base.BaseStrategy):
|
|||||||
self._meter = self.METER_NAME
|
self._meter = self.METER_NAME
|
||||||
self._ceilometer = None
|
self._ceilometer = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "outlet_temperature"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("Outlet temperature based strategy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "Outlet temperature based strategy"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ceilometer(self):
|
def ceilometer(self):
|
||||||
if self._ceilometer is None:
|
if self._ceilometer is None:
|
||||||
@@ -202,8 +212,8 @@ class OutletTempControl(base.BaseStrategy):
|
|||||||
cluster_data_model, host, cpu_capacity, memory_capacity,
|
cluster_data_model, host, cpu_capacity, memory_capacity,
|
||||||
disk_capacity)
|
disk_capacity)
|
||||||
cores_available = cpu_capacity.get_capacity(host) - cores_used
|
cores_available = cpu_capacity.get_capacity(host) - cores_used
|
||||||
disk_available = disk_capacity.get_capacity(host) - mem_used
|
disk_available = disk_capacity.get_capacity(host) - disk_used
|
||||||
mem_available = memory_capacity.get_capacity(host) - disk_used
|
mem_available = memory_capacity.get_capacity(host) - mem_used
|
||||||
if cores_available >= required_cores \
|
if cores_available >= required_cores \
|
||||||
and disk_available >= required_disk \
|
and disk_available >= required_disk \
|
||||||
and mem_available >= required_memory:
|
and mem_available >= required_memory:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from copy import deepcopy
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from watcher._i18n import _LE, _LI
|
from watcher._i18n import _, _LE, _LI
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.decision_engine.model import hypervisor_state as hyper_state
|
from watcher.decision_engine.model import hypervisor_state as hyper_state
|
||||||
from watcher.decision_engine.model import resource
|
from watcher.decision_engine.model import resource
|
||||||
@@ -34,9 +34,11 @@ from watcher.metrics_engine.cluster_history import ceilometer \
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VMWorkloadConsolidation(base.BaseStrategy):
|
class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||||
"""VM Workload Consolidation Strategy.
|
"""VM Workload Consolidation Strategy.
|
||||||
|
|
||||||
|
*Description*
|
||||||
|
|
||||||
A load consolidation strategy based on heuristic first-fit
|
A load consolidation strategy based on heuristic first-fit
|
||||||
algorithm which focuses on measured CPU utilization and tries to
|
algorithm which focuses on measured CPU utilization and tries to
|
||||||
minimize hosts which have too much or too little load respecting
|
minimize hosts which have too much or too little load respecting
|
||||||
@@ -44,10 +46,11 @@ class VMWorkloadConsolidation(base.BaseStrategy):
|
|||||||
|
|
||||||
This strategy produces a solution resulting in more efficient
|
This strategy produces a solution resulting in more efficient
|
||||||
utilization of cluster resources using following four phases:
|
utilization of cluster resources using following four phases:
|
||||||
* Offload phase - handling over-utilized resources
|
|
||||||
* Consolidation phase - handling under-utilized resources
|
* Offload phase - handling over-utilized resources
|
||||||
* Solution optimization - reducing number of migrations
|
* Consolidation phase - handling under-utilized resources
|
||||||
* Deactivation of unused hypervisors
|
* Solution optimization - reducing number of migrations
|
||||||
|
* Deactivation of unused hypervisors
|
||||||
|
|
||||||
A capacity coefficients (cc) might be used to adjust optimization
|
A capacity coefficients (cc) might be used to adjust optimization
|
||||||
thresholds. Different resources may require different coefficient
|
thresholds. Different resources may require different coefficient
|
||||||
@@ -56,7 +59,7 @@ class VMWorkloadConsolidation(base.BaseStrategy):
|
|||||||
If the cc equals 1 the full resource capacity may be used, cc
|
If the cc equals 1 the full resource capacity may be used, cc
|
||||||
values lower than 1 will lead to resource under utilization and
|
values lower than 1 will lead to resource under utilization and
|
||||||
values higher than 1 will lead to resource overbooking.
|
values higher than 1 will lead to resource overbooking.
|
||||||
e.g. If targeted utilization is 80% of hypervisor capacity,
|
e.g. If targeted utilization is 80 percent of hypervisor capacity,
|
||||||
the coefficient in the consolidation phase will be 0.8, but
|
the coefficient in the consolidation phase will be 0.8, but
|
||||||
may any lower value in the offloading phase. The lower it gets
|
may any lower value in the offloading phase. The lower it gets
|
||||||
the cluster will appear more released (distributed) for the
|
the cluster will appear more released (distributed) for the
|
||||||
@@ -67,19 +70,39 @@ class VMWorkloadConsolidation(base.BaseStrategy):
|
|||||||
correctly on all hypervisors within the cluster.
|
correctly on all hypervisors within the cluster.
|
||||||
This strategy assumes it is possible to live migrate any VM from
|
This strategy assumes it is possible to live migrate any VM from
|
||||||
an active hypervisor to any other active hypervisor.
|
an active hypervisor to any other active hypervisor.
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT_NAME = 'vm_workload_consolidation'
|
*Requirements*
|
||||||
DEFAULT_DESCRIPTION = 'VM Workload Consolidation Strategy'
|
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
|
* You must have at least 2 physical compute nodes to run this strategy.
|
||||||
osc=None):
|
|
||||||
super(VMWorkloadConsolidation, self).__init__(name, description, osc)
|
*Limitations*
|
||||||
|
|
||||||
|
<None>
|
||||||
|
|
||||||
|
*Spec URL*
|
||||||
|
|
||||||
|
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/implemented/zhaw-load-consolidation.rst
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
def __init__(self, config=None, osc=None):
|
||||||
|
super(VMWorkloadConsolidation, self).__init__(config, osc)
|
||||||
self._ceilometer = None
|
self._ceilometer = None
|
||||||
self.number_of_migrations = 0
|
self.number_of_migrations = 0
|
||||||
self.number_of_released_hypervisors = 0
|
self.number_of_released_hypervisors = 0
|
||||||
self.ceilometer_vm_data_cache = dict()
|
self.ceilometer_vm_data_cache = dict()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "vm_workload_consolidation"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("VM Workload Consolidation Strategy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "VM Workload Consolidation Strategy"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ceilometer(self):
|
def ceilometer(self):
|
||||||
if self._ceilometer is None:
|
if self._ceilometer is None:
|
||||||
@@ -375,12 +398,13 @@ class VMWorkloadConsolidation(base.BaseStrategy):
|
|||||||
This is done by eliminating unnecessary or circular set of migrations
|
This is done by eliminating unnecessary or circular set of migrations
|
||||||
which can be replaced by a more efficient solution.
|
which can be replaced by a more efficient solution.
|
||||||
e.g.:
|
e.g.:
|
||||||
* A->B, B->C => replace migrations A->B, B->C with
|
|
||||||
a single migration A->C as both solution result in
|
* A->B, B->C => replace migrations A->B, B->C with
|
||||||
VM running on hypervisor C which can be achieved with
|
a single migration A->C as both solution result in
|
||||||
one migration instead of two.
|
VM running on hypervisor C which can be achieved with
|
||||||
* A->B, B->A => remove A->B and B->A as they do not result
|
one migration instead of two.
|
||||||
in a new VM placement.
|
* A->B, B->A => remove A->B and B->A as they do not result
|
||||||
|
in a new VM placement.
|
||||||
|
|
||||||
:param model: model_root object
|
:param model: model_root object
|
||||||
"""
|
"""
|
||||||
@@ -480,10 +504,11 @@ class VMWorkloadConsolidation(base.BaseStrategy):
|
|||||||
This strategy produces a solution resulting in more
|
This strategy produces a solution resulting in more
|
||||||
efficient utilization of cluster resources using following
|
efficient utilization of cluster resources using following
|
||||||
four phases:
|
four phases:
|
||||||
* Offload phase - handling over-utilized resources
|
|
||||||
* Consolidation phase - handling under-utilized resources
|
* Offload phase - handling over-utilized resources
|
||||||
* Solution optimization - reducing number of migrations
|
* Consolidation phase - handling under-utilized resources
|
||||||
* Deactivation of unused hypervisors
|
* Solution optimization - reducing number of migrations
|
||||||
|
* Deactivation of unused hypervisors
|
||||||
|
|
||||||
:param original_model: root_model object
|
:param original_model: root_model object
|
||||||
"""
|
"""
|
||||||
|
|||||||
324
watcher/decision_engine/strategy/strategies/workload_balance.py
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2016 Intel Corp
|
||||||
|
#
|
||||||
|
# Authors: Junjie-Huang <junjie.huang@intel.com>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _LE, _LI, _LW
|
||||||
|
from watcher.common import exception as wexc
|
||||||
|
from watcher.decision_engine.model import resource
|
||||||
|
from watcher.decision_engine.model import vm_state
|
||||||
|
from watcher.decision_engine.strategy.strategies import base
|
||||||
|
from watcher.metrics_engine.cluster_history import ceilometer as ceil
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkloadBalance(base.BaseStrategy):
|
||||||
|
"""[PoC]Workload balance using live migration
|
||||||
|
|
||||||
|
*Description*
|
||||||
|
|
||||||
|
It is a migration strategy based on the VM workload of physical
|
||||||
|
servers. It generates solutions to move a workload whenever a server's
|
||||||
|
CPU utilization % is higher than the specified threshold.
|
||||||
|
The VM to be moved should make the host close to average workload
|
||||||
|
of all hypervisors.
|
||||||
|
|
||||||
|
*Requirements*
|
||||||
|
|
||||||
|
* Hardware: compute node should use the same physical CPUs
|
||||||
|
* Software: Ceilometer component ceilometer-agent-compute running
|
||||||
|
in each compute node, and Ceilometer API can report such telemetry
|
||||||
|
"cpu_util" successfully.
|
||||||
|
* You must have at least 2 physical compute nodes to run this strategy
|
||||||
|
|
||||||
|
*Limitations*
|
||||||
|
|
||||||
|
- This is a proof of concept that is not meant to be used in production
|
||||||
|
- We cannot forecast how many servers should be migrated. This is the
|
||||||
|
reason why we only plan a single virtual machine migration at a time.
|
||||||
|
So it's better to use this algorithm with `CONTINUOUS` audits.
|
||||||
|
- It assume that live migrations are possible
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The meter to report CPU utilization % of VM in ceilometer
|
||||||
|
METER_NAME = "cpu_util"
|
||||||
|
# Unit: %, value range is [0 , 100]
|
||||||
|
# TODO(Junjie): make it configurable
|
||||||
|
THRESHOLD = 25.0
|
||||||
|
# choose 300 seconds as the default duration of meter aggregation
|
||||||
|
# TODO(Junjie): make it configurable
|
||||||
|
PERIOD = 300
|
||||||
|
|
||||||
|
MIGRATION = "migrate"
|
||||||
|
|
||||||
|
def __init__(self, osc=None):
|
||||||
|
"""Using live migration
|
||||||
|
|
||||||
|
:param osc: an OpenStackClients object
|
||||||
|
"""
|
||||||
|
super(WorkloadBalance, self).__init__(osc)
|
||||||
|
# the migration plan will be triggered when the CPU utlization %
|
||||||
|
# reaches threshold
|
||||||
|
# TODO(Junjie): Threshold should be configurable for each audit
|
||||||
|
self.threshold = self.THRESHOLD
|
||||||
|
self._meter = self.METER_NAME
|
||||||
|
self._ceilometer = None
|
||||||
|
self._period = self.PERIOD
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ceilometer(self):
|
||||||
|
if self._ceilometer is None:
|
||||||
|
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
|
||||||
|
return self._ceilometer
|
||||||
|
|
||||||
|
@ceilometer.setter
|
||||||
|
def ceilometer(self, c):
|
||||||
|
self._ceilometer = c
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "workload_balance"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("workload balance migration strategy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "workload balance migration strategy"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_name(cls):
|
||||||
|
return "WORKLOAD_OPTIMIZATION"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_goal_display_name(cls):
|
||||||
|
return _("Workload optimization")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_goal_display_name(cls):
|
||||||
|
return "Workload optimization"
|
||||||
|
|
||||||
|
def calculate_used_resource(self, model, hypervisor, cap_cores, cap_mem,
|
||||||
|
cap_disk):
|
||||||
|
'''calculate the used vcpus, memory and disk based on VM flavors'''
|
||||||
|
vms = model.get_mapping().get_node_vms(hypervisor)
|
||||||
|
vcpus_used = 0
|
||||||
|
memory_mb_used = 0
|
||||||
|
disk_gb_used = 0
|
||||||
|
for vm_id in vms:
|
||||||
|
vm = model.get_vm_from_id(vm_id)
|
||||||
|
vcpus_used += cap_cores.get_capacity(vm)
|
||||||
|
memory_mb_used += cap_mem.get_capacity(vm)
|
||||||
|
disk_gb_used += cap_disk.get_capacity(vm)
|
||||||
|
|
||||||
|
return vcpus_used, memory_mb_used, disk_gb_used
|
||||||
|
|
||||||
|
def choose_vm_to_migrate(self, model, hosts, avg_workload, workload_cache):
|
||||||
|
"""pick up an active vm instance to migrate from provided hosts
|
||||||
|
|
||||||
|
:param model: it's the origin_model passed from 'execute' function
|
||||||
|
:param hosts: the array of dict which contains hypervisor object
|
||||||
|
:param avg_workload: the average workload value of all hypervisors
|
||||||
|
:param workload_cache: the map contains vm to workload mapping
|
||||||
|
"""
|
||||||
|
for hvmap in hosts:
|
||||||
|
source_hypervisor = hvmap['hv']
|
||||||
|
source_vms = model.get_mapping().get_node_vms(source_hypervisor)
|
||||||
|
if source_vms:
|
||||||
|
delta_workload = hvmap['workload'] - avg_workload
|
||||||
|
min_delta = 1000000
|
||||||
|
instance_id = None
|
||||||
|
for vm_id in source_vms:
|
||||||
|
try:
|
||||||
|
# select the first active VM to migrate
|
||||||
|
vm = model.get_vm_from_id(vm_id)
|
||||||
|
if vm.state != vm_state.VMState.ACTIVE.value:
|
||||||
|
LOG.debug("VM not active, skipped: %s",
|
||||||
|
vm.uuid)
|
||||||
|
continue
|
||||||
|
current_delta = delta_workload - workload_cache[vm_id]
|
||||||
|
if 0 <= current_delta < min_delta:
|
||||||
|
min_delta = current_delta
|
||||||
|
instance_id = vm_id
|
||||||
|
except wexc.InstanceNotFound:
|
||||||
|
LOG.error(_LE("VM not found Error: %s"), vm_id)
|
||||||
|
if instance_id:
|
||||||
|
return source_hypervisor, model.get_vm_from_id(instance_id)
|
||||||
|
else:
|
||||||
|
LOG.info(_LI("VM not found from hypervisor: %s"),
|
||||||
|
source_hypervisor.uuid)
|
||||||
|
|
||||||
|
def filter_destination_hosts(self, model, hosts, vm_to_migrate,
|
||||||
|
avg_workload, workload_cache):
|
||||||
|
'''Only return hosts with sufficient available resources'''
|
||||||
|
|
||||||
|
cap_cores = model.get_resource_from_id(resource.ResourceType.cpu_cores)
|
||||||
|
cap_disk = model.get_resource_from_id(resource.ResourceType.disk)
|
||||||
|
cap_mem = model.get_resource_from_id(resource.ResourceType.memory)
|
||||||
|
|
||||||
|
required_cores = cap_cores.get_capacity(vm_to_migrate)
|
||||||
|
required_disk = cap_disk.get_capacity(vm_to_migrate)
|
||||||
|
required_mem = cap_mem.get_capacity(vm_to_migrate)
|
||||||
|
|
||||||
|
# filter hypervisors without enough resource
|
||||||
|
destination_hosts = []
|
||||||
|
src_vm_workload = workload_cache[vm_to_migrate.uuid]
|
||||||
|
for hvmap in hosts:
|
||||||
|
host = hvmap['hv']
|
||||||
|
workload = hvmap['workload']
|
||||||
|
# calculate the available resources
|
||||||
|
cores_used, mem_used, disk_used = self.calculate_used_resource(
|
||||||
|
model, host, cap_cores, cap_mem, cap_disk)
|
||||||
|
cores_available = cap_cores.get_capacity(host) - cores_used
|
||||||
|
disk_available = cap_disk.get_capacity(host) - disk_used
|
||||||
|
mem_available = cap_mem.get_capacity(host) - mem_used
|
||||||
|
if (cores_available >= required_cores and
|
||||||
|
disk_available >= required_disk and
|
||||||
|
mem_available >= required_mem and
|
||||||
|
(src_vm_workload + workload) < self.threshold / 100 *
|
||||||
|
cap_cores.get_capacity(host)):
|
||||||
|
destination_hosts.append(hvmap)
|
||||||
|
|
||||||
|
return destination_hosts
|
||||||
|
|
||||||
|
def group_hosts_by_cpu_util(self, model):
|
||||||
|
"""Calculate the workloads of each hypervisor
|
||||||
|
|
||||||
|
try to find out the hypervisors which have reached threshold
|
||||||
|
and the hypervisors which are under threshold.
|
||||||
|
and also calculate the average workload value of all hypervisors.
|
||||||
|
and also generate the VM workload map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
hypervisors = model.get_all_hypervisors()
|
||||||
|
cluster_size = len(hypervisors)
|
||||||
|
if not hypervisors:
|
||||||
|
raise wexc.ClusterEmpty()
|
||||||
|
# get cpu cores capacity of hypervisors and vms
|
||||||
|
cap_cores = model.get_resource_from_id(resource.ResourceType.cpu_cores)
|
||||||
|
overload_hosts = []
|
||||||
|
nonoverload_hosts = []
|
||||||
|
# total workload of cluster
|
||||||
|
# it's the total core numbers being utilized in a cluster.
|
||||||
|
cluster_workload = 0.0
|
||||||
|
# use workload_cache to store the workload of VMs for reuse purpose
|
||||||
|
workload_cache = {}
|
||||||
|
for hypervisor_id in hypervisors:
|
||||||
|
hypervisor = model.get_hypervisor_from_id(hypervisor_id)
|
||||||
|
vms = model.get_mapping().get_node_vms(hypervisor)
|
||||||
|
hypervisor_workload = 0.0
|
||||||
|
for vm_id in vms:
|
||||||
|
vm = model.get_vm_from_id(vm_id)
|
||||||
|
try:
|
||||||
|
cpu_util = self.ceilometer.statistic_aggregation(
|
||||||
|
resource_id=vm_id,
|
||||||
|
meter_name=self._meter,
|
||||||
|
period=self._period,
|
||||||
|
aggregate='avg')
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(_LE("Can not get cpu_util: %s"), e.message)
|
||||||
|
continue
|
||||||
|
if cpu_util is None:
|
||||||
|
LOG.debug("%s: cpu_util is None", vm_id)
|
||||||
|
continue
|
||||||
|
vm_cores = cap_cores.get_capacity(vm)
|
||||||
|
workload_cache[vm_id] = cpu_util * vm_cores / 100
|
||||||
|
hypervisor_workload += workload_cache[vm_id]
|
||||||
|
LOG.debug("%s: cpu_util %f", vm_id, cpu_util)
|
||||||
|
hypervisor_cores = cap_cores.get_capacity(hypervisor)
|
||||||
|
hy_cpu_util = hypervisor_workload / hypervisor_cores * 100
|
||||||
|
|
||||||
|
cluster_workload += hypervisor_workload
|
||||||
|
|
||||||
|
hvmap = {'hv': hypervisor, "cpu_util": hy_cpu_util, 'workload':
|
||||||
|
hypervisor_workload}
|
||||||
|
if hy_cpu_util >= self.threshold:
|
||||||
|
# mark the hypervisor to release resources
|
||||||
|
overload_hosts.append(hvmap)
|
||||||
|
else:
|
||||||
|
nonoverload_hosts.append(hvmap)
|
||||||
|
|
||||||
|
avg_workload = cluster_workload / cluster_size
|
||||||
|
|
||||||
|
return overload_hosts, nonoverload_hosts, avg_workload, workload_cache
|
||||||
|
|
||||||
|
def execute(self, origin_model):
|
||||||
|
LOG.info(_LI("Initializing Workload Balance Strategy"))
|
||||||
|
|
||||||
|
if origin_model is None:
|
||||||
|
raise wexc.ClusterStateNotDefined()
|
||||||
|
|
||||||
|
current_model = origin_model
|
||||||
|
src_hypervisors, target_hypervisors, avg_workload, workload_cache = (
|
||||||
|
self.group_hosts_by_cpu_util(current_model))
|
||||||
|
|
||||||
|
if not src_hypervisors:
|
||||||
|
LOG.debug("No hosts require optimization")
|
||||||
|
return self.solution
|
||||||
|
|
||||||
|
if not target_hypervisors:
|
||||||
|
LOG.warning(_LW("No hosts current have CPU utilization under %s "
|
||||||
|
"percent, therefore there are no possible target "
|
||||||
|
"hosts for any migration"),
|
||||||
|
self.threshold)
|
||||||
|
return self.solution
|
||||||
|
|
||||||
|
# choose the server with largest cpu_util
|
||||||
|
src_hypervisors = sorted(src_hypervisors,
|
||||||
|
reverse=True,
|
||||||
|
key=lambda x: (x[self.METER_NAME]))
|
||||||
|
|
||||||
|
vm_to_migrate = self.choose_vm_to_migrate(current_model,
|
||||||
|
src_hypervisors,
|
||||||
|
avg_workload,
|
||||||
|
workload_cache)
|
||||||
|
if not vm_to_migrate:
|
||||||
|
return self.solution
|
||||||
|
source_hypervisor, vm_src = vm_to_migrate
|
||||||
|
# find the hosts that have enough resource for the VM to be migrated
|
||||||
|
destination_hosts = self.filter_destination_hosts(current_model,
|
||||||
|
target_hypervisors,
|
||||||
|
vm_src,
|
||||||
|
avg_workload,
|
||||||
|
workload_cache)
|
||||||
|
# sort the filtered result by workload
|
||||||
|
# pick up the lowest one as dest server
|
||||||
|
if not destination_hosts:
|
||||||
|
# for instance.
|
||||||
|
LOG.warning(_LW("No proper target host could be found, it might "
|
||||||
|
"be because of there's no enough CPU/Memory/DISK"))
|
||||||
|
return self.solution
|
||||||
|
destination_hosts = sorted(destination_hosts,
|
||||||
|
key=lambda x: (x["cpu_util"]))
|
||||||
|
# always use the host with lowerest CPU utilization
|
||||||
|
mig_dst_hypervisor = destination_hosts[0]['hv']
|
||||||
|
# generate solution to migrate the vm to the dest server,
|
||||||
|
if current_model.get_mapping().migrate_vm(vm_src,
|
||||||
|
source_hypervisor,
|
||||||
|
mig_dst_hypervisor):
|
||||||
|
parameters = {'migration_type': 'live',
|
||||||
|
'src_hypervisor': source_hypervisor.uuid,
|
||||||
|
'dst_hypervisor': mig_dst_hypervisor.uuid}
|
||||||
|
self.solution.add_action(action_type=self.MIGRATION,
|
||||||
|
resource_id=vm_src.uuid,
|
||||||
|
input_parameters=parameters)
|
||||||
|
self.solution.model = current_model
|
||||||
|
return self.solution
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2016 Servionica LLC
|
||||||
|
#
|
||||||
|
# Authors: Alexander Chadin <a.chadin@servionica.ru>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
import itertools
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
import oslo_cache
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _LI, _
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.decision_engine.model import resource
|
||||||
|
from watcher.decision_engine.model import vm_state
|
||||||
|
from watcher.decision_engine.strategy.strategies import base
|
||||||
|
from watcher.metrics_engine.cluster_history import ceilometer as \
|
||||||
|
ceilometer_cluster_history
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
metrics = ['cpu_util', 'memory.resident']
|
||||||
|
thresholds_dict = {'cpu_util': 0.2, 'memory.resident': 0.2}
|
||||||
|
weights_dict = {'cpu_util_weight': 1.0, 'memory.resident_weight': 1.0}
|
||||||
|
vm_host_measures = {'cpu_util': 'hardware.cpu.util',
|
||||||
|
'memory.resident': 'hardware.memory.used'}
|
||||||
|
|
||||||
|
ws_opts = [
|
||||||
|
cfg.ListOpt('metrics',
|
||||||
|
default=metrics,
|
||||||
|
required=True,
|
||||||
|
help='Metrics used as rates of cluster loads.'),
|
||||||
|
cfg.DictOpt('thresholds',
|
||||||
|
default=thresholds_dict,
|
||||||
|
help=''),
|
||||||
|
cfg.DictOpt('weights',
|
||||||
|
default=weights_dict,
|
||||||
|
help='These weights used to calculate '
|
||||||
|
'common standard deviation. Name of weight '
|
||||||
|
'contains meter name and _weight suffix.'),
|
||||||
|
cfg.StrOpt('host_choice',
|
||||||
|
default='retry',
|
||||||
|
required=True,
|
||||||
|
help="Method of host's choice."),
|
||||||
|
cfg.IntOpt('retry_count',
|
||||||
|
default=1,
|
||||||
|
required=True,
|
||||||
|
help='Count of random returned hosts.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
CONF.register_opts(ws_opts, 'watcher_strategies.workload_stabilization')
|
||||||
|
|
||||||
|
|
||||||
|
def _set_memoize(conf):
|
||||||
|
oslo_cache.configure(conf)
|
||||||
|
region = oslo_cache.create_region()
|
||||||
|
configured_region = oslo_cache.configure_cache_region(conf, region)
|
||||||
|
return oslo_cache.core.get_memoization_decorator(conf,
|
||||||
|
configured_region,
|
||||||
|
'cache')
|
||||||
|
|
||||||
|
|
||||||
|
class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||||
|
"""Workload Stabilization control using live migration
|
||||||
|
|
||||||
|
*Description*
|
||||||
|
|
||||||
|
This is workload stabilization strategy based on standard deviation
|
||||||
|
algorithm. The goal is to determine if there is an overload in a cluster
|
||||||
|
and respond to it by migrating VMs to stabilize the cluster.
|
||||||
|
|
||||||
|
*Requirements*
|
||||||
|
|
||||||
|
* Software: Ceilometer component ceilometer-compute running
|
||||||
|
in each compute host, and Ceilometer API can report such telemetries
|
||||||
|
``memory.resident`` and ``cpu_util`` successfully.
|
||||||
|
* You must have at least 2 physical compute nodes to run this strategy.
|
||||||
|
|
||||||
|
*Limitations*
|
||||||
|
|
||||||
|
- It assume that live migrations are possible
|
||||||
|
- Load on the system is sufficiently stable.
|
||||||
|
|
||||||
|
*Spec URL*
|
||||||
|
|
||||||
|
https://review.openstack.org/#/c/286153/
|
||||||
|
"""
|
||||||
|
|
||||||
|
MIGRATION = "migrate"
|
||||||
|
MEMOIZE = _set_memoize(CONF)
|
||||||
|
|
||||||
|
def __init__(self, osc=None):
|
||||||
|
super(WorkloadStabilization, self).__init__(osc)
|
||||||
|
self._ceilometer = None
|
||||||
|
self._nova = None
|
||||||
|
self.weights = CONF['watcher_strategies.workload_stabilization']\
|
||||||
|
.weights
|
||||||
|
self.metrics = CONF['watcher_strategies.workload_stabilization']\
|
||||||
|
.metrics
|
||||||
|
self.thresholds = CONF['watcher_strategies.workload_stabilization']\
|
||||||
|
.thresholds
|
||||||
|
self.host_choice = CONF['watcher_strategies.workload_stabilization']\
|
||||||
|
.host_choice
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "WORKLOAD_BALANCING"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("Workload balancing")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "Workload balancing"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ceilometer(self):
|
||||||
|
if self._ceilometer is None:
|
||||||
|
self._ceilometer = (ceilometer_cluster_history.
|
||||||
|
CeilometerClusterHistory(osc=self.osc))
|
||||||
|
return self._ceilometer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nova(self):
|
||||||
|
if self._nova is None:
|
||||||
|
self._nova = self.osc.nova()
|
||||||
|
return self._nova
|
||||||
|
|
||||||
|
@nova.setter
|
||||||
|
def nova(self, n):
|
||||||
|
self._nova = n
|
||||||
|
|
||||||
|
@ceilometer.setter
|
||||||
|
def ceilometer(self, c):
|
||||||
|
self._ceilometer = c
|
||||||
|
|
||||||
|
def transform_vm_cpu(self, vm_load, host_vcpus):
|
||||||
|
"""This method transforms vm cpu utilization to overall host cpu utilization.
|
||||||
|
|
||||||
|
:param vm_load: dict that contains vm uuid and utilization info.
|
||||||
|
:param host_vcpus: int
|
||||||
|
:return: float value
|
||||||
|
"""
|
||||||
|
return vm_load['cpu_util'] * (vm_load['vcpus'] / float(host_vcpus))
|
||||||
|
|
||||||
|
@MEMOIZE
|
||||||
|
def get_vm_load(self, vm_uuid, current_model):
|
||||||
|
"""Gathering vm load through ceilometer statistic.
|
||||||
|
|
||||||
|
:param vm_uuid: vm for which statistic is gathered.
|
||||||
|
:param current_model: the cluster model
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
LOG.debug(_LI('get_vm_load started'))
|
||||||
|
vm_vcpus = current_model.get_resource_from_id(
|
||||||
|
resource.ResourceType.cpu_cores).get_capacity(
|
||||||
|
current_model.get_vm_from_id(vm_uuid))
|
||||||
|
vm_load = {'uuid': vm_uuid, 'vcpus': vm_vcpus}
|
||||||
|
for meter in self.metrics:
|
||||||
|
avg_meter = self.ceilometer.statistic_aggregation(
|
||||||
|
resource_id=vm_uuid,
|
||||||
|
meter_name=meter,
|
||||||
|
period="120",
|
||||||
|
aggregate='min'
|
||||||
|
)
|
||||||
|
if avg_meter is None:
|
||||||
|
raise exception.NoMetricValuesForVM(resource_id=vm_uuid,
|
||||||
|
metric_name=meter)
|
||||||
|
vm_load[meter] = avg_meter
|
||||||
|
return vm_load
|
||||||
|
|
||||||
|
def normalize_hosts_load(self, hosts, current_model):
|
||||||
|
normalized_hosts = deepcopy(hosts)
|
||||||
|
for host in normalized_hosts:
|
||||||
|
if 'cpu_util' in normalized_hosts[host]:
|
||||||
|
normalized_hosts[host]['cpu_util'] /= float(100)
|
||||||
|
|
||||||
|
if 'memory.resident' in normalized_hosts[host]:
|
||||||
|
h_memory = current_model.get_resource_from_id(
|
||||||
|
resource.ResourceType.memory).get_capacity(
|
||||||
|
current_model.get_hypervisor_from_id(host))
|
||||||
|
normalized_hosts[host]['memory.resident'] /= float(h_memory)
|
||||||
|
|
||||||
|
return normalized_hosts
|
||||||
|
|
||||||
|
def get_hosts_load(self, current_model):
|
||||||
|
"""Get load of every host by gathering vms load"""
|
||||||
|
hosts_load = {}
|
||||||
|
for hypervisor_id in current_model.get_all_hypervisors():
|
||||||
|
hosts_load[hypervisor_id] = {}
|
||||||
|
host_vcpus = current_model.get_resource_from_id(
|
||||||
|
resource.ResourceType.cpu_cores).get_capacity(
|
||||||
|
current_model.get_hypervisor_from_id(hypervisor_id))
|
||||||
|
hosts_load[hypervisor_id]['vcpus'] = host_vcpus
|
||||||
|
|
||||||
|
for metric in self.metrics:
|
||||||
|
avg_meter = self.ceilometer.statistic_aggregation(
|
||||||
|
resource_id=hypervisor_id,
|
||||||
|
meter_name=vm_host_measures[metric],
|
||||||
|
period="60",
|
||||||
|
aggregate='avg'
|
||||||
|
)
|
||||||
|
if avg_meter is None:
|
||||||
|
raise exception.NoSuchMetricForHost(
|
||||||
|
metric=vm_host_measures[metric],
|
||||||
|
host=hypervisor_id)
|
||||||
|
hosts_load[hypervisor_id][metric] = avg_meter
|
||||||
|
return hosts_load
|
||||||
|
|
||||||
|
def get_sd(self, hosts, meter_name):
|
||||||
|
"""Get standard deviation among hosts by specified meter"""
|
||||||
|
mean = 0
|
||||||
|
variaton = 0
|
||||||
|
for host_id in hosts:
|
||||||
|
mean += hosts[host_id][meter_name]
|
||||||
|
mean /= len(hosts)
|
||||||
|
for host_id in hosts:
|
||||||
|
variaton += (hosts[host_id][meter_name] - mean) ** 2
|
||||||
|
variaton /= len(hosts)
|
||||||
|
sd = math.sqrt(variaton)
|
||||||
|
return sd
|
||||||
|
|
||||||
|
def calculate_weighted_sd(self, sd_case):
|
||||||
|
"""Calculate common standard deviation among meters on host"""
|
||||||
|
weighted_sd = 0
|
||||||
|
for metric, value in zip(self.metrics, sd_case):
|
||||||
|
try:
|
||||||
|
weighted_sd += value * float(self.weights[metric + '_weight'])
|
||||||
|
except KeyError as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
raise exception.WatcherException(
|
||||||
|
_("Incorrect mapping: could not find associated weight"
|
||||||
|
" for %s in weight dict.") % metric)
|
||||||
|
return weighted_sd
|
||||||
|
|
||||||
|
def calculate_migration_case(self, hosts, vm_id, src_hp_id, dst_hp_id,
|
||||||
|
current_model):
|
||||||
|
"""Calculate migration case
|
||||||
|
|
||||||
|
Return list of standard deviation values, that appearing in case of
|
||||||
|
migration of vm from source host to destination host
|
||||||
|
:param hosts: hosts with their workload
|
||||||
|
:param vm_id: the virtual machine
|
||||||
|
:param src_hp_id: the source hypervisor id
|
||||||
|
:param dst_hp_id: the destination hypervisor id
|
||||||
|
:param current_model: the cluster model
|
||||||
|
:return: list of standard deviation values
|
||||||
|
"""
|
||||||
|
migration_case = []
|
||||||
|
new_hosts = deepcopy(hosts)
|
||||||
|
vm_load = self.get_vm_load(vm_id, current_model)
|
||||||
|
d_host_vcpus = new_hosts[dst_hp_id]['vcpus']
|
||||||
|
s_host_vcpus = new_hosts[src_hp_id]['vcpus']
|
||||||
|
for metric in self.metrics:
|
||||||
|
if metric is 'cpu_util':
|
||||||
|
new_hosts[src_hp_id][metric] -= self.transform_vm_cpu(
|
||||||
|
vm_load,
|
||||||
|
s_host_vcpus)
|
||||||
|
new_hosts[dst_hp_id][metric] += self.transform_vm_cpu(
|
||||||
|
vm_load,
|
||||||
|
d_host_vcpus)
|
||||||
|
else:
|
||||||
|
new_hosts[src_hp_id][metric] -= vm_load[metric]
|
||||||
|
new_hosts[dst_hp_id][metric] += vm_load[metric]
|
||||||
|
normalized_hosts = self.normalize_hosts_load(new_hosts, current_model)
|
||||||
|
for metric in self.metrics:
|
||||||
|
migration_case.append(self.get_sd(normalized_hosts, metric))
|
||||||
|
migration_case.append(new_hosts)
|
||||||
|
return migration_case
|
||||||
|
|
||||||
|
def simulate_migrations(self, current_model, hosts):
|
||||||
|
"""Make sorted list of pairs vm:dst_host"""
|
||||||
|
def yield_hypervisors(hypervisors):
|
||||||
|
ct = CONF['watcher_strategies.workload_stabilization'].retry_count
|
||||||
|
if self.host_choice == 'cycle':
|
||||||
|
for i in itertools.cycle(hypervisors):
|
||||||
|
yield [i]
|
||||||
|
if self.host_choice == 'retry':
|
||||||
|
while True:
|
||||||
|
yield random.sample(hypervisors, ct)
|
||||||
|
if self.host_choice == 'fullsearch':
|
||||||
|
while True:
|
||||||
|
yield hypervisors
|
||||||
|
|
||||||
|
vm_host_map = []
|
||||||
|
for source_hp_id in current_model.get_all_hypervisors():
|
||||||
|
hypervisors = list(current_model.get_all_hypervisors())
|
||||||
|
hypervisors.remove(source_hp_id)
|
||||||
|
hypervisor_list = yield_hypervisors(hypervisors)
|
||||||
|
vms_id = current_model.get_mapping(). \
|
||||||
|
get_node_vms_from_id(source_hp_id)
|
||||||
|
for vm_id in vms_id:
|
||||||
|
min_sd_case = {'value': len(self.metrics)}
|
||||||
|
vm = current_model.get_vm_from_id(vm_id)
|
||||||
|
if vm.state not in [vm_state.VMState.ACTIVE.value,
|
||||||
|
vm_state.VMState.PAUSED.value]:
|
||||||
|
continue
|
||||||
|
for dst_hp_id in next(hypervisor_list):
|
||||||
|
sd_case = self.calculate_migration_case(hosts, vm_id,
|
||||||
|
source_hp_id,
|
||||||
|
dst_hp_id,
|
||||||
|
current_model)
|
||||||
|
|
||||||
|
weighted_sd = self.calculate_weighted_sd(sd_case[:-1])
|
||||||
|
|
||||||
|
if weighted_sd < min_sd_case['value']:
|
||||||
|
min_sd_case = {'host': dst_hp_id, 'value': weighted_sd,
|
||||||
|
's_host': source_hp_id, 'vm': vm_id}
|
||||||
|
vm_host_map.append(min_sd_case)
|
||||||
|
break
|
||||||
|
return sorted(vm_host_map, key=lambda x: x['value'])
|
||||||
|
|
||||||
|
def check_threshold(self, current_model):
|
||||||
|
"""Check if cluster is needed in balancing"""
|
||||||
|
hosts_load = self.get_hosts_load(current_model)
|
||||||
|
normalized_load = self.normalize_hosts_load(hosts_load, current_model)
|
||||||
|
for metric in self.metrics:
|
||||||
|
metric_sd = self.get_sd(normalized_load, metric)
|
||||||
|
if metric_sd > float(self.thresholds[metric]):
|
||||||
|
return self.simulate_migrations(current_model, hosts_load)
|
||||||
|
|
||||||
|
def add_migration(self,
|
||||||
|
resource_id,
|
||||||
|
migration_type,
|
||||||
|
src_hypervisor,
|
||||||
|
dst_hypervisor):
|
||||||
|
parameters = {'migration_type': migration_type,
|
||||||
|
'src_hypervisor': src_hypervisor,
|
||||||
|
'dst_hypervisor': dst_hypervisor}
|
||||||
|
self.solution.add_action(action_type=self.MIGRATION,
|
||||||
|
resource_id=resource_id,
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
def create_migration_vm(self, current_model, mig_vm, mig_src_hypervisor,
|
||||||
|
mig_dst_hypervisor):
|
||||||
|
"""Create migration VM """
|
||||||
|
if current_model.get_mapping().migrate_vm(
|
||||||
|
mig_vm, mig_src_hypervisor, mig_dst_hypervisor):
|
||||||
|
self.add_migration(mig_vm.uuid, 'live',
|
||||||
|
mig_src_hypervisor.uuid,
|
||||||
|
mig_dst_hypervisor.uuid)
|
||||||
|
|
||||||
|
def migrate(self, current_model, vm_uuid, src_host, dst_host):
|
||||||
|
mig_vm = current_model.get_vm_from_id(vm_uuid)
|
||||||
|
mig_src_hypervisor = current_model.get_hypervisor_from_id(src_host)
|
||||||
|
mig_dst_hypervisor = current_model.get_hypervisor_from_id(dst_host)
|
||||||
|
self.create_migration_vm(current_model, mig_vm, mig_src_hypervisor,
|
||||||
|
mig_dst_hypervisor)
|
||||||
|
|
||||||
|
def fill_solution(self, current_model):
|
||||||
|
self.solution.model = current_model
|
||||||
|
self.solution.efficacy = 100
|
||||||
|
return self.solution
|
||||||
|
|
||||||
|
def execute(self, orign_model):
|
||||||
|
LOG.info(_LI("Initializing Workload Stabilization"))
|
||||||
|
current_model = orign_model
|
||||||
|
|
||||||
|
if orign_model is None:
|
||||||
|
raise exception.ClusterStateNotDefined()
|
||||||
|
|
||||||
|
migration = self.check_threshold(current_model)
|
||||||
|
if migration:
|
||||||
|
hosts_load = self.get_hosts_load(current_model)
|
||||||
|
min_sd = 1
|
||||||
|
balanced = False
|
||||||
|
for vm_host in migration:
|
||||||
|
dst_hp_disk = current_model.get_resource_from_id(
|
||||||
|
resource.ResourceType.disk).get_capacity(
|
||||||
|
current_model.get_hypervisor_from_id(vm_host['host']))
|
||||||
|
vm_disk = current_model.get_resource_from_id(
|
||||||
|
resource.ResourceType.disk).get_capacity(
|
||||||
|
current_model.get_vm_from_id(vm_host['vm']))
|
||||||
|
if vm_disk > dst_hp_disk:
|
||||||
|
continue
|
||||||
|
vm_load = self.calculate_migration_case(hosts_load,
|
||||||
|
vm_host['vm'],
|
||||||
|
vm_host['s_host'],
|
||||||
|
vm_host['host'],
|
||||||
|
current_model)
|
||||||
|
weighted_sd = self.calculate_weighted_sd(vm_load[:-1])
|
||||||
|
if weighted_sd < min_sd:
|
||||||
|
min_sd = weighted_sd
|
||||||
|
hosts_load = vm_load[-1]
|
||||||
|
self.migrate(current_model, vm_host['vm'],
|
||||||
|
vm_host['s_host'], vm_host['host'])
|
||||||
|
|
||||||
|
for metric, value in zip(self.metrics, vm_load[:-1]):
|
||||||
|
if value < float(self.thresholds[metric]):
|
||||||
|
balanced = True
|
||||||
|
break
|
||||||
|
if balanced:
|
||||||
|
break
|
||||||
|
return self.fill_solution(current_model)
|
||||||
301
watcher/decision_engine/sync.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2016 b<>com
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _LE, _LI
|
||||||
|
from watcher.common import context
|
||||||
|
from watcher.decision_engine.strategy.loading import default
|
||||||
|
from watcher import objects
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
GoalMapping = collections.namedtuple('GoalMapping', ['name', 'display_name'])
|
||||||
|
StrategyMapping = collections.namedtuple(
|
||||||
|
'StrategyMapping', ['name', 'goal_name', 'display_name'])
|
||||||
|
|
||||||
|
|
||||||
|
class Syncer(object):
|
||||||
|
"""Syncs all available goals and strategies with the Watcher DB"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ctx = context.make_context()
|
||||||
|
self.discovered_map = None
|
||||||
|
|
||||||
|
self._available_goals = None
|
||||||
|
self._available_goals_map = None
|
||||||
|
|
||||||
|
self._available_strategies = None
|
||||||
|
self._available_strategies_map = None
|
||||||
|
|
||||||
|
# This goal mapping maps stale goal IDs to the synced goal
|
||||||
|
self.goal_mapping = dict()
|
||||||
|
# This strategy mapping maps stale strategy IDs to the synced goal
|
||||||
|
self.strategy_mapping = dict()
|
||||||
|
|
||||||
|
self.stale_audit_templates_map = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_goals(self):
|
||||||
|
if self._available_goals is None:
|
||||||
|
self._available_goals = objects.Goal.list(self.ctx)
|
||||||
|
return self._available_goals
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_strategies(self):
|
||||||
|
if self._available_strategies is None:
|
||||||
|
self._available_strategies = objects.Strategy.list(self.ctx)
|
||||||
|
return self._available_strategies
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_goals_map(self):
|
||||||
|
if self._available_goals_map is None:
|
||||||
|
self._available_goals_map = {
|
||||||
|
GoalMapping(
|
||||||
|
name=g.name, display_name=g.display_name): g
|
||||||
|
for g in self.available_goals
|
||||||
|
}
|
||||||
|
return self._available_goals_map
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_strategies_map(self):
|
||||||
|
if self._available_strategies_map is None:
|
||||||
|
goals_map = {g.id: g.name for g in self.available_goals}
|
||||||
|
self._available_strategies_map = {
|
||||||
|
StrategyMapping(
|
||||||
|
name=s.name, goal_name=goals_map[s.goal_id],
|
||||||
|
display_name=s.display_name): s
|
||||||
|
for s in self.available_strategies
|
||||||
|
}
|
||||||
|
return self._available_strategies_map
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
self.discovered_map = self._discover()
|
||||||
|
goals_map = self.discovered_map["goals"]
|
||||||
|
strategies_map = self.discovered_map["strategies"]
|
||||||
|
|
||||||
|
for goal_name, goal_map in goals_map.items():
|
||||||
|
if goal_map in self.available_goals_map:
|
||||||
|
LOG.info(_LI("Goal %s already exists"), goal_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.goal_mapping.update(self._sync_goal(goal_map))
|
||||||
|
|
||||||
|
for strategy_name, strategy_map in strategies_map.items():
|
||||||
|
if (strategy_map in self.available_strategies_map and
|
||||||
|
strategy_map.goal_name not in
|
||||||
|
[g.name for g in self.goal_mapping.values()]):
|
||||||
|
LOG.info(_LI("Strategy %s already exists"), strategy_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.strategy_mapping.update(self._sync_strategy(strategy_map))
|
||||||
|
|
||||||
|
self._sync_audit_templates()
|
||||||
|
|
||||||
|
def _sync_goal(self, goal_map):
|
||||||
|
goal_name = goal_map.name
|
||||||
|
goal_display_name = goal_map.display_name
|
||||||
|
goal_mapping = dict()
|
||||||
|
|
||||||
|
# Goals that are matching by name with the given discovered goal name
|
||||||
|
matching_goals = [g for g in self.available_goals
|
||||||
|
if g.name == goal_name]
|
||||||
|
stale_goals = self._soft_delete_stale_goals(goal_map, matching_goals)
|
||||||
|
|
||||||
|
if stale_goals or not matching_goals:
|
||||||
|
goal = objects.Goal(self.ctx)
|
||||||
|
goal.name = goal_name
|
||||||
|
goal.display_name = goal_display_name
|
||||||
|
goal.create()
|
||||||
|
LOG.info(_LI("Goal %s created"), goal_name)
|
||||||
|
|
||||||
|
# Updating the internal states
|
||||||
|
self.available_goals_map[goal] = goal_map
|
||||||
|
# Map the old goal IDs to the new (equivalent) goal
|
||||||
|
for matching_goal in matching_goals:
|
||||||
|
goal_mapping[matching_goal.id] = goal
|
||||||
|
|
||||||
|
return goal_mapping
|
||||||
|
|
||||||
|
def _sync_strategy(self, strategy_map):
|
||||||
|
strategy_name = strategy_map.name
|
||||||
|
strategy_display_name = strategy_map.display_name
|
||||||
|
goal_name = strategy_map.goal_name
|
||||||
|
strategy_mapping = dict()
|
||||||
|
|
||||||
|
# Strategies that are matching by name with the given
|
||||||
|
# discovered strategy name
|
||||||
|
matching_strategies = [s for s in self.available_strategies
|
||||||
|
if s.name == strategy_name]
|
||||||
|
stale_strategies = self._soft_delete_stale_strategies(
|
||||||
|
strategy_map, matching_strategies)
|
||||||
|
|
||||||
|
if stale_strategies or not matching_strategies:
|
||||||
|
strategy = objects.Strategy(self.ctx)
|
||||||
|
strategy.name = strategy_name
|
||||||
|
strategy.display_name = strategy_display_name
|
||||||
|
strategy.goal_id = objects.Goal.get_by_name(self.ctx, goal_name).id
|
||||||
|
strategy.create()
|
||||||
|
LOG.info(_LI("Strategy %s created"), strategy_name)
|
||||||
|
|
||||||
|
# Updating the internal states
|
||||||
|
self.available_strategies_map[strategy] = strategy_map
|
||||||
|
# Map the old strategy IDs to the new (equivalent) strategy
|
||||||
|
for matching_strategy in matching_strategies:
|
||||||
|
strategy_mapping[matching_strategy.id] = strategy
|
||||||
|
|
||||||
|
return strategy_mapping
|
||||||
|
|
||||||
|
def _sync_audit_templates(self):
|
||||||
|
# First we find audit templates that are stale because their associated
|
||||||
|
# goal or strategy has been modified and we update them in-memory
|
||||||
|
self._find_stale_audit_templates_due_to_goal()
|
||||||
|
self._find_stale_audit_templates_due_to_strategy()
|
||||||
|
|
||||||
|
# Then we handle the case where an audit template became
|
||||||
|
# stale because its related goal does not exist anymore.
|
||||||
|
self._soft_delete_removed_goals()
|
||||||
|
# Then we handle the case where an audit template became
|
||||||
|
# stale because its related strategy does not exist anymore.
|
||||||
|
self._soft_delete_removed_strategies()
|
||||||
|
|
||||||
|
# Finally, we save into the DB the updated stale audit templates
|
||||||
|
for stale_audit_template in self.stale_audit_templates_map.values():
|
||||||
|
stale_audit_template.save()
|
||||||
|
LOG.info(_LI("Audit Template '%s' synced"),
|
||||||
|
stale_audit_template.name)
|
||||||
|
|
||||||
|
def _find_stale_audit_templates_due_to_goal(self):
|
||||||
|
for goal_id, synced_goal in self.goal_mapping.items():
|
||||||
|
filters = {"goal_id": goal_id}
|
||||||
|
stale_audit_templates = objects.AuditTemplate.list(
|
||||||
|
self.ctx, filters=filters)
|
||||||
|
|
||||||
|
# Update the goal ID for the stale audit templates (w/o saving)
|
||||||
|
for audit_template in stale_audit_templates:
|
||||||
|
if audit_template.id not in self.stale_audit_templates_map:
|
||||||
|
audit_template.goal_id = synced_goal.id
|
||||||
|
self.stale_audit_templates_map[audit_template.id] = (
|
||||||
|
audit_template)
|
||||||
|
else:
|
||||||
|
self.stale_audit_templates_map[
|
||||||
|
audit_template.id].goal_id = synced_goal.id
|
||||||
|
|
||||||
|
def _find_stale_audit_templates_due_to_strategy(self):
|
||||||
|
for strategy_id, synced_strategy in self.strategy_mapping.items():
|
||||||
|
filters = {"strategy_id": strategy_id}
|
||||||
|
stale_audit_templates = objects.AuditTemplate.list(
|
||||||
|
self.ctx, filters=filters)
|
||||||
|
|
||||||
|
# Update strategy IDs for all stale audit templates (w/o saving)
|
||||||
|
for audit_template in stale_audit_templates:
|
||||||
|
if audit_template.id not in self.stale_audit_templates_map:
|
||||||
|
audit_template.strategy_id = synced_strategy.id
|
||||||
|
self.stale_audit_templates_map[audit_template.id] = (
|
||||||
|
audit_template)
|
||||||
|
else:
|
||||||
|
self.stale_audit_templates_map[
|
||||||
|
audit_template.id].strategy_id = synced_strategy.id
|
||||||
|
|
||||||
|
def _soft_delete_removed_goals(self):
|
||||||
|
removed_goals = [
|
||||||
|
g for g in self.available_goals
|
||||||
|
if g.name not in self.discovered_map['goals']]
|
||||||
|
for removed_goal in removed_goals:
|
||||||
|
removed_goal.soft_delete()
|
||||||
|
filters = {"goal_id": removed_goal.id}
|
||||||
|
invalid_ats = objects.AuditTemplate.list(self.ctx, filters=filters)
|
||||||
|
for at in invalid_ats:
|
||||||
|
LOG.warning(
|
||||||
|
_LE("Audit Template '%(audit_template)s' references a "
|
||||||
|
"goal that does not exist"),
|
||||||
|
audit_template=at.uuid)
|
||||||
|
|
||||||
|
def _soft_delete_removed_strategies(self):
|
||||||
|
removed_strategies = [
|
||||||
|
s for s in self.available_strategies
|
||||||
|
if s.name not in self.discovered_map['strategies']]
|
||||||
|
|
||||||
|
for removed_strategy in removed_strategies:
|
||||||
|
removed_strategy.soft_delete()
|
||||||
|
filters = {"strategy_id": removed_strategy.id}
|
||||||
|
invalid_ats = objects.AuditTemplate.list(self.ctx, filters=filters)
|
||||||
|
for at in invalid_ats:
|
||||||
|
LOG.info(
|
||||||
|
_LI("Audit Template '%(audit_template)s' references a "
|
||||||
|
"strategy that does not exist"),
|
||||||
|
audit_template=at.uuid)
|
||||||
|
# In this case we can reset the strategy ID to None
|
||||||
|
# so the audit template can still achieve the same goal
|
||||||
|
# but with a different strategy
|
||||||
|
if at.id not in self.stale_audit_templates_map:
|
||||||
|
at.strategy_id = None
|
||||||
|
self.stale_audit_templates_map[at.id] = at
|
||||||
|
else:
|
||||||
|
self.stale_audit_templates_map[at.id].strategy_id = None
|
||||||
|
|
||||||
|
def _discover(self):
|
||||||
|
strategies_map = {}
|
||||||
|
goals_map = {}
|
||||||
|
discovered_map = {"goals": goals_map, "strategies": strategies_map}
|
||||||
|
strategy_loader = default.DefaultStrategyLoader()
|
||||||
|
implemented_strategies = strategy_loader.list_available()
|
||||||
|
|
||||||
|
for _, strategy_cls in implemented_strategies.items():
|
||||||
|
goals_map[strategy_cls.get_goal_name()] = GoalMapping(
|
||||||
|
name=strategy_cls.get_goal_name(),
|
||||||
|
display_name=strategy_cls.get_translatable_goal_display_name())
|
||||||
|
|
||||||
|
strategies_map[strategy_cls.get_name()] = StrategyMapping(
|
||||||
|
name=strategy_cls.get_name(),
|
||||||
|
goal_name=strategy_cls.get_goal_name(),
|
||||||
|
display_name=strategy_cls.get_translatable_display_name())
|
||||||
|
|
||||||
|
return discovered_map
|
||||||
|
|
||||||
|
def _soft_delete_stale_goals(self, goal_map, matching_goals):
|
||||||
|
goal_name = goal_map.name
|
||||||
|
goal_display_name = goal_map.display_name
|
||||||
|
|
||||||
|
stale_goals = []
|
||||||
|
for matching_goal in matching_goals:
|
||||||
|
if (matching_goal.display_name == goal_display_name and
|
||||||
|
matching_goal.strategy_id not in self.strategy_mapping):
|
||||||
|
LOG.info(_LI("Goal %s unchanged"), goal_name)
|
||||||
|
else:
|
||||||
|
LOG.info(_LI("Goal %s modified"), goal_name)
|
||||||
|
matching_goal.soft_delete()
|
||||||
|
stale_goals.append(matching_goal)
|
||||||
|
|
||||||
|
return stale_goals
|
||||||
|
|
||||||
|
def _soft_delete_stale_strategies(self, strategy_map, matching_strategies):
|
||||||
|
strategy_name = strategy_map.name
|
||||||
|
strategy_display_name = strategy_map.display_name
|
||||||
|
|
||||||
|
stale_strategies = []
|
||||||
|
for matching_strategy in matching_strategies:
|
||||||
|
if (matching_strategy.display_name == strategy_display_name and
|
||||||
|
matching_strategy.goal_id not in self.goal_mapping):
|
||||||
|
LOG.info(_LI("Strategy %s unchanged"), strategy_name)
|
||||||
|
else:
|
||||||
|
LOG.info(_LI("Strategy %s modified"), strategy_name)
|
||||||
|
matching_strategy.soft_delete()
|
||||||
|
stale_strategies.append(matching_strategy)
|
||||||
|
|
||||||
|
return stale_strategies
|
||||||
@@ -7,16 +7,46 @@
|
|||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: python-watcher 0.25.1.dev3\n"
|
"Project-Id-Version: python-watcher 0.26.1.dev33\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2016-03-30 10:10+0200\n"
|
"POT-Creation-Date: 2016-05-11 15:31+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.2.0\n"
|
"Generated-By: Babel 2.3.4\n"
|
||||||
|
|
||||||
|
#: watcher/api/app.py:31
|
||||||
|
msgid "The port for the watcher API server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/api/app.py:34
|
||||||
|
msgid "The listen IP for the watcher API server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/api/app.py:37
|
||||||
|
msgid ""
|
||||||
|
"The maximum number of items returned in a single response from a "
|
||||||
|
"collection resource"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/api/app.py:41
|
||||||
|
msgid ""
|
||||||
|
"Number of workers for Watcher API service. The default is equal to the "
|
||||||
|
"number of CPUs available if that can be determined, else a default worker"
|
||||||
|
" count of 1 is returned."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/api/app.py:48
|
||||||
|
msgid ""
|
||||||
|
"Enable the integrated stand-alone API to service requests via HTTPS "
|
||||||
|
"instead of HTTP. If there is a front-end service performing HTTPS "
|
||||||
|
"offloading from the service, this option should be False; note, you will "
|
||||||
|
"want to change public API endpoint to represent SSL termination URL with "
|
||||||
|
"'public_endpoint' option."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/action.py:364
|
#: watcher/api/controllers/v1/action.py:364
|
||||||
msgid "Cannot create an action directly"
|
msgid "Cannot create an action directly"
|
||||||
@@ -30,41 +60,52 @@ msgstr ""
|
|||||||
msgid "Cannot delete an action directly"
|
msgid "Cannot delete an action directly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/action_plan.py:102
|
#: watcher/api/controllers/v1/action_plan.py:87
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Invalid state: %(state)s"
|
msgid "Invalid state: %(state)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/action_plan.py:422
|
#: watcher/api/controllers/v1/action_plan.py:407
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
|
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/audit.py:359
|
#: watcher/api/controllers/v1/audit.py:362
|
||||||
msgid "The audit template UUID or name specified is invalid"
|
msgid "The audit template UUID or name specified is invalid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/types.py:148
|
#: watcher/api/controllers/v1/audit_template.py:138
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"'%(strategy)s' strategy does relate to the '%(goal)s' goal. Possible "
|
||||||
|
"choices: %(choices)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/api/controllers/v1/audit_template.py:160
|
||||||
|
msgid "Cannot remove 'goal_uuid' attribute from an audit template"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/api/controllers/v1/types.py:123
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s is not JSON serializable"
|
msgid "%s is not JSON serializable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/types.py:184
|
#: watcher/api/controllers/v1/types.py:159
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Wrong type. Expected '%(type)s', got '%(value)s'"
|
msgid "Wrong type. Expected '%(type)s', got '%(value)s'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/types.py:223
|
#: watcher/api/controllers/v1/types.py:198
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' is an internal attribute and can not be updated"
|
msgid "'%s' is an internal attribute and can not be updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/types.py:227
|
#: watcher/api/controllers/v1/types.py:202
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' is a mandatory attribute and can not be removed"
|
msgid "'%s' is a mandatory attribute and can not be removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/api/controllers/v1/types.py:232
|
#: watcher/api/controllers/v1/types.py:207
|
||||||
msgid "'add' and 'replace' operations needs value"
|
msgid "'add' and 'replace' operations needs value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -135,22 +176,26 @@ msgstr ""
|
|||||||
msgid "Oops! We need disaster recover plan"
|
msgid "Oops! We need disaster recover plan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/cmd/api.py:46 watcher/cmd/applier.py:39
|
#: watcher/cmd/api.py:46
|
||||||
#: watcher/cmd/decisionengine.py:40
|
|
||||||
#, python-format
|
|
||||||
msgid "Starting server in PID %s"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: watcher/cmd/api.py:51
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "serving on 0.0.0.0:%(port)s, view at http://127.0.0.1:%(port)s"
|
msgid "serving on 0.0.0.0:%(port)s, view at http://127.0.0.1:%(port)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/cmd/api.py:55
|
#: watcher/cmd/api.py:50
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "serving on http://%(host)s:%(port)s"
|
msgid "serving on http://%(host)s:%(port)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/cmd/applier.py:41
|
||||||
|
#, python-format
|
||||||
|
msgid "Starting Watcher Applier service in PID %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/cmd/decisionengine.py:42
|
||||||
|
#, python-format
|
||||||
|
msgid "Starting Watcher Decision Engine service in PID %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/clients.py:29
|
#: watcher/common/clients.py:29
|
||||||
msgid "Version of Nova API to use in novaclient."
|
msgid "Version of Nova API to use in novaclient."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -212,189 +257,209 @@ msgstr ""
|
|||||||
|
|
||||||
#: watcher/common/exception.py:150
|
#: watcher/common/exception.py:150
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Expected an uuid or int but received %(identity)s"
|
msgid "Expected a uuid or int but received %(identity)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:154
|
#: watcher/common/exception.py:154
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Goal %(goal)s is not defined in Watcher configuration file"
|
msgid "Goal %(goal)s is invalid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:158
|
#: watcher/common/exception.py:158
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Expected a uuid but received %(uuid)s"
|
msgid "Strategy %(strategy)s is invalid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:162
|
#: watcher/common/exception.py:162
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Expected a logical name but received %(name)s"
|
msgid "Expected a uuid but received %(uuid)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:166
|
#: watcher/common/exception.py:166
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Expected a logical name or uuid but received %(name)s"
|
msgid "Expected a logical name but received %(name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:170
|
#: watcher/common/exception.py:170
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "AuditTemplate %(audit_template)s could not be found"
|
msgid "Expected a logical name or uuid but received %(name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:174
|
#: watcher/common/exception.py:174
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "An audit_template with UUID %(uuid)s or name %(name)s already exists"
|
msgid "Goal %(goal)s could not be found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:179
|
#: watcher/common/exception.py:178
|
||||||
|
#, python-format
|
||||||
|
msgid "A goal with UUID %(uuid)s already exists"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:182
|
||||||
|
#, python-format
|
||||||
|
msgid "Strategy %(strategy)s could not be found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:186
|
||||||
|
#, python-format
|
||||||
|
msgid "A strategy with UUID %(uuid)s already exists"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:190
|
||||||
|
#, python-format
|
||||||
|
msgid "AuditTemplate %(audit_template)s could not be found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:194
|
||||||
|
#, python-format
|
||||||
|
msgid "An audit_template with UUID or name %(audit_template)s already exists"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:199
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
|
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:184
|
#: watcher/common/exception.py:204
|
||||||
|
#, python-format
|
||||||
|
msgid "Audit type %(audit_type)s could not be found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:208
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Audit %(audit)s could not be found"
|
msgid "Audit %(audit)s could not be found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:188
|
#: watcher/common/exception.py:212
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "An audit with UUID %(uuid)s already exists"
|
msgid "An audit with UUID %(uuid)s already exists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:192
|
#: watcher/common/exception.py:216
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Audit %(audit)s is referenced by one or multiple action plans"
|
msgid "Audit %(audit)s is referenced by one or multiple action plans"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:197
|
#: watcher/common/exception.py:221
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "ActionPlan %(action_plan)s could not be found"
|
msgid "ActionPlan %(action_plan)s could not be found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:201
|
#: watcher/common/exception.py:225
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "An action plan with UUID %(uuid)s already exists"
|
msgid "An action plan with UUID %(uuid)s already exists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:205
|
#: watcher/common/exception.py:229
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
|
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:210
|
#: watcher/common/exception.py:234
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Action %(action)s could not be found"
|
msgid "Action %(action)s could not be found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:214
|
#: watcher/common/exception.py:238
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "An action with UUID %(uuid)s already exists"
|
msgid "An action with UUID %(uuid)s already exists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:218
|
#: watcher/common/exception.py:242
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Action plan %(action_plan)s is referenced by one or multiple goals"
|
msgid "Action plan %(action_plan)s is referenced by one or multiple goals"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:223
|
#: watcher/common/exception.py:247
|
||||||
msgid "Filtering actions on both audit and action-plan is prohibited"
|
msgid "Filtering actions on both audit and action-plan is prohibited"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:232
|
#: watcher/common/exception.py:256
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
|
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:238
|
#: watcher/common/exception.py:262
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Workflow execution error: %(error)s"
|
msgid "Workflow execution error: %(error)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:242
|
#: watcher/common/exception.py:266
|
||||||
msgid "Illegal argument"
|
msgid "Illegal argument"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:246
|
#: watcher/common/exception.py:270
|
||||||
msgid "No such metric"
|
msgid "No such metric"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:250
|
#: watcher/common/exception.py:274
|
||||||
msgid "No rows were returned"
|
msgid "No rows were returned"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:254
|
#: watcher/common/exception.py:278
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(client)s connection failed. Reason: %(reason)s"
|
msgid "%(client)s connection failed. Reason: %(reason)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:258
|
#: watcher/common/exception.py:282
|
||||||
msgid "'Keystone API endpoint is missing''"
|
msgid "'Keystone API endpoint is missing''"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:262
|
#: watcher/common/exception.py:286
|
||||||
msgid "The list of hypervisor(s) in the cluster is empty"
|
msgid "The list of hypervisor(s) in the cluster is empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:266
|
#: watcher/common/exception.py:290
|
||||||
msgid "The metrics resource collector is not defined"
|
msgid "The metrics resource collector is not defined"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:270
|
#: watcher/common/exception.py:294
|
||||||
msgid "the cluster state is not defined"
|
msgid "The cluster state is not defined"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:276
|
#: watcher/common/exception.py:298
|
||||||
|
#, python-format
|
||||||
|
msgid "No strategy could be found to achieve the '%(goal)s' goal."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/common/exception.py:304
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The instance '%(name)s' is not found"
|
msgid "The instance '%(name)s' is not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:280
|
#: watcher/common/exception.py:308
|
||||||
msgid "The hypervisor is not found"
|
msgid "The hypervisor is not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:284
|
#: watcher/common/exception.py:312
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Error loading plugin '%(name)s'"
|
msgid "Error loading plugin '%(name)s'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:288
|
#: watcher/common/exception.py:316
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The identifier '%(name)s' is a reserved word"
|
msgid "The identifier '%(name)s' is a reserved word"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:292
|
#: watcher/common/exception.py:320
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The %(name)s resource %(id)s is not soft deleted"
|
msgid "The %(name)s resource %(id)s is not soft deleted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/exception.py:296
|
#: watcher/common/exception.py:324
|
||||||
msgid "Limit should be positive"
|
msgid "Limit should be positive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/service.py:83
|
#: watcher/common/service.py:40
|
||||||
#, python-format
|
msgid "Seconds between running periodic tasks."
|
||||||
msgid "Created RPC server for service %(service)s on host %(host)s."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/service.py:92
|
#: watcher/common/service.py:43
|
||||||
#, python-format
|
|
||||||
msgid "Service error occurred when stopping the RPC server. Error: %s"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: watcher/common/service.py:97
|
|
||||||
#, python-format
|
|
||||||
msgid "Service error occurred when cleaning up the RPC manager. Error: %s"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: watcher/common/service.py:101
|
|
||||||
#, python-format
|
|
||||||
msgid "Stopped RPC server for service %(service)s on host %(host)s."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: watcher/common/service.py:106
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Got signal SIGUSR1. Not deregistering on next shutdown of service "
|
"Name of this node. This can be an opaque identifier. It is not "
|
||||||
"%(service)s on host %(host)s."
|
"necessarily a hostname, FQDN, or IP address. However, the node name must "
|
||||||
|
"be valid within an AMQP key, and if using ZeroMQ, a valid hostname, FQDN,"
|
||||||
|
" or IP address."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/common/utils.py:53
|
#: watcher/common/utils.py:53
|
||||||
@@ -413,101 +478,111 @@ msgid "Messaging configuration error"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:50
|
#: watcher/db/purge.py:50
|
||||||
msgid "Audit Templates"
|
msgid "Goals"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:51
|
#: watcher/db/purge.py:51
|
||||||
msgid "Audits"
|
msgid "Strategies"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:52
|
#: watcher/db/purge.py:52
|
||||||
msgid "Action Plans"
|
msgid "Audit Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:53
|
#: watcher/db/purge.py:53
|
||||||
|
msgid "Audits"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/db/purge.py:54
|
||||||
|
msgid "Action Plans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/db/purge.py:55
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:100
|
#: watcher/db/purge.py:102
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:158
|
#: watcher/db/purge.py:160
|
||||||
msgid "Audit Template"
|
msgid "Audit Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:206
|
#: watcher/db/purge.py:227
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Orphans found:\n"
|
"Orphans found:\n"
|
||||||
"%s"
|
"%s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:265
|
#: watcher/db/purge.py:306
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "There are %(count)d objects set for deletion. Continue? [y/N]"
|
msgid "There are %(count)d objects set for deletion. Continue? [y/N]"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:272
|
#: watcher/db/purge.py:313
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of objects (%(num)s) to delete from the database exceeds the "
|
"The number of objects (%(num)s) to delete from the database exceeds the "
|
||||||
"maximum number of objects (%(max_number)s) specified."
|
"maximum number of objects (%(max_number)s) specified."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:277
|
#: watcher/db/purge.py:318
|
||||||
msgid "Do you want to delete objects up to the specified maximum number? [y/N]"
|
msgid "Do you want to delete objects up to the specified maximum number? [y/N]"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:340
|
#: watcher/db/purge.py:408
|
||||||
msgid "Deleting..."
|
msgid "Deleting..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:346
|
#: watcher/db/purge.py:414
|
||||||
msgid "Starting purge command"
|
msgid "Starting purge command"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:356
|
#: watcher/db/purge.py:424
|
||||||
msgid " (orphans excluded)"
|
msgid " (orphans excluded)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:357
|
#: watcher/db/purge.py:425
|
||||||
msgid " (may include orphans)"
|
msgid " (may include orphans)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:360 watcher/db/purge.py:361
|
#: watcher/db/purge.py:428 watcher/db/purge.py:429
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Purge results summary%s:"
|
msgid "Purge results summary%s:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:364
|
#: watcher/db/purge.py:432
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Here below is a table containing the objects that can be purged%s:"
|
msgid "Here below is a table containing the objects that can be purged%s:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/purge.py:369
|
#: watcher/db/purge.py:437
|
||||||
msgid "Purge process completed"
|
msgid "Purge process completed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/sqlalchemy/api.py:362
|
#: watcher/db/sqlalchemy/api.py:443
|
||||||
msgid ""
|
msgid "Cannot overwrite UUID for an existing Goal."
|
||||||
"Multiple audit templates exist with the same name. Please use the audit "
|
|
||||||
"template uuid instead"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/sqlalchemy/api.py:384
|
#: watcher/db/sqlalchemy/api.py:509
|
||||||
|
msgid "Cannot overwrite UUID for an existing Strategy."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/db/sqlalchemy/api.py:586
|
||||||
msgid "Cannot overwrite UUID for an existing Audit Template."
|
msgid "Cannot overwrite UUID for an existing Audit Template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/sqlalchemy/api.py:495
|
#: watcher/db/sqlalchemy/api.py:683
|
||||||
msgid "Cannot overwrite UUID for an existing Audit."
|
msgid "Cannot overwrite UUID for an existing Audit."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/sqlalchemy/api.py:588
|
#: watcher/db/sqlalchemy/api.py:778
|
||||||
msgid "Cannot overwrite UUID for an existing Action."
|
msgid "Cannot overwrite UUID for an existing Action."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/db/sqlalchemy/api.py:699
|
#: watcher/db/sqlalchemy/api.py:891
|
||||||
msgid "Cannot overwrite UUID for an existing Action Plan."
|
msgid "Cannot overwrite UUID for an existing Action Plan."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -517,68 +592,157 @@ msgid ""
|
|||||||
"instead"
|
"instead"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/model/model_root.py:37
|
#: watcher/decision_engine/sync.py:94
|
||||||
#: watcher/decision_engine/model/model_root.py:42
|
#, python-format
|
||||||
|
msgid "Goal %s already exists"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:103
|
||||||
|
#, python-format
|
||||||
|
msgid "Strategy %s already exists"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:125
|
||||||
|
#, python-format
|
||||||
|
msgid "Goal %s created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:154
|
||||||
|
#, python-format
|
||||||
|
msgid "Strategy %s created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:180
|
||||||
|
#, python-format
|
||||||
|
msgid "Audit Template '%s' synced"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:225
|
||||||
|
#, python-format
|
||||||
|
msgid "Audit Template '%(audit_template)s' references a goal that does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:240
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Audit Template '%(audit_template)s' references a strategy that does not "
|
||||||
|
"exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:279
|
||||||
|
#, python-format
|
||||||
|
msgid "Goal %s unchanged"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:281
|
||||||
|
#, python-format
|
||||||
|
msgid "Goal %s modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:295
|
||||||
|
#, python-format
|
||||||
|
msgid "Strategy %s unchanged"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/sync.py:297
|
||||||
|
#, python-format
|
||||||
|
msgid "Strategy %s modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/model/model_root.py:33
|
||||||
|
#: watcher/decision_engine/model/model_root.py:38
|
||||||
msgid "'obj' argument type is not valid"
|
msgid "'obj' argument type is not valid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/planner/default.py:79
|
#: watcher/decision_engine/planner/default.py:78
|
||||||
msgid "The action plan is empty"
|
msgid "The action plan is empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/selection/default.py:60
|
#: watcher/decision_engine/strategy/selection/default.py:74
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Incorrect mapping: could not find associated strategy for '%s'"
|
msgid "Could not load any strategy for goal %(goal)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:288
|
#: watcher/decision_engine/strategy/strategies/base.py:165
|
||||||
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:335
|
msgid "Dummy goal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/base.py:188
|
||||||
|
msgid "Unclassified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/base.py:204
|
||||||
|
msgid "Server consolidation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/base.py:220
|
||||||
|
msgid "Thermal optimization"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:119
|
||||||
|
msgid "Basic offline consolidation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:296
|
||||||
|
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:343
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "No values returned by %(resource_id)s for %(metric_name)s"
|
msgid "No values returned by %(resource_id)s for %(metric_name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:448
|
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:456
|
||||||
msgid "Initializing Sercon Consolidation"
|
msgid "Initializing Sercon Consolidation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:492
|
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:500
|
||||||
msgid "The workloads of the compute nodes of the cluster is zero"
|
msgid "The workloads of the compute nodes of the cluster is zero"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:147
|
#: watcher/decision_engine/strategy/strategies/dummy_strategy.py:74
|
||||||
|
msgid "Dummy strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:102
|
||||||
|
msgid "Outlet temperature based strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:156
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s: no outlet temp data"
|
msgid "%s: no outlet temp data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:172
|
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:181
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "VM not active, skipped: %s"
|
msgid "VM not active, skipped: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:230
|
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:239
|
||||||
msgid "No hosts under outlet temp threshold found"
|
msgid "No hosts under outlet temp threshold found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:253
|
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:262
|
||||||
msgid "No proper target host could be found"
|
msgid "No proper target host could be found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:104
|
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:100
|
||||||
|
msgid "VM Workload Consolidation Strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:128
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Unexpexted resource state type, state=%(state)s, state_type=%(st)s."
|
msgid "Unexpexted resource state type, state=%(state)s, state_type=%(st)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:156
|
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:180
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cannot live migrate: vm_uuid=%(vm_uuid)s, state=%(vm_state)s."
|
msgid "Cannot live migrate: vm_uuid=%(vm_uuid)s, state=%(vm_state)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:240
|
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:264
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "No values returned by %(resource_id)s for memory.usage or disk.root.size"
|
msgid "No values returned by %(resource_id)s for memory.usage or disk.root.size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:489
|
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:515
|
||||||
msgid "Executing Smart Strategy"
|
msgid "Executing Smart Strategy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,11 @@
|
|||||||
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
|
||||||
from watcher.common import ceilometer_helper
|
from watcher.common import ceilometer_helper
|
||||||
|
|
||||||
from watcher.metrics_engine.cluster_history import base
|
from watcher.metrics_engine.cluster_history import base
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CeilometerClusterHistory(base.BaseClusterHistory):
|
class CeilometerClusterHistory(base.BaseClusterHistory):
|
||||||
|
|||||||
@@ -18,13 +18,11 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher.common import nova_helper
|
from watcher.common import nova_helper
|
||||||
from watcher.metrics_engine.cluster_model_collector import nova as cnova
|
from watcher.metrics_engine.cluster_model_collector import nova as cnova
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,15 @@ from watcher.objects import action
|
|||||||
from watcher.objects import action_plan
|
from watcher.objects import action_plan
|
||||||
from watcher.objects import audit
|
from watcher.objects import audit
|
||||||
from watcher.objects import audit_template
|
from watcher.objects import audit_template
|
||||||
|
from watcher.objects import goal
|
||||||
|
from watcher.objects import strategy
|
||||||
|
|
||||||
Audit = audit.Audit
|
Audit = audit.Audit
|
||||||
AuditTemplate = audit_template.AuditTemplate
|
AuditTemplate = audit_template.AuditTemplate
|
||||||
Action = action.Action
|
Action = action.Action
|
||||||
ActionPlan = action_plan.ActionPlan
|
ActionPlan = action_plan.ActionPlan
|
||||||
|
Goal = goal.Goal
|
||||||
|
Strategy = strategy.Strategy
|
||||||
|
|
||||||
__all__ = (Audit, AuditTemplate, Action, ActionPlan)
|
__all__ = ("Audit", "AuditTemplate", "Action",
|
||||||
|
"ActionPlan", "Goal", "Strategy")
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ class Action(base.WatcherObject):
|
|||||||
'action_type': obj_utils.str_or_none,
|
'action_type': obj_utils.str_or_none,
|
||||||
'input_parameters': obj_utils.dict_or_none,
|
'input_parameters': obj_utils.dict_or_none,
|
||||||
'state': obj_utils.str_or_none,
|
'state': obj_utils.str_or_none,
|
||||||
# todo(jed) remove parameter alarm
|
|
||||||
'alarm': obj_utils.str_or_none,
|
|
||||||
'next': obj_utils.int_or_none,
|
'next': obj_utils.int_or_none,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ be one of the following:
|
|||||||
:ref:`Administrator <administrator_definition>`
|
:ref:`Administrator <administrator_definition>`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
from watcher.db import api as dbapi
|
from watcher.db import api as dbapi
|
||||||
@@ -65,7 +67,7 @@ class State(object):
|
|||||||
PENDING = 'PENDING'
|
PENDING = 'PENDING'
|
||||||
|
|
||||||
|
|
||||||
class AuditType(object):
|
class AuditType(enum.Enum):
|
||||||
ONESHOT = 'ONESHOT'
|
ONESHOT = 'ONESHOT'
|
||||||
CONTINUOUS = 'CONTINUOUS'
|
CONTINUOUS = 'CONTINUOUS'
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ class AuditTemplate(base.WatcherObject):
|
|||||||
'uuid': obj_utils.str_or_none,
|
'uuid': obj_utils.str_or_none,
|
||||||
'name': obj_utils.str_or_none,
|
'name': obj_utils.str_or_none,
|
||||||
'description': obj_utils.str_or_none,
|
'description': obj_utils.str_or_none,
|
||||||
'goal': obj_utils.str_or_none,
|
'goal_id': obj_utils.int_or_none,
|
||||||
|
'strategy_id': obj_utils.int_or_none,
|
||||||
'host_aggregate': obj_utils.int_or_none,
|
'host_aggregate': obj_utils.int_or_none,
|
||||||
'extra': obj_utils.dict_or_none,
|
'extra': obj_utils.dict_or_none,
|
||||||
'version': obj_utils.str_or_none,
|
'version': obj_utils.str_or_none,
|
||||||
@@ -83,8 +84,7 @@ class AuditTemplate(base.WatcherObject):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _from_db_object_list(db_objects, cls, context):
|
def _from_db_object_list(db_objects, cls, context):
|
||||||
"""Converts a list of database entities to a list of formal objects."""
|
"""Converts a list of database entities to a list of formal objects."""
|
||||||
return \
|
return [AuditTemplate._from_db_object(cls(context), obj)
|
||||||
[AuditTemplate._from_db_object(cls(context), obj)
|
|
||||||
for obj in db_objects]
|
for obj in db_objects]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ class WatcherObject(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def obj_fields(self):
|
def obj_fields(self):
|
||||||
return self.fields.keys() + self.obj_extra_fields
|
return list(self.fields.keys()) + self.obj_extra_fields
|
||||||
|
|
||||||
# dictish syntactic sugar
|
# dictish syntactic sugar
|
||||||
def iteritems(self):
|
def iteritems(self):
|
||||||
|
|||||||
184
watcher/objects/goal.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.db import api as dbapi
|
||||||
|
from watcher.objects import base
|
||||||
|
from watcher.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(base.WatcherObject):
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'id': int,
|
||||||
|
'uuid': obj_utils.str_or_none,
|
||||||
|
'name': obj_utils.str_or_none,
|
||||||
|
'display_name': obj_utils.str_or_none,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object(goal, db_goal):
|
||||||
|
"""Converts a database entity to a formal object."""
|
||||||
|
for field in goal.fields:
|
||||||
|
goal[field] = db_goal[field]
|
||||||
|
|
||||||
|
goal.obj_reset_changes()
|
||||||
|
return goal
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object_list(db_objects, cls, context):
|
||||||
|
"""Converts a list of database entities to a list of formal objects."""
|
||||||
|
return [cls._from_db_object(cls(context), obj) for obj in db_objects]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, context, goal_id):
|
||||||
|
"""Find a goal based on its id or uuid
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Goal(context)
|
||||||
|
:param goal_id: the id *or* uuid of a goal.
|
||||||
|
:returns: a :class:`Goal` object.
|
||||||
|
"""
|
||||||
|
if utils.is_int_like(goal_id):
|
||||||
|
return cls.get_by_id(context, goal_id)
|
||||||
|
elif utils.is_uuid_like(goal_id):
|
||||||
|
return cls.get_by_uuid(context, goal_id)
|
||||||
|
else:
|
||||||
|
raise exception.InvalidIdentity(identity=goal_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, context, goal_id):
|
||||||
|
"""Find a goal based on its integer id
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Goal(context)
|
||||||
|
:param goal_id: the id *or* uuid of a goal.
|
||||||
|
:returns: a :class:`Goal` object.
|
||||||
|
"""
|
||||||
|
db_goal = cls.dbapi.get_goal_by_id(context, goal_id)
|
||||||
|
goal = cls._from_db_object(cls(context), db_goal)
|
||||||
|
return goal
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_uuid(cls, context, uuid):
|
||||||
|
"""Find a goal based on uuid
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Goal(context)
|
||||||
|
:param uuid: the uuid of a goal.
|
||||||
|
:returns: a :class:`Goal` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
db_goal = cls.dbapi.get_goal_by_uuid(context, uuid)
|
||||||
|
goal = cls._from_db_object(cls(context), db_goal)
|
||||||
|
return goal
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_name(cls, context, name):
|
||||||
|
"""Find a goal based on name
|
||||||
|
|
||||||
|
:param name: the name of a goal.
|
||||||
|
:param context: Security context
|
||||||
|
:returns: a :class:`Goal` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
db_goal = cls.dbapi.get_goal_by_name(context, name)
|
||||||
|
goal = cls._from_db_object(cls(context), db_goal)
|
||||||
|
return goal
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list(cls, context, limit=None, marker=None, filters=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""Return a list of :class:`Goal` objects.
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Goal(context)
|
||||||
|
:param filters: dict mapping the filter key to a value.
|
||||||
|
:param limit: maximum number of resources to return in a single result.
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
|
:param sort_key: column to sort results by.
|
||||||
|
:param sort_dir: direction to sort. "asc" or "desc".
|
||||||
|
:returns: a list of :class:`Goal` object.
|
||||||
|
"""
|
||||||
|
db_goals = cls.dbapi.get_goal_list(
|
||||||
|
context,
|
||||||
|
filters=filters,
|
||||||
|
limit=limit,
|
||||||
|
marker=marker,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
return cls._from_db_object_list(db_goals, cls, context)
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
"""Create a :class:`Goal` record in the DB."""
|
||||||
|
|
||||||
|
values = self.obj_get_changes()
|
||||||
|
db_goal = self.dbapi.create_goal(values)
|
||||||
|
self._from_db_object(self, db_goal)
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
"""Delete the :class:`Goal` from the DB."""
|
||||||
|
self.dbapi.destroy_goal(self.id)
|
||||||
|
self.obj_reset_changes()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Save updates to this :class:`Goal`.
|
||||||
|
|
||||||
|
Updates will be made column by column based on the result
|
||||||
|
of self.what_changed().
|
||||||
|
"""
|
||||||
|
updates = self.obj_get_changes()
|
||||||
|
self.dbapi.update_goal(self.id, updates)
|
||||||
|
|
||||||
|
self.obj_reset_changes()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Loads updates for this :class:`Goal`.
|
||||||
|
|
||||||
|
Loads a goal with the same uuid from the database and
|
||||||
|
checks for updated attributes. Updates are applied from
|
||||||
|
the loaded goal column by column, if there are any updates.
|
||||||
|
"""
|
||||||
|
current = self.get_by_uuid(self._context, uuid=self.uuid)
|
||||||
|
for field in self.fields:
|
||||||
|
if (hasattr(self, base.get_attrname(field)) and
|
||||||
|
self[field] != current[field]):
|
||||||
|
self[field] = current[field]
|
||||||
|
|
||||||
|
def soft_delete(self):
|
||||||
|
"""Soft Delete the :class:`Goal` from the DB."""
|
||||||
|
self.dbapi.soft_delete_goal(self.uuid)
|
||||||
222
watcher/objects/strategy.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2016 b<>com
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.db import api as dbapi
|
||||||
|
from watcher.objects import base
|
||||||
|
from watcher.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class Strategy(base.WatcherObject):
|
||||||
|
|
||||||
|
dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'id': int,
|
||||||
|
'uuid': obj_utils.str_or_none,
|
||||||
|
'name': obj_utils.str_or_none,
|
||||||
|
'display_name': obj_utils.str_or_none,
|
||||||
|
'goal_id': obj_utils.int_or_none,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object(strategy, db_strategy):
|
||||||
|
"""Converts a database entity to a formal object."""
|
||||||
|
for field in strategy.fields:
|
||||||
|
strategy[field] = db_strategy[field]
|
||||||
|
|
||||||
|
strategy.obj_reset_changes()
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object_list(db_objects, cls, context):
|
||||||
|
"""Converts a list of database entities to a list of formal objects."""
|
||||||
|
return [Strategy._from_db_object(cls(context), obj)
|
||||||
|
for obj in db_objects]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, context, strategy_id):
|
||||||
|
"""Find a strategy based on its id or uuid
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
:param strategy_id: the id *or* uuid of a strategy.
|
||||||
|
:returns: a :class:`Strategy` object.
|
||||||
|
"""
|
||||||
|
if utils.is_int_like(strategy_id):
|
||||||
|
return cls.get_by_id(context, strategy_id)
|
||||||
|
elif utils.is_uuid_like(strategy_id):
|
||||||
|
return cls.get_by_uuid(context, strategy_id)
|
||||||
|
else:
|
||||||
|
raise exception.InvalidIdentity(identity=strategy_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, context, strategy_id):
|
||||||
|
"""Find a strategy based on its integer id
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
:param strategy_id: the id of a strategy.
|
||||||
|
:returns: a :class:`Strategy` object.
|
||||||
|
"""
|
||||||
|
db_strategy = cls.dbapi.get_strategy_by_id(context, strategy_id)
|
||||||
|
strategy = Strategy._from_db_object(cls(context), db_strategy)
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_uuid(cls, context, uuid):
|
||||||
|
"""Find a strategy based on uuid
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
:param uuid: the uuid of a strategy.
|
||||||
|
:returns: a :class:`Strategy` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
db_strategy = cls.dbapi.get_strategy_by_uuid(context, uuid)
|
||||||
|
strategy = cls._from_db_object(cls(context), db_strategy)
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_name(cls, context, name):
|
||||||
|
"""Find a strategy based on name
|
||||||
|
|
||||||
|
:param name: the name of a strategy.
|
||||||
|
:param context: Security context
|
||||||
|
:returns: a :class:`Strategy` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
db_strategy = cls.dbapi.get_strategy_by_name(context, name)
|
||||||
|
strategy = cls._from_db_object(cls(context), db_strategy)
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list(cls, context, limit=None, marker=None, filters=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""Return a list of :class:`Strategy` objects.
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
:param filters: dict mapping the filter key to a value.
|
||||||
|
:param limit: maximum number of resources to return in a single result.
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
|
:param sort_key: column to sort results by.
|
||||||
|
:param sort_dir: direction to sort. "asc" or "desc".
|
||||||
|
:returns: a list of :class:`Strategy` object.
|
||||||
|
"""
|
||||||
|
db_strategies = cls.dbapi.get_strategy_list(
|
||||||
|
context,
|
||||||
|
filters=filters,
|
||||||
|
limit=limit,
|
||||||
|
marker=marker,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
return Strategy._from_db_object_list(db_strategies, cls, context)
|
||||||
|
|
||||||
|
def create(self, context=None):
|
||||||
|
"""Create a :class:`Strategy` record in the DB.
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
"""
|
||||||
|
|
||||||
|
values = self.obj_get_changes()
|
||||||
|
db_strategy = self.dbapi.create_strategy(values)
|
||||||
|
self._from_db_object(self, db_strategy)
|
||||||
|
|
||||||
|
def destroy(self, context=None):
|
||||||
|
"""Delete the :class:`Strategy` from the DB.
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
"""
|
||||||
|
self.dbapi.destroy_strategy(self.id)
|
||||||
|
self.obj_reset_changes()
|
||||||
|
|
||||||
|
def save(self, context=None):
|
||||||
|
"""Save updates to this :class:`Strategy`.
|
||||||
|
|
||||||
|
Updates will be made column by column based on the result
|
||||||
|
of self.what_changed().
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
"""
|
||||||
|
updates = self.obj_get_changes()
|
||||||
|
self.dbapi.update_strategy(self.id, updates)
|
||||||
|
|
||||||
|
self.obj_reset_changes()
|
||||||
|
|
||||||
|
def refresh(self, context=None):
|
||||||
|
"""Loads updates for this :class:`Strategy`.
|
||||||
|
|
||||||
|
Loads a strategy with the same uuid from the database and
|
||||||
|
checks for updated attributes. Updates are applied from
|
||||||
|
the loaded strategy column by column, if there are any updates.
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
"""
|
||||||
|
current = self.__class__.get_by_id(self._context, strategy_id=self.id)
|
||||||
|
for field in self.fields:
|
||||||
|
if (hasattr(self, base.get_attrname(field)) and
|
||||||
|
self[field] != current[field]):
|
||||||
|
self[field] = current[field]
|
||||||
|
|
||||||
|
def soft_delete(self, context=None):
|
||||||
|
"""Soft Delete the :class:`Strategy` from the DB.
|
||||||
|
|
||||||
|
:param context: Security context. NOTE: This should only
|
||||||
|
be used internally by the indirection_api.
|
||||||
|
Unfortunately, RPC requires context as the first
|
||||||
|
argument, even though we don't use it.
|
||||||
|
A context should be set when instantiating the
|
||||||
|
object, e.g.: Strategy(context)
|
||||||
|
"""
|
||||||
|
self.dbapi.soft_delete_strategy(self.id)
|
||||||
@@ -16,20 +16,32 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from keystoneauth1 import loading as ka_loading
|
from keystoneauth1 import loading as ka_loading
|
||||||
|
import prettytable as ptable
|
||||||
|
|
||||||
import watcher.api.app
|
import watcher.api.app
|
||||||
|
from watcher.applier.actions.loading import default as action_loader
|
||||||
from watcher.applier import manager as applier_manager
|
from watcher.applier import manager as applier_manager
|
||||||
|
from watcher.applier.workflow_engine.loading import default as \
|
||||||
|
workflow_engine_loader
|
||||||
from watcher.common import clients
|
from watcher.common import clients
|
||||||
|
from watcher.common import utils
|
||||||
from watcher.decision_engine import manager as decision_engine_manger
|
from watcher.decision_engine import manager as decision_engine_manger
|
||||||
|
from watcher.decision_engine.planner.loading import default as planner_loader
|
||||||
from watcher.decision_engine.planner import manager as planner_manager
|
from watcher.decision_engine.planner import manager as planner_manager
|
||||||
from watcher.decision_engine.strategy.selection import default \
|
from watcher.decision_engine.strategy.loading import default as strategy_loader
|
||||||
as strategy_selector
|
|
||||||
|
|
||||||
|
PLUGIN_LOADERS = (
|
||||||
|
action_loader.DefaultActionLoader,
|
||||||
|
planner_loader.DefaultPlannerLoader,
|
||||||
|
strategy_loader.DefaultStrategyLoader,
|
||||||
|
workflow_engine_loader.DefaultWorkFlowEngineLoader,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_opts():
|
def list_opts():
|
||||||
return [
|
watcher_opts = [
|
||||||
('api', watcher.api.app.API_SERVICE_OPTS),
|
('api', watcher.api.app.API_SERVICE_OPTS),
|
||||||
('watcher_goals', strategy_selector.WATCHER_GOALS_OPTS),
|
|
||||||
('watcher_decision_engine',
|
('watcher_decision_engine',
|
||||||
decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS),
|
decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS),
|
||||||
('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
|
('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
|
||||||
@@ -44,3 +56,45 @@ def list_opts():
|
|||||||
ka_loading.get_auth_plugin_conf_options('password') +
|
ka_loading.get_auth_plugin_conf_options('password') +
|
||||||
ka_loading.get_session_conf_options()))
|
ka_loading.get_session_conf_options()))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
watcher_opts += list_plugin_opts()
|
||||||
|
|
||||||
|
return watcher_opts
|
||||||
|
|
||||||
|
|
||||||
|
def list_plugin_opts():
|
||||||
|
plugins_opts = []
|
||||||
|
for plugin_loader_cls in PLUGIN_LOADERS:
|
||||||
|
plugin_loader = plugin_loader_cls()
|
||||||
|
plugins_map = plugin_loader.list_available()
|
||||||
|
|
||||||
|
for plugin_name, plugin_cls in plugins_map.items():
|
||||||
|
plugin_opts = plugin_cls.get_config_opts()
|
||||||
|
if plugin_opts:
|
||||||
|
plugins_opts.append(
|
||||||
|
(plugin_loader.get_entry_name(plugin_name), plugin_opts))
|
||||||
|
|
||||||
|
return plugins_opts
|
||||||
|
|
||||||
|
|
||||||
|
def _show_plugins_ascii_table(rows):
|
||||||
|
headers = ["Namespace", "Plugin name", "Import path"]
|
||||||
|
table = ptable.PrettyTable(field_names=headers)
|
||||||
|
for row in rows:
|
||||||
|
table.add_row(row)
|
||||||
|
return table.get_string()
|
||||||
|
|
||||||
|
|
||||||
|
def show_plugins():
|
||||||
|
rows = []
|
||||||
|
for plugin_loader_cls in PLUGIN_LOADERS:
|
||||||
|
plugin_loader = plugin_loader_cls()
|
||||||
|
plugins_map = plugin_loader.list_available()
|
||||||
|
|
||||||
|
rows += [
|
||||||
|
(plugin_loader.get_entry_name(plugin_name),
|
||||||
|
plugin_name,
|
||||||
|
utils.get_cls_import_path(plugin_cls))
|
||||||
|
for plugin_name, plugin_cls in plugins_map.items()]
|
||||||
|
|
||||||
|
return _show_plugins_ascii_table(rows)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@@ -19,44 +20,80 @@ from six.moves.urllib import parse as urlparse
|
|||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from watcher.api.controllers.v1 import audit_template as api_audit_template
|
from watcher.api.controllers.v1 import audit_template as api_audit_template
|
||||||
|
from watcher.common import exception
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
from watcher.db import api as db_api
|
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
from watcher.tests.api import base as api_base
|
from watcher.tests.api import base as api_base
|
||||||
from watcher.tests.api import utils as api_utils
|
from watcher.tests.api import utils as api_utils
|
||||||
from watcher.tests import base
|
from watcher.tests import base
|
||||||
|
from watcher.tests.db import utils as db_utils
|
||||||
from watcher.tests.objects import utils as obj_utils
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
def post_get_test_audit_template(**kw):
|
||||||
|
audit_template = api_utils.audit_template_post_data(**kw)
|
||||||
|
goal = db_utils.get_test_goal()
|
||||||
|
strategy = db_utils.get_test_strategy(goal_id=goal['id'])
|
||||||
|
del audit_template['uuid']
|
||||||
|
del audit_template['goal_id']
|
||||||
|
del audit_template['strategy_id']
|
||||||
|
audit_template['goal'] = kw.get('goal', goal['uuid'])
|
||||||
|
audit_template['strategy'] = kw.get('strategy', strategy['uuid'])
|
||||||
|
return audit_template
|
||||||
|
|
||||||
|
|
||||||
class TestAuditTemplateObject(base.TestCase):
|
class TestAuditTemplateObject(base.TestCase):
|
||||||
|
|
||||||
def test_audit_template_init(self):
|
def test_audit_template_init(self):
|
||||||
audit_template_dict = api_utils.audit_template_post_data()
|
audit_template_dict = post_get_test_audit_template()
|
||||||
del audit_template_dict['name']
|
del audit_template_dict['name']
|
||||||
audit_template = api_audit_template.AuditTemplate(
|
audit_template = api_audit_template.AuditTemplate(
|
||||||
**audit_template_dict)
|
**audit_template_dict)
|
||||||
self.assertEqual(wtypes.Unset, audit_template.name)
|
self.assertEqual(wtypes.Unset, audit_template.name)
|
||||||
|
|
||||||
|
|
||||||
class TestListAuditTemplate(api_base.FunctionalTest):
|
class FunctionalTestWithSetup(api_base.FunctionalTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(FunctionalTestWithSetup, self).setUp()
|
||||||
|
self.fake_goal1 = obj_utils.get_test_goal(
|
||||||
|
self.context, id=1, uuid=utils.generate_uuid(), name="DUMMY_1")
|
||||||
|
self.fake_goal2 = obj_utils.get_test_goal(
|
||||||
|
self.context, id=2, uuid=utils.generate_uuid(), name="DUMMY_2")
|
||||||
|
self.fake_goal1.create()
|
||||||
|
self.fake_goal2.create()
|
||||||
|
self.fake_strategy1 = obj_utils.get_test_strategy(
|
||||||
|
self.context, id=1, uuid=utils.generate_uuid(), name="STRATEGY_1",
|
||||||
|
goal_id=self.fake_goal1.id)
|
||||||
|
self.fake_strategy2 = obj_utils.get_test_strategy(
|
||||||
|
self.context, id=2, uuid=utils.generate_uuid(), name="STRATEGY_2",
|
||||||
|
goal_id=self.fake_goal2.id)
|
||||||
|
self.fake_strategy1.create()
|
||||||
|
self.fake_strategy2.create()
|
||||||
|
|
||||||
|
|
||||||
|
class TestListAuditTemplate(FunctionalTestWithSetup):
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
response = self.get_json('/audit_templates')
|
response = self.get_json('/audit_templates')
|
||||||
self.assertEqual([], response['audit_templates'])
|
self.assertEqual([], response['audit_templates'])
|
||||||
|
|
||||||
def _assert_audit_template_fields(self, audit_template):
|
def _assert_audit_template_fields(self, audit_template):
|
||||||
audit_template_fields = ['name', 'goal', 'host_aggregate']
|
audit_template_fields = ['name', 'goal_uuid', 'goal_name',
|
||||||
|
'strategy_uuid', 'strategy_name',
|
||||||
|
'host_aggregate']
|
||||||
for field in audit_template_fields:
|
for field in audit_template_fields:
|
||||||
self.assertIn(field, audit_template)
|
self.assertIn(field, audit_template)
|
||||||
|
|
||||||
def test_one(self):
|
def test_one(self):
|
||||||
audit_template = obj_utils.create_test_audit_template(self.context)
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
|
self.context, strategy_id=self.fake_strategy1.id)
|
||||||
response = self.get_json('/audit_templates')
|
response = self.get_json('/audit_templates')
|
||||||
self.assertEqual(audit_template.uuid,
|
self.assertEqual(audit_template.uuid,
|
||||||
response['audit_templates'][0]["uuid"])
|
response['audit_templates'][0]["uuid"])
|
||||||
self._assert_audit_template_fields(response['audit_templates'][0])
|
self._assert_audit_template_fields(response['audit_templates'][0])
|
||||||
|
|
||||||
def test_one_soft_deleted(self):
|
def test_get_one_soft_deleted_ok(self):
|
||||||
audit_template = obj_utils.create_test_audit_template(self.context)
|
audit_template = obj_utils.create_test_audit_template(self.context)
|
||||||
audit_template.soft_delete()
|
audit_template.soft_delete()
|
||||||
response = self.get_json('/audit_templates',
|
response = self.get_json('/audit_templates',
|
||||||
@@ -124,53 +161,57 @@ class TestListAuditTemplate(api_base.FunctionalTest):
|
|||||||
|
|
||||||
def test_many(self):
|
def test_many(self):
|
||||||
audit_template_list = []
|
audit_template_list = []
|
||||||
for id_ in range(5):
|
for id_ in range(1, 6):
|
||||||
audit_template = obj_utils.create_test_audit_template(
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
self.context, id=id_,
|
self.context, id=id_,
|
||||||
uuid=utils.generate_uuid(),
|
uuid=utils.generate_uuid(),
|
||||||
name='My Audit Template {0}'.format(id_))
|
name='My Audit Template {0}'.format(id_))
|
||||||
audit_template_list.append(audit_template.uuid)
|
audit_template_list.append(audit_template)
|
||||||
|
|
||||||
response = self.get_json('/audit_templates')
|
response = self.get_json('/audit_templates')
|
||||||
self.assertEqual(len(audit_template_list),
|
self.assertEqual(len(audit_template_list),
|
||||||
len(response['audit_templates']))
|
len(response['audit_templates']))
|
||||||
uuids = [s['uuid'] for s in response['audit_templates']]
|
uuids = [s['uuid'] for s in response['audit_templates']]
|
||||||
self.assertEqual(sorted(audit_template_list), sorted(uuids))
|
self.assertEqual(
|
||||||
|
sorted([at.uuid for at in audit_template_list]),
|
||||||
|
sorted(uuids))
|
||||||
|
|
||||||
def test_many_without_soft_deleted(self):
|
def test_many_without_soft_deleted(self):
|
||||||
audit_template_list = []
|
audit_template_list = []
|
||||||
for id_ in [1, 2, 3]:
|
for id_ in range(1, 6):
|
||||||
audit_template = obj_utils.create_test_audit_template(
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
name='My Audit Template {0}'.format(id_))
|
name='My Audit Template {0}'.format(id_))
|
||||||
audit_template_list.append(audit_template.uuid)
|
audit_template_list.append(audit_template)
|
||||||
for id_ in [4, 5]:
|
|
||||||
audit_template = obj_utils.create_test_audit_template(
|
# We soft delete the ones with ID 4 and 5
|
||||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
[at.soft_delete() for at in audit_template_list[3:]]
|
||||||
name='My Audit Template {0}'.format(id_))
|
|
||||||
audit_template.soft_delete()
|
|
||||||
response = self.get_json('/audit_templates')
|
response = self.get_json('/audit_templates')
|
||||||
self.assertEqual(3, len(response['audit_templates']))
|
self.assertEqual(3, len(response['audit_templates']))
|
||||||
uuids = [s['uuid'] for s in response['audit_templates']]
|
uuids = [s['uuid'] for s in response['audit_templates']]
|
||||||
self.assertEqual(sorted(audit_template_list), sorted(uuids))
|
self.assertEqual(
|
||||||
|
sorted([at.uuid for at in audit_template_list[:3]]),
|
||||||
|
sorted(uuids))
|
||||||
|
|
||||||
def test_many_with_soft_deleted(self):
|
def test_many_with_soft_deleted(self):
|
||||||
audit_template_list = []
|
audit_template_list = []
|
||||||
for id_ in [1, 2, 3]:
|
for id_ in range(1, 6):
|
||||||
audit_template = obj_utils.create_test_audit_template(
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
name='My Audit Template {0}'.format(id_))
|
name='My Audit Template {0}'.format(id_))
|
||||||
audit_template_list.append(audit_template.uuid)
|
audit_template_list.append(audit_template)
|
||||||
for id_ in [4, 5]:
|
|
||||||
audit_template = obj_utils.create_test_audit_template(
|
# We soft delete the ones with ID 4 and 5
|
||||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
[at.soft_delete() for at in audit_template_list[3:]]
|
||||||
name='My Audit Template {0}'.format(id_))
|
|
||||||
audit_template.soft_delete()
|
|
||||||
audit_template_list.append(audit_template.uuid)
|
|
||||||
response = self.get_json('/audit_templates',
|
response = self.get_json('/audit_templates',
|
||||||
headers={'X-Show-Deleted': 'True'})
|
headers={'X-Show-Deleted': 'True'})
|
||||||
self.assertEqual(5, len(response['audit_templates']))
|
self.assertEqual(5, len(response['audit_templates']))
|
||||||
uuids = [s['uuid'] for s in response['audit_templates']]
|
uuids = [s['uuid'] for s in response['audit_templates']]
|
||||||
self.assertEqual(sorted(audit_template_list), sorted(uuids))
|
self.assertEqual(
|
||||||
|
sorted([at.uuid for at in audit_template_list]),
|
||||||
|
sorted(uuids))
|
||||||
|
|
||||||
def test_links(self):
|
def test_links(self):
|
||||||
uuid = utils.generate_uuid()
|
uuid = utils.generate_uuid()
|
||||||
@@ -207,88 +248,110 @@ class TestListAuditTemplate(api_base.FunctionalTest):
|
|||||||
next_marker = response['audit_templates'][-1]['uuid']
|
next_marker = response['audit_templates'][-1]['uuid']
|
||||||
self.assertIn(next_marker, response['next'])
|
self.assertIn(next_marker, response['next'])
|
||||||
|
|
||||||
def test_filter_by_goal(self):
|
def test_filter_by_goal_uuid(self):
|
||||||
cfg.CONF.set_override('goals', {"DUMMY": "DUMMY", "BASIC": "BASIC"},
|
for id_, goal_id in enumerate(itertools.chain.from_iterable([
|
||||||
group='watcher_goals', enforce_type=True)
|
itertools.repeat(self.fake_goal1.id, 3),
|
||||||
|
itertools.repeat(self.fake_goal2.id, 2)]), 1):
|
||||||
for id_ in range(2):
|
|
||||||
obj_utils.create_test_audit_template(
|
obj_utils.create_test_audit_template(
|
||||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
name='My Audit Template {0}'.format(id_),
|
name='My Audit Template {0}'.format(id_),
|
||||||
goal="DUMMY")
|
goal_id=goal_id)
|
||||||
|
|
||||||
for id_ in range(2, 5):
|
response = self.get_json(
|
||||||
|
'/audit_templates?goal=%s' % self.fake_goal2.uuid)
|
||||||
|
self.assertEqual(2, len(response['audit_templates']))
|
||||||
|
|
||||||
|
def test_filter_by_goal_name(self):
|
||||||
|
for id_, goal_id in enumerate(itertools.chain.from_iterable([
|
||||||
|
itertools.repeat(self.fake_goal1.id, 3),
|
||||||
|
itertools.repeat(self.fake_goal2.id, 2)]), 1):
|
||||||
obj_utils.create_test_audit_template(
|
obj_utils.create_test_audit_template(
|
||||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
name='My Audit Template {0}'.format(id_),
|
name='My Audit Template {0}'.format(id_),
|
||||||
goal="BASIC")
|
goal_id=goal_id)
|
||||||
|
|
||||||
response = self.get_json('/audit_templates?goal=BASIC')
|
response = self.get_json(
|
||||||
self.assertEqual(3, len(response['audit_templates']))
|
'/audit_templates?goal=%s' % self.fake_goal2.name)
|
||||||
|
self.assertEqual(2, len(response['audit_templates']))
|
||||||
|
|
||||||
|
def test_filter_by_strategy_uuid(self):
|
||||||
|
for id_, strategy_id in enumerate(itertools.chain.from_iterable([
|
||||||
|
itertools.repeat(self.fake_strategy1.id, 3),
|
||||||
|
itertools.repeat(self.fake_strategy2.id, 2)]), 1):
|
||||||
|
obj_utils.create_test_audit_template(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
name='My Audit Template {0}'.format(id_),
|
||||||
|
strategy_id=strategy_id)
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/audit_templates?strategy=%s' % self.fake_strategy2.uuid)
|
||||||
|
self.assertEqual(2, len(response['audit_templates']))
|
||||||
|
|
||||||
|
def test_filter_by_strategy_name(self):
|
||||||
|
for id_, strategy_id in enumerate(itertools.chain.from_iterable([
|
||||||
|
itertools.repeat(self.fake_strategy1.id, 3),
|
||||||
|
itertools.repeat(self.fake_strategy2.id, 2)]), 1):
|
||||||
|
obj_utils.create_test_audit_template(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
name='My Audit Template {0}'.format(id_),
|
||||||
|
strategy_id=strategy_id)
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/audit_templates?strategy=%s' % self.fake_strategy2.name)
|
||||||
|
self.assertEqual(2, len(response['audit_templates']))
|
||||||
|
|
||||||
|
|
||||||
class TestPatch(api_base.FunctionalTest):
|
class TestPatch(FunctionalTestWithSetup):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestPatch, self).setUp()
|
super(TestPatch, self).setUp()
|
||||||
self.audit_template = obj_utils.create_test_audit_template(
|
self.audit_template = obj_utils.create_test_audit_template(
|
||||||
self.context)
|
self.context, strategy_id=None)
|
||||||
p = mock.patch.object(db_api.BaseConnection, 'update_audit_template')
|
|
||||||
self.mock_audit_template_update = p.start()
|
|
||||||
self.mock_audit_template_update.side_effect = \
|
|
||||||
self._simulate_rpc_audit_template_update
|
|
||||||
cfg.CONF.set_override('goals', {"DUMMY": "DUMMY", "BASIC": "BASIC"},
|
|
||||||
group='watcher_goals', enforce_type=True)
|
|
||||||
self.addCleanup(p.stop)
|
|
||||||
|
|
||||||
def _simulate_rpc_audit_template_update(self, audit_template):
|
@mock.patch.object(timeutils, 'utcnow')
|
||||||
audit_template.save()
|
def test_replace_goal_uuid(self, mock_utcnow):
|
||||||
return audit_template
|
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
|
||||||
def test_replace_ok(self, mock_utcnow):
|
|
||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
mock_utcnow.return_value = test_time
|
mock_utcnow.return_value = test_time
|
||||||
|
|
||||||
new_goal = "BASIC"
|
new_goal_uuid = self.fake_goal2.uuid
|
||||||
response = self.get_json(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid)
|
'/audit_templates/%s' % self.audit_template.uuid)
|
||||||
self.assertNotEqual(new_goal, response['goal'])
|
self.assertNotEqual(new_goal_uuid, response['goal_uuid'])
|
||||||
|
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid,
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
[{'path': '/goal', 'value': new_goal,
|
[{'path': '/goal', 'value': new_goal_uuid,
|
||||||
'op': 'replace'}])
|
'op': 'replace'}])
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
response = self.get_json(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid)
|
'/audit_templates/%s' % self.audit_template.uuid)
|
||||||
self.assertEqual(new_goal, response['goal'])
|
self.assertEqual(new_goal_uuid, response['goal_uuid'])
|
||||||
return_updated_at = timeutils.parse_isotime(
|
return_updated_at = timeutils.parse_isotime(
|
||||||
response['updated_at']).replace(tzinfo=None)
|
response['updated_at']).replace(tzinfo=None)
|
||||||
self.assertEqual(test_time, return_updated_at)
|
self.assertEqual(test_time, return_updated_at)
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
@mock.patch.object(timeutils, 'utcnow')
|
||||||
def test_replace_ok_by_name(self, mock_utcnow):
|
def test_replace_goal_uuid_by_name(self, mock_utcnow):
|
||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
mock_utcnow.return_value = test_time
|
mock_utcnow.return_value = test_time
|
||||||
|
|
||||||
new_goal = 'BASIC'
|
new_goal_uuid = self.fake_goal2.uuid
|
||||||
response = self.get_json(urlparse.quote(
|
response = self.get_json(urlparse.quote(
|
||||||
'/audit_templates/%s' % self.audit_template.name))
|
'/audit_templates/%s' % self.audit_template.name))
|
||||||
self.assertNotEqual(new_goal, response['goal'])
|
self.assertNotEqual(new_goal_uuid, response['goal_uuid'])
|
||||||
|
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
'/audit_templates/%s' % self.audit_template.name,
|
'/audit_templates/%s' % self.audit_template.name,
|
||||||
[{'path': '/goal', 'value': new_goal,
|
[{'path': '/goal', 'value': new_goal_uuid,
|
||||||
'op': 'replace'}])
|
'op': 'replace'}])
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
response = self.get_json(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.name)
|
'/audit_templates/%s' % self.audit_template.name)
|
||||||
self.assertEqual(new_goal, response['goal'])
|
self.assertEqual(new_goal_uuid, response['goal_uuid'])
|
||||||
return_updated_at = timeutils.parse_isotime(
|
return_updated_at = timeutils.parse_isotime(
|
||||||
response['updated_at']).replace(tzinfo=None)
|
response['updated_at']).replace(tzinfo=None)
|
||||||
self.assertEqual(test_time, return_updated_at)
|
self.assertEqual(test_time, return_updated_at)
|
||||||
@@ -296,8 +359,8 @@ class TestPatch(api_base.FunctionalTest):
|
|||||||
def test_replace_non_existent_audit_template(self):
|
def test_replace_non_existent_audit_template(self):
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
'/audit_templates/%s' % utils.generate_uuid(),
|
'/audit_templates/%s' % utils.generate_uuid(),
|
||||||
[{'path': '/goal', 'value': 'DUMMY',
|
[{'path': '/goal', 'value': self.fake_goal1.uuid,
|
||||||
'op': 'replace'}],
|
'op': 'replace'}],
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(404, response.status_int)
|
self.assertEqual(404, response.status_int)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -311,23 +374,61 @@ class TestPatch(api_base.FunctionalTest):
|
|||||||
) as cn_mock:
|
) as cn_mock:
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid,
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
[{'path': '/goal', 'value': 'INVALID_GOAL',
|
[{'path': '/goal', 'value': utils.generate_uuid(),
|
||||||
'op': 'replace'}],
|
'op': 'replace'}],
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(400, response.status_int)
|
self.assertEqual(400, response.status_int)
|
||||||
assert not cn_mock.called
|
assert not cn_mock.called
|
||||||
|
|
||||||
def test_add_ok(self):
|
def test_add_goal_uuid(self):
|
||||||
new_goal = 'DUMMY'
|
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid,
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
[{'path': '/goal', 'value': new_goal, 'op': 'add'}])
|
[{'path': '/goal',
|
||||||
|
'value': self.fake_goal2.uuid,
|
||||||
|
'op': 'add'}])
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(200, response.status_int)
|
self.assertEqual(200, response.status_int)
|
||||||
|
|
||||||
response = self.get_json(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid)
|
'/audit_templates/%s' % self.audit_template.uuid)
|
||||||
self.assertEqual(new_goal, response['goal'])
|
self.assertEqual(self.fake_goal2.uuid, response['goal_uuid'])
|
||||||
|
|
||||||
|
def test_add_strategy_uuid(self):
|
||||||
|
response = self.patch_json(
|
||||||
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
|
[{'path': '/strategy',
|
||||||
|
'value': self.fake_strategy1.uuid,
|
||||||
|
'op': 'add'}])
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(200, response.status_int)
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/audit_templates/%s' % self.audit_template.uuid)
|
||||||
|
self.assertEqual(self.fake_strategy1.uuid, response['strategy_uuid'])
|
||||||
|
|
||||||
|
def test_replace_strategy_uuid(self):
|
||||||
|
response = self.patch_json(
|
||||||
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
|
[{'path': '/strategy',
|
||||||
|
'value': self.fake_strategy2['uuid'],
|
||||||
|
'op': 'replace'}])
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(200, response.status_int)
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/audit_templates/%s' % self.audit_template.uuid)
|
||||||
|
self.assertEqual(
|
||||||
|
self.fake_strategy2['uuid'], response['strategy_uuid'])
|
||||||
|
|
||||||
|
def test_replace_invalid_strategy(self):
|
||||||
|
response = self.patch_json(
|
||||||
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
|
[{'path': '/strategy',
|
||||||
|
'value': utils.generate_uuid(), # Does not exist
|
||||||
|
'op': 'replace'}], expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
def test_add_non_existent_property(self):
|
def test_add_non_existent_property(self):
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
@@ -338,20 +439,34 @@ class TestPatch(api_base.FunctionalTest):
|
|||||||
self.assertEqual(400, response.status_int)
|
self.assertEqual(400, response.status_int)
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
def test_remove_ok(self):
|
def test_remove_strategy(self):
|
||||||
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
|
self.context, uuid=utils.generate_uuid(),
|
||||||
|
name="AT_%s" % utils.generate_uuid(),
|
||||||
|
goal_id=self.fake_goal1.id,
|
||||||
|
strategy_id=self.fake_strategy1.id)
|
||||||
response = self.get_json(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid)
|
'/audit_templates/%s' % audit_template.uuid)
|
||||||
self.assertIsNotNone(response['goal'])
|
self.assertIsNotNone(response['strategy_uuid'])
|
||||||
|
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid,
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
[{'path': '/goal', 'op': 'remove'}])
|
[{'path': '/strategy', 'op': 'remove'}])
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
def test_remove_goal(self):
|
||||||
response = self.get_json(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.uuid)
|
'/audit_templates/%s' % self.audit_template.uuid)
|
||||||
self.assertIsNone(response['goal'])
|
self.assertIsNotNone(response['goal_uuid'])
|
||||||
|
|
||||||
|
response = self.patch_json(
|
||||||
|
'/audit_templates/%s' % self.audit_template.uuid,
|
||||||
|
[{'path': '/goal', 'op': 'remove'}],
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
def test_remove_uuid(self):
|
def test_remove_uuid(self):
|
||||||
response = self.patch_json(
|
response = self.patch_json(
|
||||||
@@ -372,23 +487,13 @@ class TestPatch(api_base.FunctionalTest):
|
|||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
|
||||||
class TestPost(api_base.FunctionalTest):
|
class TestPost(FunctionalTestWithSetup):
|
||||||
|
|
||||||
def setUp(self):
|
@mock.patch.object(timeutils, 'utcnow')
|
||||||
super(TestPost, self).setUp()
|
|
||||||
p = mock.patch.object(db_api.BaseConnection, 'create_audit_template')
|
|
||||||
self.mock_create_audit_template = p.start()
|
|
||||||
self.mock_create_audit_template.side_effect = (
|
|
||||||
self._simulate_rpc_audit_template_create)
|
|
||||||
self.addCleanup(p.stop)
|
|
||||||
|
|
||||||
def _simulate_rpc_audit_template_create(self, audit_template):
|
|
||||||
audit_template.create()
|
|
||||||
return audit_template
|
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
|
||||||
def test_create_audit_template(self, mock_utcnow):
|
def test_create_audit_template(self, mock_utcnow):
|
||||||
audit_template_dict = api_utils.audit_template_post_data()
|
audit_template_dict = post_get_test_audit_template(
|
||||||
|
goal=self.fake_goal1.uuid,
|
||||||
|
strategy=self.fake_strategy1.uuid)
|
||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
mock_utcnow.return_value = test_time
|
mock_utcnow.return_value = test_time
|
||||||
|
|
||||||
@@ -398,39 +503,40 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
# Check location header
|
# Check location header
|
||||||
self.assertIsNotNone(response.location)
|
self.assertIsNotNone(response.location)
|
||||||
expected_location = \
|
expected_location = \
|
||||||
'/v1/audit_templates/%s' % audit_template_dict['uuid']
|
'/v1/audit_templates/%s' % response.json['uuid']
|
||||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||||
expected_location)
|
expected_location)
|
||||||
self.assertEqual(audit_template_dict['uuid'], response.json['uuid'])
|
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
|
||||||
self.assertNotIn('updated_at', response.json.keys)
|
self.assertNotIn('updated_at', response.json.keys)
|
||||||
self.assertNotIn('deleted_at', response.json.keys)
|
self.assertNotIn('deleted_at', response.json.keys)
|
||||||
|
self.assertEqual(self.fake_goal1.uuid, response.json['goal_uuid'])
|
||||||
|
self.assertEqual(self.fake_strategy1.uuid,
|
||||||
|
response.json['strategy_uuid'])
|
||||||
return_created_at = timeutils.parse_isotime(
|
return_created_at = timeutils.parse_isotime(
|
||||||
response.json['created_at']).replace(tzinfo=None)
|
response.json['created_at']).replace(tzinfo=None)
|
||||||
self.assertEqual(test_time, return_created_at)
|
self.assertEqual(test_time, return_created_at)
|
||||||
|
|
||||||
def test_create_audit_template_doesnt_contain_id(self):
|
def test_create_audit_template_does_autogenerate_id(self):
|
||||||
|
audit_template_dict = post_get_test_audit_template(
|
||||||
|
goal=self.fake_goal1.uuid, strategy=None)
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.dbapi,
|
self.dbapi,
|
||||||
'create_audit_template',
|
'create_audit_template',
|
||||||
wraps=self.dbapi.create_audit_template
|
wraps=self.dbapi.create_audit_template
|
||||||
) as cn_mock:
|
) as cn_mock:
|
||||||
audit_template_dict = api_utils.audit_template_post_data(
|
|
||||||
goal='DUMMY')
|
|
||||||
response = self.post_json('/audit_templates', audit_template_dict)
|
response = self.post_json('/audit_templates', audit_template_dict)
|
||||||
self.assertEqual(audit_template_dict['goal'],
|
self.assertEqual(audit_template_dict['goal'],
|
||||||
response.json['goal'])
|
response.json['goal_uuid'])
|
||||||
cn_mock.assert_called_once_with(mock.ANY)
|
# Check that 'id' is not in first arg of positional args
|
||||||
# Check that 'id' is not in first arg of positional args
|
self.assertNotIn('id', cn_mock.call_args[0][0])
|
||||||
self.assertNotIn('id', cn_mock.call_args[0][0])
|
|
||||||
|
|
||||||
def test_create_audit_template_generate_uuid(self):
|
def test_create_audit_template_generate_uuid(self):
|
||||||
audit_template_dict = api_utils.audit_template_post_data()
|
audit_template_dict = post_get_test_audit_template(
|
||||||
del audit_template_dict['uuid']
|
goal=self.fake_goal1.uuid, strategy=None)
|
||||||
|
|
||||||
response = self.post_json('/audit_templates', audit_template_dict)
|
response = self.post_json('/audit_templates', audit_template_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(201, response.status_int)
|
self.assertEqual(201, response.status_int)
|
||||||
self.assertEqual(audit_template_dict['goal'], response.json['goal'])
|
|
||||||
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
|
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
|
||||||
|
|
||||||
def test_create_audit_template_with_invalid_goal(self):
|
def test_create_audit_template_with_invalid_goal(self):
|
||||||
@@ -439,13 +545,54 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
'create_audit_template',
|
'create_audit_template',
|
||||||
wraps=self.dbapi.create_audit_template
|
wraps=self.dbapi.create_audit_template
|
||||||
) as cn_mock:
|
) as cn_mock:
|
||||||
audit_template_dict = api_utils.audit_template_post_data(
|
audit_template_dict = post_get_test_audit_template(
|
||||||
goal='INVALID_GOAL')
|
goal_uuid=utils.generate_uuid())
|
||||||
response = self.post_json('/audit_templates',
|
response = self.post_json('/audit_templates',
|
||||||
audit_template_dict, expect_errors=True)
|
audit_template_dict, expect_errors=True)
|
||||||
self.assertEqual(400, response.status_int)
|
self.assertEqual(400, response.status_int)
|
||||||
assert not cn_mock.called
|
assert not cn_mock.called
|
||||||
|
|
||||||
|
def test_create_audit_template_with_invalid_strategy(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.dbapi,
|
||||||
|
'create_audit_template',
|
||||||
|
wraps=self.dbapi.create_audit_template
|
||||||
|
) as cn_mock:
|
||||||
|
audit_template_dict = post_get_test_audit_template(
|
||||||
|
goal_uuid=self.fake_goal1['uuid'],
|
||||||
|
strategy_uuid=utils.generate_uuid())
|
||||||
|
response = self.post_json('/audit_templates',
|
||||||
|
audit_template_dict, expect_errors=True)
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
assert not cn_mock.called
|
||||||
|
|
||||||
|
def test_create_audit_template_with_unrelated_strategy(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.dbapi,
|
||||||
|
'create_audit_template',
|
||||||
|
wraps=self.dbapi.create_audit_template
|
||||||
|
) as cn_mock:
|
||||||
|
audit_template_dict = post_get_test_audit_template(
|
||||||
|
goal_uuid=self.fake_goal1['uuid'],
|
||||||
|
strategy=self.fake_strategy2['uuid'])
|
||||||
|
response = self.post_json('/audit_templates',
|
||||||
|
audit_template_dict, expect_errors=True)
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
assert not cn_mock.called
|
||||||
|
|
||||||
|
def test_create_audit_template_with_uuid(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.dbapi,
|
||||||
|
'create_audit_template',
|
||||||
|
wraps=self.dbapi.create_audit_template
|
||||||
|
) as cn_mock:
|
||||||
|
audit_template_dict = post_get_test_audit_template()
|
||||||
|
response = self.post_json('/audit_templates', audit_template_dict,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
assert not cn_mock.called
|
||||||
|
|
||||||
|
|
||||||
class TestDelete(api_base.FunctionalTest):
|
class TestDelete(api_base.FunctionalTest):
|
||||||
|
|
||||||
@@ -453,54 +600,52 @@ class TestDelete(api_base.FunctionalTest):
|
|||||||
super(TestDelete, self).setUp()
|
super(TestDelete, self).setUp()
|
||||||
self.audit_template = obj_utils.create_test_audit_template(
|
self.audit_template = obj_utils.create_test_audit_template(
|
||||||
self.context)
|
self.context)
|
||||||
p = mock.patch.object(db_api.BaseConnection, 'update_audit_template')
|
|
||||||
self.mock_audit_template_update = p.start()
|
|
||||||
self.mock_audit_template_update.side_effect = \
|
|
||||||
self._simulate_rpc_audit_template_update
|
|
||||||
self.addCleanup(p.stop)
|
|
||||||
|
|
||||||
def _simulate_rpc_audit_template_update(self, audit_template):
|
@mock.patch.object(timeutils, 'utcnow')
|
||||||
audit_template.save()
|
|
||||||
return audit_template
|
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
|
||||||
def test_delete_audit_template_by_uuid(self, mock_utcnow):
|
def test_delete_audit_template_by_uuid(self, mock_utcnow):
|
||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
|
||||||
mock_utcnow.return_value = test_time
|
|
||||||
self.delete('/audit_templates/%s' % self.audit_template.uuid)
|
|
||||||
response = self.get_json(
|
|
||||||
'/audit_templates/%s' % self.audit_template.uuid,
|
|
||||||
expect_errors=True)
|
|
||||||
# self.assertEqual(404, response.status_int)
|
|
||||||
self.assertEqual('application/json', response.content_type)
|
|
||||||
self.assertTrue(response.json['error_message'])
|
|
||||||
|
|
||||||
self.context.show_deleted = True
|
|
||||||
audit_template = objects.AuditTemplate.get_by_uuid(
|
|
||||||
self.context, self.audit_template.uuid)
|
|
||||||
|
|
||||||
return_deleted_at = timeutils.strtime(audit_template['deleted_at'])
|
|
||||||
self.assertEqual(timeutils.strtime(test_time), return_deleted_at)
|
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
|
||||||
def test_delete_audit_template_by_name(self, mock_utcnow):
|
|
||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
mock_utcnow.return_value = test_time
|
mock_utcnow.return_value = test_time
|
||||||
self.delete(urlparse.quote('/audit_templates/%s' %
|
self.delete(urlparse.quote('/audit_templates/%s' %
|
||||||
self.audit_template.name))
|
self.audit_template.uuid))
|
||||||
response = self.get_json(urlparse.quote(
|
response = self.get_json(
|
||||||
'/audit_templates/%s' % self.audit_template.name),
|
urlparse.quote('/audit_templates/%s' % self.audit_template.uuid),
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(404, response.status_int)
|
self.assertEqual(404, response.status_int)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
self.context.show_deleted = True
|
self.assertRaises(exception.AuditTemplateNotFound,
|
||||||
audit_template = objects.AuditTemplate.get_by_name(
|
objects.AuditTemplate.get_by_uuid,
|
||||||
self.context, self.audit_template.name)
|
self.context,
|
||||||
|
self.audit_template.uuid)
|
||||||
|
|
||||||
return_deleted_at = timeutils.strtime(audit_template['deleted_at'])
|
self.context.show_deleted = True
|
||||||
self.assertEqual(timeutils.strtime(test_time), return_deleted_at)
|
at = objects.AuditTemplate.get_by_uuid(self.context,
|
||||||
|
self.audit_template.uuid)
|
||||||
|
self.assertEqual(self.audit_template.name, at.name)
|
||||||
|
|
||||||
|
@mock.patch.object(timeutils, 'utcnow')
|
||||||
|
def test_delete_audit_template_by_name(self, mock_utcnow):
|
||||||
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
|
mock_utcnow.return_value = test_time
|
||||||
|
self.delete(urlparse.quote('/audit_templates/%s' %
|
||||||
|
self.audit_template.name))
|
||||||
|
response = self.get_json(
|
||||||
|
urlparse.quote('/audit_templates/%s' % self.audit_template.name),
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(404, response.status_int)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
self.assertRaises(exception.AuditTemplateNotFound,
|
||||||
|
objects.AuditTemplate.get_by_name,
|
||||||
|
self.context,
|
||||||
|
self.audit_template.name)
|
||||||
|
|
||||||
|
self.context.show_deleted = True
|
||||||
|
at = objects.AuditTemplate.get_by_name(self.context,
|
||||||
|
self.audit_template.name)
|
||||||
|
self.assertEqual(self.audit_template.uuid, at.uuid)
|
||||||
|
|
||||||
def test_delete_audit_template_not_found(self):
|
def test_delete_audit_template_not_found(self):
|
||||||
uuid = utils.generate_uuid()
|
uuid = utils.generate_uuid()
|
||||||
|
|||||||
@@ -432,17 +432,18 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
mock_utcnow.return_value = test_time
|
mock_utcnow.return_value = test_time
|
||||||
|
|
||||||
audit_dict = post_get_test_audit()
|
audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
|
||||||
|
del audit_dict['uuid']
|
||||||
|
del audit_dict['state']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(201, response.status_int)
|
self.assertEqual(201, response.status_int)
|
||||||
# Check location header
|
# Check location header
|
||||||
self.assertIsNotNone(response.location)
|
self.assertIsNotNone(response.location)
|
||||||
expected_location = '/v1/audits/%s' % audit_dict['uuid']
|
expected_location = '/v1/audits/%s' % response.json['uuid']
|
||||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||||
expected_location)
|
expected_location)
|
||||||
self.assertEqual(audit_dict['uuid'], response.json['uuid'])
|
|
||||||
self.assertEqual(objects.audit.State.PENDING,
|
self.assertEqual(objects.audit.State.PENDING,
|
||||||
response.json['state'])
|
response.json['state'])
|
||||||
self.assertNotIn('updated_at', response.json.keys)
|
self.assertNotIn('updated_at', response.json.keys)
|
||||||
@@ -451,12 +452,29 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
response.json['created_at']).replace(tzinfo=None)
|
response.json['created_at']).replace(tzinfo=None)
|
||||||
self.assertEqual(test_time, return_created_at)
|
self.assertEqual(test_time, return_created_at)
|
||||||
|
|
||||||
|
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||||
|
@mock.patch('oslo_utils.timeutils.utcnow')
|
||||||
|
def test_create_audit_with_state_not_allowed(self, mock_utcnow,
|
||||||
|
mock_trigger_audit):
|
||||||
|
mock_trigger_audit.return_value = mock.ANY
|
||||||
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
|
mock_utcnow.return_value = test_time
|
||||||
|
|
||||||
|
audit_dict = post_get_test_audit(state=objects.audit.State.SUCCEEDED)
|
||||||
|
|
||||||
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
@mock.patch('oslo_utils.timeutils.utcnow')
|
||||||
def test_create_audit_invalid_audit_template_uuid(self, mock_utcnow):
|
def test_create_audit_invalid_audit_template_uuid(self, mock_utcnow):
|
||||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
mock_utcnow.return_value = test_time
|
mock_utcnow.return_value = test_time
|
||||||
|
|
||||||
audit_dict = post_get_test_audit()
|
audit_dict = post_get_test_audit()
|
||||||
|
del audit_dict['uuid']
|
||||||
|
del audit_dict['state']
|
||||||
# Make the audit template UUID some garbage value
|
# Make the audit template UUID some garbage value
|
||||||
audit_dict['audit_template_uuid'] = (
|
audit_dict['audit_template_uuid'] = (
|
||||||
'01234567-8910-1112-1314-151617181920')
|
'01234567-8910-1112-1314-151617181920')
|
||||||
@@ -473,11 +491,14 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
def test_create_audit_doesnt_contain_id(self, mock_trigger_audit):
|
def test_create_audit_doesnt_contain_id(self, mock_trigger_audit):
|
||||||
mock_trigger_audit.return_value = mock.ANY
|
mock_trigger_audit.return_value = mock.ANY
|
||||||
|
|
||||||
audit_dict = post_get_test_audit(state='ONGOING')
|
audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
|
||||||
|
state = audit_dict['state']
|
||||||
|
del audit_dict['uuid']
|
||||||
|
del audit_dict['state']
|
||||||
with mock.patch.object(self.dbapi, 'create_audit',
|
with mock.patch.object(self.dbapi, 'create_audit',
|
||||||
wraps=self.dbapi.create_audit) as cn_mock:
|
wraps=self.dbapi.create_audit) as cn_mock:
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual(audit_dict['state'], response.json['state'])
|
self.assertEqual(state, response.json['state'])
|
||||||
cn_mock.assert_called_once_with(mock.ANY)
|
cn_mock.assert_called_once_with(mock.ANY)
|
||||||
# Check that 'id' is not in first arg of positional args
|
# Check that 'id' is not in first arg of positional args
|
||||||
self.assertNotIn('id', cn_mock.call_args[0][0])
|
self.assertNotIn('id', cn_mock.call_args[0][0])
|
||||||
@@ -488,6 +509,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict = post_get_test_audit()
|
audit_dict = post_get_test_audit()
|
||||||
del audit_dict['uuid']
|
del audit_dict['uuid']
|
||||||
|
del audit_dict['state']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@@ -499,9 +521,21 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
def test_create_audit_trigger_decision_engine(self):
|
def test_create_audit_trigger_decision_engine(self):
|
||||||
with mock.patch.object(deapi.DecisionEngineAPI,
|
with mock.patch.object(deapi.DecisionEngineAPI,
|
||||||
'trigger_audit') as de_mock:
|
'trigger_audit') as de_mock:
|
||||||
audit_dict = post_get_test_audit(state='ONGOING')
|
audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
|
||||||
self.post_json('/audits', audit_dict)
|
del audit_dict['uuid']
|
||||||
de_mock.assert_called_once_with(mock.ANY, audit_dict['uuid'])
|
del audit_dict['state']
|
||||||
|
response = self.post_json('/audits', audit_dict)
|
||||||
|
de_mock.assert_called_once_with(mock.ANY, response.json['uuid'])
|
||||||
|
|
||||||
|
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||||
|
def test_create_audit_with_uuid(self, mock_trigger_audit):
|
||||||
|
mock_trigger_audit.return_value = mock.ANY
|
||||||
|
|
||||||
|
audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
|
||||||
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
assert not mock_trigger_audit.called
|
||||||
|
|
||||||
|
|
||||||
# class TestDelete(api_base.FunctionalTest):
|
# class TestDelete(api_base.FunctionalTest):
|
||||||
|
|||||||
@@ -11,60 +11,109 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from watcher.tests.api import base as api_base
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
CONF = cfg.CONF
|
from watcher.common import utils
|
||||||
|
from watcher.tests.api import base as api_base
|
||||||
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
class TestListGoal(api_base.FunctionalTest):
|
class TestListGoal(api_base.FunctionalTest):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestListGoal, self).setUp()
|
|
||||||
# Override the default to get enough goals to test limit on query
|
|
||||||
cfg.CONF.set_override(
|
|
||||||
"goals", {
|
|
||||||
"DUMMY_1": "dummy", "DUMMY_2": "dummy",
|
|
||||||
"DUMMY_3": "dummy", "DUMMY_4": "dummy",
|
|
||||||
},
|
|
||||||
group='watcher_goals', enforce_type=True)
|
|
||||||
|
|
||||||
def _assert_goal_fields(self, goal):
|
def _assert_goal_fields(self, goal):
|
||||||
goal_fields = ['name', 'strategy']
|
goal_fields = ['uuid', 'name', 'display_name']
|
||||||
for field in goal_fields:
|
for field in goal_fields:
|
||||||
self.assertIn(field, goal)
|
self.assertIn(field, goal)
|
||||||
|
|
||||||
def test_one(self):
|
def test_one(self):
|
||||||
|
goal = obj_utils.create_test_goal(self.context)
|
||||||
response = self.get_json('/goals')
|
response = self.get_json('/goals')
|
||||||
|
self.assertEqual(goal.uuid, response['goals'][0]["uuid"])
|
||||||
self._assert_goal_fields(response['goals'][0])
|
self._assert_goal_fields(response['goals'][0])
|
||||||
|
|
||||||
def test_get_one(self):
|
def test_get_one_by_uuid(self):
|
||||||
goal_name = list(CONF.watcher_goals.goals.keys())[0]
|
goal = obj_utils.create_test_goal(self.context)
|
||||||
response = self.get_json('/goals/%s' % goal_name)
|
response = self.get_json('/goals/%s' % goal.uuid)
|
||||||
self.assertEqual(goal_name, response['name'])
|
self.assertEqual(goal.uuid, response["uuid"])
|
||||||
|
self.assertEqual(goal.name, response["name"])
|
||||||
self._assert_goal_fields(response)
|
self._assert_goal_fields(response)
|
||||||
|
|
||||||
|
def test_get_one_by_name(self):
|
||||||
|
goal = obj_utils.create_test_goal(self.context)
|
||||||
|
response = self.get_json(urlparse.quote(
|
||||||
|
'/goals/%s' % goal['name']))
|
||||||
|
self.assertEqual(goal.uuid, response['uuid'])
|
||||||
|
self._assert_goal_fields(response)
|
||||||
|
|
||||||
|
def test_get_one_soft_deleted(self):
|
||||||
|
goal = obj_utils.create_test_goal(self.context)
|
||||||
|
goal.soft_delete()
|
||||||
|
response = self.get_json(
|
||||||
|
'/goals/%s' % goal['uuid'],
|
||||||
|
headers={'X-Show-Deleted': 'True'})
|
||||||
|
self.assertEqual(goal.uuid, response['uuid'])
|
||||||
|
self._assert_goal_fields(response)
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/goals/%s' % goal['uuid'],
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(404, response.status_int)
|
||||||
|
|
||||||
def test_detail(self):
|
def test_detail(self):
|
||||||
goal_name = list(CONF.watcher_goals.goals.keys())[0]
|
goal = obj_utils.create_test_goal(self.context)
|
||||||
response = self.get_json('/goals/detail')
|
response = self.get_json('/goals/detail')
|
||||||
self.assertEqual(goal_name, response['goals'][0]["name"])
|
self.assertEqual(goal.uuid, response['goals'][0]["uuid"])
|
||||||
self._assert_goal_fields(response['goals'][0])
|
self._assert_goal_fields(response['goals'][0])
|
||||||
|
|
||||||
def test_detail_against_single(self):
|
def test_detail_against_single(self):
|
||||||
goal_name = list(CONF.watcher_goals.goals.keys())[0]
|
goal = obj_utils.create_test_goal(self.context)
|
||||||
response = self.get_json('/goals/%s/detail' % goal_name,
|
response = self.get_json('/goals/%s/detail' % goal.uuid,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(404, response.status_int)
|
self.assertEqual(404, response.status_int)
|
||||||
|
|
||||||
def test_many(self):
|
def test_many(self):
|
||||||
|
goal_list = []
|
||||||
|
for idx in range(1, 6):
|
||||||
|
goal = obj_utils.create_test_goal(
|
||||||
|
self.context, id=idx,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='GOAL_{0}'.format(idx))
|
||||||
|
goal_list.append(goal.uuid)
|
||||||
response = self.get_json('/goals')
|
response = self.get_json('/goals')
|
||||||
self.assertEqual(len(CONF.watcher_goals.goals),
|
self.assertTrue(len(response['goals']) > 2)
|
||||||
len(response['goals']))
|
|
||||||
|
def test_many_without_soft_deleted(self):
|
||||||
|
goal_list = []
|
||||||
|
for id_ in [1, 2, 3]:
|
||||||
|
goal = obj_utils.create_test_goal(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
name='GOAL_{0}'.format(id_))
|
||||||
|
goal_list.append(goal.uuid)
|
||||||
|
for id_ in [4, 5]:
|
||||||
|
goal = obj_utils.create_test_goal(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
name='GOAL_{0}'.format(id_))
|
||||||
|
goal.soft_delete()
|
||||||
|
response = self.get_json('/goals')
|
||||||
|
self.assertEqual(3, len(response['goals']))
|
||||||
|
uuids = [s['uuid'] for s in response['goals']]
|
||||||
|
self.assertEqual(sorted(goal_list), sorted(uuids))
|
||||||
|
|
||||||
def test_goals_collection_links(self):
|
def test_goals_collection_links(self):
|
||||||
|
for idx in range(1, 6):
|
||||||
|
obj_utils.create_test_goal(
|
||||||
|
self.context, id=idx,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='GOAL_{0}'.format(idx))
|
||||||
response = self.get_json('/goals/?limit=2')
|
response = self.get_json('/goals/?limit=2')
|
||||||
self.assertEqual(2, len(response['goals']))
|
self.assertEqual(2, len(response['goals']))
|
||||||
|
|
||||||
def test_goals_collection_links_default_limit(self):
|
def test_goals_collection_links_default_limit(self):
|
||||||
|
for idx in range(1, 6):
|
||||||
|
obj_utils.create_test_goal(
|
||||||
|
self.context, id=idx,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='GOAL_{0}'.format(idx))
|
||||||
cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
|
cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
|
||||||
response = self.get_json('/goals')
|
response = self.get_json('/goals')
|
||||||
self.assertEqual(3, len(response['goals']))
|
self.assertEqual(3, len(response['goals']))
|
||||||
|
|||||||
189
watcher/tests/api/v1/test_strategies.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.tests.api import base as api_base
|
||||||
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestListStrategy(api_base.FunctionalTest):
|
||||||
|
|
||||||
|
def _assert_strategy_fields(self, strategy):
|
||||||
|
strategy_fields = ['uuid', 'name', 'display_name', 'goal_uuid']
|
||||||
|
for field in strategy_fields:
|
||||||
|
self.assertIn(field, strategy)
|
||||||
|
|
||||||
|
def test_one(self):
|
||||||
|
strategy = obj_utils.create_test_strategy(self.context)
|
||||||
|
response = self.get_json('/strategies')
|
||||||
|
self.assertEqual(strategy.uuid, response['strategies'][0]["uuid"])
|
||||||
|
self._assert_strategy_fields(response['strategies'][0])
|
||||||
|
|
||||||
|
def test_get_one_by_uuid(self):
|
||||||
|
strategy = obj_utils.create_test_strategy(self.context)
|
||||||
|
response = self.get_json('/strategies/%s' % strategy.uuid)
|
||||||
|
self.assertEqual(strategy.uuid, response["uuid"])
|
||||||
|
self.assertEqual(strategy.name, response["name"])
|
||||||
|
self._assert_strategy_fields(response)
|
||||||
|
|
||||||
|
def test_get_one_by_name(self):
|
||||||
|
strategy = obj_utils.create_test_strategy(self.context)
|
||||||
|
response = self.get_json(urlparse.quote(
|
||||||
|
'/strategies/%s' % strategy['name']))
|
||||||
|
self.assertEqual(strategy.uuid, response['uuid'])
|
||||||
|
self._assert_strategy_fields(response)
|
||||||
|
|
||||||
|
def test_get_one_soft_deleted(self):
|
||||||
|
strategy = obj_utils.create_test_strategy(self.context)
|
||||||
|
strategy.soft_delete()
|
||||||
|
response = self.get_json(
|
||||||
|
'/strategies/%s' % strategy['uuid'],
|
||||||
|
headers={'X-Show-Deleted': 'True'})
|
||||||
|
self.assertEqual(strategy.uuid, response['uuid'])
|
||||||
|
self._assert_strategy_fields(response)
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/strategies/%s' % strategy['uuid'],
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(404, response.status_int)
|
||||||
|
|
||||||
|
def test_detail(self):
|
||||||
|
obj_utils.create_test_goal(self.context)
|
||||||
|
strategy = obj_utils.create_test_strategy(self.context)
|
||||||
|
response = self.get_json('/strategies/detail')
|
||||||
|
self.assertEqual(strategy.uuid, response['strategies'][0]["uuid"])
|
||||||
|
self._assert_strategy_fields(response['strategies'][0])
|
||||||
|
for strategy in response['strategies']:
|
||||||
|
self.assertTrue(
|
||||||
|
all(val is not None for key, val in strategy.items()
|
||||||
|
if key in ['uuid', 'name', 'display_name', 'goal_uuid']))
|
||||||
|
|
||||||
|
def test_detail_against_single(self):
|
||||||
|
strategy = obj_utils.create_test_strategy(self.context)
|
||||||
|
response = self.get_json('/strategies/%s/detail' % strategy.uuid,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(404, response.status_int)
|
||||||
|
|
||||||
|
def test_many(self):
|
||||||
|
obj_utils.create_test_goal(self.context)
|
||||||
|
strategy_list = []
|
||||||
|
for idx in range(1, 6):
|
||||||
|
strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, id=idx,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='STRATEGY_{0}'.format(idx))
|
||||||
|
strategy_list.append(strategy.uuid)
|
||||||
|
response = self.get_json('/strategies')
|
||||||
|
self.assertEqual(5, len(response['strategies']))
|
||||||
|
for strategy in response['strategies']:
|
||||||
|
self.assertTrue(
|
||||||
|
all(val is not None for key, val in strategy.items()
|
||||||
|
if key in ['uuid', 'name', 'display_name', 'goal_uuid']))
|
||||||
|
|
||||||
|
def test_many_without_soft_deleted(self):
|
||||||
|
strategy_list = []
|
||||||
|
for id_ in [1, 2, 3]:
|
||||||
|
strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
name='STRATEGY_{0}'.format(id_))
|
||||||
|
strategy_list.append(strategy.uuid)
|
||||||
|
for id_ in [4, 5]:
|
||||||
|
strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
name='STRATEGY_{0}'.format(id_))
|
||||||
|
strategy.soft_delete()
|
||||||
|
response = self.get_json('/strategies')
|
||||||
|
self.assertEqual(3, len(response['strategies']))
|
||||||
|
uuids = [s['uuid'] for s in response['strategies']]
|
||||||
|
self.assertEqual(sorted(strategy_list), sorted(uuids))
|
||||||
|
|
||||||
|
def test_strategies_collection_links(self):
|
||||||
|
for idx in range(1, 6):
|
||||||
|
obj_utils.create_test_strategy(
|
||||||
|
self.context, id=idx,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='STRATEGY_{0}'.format(idx))
|
||||||
|
response = self.get_json('/strategies/?limit=2')
|
||||||
|
self.assertEqual(2, len(response['strategies']))
|
||||||
|
|
||||||
|
def test_strategies_collection_links_default_limit(self):
|
||||||
|
for idx in range(1, 6):
|
||||||
|
obj_utils.create_test_strategy(
|
||||||
|
self.context, id=idx,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='STRATEGY_{0}'.format(idx))
|
||||||
|
cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
|
||||||
|
response = self.get_json('/strategies')
|
||||||
|
self.assertEqual(3, len(response['strategies']))
|
||||||
|
|
||||||
|
def test_filter_by_goal_uuid(self):
|
||||||
|
goal1 = obj_utils.create_test_goal(
|
||||||
|
self.context,
|
||||||
|
id=1,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='My_Goal 1')
|
||||||
|
goal2 = obj_utils.create_test_goal(
|
||||||
|
self.context,
|
||||||
|
id=2,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='My Goal 2')
|
||||||
|
|
||||||
|
for id_ in range(1, 3):
|
||||||
|
obj_utils.create_test_strategy(
|
||||||
|
self.context, id=id_,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
goal_id=goal1['id'])
|
||||||
|
for id_ in range(3, 5):
|
||||||
|
obj_utils.create_test_strategy(
|
||||||
|
self.context, id=id_,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
goal_id=goal2['id'])
|
||||||
|
|
||||||
|
response = self.get_json('/strategies/?goal=%s' % goal1['uuid'])
|
||||||
|
|
||||||
|
strategies = response['strategies']
|
||||||
|
self.assertEqual(2, len(strategies))
|
||||||
|
for strategy in strategies:
|
||||||
|
self.assertEqual(goal1['uuid'], strategy['goal_uuid'])
|
||||||
|
|
||||||
|
def test_filter_by_goal_name(self):
|
||||||
|
goal1 = obj_utils.create_test_goal(
|
||||||
|
self.context,
|
||||||
|
id=1,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='My_Goal 1')
|
||||||
|
goal2 = obj_utils.create_test_goal(
|
||||||
|
self.context,
|
||||||
|
id=2,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
name='My Goal 2')
|
||||||
|
|
||||||
|
for id_ in range(1, 3):
|
||||||
|
obj_utils.create_test_strategy(
|
||||||
|
self.context, id=id_,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
goal_id=goal1['id'])
|
||||||
|
for id_ in range(3, 5):
|
||||||
|
obj_utils.create_test_strategy(
|
||||||
|
self.context, id=id_,
|
||||||
|
uuid=utils.generate_uuid(),
|
||||||
|
goal_id=goal2['id'])
|
||||||
|
|
||||||
|
response = self.get_json('/strategies/?goal=%s' % goal1['name'])
|
||||||
|
|
||||||
|
strategies = response['strategies']
|
||||||
|
self.assertEqual(2, len(strategies))
|
||||||
|
for strategy in strategies:
|
||||||
|
self.assertEqual(goal1['uuid'], strategy['goal_uuid'])
|
||||||
@@ -54,7 +54,8 @@ class TestChangeNovaServiceState(base.TestCase):
|
|||||||
baction.BaseAction.RESOURCE_ID: "compute-1",
|
baction.BaseAction.RESOURCE_ID: "compute-1",
|
||||||
"state": hstate.HypervisorState.ENABLED.value,
|
"state": hstate.HypervisorState.ENABLED.value,
|
||||||
}
|
}
|
||||||
self.action = change_nova_service_state.ChangeNovaServiceState()
|
self.action = change_nova_service_state.ChangeNovaServiceState(
|
||||||
|
mock.Mock())
|
||||||
self.action.input_parameters = self.input_parameters
|
self.action.input_parameters = self.input_parameters
|
||||||
|
|
||||||
def test_parameters_down(self):
|
def test_parameters_down(self):
|
||||||
|
|||||||
@@ -58,9 +58,18 @@ class TestMigration(base.TestCase):
|
|||||||
"dst_hypervisor": "hypervisor2-hostname",
|
"dst_hypervisor": "hypervisor2-hostname",
|
||||||
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
|
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
|
||||||
}
|
}
|
||||||
self.action = migration.Migrate()
|
self.action = migration.Migrate(mock.Mock())
|
||||||
self.action.input_parameters = self.input_parameters
|
self.action.input_parameters = self.input_parameters
|
||||||
|
|
||||||
|
self.input_parameters_cold = {
|
||||||
|
"migration_type": "cold",
|
||||||
|
"src_hypervisor": "hypervisor1-hostname",
|
||||||
|
"dst_hypervisor": "hypervisor2-hostname",
|
||||||
|
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
|
||||||
|
}
|
||||||
|
self.action_cold = migration.Migrate(mock.Mock())
|
||||||
|
self.action_cold.input_parameters = self.input_parameters_cold
|
||||||
|
|
||||||
def test_parameters(self):
|
def test_parameters(self):
|
||||||
params = {baction.BaseAction.RESOURCE_ID:
|
params = {baction.BaseAction.RESOURCE_ID:
|
||||||
self.INSTANCE_UUID,
|
self.INSTANCE_UUID,
|
||||||
@@ -70,6 +79,15 @@ class TestMigration(base.TestCase):
|
|||||||
self.action.input_parameters = params
|
self.action.input_parameters = params
|
||||||
self.assertEqual(True, self.action.validate_parameters())
|
self.assertEqual(True, self.action.validate_parameters())
|
||||||
|
|
||||||
|
def test_parameters_cold(self):
|
||||||
|
params = {baction.BaseAction.RESOURCE_ID:
|
||||||
|
self.INSTANCE_UUID,
|
||||||
|
self.action.MIGRATION_TYPE: 'cold',
|
||||||
|
self.action.DST_HYPERVISOR: 'compute-2',
|
||||||
|
self.action.SRC_HYPERVISOR: 'compute-3'}
|
||||||
|
self.action_cold.input_parameters = params
|
||||||
|
self.assertEqual(True, self.action_cold.validate_parameters())
|
||||||
|
|
||||||
def test_parameters_exception_empty_fields(self):
|
def test_parameters_exception_empty_fields(self):
|
||||||
parameters = {baction.BaseAction.RESOURCE_ID: None,
|
parameters = {baction.BaseAction.RESOURCE_ID: None,
|
||||||
'migration_type': None,
|
'migration_type': None,
|
||||||
@@ -87,7 +105,7 @@ class TestMigration(base.TestCase):
|
|||||||
def test_parameters_exception_migration_type(self):
|
def test_parameters_exception_migration_type(self):
|
||||||
parameters = {baction.BaseAction.RESOURCE_ID:
|
parameters = {baction.BaseAction.RESOURCE_ID:
|
||||||
self.INSTANCE_UUID,
|
self.INSTANCE_UUID,
|
||||||
'migration_type': 'cold',
|
'migration_type': 'unknown',
|
||||||
'src_hypervisor': 'compute-2',
|
'src_hypervisor': 'compute-2',
|
||||||
'dst_hypervisor': 'compute-3'}
|
'dst_hypervisor': 'compute-3'}
|
||||||
self.action.input_parameters = parameters
|
self.action.input_parameters = parameters
|
||||||
@@ -154,6 +172,13 @@ class TestMigration(base.TestCase):
|
|||||||
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
|
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
|
||||||
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
|
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
|
||||||
|
|
||||||
|
def test_execute_cold_migration_invalid_instance(self):
|
||||||
|
self.m_helper.find_instance.return_value = None
|
||||||
|
exc = self.assertRaises(
|
||||||
|
exception.InstanceNotFound, self.action_cold.execute)
|
||||||
|
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
|
||||||
|
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
|
||||||
|
|
||||||
def test_execute_live_migration(self):
|
def test_execute_live_migration(self):
|
||||||
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
@@ -166,6 +191,20 @@ class TestMigration(base.TestCase):
|
|||||||
instance_id=self.INSTANCE_UUID,
|
instance_id=self.INSTANCE_UUID,
|
||||||
dest_hostname="hypervisor2-hostname")
|
dest_hostname="hypervisor2-hostname")
|
||||||
|
|
||||||
|
def test_execute_cold_migration(self):
|
||||||
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.action_cold.execute()
|
||||||
|
except Exception as exc:
|
||||||
|
self.fail(exc)
|
||||||
|
|
||||||
|
self.m_helper.watcher_non_live_migrate_instance.\
|
||||||
|
assert_called_once_with(
|
||||||
|
instance_id=self.INSTANCE_UUID,
|
||||||
|
dest_hostname="hypervisor2-hostname"
|
||||||
|
)
|
||||||
|
|
||||||
def test_revert_live_migration(self):
|
def test_revert_live_migration(self):
|
||||||
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
@@ -177,6 +216,18 @@ class TestMigration(base.TestCase):
|
|||||||
dest_hostname="hypervisor1-hostname"
|
dest_hostname="hypervisor1-hostname"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_revert_cold_migration(self):
|
||||||
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
|
self.action_cold.revert()
|
||||||
|
|
||||||
|
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
|
||||||
|
self.m_helper.watcher_non_live_migrate_instance.\
|
||||||
|
assert_called_once_with(
|
||||||
|
instance_id=self.INSTANCE_UUID,
|
||||||
|
dest_hostname="hypervisor1-hostname"
|
||||||
|
)
|
||||||
|
|
||||||
def test_live_migrate_non_shared_storage_instance(self):
|
def test_live_migrate_non_shared_storage_instance(self):
|
||||||
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import mock
|
||||||
import voluptuous
|
import voluptuous
|
||||||
|
|
||||||
from watcher.applier.actions import sleep
|
from watcher.applier.actions import sleep
|
||||||
@@ -23,7 +25,7 @@ from watcher.tests import base
|
|||||||
class TestSleep(base.TestCase):
|
class TestSleep(base.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestSleep, self).setUp()
|
super(TestSleep, self).setUp()
|
||||||
self.s = sleep.Sleep()
|
self.s = sleep.Sleep(mock.Mock())
|
||||||
|
|
||||||
def test_parameters_duration(self):
|
def test_parameters_duration(self):
|
||||||
self.s.input_parameters = {self.s.DURATION: 1.0}
|
self.s.input_parameters = {self.s.DURATION: 1.0}
|
||||||
|
|||||||
@@ -18,22 +18,22 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from watcher.applier.manager import ApplierManager
|
from watcher.applier import manager as applier_manager
|
||||||
from watcher.common.messaging.messaging_core import MessagingCore
|
from watcher.common.messaging import messaging_handler
|
||||||
|
from watcher.common import service
|
||||||
from watcher.tests import base
|
from watcher.tests import base
|
||||||
|
|
||||||
|
|
||||||
class TestApplierManager(base.TestCase):
|
class TestApplierManager(base.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestApplierManager, self).setUp()
|
super(TestApplierManager, self).setUp()
|
||||||
self.applier = ApplierManager()
|
self.applier = service.Service(applier_manager.ApplierManager)
|
||||||
|
|
||||||
@patch.object(MessagingCore, "connect")
|
@patch.object(messaging_handler.MessagingHandler, "stop")
|
||||||
@patch.object(Thread, "join")
|
@patch.object(messaging_handler.MessagingHandler, "start")
|
||||||
def test_connect(self, m_messaging, m_thread):
|
def test_start(self, m_messaging_start, m_messaging_stop):
|
||||||
self.applier.connect()
|
self.applier.start()
|
||||||
self.applier.join()
|
self.applier.stop()
|
||||||
self.assertEqual(2, m_messaging.call_count)
|
self.assertEqual(2, m_messaging_start.call_count)
|
||||||
self.assertEqual(1, m_thread.call_count)
|
self.assertEqual(2, m_messaging_stop.call_count)
|
||||||
|
|||||||