Compare commits
204 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 | ||
|
|
46d5094add | ||
|
|
783c7c0177 | ||
|
|
6d0717199c | ||
|
|
8b77e78f3d | ||
|
|
22c9c4df87 | ||
|
|
99ff6d3348 | ||
|
|
c67f83cce0 | ||
|
|
397bb3497e | ||
|
|
4c924fc505 | ||
|
|
4c5ecc808d | ||
|
|
64b5a7c3e4 | ||
|
|
40bb92f749 | ||
|
|
92bd06cf94 | ||
|
|
c9e0dfd3f5 | ||
|
|
446fe1307a | ||
|
|
2836f460e3 | ||
|
|
cb9bb7301b | ||
|
|
cb644fcef9 | ||
|
|
0a7c87eebf | ||
|
|
d7f4f42772 | ||
|
|
bdc0eb196a | ||
|
|
59427eb0d9 | ||
|
|
b6801b192a | ||
|
|
0a6c2c16a4 | ||
|
|
9a44941c66 | ||
|
|
a6508a0013 | ||
|
|
c3db66ca09 | ||
|
|
5d0fe553c4 | ||
|
|
8b8239c3d8 | ||
|
|
920bd502ec | ||
|
|
c68d33f341 | ||
|
|
8e8fdbd809 | ||
|
|
681536c8c7 | ||
|
|
083b170083 | ||
|
|
de7b0129a1 | ||
|
|
323fd01a85 | ||
|
|
c0133e6585 | ||
|
|
63fffeacd8 | ||
|
|
bc791f0e75 | ||
|
|
6ed417e6a7 | ||
|
|
4afefa3dfb | ||
|
|
9fadfbe40a | ||
|
|
f278874a93 | ||
|
|
1c5b247300 | ||
|
|
547bf5f87e | ||
|
|
96c0ac0ca8 | ||
|
|
a80fd2a51e | ||
|
|
58ea85c852 | ||
|
|
78f122f241 | ||
|
|
43eb997edb | ||
|
|
c440cdd69f | ||
|
|
b5bccba169 | ||
|
|
e058437ae0 | ||
|
|
1acacaa812 | ||
|
|
5bb1b6cbf0 | ||
|
|
de058d7ed1 | ||
|
|
98a65efb16 | ||
|
|
338539ec53 | ||
|
|
02f0f8e70a | ||
|
|
7f1bd20a09 | ||
|
|
d3d2a5ef8c | ||
|
|
3a6ae820c0 | ||
|
|
5a8860419e | ||
|
|
4aa1c7558b | ||
|
|
a8dab52376 | ||
|
|
5615d0523d | ||
|
|
10823ce133 | ||
|
|
18c098c4c1 | ||
|
|
db649d86b6 | ||
|
|
6e380b685b | ||
|
|
8dfff0e8e6 | ||
|
|
fbc7da755a | ||
|
|
b947c30910 | ||
|
|
9af96114af | ||
|
|
1ddf69a68f | ||
|
|
379ac791a8 | ||
|
|
81ea37de41 | ||
|
|
1c963fdc96 | ||
|
|
f32995228b | ||
|
|
0ec3d68994 | ||
|
|
3503e11506 | ||
|
|
c7f0ef37d0 | ||
|
|
d93b1ffe9f | ||
|
|
4e71a0c655 | ||
|
|
37dd713ed5 | ||
|
|
55aeb783e3 | ||
|
|
fe3f6e73be | ||
|
|
5baff7dc3e | ||
|
|
e3198d25a5 | ||
|
|
33ee575936 | ||
|
|
259f2562e6 | ||
|
|
1629247413 | ||
|
|
58d84aca6d | ||
|
|
236879490d | ||
|
|
79850cc89c | ||
|
|
b958214db8 | ||
|
|
a0b5f5aa1d | ||
|
|
a4a009a2c6 | ||
|
|
0ba8a35ade | ||
|
|
8bcc1b2097 | ||
|
|
b440f5c69a | ||
|
|
ad40c61ea9 | ||
|
|
858bbbf126 | ||
|
|
1d74f7e3bc | ||
|
|
b7641a9311 | ||
|
|
376d669af6 | ||
|
|
25d27f0288 | ||
|
|
3f4686ce79 | ||
|
|
86c1a9d77f | ||
|
|
9a6811ae6b | ||
|
|
e520f5f452 | ||
|
|
6a25bd983c | ||
|
|
c175ef2170 | ||
|
|
28733a5f30 | ||
|
|
7f8fec1bca | ||
|
|
278b1819d6 | ||
|
|
978bb11d4a | ||
|
|
3027b28942 | ||
|
|
2f0c1c12cf | ||
|
|
e122c61840 | ||
|
|
8f6eac819f | ||
|
|
de307e536e | ||
|
|
7406a1e713 |
@@ -4,6 +4,7 @@ source = watcher
|
||||
omit = watcher/tests/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
ignore_errors = True
|
||||
exclude_lines =
|
||||
@abstract
|
||||
@abc.abstract
|
||||
raise NotImplementedError
|
||||
|
||||
2
.gitignore
vendored
@@ -44,6 +44,8 @@ output/*/index.html
|
||||
# Sphinx
|
||||
doc/build
|
||||
doc/source/api
|
||||
doc/source/samples
|
||||
doc/source/watcher.conf.sample
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
|
||||
@@ -10,12 +10,12 @@ Watcher
|
||||
|
||||
OpenStack Watcher provides a flexible and scalable resource optimization
|
||||
service for multi-tenant OpenStack-based clouds.
|
||||
Watcher provides a complete optimization loop—including everything from a
|
||||
Watcher provides a complete optimization loop-including everything from a
|
||||
metrics receiver, complex event processor and profiler, optimization processor
|
||||
and an action plan applier. This provides a robust framework to realize a wide
|
||||
range of cloud optimization goals, including the reduction of data center
|
||||
operating costs, increased system performance via intelligent virtual machine
|
||||
migration, increased energy efficiency—and more!
|
||||
migration, increased energy efficiency-and more!
|
||||
|
||||
* Free software: Apache license
|
||||
* Wiki: http://wiki.openstack.org/wiki/Watcher
|
||||
|
||||
@@ -80,10 +80,7 @@ function cleanup_watcher {
|
||||
# configure_watcher() - Set config files, create data dirs, etc
|
||||
function configure_watcher {
|
||||
# Put config files in ``/etc/watcher`` for everyone to find
|
||||
if [[ ! -d $WATCHER_CONF_DIR ]]; then
|
||||
sudo mkdir -p $WATCHER_CONF_DIR
|
||||
sudo chown $STACK_USER $WATCHER_CONF_DIR
|
||||
fi
|
||||
sudo install -d -o $STACK_USER $WATCHER_CONF_DIR
|
||||
|
||||
install_default_policy watcher
|
||||
|
||||
@@ -99,15 +96,13 @@ function configure_watcher {
|
||||
function create_watcher_accounts {
|
||||
create_service_user "watcher" "admin"
|
||||
|
||||
if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then
|
||||
local watcher_service=$(get_or_create_service "watcher" \
|
||||
"infra-optim" "Watcher Infrastructure Optimization Service")
|
||||
get_or_create_endpoint $watcher_service \
|
||||
"$REGION_NAME" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT"
|
||||
fi
|
||||
local watcher_service=$(get_or_create_service "watcher" \
|
||||
"infra-optim" "Watcher Infrastructure Optimization Service")
|
||||
get_or_create_endpoint $watcher_service \
|
||||
"$REGION_NAME" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT"
|
||||
}
|
||||
|
||||
# create_watcher_conf() - Create a new watcher.conf file
|
||||
@@ -128,14 +123,8 @@ function create_watcher_conf {
|
||||
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD
|
||||
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_host $RABBIT_HOST
|
||||
|
||||
iniset $WATCHER_CONF keystone_authtoken admin_user watcher
|
||||
iniset $WATCHER_CONF keystone_authtoken admin_password $SERVICE_PASSWORD
|
||||
iniset $WATCHER_CONF keystone_authtoken admin_tenant_name $SERVICE_TENANT_NAME
|
||||
|
||||
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR
|
||||
|
||||
iniset $WATCHER_CONF keystone_authtoken auth_uri $KEYSTONE_SERVICE_URI/v3
|
||||
iniset $WATCHER_CONF keystone_authtoken auth_version v3
|
||||
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR "watcher_clients_auth"
|
||||
|
||||
if is_fedora || is_suse; then
|
||||
# watcher defaults to /usr/local/bin, but fedora and suse pip like to
|
||||
@@ -178,9 +167,8 @@ function create_watcher_conf {
|
||||
# create_watcher_cache_dir() - Part of the init_watcher() process
|
||||
function create_watcher_cache_dir {
|
||||
# Create cache dir
|
||||
sudo mkdir -p $WATCHER_AUTH_CACHE_DIR
|
||||
sudo chown $STACK_USER $WATCHER_AUTH_CACHE_DIR
|
||||
rm -f $WATCHER_AUTH_CACHE_DIR/*
|
||||
sudo install -d -o $STACK_USER $WATCHER_AUTH_CACHE_DIR
|
||||
rm -rf $WATCHER_AUTH_CACHE_DIR/*
|
||||
}
|
||||
|
||||
# init_watcher() - Initialize databases, etc.
|
||||
|
||||
@@ -42,7 +42,3 @@ LOGDAYS=2
|
||||
[[post-config|$NOVA_CONF]]
|
||||
[DEFAULT]
|
||||
compute_monitors=cpu.virt_driver
|
||||
|
||||
[[post-config|$WATCHER_CONF]]
|
||||
[watcher_goals]
|
||||
goals=BASIC_CONSOLIDATION:basic,DUMMY:dummy
|
||||
|
||||
@@ -127,7 +127,19 @@ Watcher CLI
|
||||
The watcher command-line interface (CLI) can be used to interact with the
|
||||
Watcher system in order to control it or to know its current status.
|
||||
|
||||
Please, read `the detailed documentation about Watcher CLI <https://factory.b-com.com/www/watcher/doc/python-watcherclient/>`_
|
||||
Please, read `the detailed documentation about Watcher CLI
|
||||
<https://factory.b-com.com/www/watcher/doc/python-watcherclient/>`_.
|
||||
|
||||
.. _archi_watcher_dashboard_definition:
|
||||
|
||||
Watcher Dashboard
|
||||
-----------------
|
||||
|
||||
The Watcher Dashboard can be used to interact with the Watcher system through
|
||||
Horizon in order to control it or to know its current status.
|
||||
|
||||
Please, read `the detailed documentation about Watcher Dashboard
|
||||
<https://factory.b-com.com/www/watcher/doc/watcher-dashboard/>`_.
|
||||
|
||||
.. _archi_watcher_database_definition:
|
||||
|
||||
@@ -143,6 +155,7 @@ by the :ref:`Watcher API <archi_watcher_api_definition>` or the
|
||||
- :ref:`Action plans <action_plan_definition>`
|
||||
- :ref:`Actions <action_definition>`
|
||||
- :ref:`Goals <goal_definition>`
|
||||
- :ref:`Strategies <strategy_definition>`
|
||||
|
||||
The Watcher domain being here "*optimization of some resources provided by an
|
||||
OpenStack system*".
|
||||
@@ -184,8 +197,6 @@ Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
|
||||
which provides information about the past of the
|
||||
:ref:`Cluster <cluster_definition>`
|
||||
|
||||
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
|
||||
given :ref:`Goal <goal_definition>` via the main Watcher configuration file.
|
||||
|
||||
.. _data_model:
|
||||
|
||||
@@ -199,6 +210,14 @@ view (Goals, Audits, Action Plans, ...):
|
||||
.. image:: ./images/functional_data_model.svg
|
||||
: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
|
||||
@@ -218,13 +237,15 @@ following parameters:
|
||||
|
||||
- A name
|
||||
- A goal to achieve
|
||||
- An optional strategy
|
||||
|
||||
.. image:: ./images/sequence_create_audit_template.png
|
||||
:width: 100%
|
||||
|
||||
The `Watcher API`_ just makes sure that the goal exists (i.e. it is declared
|
||||
in the Watcher configuration file) and stores a new audit template in the
|
||||
:ref:`Watcher Database <watcher_database_definition>`.
|
||||
The `Watcher API`_ makes sure that both the specified goal (mandatory) and
|
||||
its associated strategy (optional) are registered inside the :ref:`Watcher
|
||||
Database <watcher_database_definition>` before storing a new audit template in
|
||||
the :ref:`Watcher Database <watcher_database_definition>`.
|
||||
|
||||
.. _sequence_diagrams_create_and_launch_audit:
|
||||
|
||||
@@ -248,12 +269,11 @@ the Audit in the
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads
|
||||
the Audit parameters from the
|
||||
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the
|
||||
appropriate :ref:`Strategy <strategy_definition>` (using entry points)
|
||||
associated to the :ref:`Goal <goal_definition>` of the
|
||||
:ref:`Audit <audit_definition>` (it uses the information of the Watcher
|
||||
configuration file to find the mapping between the
|
||||
:ref:`Goal <goal_definition>` and the :ref:`Strategy <strategy_definition>`
|
||||
python class).
|
||||
appropriate :ref:`strategy <strategy_definition>` (using entry points)
|
||||
given both the :ref:`goal <goal_definition>` and the strategy associated to the
|
||||
parent :ref:`audit template <audit_template_definition>` of the :ref:`Audit
|
||||
<audit_definition>`. If no strategy is associated to the audit template, the
|
||||
strategy is dynamically selected by the Decision Engine.
|
||||
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
|
||||
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This
|
||||
@@ -349,6 +369,28 @@ State Machine diagrams
|
||||
Audit State Machine
|
||||
-------------------
|
||||
|
||||
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
|
||||
be one of the following:
|
||||
|
||||
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
|
||||
submitted (either manually by the
|
||||
:ref:`Administrator <administrator_definition>` or automatically via some
|
||||
event handling mechanism) and is in the queue for being processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
|
||||
processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
|
||||
successfully and at least one solution was found
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Audit <audit_definition>`
|
||||
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
|
||||
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||
any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
|
||||
The following diagram shows the different possible states of an
|
||||
:ref:`Audit <audit_definition>` and what event makes the state change to a new
|
||||
value:
|
||||
@@ -361,6 +403,31 @@ value:
|
||||
Action Plan State Machine
|
||||
-------------------------
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
|
||||
state may be one of the following:
|
||||
|
||||
- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
|
||||
for a validation from the :ref:`Administrator <administrator_definition>`
|
||||
- **PENDING** : a request for an :ref:`Action Plan <action_plan_definition>`
|
||||
has been submitted (due to an
|
||||
:ref:`Administrator <administrator_definition>` executing an
|
||||
:ref:`Audit <audit_definition>`) and is in the queue for
|
||||
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
|
||||
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
|
||||
executed successfully (i.e. all :ref:`Actions <action_definition>` that it
|
||||
contains have been executed successfully)
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
|
||||
stored in the :ref:`Watcher database <watcher_database_definition>` but is
|
||||
not returned any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
|
||||
**PENDING** or **ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
|
||||
|
||||
The following diagram shows the different possible states of an
|
||||
:ref:`Action Plan <action_plan_definition>` and what event makes the state
|
||||
change to a new value:
|
||||
|
||||
@@ -18,6 +18,7 @@ from watcher import version as watcher_version
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'oslo_config.sphinxconfiggen',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinxcontrib.httpdomain',
|
||||
@@ -28,7 +29,8 @@ extensions = [
|
||||
]
|
||||
|
||||
wsme_protocols = ['restjson']
|
||||
|
||||
config_generator_config_file = '../../etc/watcher/watcher-config-generator.conf'
|
||||
sample_config_basename = 'watcher'
|
||||
|
||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||
# text edit cycles.
|
||||
|
||||
1
doc/source/config-generator.conf
Normal file
@@ -0,0 +1 @@
|
||||
../../etc/watcher/watcher-config-generator.conf
|
||||
14
doc/source/deploy/conf-files.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
.. _watcher_sample_configuration_files:
|
||||
|
||||
==================================
|
||||
Watcher sample configuration files
|
||||
==================================
|
||||
|
||||
watcher.conf
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The ``watcher.conf`` file contains most of the options to configure the
|
||||
Watcher services.
|
||||
|
||||
.. literalinclude:: ../watcher.conf.sample
|
||||
:language: ini
|
||||
@@ -34,6 +34,8 @@ The Watcher service includes the following components:
|
||||
- ``watcher-applier``: applies the action plan.
|
||||
- `python-watcherclient`_: A command-line interface (CLI) for interacting with
|
||||
the Watcher service.
|
||||
- `watcher-dashboard`_: An Horizon plugin for interacting with the Watcher
|
||||
service.
|
||||
|
||||
Additionally, the Bare Metal service has certain external dependencies, which
|
||||
are very similar to other OpenStack services:
|
||||
@@ -52,6 +54,7 @@ additional functionality:
|
||||
.. _`ceilometer`: https://github.com/openstack/ceilometer
|
||||
.. _`nova`: https://github.com/openstack/nova
|
||||
.. _`python-watcherclient`: https://github.com/openstack/python-watcherclient
|
||||
.. _`watcher-dashboard`: https://github.com/openstack/watcher-dashboard
|
||||
.. _`watcher metering`: https://github.com/b-com/watcher-metering
|
||||
.. _`RabbitMQ`: https://www.rabbitmq.com/
|
||||
|
||||
@@ -160,17 +163,31 @@ Configure the Watcher service
|
||||
The Watcher service is configured via its configuration file. This file
|
||||
is typically located at ``/etc/watcher/watcher.conf``.
|
||||
|
||||
You can easily generate and update a sample configuration file
|
||||
named :ref:`watcher.conf.sample <watcher_sample_configuration_files>` by using
|
||||
these following commands::
|
||||
|
||||
$ git clone git://git.openstack.org/openstack/watcher
|
||||
$ cd watcher/
|
||||
$ tox -econfig
|
||||
$ vi etc/watcher/watcher.conf.sample
|
||||
|
||||
|
||||
The configuration file is organized into the following sections:
|
||||
|
||||
* ``[DEFAULT]`` - General configuration
|
||||
* ``[api]`` - API server configuration
|
||||
* ``[database]`` - SQL driver configuration
|
||||
* ``[keystone_authtoken]`` - Keystone Authentication plugin configuration
|
||||
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients
|
||||
* ``[watcher_applier]`` - Watcher Applier 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
|
||||
* ``[ceilometer_client]`` - Ceilometer client configuration
|
||||
* ``[cinder_client]`` - Cinder client configuration
|
||||
* ``[glance_client]`` - Glance client configuration
|
||||
* ``[nova_client]`` - Nova client configuration
|
||||
* ``[neutron_client]`` - Neutron client configuration
|
||||
|
||||
The Watcher configuration file is expected to be named
|
||||
``watcher.conf``. When starting Watcher, you can specify a different
|
||||
@@ -237,39 +254,105 @@ so that the watcher service is configured for your needs.
|
||||
#rabbit_port = 5672
|
||||
|
||||
|
||||
#. Configure the Watcher Service to use these credentials with the Identity
|
||||
Service. Replace IDENTITY_IP with the IP of the Identity server, and
|
||||
replace WATCHER_PASSWORD with the password you chose for the ``watcher``
|
||||
user in the Identity Service::
|
||||
#. Watcher API shall validate the token provided by every incoming request,
|
||||
via keystonemiddleware, which requires the Watcher service to be configured
|
||||
with the right credentials for the Identity service.
|
||||
|
||||
[keystone_authtoken]
|
||||
In the configuration section here below:
|
||||
|
||||
# Complete public Identity API endpoint (string value)
|
||||
#auth_uri=<None>
|
||||
auth_uri=http://IDENTITY_IP:5000/v3
|
||||
* replace IDENTITY_IP with the IP of the Identity server
|
||||
* replace WATCHER_PASSWORD with the password you chose for the ``watcher``
|
||||
user
|
||||
* replace KEYSTONE_SERVICE_PROJECT_NAME with the name of project created
|
||||
for OpenStack services (e.g. ``service``) ::
|
||||
|
||||
# Complete admin Identity API endpoint. This should specify the
|
||||
# unversioned root endpoint e.g. https://localhost:35357/ (string
|
||||
# value)
|
||||
#identity_uri = <None>
|
||||
identity_uri = http://IDENTITY_IP:5000
|
||||
[keystone_authtoken]
|
||||
|
||||
# Keystone account username (string value)
|
||||
#admin_user=<None>
|
||||
admin_user=watcher
|
||||
# Authentication type to load (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/auth_plugin
|
||||
#auth_type = <None>
|
||||
auth_type = password
|
||||
|
||||
# Keystone account password (string value)
|
||||
#admin_password=<None>
|
||||
admin_password=WATCHER_DBPASSWORD
|
||||
# Authentication URL (unknown value)
|
||||
#auth_url = <None>
|
||||
auth_url = http://IDENTITY_IP:35357
|
||||
|
||||
# Keystone service account tenant name to validate user tokens
|
||||
# (string value)
|
||||
#admin_tenant_name=admin
|
||||
admin_tenant_name=KEYSTONE_SERVICE_PROJECT_NAME
|
||||
# Username (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/username
|
||||
#username = <None>
|
||||
username=watcher
|
||||
|
||||
# Directory used to cache files related to PKI tokens (string
|
||||
# value)
|
||||
#signing_dir=<None>
|
||||
# User's password (unknown value)
|
||||
#password = <None>
|
||||
password = WATCHER_PASSWORD
|
||||
|
||||
# Domain ID containing project (unknown value)
|
||||
#project_domain_id = <None>
|
||||
project_domain_id = default
|
||||
|
||||
# User's domain id (unknown value)
|
||||
#user_domain_id = <None>
|
||||
user_domain_id = default
|
||||
|
||||
# Project name to scope to (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/tenant-name
|
||||
#project_name = <None>
|
||||
project_name = KEYSTONE_SERVICE_PROJECT_NAME
|
||||
|
||||
#. Watcher's decision engine and applier interact with other OpenStack
|
||||
projects through those projects' clients. In order to instantiate these
|
||||
clients, Watcher needs to request a new session from the Identity service
|
||||
using the right credentials.
|
||||
|
||||
In the configuration section here below:
|
||||
|
||||
* replace IDENTITY_IP with the IP of the Identity server
|
||||
* replace WATCHER_PASSWORD with the password you chose for the ``watcher``
|
||||
user
|
||||
* replace KEYSTONE_SERVICE_PROJECT_NAME with the name of project created
|
||||
for OpenStack services (e.g. ``service``) ::
|
||||
|
||||
[watcher_clients_auth]
|
||||
|
||||
# Authentication type to load (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/auth_plugin
|
||||
#auth_type = <None>
|
||||
auth_type = password
|
||||
|
||||
# Authentication URL (unknown value)
|
||||
#auth_url = <None>
|
||||
auth_url = http://IDENTITY_IP:35357
|
||||
|
||||
# Username (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/username
|
||||
#username = <None>
|
||||
username=watcher
|
||||
|
||||
# User's password (unknown value)
|
||||
#password = <None>
|
||||
password = WATCHER_PASSWORD
|
||||
|
||||
# Domain ID containing project (unknown value)
|
||||
#project_domain_id = <None>
|
||||
project_domain_id = default
|
||||
|
||||
# User's domain id (unknown value)
|
||||
#user_domain_id = <None>
|
||||
user_domain_id = default
|
||||
|
||||
# Project name to scope to (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/tenant-name
|
||||
#project_name = <None>
|
||||
project_name = KEYSTONE_SERVICE_PROJECT_NAME
|
||||
|
||||
#. Configure the clients to use a specific version if desired. For example, to
|
||||
configure Watcher to use a Nova client with version 2.1, use::
|
||||
|
||||
[nova_client]
|
||||
|
||||
# Version of Nova API to use in novaclient. (string value)
|
||||
#api_version = 2
|
||||
api_version = 2.1
|
||||
|
||||
#. Create the Watcher Service database tables::
|
||||
|
||||
|
||||
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,13 +32,17 @@ This guide assumes you have a working installation of Watcher. If you get
|
||||
Please refer to the `installation guide`_.
|
||||
In order to use Watcher, you have to configure your credentials suitable for
|
||||
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
|
||||
plugin installation guide`_.
|
||||
|
||||
.. _`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
|
||||
.. _`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 ?
|
||||
------------------------------------
|
||||
@@ -47,23 +51,66 @@ watcher binary without options.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ watcher
|
||||
$ watcher help
|
||||
|
||||
or::
|
||||
|
||||
$ openstack help optimize
|
||||
|
||||
How do I run an audit of my cluster ?
|
||||
-------------------------------------
|
||||
|
||||
First, you need to 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).
|
||||
This goal should be declared in the Watcher service configuration file
|
||||
**/etc/watcher/watcher.conf**.
|
||||
First, you need to find the :ref:`goal <goal_definition>` you want to achieve:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ watcher audit-template-create my_first_audit SERVERS_CONSOLIDATION
|
||||
$ watcher goal list
|
||||
|
||||
If you get "*You must provide a username via either --os-username or via
|
||||
env[OS_USERNAME]*" you may have to verify your credentials.
|
||||
or::
|
||||
|
||||
$ 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
|
||||
cluster depending on the specified :ref:`goal <goal_definition>`.
|
||||
@@ -72,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
|
||||
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
|
||||
|
||||
$ watcher audit-template-list
|
||||
$ watcher audittemplate list
|
||||
|
||||
or::
|
||||
|
||||
$ openstack optimize audittemplate list
|
||||
|
||||
- Start an audit based on this :ref:`audit template
|
||||
<audit_template_definition>` settings:
|
||||
|
||||
.. 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>`
|
||||
composed of a list of potential optimization :ref:`actions <action_definition>`
|
||||
@@ -98,15 +152,22 @@ configuration file.
|
||||
|
||||
.. 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>`
|
||||
contained in this new :ref:`action plan <action_plan_definition>`:
|
||||
|
||||
.. 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
|
||||
<action_plan_definition>`, it's time to go further by applying it to your
|
||||
@@ -116,18 +177,30 @@ cluster:
|
||||
|
||||
.. 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
|
||||
periodically calling:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ watcher action-list
|
||||
$ watcher action list
|
||||
|
||||
or::
|
||||
|
||||
$ openstack optimize action list
|
||||
|
||||
You can also obtain more detailed information about a specific action:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ watcher action-show <the_action_uuid>
|
||||
$ watcher action show <the_action_uuid>
|
||||
|
||||
or::
|
||||
|
||||
$ openstack optimize action show <the_action_uuid>
|
||||
|
||||
|
||||
@@ -60,3 +60,12 @@ Code Hosting
|
||||
Code Review
|
||||
https://review.openstack.org/#/q/status:open+project:openstack/watcher,n,z
|
||||
|
||||
IRC Channel
|
||||
``#openstack-watcher`` (changelog_)
|
||||
|
||||
Weekly Meetings
|
||||
on Wednesdays at 14:00 UTC in the ``#openstack-meeting-4`` IRC
|
||||
channel (`meetings logs`_)
|
||||
|
||||
.. _changelog: http://eavesdrop.openstack.org/irclogs/%23openstack-watcher/
|
||||
.. _meetings logs: http://eavesdrop.openstack.org/meetings/watcher/
|
||||
|
||||
@@ -9,35 +9,97 @@ Set up a development environment via DevStack
|
||||
=============================================
|
||||
|
||||
Watcher is currently able to optimize compute resources - specifically Nova
|
||||
compute hosts - via operations such as live migrations. In order for you to
|
||||
compute hosts - via operations such as live migrations. In order for you to
|
||||
fully be able to exercise what Watcher can do, it is necessary to have a
|
||||
multinode environment to use. If you have no experience with DevStack, you
|
||||
should check out the `DevStack documentation`_ and be comfortable with the
|
||||
basics of DevStack before attempting to get a multinode DevStack setup with
|
||||
the Watcher plugin.
|
||||
multinode environment to use.
|
||||
|
||||
You can set up the Watcher services quickly and easily using a Watcher
|
||||
DevStack plugin. See `PluginModelDocs`_ for information on DevStack's plugin
|
||||
model.
|
||||
|
||||
.. _DevStack documentation: http://docs.openstack.org/developer/devstack/
|
||||
.. _PluginModelDocs: http://docs.openstack.org/developer/devstack/plugins.html
|
||||
|
||||
It is recommended that you build off of the provided example local.conf files
|
||||
(`local.conf.controller`_, `local.conf.compute`_). You'll likely want to
|
||||
configure something to obtain metrics, such as Ceilometer. Ceilometer is used
|
||||
in the example local.conf files.
|
||||
|
||||
To configure the Watcher services with DevStack, add the following to the
|
||||
DevStack plugin. See `PluginModelDocs`_ for information on DevStack's plugin
|
||||
model. To enable the Watcher plugin with DevStack, add the following to the
|
||||
`[[local|localrc]]` section of your controller's `local.conf` to enable the
|
||||
Watcher plugin::
|
||||
|
||||
enable_plugin watcher git://git.openstack.org/openstack/watcher
|
||||
|
||||
Then run devstack normally::
|
||||
For more detailed instructions, see `Detailed DevStack Instructions`_. Check
|
||||
out the `DevStack documentation`_ for more information regarding DevStack.
|
||||
|
||||
cd /opt/stack/devstack
|
||||
./stack.sh
|
||||
.. _PluginModelDocs: http://docs.openstack.org/developer/devstack/plugins.html
|
||||
.. _DevStack documentation: http://docs.openstack.org/developer/devstack/
|
||||
|
||||
Detailed DevStack Instructions
|
||||
==============================
|
||||
|
||||
#. Obtain N (where N >= 1) servers (virtual machines preferred for DevStack).
|
||||
One of these servers will be the controller node while the others will be
|
||||
compute nodes. N is preferably >= 3 so that you have at least 2 compute
|
||||
nodes, but in order to stand up the Watcher services only 1 server is
|
||||
needed (i.e., no computes are needed if you want to just experiment with
|
||||
the Watcher services). These servers can be VMs running on your local
|
||||
machine via VirtualBox if you prefer. DevStack currently recommends that
|
||||
you use Ubuntu 14.04 LTS. The servers should also have connections to the
|
||||
same network such that they are all able to communicate with one another.
|
||||
|
||||
#. For each server, clone the DevStack repository and create the stack user::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install git
|
||||
git clone https://git.openstack.org/openstack-dev/devstack
|
||||
sudo ./devstack/tools/create-stack-user.sh
|
||||
|
||||
Now you have a stack user that is used to run the DevStack processes. You
|
||||
may want to give your stack user a password to allow SSH via a password::
|
||||
|
||||
sudo passwd stack
|
||||
|
||||
#. Switch to the stack user and clone the DevStack repo again::
|
||||
|
||||
sudo su stack
|
||||
cd ~
|
||||
git clone https://git.openstack.org/openstack-dev/devstack
|
||||
|
||||
#. For each compute node, copy the provided `local.conf.compute`_ example file
|
||||
to the compute node's system at ~/devstack/local.conf. Make sure the
|
||||
HOST_IP and SERVICE_HOST values are changed appropriately - i.e., HOST_IP
|
||||
is set to the IP address of the compute node and SERVICE_HOST is set to the
|
||||
IP address of the controller node.
|
||||
|
||||
If you need specific metrics collected (or want to use something other
|
||||
than Ceilometer), be sure to configure it. For example, in the
|
||||
`local.conf.compute`_ example file, the appropriate ceilometer plugins and
|
||||
services are enabled and disabled. If you were using something other than
|
||||
Ceilometer, then you would likely want to configure it likewise. The
|
||||
example file also sets the compute monitors nova configuration option to
|
||||
use the CPU virt driver. If you needed other metrics, it may be necessary
|
||||
to configure similar configuration options for the projects providing those
|
||||
metrics.
|
||||
|
||||
#. For the controller node, copy the provided `local.conf.controller`_ example
|
||||
file to the controller node's system at ~/devstack/local.conf. Make sure
|
||||
the HOST_IP value is changed appropriately - i.e., HOST_IP is set to the IP
|
||||
address of the controller node.
|
||||
|
||||
Note: if you want to use another Watcher git repository (such as a local
|
||||
one), then change the enable plugin line::
|
||||
|
||||
enable_plugin watcher <your_local_git_repo> [optional_branch]
|
||||
|
||||
If you do this, then the Watcher DevStack plugin will try to pull the
|
||||
python-watcherclient repo from <your_local_git_repo>/../, so either make
|
||||
sure that is also available or specify WATCHERCLIENT_REPO in the local.conf
|
||||
file.
|
||||
|
||||
Note: if you want to use a specific branch, specify WATCHER_BRANCH in the
|
||||
local.conf file. By default it will use the master branch.
|
||||
|
||||
#. Start stacking from the controller node::
|
||||
|
||||
./devstack/stack.sh
|
||||
|
||||
#. Start stacking on each of the compute nodes using the same command.
|
||||
|
||||
#. Configure the environment for live migration via NFS. See the
|
||||
`Multi-Node DevStack Environment`_ section for more details.
|
||||
|
||||
.. _local.conf.controller: https://github.com/openstack/watcher/tree/master/devstack/local.conf.controller
|
||||
.. _local.conf.compute: https://github.com/openstack/watcher/tree/master/devstack/local.conf.compute
|
||||
@@ -104,7 +166,6 @@ Restart the libvirt service::
|
||||
|
||||
sudo service libvirt-bin restart
|
||||
|
||||
|
||||
Setting up SSH keys between compute nodes to enable live migration
|
||||
------------------------------------------------------------------
|
||||
|
||||
@@ -113,8 +174,8 @@ each compute node:
|
||||
|
||||
1. The SOURCE root user's public RSA key (likely in /root/.ssh/id_rsa.pub)
|
||||
needs to be in the DESTINATION stack user's authorized_keys file
|
||||
(~stack/.ssh/authorized_keys). This can be accomplished by manually
|
||||
copying the contents from the file on the SOURCE to the DESTINATION. If
|
||||
(~stack/.ssh/authorized_keys). This can be accomplished by manually
|
||||
copying the contents from the file on the SOURCE to the DESTINATION. If
|
||||
you have a password configured for the stack user, then you can use the
|
||||
following command to accomplish the same thing::
|
||||
|
||||
@@ -122,7 +183,7 @@ each compute node:
|
||||
|
||||
2. The DESTINATION host's public ECDSA key (/etc/ssh/ssh_host_ecdsa_key.pub)
|
||||
needs to be in the SOURCE root user's known_hosts file
|
||||
(/root/.ssh/known_hosts). This can be accomplished by running the
|
||||
(/root/.ssh/known_hosts). This can be accomplished by running the
|
||||
following on the SOURCE machine (hostname must be used)::
|
||||
|
||||
ssh-keyscan -H DEST_HOSTNAME | sudo tee -a /root/.ssh/known_hosts
|
||||
@@ -131,3 +192,13 @@ In essence, this means that every compute node's root user's public RSA key
|
||||
must exist in every other compute node's stack user's authorized_keys file and
|
||||
every compute node's public ECDSA key needs to be in every other compute
|
||||
node's root user's known_hosts file.
|
||||
|
||||
|
||||
Environment final checkup
|
||||
-------------------------
|
||||
|
||||
If you are willing to make sure everything is in order in your DevStack
|
||||
environment, you can run the Watcher Tempest tests which will validate its API
|
||||
but also that you can perform the typical Watcher workflows. To do so, have a
|
||||
look at the :ref:`Tempest tests <tempest_tests>` section which will explain to
|
||||
you how to run them.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
.. _watcher_developement_environment:
|
||||
|
||||
=========================================
|
||||
Set up a development environment manually
|
||||
=========================================
|
||||
@@ -143,34 +145,13 @@ You should then be able to `import watcher` using Python without issue:
|
||||
|
||||
If you can import watcher without a traceback, you should be ready to develop.
|
||||
|
||||
Run Watcher unit tests
|
||||
======================
|
||||
Run Watcher tests
|
||||
=================
|
||||
|
||||
All unit tests should be run using tox. To run the unit tests under py27 and
|
||||
also run the pep8 tests:
|
||||
Watcher provides both :ref:`unit tests <unit_tests>` and
|
||||
:ref:`functional/tempest tests <tempest_tests>`. Please refer to :doc:`testing`
|
||||
to understand how to run them.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ workon watcher
|
||||
(watcher) $ pip install tox
|
||||
|
||||
(watcher) $ cd watcher
|
||||
(watcher) $ tox -epep8 -epy27
|
||||
|
||||
You may pass options to the test programs using positional arguments. To run a
|
||||
specific unit test, this passes the -r option and desired test (regex string)
|
||||
to os-testr:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ workon watcher
|
||||
(watcher) $ tox -epy27 -- tests.api
|
||||
|
||||
When you're done, deactivate the virtualenv:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ deactivate
|
||||
|
||||
Build the Watcher documentation
|
||||
===============================
|
||||
@@ -224,7 +205,7 @@ place:
|
||||
|
||||
$ workon watcher
|
||||
|
||||
(watcher) $ watcher-db-manage --create_schema
|
||||
(watcher) $ watcher-db-manage create_schema
|
||||
|
||||
|
||||
Running Watcher services
|
||||
@@ -269,6 +250,11 @@ interface.
|
||||
|
||||
.. _`python-watcherclient`: https://github.com/openstack/python-watcherclient
|
||||
|
||||
There is also an Horizon plugin for Watcher `watcher-dashboard`_ which
|
||||
allows to interact with Watcher through a web-based interface.
|
||||
|
||||
.. _`watcher-dashboard`: https://github.com/openstack/watcher-dashboard
|
||||
|
||||
|
||||
Exercising the Watcher Services locally
|
||||
=======================================
|
||||
|
||||
214
doc/source/dev/plugin/action-plugin.rst
Normal file
@@ -0,0 +1,214 @@
|
||||
..
|
||||
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/
|
||||
|
||||
==================
|
||||
Build a new action
|
||||
==================
|
||||
|
||||
Watcher Applier has an external :ref:`action <action_definition>` plugin
|
||||
interface which gives anyone the ability to integrate an external
|
||||
:ref:`action <action_definition>` in order to extend the initial set of actions
|
||||
Watcher provides.
|
||||
|
||||
This section gives some guidelines on how to implement and integrate custom
|
||||
actions with Watcher.
|
||||
|
||||
|
||||
Creating a new plugin
|
||||
=====================
|
||||
|
||||
First of all you have to extend the base :py:class:`BaseAction` class which
|
||||
defines a set of abstract methods and/or properties that you will have to
|
||||
implement:
|
||||
|
||||
- The :py:attr:`~.BaseAction.schema` is an abstract property that you have to
|
||||
implement. This is the first function to be called by the
|
||||
:ref:`applier <watcher_applier_definition>` before any further processing
|
||||
and its role is to validate the input parameters that were provided to it.
|
||||
- The :py:meth:`~.BaseAction.precondition` is called before the execution of
|
||||
an action. This method is a hook that can be used to perform some
|
||||
initializations or to make some more advanced validation on its input
|
||||
parameters. If you wish to block the execution based on this factor, you
|
||||
simply have to ``raise`` an exception.
|
||||
- The :py:meth:`~.BaseAction.postcondition` is called after the execution of
|
||||
an action. As this function is called regardless of whether an action
|
||||
succeeded or not, this can prove itself useful to perform cleanup
|
||||
operations.
|
||||
- The :py:meth:`~.BaseAction.execute` is the main component of an action.
|
||||
This is where you should implement the logic of your action.
|
||||
- The :py:meth:`~.BaseAction.revert` allows you to roll back the targeted
|
||||
resource to its original state following a faulty execution. Indeed, this
|
||||
method is called by the workflow engine whenever an action raises an
|
||||
exception.
|
||||
|
||||
Here is an example showing how you can write a plugin called ``DummyAction``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Filepath = <PROJECT_DIR>/thirdparty/dummy.py
|
||||
# Import path = thirdparty.dummy
|
||||
import voluptuous
|
||||
|
||||
from watcher.applier.actions import base
|
||||
|
||||
|
||||
class DummyAction(base.BaseAction):
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema({})
|
||||
|
||||
def execute(self):
|
||||
# Does nothing
|
||||
pass # Only returning False is considered as a failure
|
||||
|
||||
def revert(self):
|
||||
# Does nothing
|
||||
pass
|
||||
|
||||
def precondition(self):
|
||||
# No pre-checks are done here
|
||||
pass
|
||||
|
||||
def postcondition(self):
|
||||
# Nothing done here
|
||||
pass
|
||||
|
||||
|
||||
This implementation is the most basic one. So if you want to have more advanced
|
||||
examples, have a look at the implementation of the actions already provided
|
||||
by Watcher like.
|
||||
To get a better understanding on how to implement a more advanced action,
|
||||
have a look at the :py:class:`~watcher.applier.actions.migration.Migrate`
|
||||
class.
|
||||
|
||||
Input validation
|
||||
----------------
|
||||
|
||||
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
|
||||
work with `Voluptuous`_, you can have a look at their `documentation`_:
|
||||
|
||||
.. _Voluptuous: https://github.com/alecthomas/voluptuous
|
||||
.. _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
|
||||
=====================
|
||||
|
||||
Here below is the abstract ``BaseAction`` class that every single action
|
||||
should implement:
|
||||
|
||||
.. autoclass:: watcher.applier.actions.base.BaseAction
|
||||
:members:
|
||||
:special-members: __init__
|
||||
:noindex:
|
||||
|
||||
.. py:attribute:: schema
|
||||
|
||||
Defines a Schema that the input parameters shall comply to
|
||||
|
||||
:returns: A schema declaring the input parameters this action should be
|
||||
provided along with their respective constraints
|
||||
(e.g. type, value range, ...)
|
||||
:rtype: :py:class:`voluptuous.Schema` instance
|
||||
|
||||
|
||||
Register a new entry point
|
||||
==========================
|
||||
|
||||
In order for the Watcher Applier to load your new action, the
|
||||
action must be registered as a named entry point under the
|
||||
``watcher_actions`` entry point of your ``setup.py`` file. If you are using
|
||||
pbr_, this entry point should be placed in your ``setup.cfg`` file.
|
||||
|
||||
The name you give to your entry point has to be unique.
|
||||
|
||||
Here below is how you would proceed to register ``DummyAction`` using pbr_:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[entry_points]
|
||||
watcher_actions =
|
||||
dummy = thirdparty.dummy:DummyAction
|
||||
|
||||
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||
|
||||
|
||||
Using action plugins
|
||||
====================
|
||||
|
||||
The Watcher Applier service will automatically discover any installed plugins
|
||||
when it is restarted. If a Python package containing a custom plugin is
|
||||
installed within the same environment as Watcher, Watcher will automatically
|
||||
make that plugin available for use.
|
||||
|
||||
At this point, you can use your new action plugin in your :ref:`strategy plugin
|
||||
<implement_strategy_plugin>` if you reference it via the use of the
|
||||
:py:meth:`~.Solution.add_action` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# [...]
|
||||
self.solution.add_action(
|
||||
action_type="dummy", # Name of the entry point we registered earlier
|
||||
applies_to="",
|
||||
input_parameters={})
|
||||
|
||||
By doing so, your action will be saved within the Watcher Database, ready to be
|
||||
processed by the planner for creating an action plan which can then be executed
|
||||
by the Watcher Applier via its workflow engine.
|
||||
|
||||
|
||||
Scheduling of an action plugin
|
||||
==============================
|
||||
|
||||
Watcher provides a basic built-in :ref:`planner <watcher_planner_definition>`
|
||||
which is only able to process the Watcher built-in actions. Therefore, you will
|
||||
either have to use an existing third-party planner or :ref:`implement another
|
||||
planner <implement_planner_plugin>` that will be able to take into account your
|
||||
new action plugin.
|
||||
90
doc/source/dev/plugin/base-setup.rst
Normal file
@@ -0,0 +1,90 @@
|
||||
..
|
||||
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/
|
||||
|
||||
.. _plugin-base_setup:
|
||||
|
||||
=======================================
|
||||
Create a third-party plugin for Watcher
|
||||
=======================================
|
||||
|
||||
Watcher provides a plugin architecture which allows anyone to extend the
|
||||
existing functionalities by implementing third-party plugins. This process can
|
||||
be cumbersome so this documentation is there to help you get going as quickly
|
||||
as possible.
|
||||
|
||||
|
||||
Pre-requisites
|
||||
==============
|
||||
|
||||
We assume that you have set up a working Watcher development environment. So if
|
||||
this not already the case, you can check out our documentation which explains
|
||||
how to set up a :ref:`development environment
|
||||
<watcher_developement_environment>`.
|
||||
|
||||
.. _development environment:
|
||||
|
||||
Third party project scaffolding
|
||||
===============================
|
||||
|
||||
First off, we need to create the project structure. To do so, we can use
|
||||
`cookiecutter`_ and the `OpenStack cookiecutter`_ project scaffolder to
|
||||
generate the skeleton of our project::
|
||||
|
||||
$ virtualenv thirdparty
|
||||
$ source thirdparty/bin/activate
|
||||
$ pip install cookiecutter
|
||||
$ cookiecutter https://github.com/openstack-dev/cookiecutter
|
||||
|
||||
The last command will ask you for many information, and If you set
|
||||
``module_name`` and ``repo_name`` as ``thirdparty``, you should end up with a
|
||||
structure that looks like this::
|
||||
|
||||
$ cd thirdparty
|
||||
$ tree .
|
||||
.
|
||||
├── babel.cfg
|
||||
├── CONTRIBUTING.rst
|
||||
├── doc
|
||||
│ └── source
|
||||
│ ├── conf.py
|
||||
│ ├── contributing.rst
|
||||
│ ├── index.rst
|
||||
│ ├── installation.rst
|
||||
│ ├── readme.rst
|
||||
│ └── usage.rst
|
||||
├── HACKING.rst
|
||||
├── LICENSE
|
||||
├── MANIFEST.in
|
||||
├── README.rst
|
||||
├── requirements.txt
|
||||
├── setup.cfg
|
||||
├── setup.py
|
||||
├── test-requirements.txt
|
||||
├── thirdparty
|
||||
│ ├── __init__.py
|
||||
│ └── tests
|
||||
│ ├── base.py
|
||||
│ ├── __init__.py
|
||||
│ └── test_thirdparty.py
|
||||
└── tox.ini
|
||||
|
||||
.. _cookiecutter: https://github.com/audreyr/cookiecutter
|
||||
.. _OpenStack cookiecutter: https://github.com/openstack-dev/cookiecutter
|
||||
|
||||
Implementing a plugin for Watcher
|
||||
=================================
|
||||
|
||||
Now that the project skeleton has been created, you can start the
|
||||
implementation of your plugin. As of now, you can implement the following
|
||||
plugins for Watcher:
|
||||
|
||||
- A :ref:`strategy plugin <implement_strategy_plugin>`
|
||||
- A :ref:`planner plugin <implement_planner_plugin>`
|
||||
- An :ref:`action plugin <implement_strategy_plugin>`
|
||||
- A :ref:`workflow engine plugin <implement_workflow_engine_plugin>`
|
||||
|
||||
If you want to learn more on how to implement them, you can refer to their
|
||||
dedicated documentation.
|
||||
171
doc/source/dev/plugin/planner-plugin.rst
Normal file
@@ -0,0 +1,171 @@
|
||||
..
|
||||
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/
|
||||
|
||||
.. _implement_planner_plugin:
|
||||
|
||||
===================
|
||||
Build a new planner
|
||||
===================
|
||||
|
||||
Watcher :ref:`Decision Engine <watcher_decision_engine_definition>` has an
|
||||
external :ref:`planner <planner_definition>` plugin interface which gives
|
||||
anyone the ability to integrate an external :ref:`planner <planner_definition>`
|
||||
in order to extend the initial set of planners Watcher provides.
|
||||
|
||||
This section gives some guidelines on how to implement and integrate custom
|
||||
planners with Watcher.
|
||||
|
||||
.. _Decision Engine: watcher_decision_engine_definition
|
||||
|
||||
Creating a new plugin
|
||||
=====================
|
||||
|
||||
First of all you have to extend the base :py:class:`~.BasePlanner` class which
|
||||
defines an abstract method that you will have to implement. The
|
||||
:py:meth:`~.BasePlanner.schedule` is the method being called by the Decision
|
||||
Engine to schedule a given solution (:py:class:`~.BaseSolution`) into an
|
||||
:ref:`action plan <action_plan_definition>` by ordering/sequencing an unordered
|
||||
set of actions contained in the proposed solution (for more details, see
|
||||
:ref:`definition of a solution <solution_definition>`).
|
||||
|
||||
Here is an example showing how you can write a planner plugin called
|
||||
``DummyPlanner``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Filepath = third-party/third_party/dummy.py
|
||||
# Import path = third_party.dummy
|
||||
import uuid
|
||||
from watcher.decision_engine.planner import base
|
||||
|
||||
|
||||
class DummyPlanner(base.BasePlanner):
|
||||
|
||||
def _create_action_plan(self, context, audit_id):
|
||||
action_plan_dict = {
|
||||
'uuid': uuid.uuid4(),
|
||||
'audit_id': audit_id,
|
||||
'first_action_id': None,
|
||||
'state': objects.action_plan.State.RECOMMENDED
|
||||
}
|
||||
|
||||
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||
new_action_plan.create(context)
|
||||
new_action_plan.save()
|
||||
return new_action_plan
|
||||
|
||||
def schedule(self, context, audit_id, solution):
|
||||
# Empty action plan
|
||||
action_plan = self._create_action_plan(context, audit_id)
|
||||
# todo: You need to create the workflow of actions here
|
||||
# and attach it to the action plan
|
||||
return action_plan
|
||||
|
||||
This implementation is the most basic one. So if you want to have more advanced
|
||||
examples, have a look at the implementation of planners already provided by
|
||||
Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner
|
||||
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
|
||||
=====================
|
||||
|
||||
Here below is the abstract ``BasePlanner`` class that every single planner
|
||||
should implement:
|
||||
|
||||
.. autoclass:: watcher.decision_engine.planner.base.BasePlanner
|
||||
:members:
|
||||
:special-members: __init__
|
||||
:noindex:
|
||||
|
||||
|
||||
Register a new entry point
|
||||
==========================
|
||||
|
||||
In order for the Watcher Decision Engine to load your new planner, the
|
||||
latter must be registered as a new entry point under the
|
||||
``watcher_planners`` entry point namespace of your ``setup.py`` file. If you
|
||||
are using pbr_, this entry point should be placed in your ``setup.cfg`` file.
|
||||
|
||||
The name you give to your entry point has to be unique.
|
||||
|
||||
Here below is how you would proceed to register ``DummyPlanner`` using pbr_:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[entry_points]
|
||||
watcher_planners =
|
||||
dummy = third_party.dummy:DummyPlanner
|
||||
|
||||
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||
|
||||
|
||||
Using planner plugins
|
||||
=====================
|
||||
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` service
|
||||
will automatically discover any installed plugins when it is started. This
|
||||
means that if Watcher is already running when you install your plugin, you will
|
||||
have to restart the related Watcher services. If a Python package containing a
|
||||
custom plugin is installed within the same environment as Watcher, Watcher will
|
||||
automatically make that plugin available for use.
|
||||
|
||||
At this point, Watcher will use your new planner if you referenced it in the
|
||||
``planner`` option under the ``[watcher_planner]`` section of your
|
||||
``watcher.conf`` configuration file when you started it. For example, if you
|
||||
want to use the ``dummy`` planner you just installed, you would have to
|
||||
select it as followed:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[watcher_planner]
|
||||
planner = dummy
|
||||
|
||||
As you may have noticed, only a single planner implementation can be activated
|
||||
at a time, so make sure it is generic enough to support all your strategies
|
||||
and actions.
|
||||
365
doc/source/dev/plugin/strategy-plugin.rst
Normal file
@@ -0,0 +1,365 @@
|
||||
..
|
||||
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/
|
||||
|
||||
.. _implement_strategy_plugin:
|
||||
|
||||
=================================
|
||||
Build a new optimization strategy
|
||||
=================================
|
||||
|
||||
Watcher Decision Engine has an external :ref:`strategy <strategy_definition>`
|
||||
plugin interface which gives anyone the ability to integrate an external
|
||||
strategy in order to make use of placement algorithms.
|
||||
|
||||
This section gives some guidelines on how to implement and integrate custom
|
||||
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
|
||||
==============
|
||||
|
||||
Before using any strategy, you should make sure you have your Telemetry service
|
||||
configured so that it would provide you all the metrics you need to be able to
|
||||
use your strategy.
|
||||
|
||||
|
||||
Create a new plugin
|
||||
===================
|
||||
|
||||
In order to create a new strategy, you have to:
|
||||
|
||||
- Extend the :py:class:`~.UnclassifiedStrategy` class
|
||||
- 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 ``NewStrategy``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
|
||||
class NewStrategy(base.UnclassifiedStrategy):
|
||||
|
||||
def __init__(self, osc=None):
|
||||
super(NewStrategy, self).__init__(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"
|
||||
|
||||
|
||||
As you can see in the above example, the :py:meth:`~.BaseStrategy.execute`
|
||||
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
|
||||
solution is then processed by a :ref:`planner <planner_definition>` to produce
|
||||
an action plan which contains the sequenced flow of actions to be
|
||||
executed by the :ref:`Watcher Applier <watcher_applier_definition>`.
|
||||
|
||||
Please note that your strategy class will expect to find the same constructor
|
||||
signature as BaseStrategy to instantiate you strategy. Therefore, you should
|
||||
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
|
||||
=====================
|
||||
|
||||
Here below is the abstract :py:class:`~.BaseStrategy` class:
|
||||
|
||||
.. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy
|
||||
:members:
|
||||
:special-members: __init__
|
||||
:noindex:
|
||||
|
||||
.. _strategy_plugin_add_entrypoint:
|
||||
|
||||
Add a new entry point
|
||||
=====================
|
||||
|
||||
In order for the Watcher Decision Engine to load your new strategy, the
|
||||
strategy must be registered as a named entry point under the
|
||||
``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.
|
||||
|
||||
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_:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[entry_points]
|
||||
watcher_strategies =
|
||||
dummy_strategy = thirdparty.dummy:DummyStrategy
|
||||
|
||||
|
||||
To get a better understanding on how to implement a more advanced strategy,
|
||||
have a look at the :py:class:`~.BasicConsolidation` class.
|
||||
|
||||
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||
|
||||
Using strategy plugins
|
||||
======================
|
||||
|
||||
The Watcher Decision Engine service will automatically discover any installed
|
||||
plugins when it is restarted. If a Python package containing a custom plugin is
|
||||
installed within the same environment as Watcher, Watcher will automatically
|
||||
make that plugin available for use.
|
||||
|
||||
At this point, Watcher will scan and register inside the :ref:`Watcher Database
|
||||
<watcher_database_definition>` all the strategies (alongside the goals they
|
||||
should satisfy) you implemented upon restarting the :ref:`Watcher Decision
|
||||
Engine <watcher_decision_engine_definition>`.
|
||||
|
||||
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
|
||||
they may require a set of metrics which is not yet available within the
|
||||
Telemetry service. In such a case, please do make sure that you first
|
||||
check/configure the latter so your new strategy can be fully functional.
|
||||
|
||||
Querying metrics
|
||||
----------------
|
||||
|
||||
A large set of metrics, generated by OpenStack modules, can be used in your
|
||||
strategy implementation. To collect these metrics, Watcher provides a
|
||||
`Helper`_ to the Ceilometer API, which makes this API reusable and easier
|
||||
to used.
|
||||
|
||||
If you want to use your own metrics database backend, please refer to the
|
||||
`Ceilometer developer guide`_. Indeed, Ceilometer's pluggable model allows
|
||||
for various types of backends. A list of the available backends is located
|
||||
here_. The Ceilosca project is a good example of how to create your own
|
||||
pluggable backend.
|
||||
|
||||
Finally, if your strategy requires new metrics not covered by Ceilometer, you
|
||||
can add them through a Ceilometer `plugin`_.
|
||||
|
||||
.. _`Helper`: https://github.com/openstack/watcher/blob/master/watcher/metrics_engine/cluster_history/ceilometer.py#L31
|
||||
.. _`Ceilometer developer guide`: http://docs.openstack.org/developer/ceilometer/architecture.html#storing-the-data
|
||||
.. _`here`: http://docs.openstack.org/developer/ceilometer/install/dbreco.html#choosing-a-database-backend
|
||||
.. _`plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
|
||||
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
|
||||
|
||||
|
||||
Read usage metrics using the Python binding
|
||||
-------------------------------------------
|
||||
|
||||
You can find the information about the Ceilometer Python binding on the
|
||||
OpenStack `ceilometer client python API documentation
|
||||
<http://docs.openstack.org/developer/python-ceilometerclient/api.html>`_
|
||||
|
||||
To facilitate the process, Watcher provides the ``osc`` attribute to every
|
||||
strategy which includes clients to major OpenStack services, including
|
||||
Ceilometer. So to access it within your strategy, you can do the following:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
# Within your strategy "execute()"
|
||||
cclient = self.osc.ceilometer
|
||||
# TODO: Do something here
|
||||
|
||||
Using that you can now query the values for that specific metric:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
query = None # e.g. [{'field': 'foo', 'op': 'le', 'value': 34},]
|
||||
value_cpu = cclient.samples.list(
|
||||
meter_name='cpu_util',
|
||||
limit=10, q=query)
|
||||
|
||||
|
||||
Read usage metrics using the Watcher Cluster History Helper
|
||||
-----------------------------------------------------------
|
||||
|
||||
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
|
||||
|
||||
.. autoclass:: watcher.metrics_engine.cluster_history.base.BaseClusterHistory
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
|
||||
The following code snippet shows how to create a Cluster History class:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
from watcher.metrics_engine.cluster_history import ceilometer as ceil
|
||||
|
||||
query_history = ceil.CeilometerClusterHistory()
|
||||
|
||||
Using that you can now query the values for that specific metric:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
query_history.statistic_aggregation(resource_id=hypervisor.uuid,
|
||||
meter_name='compute.node.cpu.percent',
|
||||
period="7200",
|
||||
aggregate='avg'
|
||||
)
|
||||
42
doc/source/dev/plugins.rst
Normal file
@@ -0,0 +1,42 @@
|
||||
..
|
||||
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/
|
||||
|
||||
|
||||
=================
|
||||
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:
|
||||
|
||||
Strategies
|
||||
==========
|
||||
|
||||
.. drivers-doc:: watcher_strategies
|
||||
|
||||
.. _watcher_actions:
|
||||
|
||||
Actions
|
||||
=======
|
||||
|
||||
.. drivers-doc:: watcher_actions
|
||||
|
||||
.. _watcher_workflow_engines:
|
||||
|
||||
Workflow Engines
|
||||
================
|
||||
|
||||
.. drivers-doc:: watcher_workflow_engines
|
||||
|
||||
.. _watcher_planners:
|
||||
|
||||
Planners
|
||||
========
|
||||
|
||||
.. drivers-doc:: watcher_planners
|
||||
@@ -1,203 +0,0 @@
|
||||
..
|
||||
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/
|
||||
|
||||
=================================
|
||||
Build a new optimization strategy
|
||||
=================================
|
||||
|
||||
Watcher Decision Engine has an external :ref:`strategy <strategy_definition>`
|
||||
plugin interface which gives anyone the ability to integrate an external
|
||||
:ref:`strategy <strategy_definition>` in order to make use of placement
|
||||
algorithms.
|
||||
|
||||
This section gives some guidelines on how to implement and integrate custom
|
||||
Stategies with Watcher.
|
||||
|
||||
Pre-requisites
|
||||
==============
|
||||
|
||||
Before using any strategy, you should make sure you have your Telemetry service
|
||||
configured so that it would provide you all the metrics you need to be able to
|
||||
use your strategy.
|
||||
|
||||
|
||||
Creating a new plugin
|
||||
=====================
|
||||
|
||||
First of all you have to:
|
||||
|
||||
- Extend the base ``BaseStrategy`` class
|
||||
- Implement its ``execute`` method
|
||||
|
||||
Here is an example showing how you can write a plugin called ``DummyStrategy``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Filepath = third-party/third_party/dummy.py
|
||||
# Import path = third_party.dummy
|
||||
|
||||
class DummyStrategy(BaseStrategy):
|
||||
|
||||
DEFAULT_NAME = "dummy"
|
||||
DEFAULT_DESCRIPTION = "Dummy Strategy"
|
||||
|
||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
||||
super(DummyStrategy, self).__init__(name, description)
|
||||
|
||||
def execute(self, model):
|
||||
self.solution.add_change_request(
|
||||
Migrate(vm=my_vm, src_hypervisor=src, dest_hypervisor=dest)
|
||||
)
|
||||
# Do some more stuff here ...
|
||||
return self.solution
|
||||
|
||||
As you can see in the above example, the ``execute()`` method returns a
|
||||
solution as required.
|
||||
|
||||
Please note that your strategy class will be instantiated without any
|
||||
parameter. Therefore, you should make sure not to make any of them required in
|
||||
your ``__init__`` method.
|
||||
|
||||
|
||||
Abstract Plugin Class
|
||||
=====================
|
||||
|
||||
Here below is the abstract ``BaseStrategy`` class that every single strategy
|
||||
should implement:
|
||||
|
||||
.. automodule:: watcher.decision_engine.strategy.strategies.base
|
||||
:noindex:
|
||||
|
||||
.. autoclass:: BaseStrategy
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
|
||||
Add a new entry point
|
||||
=====================
|
||||
|
||||
In order for the Watcher Decision Engine to load your new strategy, the
|
||||
strategy must be registered as a named entry point under the
|
||||
``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.
|
||||
|
||||
The name you give to your entry point has to be unique.
|
||||
|
||||
Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[entry_points]
|
||||
watcher_strategies =
|
||||
dummy = third_party.dummy:DummyStrategy
|
||||
|
||||
|
||||
To get a better understanding on how to implement a more advanced strategy,
|
||||
have a look at the :py:class:`BasicConsolidation` class.
|
||||
|
||||
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||
|
||||
Using strategy plugins
|
||||
======================
|
||||
|
||||
The Watcher Decision Engine service will automatically discover any installed
|
||||
plugins when it is run. If a Python package containing a custom plugin is
|
||||
installed within the same environment as Watcher, Watcher will automatically
|
||||
make that plugin available for use.
|
||||
|
||||
At this point, the way Watcher will use your new strategy if you reference it
|
||||
in the ``goals`` under the ``[watcher_goals]`` section of your ``watcher.conf``
|
||||
configuration file. For example, if you want to use a ``dummy`` strategy you
|
||||
just installed, you would have to associate it to a goal like this:
|
||||
|
||||
.. 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,
|
||||
there are no guarantees that utilizing them as is will be supported, as
|
||||
they may require a set of metrics which is not yet available within the
|
||||
Telemetry service. In such a case, please do make sure that you first
|
||||
check/configure the latter so your new strategy can be fully functional.
|
||||
|
||||
Querying metrics
|
||||
----------------
|
||||
|
||||
A large set of metrics, generated by OpenStack modules, can be used in your
|
||||
strategy implementation. To collect these metrics, Watcher provides a
|
||||
`Helper`_ to the Ceilometer API, which makes this API reusable and easier
|
||||
to used.
|
||||
|
||||
If you want to use your own metrics database backend, please refer to the
|
||||
`Ceilometer developer guide`_. Indeed, Ceilometer's pluggable model allows
|
||||
for various types of backends. A list of the available backends is located
|
||||
here_. The Ceilosca project is a good example of how to create your own
|
||||
pluggable backend.
|
||||
|
||||
|
||||
Finally, if your strategy requires new metrics not covered by Ceilometer, you
|
||||
can add them through a Ceilometer `plugin`_.
|
||||
|
||||
|
||||
.. _`Helper`: https://github.com/openstack/watcher/blob/master/watcher/metrics_engine/cluster_history/ceilometer.py#L31
|
||||
.. _`Ceilometer developer guide`: http://docs.openstack.org/developer/ceilometer/architecture.html#storing-the-data
|
||||
.. _`here`: http://docs.openstack.org/developer/ceilometer/install/dbreco.html#choosing-a-database-backend
|
||||
.. _`plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
|
||||
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
|
||||
|
||||
Read usage metrics using the Python binding
|
||||
-------------------------------------------
|
||||
|
||||
You can find the information about the Ceilometer Python binding on the
|
||||
OpenStack `ceilometer client python API documentation
|
||||
<http://docs.openstack.org/developer/python-ceilometerclient/api.html>`_
|
||||
|
||||
The first step is to authenticate against the Ceilometer service
|
||||
(assuming that you already imported the Ceilometer client for Python)
|
||||
with this call:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
cclient = ceilometerclient.client.get_client(VERSION, os_username=USERNAME,
|
||||
os_password=PASSWORD, os_tenant_name=PROJECT_NAME, os_auth_url=AUTH_URL)
|
||||
|
||||
Using that you can now query the values for that specific metric:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
value_cpu = cclient.samples.list(meter_name='cpu_util', limit=10, q=query)
|
||||
|
||||
Read usage metrics using the Watcher Cluster History Helper
|
||||
-----------------------------------------------------------
|
||||
|
||||
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
|
||||
|
||||
.. automodule:: watcher.metrics_engine.cluster_history.api
|
||||
:noindex:
|
||||
|
||||
.. autoclass:: BaseClusterHistory
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
|
||||
The following snippet code shows how to create a Cluster History class:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
query_history = CeilometerClusterHistory()
|
||||
|
||||
Using that you can now query the values for that specific metric:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
query_history.statistic_aggregation(resource_id=hypervisor.uuid,
|
||||
meter_name='compute.node.cpu.percent',
|
||||
period="7200",
|
||||
aggregate='avg'
|
||||
)
|
||||
|
||||
50
doc/source/dev/testing.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
..
|
||||
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/
|
||||
|
||||
=======
|
||||
Testing
|
||||
=======
|
||||
|
||||
.. _unit_tests:
|
||||
|
||||
Unit tests
|
||||
==========
|
||||
|
||||
All unit tests should be run using `tox`_. To run the same unit tests that are
|
||||
executing onto `Gerrit`_ which includes ``py34``, ``py27`` and ``pep8``, you
|
||||
can issue the following command::
|
||||
|
||||
$ workon watcher
|
||||
(watcher) $ pip install tox
|
||||
(watcher) $ cd watcher
|
||||
(watcher) $ tox
|
||||
|
||||
If you want to only run one of the aforementioned, you can then issue one of
|
||||
the following::
|
||||
|
||||
$ workon watcher
|
||||
(watcher) $ tox -e py34
|
||||
(watcher) $ tox -e py27
|
||||
(watcher) $ tox -e pep8
|
||||
|
||||
.. _tox: https://tox.readthedocs.org/
|
||||
.. _Gerrit: http://review.openstack.org/
|
||||
|
||||
You may pass options to the test programs using positional arguments. To run a
|
||||
specific unit test, you can pass extra options to `os-testr`_ after putting
|
||||
the ``--`` separator. So using the ``-r`` option followed by a regex string,
|
||||
you can run the desired test::
|
||||
|
||||
$ workon watcher
|
||||
(watcher) $ tox -e py27 -- -r watcher.tests.api
|
||||
|
||||
.. _os-testr: http://docs.openstack.org/developer/os-testr/
|
||||
|
||||
When you're done, deactivate the virtualenv::
|
||||
|
||||
$ deactivate
|
||||
|
||||
.. include:: ../../../watcher_tempest_plugin/README.rst
|
||||
@@ -99,14 +99,14 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
|
||||
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
|
||||
===============
|
||||
|
||||
.. watcher-term:: watcher.metrics_engine.cluster_history.api
|
||||
.. watcher-term:: watcher.metrics_engine.cluster_history.base
|
||||
|
||||
.. _controller_node_definition:
|
||||
|
||||
@@ -213,27 +213,27 @@ Here are some examples of
|
||||
|
||||
It can be any of the `the official list of available resource types defined in OpenStack for HEAT <http://docs.openstack.org/developer/heat/template_guide/openstack.html>`_.
|
||||
|
||||
.. _efficiency_definition:
|
||||
.. _efficacy_definition:
|
||||
|
||||
Optimization Efficiency
|
||||
=======================
|
||||
Optimization Efficacy
|
||||
=====================
|
||||
|
||||
The :ref:`Optimization Efficiency <efficiency_definition>` is the objective
|
||||
The :ref:`Optimization Efficacy <efficacy_definition>` is the objective
|
||||
measure of how much of the :ref:`Goal <goal_definition>` has been achieved in
|
||||
respect with constraints and :ref:`SLAs <sla_definition>` defined by the
|
||||
:ref:`Customer <customer_definition>`.
|
||||
|
||||
The way efficiency is evaluated will depend on the
|
||||
:ref:`Goal <goal_definition>` to achieve.
|
||||
The way efficacy is evaluated will depend on the :ref:`Goal <goal_definition>`
|
||||
to achieve.
|
||||
|
||||
Of course, the efficiency 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
|
||||
(i.e., the current state of the :ref:`Cluster <cluster_definition>`
|
||||
has not changed in a way that a new :ref:`Audit <audit_definition>` would need
|
||||
to be launched).
|
||||
|
||||
For example, if the :ref:`Goal <goal_definition>` is to lower the energy
|
||||
consumption, the :ref:`Efficiency <efficiency_definition>` will be computed
|
||||
consumption, the :ref:`Efficacy <efficacy_definition>` will be computed
|
||||
using several indicators (KPIs):
|
||||
|
||||
- the percentage of energy gain (which must be the highest possible)
|
||||
@@ -244,7 +244,7 @@ using several indicators (KPIs):
|
||||
All those indicators (KPIs) are computed within a given timeframe, which is the
|
||||
time taken to execute the whole :ref:`Action Plan <action_plan_definition>`.
|
||||
|
||||
The efficiency also enables the :ref:`Administrator <administrator_definition>`
|
||||
The efficacy also enables the :ref:`Administrator <administrator_definition>`
|
||||
to objectively compare different :ref:`Strategies <strategy_definition>` for
|
||||
the same goal and same workload of the :ref:`Cluster <cluster_definition>`.
|
||||
|
||||
@@ -323,7 +323,7 @@ Solution
|
||||
Strategy
|
||||
========
|
||||
|
||||
.. watcher-term:: watcher.decision_engine.strategy.strategies.base
|
||||
.. watcher-term:: watcher.api.controllers.v1.strategy
|
||||
|
||||
.. _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
|
||||
@@ -1,15 +1,15 @@
|
||||
@startuml
|
||||
|
||||
[*] --> RECOMMENDED: The Watcher Planner\ncreates the Action Plan
|
||||
RECOMMENDED --> TRIGGERED: Administrator launches\nthe Action Plan
|
||||
TRIGGERED --> ONGOING: The Watcher Applier receives the request\nto launch the Action Plan
|
||||
RECOMMENDED --> PENDING: Adminisrator launches\nthe Action Plan
|
||||
PENDING --> ONGOING: The Watcher Applier receives the request\nto launch the Action Plan
|
||||
ONGOING --> FAILED: Something failed while executing\nthe Action Plan in the Watcher Applier
|
||||
ONGOING --> SUCCEEDED: The Watcher Applier executed\nthe Action Plan successfully
|
||||
FAILED --> DELETED : Administrator removes\nAction Plan
|
||||
SUCCEEDED --> DELETED : Administrator removes\nAction Plan
|
||||
ONGOING --> CANCELLED : Administrator cancels\nAction Plan
|
||||
RECOMMENDED --> CANCELLED : Administrator cancels\nAction Plan
|
||||
TRIGGERED --> CANCELLED : Administrator cancels\nAction Plan
|
||||
PENDING --> CANCELLED : Administrator cancels\nAction Plan
|
||||
CANCELLED --> DELETED
|
||||
DELETED --> [*]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
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 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
|
||||
|
||||
"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"
|
||||
Trigger audit in the
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
|
||||
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 API" -> "Watcher API" : make sure goal exist in configuration
|
||||
"Watcher API" -> "Watcher Database" : create new audit_template in database
|
||||
"Watcher API" -> "Watcher Database" : Request if goal exists in database
|
||||
"Watcher API" <-- "Watcher Database" : OK
|
||||
|
||||
"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
|
||||
"Watcher API" -> "Watcher Database" : Request if strategy exists in database (if provided)
|
||||
"Watcher API" <-- "Watcher Database" : OK
|
||||
|
||||
"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
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
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 API" -> "Watcher Database" : action_plan.state=TRIGGERED
|
||||
"Watcher CLI" -> "Watcher API" : PATCH action_plan(state=PENDING)
|
||||
"Watcher API" -> "Watcher Database" : action_plan.state=PENDING
|
||||
|
||||
"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: 47 KiB After Width: | Height: | Size: 48 KiB |
|
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 |
@@ -22,6 +22,7 @@ Watcher project consists of several source code repositories:
|
||||
* `watcher`_ - is the main repository. It contains code for Watcher API server,
|
||||
Watcher Decision Engine and Watcher Applier.
|
||||
* `python-watcherclient`_ - Client library and CLI client for Watcher.
|
||||
* `watcher-dashboard`_ - Watcher Horizon plugin.
|
||||
|
||||
The documentation provided here is continually kept up-to-date based
|
||||
on the latest code, and may not represent the state of the project at any
|
||||
@@ -29,6 +30,7 @@ specific prior release.
|
||||
|
||||
.. _watcher: https://git.openstack.org/cgit/openstack/watcher/
|
||||
.. _python-watcherclient: https://git.openstack.org/cgit/openstack/python-watcherclient/
|
||||
.. _watcher-dashboard: https://git.openstack.org/cgit/openstack/watcher-dashboard/
|
||||
|
||||
Developer Guide
|
||||
===============
|
||||
@@ -53,7 +55,8 @@ Getting Started
|
||||
dev/environment
|
||||
dev/devstack
|
||||
deploy/configuration
|
||||
|
||||
deploy/conf-files
|
||||
dev/testing
|
||||
|
||||
API References
|
||||
--------------
|
||||
@@ -69,7 +72,11 @@ Plugins
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
dev/strategy-plugin
|
||||
dev/plugin/base-setup
|
||||
dev/plugin/strategy-plugin
|
||||
dev/plugin/action-plugin
|
||||
dev/plugin/planner-plugin
|
||||
dev/plugins
|
||||
|
||||
|
||||
Admin Guide
|
||||
@@ -83,6 +90,7 @@ Introduction
|
||||
|
||||
deploy/installation
|
||||
deploy/user-guide
|
||||
deploy/gmr
|
||||
|
||||
Watcher Manual Pages
|
||||
====================
|
||||
|
||||
@@ -52,7 +52,7 @@ run the following::
|
||||
|
||||
Show the program's version number and exit.
|
||||
|
||||
.. option:: upgrade, downgrade, stamp, revision, version, create_schema
|
||||
.. option:: upgrade, downgrade, stamp, revision, version, create_schema, purge
|
||||
|
||||
The :ref:`command <db-manage_cmds>` to run.
|
||||
|
||||
@@ -219,3 +219,42 @@ version
|
||||
Show help for version and exit.
|
||||
|
||||
This command will output the current database version.
|
||||
|
||||
purge
|
||||
-----
|
||||
|
||||
.. program:: purge
|
||||
|
||||
.. option:: -h, --help
|
||||
|
||||
Show help for purge and exit.
|
||||
|
||||
.. option:: -d, --age-in-days
|
||||
|
||||
The number of days (starting from today) before which we consider soft
|
||||
deleted objects as expired and should hence be erased. By default, all
|
||||
objects soft deleted are considered expired. This can be useful as removing
|
||||
a significant amount of objects may cause a performance issues.
|
||||
|
||||
.. option:: -n, --max-number
|
||||
|
||||
The maximum number of database objects we expect to be deleted. If exceeded,
|
||||
this will prevent any deletion.
|
||||
|
||||
.. option:: -t, --audit-template
|
||||
|
||||
Either the UUID or name of the soft deleted audit template to purge. This
|
||||
will also include any related objects with it.
|
||||
|
||||
.. option:: -e, --exclude-orphans
|
||||
|
||||
This is a flag to indicate when we want to exclude orphan objects from
|
||||
deletion.
|
||||
|
||||
.. option:: --dry-run
|
||||
|
||||
This is a flag to indicate when we want to perform a dry run. This will show
|
||||
the objects that would be deleted instead of actually deleting them.
|
||||
|
||||
This command will purge the current database by removing both its soft deleted
|
||||
and orphan objects.
|
||||
|
||||
@@ -8,6 +8,19 @@
|
||||
RESTful Web API (v1)
|
||||
====================
|
||||
|
||||
Goals
|
||||
=====
|
||||
|
||||
.. rest-controller:: watcher.api.controllers.v1.goal:GoalsController
|
||||
:webprefix: /v1/goal
|
||||
|
||||
.. autotype:: watcher.api.controllers.v1.goal.GoalCollection
|
||||
:members:
|
||||
|
||||
.. autotype:: watcher.api.controllers.v1.goal.Goal
|
||||
:members:
|
||||
|
||||
|
||||
Audit Templates
|
||||
===============
|
||||
|
||||
|
||||
4
etc/watcher/README-watcher.conf.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
To generate the sample watcher.conf file, run the following
|
||||
command from the top level of the watcher directory:
|
||||
|
||||
tox -econfig
|
||||
9
etc/watcher/watcher-config-generator.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/watcher/watcher.conf.sample
|
||||
wrap_width = 79
|
||||
|
||||
namespace = watcher
|
||||
namespace = keystonemiddleware.auth_token
|
||||
namespace = oslo.log
|
||||
namespace = oslo.db
|
||||
namespace = oslo.messaging
|
||||
@@ -1,775 +0,0 @@
|
||||
[DEFAULT]
|
||||
|
||||
#
|
||||
# From oslo.log
|
||||
#
|
||||
|
||||
# Use syslog for logging. Existing syslog format is DEPRECATED and
|
||||
# will be changed later to honor RFC5424. This option is ignored if
|
||||
# log_config_append is set. (boolean value)
|
||||
#use_syslog = false
|
||||
|
||||
# Enables or disables syslog rfc5424 format for logging. If enabled,
|
||||
# prefixes the MSG part of the syslog message with APP-NAME (RFC5424).
|
||||
# The format without the APP-NAME is deprecated in Kilo, and will be
|
||||
# removed in Mitaka, along with this option. This option is ignored if
|
||||
# log_config_append is set. (boolean value)
|
||||
# This option is deprecated for removal.
|
||||
# Its value may be silently ignored in the future.
|
||||
#use_syslog_rfc_format = true
|
||||
|
||||
# (Optional) The base directory used for relative log_file paths.
|
||||
# This option is ignored if log_config_append is set. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/logdir
|
||||
#log_dir = <None>
|
||||
|
||||
# Syslog facility to receive log lines. This option is ignored if
|
||||
# log_config_append is set. (string value)
|
||||
#syslog_log_facility = LOG_USER
|
||||
|
||||
# Log output to standard error. This option is ignored if
|
||||
# log_config_append is set. (boolean value)
|
||||
#use_stderr = true
|
||||
|
||||
# Format string to use for log messages with context. (string value)
|
||||
#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
|
||||
|
||||
# Format string to use for log messages when context is undefined.
|
||||
# (string value)
|
||||
#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
|
||||
|
||||
# Additional data to append to log message when logging level for the
|
||||
# message is DEBUG. (string value)
|
||||
#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d
|
||||
|
||||
# If set to true, the logging level will be set to DEBUG instead of
|
||||
# the default INFO level. (boolean value)
|
||||
#debug = false
|
||||
|
||||
# Prefix each line of exception output with this format. (string
|
||||
# value)
|
||||
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
|
||||
|
||||
# If set to false, the logging level will be set to WARNING instead of
|
||||
# the default INFO level. (boolean value)
|
||||
# This option is deprecated for removal.
|
||||
# Its value may be silently ignored in the future.
|
||||
#verbose = true
|
||||
|
||||
# Defines the format string for %(user_identity)s that is used in
|
||||
# logging_context_format_string. (string value)
|
||||
#logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s
|
||||
|
||||
# The name of a logging configuration file. This file is appended to
|
||||
# any existing logging configuration files. For details about logging
|
||||
# configuration files, see the Python logging module documentation.
|
||||
# Note that when logging configuration files are used all logging
|
||||
# configuration is defined in the configuration file and other logging
|
||||
# configuration options are ignored (for example, log_format). (string
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/log_config
|
||||
#log_config_append = <None>
|
||||
|
||||
# List of package logging levels in logger=LEVEL pairs. This option is
|
||||
# ignored if log_config_append is set. (list value)
|
||||
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN
|
||||
|
||||
# DEPRECATED. A logging.Formatter log message format string which may
|
||||
# use any of the available logging.LogRecord attributes. This option
|
||||
# is deprecated. Please use logging_context_format_string and
|
||||
# logging_default_format_string instead. This option is ignored if
|
||||
# log_config_append is set. (string value)
|
||||
#log_format = <None>
|
||||
|
||||
# Enables or disables publication of error events. (boolean value)
|
||||
#publish_errors = false
|
||||
|
||||
# Defines the format string for %%(asctime)s in log records. Default:
|
||||
# %(default)s . This option is ignored if log_config_append is set.
|
||||
# (string value)
|
||||
#log_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
# The format for an instance that is passed with the log message.
|
||||
# (string value)
|
||||
#instance_format = "[instance: %(uuid)s] "
|
||||
|
||||
# (Optional) Name of log file to send logging output to. If no default
|
||||
# is set, logging will go to stderr as defined by use_stderr. This
|
||||
# option is ignored if log_config_append is set. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/logfile
|
||||
#log_file = <None>
|
||||
|
||||
# The format for an instance UUID that is passed with the log message.
|
||||
# (string value)
|
||||
#instance_uuid_format = "[instance: %(uuid)s] "
|
||||
|
||||
# Enables or disables fatal status of deprecations. (boolean value)
|
||||
#fatal_deprecations = false
|
||||
|
||||
# Uses logging handler designed to watch file system. When log file is
|
||||
# moved or removed this handler will open a new log file with
|
||||
# specified path instantaneously. It makes sense only if log_file
|
||||
# option is specified and Linux platform is used. This option is
|
||||
# ignored if log_config_append is set. (boolean value)
|
||||
#watch_log_file = false
|
||||
|
||||
#
|
||||
# From oslo.messaging
|
||||
#
|
||||
|
||||
# Directory for holding IPC sockets. (string value)
|
||||
#rpc_zmq_ipc_dir = /var/run/openstack
|
||||
|
||||
# Number of retries to find free port number before fail with
|
||||
# ZMQBindError. (integer value)
|
||||
#rpc_zmq_bind_port_retries = 100
|
||||
|
||||
# AMQP topic used for OpenStack notifications. (list value)
|
||||
# Deprecated group/name - [rpc_notifier2]/topics
|
||||
# Deprecated group/name - [DEFAULT]/notification_topics
|
||||
#topics = notifications
|
||||
|
||||
# Name of this node. Must be a valid hostname, FQDN, or IP address.
|
||||
# Must match "host" option, if running Nova. (string value)
|
||||
#rpc_zmq_host = localhost
|
||||
|
||||
# The messaging driver to use, defaults to rabbit. Other drivers
|
||||
# include amqp and zmq. (string value)
|
||||
#rpc_backend = rabbit
|
||||
|
||||
# Host to locate redis. (string value)
|
||||
#host = 127.0.0.1
|
||||
|
||||
# Seconds to wait before a cast expires (TTL). Only supported by
|
||||
# impl_zmq. (integer value)
|
||||
#rpc_cast_timeout = 30
|
||||
|
||||
# Seconds to wait for a response from a call. (integer value)
|
||||
#rpc_response_timeout = 60
|
||||
|
||||
# Use this port to connect to redis host. (port value)
|
||||
# Minimum value: 1
|
||||
# Maximum value: 65535
|
||||
#port = 6379
|
||||
|
||||
# The default number of seconds that poll should wait. Poll raises
|
||||
# timeout exception when timeout expired. (integer value)
|
||||
#rpc_poll_timeout = 1
|
||||
|
||||
# A URL representing the messaging driver to use and its full
|
||||
# configuration. If not set, we fall back to the rpc_backend option
|
||||
# and driver specific configuration. (string value)
|
||||
#transport_url = <None>
|
||||
|
||||
# Password for Redis server (optional). (string value)
|
||||
#password =
|
||||
|
||||
# Configures zmq-messaging to use proxy with non PUB/SUB patterns.
|
||||
# (boolean value)
|
||||
#direct_over_proxy = true
|
||||
|
||||
# The Drivers(s) to handle sending notifications. Possible values are
|
||||
# messaging, messagingv2, routing, log, test, noop (multi valued)
|
||||
# Deprecated group/name - [DEFAULT]/notification_driver
|
||||
#driver =
|
||||
|
||||
# Size of executor thread pool. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/rpc_thread_pool_size
|
||||
#executor_thread_pool_size = 64
|
||||
|
||||
# Size of RPC connection pool. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/rpc_conn_pool_size
|
||||
#rpc_conn_pool_size = 30
|
||||
|
||||
# Use PUB/SUB pattern for fanout methods. PUB/SUB always uses proxy.
|
||||
# (boolean value)
|
||||
#use_pub_sub = true
|
||||
|
||||
# A URL representing the messaging driver to use for notifications. If
|
||||
# not set, we fall back to the same configuration used for RPC.
|
||||
# (string value)
|
||||
# Deprecated group/name - [DEFAULT]/notification_transport_url
|
||||
#transport_url = <None>
|
||||
|
||||
# ZeroMQ bind address. Should be a wildcard (*), an ethernet
|
||||
# interface, or IP. The "host" option should point or resolve to this
|
||||
# address. (string value)
|
||||
#rpc_zmq_bind_address = *
|
||||
|
||||
# MatchMaker driver. (string value)
|
||||
#rpc_zmq_matchmaker = redis
|
||||
|
||||
# Minimal port number for random ports range. (port value)
|
||||
# Minimum value: 1
|
||||
# Maximum value: 65535
|
||||
#rpc_zmq_min_port = 49152
|
||||
|
||||
# Type of concurrency used. Either "native" or "eventlet" (string
|
||||
# value)
|
||||
#rpc_zmq_concurrency = eventlet
|
||||
|
||||
# Number of ZeroMQ contexts, defaults to 1. (integer value)
|
||||
#rpc_zmq_contexts = 1
|
||||
|
||||
# Maximal port number for random ports range. (integer value)
|
||||
# Minimum value: 1
|
||||
# Maximum value: 65536
|
||||
#rpc_zmq_max_port = 65536
|
||||
|
||||
# The default exchange under which topics are scoped. May be
|
||||
# overridden by an exchange name specified in the transport_url
|
||||
# option. (string value)
|
||||
#control_exchange = openstack
|
||||
|
||||
# Maximum number of ingress messages to locally buffer per topic.
|
||||
# Default is unlimited. (integer value)
|
||||
#rpc_zmq_topic_backlog = <None>
|
||||
|
||||
|
||||
[api]
|
||||
|
||||
#
|
||||
# From watcher
|
||||
#
|
||||
|
||||
# The port for the watcher API server (integer value)
|
||||
#port = 9322
|
||||
|
||||
# The listen IP for the watcher API server (string value)
|
||||
#host = 0.0.0.0
|
||||
|
||||
# The maximum number of items returned in a single response from a
|
||||
# collection resource. (integer value)
|
||||
#max_limit = 1000
|
||||
|
||||
|
||||
[database]
|
||||
|
||||
#
|
||||
# From oslo.db
|
||||
#
|
||||
|
||||
# The SQLAlchemy connection string to use to connect to the database.
|
||||
# (string value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_connection
|
||||
# Deprecated group/name - [DATABASE]/sql_connection
|
||||
# Deprecated group/name - [sql]/connection
|
||||
#connection = <None>
|
||||
|
||||
# Add Python stack traces to SQL as comment strings. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_connection_trace
|
||||
#connection_trace = false
|
||||
|
||||
# Seconds between retries of a database transaction. (integer value)
|
||||
#db_retry_interval = 1
|
||||
|
||||
# Timeout before idle SQL connections are reaped. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_idle_timeout
|
||||
# Deprecated group/name - [DATABASE]/sql_idle_timeout
|
||||
# Deprecated group/name - [sql]/idle_timeout
|
||||
#idle_timeout = 3600
|
||||
|
||||
# If set, use this value for pool_timeout with SQLAlchemy. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DATABASE]/sqlalchemy_pool_timeout
|
||||
#pool_timeout = <None>
|
||||
|
||||
# If True, SQLite uses synchronous mode. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/sqlite_synchronous
|
||||
#sqlite_synchronous = true
|
||||
|
||||
# If db_inc_retry_interval is set, the maximum seconds between retries
|
||||
# of a database operation. (integer value)
|
||||
#db_max_retry_interval = 10
|
||||
|
||||
# Enable the experimental use of database reconnect on connection
|
||||
# lost. (boolean value)
|
||||
#use_db_reconnect = false
|
||||
|
||||
# Interval between retries of opening a SQL connection. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_retry_interval
|
||||
# Deprecated group/name - [DATABASE]/reconnect_interval
|
||||
#retry_interval = 10
|
||||
|
||||
# The file name to use with SQLite. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/sqlite_db
|
||||
#sqlite_db = oslo.sqlite
|
||||
|
||||
# Maximum retries in case of connection error or deadlock error before
|
||||
# error is raised. Set to -1 to specify an infinite retry count.
|
||||
# (integer value)
|
||||
#db_max_retries = 20
|
||||
|
||||
# If True, increases the interval between retries of a database
|
||||
# operation up to db_max_retry_interval. (boolean value)
|
||||
#db_inc_retry_interval = true
|
||||
|
||||
# The SQLAlchemy connection string to use to connect to the slave
|
||||
# database. (string value)
|
||||
#slave_connection = <None>
|
||||
|
||||
# Maximum number of SQL connections to keep open in a pool. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_max_pool_size
|
||||
# Deprecated group/name - [DATABASE]/sql_max_pool_size
|
||||
#max_pool_size = <None>
|
||||
|
||||
# Maximum number of database connection retries during startup. Set to
|
||||
# -1 to specify an infinite retry count. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_max_retries
|
||||
# Deprecated group/name - [DATABASE]/sql_max_retries
|
||||
#max_retries = 10
|
||||
|
||||
# Minimum number of SQL connections to keep open in a pool. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_min_pool_size
|
||||
# Deprecated group/name - [DATABASE]/sql_min_pool_size
|
||||
#min_pool_size = 1
|
||||
|
||||
# Verbosity of SQL debugging information: 0=None, 100=Everything.
|
||||
# (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_connection_debug
|
||||
#connection_debug = 0
|
||||
|
||||
# If set, use this value for max_overflow with SQLAlchemy. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_max_overflow
|
||||
# Deprecated group/name - [DATABASE]/sqlalchemy_max_overflow
|
||||
#max_overflow = <None>
|
||||
|
||||
# The SQL mode to be used for MySQL sessions. This option, including
|
||||
# the default, overrides any server-set SQL mode. To use whatever SQL
|
||||
# mode is set by the server configuration, set this to no value.
|
||||
# Example: mysql_sql_mode= (string value)
|
||||
#mysql_sql_mode = TRADITIONAL
|
||||
|
||||
# The back end to use for the database. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/db_backend
|
||||
#backend = sqlalchemy
|
||||
|
||||
|
||||
[keystone_authtoken]
|
||||
|
||||
#
|
||||
# From keystonemiddleware.auth_token
|
||||
#
|
||||
|
||||
# A PEM encoded Certificate Authority to use when verifying HTTPs
|
||||
# connections. Defaults to system CAs. (string value)
|
||||
#cafile = <None>
|
||||
|
||||
# (Optional) Indicate whether to set the X-Service-Catalog header. If
|
||||
# False, middleware will not ask for service catalog on token
|
||||
# validation and will not set the X-Service-Catalog header. (boolean
|
||||
# value)
|
||||
#include_service_catalog = true
|
||||
|
||||
# (Optional) If defined, indicate whether token data should be
|
||||
# authenticated or authenticated and encrypted. If MAC, token data is
|
||||
# authenticated (with HMAC) in the cache. If ENCRYPT, token data is
|
||||
# encrypted and authenticated in the cache. If the value is not one of
|
||||
# these options or empty, auth_token will raise an exception on
|
||||
# initialization. (string value)
|
||||
# Allowed values: None, MAC, ENCRYPT
|
||||
#memcache_security_strategy = None
|
||||
|
||||
# Verify HTTPS connections. (boolean value)
|
||||
#insecure = false
|
||||
|
||||
# Used to control the use and type of token binding. Can be set to:
|
||||
# "disabled" to not check token binding. "permissive" (default) to
|
||||
# validate binding information if the bind type is of a form known to
|
||||
# the server and ignore it if not. "strict" like "permissive" but if
|
||||
# the bind type is unknown the token will be rejected. "required" any
|
||||
# form of token binding is needed to be allowed. Finally the name of a
|
||||
# binding method that must be present in tokens. (string value)
|
||||
#enforce_token_bind = permissive
|
||||
|
||||
# The region in which the identity server can be found. (string value)
|
||||
#region_name = <None>
|
||||
|
||||
# If true, the revocation list will be checked for cached tokens. This
|
||||
# requires that PKI tokens are configured on the identity server.
|
||||
# (boolean value)
|
||||
#check_revocations_for_cached = false
|
||||
|
||||
# Request timeout value for communicating with Identity API server.
|
||||
# (integer value)
|
||||
#http_connect_timeout = <None>
|
||||
|
||||
# Directory used to cache files related to PKI tokens. (string value)
|
||||
#signing_dir = <None>
|
||||
|
||||
# Hash algorithms to use for hashing PKI tokens. This may be a single
|
||||
# algorithm or multiple. The algorithms are those supported by Python
|
||||
# standard hashlib.new(). The hashes will be tried in the order given,
|
||||
# so put the preferred one first for performance. The result of the
|
||||
# first hash will be stored in the cache. This will typically be set
|
||||
# to multiple values only while migrating from a less secure algorithm
|
||||
# to a more secure one. Once all the old tokens are expired this
|
||||
# option should be set to a single value for better performance. (list
|
||||
# value)
|
||||
#hash_algorithms = md5
|
||||
|
||||
# Optionally specify a list of memcached server(s) to use for caching.
|
||||
# If left undefined, tokens will instead be cached in-process. (list
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/memcache_servers
|
||||
#memcached_servers = <None>
|
||||
|
||||
# Required if identity server requires client certificate (string
|
||||
# value)
|
||||
#certfile = <None>
|
||||
|
||||
# Authentication type to load (unknown value)
|
||||
# Deprecated group/name - [DEFAULT]/auth_plugin
|
||||
#auth_type = <None>
|
||||
|
||||
# Config Section from which to load plugin specific options (unknown
|
||||
# value)
|
||||
#auth_section = <None>
|
||||
|
||||
# In order to prevent excessive effort spent validating tokens, the
|
||||
# middleware caches previously-seen tokens for a configurable duration
|
||||
# (in seconds). Set to -1 to disable caching completely. (integer
|
||||
# value)
|
||||
#token_cache_time = 300
|
||||
|
||||
# API version of the admin Identity API endpoint. (string value)
|
||||
#auth_version = <None>
|
||||
|
||||
# Determines the frequency at which the list of revoked tokens is
|
||||
# retrieved from the Identity service (in seconds). A high number of
|
||||
# revocation events combined with a low cache duration may
|
||||
# significantly reduce performance. (integer value)
|
||||
#revocation_cache_time = 10
|
||||
|
||||
# Complete public Identity API endpoint. (string value)
|
||||
#auth_uri = <None>
|
||||
|
||||
# (Optional) Socket timeout in seconds for communicating with a
|
||||
# memcached server. (integer value)
|
||||
#memcache_pool_socket_timeout = 3
|
||||
|
||||
# (Optional, mandatory if memcache_security_strategy is defined) This
|
||||
# string is used for key derivation. (string value)
|
||||
#memcache_secret_key = <None>
|
||||
|
||||
# Do not handle authorization requests within the middleware, but
|
||||
# delegate the authorization decision to downstream WSGI components.
|
||||
# (boolean value)
|
||||
#delay_auth_decision = false
|
||||
|
||||
# (Optional) Use the advanced (eventlet safe) memcached client pool.
|
||||
# The advanced pool will only work under python 2.x. (boolean value)
|
||||
#memcache_use_advanced_pool = false
|
||||
|
||||
# (Optional) Maximum total number of open connections to every
|
||||
# memcached server. (integer value)
|
||||
#memcache_pool_maxsize = 10
|
||||
|
||||
# How many times are we trying to reconnect when communicating with
|
||||
# Identity API Server. (integer value)
|
||||
#http_request_max_retries = 3
|
||||
|
||||
# (Optional) Number of seconds memcached server is considered dead
|
||||
# before it is tried again. (integer value)
|
||||
#memcache_pool_dead_retry = 300
|
||||
|
||||
# (Optional) Number of seconds a connection to memcached is held
|
||||
# unused in the pool before it is closed. (integer value)
|
||||
#memcache_pool_unused_timeout = 60
|
||||
|
||||
# (Optional) Number of seconds that an operation will wait to get a
|
||||
# memcached client connection from the pool. (integer value)
|
||||
#memcache_pool_conn_get_timeout = 10
|
||||
|
||||
# Env key for the swift cache. (string value)
|
||||
#cache = <None>
|
||||
|
||||
# Required if identity server requires client certificate (string
|
||||
# value)
|
||||
#keyfile = <None>
|
||||
|
||||
|
||||
[matchmaker_redis]
|
||||
|
||||
#
|
||||
# From oslo.messaging
|
||||
#
|
||||
|
||||
# Password for Redis server (optional). (string value)
|
||||
#password =
|
||||
|
||||
# Host to locate redis. (string value)
|
||||
#host = 127.0.0.1
|
||||
|
||||
# Use this port to connect to redis host. (port value)
|
||||
# Minimum value: 1
|
||||
# Maximum value: 65535
|
||||
#port = 6379
|
||||
|
||||
|
||||
[oslo_messaging_amqp]
|
||||
|
||||
#
|
||||
# From oslo.messaging
|
||||
#
|
||||
|
||||
# Timeout for inactive connections (in seconds) (integer value)
|
||||
# Deprecated group/name - [amqp1]/idle_timeout
|
||||
#idle_timeout = 0
|
||||
|
||||
# Password for message broker authentication (string value)
|
||||
# Deprecated group/name - [amqp1]/password
|
||||
#password =
|
||||
|
||||
# Identifying certificate PEM file to present to clients (string
|
||||
# value)
|
||||
# Deprecated group/name - [amqp1]/ssl_cert_file
|
||||
#ssl_cert_file =
|
||||
|
||||
# Private key PEM file used to sign cert_file certificate (string
|
||||
# value)
|
||||
# Deprecated group/name - [amqp1]/ssl_key_file
|
||||
#ssl_key_file =
|
||||
|
||||
# address prefix when sending to any server in group (string value)
|
||||
# Deprecated group/name - [amqp1]/group_request_prefix
|
||||
#group_request_prefix = unicast
|
||||
|
||||
# Path to directory that contains the SASL configuration (string
|
||||
# value)
|
||||
# Deprecated group/name - [amqp1]/sasl_config_dir
|
||||
#sasl_config_dir =
|
||||
|
||||
# Debug: dump AMQP frames to stdout (boolean value)
|
||||
# Deprecated group/name - [amqp1]/trace
|
||||
#trace = false
|
||||
|
||||
# Password for decrypting ssl_key_file (if encrypted) (string value)
|
||||
# Deprecated group/name - [amqp1]/ssl_key_password
|
||||
#ssl_key_password = <None>
|
||||
|
||||
# address prefix used when sending to a specific server (string value)
|
||||
# Deprecated group/name - [amqp1]/server_request_prefix
|
||||
#server_request_prefix = exclusive
|
||||
|
||||
# Name of configuration file (without .conf suffix) (string value)
|
||||
# Deprecated group/name - [amqp1]/sasl_config_name
|
||||
#sasl_config_name =
|
||||
|
||||
# Name for the AMQP container (string value)
|
||||
# Deprecated group/name - [amqp1]/container_name
|
||||
#container_name = <None>
|
||||
|
||||
# Accept clients using either SSL or plain TCP (boolean value)
|
||||
# Deprecated group/name - [amqp1]/allow_insecure_clients
|
||||
#allow_insecure_clients = false
|
||||
|
||||
# CA certificate PEM file to verify server certificate (string value)
|
||||
# Deprecated group/name - [amqp1]/ssl_ca_file
|
||||
#ssl_ca_file =
|
||||
|
||||
# User name for message broker authentication (string value)
|
||||
# Deprecated group/name - [amqp1]/username
|
||||
#username =
|
||||
|
||||
# address prefix used when broadcasting to all servers (string value)
|
||||
# Deprecated group/name - [amqp1]/broadcast_prefix
|
||||
#broadcast_prefix = broadcast
|
||||
|
||||
# Space separated list of acceptable SASL mechanisms (string value)
|
||||
# Deprecated group/name - [amqp1]/sasl_mechanisms
|
||||
#sasl_mechanisms =
|
||||
|
||||
|
||||
[oslo_messaging_rabbit]
|
||||
|
||||
#
|
||||
# From oslo.messaging
|
||||
#
|
||||
|
||||
# The RabbitMQ password. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_password
|
||||
#rabbit_password = guest
|
||||
|
||||
# SSL cert file (valid only if SSL enabled). (string value)
|
||||
# Deprecated group/name - [DEFAULT]/kombu_ssl_certfile
|
||||
#kombu_ssl_certfile =
|
||||
|
||||
# The RabbitMQ login method. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_login_method
|
||||
#rabbit_login_method = AMQPLAIN
|
||||
|
||||
# How long to backoff for between retries when connecting to RabbitMQ.
|
||||
# (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_retry_backoff
|
||||
#rabbit_retry_backoff = 2
|
||||
|
||||
# SSL certification authority file (valid only if SSL enabled).
|
||||
# (string value)
|
||||
# Deprecated group/name - [DEFAULT]/kombu_ssl_ca_certs
|
||||
#kombu_ssl_ca_certs =
|
||||
|
||||
# The RabbitMQ virtual host. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_virtual_host
|
||||
#rabbit_virtual_host = /
|
||||
|
||||
# How long to wait before reconnecting in response to an AMQP consumer
|
||||
# cancel notification. (floating point value)
|
||||
# Deprecated group/name - [DEFAULT]/kombu_reconnect_delay
|
||||
#kombu_reconnect_delay = 1.0
|
||||
|
||||
# How frequently to retry connecting with RabbitMQ. (integer value)
|
||||
#rabbit_retry_interval = 1
|
||||
|
||||
# How long to wait a missing client beforce abandoning to send it its
|
||||
# replies. This value should not be longer than rpc_response_timeout.
|
||||
# (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/kombu_reconnect_timeout
|
||||
#kombu_missing_consumer_retry_timeout = 60
|
||||
|
||||
# Deprecated, use rpc_backend=kombu+memory or rpc_backend=fake
|
||||
# (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/fake_rabbit
|
||||
#fake_rabbit = false
|
||||
|
||||
# Determines how the next RabbitMQ node is chosen in case the one we
|
||||
# are currently connected to becomes unavailable. Takes effect only if
|
||||
# more than one RabbitMQ node is provided in config. (string value)
|
||||
# Allowed values: round-robin, shuffle
|
||||
#kombu_failover_strategy = round-robin
|
||||
|
||||
# How often times during the heartbeat_timeout_threshold we check the
|
||||
# heartbeat. (integer value)
|
||||
#heartbeat_rate = 2
|
||||
|
||||
# The RabbitMQ broker address where a single node is used. (string
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_host
|
||||
#rabbit_host = localhost
|
||||
|
||||
# The RabbitMQ broker port where a single node is used. (port value)
|
||||
# Minimum value: 1
|
||||
# Maximum value: 65535
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_port
|
||||
#rabbit_port = 5672
|
||||
|
||||
# Use HA queues in RabbitMQ (x-ha-policy: all). If you change this
|
||||
# option, you must wipe the RabbitMQ database. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_ha_queues
|
||||
#rabbit_ha_queues = false
|
||||
|
||||
# Use durable queues in AMQP. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/amqp_durable_queues
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_durable_queues
|
||||
#amqp_durable_queues = false
|
||||
|
||||
# Maximum number of RabbitMQ connection retries. Default is 0
|
||||
# (infinite retry count). (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_max_retries
|
||||
#rabbit_max_retries = 0
|
||||
|
||||
# RabbitMQ HA cluster host:port pairs. (list value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_hosts
|
||||
#rabbit_hosts = $rabbit_host:$rabbit_port
|
||||
|
||||
# Auto-delete queues in AMQP. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/amqp_auto_delete
|
||||
#amqp_auto_delete = false
|
||||
|
||||
# Number of seconds after which the Rabbit broker is considered down
|
||||
# if heartbeat's keep-alive fails (0 disable the heartbeat).
|
||||
# EXPERIMENTAL (integer value)
|
||||
#heartbeat_timeout_threshold = 60
|
||||
|
||||
# Connect over SSL for RabbitMQ. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_use_ssl
|
||||
#rabbit_use_ssl = false
|
||||
|
||||
# SSL version to use (valid only if SSL enabled). Valid values are
|
||||
# TLSv1 and SSLv23. SSLv2, SSLv3, TLSv1_1, and TLSv1_2 may be
|
||||
# available on some distributions. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/kombu_ssl_version
|
||||
#kombu_ssl_version =
|
||||
|
||||
# The RabbitMQ userid. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_userid
|
||||
#rabbit_userid = guest
|
||||
|
||||
# SSL key file (valid only if SSL enabled). (string value)
|
||||
# Deprecated group/name - [DEFAULT]/kombu_ssl_keyfile
|
||||
#kombu_ssl_keyfile =
|
||||
|
||||
|
||||
[watcher_applier]
|
||||
|
||||
#
|
||||
# From watcher
|
||||
#
|
||||
|
||||
# Number of workers for applier, default value is 1. (integer value)
|
||||
# Minimum value: 1
|
||||
#workers = 1
|
||||
|
||||
# Select the engine to use to execute the workflow (string value)
|
||||
#workflow_engine = taskflow
|
||||
|
||||
# The topic name used forcontrol events, this topic used for rpc call
|
||||
# (string value)
|
||||
#topic_control = watcher.applier.control
|
||||
|
||||
# The identifier used by watcher module on the message broker (string
|
||||
# value)
|
||||
#publisher_id = watcher.applier.api
|
||||
|
||||
# The topic name used for status events, this topic is used so as to
|
||||
# notifythe others components of the system (string value)
|
||||
#topic_status = watcher.applier.status
|
||||
|
||||
|
||||
[watcher_decision_engine]
|
||||
|
||||
#
|
||||
# From watcher
|
||||
#
|
||||
|
||||
# The identifier used by watcher module on the message broker (string
|
||||
# value)
|
||||
#publisher_id = watcher.decision.api
|
||||
|
||||
# The topic name used forcontrol events, this topic used for rpc call
|
||||
# (string value)
|
||||
#topic_control = watcher.decision.control
|
||||
|
||||
# The maximum number of threads that can be used to execute strategies
|
||||
# (integer value)
|
||||
#max_workers = 2
|
||||
|
||||
# The topic name used for status events, this topic is used so as to
|
||||
# notifythe others components of the system (string value)
|
||||
#topic_status = watcher.decision.status
|
||||
|
||||
|
||||
[watcher_goals]
|
||||
|
||||
#
|
||||
# From watcher
|
||||
#
|
||||
|
||||
# Goals used for the optimization. Maps each goal to an associated
|
||||
# strategy (for example: BASIC_CONSOLIDATION:basic,
|
||||
# MY_GOAL:my_strategy_1) (dict value)
|
||||
#goals = DUMMY:dummy
|
||||
|
||||
|
||||
[watcher_planner]
|
||||
|
||||
#
|
||||
# From watcher
|
||||
#
|
||||
|
||||
# The selected planner used to schedule the actions (string value)
|
||||
#planner = default
|
||||
@@ -2,29 +2,37 @@
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
enum34;python_version=='2.7' or python_version=='2.6'
|
||||
jsonpatch>=1.1
|
||||
keystonemiddleware>=2.0.0,!=2.4.0
|
||||
oslo.config>=2.3.0 # Apache-2.0
|
||||
oslo.db>=2.4.1 # Apache-2.0
|
||||
oslo.i18n>=1.5.0 # Apache-2.0
|
||||
oslo.log>=1.8.0 # Apache-2.0
|
||||
oslo.messaging>=1.16.0,!=1.17.0,!=1.17.1,!=2.6.0,!=2.6.1 # Apache-2.0
|
||||
oslo.policy>=0.5.0 # Apache-2.0
|
||||
oslo.service>=0.7.0 # Apache-2.0
|
||||
oslo.utils>=2.0.0,!=2.6.0 # Apache-2.0
|
||||
PasteDeploy>=1.5.0
|
||||
pbr>=1.6
|
||||
pecan>=1.0.0
|
||||
python-ceilometerclient>=1.5.0
|
||||
python-cinderclient>=1.3.1
|
||||
python-glanceclient>=0.18.0
|
||||
python-keystoneclient>=1.6.0,!=1.8.0
|
||||
python-neutronclient>=2.6.0
|
||||
python-novaclient>=2.28.1,!=2.33.0
|
||||
python-openstackclient>=1.5.0
|
||||
six>=1.9.0
|
||||
SQLAlchemy>=0.9.9,<1.1.0
|
||||
stevedore>=1.5.0 # Apache-2.0
|
||||
taskflow>=1.25.0 # Apache-2.0
|
||||
WSME>=0.7
|
||||
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
||||
jsonpatch>=1.1 # BSD
|
||||
keystoneauth1>=2.1.0 # Apache-2.0
|
||||
keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
|
||||
oslo.concurrency>=3.8.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.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.log>=1.14.0 # Apache-2.0
|
||||
oslo.messaging>=4.5.0 # Apache-2.0
|
||||
oslo.policy>=0.5.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
|
||||
PasteDeploy>=1.5.0 # MIT
|
||||
pbr>=1.6 # Apache-2.0
|
||||
pecan>=1.0.0 # BSD
|
||||
PrettyTable<0.8,>=0.7 # BSD
|
||||
voluptuous>=0.8.9 # BSD License
|
||||
python-ceilometerclient>=2.2.1 # Apache-2.0
|
||||
python-cinderclient!=1.7.0,>=1.6.0 # Apache-2.0
|
||||
python-glanceclient>=2.0.0 # Apache-2.0
|
||||
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
|
||||
python-neutronclient>=4.2.0 # Apache-2.0
|
||||
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
|
||||
python-openstackclient>=2.1.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
SQLAlchemy<1.1.0,>=1.0.10 # MIT
|
||||
stevedore>=1.10.0 # Apache-2.0
|
||||
taskflow>=1.26.0 # Apache-2.0
|
||||
WebOb>=1.2.3 # MIT
|
||||
WSME>=0.8 # MIT
|
||||
|
||||
@@ -49,6 +49,9 @@ watcher_strategies =
|
||||
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
|
||||
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
||||
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
||||
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 =
|
||||
migrate = watcher.applier.actions.migration:Migrate
|
||||
@@ -63,7 +66,8 @@ watcher_planners =
|
||||
default = watcher.decision_engine.planner.default:DefaultPlanner
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
warnerrors = true
|
||||
autodoc_index_modules = true
|
||||
autodoc_exclude_modules =
|
||||
watcher.db.sqlalchemy.alembic.env
|
||||
watcher.db.sqlalchemy.alembic.versions.*
|
||||
|
||||
3
setup.py
Executable file → Normal file
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -26,5 +25,5 @@ except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
setup_requires=['pbr>=1.8'],
|
||||
pbr=True)
|
||||
|
||||
@@ -2,23 +2,20 @@
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
coverage>=3.6
|
||||
discover
|
||||
coverage>=3.6 # Apache-2.0
|
||||
discover # BSD
|
||||
doc8 # Apache-2.0
|
||||
hacking>=0.10.2,<0.11
|
||||
mock>=1.2
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
os-testr>=0.1.0
|
||||
python-subunit>=0.0.18
|
||||
testrepository>=0.0.18
|
||||
testscenarios>=0.4
|
||||
testtools>=1.4.0
|
||||
freezegun # Apache-2.0
|
||||
hacking<0.11,>=0.10.2
|
||||
mock>=2.0 # BSD
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
os-testr>=0.4.1 # Apache-2.0
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
|
||||
# Doc requirements
|
||||
oslosphinx>=2.5.0 # Apache-2.0
|
||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||
sphinxcontrib-pecanwsme>=0.8
|
||||
|
||||
# For PyPI distribution
|
||||
twine
|
||||
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
|
||||
sphinxcontrib-pecanwsme>=0.8 # Apache-2.0
|
||||
|
||||
18
tox.ini
@@ -26,7 +26,7 @@ setenv = PYTHONHASHSEED=0
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --omit="watcher/tests/*" --testr-args='{posargs}'
|
||||
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
setenv = PYTHONHASHSEED=0
|
||||
@@ -35,17 +35,12 @@ commands =
|
||||
python setup.py build_sphinx
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper {posargs}
|
||||
commands = oslo_debug_helper -t watcher/tests {posargs}
|
||||
|
||||
[testenv:config]
|
||||
sitepackages = False
|
||||
commands =
|
||||
oslo-config-generator --namespace watcher \
|
||||
--namespace keystonemiddleware.auth_token \
|
||||
--namespace oslo.log \
|
||||
--namespace oslo.db \
|
||||
--namespace oslo.messaging \
|
||||
--output-file etc/watcher/watcher.conf.sample
|
||||
oslo-config-generator --config-file etc/watcher/watcher-config-generator.conf
|
||||
|
||||
[flake8]
|
||||
show-source=True
|
||||
@@ -53,11 +48,6 @@ ignore=
|
||||
builtins= _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
|
||||
|
||||
[testenv:pypi]
|
||||
commands =
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload --config-file .pypirc {posargs} dist/*
|
||||
|
||||
[testenv:wheel]
|
||||
commands = python setup.py bdist_wheel
|
||||
|
||||
@@ -67,4 +57,4 @@ import_exceptions = watcher._i18n
|
||||
[doc8]
|
||||
extension=.rst
|
||||
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed
|
||||
ignore-path=doc/source/image_src,doc/source/man
|
||||
ignore-path=doc/source/image_src,doc/source/man,doc/source/api
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
import oslo_i18n
|
||||
from oslo_i18n import _lazy
|
||||
|
||||
# The domain is the name of the App which is used to generate the folder
|
||||
# containing the translation files (i.e. the .pot file and the various locales)
|
||||
@@ -42,5 +43,9 @@ _LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
|
||||
|
||||
def lazy_translation_enabled():
|
||||
return _lazy.USE_LAZY
|
||||
|
||||
|
||||
def get_available_languages():
|
||||
return oslo_i18n.get_available_languages(DOMAIN)
|
||||
|
||||
@@ -19,24 +19,38 @@
|
||||
from oslo_config import cfg
|
||||
import pecan
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.api import acl
|
||||
from watcher.api import config as api_config
|
||||
from watcher.api import middleware
|
||||
from watcher.decision_engine.strategy.selection import default \
|
||||
as strategy_selector
|
||||
|
||||
# Register options for the service
|
||||
API_SERVICE_OPTS = [
|
||||
cfg.IntOpt('port',
|
||||
default=9322,
|
||||
help='The port for the watcher API server'),
|
||||
cfg.PortOpt('port',
|
||||
default=9322,
|
||||
help=_('The port for the watcher API server')),
|
||||
cfg.StrOpt('host',
|
||||
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',
|
||||
default=1000,
|
||||
help='The maximum number of items returned in a single '
|
||||
'response from a collection resource.')
|
||||
help=_('The maximum number of items returned in a single '
|
||||
'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
|
||||
@@ -45,7 +59,6 @@ opt_group = cfg.OptGroup(name='api',
|
||||
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opts(API_SERVICE_OPTS, opt_group)
|
||||
CONF.register_opts(strategy_selector.WATCHER_GOALS_OPTS)
|
||||
|
||||
|
||||
def get_pecan_config():
|
||||
@@ -68,3 +81,12 @@ def setup_app(config=None):
|
||||
)
|
||||
|
||||
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_template
|
||||
from watcher.api.controllers.v1 import goal
|
||||
from watcher.api.controllers.v1 import strategy
|
||||
|
||||
|
||||
class APIBase(wtypes.Base):
|
||||
@@ -157,6 +158,7 @@ class Controller(rest.RestController):
|
||||
actions = action.ActionsController()
|
||||
action_plans = action_plan.ActionPlansController()
|
||||
goals = goal.GoalsController()
|
||||
strategies = strategy.StrategiesController()
|
||||
|
||||
@wsme_pecan.wsexpose(V1)
|
||||
def get(self):
|
||||
|
||||
@@ -49,6 +49,10 @@ be one of the following:
|
||||
- **CANCELLED** : the :ref:`Action <action_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
|
||||
:ref:`Some default implementations are provided <watcher_planners>`, but it is
|
||||
possible to :ref:`develop new implementations <implement_action_plugin>` which
|
||||
are dynamically loaded by Watcher at launch time.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@@ -115,7 +119,7 @@ class Action(base.APIBase):
|
||||
self.action_next_uuid = None
|
||||
# raise e
|
||||
|
||||
uuid = types.uuid
|
||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||
"""Unique UUID for this action"""
|
||||
|
||||
action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid,
|
||||
@@ -126,16 +130,10 @@ class Action(base.APIBase):
|
||||
state = wtypes.text
|
||||
"""This audit state"""
|
||||
|
||||
alarm = types.uuid
|
||||
"""An alarm UUID related to this action"""
|
||||
|
||||
applies_to = wtypes.text
|
||||
"""Applies to"""
|
||||
|
||||
action_type = wtypes.text
|
||||
"""Action type"""
|
||||
|
||||
input_parameters = wtypes.DictType(wtypes.text, wtypes.text)
|
||||
input_parameters = types.jsontype
|
||||
"""One or more key/value pairs """
|
||||
|
||||
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
||||
@@ -193,7 +191,6 @@ class Action(base.APIBase):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
description='action description',
|
||||
state='PENDING',
|
||||
alarm=None,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
@@ -257,7 +254,7 @@ class ActionsController(rest.RestController):
|
||||
resource_url=None,
|
||||
action_plan_uuid=None, audit_uuid=None):
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
@@ -288,10 +285,10 @@ class ActionsController(rest.RestController):
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionCollection, types.uuid, types.uuid,
|
||||
int, wtypes.text, wtypes.text, types.uuid,
|
||||
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
|
||||
wtypes.text, wtypes.text, types.uuid,
|
||||
types.uuid)
|
||||
def get_all(self, action_uuid=None, marker=None, limit=None,
|
||||
def get_all(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', action_plan_uuid=None,
|
||||
audit_uuid=None):
|
||||
"""Retrieve a list of actions.
|
||||
@@ -312,16 +309,14 @@ class ActionsController(rest.RestController):
|
||||
marker, limit, sort_key, sort_dir,
|
||||
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text, wtypes.text,
|
||||
types.uuid, types.uuid)
|
||||
def detail(self, action_uuid=None, marker=None, limit=None,
|
||||
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
|
||||
wtypes.text, wtypes.text, types.uuid,
|
||||
types.uuid)
|
||||
def detail(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', action_plan_uuid=None,
|
||||
audit_uuid=None):
|
||||
"""Retrieve a list of actions with detail.
|
||||
|
||||
:param action_uuid: UUID of a action, to get only actions for that
|
||||
action.
|
||||
: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.
|
||||
@@ -364,6 +359,10 @@ class ActionsController(rest.RestController):
|
||||
|
||||
:param action: a action within the request body.
|
||||
"""
|
||||
# FIXME: blueprint edit-action-plan-flow
|
||||
raise exception.OperationNotPermitted(
|
||||
_("Cannot create an action directly"))
|
||||
|
||||
if self.from_actions:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
@@ -384,6 +383,10 @@ class ActionsController(rest.RestController):
|
||||
:param action_uuid: UUID of a action.
|
||||
:param patch: a json PATCH document to apply to this action.
|
||||
"""
|
||||
# FIXME: blueprint edit-action-plan-flow
|
||||
raise exception.OperationNotPermitted(
|
||||
_("Cannot modify an action directly"))
|
||||
|
||||
if self.from_actions:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
@@ -416,6 +419,9 @@ class ActionsController(rest.RestController):
|
||||
|
||||
:param action_uuid: UUID of a action.
|
||||
"""
|
||||
# FIXME: blueprint edit-action-plan-flow
|
||||
raise exception.OperationNotPermitted(
|
||||
_("Cannot delete an action directly"))
|
||||
|
||||
action_to_delete = objects.Action.get_by_uuid(
|
||||
pecan.request.context,
|
||||
|
||||
@@ -49,24 +49,9 @@ standard workflow model description formats such as
|
||||
`Business Process Model and Notation 2.0 (BPMN 2.0) <http://www.omg.org/spec/BPMN/2.0/>`_
|
||||
or `Unified Modeling Language (UML) <http://www.uml.org/>`_.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
|
||||
state may be one of the following:
|
||||
|
||||
- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
|
||||
for a validation from the :ref:`Administrator <administrator_definition>`
|
||||
- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
|
||||
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
|
||||
executed successfully (i.e. all :ref:`Actions <action_definition>` that it
|
||||
contains have been executed successfully)
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
|
||||
stored in the :ref:`Watcher database <watcher_database_definition>` but is
|
||||
not returned any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
|
||||
**PENDING** or **ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
To see the life-cycle and description of
|
||||
:ref:`Action Plan <action_plan_definition>` states, visit :ref:`the Action Plan state
|
||||
machine <action_plan_state_machine>`.
|
||||
""" # noqa
|
||||
|
||||
import datetime
|
||||
@@ -158,7 +143,7 @@ class ActionPlan(base.APIBase):
|
||||
except exception.ActionNotFound:
|
||||
self._first_action_uuid = None
|
||||
|
||||
uuid = types.uuid
|
||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||
"""Unique UUID for this action plan"""
|
||||
|
||||
first_action_uuid = wsme.wsproperty(
|
||||
@@ -181,7 +166,6 @@ class ActionPlan(base.APIBase):
|
||||
|
||||
self.fields = []
|
||||
fields = list(objects.ActionPlan.fields)
|
||||
fields.append('audit_uuid')
|
||||
for field in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
@@ -189,14 +173,19 @@ class ActionPlan(base.APIBase):
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
self.fields.append('audit_id')
|
||||
self.fields.append('audit_uuid')
|
||||
self.fields.append('first_action_uuid')
|
||||
|
||||
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
|
||||
setattr(self, 'first_action_uuid',
|
||||
kwargs.get('first_action_id', wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(action_plan, url, expand=True):
|
||||
if not expand:
|
||||
action_plan.unset_fields_except(['uuid', 'state', 'updated_at',
|
||||
'audit_uuid'])
|
||||
action_plan.unset_fields_except(
|
||||
['uuid', 'state', 'updated_at',
|
||||
'audit_uuid', 'first_action_uuid'])
|
||||
|
||||
action_plan.links = [link.Link.make_link(
|
||||
'self', url,
|
||||
@@ -279,7 +268,7 @@ class ActionPlansController(rest.RestController):
|
||||
resource_url=None, audit_uuid=None):
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
@@ -402,12 +391,12 @@ class ActionPlansController(rest.RestController):
|
||||
# transitions that are allowed via PATCH
|
||||
allowed_patch_transitions = [
|
||||
(ap_objects.State.RECOMMENDED,
|
||||
ap_objects.State.TRIGGERED),
|
||||
ap_objects.State.PENDING),
|
||||
(ap_objects.State.RECOMMENDED,
|
||||
ap_objects.State.CANCELLED),
|
||||
(ap_objects.State.ONGOING,
|
||||
ap_objects.State.CANCELLED),
|
||||
(ap_objects.State.TRIGGERED,
|
||||
(ap_objects.State.PENDING,
|
||||
ap_objects.State.CANCELLED),
|
||||
]
|
||||
|
||||
@@ -423,7 +412,7 @@ class ActionPlansController(rest.RestController):
|
||||
initial_state=action_plan_to_update.state,
|
||||
new_state=action_plan.state))
|
||||
|
||||
if action_plan.state == ap_objects.State.TRIGGERED:
|
||||
if action_plan.state == ap_objects.State.PENDING:
|
||||
launch_action_plan = True
|
||||
|
||||
# Update only the fields that have changed
|
||||
@@ -439,7 +428,7 @@ class ActionPlansController(rest.RestController):
|
||||
action_plan_to_update[field] = patch_val
|
||||
|
||||
if (field == 'state'
|
||||
and patch_val == objects.action_plan.State.TRIGGERED):
|
||||
and patch_val == objects.action_plan.State.PENDING):
|
||||
launch_action_plan = True
|
||||
|
||||
action_plan_to_update.save()
|
||||
|
||||
@@ -25,28 +25,8 @@ on a given :ref:`Cluster <cluster_definition>`.
|
||||
For each :ref:`Audit <audit_definition>`, the Watcher system generates an
|
||||
:ref:`Action Plan <action_plan_definition>`.
|
||||
|
||||
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
|
||||
be one of the following:
|
||||
|
||||
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
|
||||
submitted (either manually by the
|
||||
:ref:`Administrator <administrator_definition>` or automatically via some
|
||||
event handling mechanism) and is in the queue for being processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
|
||||
processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
|
||||
successfully (note that it may not necessarily produce a
|
||||
:ref:`Solution <solution_definition>`).
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Audit <audit_definition>`
|
||||
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
|
||||
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||
any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
To see the life-cycle and description of an :ref:`Audit <audit_definition>`
|
||||
states, visit :ref:`the Audit State machine <audit_state_machine>`.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@@ -65,10 +45,32 @@ 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
|
||||
from watcher.decision_engine.rpcapi import DecisionEngineAPI
|
||||
from watcher.decision_engine import rpcapi
|
||||
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):
|
||||
|
||||
@staticmethod
|
||||
@@ -263,7 +265,7 @@ class AuditsController(rest.RestController):
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None, audit_template=None):
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
@@ -345,12 +347,13 @@ class AuditsController(rest.RestController):
|
||||
audit_uuid)
|
||||
return Audit.convert_with_links(rpc_audit)
|
||||
|
||||
@wsme_pecan.wsexpose(Audit, body=Audit, status_code=201)
|
||||
def post(self, audit):
|
||||
@wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201)
|
||||
def post(self, audit_p):
|
||||
"""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:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
@@ -369,7 +372,7 @@ class AuditsController(rest.RestController):
|
||||
|
||||
# trigger decision-engine to run the audit
|
||||
|
||||
dc_client = DecisionEngineAPI()
|
||||
dc_client = rpcapi.DecisionEngineAPI()
|
||||
dc_client.trigger_audit(context, new_audit.uuid)
|
||||
|
||||
return Audit.convert_with_links(new_audit)
|
||||
|
||||
@@ -56,22 +56,157 @@ import wsme
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from watcher._i18n import _
|
||||
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 context as context_utils
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils as common_utils
|
||||
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):
|
||||
|
||||
_ctx = context_utils.make_context()
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def validate(patch):
|
||||
if patch.path == "/goal" and patch.op != "remove":
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _validate_goal(patch):
|
||||
patch.path = "/goal_id"
|
||||
goal = patch.value
|
||||
|
||||
if goal:
|
||||
available_goals = objects.Goal.list(
|
||||
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):
|
||||
"""API representation of a audit template.
|
||||
@@ -80,7 +215,90 @@ class AuditTemplate(base.APIBase):
|
||||
between the internal object model and the API representation of an
|
||||
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"""
|
||||
|
||||
name = wtypes.text
|
||||
@@ -98,8 +316,21 @@ class AuditTemplate(base.APIBase):
|
||||
extra = {wtypes.text: types.jsontype}
|
||||
"""The metadata of the audit template"""
|
||||
|
||||
goal = wtypes.text
|
||||
"""Goal type of the audit template"""
|
||||
goal_uuid = wsme.wsproperty(
|
||||
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
|
||||
"""Internal version of the audit template"""
|
||||
@@ -112,20 +343,43 @@ class AuditTemplate(base.APIBase):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AuditTemplate, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
for field in objects.AuditTemplate.fields:
|
||||
fields = list(objects.AuditTemplate.fields)
|
||||
|
||||
for k in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
if not hasattr(self, k):
|
||||
continue
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
self.fields.append(k)
|
||||
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
|
||||
def _convert_with_links(audit_template, url, expand=True):
|
||||
if not expand:
|
||||
audit_template.unset_fields_except(['uuid', 'name',
|
||||
'host_aggregate', 'goal'])
|
||||
audit_template.unset_fields_except(
|
||||
['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_templates',
|
||||
@@ -133,8 +387,7 @@ class AuditTemplate(base.APIBase):
|
||||
link.Link.make_link('bookmark', url,
|
||||
'audit_templates',
|
||||
audit_template.uuid,
|
||||
bookmark=True)
|
||||
]
|
||||
bookmark=True)]
|
||||
return audit_template
|
||||
|
||||
@classmethod
|
||||
@@ -149,7 +402,8 @@ class AuditTemplate(base.APIBase):
|
||||
name='My Audit Template',
|
||||
description='Description of my audit template',
|
||||
host_aggregate=5,
|
||||
goal='SERVERS_CONSOLIDATION',
|
||||
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
|
||||
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
|
||||
extra={'automatic': True},
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
@@ -170,12 +424,12 @@ class AuditTemplateCollection(collection.Collection):
|
||||
@staticmethod
|
||||
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
|
||||
**kwargs):
|
||||
collection = AuditTemplateCollection()
|
||||
collection.audit_templates = \
|
||||
[AuditTemplate.convert_with_links(p, expand)
|
||||
for p in rpc_audit_templates]
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
at_collection = AuditTemplateCollection()
|
||||
at_collection.audit_templates = [
|
||||
AuditTemplate.convert_with_links(p, expand)
|
||||
for p in rpc_audit_templates]
|
||||
at_collection.next = at_collection.get_next(limit, url=url, **kwargs)
|
||||
return at_collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
@@ -197,12 +451,14 @@ class AuditTemplatesController(rest.RestController):
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_audit_templates_collection(self, marker, limit,
|
||||
def _get_audit_templates_collection(self, filters, marker, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None):
|
||||
|
||||
api_utils.validate_search_filters(
|
||||
filters, list(objects.audit_template.AuditTemplate.fields.keys()) +
|
||||
["goal_uuid", "goal_name", "strategy_uuid", "strategy_name"])
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
@@ -212,6 +468,7 @@ class AuditTemplatesController(rest.RestController):
|
||||
|
||||
audit_templates = objects.AuditTemplate.list(
|
||||
pecan.request.context,
|
||||
filters,
|
||||
limit,
|
||||
marker_obj, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
@@ -223,26 +480,43 @@ class AuditTemplatesController(rest.RestController):
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplateCollection, types.uuid, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def get_all(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
|
||||
types.uuid, int, wtypes.text, wtypes.text)
|
||||
def get_all(self, goal=None, strategy=None, marker=None,
|
||||
limit=None, sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audit templates.
|
||||
|
||||
: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 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.
|
||||
"""
|
||||
return self._get_audit_templates_collection(marker, limit, sort_key,
|
||||
sort_dir)
|
||||
filters = {}
|
||||
if goal:
|
||||
if common_utils.is_uuid_like(goal):
|
||||
filters['goal_uuid'] = goal
|
||||
else:
|
||||
filters['goal_name'] = goal
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplateCollection, types.uuid, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def detail(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
if strategy:
|
||||
if common_utils.is_uuid_like(strategy):
|
||||
filters['strategy_uuid'] = strategy
|
||||
else:
|
||||
filters['strategy_name'] = strategy
|
||||
|
||||
return self._get_audit_templates_collection(
|
||||
filters, marker, limit, sort_key, sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
|
||||
types.uuid, int, wtypes.text, wtypes.text)
|
||||
def detail(self, goal=None, strategy=None, marker=None,
|
||||
limit=None, sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audit templates with detail.
|
||||
|
||||
: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 limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
@@ -253,9 +527,22 @@ class AuditTemplatesController(rest.RestController):
|
||||
if parent != "audit_templates":
|
||||
raise exception.HTTPNotFound
|
||||
|
||||
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
|
||||
resource_url = '/'.join(['audit_templates', 'detail'])
|
||||
return self._get_audit_templates_collection(marker, limit,
|
||||
return self._get_audit_templates_collection(filters, marker, limit,
|
||||
sort_key, sort_dir, expand,
|
||||
resource_url)
|
||||
|
||||
@@ -263,7 +550,7 @@ class AuditTemplatesController(rest.RestController):
|
||||
def get_one(self, audit_template):
|
||||
"""Retrieve information about the given audit template.
|
||||
|
||||
:param audit template_uuid: UUID or name of an audit template.
|
||||
:param audit audit_template: UUID or name of an audit template.
|
||||
"""
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
@@ -279,17 +566,21 @@ class AuditTemplatesController(rest.RestController):
|
||||
|
||||
return AuditTemplate.convert_with_links(rpc_audit_template)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplate, status_code=201)
|
||||
def post(self, audit_template):
|
||||
@wsme.validate(types.uuid, AuditTemplatePostType)
|
||||
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
|
||||
status_code=201)
|
||||
def post(self, audit_template_postdata):
|
||||
"""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:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
audit_template_dict = audit_template.as_dict()
|
||||
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,
|
||||
**audit_template_dict)
|
||||
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])
|
||||
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
|
||||
'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,
|
||||
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 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 Goal(base.APIBase):
|
||||
"""API representation of a action.
|
||||
"""API representation of a goal.
|
||||
|
||||
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 of the goal"""
|
||||
|
||||
strategy = wtypes.text
|
||||
"""The strategy associated with the goal"""
|
||||
|
||||
uuid = types.uuid
|
||||
"""Unused field"""
|
||||
display_name = wtypes.text
|
||||
"""Localized name of the goal"""
|
||||
|
||||
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):
|
||||
super(Goal, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
self.fields.append('uuid')
|
||||
self.fields.append('name')
|
||||
self.fields.append('strategy')
|
||||
setattr(self, 'name', kwargs.get('name',
|
||||
wtypes.Unset))
|
||||
setattr(self, 'strategy', kwargs.get('strategy',
|
||||
wtypes.Unset))
|
||||
self.fields.append('display_name')
|
||||
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))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(goal, url, expand=True):
|
||||
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,
|
||||
'goals', goal.name),
|
||||
'goals', goal.uuid),
|
||||
link.Link.make_link('bookmark', url,
|
||||
'goals', goal.name,
|
||||
'goals', goal.uuid,
|
||||
bookmark=True)]
|
||||
return goal
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(name='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
strategy='action description')
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
name='DUMMY',
|
||||
display_name='Dummy strategy')
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
@@ -117,27 +120,28 @@ class GoalCollection(collection.Collection):
|
||||
@staticmethod
|
||||
def convert_with_links(goals, limit, url=None, expand=False,
|
||||
**kwargs):
|
||||
|
||||
collection = GoalCollection()
|
||||
collection.goals = [Goal.convert_with_links(g, expand) for g in goals]
|
||||
goal_collection = GoalCollection()
|
||||
goal_collection.goals = [
|
||||
Goal.convert_with_links(g, expand) for g in goals]
|
||||
|
||||
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
|
||||
collection.goals = sorted(
|
||||
collection.goals,
|
||||
key=lambda goal: goal.name,
|
||||
goal_collection.goals = sorted(
|
||||
goal_collection.goals,
|
||||
key=lambda goal: goal.uuid,
|
||||
reverse=reverse)
|
||||
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
goal_collection.next = goal_collection.get_next(
|
||||
limit, url=url, **kwargs)
|
||||
return goal_collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.actions = [Goal.sample(expand=False)]
|
||||
sample.goals = [Goal.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
@@ -154,51 +158,49 @@ class GoalsController(rest.RestController):
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_goals_collection(self, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None, goal_name=None):
|
||||
|
||||
def _get_goals_collection(self, marker, limit, sort_key, sort_dir,
|
||||
expand=False, resource_url=None):
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = 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():
|
||||
goals.append({'name': goal_name, 'strategy': goals[goal_name]})
|
||||
else:
|
||||
for name, strategy in CONF.watcher_goals.goals.items():
|
||||
goals.append({'name': name, 'strategy': strategy})
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.Goal.get_by_uuid(
|
||||
pecan.request.context, marker)
|
||||
|
||||
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,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(GoalCollection, int, wtypes.text, wtypes.text)
|
||||
def get_all(self, limit=None,
|
||||
sort_key='name', sort_dir='asc'):
|
||||
@wsme_pecan.wsexpose(GoalCollection, wtypes.text,
|
||||
int, wtypes.text, wtypes.text)
|
||||
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||
"""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 sort_key: column to sort results by. Default: id.
|
||||
: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,
|
||||
wtypes.text, wtypes.text)
|
||||
def detail(self, goal_name=None, limit=None,
|
||||
sort_key='name', sort_dir='asc'):
|
||||
"""Retrieve a list of actions with detail.
|
||||
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of goals with detail.
|
||||
|
||||
:param goal_name: name of a goal, to get only goals for that
|
||||
action.
|
||||
: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.
|
||||
to get only goals for that goal.
|
||||
"""
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
@@ -206,21 +208,23 @@ class GoalsController(rest.RestController):
|
||||
raise exception.HTTPNotFound
|
||||
expand = True
|
||||
resource_url = '/'.join(['goals', 'detail'])
|
||||
return self._get_goals_collection(limit, sort_key, sort_dir,
|
||||
expand, resource_url, goal_name)
|
||||
return self._get_goals_collection(marker, limit, sort_key, sort_dir,
|
||||
expand, resource_url)
|
||||
|
||||
@wsme_pecan.wsexpose(Goal, wtypes.text)
|
||||
def get_one(self, goal_name):
|
||||
def get_one(self, 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:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
goals = CONF.watcher_goals.goals
|
||||
goal = {}
|
||||
if goal_name in goals.keys():
|
||||
goal = {'name': goal_name, 'strategy': goals[goal_name]}
|
||||
if common_utils.is_uuid_like(goal):
|
||||
get_goal_func = objects.Goal.get_by_uuid
|
||||
else:
|
||||
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
|
||||
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
|
||||
def validate(value):
|
||||
@@ -55,11 +50,6 @@ class NameType(wtypes.UserType):
|
||||
|
||||
basetype = wtypes.text
|
||||
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
|
||||
def validate(value):
|
||||
@@ -79,11 +69,6 @@ class UuidType(wtypes.UserType):
|
||||
|
||||
basetype = wtypes.text
|
||||
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
|
||||
def validate(value):
|
||||
@@ -103,11 +88,6 @@ class BooleanType(wtypes.UserType):
|
||||
|
||||
basetype = wtypes.text
|
||||
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
|
||||
def validate(value):
|
||||
@@ -129,11 +109,6 @@ class JsonType(wtypes.UserType):
|
||||
|
||||
basetype = wtypes.text
|
||||
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):
|
||||
# These are the json serializable native types
|
||||
|
||||
@@ -47,7 +47,15 @@ def validate_sort_dir(sort_dir):
|
||||
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
|
||||
"Acceptable values are "
|
||||
"'asc' or 'desc'") % sort_dir)
|
||||
return sort_dir
|
||||
|
||||
|
||||
def validate_search_filters(filters, allowed_fields):
|
||||
# Very leightweight validation for now
|
||||
# todo: improve this (e.g. https://www.parse.com/docs/rest/guide/#queries)
|
||||
for filter_name in filters.keys():
|
||||
if filter_name not in allowed_fields:
|
||||
raise wsme.exc.ClientSideError(
|
||||
_("Invalid filter: %s") % filter_name)
|
||||
|
||||
|
||||
def apply_jsonpatch(doc, patch):
|
||||
@@ -58,3 +66,12 @@ def apply_jsonpatch(doc, patch):
|
||||
' the resource is not allowed')
|
||||
raise wsme.exc.ClientSideError(msg % p['path'])
|
||||
return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch))
|
||||
|
||||
|
||||
def as_filters_dict(**filters):
|
||||
filters_dict = {}
|
||||
for filter_name, filter_value in filters.items():
|
||||
if filter_value:
|
||||
filters_dict[filter_name] = filter_value
|
||||
|
||||
return filters_dict
|
||||
|
||||
@@ -43,8 +43,8 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
|
||||
ev.data = {}
|
||||
payload = {'action_plan__uuid': uuid,
|
||||
'action_plan_state': state}
|
||||
self.applier_manager.topic_status.publish_event(ev.type.name,
|
||||
payload)
|
||||
self.applier_manager.status_topic_handler.publish_event(
|
||||
ev.type.name, payload)
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
@@ -52,17 +52,16 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
|
||||
self.notify(self.action_plan_uuid,
|
||||
event_types.EventTypes.LAUNCH_ACTION_PLAN,
|
||||
ap_objects.State.ONGOING)
|
||||
applier = default.DefaultApplier(self.applier_manager, self.ctx)
|
||||
result = applier.execute(self.action_plan_uuid)
|
||||
applier = default.DefaultApplier(self.ctx, self.applier_manager)
|
||||
applier.execute(self.action_plan_uuid)
|
||||
state = ap_objects.State.SUCCEEDED
|
||||
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
result = False
|
||||
state = ap_objects.State.FAILED
|
||||
|
||||
finally:
|
||||
if result is True:
|
||||
status = ap_objects.State.SUCCEEDED
|
||||
else:
|
||||
status = ap_objects.State.FAILED
|
||||
# update state
|
||||
self.notify(self.action_plan_uuid,
|
||||
event_types.EventTypes.LAUNCH_ACTION_PLAN,
|
||||
status)
|
||||
state)
|
||||
|
||||
@@ -22,12 +22,35 @@ import abc
|
||||
|
||||
import six
|
||||
|
||||
from watcher.common import clients
|
||||
from watcher.common.loader import loadable
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAction(object):
|
||||
def __init__(self):
|
||||
class BaseAction(loadable.Loadable):
|
||||
# NOTE(jed) by convention we decided
|
||||
# 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
|
||||
# watcher dashboard and will be nested in input_parameters
|
||||
RESOURCE_ID = 'resource_id'
|
||||
|
||||
def __init__(self, config, osc=None):
|
||||
"""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._applies_to = ""
|
||||
self._osc = osc
|
||||
|
||||
@property
|
||||
def osc(self):
|
||||
if not self._osc:
|
||||
self._osc = clients.OpenStackClients()
|
||||
return self._osc
|
||||
|
||||
@property
|
||||
def input_parameters(self):
|
||||
@@ -38,25 +61,73 @@ class BaseAction(object):
|
||||
self._input_parameters = p
|
||||
|
||||
@property
|
||||
def applies_to(self):
|
||||
return self._applies_to
|
||||
def resource_id(self):
|
||||
return self.input_parameters[self.RESOURCE_ID]
|
||||
|
||||
@applies_to.setter
|
||||
def applies_to(self, a):
|
||||
self._applies_to = a
|
||||
@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
|
||||
def execute(self):
|
||||
"""Executes the main logic of the action
|
||||
|
||||
This method can be used to perform an action on a given set of input
|
||||
parameters to accomplish some type of operation. This operation may
|
||||
return a boolean value as a result of its execution. If False, this
|
||||
will be considered as an error and will then trigger the reverting of
|
||||
the actions.
|
||||
|
||||
:returns: A flag indicating whether or not the action succeeded
|
||||
:rtype: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def revert(self):
|
||||
"""Revert this action
|
||||
|
||||
This method should rollback the resource to its initial state in the
|
||||
event of a faulty execution. This happens when the action raised an
|
||||
exception during its :py:meth:`~.BaseAction.execute`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def precondition(self):
|
||||
"""Hook: called before the execution of an action
|
||||
|
||||
This method can be used to perform some initializations or to make
|
||||
some more advanced validation on its input parameters. So if you wish
|
||||
to block its execution based on this factor, `raise` the related
|
||||
exception.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def postcondition(self):
|
||||
"""Hook: called after the execution of an action
|
||||
|
||||
This function is called regardless of whether an action succeded or
|
||||
not. So you can use it to perform cleanup operations.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractproperty
|
||||
def schema(self):
|
||||
"""Defines a Schema that the input parameters shall comply to
|
||||
|
||||
:returns: A schema declaring the input parameters this action should be
|
||||
provided along with their respective constraints
|
||||
:rtype: :py:class:`voluptuous.Schema` instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_parameters(self):
|
||||
self.schema(self.input_parameters)
|
||||
return True
|
||||
|
||||
@@ -16,54 +16,84 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import six
|
||||
import voluptuous
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.applier.actions import base
|
||||
from watcher.common import exception
|
||||
from watcher.common import keystone as kclient
|
||||
from watcher.common import nova as nclient
|
||||
from watcher.common import nova_helper
|
||||
from watcher.decision_engine.model import hypervisor_state as hstate
|
||||
|
||||
|
||||
class ChangeNovaServiceState(base.BaseAction):
|
||||
"""Disables or enables the nova-compute service, deployed on a host
|
||||
|
||||
By using this action, you will be able to update the state of a
|
||||
nova-compute service. A disabled nova-compute service can not be selected
|
||||
by the nova scheduler for future deployment of server.
|
||||
|
||||
The action schema is::
|
||||
|
||||
schema = Schema({
|
||||
'resource_id': str,
|
||||
'state': str,
|
||||
})
|
||||
|
||||
The `resource_id` references a nova-compute service name (list of available
|
||||
nova-compute services is returned by this command: ``nova service-list
|
||||
--binary nova-compute``).
|
||||
The `state` value should either be `ONLINE` or `OFFLINE`.
|
||||
"""
|
||||
|
||||
STATE = 'state'
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema({
|
||||
voluptuous.Required(self.RESOURCE_ID):
|
||||
voluptuous.All(
|
||||
voluptuous.Any(*six.string_types),
|
||||
voluptuous.Length(min=1)),
|
||||
voluptuous.Required(self.STATE):
|
||||
voluptuous.Any(*[state.value
|
||||
for state in list(hstate.HypervisorState)]),
|
||||
})
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.applies_to
|
||||
return self.resource_id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.input_parameters.get('state')
|
||||
return self.input_parameters.get(self.STATE)
|
||||
|
||||
def execute(self):
|
||||
target_state = None
|
||||
if self.state == hstate.HypervisorState.OFFLINE.value:
|
||||
if self.state == hstate.HypervisorState.DISABLED.value:
|
||||
target_state = False
|
||||
elif self.status == hstate.HypervisorState.ONLINE.value:
|
||||
elif self.state == hstate.HypervisorState.ENABLED.value:
|
||||
target_state = True
|
||||
return self.nova_manage_service(target_state)
|
||||
return self._nova_manage_service(target_state)
|
||||
|
||||
def revert(self):
|
||||
target_state = None
|
||||
if self.state == hstate.HypervisorState.OFFLINE.value:
|
||||
if self.state == hstate.HypervisorState.DISABLED.value:
|
||||
target_state = True
|
||||
elif self.state == hstate.HypervisorState.ONLINE.value:
|
||||
elif self.state == hstate.HypervisorState.ENABLED.value:
|
||||
target_state = False
|
||||
return self.nova_manage_service(target_state)
|
||||
return self._nova_manage_service(target_state)
|
||||
|
||||
def nova_manage_service(self, state):
|
||||
def _nova_manage_service(self, state):
|
||||
if state is None:
|
||||
raise exception.IllegalArgumentException(
|
||||
message=_("The target state is not defined"))
|
||||
|
||||
keystone = kclient.KeystoneClient()
|
||||
wrapper = nclient.NovaClient(keystone.get_credentials(),
|
||||
session=keystone.get_session())
|
||||
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||
if state is True:
|
||||
return wrapper.enable_service_nova_compute(self.host)
|
||||
return nova.enable_service_nova_compute(self.host)
|
||||
else:
|
||||
return wrapper.disable_service_nova_compute(self.host)
|
||||
return nova.disable_service_nova_compute(self.host)
|
||||
|
||||
def precondition(self):
|
||||
pass
|
||||
|
||||
@@ -28,9 +28,15 @@ class ActionFactory(object):
|
||||
def __init__(self):
|
||||
self.action_loader = default.DefaultActionLoader()
|
||||
|
||||
def make_action(self, object_action):
|
||||
def make_action(self, object_action, osc=None):
|
||||
LOG.debug("Creating instance of %s", object_action.action_type)
|
||||
loaded_action = self.action_loader.load(name=object_action.action_type)
|
||||
loaded_action = self.action_loader.load(name=object_action.action_type,
|
||||
osc=osc)
|
||||
loaded_action.input_parameters = object_action.input_parameters
|
||||
loaded_action.applies_to = object_action.applies_to
|
||||
LOG.debug("Checking the input parameters")
|
||||
# NOTE(jed) if we change the schema of an action and we try to reload
|
||||
# an older version of the Action, the validation can fail.
|
||||
# We need to add the versioning of an Action or a migration tool.
|
||||
# We can also create an new Action which extends the previous one.
|
||||
loaded_action.validate_parameters()
|
||||
return loaded_action
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.loader import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultActionLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
|
||||
@@ -18,45 +18,143 @@
|
||||
#
|
||||
|
||||
from oslo_log import log
|
||||
import six
|
||||
import voluptuous
|
||||
|
||||
from watcher._i18n import _, _LC
|
||||
from watcher.applier.actions import base
|
||||
from watcher.common import exception
|
||||
from watcher.common import keystone as kclient
|
||||
from watcher.common import nova as nclient
|
||||
from watcher.common import nova_helper
|
||||
from watcher.common import utils
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Migrate(base.BaseAction):
|
||||
"""Migrates a server to a destination nova-compute host
|
||||
|
||||
This action will allow you to migrate a server to another compute
|
||||
destination host.
|
||||
Migration type 'live' can only be used for migrating active VMs.
|
||||
Migration type 'cold' can be used for migrating non-active VMs
|
||||
as well active VMs, which will be shut down while migrating.
|
||||
|
||||
The action schema is::
|
||||
|
||||
schema = Schema({
|
||||
'resource_id': str, # should be a UUID
|
||||
'migration_type': str, # choices -> "live", "cold"
|
||||
'dst_hypervisor': str,
|
||||
'src_hypervisor': str,
|
||||
})
|
||||
|
||||
The `resource_id` is the UUID of the server to migrate.
|
||||
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
|
||||
source and the destination compute hostname (list of available compute
|
||||
hosts is returned by this command: ``nova service-list --binary
|
||||
nova-compute``).
|
||||
"""
|
||||
|
||||
# input parameters constants
|
||||
MIGRATION_TYPE = 'migration_type'
|
||||
LIVE_MIGRATION = 'live'
|
||||
COLD_MIGRATION = 'cold'
|
||||
DST_HYPERVISOR = 'dst_hypervisor'
|
||||
SRC_HYPERVISOR = 'src_hypervisor'
|
||||
|
||||
def check_resource_id(self, value):
|
||||
if (value is not None and
|
||||
len(value) > 0 and not
|
||||
utils.is_uuid_like(value)):
|
||||
raise voluptuous.Invalid(_("The parameter"
|
||||
" resource_id is invalid."))
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema({
|
||||
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
||||
voluptuous.Required(self.MIGRATION_TYPE,
|
||||
default=self.LIVE_MIGRATION):
|
||||
voluptuous.Any(*[self.LIVE_MIGRATION,
|
||||
self.COLD_MIGRATION]),
|
||||
voluptuous.Required(self.DST_HYPERVISOR):
|
||||
voluptuous.All(voluptuous.Any(*six.string_types),
|
||||
voluptuous.Length(min=1)),
|
||||
voluptuous.Required(self.SRC_HYPERVISOR):
|
||||
voluptuous.All(voluptuous.Any(*six.string_types),
|
||||
voluptuous.Length(min=1)),
|
||||
})
|
||||
|
||||
@property
|
||||
def instance_uuid(self):
|
||||
return self.applies_to
|
||||
return self.resource_id
|
||||
|
||||
@property
|
||||
def migration_type(self):
|
||||
return self.input_parameters.get('migration_type')
|
||||
return self.input_parameters.get(self.MIGRATION_TYPE)
|
||||
|
||||
@property
|
||||
def dst_hypervisor(self):
|
||||
return self.input_parameters.get('dst_hypervisor')
|
||||
return self.input_parameters.get(self.DST_HYPERVISOR)
|
||||
|
||||
@property
|
||||
def src_hypervisor(self):
|
||||
return self.input_parameters.get('src_hypervisor')
|
||||
return self.input_parameters.get(self.SRC_HYPERVISOR)
|
||||
|
||||
def _live_migrate_instance(self, nova, destination):
|
||||
result = None
|
||||
try:
|
||||
result = nova.live_migrate_instance(instance_id=self.instance_uuid,
|
||||
dest_hostname=destination)
|
||||
except nova_helper.nvexceptions.ClientException as e:
|
||||
if e.code == 400:
|
||||
LOG.debug("Live migration of instance %s failed. "
|
||||
"Trying to live migrate using block migration."
|
||||
% self.instance_uuid)
|
||||
result = nova.live_migrate_instance(
|
||||
instance_id=self.instance_uuid,
|
||||
dest_hostname=destination,
|
||||
block_migration=True)
|
||||
else:
|
||||
LOG.debug("Nova client exception occured while live migrating "
|
||||
"instance %s.Exception: %s" %
|
||||
(self.instance_uuid, e))
|
||||
except Exception:
|
||||
LOG.critical(_LC("Unexpected error occured. Migration failed for"
|
||||
"instance %s. Leaving instance on previous "
|
||||
"host."), self.instance_uuid)
|
||||
|
||||
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):
|
||||
keystone = kclient.KeystoneClient()
|
||||
wrapper = nclient.NovaClient(keystone.get_credentials(),
|
||||
session=keystone.get_session())
|
||||
LOG.debug("Migrate instance %s to %s ", self.instance_uuid,
|
||||
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
|
||||
destination)
|
||||
instance = wrapper.find_instance(self.instance_uuid)
|
||||
instance = nova.find_instance(self.instance_uuid)
|
||||
if instance:
|
||||
if self.migration_type == 'live':
|
||||
return wrapper.live_migrate_instance(
|
||||
instance_id=self.instance_uuid, dest_hostname=destination)
|
||||
if self.migration_type == self.LIVE_MIGRATION:
|
||||
return self._live_migrate_instance(nova, destination)
|
||||
elif self.migration_type == self.COLD_MIGRATION:
|
||||
return self._cold_migrate_instance(nova, destination)
|
||||
else:
|
||||
raise exception.InvalidParameterValue(err=self.migration_type)
|
||||
raise exception.Invalid(
|
||||
message=(_('Migration of type %(migration_type)s is not '
|
||||
'supported.') %
|
||||
{'migration_type': self.migration_type}))
|
||||
else:
|
||||
raise exception.InstanceNotFound(name=self.instance_uuid)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#
|
||||
|
||||
from oslo_log import log
|
||||
import six
|
||||
import voluptuous
|
||||
|
||||
from watcher.applier.actions import base
|
||||
|
||||
@@ -26,10 +28,29 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Nop(base.BaseAction):
|
||||
"""logs a message
|
||||
|
||||
The action schema is::
|
||||
|
||||
schema = Schema({
|
||||
'message': str,
|
||||
})
|
||||
|
||||
The `message` is the actual message that will be logged.
|
||||
"""
|
||||
|
||||
MESSAGE = 'message'
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema({
|
||||
voluptuous.Required(self.MESSAGE): voluptuous.Any(
|
||||
voluptuous.Any(*six.string_types), None)
|
||||
})
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return self.input_parameters.get('message')
|
||||
return self.input_parameters.get(self.MESSAGE)
|
||||
|
||||
def execute(self):
|
||||
LOG.debug("executing action NOP message:%s ", self.message)
|
||||
|
||||
@@ -18,19 +18,39 @@
|
||||
#
|
||||
import time
|
||||
|
||||
|
||||
from oslo_log import log
|
||||
import voluptuous
|
||||
|
||||
from watcher.applier.actions import base
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Sleep(base.BaseAction):
|
||||
"""Makes the executor of the action plan wait for a given duration
|
||||
|
||||
The action schema is::
|
||||
|
||||
schema = Schema({
|
||||
'duration': float,
|
||||
})
|
||||
|
||||
The `duration` is expressed in seconds.
|
||||
"""
|
||||
|
||||
DURATION = 'duration'
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema({
|
||||
voluptuous.Required(self.DURATION, default=1):
|
||||
voluptuous.All(float, voluptuous.Range(min=0))
|
||||
})
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
return int(self.input_parameters.get('duration'))
|
||||
return int(self.input_parameters.get(self.DURATION))
|
||||
|
||||
def execute(self):
|
||||
LOG.debug("Starting action Sleep duration:%s ", self.duration)
|
||||
|
||||
@@ -28,7 +28,7 @@ CONF = cfg.CONF
|
||||
|
||||
|
||||
class DefaultApplier(base.BaseApplier):
|
||||
def __init__(self, applier_manager, context):
|
||||
def __init__(self, context, applier_manager):
|
||||
super(DefaultApplier, self).__init__()
|
||||
self._applier_manager = applier_manager
|
||||
self._loader = default.DefaultWorkFlowEngineLoader()
|
||||
@@ -48,9 +48,10 @@ class DefaultApplier(base.BaseApplier):
|
||||
if self._engine is None:
|
||||
selected_workflow_engine = CONF.watcher_applier.workflow_engine
|
||||
LOG.debug("Loading workflow engine %s ", selected_workflow_engine)
|
||||
self._engine = self._loader.load(name=selected_workflow_engine)
|
||||
self._engine.context = self.context
|
||||
self._engine.applier_manager = self.applier_manager
|
||||
self._engine = self._loader.load(
|
||||
name=selected_workflow_engine,
|
||||
context=self.context,
|
||||
applier_manager=self.applier_manager)
|
||||
return self._engine
|
||||
|
||||
def execute(self, action_plan_uuid):
|
||||
|
||||
@@ -21,7 +21,6 @@ from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.messaging import trigger
|
||||
from watcher.common.messaging import messaging_core
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
@@ -34,12 +33,12 @@ APPLIER_MANAGER_OPTS = [
|
||||
min=1,
|
||||
required=True,
|
||||
help='Number of workers for applier, default value is 1.'),
|
||||
cfg.StrOpt('topic_control',
|
||||
cfg.StrOpt('conductor_topic',
|
||||
default='watcher.applier.control',
|
||||
help='The topic name used for'
|
||||
'control events, this topic '
|
||||
'used for rpc call '),
|
||||
cfg.StrOpt('topic_status',
|
||||
cfg.StrOpt('status_topic',
|
||||
default='watcher.applier.status',
|
||||
help='The topic name used for '
|
||||
'status events, this topic '
|
||||
@@ -63,16 +62,15 @@ CONF.register_group(opt_group)
|
||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||
|
||||
|
||||
class ApplierManager(messaging_core.MessagingCore):
|
||||
def __init__(self):
|
||||
super(ApplierManager, self).__init__(
|
||||
CONF.watcher_applier.publisher_id,
|
||||
CONF.watcher_applier.topic_control,
|
||||
CONF.watcher_applier.topic_status,
|
||||
api_version=self.API_VERSION,
|
||||
)
|
||||
self.topic_control.add_endpoint(trigger.TriggerActionPlan(self))
|
||||
class ApplierManager(object):
|
||||
|
||||
def join(self):
|
||||
self.topic_control.join()
|
||||
self.topic_status.join()
|
||||
API_VERSION = '1.0'
|
||||
|
||||
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,55 +18,43 @@
|
||||
#
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import oslo_messaging as om
|
||||
|
||||
from watcher.applier.manager import APPLIER_MANAGER_OPTS
|
||||
from watcher.applier.manager import opt_group
|
||||
from watcher.applier import manager
|
||||
from watcher.common import exception
|
||||
from watcher.common.messaging import messaging_core
|
||||
from watcher.common.messaging import notification_handler as notification
|
||||
from watcher.common import service
|
||||
from watcher.common import utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||
CONF.register_group(manager.opt_group)
|
||||
CONF.register_opts(manager.APPLIER_MANAGER_OPTS, manager.opt_group)
|
||||
|
||||
|
||||
class ApplierAPI(messaging_core.MessagingCore):
|
||||
class ApplierAPI(service.Service):
|
||||
|
||||
def __init__(self):
|
||||
super(ApplierAPI, self).__init__(
|
||||
CONF.watcher_applier.publisher_id,
|
||||
CONF.watcher_applier.topic_control,
|
||||
CONF.watcher_applier.topic_status,
|
||||
api_version=self.API_VERSION,
|
||||
)
|
||||
self.handler = notification.NotificationHandler(self.publisher_id)
|
||||
self.handler.register_observer(self)
|
||||
self.topic_status.add_endpoint(self.handler)
|
||||
transport = om.get_transport(CONF)
|
||||
|
||||
target = om.Target(
|
||||
topic=CONF.watcher_applier.topic_control,
|
||||
version=self.API_VERSION,
|
||||
)
|
||||
|
||||
self.client = om.RPCClient(transport, target,
|
||||
serializer=self.serializer)
|
||||
super(ApplierAPI, self).__init__(ApplierAPIManager)
|
||||
|
||||
def launch_action_plan(self, context, action_plan_uuid=None):
|
||||
if not utils.is_uuid_like(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',
|
||||
action_plan_uuid=action_plan_uuid)
|
||||
|
||||
def event_receive(self, event):
|
||||
try:
|
||||
pass
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
raise
|
||||
|
||||
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
|
||||
|
||||
@@ -22,33 +22,53 @@ import six
|
||||
|
||||
from watcher.applier.actions import factory
|
||||
from watcher.applier.messaging import event_types
|
||||
from watcher.common import clients
|
||||
from watcher.common.loader import loadable
|
||||
from watcher.common.messaging.events import event
|
||||
from watcher import objects
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseWorkFlowEngine(object):
|
||||
def __init__(self):
|
||||
self._applier_manager = None
|
||||
self._context = None
|
||||
class BaseWorkFlowEngine(loadable.Loadable):
|
||||
|
||||
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._applier_manager = applier_manager
|
||||
self._action_factory = factory.ActionFactory()
|
||||
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
|
||||
def context(self):
|
||||
return self._context
|
||||
|
||||
@context.setter
|
||||
def context(self, c):
|
||||
self._context = c
|
||||
@property
|
||||
def osc(self):
|
||||
if not self._osc:
|
||||
self._osc = clients.OpenStackClients()
|
||||
return self._osc
|
||||
|
||||
@property
|
||||
def applier_manager(self):
|
||||
return self._applier_manager
|
||||
|
||||
@applier_manager.setter
|
||||
def applier_manager(self, a):
|
||||
self._applier_manager = a
|
||||
|
||||
@property
|
||||
def action_factory(self):
|
||||
return self._action_factory
|
||||
@@ -62,8 +82,8 @@ class BaseWorkFlowEngine(object):
|
||||
ev.data = {}
|
||||
payload = {'action_uuid': action.uuid,
|
||||
'action_state': state}
|
||||
self.applier_manager.topic_status.publish_event(ev.type.name,
|
||||
payload)
|
||||
self.applier_manager.status_topic_handler.publish_event(
|
||||
ev.type.name, payload)
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self, actions):
|
||||
|
||||
@@ -22,12 +22,19 @@ from taskflow import task
|
||||
|
||||
from watcher._i18n import _LE, _LW, _LC
|
||||
from watcher.applier.workflow_engine import base
|
||||
from watcher.common import exception
|
||||
from watcher.objects import action as obj_action
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
|
||||
"""Taskflow as a workflow engine for Watcher
|
||||
|
||||
Full documentation on taskflow at
|
||||
http://docs.openstack.org/developer/taskflow/
|
||||
"""
|
||||
|
||||
def decider(self, history):
|
||||
# FIXME(jed) not possible with the current Watcher Planner
|
||||
#
|
||||
@@ -71,10 +78,9 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
|
||||
|
||||
e = engines.load(flow)
|
||||
e.run()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
return False
|
||||
raise exception.WorkflowExecutionException(error=e)
|
||||
|
||||
|
||||
class TaskFlowActionContainer(task.Task):
|
||||
@@ -89,7 +95,9 @@ class TaskFlowActionContainer(task.Task):
|
||||
@property
|
||||
def action(self):
|
||||
if self.loaded_action is None:
|
||||
action = self.engine.action_factory.make_action(self._db_action)
|
||||
action = self.engine.action_factory.make_action(
|
||||
self._db_action,
|
||||
osc=self._engine.osc)
|
||||
self.loaded_action = action
|
||||
return self.loaded_action
|
||||
|
||||
@@ -113,14 +121,9 @@ class TaskFlowActionContainer(task.Task):
|
||||
try:
|
||||
LOG.debug("Running action %s", self.name)
|
||||
|
||||
# todo(jed) remove return (true or false) raise an Exception
|
||||
result = self.action.execute()
|
||||
if result is not True:
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.FAILED)
|
||||
else:
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.SUCCEEDED)
|
||||
self.action.execute()
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.SUCCEEDED)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
LOG.error(_LE('The WorkFlow Engine has failed '
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.loader import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultWorkFlowEngineLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
|
||||
@@ -17,19 +17,14 @@
|
||||
|
||||
"""Starter script for the Watcher API service."""
|
||||
|
||||
import logging as std_logging
|
||||
import os
|
||||
import sys
|
||||
from wsgiref import simple_server
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.api import app as api_app
|
||||
from watcher._i18n import _LI
|
||||
from watcher.common import service
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
@@ -37,22 +32,20 @@ CONF = cfg.CONF
|
||||
def main():
|
||||
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
|
||||
srv = simple_server.make_server(host, port, app)
|
||||
|
||||
LOG.info(_('Starting server in PID %s') % os.getpid())
|
||||
LOG.debug("Watcher configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
protocol = "http" if not CONF.api.enable_ssl_api else "https"
|
||||
# Build and start the WSGI app
|
||||
server = service.WSGIService(
|
||||
'watcher-api', CONF.api.enable_ssl_api)
|
||||
|
||||
if host == '0.0.0.0':
|
||||
LOG.info(_('serving on 0.0.0.0:%(port)s, '
|
||||
'view at http://127.0.0.1:%(port)s') %
|
||||
dict(port=port))
|
||||
LOG.info(_LI('serving on 0.0.0.0:%(port)s, '
|
||||
'view at %(protocol)s://127.0.0.1:%(port)s') %
|
||||
dict(protocol=protocol, port=port))
|
||||
else:
|
||||
LOG.info(_('serving on http://%(host)s:%(port)s') %
|
||||
dict(host=host, port=port))
|
||||
LOG.info(_LI('serving on %(protocol)s://%(host)s:%(port)s') %
|
||||
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."""
|
||||
|
||||
import logging as std_logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
|
||||
from watcher import _i18n
|
||||
from watcher.applier.manager import ApplierManager
|
||||
from watcher.common import service
|
||||
from watcher._i18n import _LI
|
||||
from watcher.applier import manager
|
||||
from watcher.common import service as watcher_service
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
_LI = _i18n._LI
|
||||
|
||||
|
||||
def main():
|
||||
service.prepare_service(sys.argv)
|
||||
watcher_service.prepare_service(sys.argv)
|
||||
|
||||
LOG.info(_LI('Starting server in PID %s') % os.getpid())
|
||||
LOG.debug("Configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
LOG.info(_LI('Starting Watcher Applier service in PID %s'), os.getpid())
|
||||
|
||||
server = ApplierManager()
|
||||
server.connect()
|
||||
server.join()
|
||||
applier_service = watcher_service.Service(manager.ApplierManager)
|
||||
launcher = service.launch(CONF, applier_service)
|
||||
launcher.wait()
|
||||
|
||||
@@ -25,7 +25,7 @@ from oslo_config import cfg
|
||||
|
||||
from watcher.common import service
|
||||
from watcher.db import migration
|
||||
|
||||
from watcher.db import purge
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@@ -56,6 +56,12 @@ class DBCommand(object):
|
||||
def create_schema():
|
||||
migration.create_schema()
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
purge.purge(CONF.command.age_in_days, CONF.command.max_number,
|
||||
CONF.command.audit_template, CONF.command.exclude_orphans,
|
||||
CONF.command.dry_run)
|
||||
|
||||
|
||||
def add_command_parsers(subparsers):
|
||||
parser = subparsers.add_parser(
|
||||
@@ -96,6 +102,33 @@ def add_command_parsers(subparsers):
|
||||
help="Create the database schema.")
|
||||
parser.set_defaults(func=DBCommand.create_schema)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'purge',
|
||||
help="Purge the database.")
|
||||
parser.add_argument('-d', '--age-in-days',
|
||||
help="Number of days since deletion (from today) "
|
||||
"to exclude from the purge. If None, everything "
|
||||
"will be purged.",
|
||||
type=int, default=None, nargs='?')
|
||||
parser.add_argument('-n', '--max-number',
|
||||
help="Max number of objects expected to be deleted. "
|
||||
"Prevents the deletion if exceeded. No limit if "
|
||||
"set to None.",
|
||||
type=int, default=None, nargs='?')
|
||||
parser.add_argument('-t', '--audit-template',
|
||||
help="UUID or name of the audit template to purge.",
|
||||
type=str, default=None, nargs='?')
|
||||
parser.add_argument('-e', '--exclude-orphans', action='store_true',
|
||||
help="Flag to indicate whether or not you want to "
|
||||
"exclude orphans from deletion (default: False).",
|
||||
default=False)
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help="Flag to indicate whether or not you want to "
|
||||
"perform a dry run (no deletion).",
|
||||
default=False)
|
||||
|
||||
parser.set_defaults(func=DBCommand.purge)
|
||||
|
||||
|
||||
command_opt = cfg.SubCommandOpt('command',
|
||||
title='Command',
|
||||
@@ -114,6 +147,7 @@ def main():
|
||||
valid_commands = set([
|
||||
'upgrade', 'downgrade', 'revision',
|
||||
'version', 'stamp', 'create_schema',
|
||||
'purge',
|
||||
])
|
||||
if not set(sys.argv).intersection(valid_commands):
|
||||
sys.argv.append('upgrade')
|
||||
|
||||
@@ -17,30 +17,31 @@
|
||||
|
||||
"""Starter script for the Decision Engine manager service."""
|
||||
|
||||
import logging as std_logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
|
||||
from watcher import _i18n
|
||||
from watcher.common import service
|
||||
from watcher.decision_engine.manager import DecisionEngineManager
|
||||
|
||||
from watcher._i18n import _LI
|
||||
from watcher.common import service as watcher_service
|
||||
from watcher.decision_engine import manager
|
||||
from watcher.decision_engine import sync
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
_LI = _i18n._LI
|
||||
|
||||
|
||||
def main():
|
||||
service.prepare_service(sys.argv)
|
||||
watcher_service.prepare_service(sys.argv)
|
||||
|
||||
LOG.info(_LI('Starting server in PID %s') % os.getpid())
|
||||
LOG.debug("Configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
LOG.info(_LI('Starting Watcher Decision Engine service in PID %s'),
|
||||
os.getpid())
|
||||
|
||||
server = DecisionEngineManager()
|
||||
server.connect()
|
||||
server.join()
|
||||
syncer = sync.Syncer()
|
||||
syncer.sync()
|
||||
|
||||
de_service = watcher_service.Service(manager.DecisionEngineManager)
|
||||
launcher = service.launch(CONF, de_service)
|
||||
launcher.wait()
|
||||
|
||||
@@ -17,30 +17,16 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from ceilometerclient import client
|
||||
from ceilometerclient.exc import HTTPUnauthorized
|
||||
|
||||
from watcher.common import keystone
|
||||
from watcher.common import clients
|
||||
|
||||
|
||||
class CeilometerClient(object):
|
||||
def __init__(self, api_version='2'):
|
||||
self._cmclient = None
|
||||
self._api_version = api_version
|
||||
|
||||
@property
|
||||
def cmclient(self):
|
||||
"""Initialization of Ceilometer client."""
|
||||
if not self._cmclient:
|
||||
ksclient = keystone.KeystoneClient()
|
||||
creds = ksclient.get_credentials()
|
||||
endpoint = ksclient.get_endpoint(
|
||||
service_type='metering',
|
||||
endpoint_type='publicURL')
|
||||
self._cmclient = client.get_client(self._api_version,
|
||||
ceilometer_url=endpoint,
|
||||
**creds)
|
||||
return self._cmclient
|
||||
class CeilometerHelper(object):
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
self.ceilometer = self.osc.ceilometer()
|
||||
|
||||
def build_query(self, user_id=None, tenant_id=None, resource_id=None,
|
||||
user_ids=None, tenant_ids=None, resource_ids=None):
|
||||
@@ -83,20 +69,21 @@ class CeilometerClient(object):
|
||||
try:
|
||||
return f(*args, **kargs)
|
||||
except HTTPUnauthorized:
|
||||
self.reset_client()
|
||||
self.osc.reset_clients()
|
||||
self.ceilometer = self.osc.ceilometer()
|
||||
return f(*args, **kargs)
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
def query_sample(self, meter_name, query, limit=1):
|
||||
return self.query_retry(f=self.cmclient.samples.list,
|
||||
return self.query_retry(f=self.ceilometer.samples.list,
|
||||
meter_name=meter_name,
|
||||
limit=limit,
|
||||
q=query)
|
||||
|
||||
def statistic_list(self, meter_name, query=None, period=None):
|
||||
"""List of statistics."""
|
||||
statistics = self.cmclient.statistics.list(
|
||||
statistics = self.ceilometer.statistics.list(
|
||||
meter_name=meter_name,
|
||||
q=query,
|
||||
period=period)
|
||||
@@ -104,7 +91,8 @@ class CeilometerClient(object):
|
||||
|
||||
def meter_list(self, query=None):
|
||||
"""List the user's meters."""
|
||||
meters = self.query_retry(f=self.cmclient.meters.list, query=query)
|
||||
meters = self.query_retry(f=self.ceilometer.meters.list,
|
||||
query=query)
|
||||
return meters
|
||||
|
||||
def statistic_aggregation(self,
|
||||
@@ -125,7 +113,7 @@ class CeilometerClient(object):
|
||||
"""
|
||||
|
||||
query = self.build_query(resource_id=resource_id)
|
||||
statistic = self.query_retry(f=self.cmclient.statistics.list,
|
||||
statistic = self.query_retry(f=self.ceilometer.statistics.list,
|
||||
meter_name=meter_name,
|
||||
q=query,
|
||||
period=period,
|
||||
@@ -135,12 +123,13 @@ class CeilometerClient(object):
|
||||
|
||||
item_value = None
|
||||
if statistic:
|
||||
item_value = statistic[-1]._info.get('aggregate').get('avg')
|
||||
item_value = statistic[-1]._info.get('aggregate').get(aggregate)
|
||||
return item_value
|
||||
|
||||
def get_last_sample_values(self, resource_id, meter_name, limit=1):
|
||||
samples = self.query_sample(meter_name=meter_name,
|
||||
query=self.build_query(resource_id))
|
||||
query=self.build_query(resource_id),
|
||||
limit=limit)
|
||||
values = []
|
||||
for index, sample in enumerate(samples):
|
||||
values.append(
|
||||
@@ -156,6 +145,3 @@ class CeilometerClient(object):
|
||||
return samples[-1]._info['counter_volume']
|
||||
else:
|
||||
return False
|
||||
|
||||
def reset_client(self):
|
||||
self._cmclient = None
|
||||
158
watcher/common/clients.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# 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 ceilometerclient import client as ceclient
|
||||
from cinderclient import client as ciclient
|
||||
from glanceclient import client as glclient
|
||||
from keystoneauth1 import loading as ka_loading
|
||||
from keystoneclient import client as keyclient
|
||||
from neutronclient.neutron import client as netclient
|
||||
from novaclient import client as nvclient
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
|
||||
|
||||
NOVA_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='2',
|
||||
help=_('Version of Nova API to use in novaclient.'))]
|
||||
|
||||
GLANCE_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='2',
|
||||
help=_('Version of Glance API to use in glanceclient.'))]
|
||||
|
||||
CINDER_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='2',
|
||||
help=_('Version of Cinder API to use in cinderclient.'))]
|
||||
|
||||
CEILOMETER_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='2',
|
||||
help=_('Version of Ceilometer API to use in '
|
||||
'ceilometerclient.'))]
|
||||
|
||||
NEUTRON_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='2.0',
|
||||
help=_('Version of Neutron API to use in neutronclient.'))]
|
||||
|
||||
cfg.CONF.register_opts(NOVA_CLIENT_OPTS, group='nova_client')
|
||||
cfg.CONF.register_opts(GLANCE_CLIENT_OPTS, group='glance_client')
|
||||
cfg.CONF.register_opts(CINDER_CLIENT_OPTS, group='cinder_client')
|
||||
cfg.CONF.register_opts(CEILOMETER_CLIENT_OPTS, group='ceilometer_client')
|
||||
cfg.CONF.register_opts(NEUTRON_CLIENT_OPTS, group='neutron_client')
|
||||
|
||||
_CLIENTS_AUTH_GROUP = 'watcher_clients_auth'
|
||||
|
||||
ka_loading.register_auth_conf_options(cfg.CONF, _CLIENTS_AUTH_GROUP)
|
||||
ka_loading.register_session_conf_options(cfg.CONF, _CLIENTS_AUTH_GROUP)
|
||||
|
||||
|
||||
class OpenStackClients(object):
|
||||
"""Convenience class to create and cache client instances."""
|
||||
|
||||
def __init__(self):
|
||||
self.reset_clients()
|
||||
|
||||
def reset_clients(self):
|
||||
self._session = None
|
||||
self._keystone = None
|
||||
self._nova = None
|
||||
self._glance = None
|
||||
self._cinder = None
|
||||
self._ceilometer = None
|
||||
self._neutron = None
|
||||
|
||||
def _get_keystone_session(self):
|
||||
auth = ka_loading.load_auth_from_conf_options(cfg.CONF,
|
||||
_CLIENTS_AUTH_GROUP)
|
||||
sess = ka_loading.load_session_from_conf_options(cfg.CONF,
|
||||
_CLIENTS_AUTH_GROUP,
|
||||
auth=auth)
|
||||
return sess
|
||||
|
||||
@property
|
||||
def auth_url(self):
|
||||
return self.keystone().auth_url
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
if not self._session:
|
||||
self._session = self._get_keystone_session()
|
||||
return self._session
|
||||
|
||||
def _get_client_option(self, client, option):
|
||||
return getattr(getattr(cfg.CONF, '%s_client' % client), option)
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def keystone(self):
|
||||
if not self._keystone:
|
||||
self._keystone = keyclient.Client(session=self.session)
|
||||
|
||||
return self._keystone
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def nova(self):
|
||||
if self._nova:
|
||||
return self._nova
|
||||
|
||||
novaclient_version = self._get_client_option('nova', 'api_version')
|
||||
self._nova = nvclient.Client(novaclient_version,
|
||||
session=self.session)
|
||||
return self._nova
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def glance(self):
|
||||
if self._glance:
|
||||
return self._glance
|
||||
|
||||
glanceclient_version = self._get_client_option('glance', 'api_version')
|
||||
self._glance = glclient.Client(glanceclient_version,
|
||||
session=self.session)
|
||||
return self._glance
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def cinder(self):
|
||||
if self._cinder:
|
||||
return self._cinder
|
||||
|
||||
cinderclient_version = self._get_client_option('cinder', 'api_version')
|
||||
self._cinder = ciclient.Client(cinderclient_version,
|
||||
session=self.session)
|
||||
return self._cinder
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def ceilometer(self):
|
||||
if self._ceilometer:
|
||||
return self._ceilometer
|
||||
|
||||
ceilometerclient_version = self._get_client_option('ceilometer',
|
||||
'api_version')
|
||||
self._ceilometer = ceclient.get_client(ceilometerclient_version,
|
||||
session=self.session)
|
||||
return self._ceilometer
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def neutron(self):
|
||||
if self._neutron:
|
||||
return self._neutron
|
||||
|
||||
neutronclient_version = self._get_client_option('neutron',
|
||||
'api_version')
|
||||
self._neutron = netclient.Client(neutronclient_version,
|
||||
session=self.session)
|
||||
self._neutron.format = 'json'
|
||||
return self._neutron
|
||||
@@ -22,6 +22,8 @@ from watcher import version
|
||||
|
||||
|
||||
def parse_args(argv, default_config_files=None):
|
||||
default_config_files = (default_config_files or
|
||||
cfg.find_config_files(project='watcher'))
|
||||
rpc.set_defaults(control_exchange='watcher')
|
||||
cfg.CONF(argv[1:],
|
||||
project='python-watcher',
|
||||
|
||||
@@ -22,6 +22,10 @@ SHOULD include dedicated exception logging.
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
import sys
|
||||
|
||||
from keystoneclient import exceptions as keystone_exceptions
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
@@ -40,6 +44,23 @@ CONF = cfg.CONF
|
||||
CONF.register_opts(exc_log_opts)
|
||||
|
||||
|
||||
def wrap_keystone_exception(func):
|
||||
"""Wrap keystone exceptions and throw Watcher specific exceptions."""
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kw):
|
||||
try:
|
||||
return func(*args, **kw)
|
||||
except keystone_exceptions.AuthorizationFailure:
|
||||
raise AuthorizationFailure(
|
||||
client=func.__name__, reason=sys.exc_info()[1])
|
||||
except keystone_exceptions.ClientException:
|
||||
raise AuthorizationFailure(
|
||||
client=func.__name__,
|
||||
reason=(_('Unexpected keystone client error occurred: %s')
|
||||
% sys.exc_info()[1]))
|
||||
return wrapped
|
||||
|
||||
|
||||
class WatcherException(Exception):
|
||||
"""Base Watcher Exception
|
||||
|
||||
@@ -126,17 +147,15 @@ class ResourceNotFound(ObjectNotFound):
|
||||
|
||||
|
||||
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):
|
||||
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file")
|
||||
msg_fmt = _("Goal %(goal)s is invalid")
|
||||
|
||||
|
||||
# Cannot be templated as the error syntax varies.
|
||||
# msg needs to be constructed when raised.
|
||||
class InvalidParameterValue(Invalid):
|
||||
msg_fmt = _("%(err)s")
|
||||
class InvalidStrategy(Invalid):
|
||||
msg_fmt = _("Strategy %(strategy)s is invalid")
|
||||
|
||||
|
||||
class InvalidUUID(Invalid):
|
||||
@@ -151,12 +170,28 @@ class InvalidUuidOrName(Invalid):
|
||||
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):
|
||||
msg_fmt = _("AuditTemplate %(audit_template)s could not be found")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
@@ -165,6 +200,10 @@ class AuditTemplateReferenced(Invalid):
|
||||
"multiple audit")
|
||||
|
||||
|
||||
class AuditTypeNotFound(Invalid):
|
||||
msg_fmt = _("Audit type %(audit_type)s could not be found")
|
||||
|
||||
|
||||
class AuditNotFound(ResourceNotFound):
|
||||
msg_fmt = _("Audit %(audit)s could not be found")
|
||||
|
||||
@@ -179,7 +218,7 @@ class AuditReferenced(Invalid):
|
||||
|
||||
|
||||
class ActionPlanNotFound(ResourceNotFound):
|
||||
msg_fmt = _("ActionPlan %(action plan)s could not be found")
|
||||
msg_fmt = _("ActionPlan %(action_plan)s could not be found")
|
||||
|
||||
|
||||
class ActionPlanAlreadyExists(Conflict):
|
||||
@@ -219,6 +258,9 @@ class PatchError(Invalid):
|
||||
|
||||
# decision engine
|
||||
|
||||
class WorkflowExecutionException(WatcherException):
|
||||
msg_fmt = _('Workflow execution error: %(error)s')
|
||||
|
||||
|
||||
class IllegalArgumentException(WatcherException):
|
||||
msg_fmt = _('Illegal argument')
|
||||
@@ -232,6 +274,10 @@ class NoDataFound(WatcherException):
|
||||
msg_fmt = _('No rows were returned')
|
||||
|
||||
|
||||
class AuthorizationFailure(WatcherException):
|
||||
msg_fmt = _('%(client)s connection failed. Reason: %(reason)s')
|
||||
|
||||
|
||||
class KeystoneFailure(WatcherException):
|
||||
msg_fmt = _("'Keystone API endpoint is missing''")
|
||||
|
||||
@@ -245,7 +291,19 @@ class MetricCollectorNotDefined(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
|
||||
@@ -260,3 +318,15 @@ class HypervisorNotFound(WatcherException):
|
||||
|
||||
class LoadingError(WatcherException):
|
||||
msg_fmt = _("Error loading plugin '%(name)s'")
|
||||
|
||||
|
||||
class ReservedWord(WatcherException):
|
||||
msg_fmt = _("The identifier '%(name)s' is a reserved word")
|
||||
|
||||
|
||||
class NotSoftDeletedStateError(WatcherException):
|
||||
msg_fmt = _("The %(name)s resource %(id)s is not soft deleted")
|
||||
|
||||
|
||||
class NegativeLimitError(WatcherException):
|
||||
msg_fmt = _("Limit should be positive")
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from keystoneclient.auth.identity import generic
|
||||
from keystoneclient import session as keystone_session
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from six.moves.urllib.parse import urljoin
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('admin_password', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('auth_version', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('insecure', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
|
||||
|
||||
class KeystoneClient(object):
|
||||
def __init__(self):
|
||||
self._ks_client = None
|
||||
self._session = None
|
||||
self._auth = None
|
||||
self._token = None
|
||||
|
||||
def get_endpoint(self, **kwargs):
|
||||
kc = self.get_ksclient()
|
||||
if not kc.has_service_catalog():
|
||||
raise exception.KeystoneFailure(
|
||||
_('No Keystone service catalog loaded')
|
||||
)
|
||||
attr = None
|
||||
filter_value = None
|
||||
if kwargs.get('region_name'):
|
||||
attr = 'region'
|
||||
filter_value = kwargs.get('region_name')
|
||||
return kc.service_catalog.url_for(
|
||||
service_type=kwargs.get('service_type') or 'metering',
|
||||
attr=attr,
|
||||
filter_value=filter_value,
|
||||
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
|
||||
|
||||
def _is_apiv3(self, auth_url, auth_version):
|
||||
return auth_version == 'v3.0' or '/v3' in urlparse(auth_url).path
|
||||
|
||||
def get_keystone_url(self, auth_url, auth_version):
|
||||
"""Gives an http/https url to contact keystone."""
|
||||
|
||||
api_v3 = self._is_apiv3(auth_url, auth_version)
|
||||
api_version = 'v3' if api_v3 else 'v2.0'
|
||||
# NOTE(lucasagomes): Get rid of the trailing '/' otherwise urljoin()
|
||||
# fails to override the version in the URL
|
||||
return urljoin(auth_url.rstrip('/'), api_version)
|
||||
|
||||
def get_ksclient(self, creds=None):
|
||||
"""Get an endpoint and auth token from Keystone."""
|
||||
auth_version = CONF.keystone_authtoken.auth_version
|
||||
auth_url = CONF.keystone_authtoken.auth_uri
|
||||
api_v3 = self._is_apiv3(auth_url, auth_version)
|
||||
if creds is None:
|
||||
ks_args = self._get_credentials(api_v3)
|
||||
else:
|
||||
ks_args = creds
|
||||
|
||||
if api_v3:
|
||||
from keystoneclient.v3 import client
|
||||
else:
|
||||
from keystoneclient.v2_0 import client
|
||||
# generic
|
||||
# ksclient = client.Client(version=api_version,
|
||||
# session=session,**ks_args)
|
||||
|
||||
return client.Client(**ks_args)
|
||||
|
||||
def _get_credentials(self, api_v3):
|
||||
if api_v3:
|
||||
creds = \
|
||||
{'auth_url': CONF.keystone_authtoken.auth_uri,
|
||||
'username': CONF.keystone_authtoken.admin_user,
|
||||
'password': CONF.keystone_authtoken.admin_password,
|
||||
'project_name': CONF.keystone_authtoken.admin_tenant_name,
|
||||
'user_domain_name': "default",
|
||||
'project_domain_name': "default"}
|
||||
else:
|
||||
creds = \
|
||||
{'auth_url': CONF.keystone_authtoken.auth_uri,
|
||||
'username': CONF.keystone_authtoken.admin_user,
|
||||
'password': CONF.keystone_authtoken.admin_password,
|
||||
'tenant_name': CONF.keystone_authtoken.admin_tenant_name}
|
||||
LOG.debug(creds)
|
||||
return creds
|
||||
|
||||
def get_credentials(self):
|
||||
api_v3 = self._is_apiv3(CONF.keystone_authtoken.auth_uri,
|
||||
CONF.keystone_authtoken.auth_version)
|
||||
return self._get_credentials(api_v3)
|
||||
|
||||
def get_session(self):
|
||||
creds = self.get_credentials()
|
||||
self._auth = generic.Password(**creds)
|
||||
session = keystone_session.Session(auth=self._auth)
|
||||
return session
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
# 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.
|
||||
@@ -16,33 +16,82 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from stevedore.driver import DriverManager
|
||||
from stevedore import ExtensionManager
|
||||
from stevedore import driver as drivermanager
|
||||
from stevedore import extension as extensionmanager
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class DefaultLoader(BaseLoader):
|
||||
def __init__(self, namespace):
|
||||
class DefaultLoader(base.BaseLoader):
|
||||
|
||||
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__()
|
||||
self.namespace = namespace
|
||||
self.conf = conf
|
||||
|
||||
def load(self, name):
|
||||
def load(self, name, **kwargs):
|
||||
try:
|
||||
LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
|
||||
driver_manager = DriverManager(namespace=self.namespace,
|
||||
name=name)
|
||||
loaded = driver_manager.driver
|
||||
driver_manager = drivermanager.DriverManager(
|
||||
namespace=self.namespace,
|
||||
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:
|
||||
LOG.exception(exc)
|
||||
raise exception.LoadingError(name=name)
|
||||
|
||||
return loaded()
|
||||
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):
|
||||
extension_manager = ExtensionManager(namespace=self.namespace)
|
||||
extension_manager = extensionmanager.ExtensionManager(
|
||||
namespace=self.namespace)
|
||||
return {ext.name: ext.plugin for ext in extension_manager.extensions}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
# 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.
|
||||
@@ -13,16 +13,29 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class FakeLoadable(object):
|
||||
@classmethod
|
||||
def namespace(cls):
|
||||
return "TESTING"
|
||||
class Loadable(object):
|
||||
"""Generic interface for dynamically loading a driver/entry point.
|
||||
|
||||
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
|
||||
def get_name(cls):
|
||||
return 'fake'
|
||||
@abc.abstractmethod
|
||||
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
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.decision_engine.messaging.events import Events
|
||||
from watcher.decision_engine.messaging import events as messaging_events
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
@@ -43,8 +43,8 @@ class EventDispatcher(object):
|
||||
"""
|
||||
Dispatch an instance of Event class
|
||||
"""
|
||||
if Events.ALL in self._events.keys():
|
||||
listeners = self._events[Events.ALL]
|
||||
if messaging_events.Events.ALL in self._events.keys():
|
||||
listeners = self._events[messaging_events.Events.ALL]
|
||||
for listener in listeners:
|
||||
listener(event)
|
||||
|
||||
|
||||
@@ -1,80 +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
|
||||
from watcher.common.messaging.events.event_dispatcher import \
|
||||
EventDispatcher
|
||||
from watcher.common.messaging.messaging_handler import \
|
||||
MessagingHandler
|
||||
from watcher.common.rpc import RequestContextSerializer
|
||||
|
||||
from watcher.objects.base import WatcherObjectSerializer
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class MessagingCore(EventDispatcher):
|
||||
|
||||
API_VERSION = '1.0'
|
||||
|
||||
def __init__(self, publisher_id, topic_control, topic_status,
|
||||
api_version=API_VERSION):
|
||||
super(MessagingCore, self).__init__()
|
||||
self.serializer = RequestContextSerializer(WatcherObjectSerializer())
|
||||
self.publisher_id = publisher_id
|
||||
self.api_version = api_version
|
||||
self.topic_control = self.build_topic(topic_control)
|
||||
self.topic_status = self.build_topic(topic_status)
|
||||
|
||||
def build_topic(self, topic_name):
|
||||
return 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.topic_control.start()
|
||||
self.topic_status.start()
|
||||
|
||||
def disconnect(self):
|
||||
LOG.debug("Disconnecting from '%s' (%s)",
|
||||
CONF.transport_url, CONF.rpc_backend)
|
||||
self.topic_control.stop()
|
||||
self.topic_status.stop()
|
||||
|
||||
def publish_control(self, event, payload):
|
||||
return self.topic_control.publish_event(event, payload)
|
||||
|
||||
def publish_status(self, event, payload, request_id=None):
|
||||
return self.topic_status.publish_event(event, payload, request_id)
|
||||
|
||||
def get_version(self):
|
||||
return self.api_version
|
||||
|
||||
def check_api_version(self, context):
|
||||
api_manager_version = self.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,11 +38,11 @@ CONF = cfg.CONF
|
||||
|
||||
class MessagingHandler(threading.Thread):
|
||||
|
||||
def __init__(self, publisher_id, topic_watcher, endpoint, version,
|
||||
def __init__(self, publisher_id, topic_name, endpoints, version,
|
||||
serializer=None):
|
||||
super(MessagingHandler, self).__init__()
|
||||
self.publisher_id = publisher_id
|
||||
self.topic_watcher = topic_watcher
|
||||
self.topic_name = topic_name
|
||||
self.__endpoints = []
|
||||
self.__serializer = serializer
|
||||
self.__version = version
|
||||
@@ -50,10 +50,10 @@ class MessagingHandler(threading.Thread):
|
||||
self.__server = None
|
||||
self.__notifier = None
|
||||
self.__transport = None
|
||||
self.add_endpoint(endpoint)
|
||||
self.add_endpoints(endpoints)
|
||||
|
||||
def add_endpoint(self, endpoint):
|
||||
self.__endpoints.append(endpoint)
|
||||
def add_endpoints(self, endpoints):
|
||||
self.__endpoints.extend(endpoints)
|
||||
|
||||
def remove_endpoint(self, endpoint):
|
||||
if endpoint in self.__endpoints:
|
||||
@@ -72,7 +72,7 @@ class MessagingHandler(threading.Thread):
|
||||
return om.Notifier(
|
||||
self.__transport,
|
||||
publisher_id=self.publisher_id,
|
||||
topic=self.topic_watcher,
|
||||
topic=self.topic_name,
|
||||
serializer=serializer
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ class MessagingHandler(threading.Thread):
|
||||
self.__notifier = self.build_notifier()
|
||||
if len(self.__endpoints):
|
||||
target = om.Target(
|
||||
topic=self.topic_watcher,
|
||||
topic=self.topic_name,
|
||||
# For compatibility, we can override it with 'host' opt
|
||||
server=CONF.host or socket.getfqdn(),
|
||||
version=self.__version,
|
||||
@@ -101,7 +101,7 @@ class MessagingHandler(threading.Thread):
|
||||
LOG.error(_LE("Messaging configuration error"))
|
||||
|
||||
def run(self):
|
||||
LOG.debug("configure MessagingHandler for %s" % self.topic_watcher)
|
||||
LOG.debug("configure MessagingHandler for %s" % self.topic_name)
|
||||
self._configure()
|
||||
if len(self.__endpoints) > 0:
|
||||
LOG.debug("Starting up server")
|
||||
|
||||
@@ -15,20 +15,17 @@
|
||||
# limitations under the License.
|
||||
|
||||
import eventlet
|
||||
from oslo_log import log
|
||||
import oslo_messaging as messaging
|
||||
|
||||
from watcher.common.messaging.utils.observable import \
|
||||
Observable
|
||||
from watcher.common.messaging.utils import observable
|
||||
|
||||
|
||||
eventlet.monkey_patch()
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationHandler(Observable):
|
||||
class NotificationHandler(observable.Observable):
|
||||
def __init__(self, publisher_id):
|
||||
Observable.__init__(self)
|
||||
super(NotificationHandler, self).__init__()
|
||||
self.publisher_id = publisher_id
|
||||
|
||||
def info(self, ctx, publisher_id, event_type, payload, metadata):
|
||||
|
||||
@@ -14,19 +14,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.messaging.utils.synchronization import \
|
||||
Synchronization
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
from watcher.common.messaging.utils import synchronization
|
||||
|
||||
|
||||
class Observable(Synchronization):
|
||||
class Observable(synchronization.Synchronization):
|
||||
def __init__(self):
|
||||
super(Observable, self).__init__()
|
||||
self.__observers = []
|
||||
self.changed = 0
|
||||
Synchronization.__init__(self)
|
||||
|
||||
def set_changed(self):
|
||||
self.changed = 1
|
||||
|
||||
@@ -23,29 +23,22 @@ import time
|
||||
from oslo_log import log
|
||||
|
||||
import cinderclient.exceptions as ciexceptions
|
||||
import cinderclient.v2.client as ciclient
|
||||
import glanceclient.v2.client as glclient
|
||||
import neutronclient.neutron.client as netclient
|
||||
import novaclient.client as nvclient
|
||||
import novaclient.exceptions as nvexceptions
|
||||
|
||||
from watcher.common import keystone
|
||||
from watcher.common import clients
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NovaClient(object):
|
||||
NOVA_CLIENT_API_VERSION = "2"
|
||||
class NovaHelper(object):
|
||||
|
||||
def __init__(self, creds, session):
|
||||
self.user = creds['username']
|
||||
self.session = session
|
||||
self.neutron = None
|
||||
self.cinder = None
|
||||
self.nova = nvclient.Client(self.NOVA_CLIENT_API_VERSION,
|
||||
session=session)
|
||||
self.keystone = keystone.KeystoneClient().get_ksclient(creds)
|
||||
self.glance = None
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
self.neutron = self.osc.neutron()
|
||||
self.cinder = self.osc.cinder()
|
||||
self.nova = self.osc.nova()
|
||||
self.glance = self.osc.glance()
|
||||
|
||||
def get_hypervisors_list(self):
|
||||
return self.nova.hypervisors.list()
|
||||
@@ -94,7 +87,6 @@ class NovaClient(object):
|
||||
return False
|
||||
else:
|
||||
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
|
||||
# https://bugs.launchpad.net/nova/+bug/1182965
|
||||
LOG.debug(
|
||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||
|
||||
@@ -180,9 +172,6 @@ class NovaClient(object):
|
||||
volume_id = attached_volume['id']
|
||||
|
||||
try:
|
||||
if self.cinder is None:
|
||||
self.cinder = ciclient.Client('2',
|
||||
session=self.session)
|
||||
volume = self.cinder.volumes.get(volume_id)
|
||||
|
||||
attachments_list = getattr(volume, "attachments")
|
||||
@@ -275,58 +264,6 @@ class NovaClient(object):
|
||||
|
||||
return True
|
||||
|
||||
def built_in_non_live_migrate_instance(self, instance_id, hypervisor_id):
|
||||
"""This method does a live migration of a given instance
|
||||
|
||||
This method uses the Nova built-in non-live migrate()
|
||||
action to migrate a given instance.
|
||||
It returns True if the migration was successful, False otherwise.
|
||||
|
||||
:param instance_id: the unique id of the instance to migrate.
|
||||
"""
|
||||
|
||||
LOG.debug(
|
||||
"Trying a Nova built-in non-live "
|
||||
"migrate of instance %s ..." % instance_id)
|
||||
|
||||
# Looking for the instance to migrate
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return False
|
||||
else:
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
LOG.debug(
|
||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||
|
||||
instance.migrate()
|
||||
|
||||
# Poll at 5 second intervals, until the status is as expected
|
||||
if self.wait_for_instance_status(instance,
|
||||
('VERIFY_RESIZE', 'ERROR'),
|
||||
5, 10):
|
||||
|
||||
instance = self.nova.servers.get(instance.id)
|
||||
|
||||
if instance.status == 'VERIFY_RESIZE':
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
LOG.debug(
|
||||
"Instance %s has been successfully "
|
||||
"migrated to host '%s'." % (
|
||||
instance_id, host_name))
|
||||
|
||||
# We need to confirm that the resize() operation
|
||||
# has succeeded in order to
|
||||
# get back instance state to 'ACTIVE'
|
||||
instance.confirm_resize()
|
||||
|
||||
return True
|
||||
elif instance.status == 'ERROR':
|
||||
LOG.debug("Instance %s migration failed" % instance_id)
|
||||
|
||||
return False
|
||||
|
||||
def live_migrate_instance(self, instance_id, dest_hostname,
|
||||
block_migration=False, retry=120):
|
||||
"""This method does a live migration of a given instance
|
||||
@@ -446,13 +383,6 @@ class NovaClient(object):
|
||||
:param metadata: a dictionary containing the list of
|
||||
key-value pairs to associate to the image as metadata.
|
||||
"""
|
||||
if self.glance is None:
|
||||
glance_endpoint = self.keystone. \
|
||||
service_catalog.url_for(service_type='image',
|
||||
endpoint_type='publicURL')
|
||||
self.glance = glclient.Client(glance_endpoint,
|
||||
token=self.keystone.auth_token)
|
||||
|
||||
LOG.debug(
|
||||
"Trying to create an image from instance %s ..." % instance_id)
|
||||
|
||||
@@ -601,16 +531,12 @@ class NovaClient(object):
|
||||
"Trying to create new instance '%s' "
|
||||
"from image '%s' with flavor '%s' ..." % (
|
||||
inst_name, image_id, flavor_name))
|
||||
# TODO(jed) wait feature
|
||||
# Allow admin users to view any keypair
|
||||
# https://bugs.launchpad.net/nova/+bug/1182965
|
||||
if not self.nova.keypairs.findall(name=keypair_name):
|
||||
LOG.debug("Key pair '%s' not found with user '%s'" % (
|
||||
keypair_name, self.user))
|
||||
|
||||
try:
|
||||
self.nova.keypairs.findall(name=keypair_name)
|
||||
except nvexceptions.NotFound:
|
||||
LOG.debug("Key pair '%s' not found " % keypair_name)
|
||||
return
|
||||
else:
|
||||
LOG.debug("Key pair '%s' found with user '%s'" % (
|
||||
keypair_name, self.user))
|
||||
|
||||
try:
|
||||
image = self.nova.images.get(image_id)
|
||||
@@ -676,10 +602,6 @@ class NovaClient(object):
|
||||
|
||||
def get_network_id_from_name(self, net_name="private"):
|
||||
"""This method returns the unique id of the provided network name"""
|
||||
if self.neutron is None:
|
||||
self.neutron = netclient.Client('2.0', session=self.session)
|
||||
self.neutron.format = 'json'
|
||||
|
||||
networks = self.neutron.list_networks(name=net_name)
|
||||
|
||||
# LOG.debug(networks)
|
||||
@@ -15,108 +15,46 @@
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import signal
|
||||
import socket
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import _options
|
||||
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_utils import importutils
|
||||
from oslo_service import wsgi
|
||||
|
||||
from watcher._i18n import _LE
|
||||
from watcher._i18n import _LI
|
||||
from watcher._i18n import _
|
||||
from watcher.api import app
|
||||
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.objects import base as objects_base
|
||||
|
||||
from watcher.objects import base
|
||||
from watcher import opts
|
||||
from watcher import version
|
||||
|
||||
service_opts = [
|
||||
cfg.IntOpt('periodic_interval',
|
||||
default=60,
|
||||
help='Seconds between running periodic tasks.'),
|
||||
help=_('Seconds between running periodic tasks.')),
|
||||
cfg.StrOpt('host',
|
||||
default=socket.getfqdn(),
|
||||
help='Name of this node. This can be an opaque identifier. '
|
||||
'It is not 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.'),
|
||||
help=_('Name of this node. This can be an opaque identifier. '
|
||||
'It is not 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.')),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(service_opts)
|
||||
|
||||
CONF = cfg.CONF
|
||||
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, signo, frame):
|
||||
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',
|
||||
'oslo.messaging=INFO', 'sqlalchemy=WARN',
|
||||
'keystoneclient=INFO', 'stevedore=INFO',
|
||||
@@ -125,10 +63,165 @@ _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
|
||||
'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)
|
||||
gmr_opts.set_defaults(conf)
|
||||
|
||||
config.parse_args(argv)
|
||||
cfg.set_defaults(_options.log_opts,
|
||||
default_log_levels=_DEFAULT_LOG_LEVELS)
|
||||
log.setup(conf, 'python-watcher')
|
||||
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__)
|
||||
|
||||
|
||||
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):
|
||||
"""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.
|
||||
|
||||
"""
|
||||
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
|
||||
(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):
|
||||
"""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
|
||||
def get_audit_template_list(self, context, columns=None, filters=None,
|
||||
limit=None, marker=None, sort_key=None,
|
||||
@@ -71,11 +257,11 @@ class BaseConnection(object):
|
||||
'name': 'example',
|
||||
'description': 'free text description'
|
||||
'host_aggregate': 'nova aggregate name or id'
|
||||
'goal': 'SERVER_CONSOLiDATION'
|
||||
'goal': 'DUMMY'
|
||||
'extra': {'automatic': True}
|
||||
}
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateAlreadyExists
|
||||
:raises: :py:class:`~.AuditTemplateAlreadyExists`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -85,7 +271,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param audit_template_id: The id of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -95,16 +281,16 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param audit_template_uuid: The uuid of 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):
|
||||
"""Return an audit template.
|
||||
|
||||
:param context: The security context
|
||||
:param audit_template_name: The name of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -112,7 +298,7 @@ class BaseConnection(object):
|
||||
"""Destroy an audit_template.
|
||||
|
||||
:param audit_template_id: The id or uuid of an audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -121,8 +307,8 @@ class BaseConnection(object):
|
||||
|
||||
:param audit_template_id: The id or uuid of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
:raises: InvalidParameterValue
|
||||
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||
:raises: :py:class:`~.Invalid`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -130,7 +316,7 @@ class BaseConnection(object):
|
||||
"""Soft delete an audit_template.
|
||||
|
||||
:param audit_template_id: The id or uuid of an audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
:raises: :py:class:`~.AuditTemplateNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -171,7 +357,7 @@ class BaseConnection(object):
|
||||
'deadline': None
|
||||
}
|
||||
:returns: An audit.
|
||||
:raises: AuditAlreadyExists
|
||||
:raises: :py:class:`~.AuditAlreadyExists`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -181,7 +367,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param audit_id: The id of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
:raises: :py:class:`~.AuditNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -191,7 +377,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param audit_uuid: The uuid of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
:raises: :py:class:`~.AuditNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -199,7 +385,7 @@ class BaseConnection(object):
|
||||
"""Destroy an audit and all associated action plans.
|
||||
|
||||
:param audit_id: The id or uuid of an audit.
|
||||
:raises: AuditNotFound
|
||||
:raises: :py:class:`~.AuditNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -208,8 +394,8 @@ class BaseConnection(object):
|
||||
|
||||
:param audit_id: The id or uuid of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
:raises: InvalidParameterValue
|
||||
:raises: :py:class:`~.AuditNotFound`
|
||||
:raises: :py:class:`~.Invalid`
|
||||
"""
|
||||
|
||||
def soft_delete_audit(self, audit_id):
|
||||
@@ -217,7 +403,7 @@ class BaseConnection(object):
|
||||
|
||||
:param audit_id: The id or uuid of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
:raises: :py:class:`~.AuditNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -259,7 +445,7 @@ class BaseConnection(object):
|
||||
'aggregate': 'nova aggregate name or uuid'
|
||||
}
|
||||
:returns: A action.
|
||||
:raises: ActionAlreadyExists
|
||||
:raises: :py:class:`~.ActionAlreadyExists`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -269,7 +455,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param action_id: The id of a action.
|
||||
:returns: A action.
|
||||
:raises: ActionNotFound
|
||||
:raises: :py:class:`~.ActionNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -279,7 +465,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param action_uuid: The uuid of a action.
|
||||
:returns: A action.
|
||||
:raises: ActionNotFound
|
||||
:raises: :py:class:`~.ActionNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -287,8 +473,8 @@ class BaseConnection(object):
|
||||
"""Destroy a action and all associated interfaces.
|
||||
|
||||
:param action_id: The id or uuid of a action.
|
||||
:raises: ActionNotFound
|
||||
:raises: ActionReferenced
|
||||
:raises: :py:class:`~.ActionNotFound`
|
||||
:raises: :py:class:`~.ActionReferenced`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -297,8 +483,9 @@ class BaseConnection(object):
|
||||
|
||||
:param action_id: The id or uuid of a action.
|
||||
:returns: A action.
|
||||
:raises: ActionNotFound
|
||||
:raises: ActionReferenced
|
||||
:raises: :py:class:`~.ActionNotFound`
|
||||
:raises: :py:class:`~.ActionReferenced`
|
||||
:raises: :py:class:`~.Invalid`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -331,7 +518,7 @@ class BaseConnection(object):
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanAlreadyExists
|
||||
:raises: :py:class:`~.ActionPlanAlreadyExists`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -341,7 +528,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param action_plan_id: The id of an action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
:raises: :py:class:`~.ActionPlanNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -351,7 +538,7 @@ class BaseConnection(object):
|
||||
:param context: The security context
|
||||
:param action_plan__uuid: The uuid of an action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
:raises: :py:class:`~.ActionPlanNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -359,8 +546,8 @@ class BaseConnection(object):
|
||||
"""Destroy an action plan and all associated interfaces.
|
||||
|
||||
:param action_plan_id: The id or uuid of a action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
:raises: ActionPlanReferenced
|
||||
:raises: :py:class:`~.ActionPlanNotFound`
|
||||
:raises: :py:class:`~.ActionPlanReferenced`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -369,6 +556,7 @@ class BaseConnection(object):
|
||||
|
||||
:param action_plan_id: The id or uuid of an action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
:raises: ActionPlanReferenced
|
||||
:raises: :py:class:`~.ActionPlanNotFound`
|
||||
:raises: :py:class:`~.ActionPlanReferenced`
|
||||
:raises: :py:class:`~.Invalid`
|
||||
"""
|
||||
|
||||
484
watcher/db/purge.py
Normal file
@@ -0,0 +1,484 @@
|
||||
# -*- 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 __future__ import print_function
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_utils import strutils
|
||||
import prettytable as ptable
|
||||
from six.moves import input
|
||||
|
||||
from watcher._i18n import _, _LI
|
||||
from watcher._i18n import lazy_translation_enabled
|
||||
from watcher.common import context
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class WatcherObjectsMap(object):
|
||||
"""Wrapper to deal with watcher objects per type
|
||||
|
||||
This wrapper object contains a list of watcher objects per type.
|
||||
Its main use is to simplify the merge of watcher objects by avoiding
|
||||
duplicates, but also for representing the relationships between these
|
||||
objects.
|
||||
"""
|
||||
|
||||
# This is for generating the .pot translations
|
||||
keymap = collections.OrderedDict([
|
||||
("goals", _("Goals")),
|
||||
("strategies", _("Strategies")),
|
||||
("audit_templates", _("Audit Templates")),
|
||||
("audits", _("Audits")),
|
||||
("action_plans", _("Action Plans")),
|
||||
("actions", _("Actions")),
|
||||
])
|
||||
|
||||
def __init__(self):
|
||||
for attr_name in self.keys():
|
||||
setattr(self, attr_name, [])
|
||||
|
||||
def values(self):
|
||||
return (getattr(self, key) for key in self.keys())
|
||||
|
||||
@classmethod
|
||||
def keys(cls):
|
||||
return cls.keymap.keys()
|
||||
|
||||
def __iter__(self):
|
||||
return itertools.chain(*self.values())
|
||||
|
||||
def __add__(self, other):
|
||||
new_map = self.__class__()
|
||||
|
||||
# Merge the 2 items dicts into a new object (and avoid dupes)
|
||||
for attr_name, initials, others in zip(self.keys(), self.values(),
|
||||
other.values()):
|
||||
# Creates a copy
|
||||
merged = initials[:]
|
||||
initials_ids = [item.id for item in initials]
|
||||
non_dupes = [item for item in others
|
||||
if item.id not in initials_ids]
|
||||
merged += non_dupes
|
||||
|
||||
setattr(new_map, attr_name, merged)
|
||||
|
||||
return new_map
|
||||
|
||||
def __str__(self):
|
||||
out = ""
|
||||
for key, vals in zip(self.keys(), self.values()):
|
||||
ids = [val.id for val in vals]
|
||||
out += "%(key)s: %(val)s" % (dict(key=key, val=ids))
|
||||
out += "\n"
|
||||
return out
|
||||
|
||||
def __len__(self):
|
||||
return sum(len(getattr(self, key)) for key in self.keys())
|
||||
|
||||
def get_count_table(self):
|
||||
headers = list(self.keymap.values())
|
||||
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)]
|
||||
table = ptable.PrettyTable(field_names=translated_headers)
|
||||
table.add_row(counters)
|
||||
return table.get_string()
|
||||
|
||||
|
||||
class PurgeCommand(object):
|
||||
"""Purges the DB by removing soft deleted entries
|
||||
|
||||
The workflow for this purge is the following:
|
||||
|
||||
# Find soft deleted objects which are expired
|
||||
# Find orphan objects
|
||||
# Find their related objects whether they are expired or not
|
||||
# Merge them together
|
||||
# If it does not exceed the limit, destroy them all
|
||||
"""
|
||||
|
||||
ctx = context.make_context(show_deleted=True)
|
||||
|
||||
def __init__(self, age_in_days=None, max_number=None,
|
||||
uuid=None, exclude_orphans=False, dry_run=None):
|
||||
self.age_in_days = age_in_days
|
||||
self.max_number = max_number
|
||||
self.uuid = uuid
|
||||
self.exclude_orphans = exclude_orphans
|
||||
self.dry_run = dry_run
|
||||
|
||||
self._delete_up_to_max = None
|
||||
self._objects_map = WatcherObjectsMap()
|
||||
|
||||
def get_expiry_date(self):
|
||||
if not self.age_in_days:
|
||||
return None
|
||||
today = datetime.datetime.today()
|
||||
expiry_date = today - datetime.timedelta(days=self.age_in_days)
|
||||
return expiry_date
|
||||
|
||||
@classmethod
|
||||
def get_audit_template_uuid(cls, uuid_or_name):
|
||||
if uuid_or_name is None:
|
||||
return
|
||||
|
||||
query_func = None
|
||||
if not utils.is_uuid_like(uuid_or_name):
|
||||
query_func = objects.AuditTemplate.get_by_name
|
||||
else:
|
||||
query_func = objects.AuditTemplate.get_by_uuid
|
||||
|
||||
try:
|
||||
audit_template = query_func(cls.ctx, uuid_or_name)
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise exception.AuditTemplateNotFound(audit_template=uuid_or_name)
|
||||
|
||||
if not audit_template.deleted_at:
|
||||
raise exception.NotSoftDeletedStateError(
|
||||
name=_('Audit Template'), id=uuid_or_name)
|
||||
|
||||
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):
|
||||
return objects.AuditTemplate.list(self.ctx, filters=filters)
|
||||
|
||||
def _find_audits(self, filters=None):
|
||||
return objects.Audit.list(self.ctx, filters=filters)
|
||||
|
||||
def _find_action_plans(self, filters=None):
|
||||
return objects.ActionPlan.list(self.ctx, filters=filters)
|
||||
|
||||
def _find_actions(self, filters=None):
|
||||
return objects.Action.list(self.ctx, filters=filters)
|
||||
|
||||
def _find_orphans(self):
|
||||
orphans = WatcherObjectsMap()
|
||||
|
||||
filters = dict(deleted=False)
|
||||
goals = objects.Goal.list(self.ctx, filters=filters)
|
||||
strategies = objects.Strategy.list(self.ctx, filters=filters)
|
||||
audit_templates = objects.AuditTemplate.list(self.ctx, filters=filters)
|
||||
audits = objects.Audit.list(self.ctx, filters=filters)
|
||||
action_plans = objects.ActionPlan.list(self.ctx, filters=filters)
|
||||
actions = objects.Action.list(self.ctx, filters=filters)
|
||||
|
||||
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 = [
|
||||
audit for audit in audits
|
||||
if audit.audit_template_id not in audit_template_ids]
|
||||
|
||||
# Objects with orphan parents are themselves orphans
|
||||
audit_ids = [audit.id for audit in audits
|
||||
if audit not in orphans.audits]
|
||||
orphans.action_plans = [
|
||||
ap for ap in action_plans
|
||||
if ap.audit_id not in audit_ids]
|
||||
|
||||
# Objects with orphan parents are themselves orphans
|
||||
action_plan_ids = [ap.id for ap in action_plans
|
||||
if ap not in orphans.action_plans]
|
||||
orphans.actions = [
|
||||
action for action in actions
|
||||
if action.action_plan_id not in action_plan_ids]
|
||||
|
||||
LOG.debug("Orphans found:\n%s", orphans)
|
||||
LOG.info(_LI("Orphans found:\n%s"), orphans.get_count_table())
|
||||
|
||||
return orphans
|
||||
|
||||
def _find_soft_deleted_objects(self):
|
||||
to_be_deleted = WatcherObjectsMap()
|
||||
expiry_date = self.get_expiry_date()
|
||||
filters = dict(deleted=True)
|
||||
|
||||
if self.uuid:
|
||||
filters["uuid"] = self.uuid
|
||||
if 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(
|
||||
self._find_audit_templates(filters))
|
||||
to_be_deleted.audits.extend(self._find_audits(filters))
|
||||
to_be_deleted.action_plans.extend(
|
||||
self._find_action_plans(filters))
|
||||
to_be_deleted.actions.extend(self._find_actions(filters))
|
||||
|
||||
soft_deleted_objs = self._find_related_objects(
|
||||
to_be_deleted, base_filters=dict(deleted=True))
|
||||
|
||||
LOG.debug("Soft deleted objects:\n%s", soft_deleted_objs)
|
||||
|
||||
return soft_deleted_objs
|
||||
|
||||
def _find_related_objects(self, objects_map, base_filters=None):
|
||||
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:
|
||||
filters = {}
|
||||
filters.update(base_filters)
|
||||
filters.update(dict(audit_template_id=audit_template.id))
|
||||
related_objs = WatcherObjectsMap()
|
||||
related_objs.audits = self._find_audits(filters)
|
||||
objects_map += related_objs
|
||||
|
||||
for audit in objects_map.audits:
|
||||
filters = {}
|
||||
filters.update(base_filters)
|
||||
filters.update(dict(audit_id=audit.id))
|
||||
related_objs = WatcherObjectsMap()
|
||||
related_objs.action_plans = self._find_action_plans(filters)
|
||||
objects_map += related_objs
|
||||
|
||||
for action_plan in objects_map.action_plans:
|
||||
filters = {}
|
||||
filters.update(base_filters)
|
||||
filters.update(dict(action_plan_id=action_plan.id))
|
||||
related_objs = WatcherObjectsMap()
|
||||
related_objs.actions = self._find_actions(filters)
|
||||
objects_map += related_objs
|
||||
|
||||
return objects_map
|
||||
|
||||
def confirmation_prompt(self):
|
||||
print(self._objects_map.get_count_table())
|
||||
raw_val = input(
|
||||
_("There are %(count)d objects set for deletion. "
|
||||
"Continue? [y/N]") % dict(count=len(self._objects_map)))
|
||||
|
||||
return strutils.bool_from_string(raw_val)
|
||||
|
||||
def delete_up_to_max_prompt(self, objects_map):
|
||||
print(objects_map.get_count_table())
|
||||
print(_("The number of objects (%(num)s) to delete from the database "
|
||||
"exceeds the maximum number of objects (%(max_number)s) "
|
||||
"specified.") % dict(max_number=self.max_number,
|
||||
num=len(objects_map)))
|
||||
raw_val = input(
|
||||
_("Do you want to delete objects up to the specified maximum "
|
||||
"number? [y/N]"))
|
||||
|
||||
self._delete_up_to_max = strutils.bool_from_string(raw_val)
|
||||
|
||||
return self._delete_up_to_max
|
||||
|
||||
def _aggregate_objects(self):
|
||||
"""Objects aggregated on a 'per goal' basis"""
|
||||
# todo: aggregate orphans as well
|
||||
aggregate = []
|
||||
for goal in self._objects_map.goals:
|
||||
related_objs = WatcherObjectsMap()
|
||||
|
||||
# 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 = [
|
||||
audit for audit in self._objects_map.audits
|
||||
if audit.audit_template_id in audit_template_ids
|
||||
]
|
||||
|
||||
# action plans
|
||||
audit_ids = [audit.id for audit in related_objs.audits]
|
||||
related_objs.action_plans = [
|
||||
action_plan for action_plan in self._objects_map.action_plans
|
||||
if action_plan.audit_id in audit_ids
|
||||
]
|
||||
|
||||
# actions
|
||||
action_plan_ids = [
|
||||
action_plan.id for action_plan in related_objs.action_plans
|
||||
]
|
||||
related_objs.actions = [
|
||||
action for action in self._objects_map.actions
|
||||
if action.action_plan_id in action_plan_ids
|
||||
]
|
||||
aggregate.append(related_objs)
|
||||
|
||||
return aggregate
|
||||
|
||||
def _get_objects_up_to_limit(self):
|
||||
aggregated_objects = self._aggregate_objects()
|
||||
to_be_deleted_subset = WatcherObjectsMap()
|
||||
|
||||
for aggregate in aggregated_objects:
|
||||
if len(aggregate) + len(to_be_deleted_subset) <= self.max_number:
|
||||
to_be_deleted_subset += aggregate
|
||||
else:
|
||||
break
|
||||
|
||||
LOG.debug(to_be_deleted_subset)
|
||||
return to_be_deleted_subset
|
||||
|
||||
def find_objects_to_delete(self):
|
||||
"""Finds all the objects to be purged
|
||||
|
||||
:returns: A mapping with all the Watcher objects to purged
|
||||
:rtype: :py:class:`~.WatcherObjectsMap` instance
|
||||
"""
|
||||
to_be_deleted = self._find_soft_deleted_objects()
|
||||
|
||||
if not self.exclude_orphans:
|
||||
to_be_deleted += self._find_orphans()
|
||||
|
||||
LOG.debug("Objects to be deleted:\n%s", to_be_deleted)
|
||||
|
||||
return to_be_deleted
|
||||
|
||||
def do_delete(self):
|
||||
LOG.info(_LI("Deleting..."))
|
||||
# Reversed to avoid errors with foreign keys
|
||||
for entry in reversed(list(self._objects_map)):
|
||||
entry.destroy()
|
||||
|
||||
def execute(self):
|
||||
LOG.info(_LI("Starting purge command"))
|
||||
self._objects_map = self.find_objects_to_delete()
|
||||
|
||||
if (self.max_number is not None and
|
||||
len(self._objects_map) > self.max_number):
|
||||
if self.delete_up_to_max_prompt(self._objects_map):
|
||||
self._objects_map = self._get_objects_up_to_limit()
|
||||
else:
|
||||
return
|
||||
|
||||
_orphans_note = (_(" (orphans excluded)") if self.exclude_orphans
|
||||
else _(" (may include orphans)"))
|
||||
if not self.dry_run and self.confirmation_prompt():
|
||||
self.do_delete()
|
||||
print(_("Purge results summary%s:") % _orphans_note)
|
||||
LOG.info(_LI("Purge results summary%s:"), _orphans_note)
|
||||
else:
|
||||
LOG.debug(self._objects_map)
|
||||
print(_("Here below is a table containing the objects "
|
||||
"that can be purged%s:") % _orphans_note)
|
||||
|
||||
LOG.info("\n%s", self._objects_map.get_count_table())
|
||||
print(self._objects_map.get_count_table())
|
||||
LOG.info(_LI("Purge process completed"))
|
||||
|
||||
|
||||
def purge(age_in_days, max_number, audit_template, exclude_orphans, dry_run):
|
||||
"""Removes soft deleted objects from the database
|
||||
|
||||
:param age_in_days: Number of days since deletion (from today)
|
||||
to exclude from the purge. If None, everything will be purged.
|
||||
:type age_in_days: int
|
||||
:param max_number: Max number of objects expected to be deleted.
|
||||
Prevents the deletion if exceeded. No limit if set to None.
|
||||
:type max_number: int
|
||||
:param audit_template: UUID or name of the audit template to purge.
|
||||
:type audit_template: str
|
||||
:param exclude_orphans: Flag to indicate whether or not you want to
|
||||
exclude orphans from deletion (default: False).
|
||||
:type exclude_orphans: bool
|
||||
:param dry_run: Flag to indicate whether or not you want to perform
|
||||
a dry run (no deletion).
|
||||
:type dry_run: bool
|
||||
"""
|
||||
try:
|
||||
if max_number and max_number < 0:
|
||||
raise exception.NegativeLimitError
|
||||
|
||||
LOG.info("[options] age_in_days = %s", age_in_days)
|
||||
LOG.info("[options] max_number = %s", max_number)
|
||||
LOG.info("[options] audit_template = %s", audit_template)
|
||||
LOG.info("[options] exclude_orphans = %s", exclude_orphans)
|
||||
LOG.info("[options] dry_run = %s", dry_run)
|
||||
|
||||
uuid = PurgeCommand.get_audit_template_uuid(audit_template)
|
||||
|
||||
cmd = PurgeCommand(age_in_days, max_number, uuid,
|
||||
exclude_orphans, dry_run)
|
||||
|
||||
cmd.execute()
|
||||
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
print(exc)
|
||||
sys.exit(1)
|
||||
@@ -21,7 +21,6 @@ from oslo_config import cfg
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_db.sqlalchemy import session as db_session
|
||||
from oslo_db.sqlalchemy import utils as db_utils
|
||||
from oslo_log import log
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
from watcher import _i18n
|
||||
@@ -29,10 +28,12 @@ from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
from watcher.db import api
|
||||
from watcher.db.sqlalchemy import models
|
||||
from watcher.objects import action as action_objects
|
||||
from watcher.objects import action_plan as ap_objects
|
||||
from watcher.objects import audit as audit_objects
|
||||
from watcher.objects import utils as objutils
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
_ = _i18n._
|
||||
|
||||
_FACADE = None
|
||||
@@ -105,25 +106,213 @@ class Connection(api.BaseConnection):
|
||||
"""SqlAlchemy connection."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
super(Connection, self).__init__()
|
||||
|
||||
def __add_soft_delete_mixin_filters(self, query, filters, model):
|
||||
if 'deleted' in filters:
|
||||
if bool(filters['deleted']):
|
||||
query = query.filter(model.deleted != 0)
|
||||
else:
|
||||
query = query.filter(model.deleted == 0)
|
||||
if 'deleted_at__eq' in filters:
|
||||
query = query.filter(
|
||||
model.deleted_at == objutils.datetime_or_str_or_none(
|
||||
filters['deleted_at__eq']))
|
||||
if 'deleted_at__gt' in filters:
|
||||
query = query.filter(
|
||||
model.deleted_at > objutils.datetime_or_str_or_none(
|
||||
filters['deleted_at__gt']))
|
||||
if 'deleted_at__gte' in filters:
|
||||
query = query.filter(
|
||||
model.deleted_at >= objutils.datetime_or_str_or_none(
|
||||
filters['deleted_at__gte']))
|
||||
if 'deleted_at__lt' in filters:
|
||||
query = query.filter(
|
||||
model.deleted_at < objutils.datetime_or_str_or_none(
|
||||
filters['deleted_at__lt']))
|
||||
if 'deleted_at__lte' in filters:
|
||||
query = query.filter(
|
||||
model.deleted_at <= objutils.datetime_or_str_or_none(
|
||||
filters['deleted_at__lte']))
|
||||
|
||||
return query
|
||||
|
||||
def __add_timestamp_mixin_filters(self, query, filters, model):
|
||||
if 'created_at__eq' in filters:
|
||||
query = query.filter(
|
||||
model.created_at == objutils.datetime_or_str_or_none(
|
||||
filters['created_at__eq']))
|
||||
if 'created_at__gt' in filters:
|
||||
query = query.filter(
|
||||
model.created_at > objutils.datetime_or_str_or_none(
|
||||
filters['created_at__gt']))
|
||||
if 'created_at__gte' in filters:
|
||||
query = query.filter(
|
||||
model.created_at >= objutils.datetime_or_str_or_none(
|
||||
filters['created_at__gte']))
|
||||
if 'created_at__lt' in filters:
|
||||
query = query.filter(
|
||||
model.created_at < objutils.datetime_or_str_or_none(
|
||||
filters['created_at__lt']))
|
||||
if 'created_at__lte' in filters:
|
||||
query = query.filter(
|
||||
model.created_at <= objutils.datetime_or_str_or_none(
|
||||
filters['created_at__lte']))
|
||||
|
||||
if 'updated_at__eq' in filters:
|
||||
query = query.filter(
|
||||
model.updated_at == objutils.datetime_or_str_or_none(
|
||||
filters['updated_at__eq']))
|
||||
if 'updated_at__gt' in filters:
|
||||
query = query.filter(
|
||||
model.updated_at > objutils.datetime_or_str_or_none(
|
||||
filters['updated_at__gt']))
|
||||
if 'updated_at__gte' in filters:
|
||||
query = query.filter(
|
||||
model.updated_at >= objutils.datetime_or_str_or_none(
|
||||
filters['updated_at__gte']))
|
||||
if 'updated_at__lt' in filters:
|
||||
query = query.filter(
|
||||
model.updated_at < objutils.datetime_or_str_or_none(
|
||||
filters['updated_at__lt']))
|
||||
if 'updated_at__lte' in filters:
|
||||
query = query.filter(
|
||||
model.updated_at <= objutils.datetime_or_str_or_none(
|
||||
filters['updated_at__lte']))
|
||||
|
||||
return query
|
||||
|
||||
def __add_simple_filter(self, query, model, fieldname, value):
|
||||
return query.filter(getattr(model, fieldname) == value)
|
||||
|
||||
def __add_join_filter(self, query, model, join_model, fieldname, value):
|
||||
query = query.join(join_model)
|
||||
return self.__add_simple_filter(query, join_model, fieldname, value)
|
||||
|
||||
def _add_filters(self, query, model, filters=None,
|
||||
plain_fields=None, join_fieldmap=None):
|
||||
"""Generic way to add filters to a Watcher model
|
||||
|
||||
: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
|
||||
|
||||
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 = []
|
||||
filters = {}
|
||||
|
||||
if 'name' in filters:
|
||||
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'])
|
||||
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 query
|
||||
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):
|
||||
if filters is None:
|
||||
filters = []
|
||||
|
||||
if 'uuid' in filters:
|
||||
query = query.filter_by(uuid=filters['uuid'])
|
||||
if 'type' in filters:
|
||||
query = query.filter_by(type=filters['type'])
|
||||
if 'state' in filters:
|
||||
@@ -144,12 +333,20 @@ class Connection(api.BaseConnection):
|
||||
query = query.filter(
|
||||
models.AuditTemplate.name ==
|
||||
filters['audit_template_name'])
|
||||
|
||||
query = self.__add_soft_delete_mixin_filters(
|
||||
query, filters, models.Audit)
|
||||
query = self.__add_timestamp_mixin_filters(
|
||||
query, filters, models.Audit)
|
||||
|
||||
return query
|
||||
|
||||
def _add_action_plans_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = []
|
||||
|
||||
if 'uuid' in filters:
|
||||
query = query.filter_by(uuid=filters['uuid'])
|
||||
if 'state' in filters:
|
||||
query = query.filter_by(state=filters['state'])
|
||||
if 'audit_id' in filters:
|
||||
@@ -158,12 +355,20 @@ class Connection(api.BaseConnection):
|
||||
query = query.join(models.Audit,
|
||||
models.ActionPlan.audit_id == models.Audit.id)
|
||||
query = query.filter(models.Audit.uuid == filters['audit_uuid'])
|
||||
|
||||
query = self.__add_soft_delete_mixin_filters(
|
||||
query, filters, models.ActionPlan)
|
||||
query = self.__add_timestamp_mixin_filters(
|
||||
query, filters, models.ActionPlan)
|
||||
|
||||
return query
|
||||
|
||||
def _add_actions_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = []
|
||||
|
||||
if 'uuid' in filters:
|
||||
query = query.filter_by(uuid=filters['uuid'])
|
||||
if 'action_plan_id' in filters:
|
||||
query = query.filter_by(action_plan_id=filters['action_plan_id'])
|
||||
if 'action_plan_uuid' in filters:
|
||||
@@ -181,11 +386,146 @@ class Connection(api.BaseConnection):
|
||||
|
||||
if 'state' in filters:
|
||||
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, filters, models.Action)
|
||||
query = self.__add_timestamp_mixin_filters(
|
||||
query, filters, models.Action)
|
||||
|
||||
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,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
|
||||
@@ -193,7 +533,6 @@ class Connection(api.BaseConnection):
|
||||
query = self._add_audit_templates_filters(query, filters)
|
||||
if not context.show_deleted:
|
||||
query = query.filter_by(deleted_at=None)
|
||||
|
||||
return _paginate_query(models.AuditTemplate, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
@@ -202,116 +541,78 @@ class Connection(api.BaseConnection):
|
||||
if not values.get('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.update(values)
|
||||
|
||||
try:
|
||||
audit_template.save()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.AuditTemplateAlreadyExists(uuid=values['uuid'],
|
||||
name=values['name'])
|
||||
raise exception.AuditTemplateAlreadyExists(
|
||||
audit_template=values['name'])
|
||||
return audit_template
|
||||
|
||||
def get_audit_template_by_id(self, context, audit_template_id):
|
||||
query = model_query(models.AuditTemplate)
|
||||
query = query.filter_by(id=audit_template_id)
|
||||
def _get_audit_template(self, context, fieldname, value):
|
||||
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_id)
|
||||
return audit_template
|
||||
except exc.NoResultFound:
|
||||
return self._get(context, model=models.AuditTemplate,
|
||||
fieldname=fieldname, value=value)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.AuditTemplateNotFound(audit_template=value)
|
||||
|
||||
def get_audit_template_by_id(self, context, audit_template_id):
|
||||
return self._get_audit_template(
|
||||
context, fieldname="id", value=audit_template_id)
|
||||
|
||||
def get_audit_template_by_uuid(self, context, audit_template_uuid):
|
||||
return self._get_audit_template(
|
||||
context, fieldname="uuid", value=audit_template_uuid)
|
||||
|
||||
def get_audit_template_by_name(self, context, audit_template_name):
|
||||
return self._get_audit_template(
|
||||
context, fieldname="name", value=audit_template_name)
|
||||
|
||||
def destroy_audit_template(self, audit_template_id):
|
||||
try:
|
||||
return self._destroy(models.AuditTemplate, audit_template_id)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.AuditTemplateNotFound(
|
||||
audit_template=audit_template_id)
|
||||
|
||||
def get_audit_template_by_uuid(self, context, audit_template_uuid):
|
||||
query = model_query(models.AuditTemplate)
|
||||
query = query.filter_by(uuid=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):
|
||||
query = model_query(models.AuditTemplate)
|
||||
query = query.filter_by(name=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):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.AuditTemplate, session=session)
|
||||
query = add_identity_filter(query, 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):
|
||||
if 'uuid' in values:
|
||||
msg = _("Cannot overwrite UUID for an existing AuditTemplate.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
|
||||
return self._do_update_audit_template(audit_template_id, values)
|
||||
|
||||
def _do_update_audit_template(self, audit_template_id, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
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
|
||||
raise exception.Invalid(
|
||||
message=_("Cannot overwrite UUID for an existing "
|
||||
"Audit Template."))
|
||||
try:
|
||||
return self._update(
|
||||
models.AuditTemplate, audit_template_id, values)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.AuditTemplateNotFound(
|
||||
audit_template=audit_template_id)
|
||||
|
||||
def soft_delete_audit_template(self, audit_template_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.AuditTemplate, session=session)
|
||||
query = add_identity_filter(query, audit_template_id)
|
||||
try:
|
||||
self._soft_delete(models.AuditTemplate, audit_template_id)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.AuditTemplateNotFound(
|
||||
audit_template=audit_template_id)
|
||||
|
||||
try:
|
||||
query.one()
|
||||
except exc.NoResultFound:
|
||||
raise exception.AuditTemplateNotFound(node=audit_template_id)
|
||||
|
||||
query.soft_delete()
|
||||
# ### AUDITS ### #
|
||||
|
||||
def get_audit_list(self, context, filters=None, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
query = model_query(models.Audit)
|
||||
query = self._add_audits_filters(query, filters)
|
||||
if not context.show_deleted:
|
||||
query = query.filter(~(models.Audit.state == 'DELETED'))
|
||||
query = query.filter(
|
||||
~(models.Audit.state == audit_objects.State.DELETED))
|
||||
|
||||
return _paginate_query(models.Audit, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
@@ -339,7 +640,7 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
audit = query.one()
|
||||
if not context.show_deleted:
|
||||
if audit.state == 'DELETED':
|
||||
if audit.state == audit_objects.State.DELETED:
|
||||
raise exception.AuditNotFound(audit=audit_id)
|
||||
return audit
|
||||
except exc.NoResultFound:
|
||||
@@ -352,7 +653,7 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
audit = query.one()
|
||||
if not context.show_deleted:
|
||||
if audit.state == 'DELETED':
|
||||
if audit.state == audit_objects.State.DELETED:
|
||||
raise exception.AuditNotFound(audit=audit_uuid)
|
||||
return audit
|
||||
except exc.NoResultFound:
|
||||
@@ -383,8 +684,9 @@ class Connection(api.BaseConnection):
|
||||
|
||||
def update_audit(self, audit_id, values):
|
||||
if 'uuid' in values:
|
||||
msg = _("Cannot overwrite UUID for an existing Audit.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
raise exception.Invalid(
|
||||
message=_("Cannot overwrite UUID for an existing "
|
||||
"Audit."))
|
||||
|
||||
return self._do_update_audit(audit_id, values)
|
||||
|
||||
@@ -410,16 +712,19 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
query.one()
|
||||
except exc.NoResultFound:
|
||||
raise exception.AuditNotFound(node=audit_id)
|
||||
raise exception.AuditNotFound(audit=audit_id)
|
||||
|
||||
query.soft_delete()
|
||||
|
||||
# ### ACTIONS ### #
|
||||
|
||||
def get_action_list(self, context, filters=None, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
query = model_query(models.Action)
|
||||
query = self._add_actions_filters(query, filters)
|
||||
if not context.show_deleted:
|
||||
query = query.filter(~(models.Action.state == 'DELETED'))
|
||||
query = query.filter(
|
||||
~(models.Action.state == action_objects.State.DELETED))
|
||||
return _paginate_query(models.Action, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
@@ -442,7 +747,7 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
action = query.one()
|
||||
if not context.show_deleted:
|
||||
if action.state == 'DELETED':
|
||||
if action.state == action_objects.State.DELETED:
|
||||
raise exception.ActionNotFound(
|
||||
action=action_id)
|
||||
return action
|
||||
@@ -455,7 +760,7 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
action = query.one()
|
||||
if not context.show_deleted:
|
||||
if action.state == 'DELETED':
|
||||
if action.state == action_objects.State.DELETED:
|
||||
raise exception.ActionNotFound(
|
||||
action=action_uuid)
|
||||
return action
|
||||
@@ -474,8 +779,9 @@ class Connection(api.BaseConnection):
|
||||
def update_action(self, action_id, values):
|
||||
# NOTE(dtantsur): this can lead to very strange errors
|
||||
if 'uuid' in values:
|
||||
msg = _("Cannot overwrite UUID for an existing Action.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
raise exception.Invalid(
|
||||
message=_("Cannot overwrite UUID for an existing "
|
||||
"Action."))
|
||||
|
||||
return self._do_update_action(action_id, values)
|
||||
|
||||
@@ -501,17 +807,20 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
query.one()
|
||||
except exc.NoResultFound:
|
||||
raise exception.ActionNotFound(node=action_id)
|
||||
raise exception.ActionNotFound(action=action_id)
|
||||
|
||||
query.soft_delete()
|
||||
|
||||
# ### ACTION PLANS ### #
|
||||
|
||||
def get_action_plan_list(
|
||||
self, context, columns=None, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
query = model_query(models.ActionPlan)
|
||||
query = self._add_action_plans_filters(query, filters)
|
||||
if not context.show_deleted:
|
||||
query = query.filter(~(models.ActionPlan.state == 'DELETED'))
|
||||
query = query.filter(
|
||||
~(models.ActionPlan.state == ap_objects.State.DELETED))
|
||||
|
||||
return _paginate_query(models.ActionPlan, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
@@ -536,7 +845,7 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
action_plan = query.one()
|
||||
if not context.show_deleted:
|
||||
if action_plan.state == 'DELETED':
|
||||
if action_plan.state == ap_objects.State.DELETED:
|
||||
raise exception.ActionPlanNotFound(
|
||||
action_plan=action_plan_id)
|
||||
return action_plan
|
||||
@@ -550,7 +859,7 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
action_plan = query.one()
|
||||
if not context.show_deleted:
|
||||
if action_plan.state == 'DELETED':
|
||||
if action_plan.state == ap_objects.State.DELETED:
|
||||
raise exception.ActionPlanNotFound(
|
||||
action_plan=action_plan__uuid)
|
||||
return action_plan
|
||||
@@ -583,8 +892,9 @@ class Connection(api.BaseConnection):
|
||||
|
||||
def update_action_plan(self, action_plan_id, values):
|
||||
if 'uuid' in values:
|
||||
msg = _("Cannot overwrite UUID for an existing Audit.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
raise exception.Invalid(
|
||||
message=_("Cannot overwrite UUID for an existing "
|
||||
"Action Plan."))
|
||||
|
||||
return self._do_update_action_plan(action_plan_id, values)
|
||||
|
||||
@@ -610,6 +920,6 @@ class Connection(api.BaseConnection):
|
||||
try:
|
||||
query.one()
|
||||
except exc.NoResultFound:
|
||||
raise exception.ActionPlanNotFound(node=action_plan_id)
|
||||
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
|
||||
|
||||
query.soft_delete()
|
||||
|
||||
@@ -32,7 +32,7 @@ def _alembic_config():
|
||||
return config
|
||||
|
||||
|
||||
def version(config=None, engine=None):
|
||||
def version(engine=None):
|
||||
"""Current database version.
|
||||
|
||||
:returns: Database version
|
||||
|
||||
@@ -110,13 +110,41 @@ class WatcherBase(models.SoftDeleteMixin,
|
||||
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):
|
||||
"""Represents an audit template."""
|
||||
|
||||
__tablename__ = 'audit_templates'
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
|
||||
schema.UniqueConstraint('name', name='uniq_audit_templates0name'),
|
||||
table_args()
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
@@ -124,7 +152,8 @@ class AuditTemplate(Base):
|
||||
name = Column(String(63), nullable=True)
|
||||
description = Column(String(255), 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)
|
||||
version = Column(String(15), nullable=True)
|
||||
|
||||
@@ -160,11 +189,8 @@ class Action(Base):
|
||||
nullable=False)
|
||||
# only for the first version
|
||||
action_type = Column(String(255), nullable=False)
|
||||
applies_to = Column(String(255), nullable=True)
|
||||
input_parameters = Column(JSONEncodedDict, nullable=True)
|
||||
state = Column(String(20), nullable=True)
|
||||
# todo(jed) remove parameter alarm
|
||||
alarm = Column(String(36))
|
||||
next = Column(String(36), nullable=True)
|
||||
|
||||
|
||||
@@ -179,9 +205,6 @@ class ActionPlan(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36))
|
||||
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'),
|
||||
nullable=True)
|
||||
state = Column(String(20), nullable=True)
|
||||
|
||||
@@ -54,8 +54,8 @@ class DefaultAuditHandler(base.BaseAuditHandler):
|
||||
event.data = {}
|
||||
payload = {'audit_uuid': audit_uuid,
|
||||
'audit_status': status}
|
||||
self.messaging.topic_status.publish_event(event.type.name,
|
||||
payload)
|
||||
self.messaging.status_topic_handler.publish_event(
|
||||
event.type.name, payload)
|
||||
|
||||
def update_audit_state(self, request_context, audit_uuid, state):
|
||||
LOG.debug("Update audit state: %s", state)
|
||||
|
||||
@@ -38,22 +38,19 @@ See :doc:`../architecture` for more details on this component.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.messaging.messaging_core import MessagingCore
|
||||
from watcher.decision_engine.messaging.audit_endpoint import AuditEndpoint
|
||||
from watcher.decision_engine.messaging import audit_endpoint
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
WATCHER_DECISION_ENGINE_OPTS = [
|
||||
cfg.StrOpt('topic_control',
|
||||
cfg.StrOpt('conductor_topic',
|
||||
default='watcher.decision.control',
|
||||
help='The topic name used for'
|
||||
'control events, this topic '
|
||||
'used for rpc call '),
|
||||
cfg.StrOpt('topic_status',
|
||||
cfg.StrOpt('status_topic',
|
||||
default='watcher.decision.status',
|
||||
help='The topic name used for '
|
||||
'status events, this topic '
|
||||
@@ -78,18 +75,15 @@ CONF.register_group(decision_engine_opt_group)
|
||||
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
|
||||
|
||||
|
||||
class DecisionEngineManager(MessagingCore):
|
||||
def __init__(self):
|
||||
super(DecisionEngineManager, self).__init__(
|
||||
CONF.watcher_decision_engine.publisher_id,
|
||||
CONF.watcher_decision_engine.topic_control,
|
||||
CONF.watcher_decision_engine.topic_status,
|
||||
api_version=self.API_VERSION)
|
||||
endpoint = AuditEndpoint(self,
|
||||
max_workers=CONF.watcher_decision_engine.
|
||||
max_workers)
|
||||
self.topic_control.add_endpoint(endpoint)
|
||||
class DecisionEngineManager(object):
|
||||
|
||||
def join(self):
|
||||
self.topic_control.join()
|
||||
self.topic_status.join()
|
||||
API_VERSION = '1.0'
|
||||
|
||||
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
|
||||
|
||||
@@ -16,19 +16,22 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from concurrent import futures
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.decision_engine.audit.default import DefaultAuditHandler
|
||||
from watcher.decision_engine.audit import default
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditEndpoint(object):
|
||||
def __init__(self, messaging, max_workers):
|
||||
def __init__(self, messaging):
|
||||
self._messaging = messaging
|
||||
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
self._executor = futures.ThreadPoolExecutor(
|
||||
max_workers=CONF.watcher_decision_engine.max_workers)
|
||||
|
||||
@property
|
||||
def executor(self):
|
||||
@@ -39,7 +42,7 @@ class AuditEndpoint(object):
|
||||
return self._messaging
|
||||
|
||||
def do_trigger_audit(self, context, audit_uuid):
|
||||
audit = DefaultAuditHandler(self.messaging)
|
||||
audit = default.DefaultAuditHandler(self.messaging)
|
||||
audit.execute(audit_uuid, context)
|
||||
|
||||
def trigger_audit(self, context, audit_uuid):
|
||||
|
||||
@@ -13,16 +13,12 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.model import hypervisor
|
||||
from watcher.decision_engine.model import mapping
|
||||
from watcher.decision_engine.model import vm
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelRoot(object):
|
||||
def __init__(self):
|
||||
|
||||
@@ -21,6 +21,7 @@ class ResourceType(Enum):
|
||||
cpu_cores = 'num_cores'
|
||||
memory = 'memory'
|
||||
disk = 'disk'
|
||||
disk_capacity = 'disk_capacity'
|
||||
|
||||
|
||||
class Resource(object):
|
||||
|
||||