Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8635beb045 | ||
|
|
ff1af61c1b | ||
|
|
00adcb84f6 | ||
|
|
a68eb0d619 | ||
|
|
517f1d083e | ||
|
|
76a61e234a | ||
|
|
f4b631ebb3 | ||
|
|
e78610e7d1 | ||
|
|
405cde2ff9 | ||
|
|
b25ecde0db | ||
|
|
e6b4befbf4 | ||
|
|
dcf52077a4 | ||
|
|
1de00086f5 | ||
|
|
518b4c82f1 | ||
|
|
4423fdd231 | ||
|
|
b1c6ea71a4 | ||
|
|
533a4c7a34 | ||
|
|
d61c4c12d1 | ||
|
|
1d3891e1e4 | ||
|
|
f13d6dd805 | ||
|
|
81bdc51e21 | ||
|
|
e7bbf1d1b9 | ||
|
|
e34198f35c | ||
|
|
bc06a7d419 | ||
|
|
1a1d9d09f4 | ||
|
|
47ff9bb105 | ||
|
|
ca5b849f6b | ||
|
|
2272b515bf | ||
|
|
f6b1b515c4 | ||
|
|
64903ce56c | ||
|
|
86cbe4bd0c | ||
|
|
0b29a8394b | ||
|
|
ec64203bee | ||
|
|
dc01e35760 | ||
|
|
117079f81e | ||
|
|
d03e1d54bd | ||
|
|
6bdc4cac82 | ||
|
|
b4c5e2bb81 | ||
|
|
369428c313 | ||
|
|
2381a677b6 | ||
|
|
da8b24f483 | ||
|
|
d7a7b3ea54 | ||
|
|
37595e9bb7 | ||
|
|
cb693e4093 | ||
|
|
9918f59227 | ||
|
|
07c7ba9b2e | ||
|
|
73cf5a98ae | ||
|
|
80abcd6fd9 | ||
|
|
e3edc67045 | ||
|
|
b3c2d3af1f | ||
|
|
4fc3307525 | ||
|
|
77c07a466f | ||
|
|
80867703ba | ||
|
|
7b403c0d3b | ||
|
|
6f2c82316c | ||
|
|
b21193aaf2 | ||
|
|
e1a1cd79f4 | ||
|
|
019fc498ae | ||
|
|
407a505e73 | ||
|
|
488c6e3f4e | ||
|
|
28a2422ceb | ||
|
|
58b5baaeae | ||
|
|
e8fc4a1bc8 | ||
|
|
921e531adf | ||
|
|
b5cb6631cc | ||
|
|
45801cf9c5 | ||
|
|
442512cd71 | ||
|
|
2b95a4cbc4 | ||
|
|
84b12f8f1e | ||
|
|
f665d83657 | ||
|
|
5e16e032be | ||
|
|
eab47bf182 | ||
|
|
2544327979 | ||
|
|
2412df4b6c | ||
|
|
d811dd93ae | ||
|
|
cb342b45b7 | ||
|
|
1a4d5a3df3 | ||
|
|
8b8d2f01fe | ||
|
|
ca37358cac | ||
|
|
46091385d8 | ||
|
|
a457707e56 | ||
|
|
6c922b43ea | ||
|
|
5d66f66050 | ||
|
|
e7e50b3955 | ||
|
|
d9a2b8809b | ||
|
|
164a802718 | ||
|
|
dbdf690cd0 |
7
.gitignore
vendored
@@ -64,3 +64,10 @@ sftp-config.json
|
||||
|
||||
cover
|
||||
/demo/
|
||||
|
||||
|
||||
# Files created by releasenotes build
|
||||
releasenotes/build
|
||||
|
||||
# Desktop Service Store
|
||||
*.DS_Store
|
||||
|
||||
@@ -21,4 +21,4 @@ migration, increased energy efficiency-and more!
|
||||
* Wiki: http://wiki.openstack.org/wiki/Watcher
|
||||
* Source: https://github.com/openstack/watcher
|
||||
* Bugs: http://bugs.launchpad.net/watcher
|
||||
* Documentation: https://factory.b-com.com/www/watcher/doc/watcher/index.html
|
||||
* Documentation: http://docs.openstack.org/developer/watcher/
|
||||
|
||||
@@ -27,6 +27,9 @@ ENABLED_SERVICES+=,q-svc,q-dhcp,q-meta,q-agt,q-l3,neutron
|
||||
# Enable remote console access
|
||||
enable_service n-cauth
|
||||
|
||||
# Enable the Watcher Dashboard plugin
|
||||
enable_plugin watcher-dashboard git://git.openstack.org/openstack/watcher-dashboard
|
||||
|
||||
# Enable the Watcher plugin
|
||||
enable_plugin watcher git://git.openstack.org/openstack/watcher
|
||||
|
||||
|
||||
@@ -150,12 +150,14 @@ This database stores all the Watcher domain objects which can be requested
|
||||
by the :ref:`Watcher API <archi_watcher_api_definition>` or the
|
||||
:ref:`Watcher CLI <archi_watcher_cli_definition>`:
|
||||
|
||||
- :ref:`Goals <goal_definition>`
|
||||
- :ref:`Strategies <strategy_definition>`
|
||||
- :ref:`Audit templates <audit_template_definition>`
|
||||
- :ref:`Audits <audit_definition>`
|
||||
- :ref:`Action plans <action_plan_definition>`
|
||||
- :ref:`Efficacy indicators <efficacy_indicator_definition>` via the Action
|
||||
Plan API.
|
||||
- :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*".
|
||||
@@ -210,10 +212,10 @@ 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
|
||||
Here below is a diagram representing the main objects in Watcher from a
|
||||
database perspective:
|
||||
|
||||
.. image:: ./images/watcher_class_diagram.png
|
||||
.. image:: ./images/watcher_db_schema_diagram.png
|
||||
:width: 100%
|
||||
|
||||
|
||||
@@ -259,6 +261,13 @@ previously created :ref:`Audit template <audit_template_definition>`:
|
||||
.. image:: ./images/sequence_create_and_launch_audit.png
|
||||
:width: 100%
|
||||
|
||||
The :ref:`Administrator <administrator_definition>` also can specify type of
|
||||
Audit and interval (in case of CONTINUOUS type). There is two types of Audit:
|
||||
ONESHOT and CONTINUOUS. Oneshot Audit is launched once and if it succeeded
|
||||
executed new action plan list will be provided. Continuous Audit creates
|
||||
action plans with specified interval (in seconds); if action plan
|
||||
has been created, all previous action plans get CANCELLED state.
|
||||
|
||||
A message is sent on the :ref:`AMQP bus <amqp_bus_definition>` which triggers
|
||||
the Audit in the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`:
|
||||
@@ -297,9 +306,11 @@ This method finds an appropriate scheduling of
|
||||
:ref:`Actions <action_definition>` taking into account some scheduling rules
|
||||
(such as priorities between actions).
|
||||
It generates a new :ref:`Action Plan <action_plan_definition>` with status
|
||||
**RECOMMENDED** and saves it into the
|
||||
:ref:`Watcher Database <watcher_database_definition>`. The saved action plan is
|
||||
now a scheduled flow of actions.
|
||||
**RECOMMENDED** and saves it into the:ref:`Watcher Database
|
||||
<watcher_database_definition>`. The saved action plan is now a scheduled flow
|
||||
of actions to which a global efficacy is associated alongside a number of
|
||||
:ref:`Efficacy Indicators <efficacy_indicator_definition>` as specified by the
|
||||
related :ref:`goal <goal_definition>`.
|
||||
|
||||
If every step executed successfully, the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` updates
|
||||
@@ -308,6 +319,11 @@ the current status of the Audit to **SUCCEEDED** in the
|
||||
on the bus to inform other components that the :ref:`Audit <audit_definition>`
|
||||
was successful.
|
||||
|
||||
This internal workflow the Decision Engine follows to conduct an audit can be
|
||||
seen in the sequence diagram here below:
|
||||
|
||||
.. image:: ./images/sequence_from_audit_execution_to_actionplan_creation.png
|
||||
:width: 100%
|
||||
|
||||
.. _sequence_diagrams_launch_action_plan:
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ should be able to run the Watcher services by issuing these commands:
|
||||
By default, this will show logging on the console from which it was started.
|
||||
Once started, you can use the `Watcher Client`_ to play with Watcher service.
|
||||
|
||||
.. _`Watcher Client`: https://git.openstack.org/openstack/python-watcherclient.git
|
||||
.. _`Watcher Client`: https://git.openstack.org/cgit/openstack/python-watcherclient
|
||||
|
||||
Installing from packages: PyPI
|
||||
--------------------------------
|
||||
|
||||
142
doc/source/deploy/policy.rst
Normal file
@@ -0,0 +1,142 @@
|
||||
..
|
||||
Copyright 2016 OpenStack Foundation
|
||||
All Rights Reserved.
|
||||
|
||||
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.
|
||||
|
||||
Policies
|
||||
========
|
||||
|
||||
Watcher's public API calls may be restricted to certain sets of users using a
|
||||
policy configuration file. This document explains exactly how policies are
|
||||
configured and what they apply to.
|
||||
|
||||
A policy is composed of a set of rules that are used in determining if a
|
||||
particular action may be performed by the authorized tenant.
|
||||
|
||||
Constructing a Policy Configuration File
|
||||
----------------------------------------
|
||||
|
||||
A policy configuration file is a simply JSON object that contain sets of
|
||||
rules. Each top-level key is the name of a rule. Each rule
|
||||
is a string that describes an action that may be performed in the Watcher API.
|
||||
|
||||
The actions that may have a rule enforced on them are:
|
||||
|
||||
* ``strategy:get_all``, ``strategy:detail`` - List available strategies
|
||||
|
||||
* ``GET /v1/strategies``
|
||||
* ``GET /v1/strategies/detail``
|
||||
|
||||
* ``strategy:get`` - Retrieve a specific strategy entity
|
||||
|
||||
* ``GET /v1/strategies/<STRATEGY_UUID>``
|
||||
* ``GET /v1/strategies/<STRATEGY_NAME>``
|
||||
|
||||
|
||||
* ``goal:get_all``, ``goal:detail`` - List available goals
|
||||
|
||||
* ``GET /v1/goals``
|
||||
* ``GET /v1/goals/detail``
|
||||
|
||||
* ``goal:get`` - Retrieve a specific goal entity
|
||||
|
||||
* ``GET /v1/goals/<GOAL_UUID>``
|
||||
* ``GET /v1/goals/<GOAL_NAME>``
|
||||
|
||||
|
||||
* ``audit_template:get_all``, ``audit_template:detail`` - List available
|
||||
audit_templates
|
||||
|
||||
* ``GET /v1/audit_templates``
|
||||
* ``GET /v1/audit_templates/detail``
|
||||
|
||||
* ``audit_template:get`` - Retrieve a specific audit template entity
|
||||
|
||||
* ``GET /v1/audit_templates/<AUDIT_TEMPLATE_UUID>``
|
||||
* ``GET /v1/audit_templates/<AUDIT_TEMPLATE_NAME>``
|
||||
|
||||
* ``audit_template:create`` - Create an audit template entity
|
||||
|
||||
* ``POST /v1/audit_templates``
|
||||
|
||||
* ``audit_template:delete`` - Delete an audit template entity
|
||||
|
||||
* ``DELETE /v1/audit_templates/<AUDIT_TEMPLATE_UUID>``
|
||||
* ``DELETE /v1/audit_templates/<AUDIT_TEMPLATE_NAME>``
|
||||
|
||||
* ``audit_template:update`` - Update an audit template entity
|
||||
|
||||
* ``PATCH /v1/audit_templates/<AUDIT_TEMPLATE_UUID>``
|
||||
* ``PATCH /v1/audit_templates/<AUDIT_TEMPLATE_NAME>``
|
||||
|
||||
|
||||
* ``audit:get_all``, ``audit:detail`` - List available audits
|
||||
|
||||
* ``GET /v1/audits``
|
||||
* ``GET /v1/audits/detail``
|
||||
|
||||
* ``audit:get`` - Retrieve a specific audit entity
|
||||
|
||||
* ``GET /v1/audits/<AUDIT_UUID>``
|
||||
|
||||
* ``audit:create`` - Create an audit entity
|
||||
|
||||
* ``POST /v1/audits``
|
||||
|
||||
* ``audit:delete`` - Delete an audit entity
|
||||
|
||||
* ``DELETE /v1/audits/<AUDIT_UUID>``
|
||||
|
||||
* ``audit:update`` - Update an audit entity
|
||||
|
||||
* ``PATCH /v1/audits/<AUDIT_UUID>``
|
||||
|
||||
|
||||
* ``action_plan:get_all``, ``action_plan:detail`` - List available action plans
|
||||
|
||||
* ``GET /v1/action_plans``
|
||||
* ``GET /v1/action_plans/detail``
|
||||
|
||||
* ``action_plan:get`` - Retrieve a specific action plan entity
|
||||
|
||||
* ``GET /v1/action_plans/<ACTION_PLAN_UUID>``
|
||||
|
||||
* ``action_plan:delete`` - Delete an action plan entity
|
||||
|
||||
* ``DELETE /v1/action_plans/<ACTION_PLAN_UUID>``
|
||||
|
||||
* ``action_plan:update`` - Update an action plan entity
|
||||
|
||||
* ``PATCH /v1/audits/<ACTION_PLAN_UUID>``
|
||||
|
||||
|
||||
* ``action:get_all``, ``action:detail`` - List available action
|
||||
|
||||
* ``GET /v1/actions``
|
||||
* ``GET /v1/actions/detail``
|
||||
|
||||
* ``action:get`` - Retrieve a specific action plan entity
|
||||
|
||||
* ``GET /v1/actions/<ACTION_UUID>``
|
||||
|
||||
|
||||
|
||||
To limit an action to a particular role or roles, you list the roles like so ::
|
||||
|
||||
{
|
||||
"audit:create": ["role:admin", "role:superuser"]
|
||||
}
|
||||
|
||||
The above would add a rule that only allowed users that had roles of either
|
||||
"admin" or "superuser" to launch an audit.
|
||||
@@ -11,7 +11,7 @@ Watcher User Guide
|
||||
==================
|
||||
|
||||
See the
|
||||
`architecture page <https://factory.b-com.com/www/watcher/doc/watcher/architecture.html>`_
|
||||
`architecture page <http://docs.openstack.org/developer/watcher/architecture.html>`_
|
||||
for an architectural overview of the different components of Watcher and how
|
||||
they fit together.
|
||||
|
||||
@@ -99,6 +99,16 @@ or::
|
||||
|
||||
$ openstack optimize strategy list --goal-uuid <your_goal_uuid>
|
||||
|
||||
You can use the following command to check strategy details including which
|
||||
parameters of which format it supports:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ watcher strategy show <your_strategy>
|
||||
|
||||
or::
|
||||
|
||||
$ openstack optimize strategy show <your_strategy>
|
||||
|
||||
The command to create your audit template would then be:
|
||||
|
||||
@@ -140,6 +150,26 @@ or::
|
||||
|
||||
$ openstack optimize audit create -a <your_audit_template>
|
||||
|
||||
If your_audit_template was created by --strategy <your_strategy>, and it
|
||||
defines some parameters (command `watcher strategy show` to check parameters
|
||||
format), your can append `-p` to input required parameters:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ watcher audit create -a <your_audit_template> \
|
||||
-p <your_strategy_para1>=5.5 -p <your_strategy_para2>=hi
|
||||
|
||||
or::
|
||||
|
||||
$ openstack optimize audit create -a <your_audit_template> \
|
||||
-p <your_strategy_para1>=5.5 -p <your_strategy_para2>=hi
|
||||
|
||||
Input parameter could cause audit creation failure, when:
|
||||
|
||||
- no predefined strategy for audit template
|
||||
- no parameters spec in predefined strategy
|
||||
- input parameters don't comply with spec
|
||||
|
||||
Watcher service will compute an :ref:`Action Plan <action_plan_definition>`
|
||||
composed of a list of potential optimization :ref:`actions <action_definition>`
|
||||
(instance migration, disabling of an hypervisor, ...) according to the
|
||||
|
||||
@@ -64,8 +64,8 @@ IRC Channel
|
||||
``#openstack-watcher`` (changelog_)
|
||||
|
||||
Weekly Meetings
|
||||
on Wednesdays at 14:00 UTC in the ``#openstack-meeting-4`` IRC
|
||||
channel (`meetings logs`_)
|
||||
on Wednesdays at 14:00 UTC on even weeks, 9:00 UTC on odd weeks, 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/
|
||||
|
||||
@@ -17,7 +17,7 @@ To install Watcher from packaging, refer instead to Watcher `User
|
||||
Documentation`_.
|
||||
|
||||
.. _`Git Repository`: http://git.openstack.org/cgit/openstack/watcher
|
||||
.. _`User Documentation`: https://factory.b-com.com/www/watcher/doc/watcher/
|
||||
.. _`User Documentation`: http://docs.openstack.org/developer/watcher/
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
.. _implement_action_plugin:
|
||||
|
||||
==================
|
||||
Build a new action
|
||||
==================
|
||||
@@ -203,6 +205,9 @@ 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.
|
||||
|
||||
At the last, remember to add the action into the weights in ``watcher.conf``,
|
||||
otherwise you will get an error when the action be referenced in a strategy.
|
||||
|
||||
|
||||
Scheduling of an action plugin
|
||||
==============================
|
||||
|
||||
@@ -71,8 +71,15 @@ structure that looks like this::
|
||||
│ └── test_thirdparty.py
|
||||
└── tox.ini
|
||||
|
||||
**Note:** You should add `python-watcher`_ as a dependency in the
|
||||
requirements.txt file::
|
||||
|
||||
# Watcher-specific requirements
|
||||
python-watcher
|
||||
|
||||
.. _cookiecutter: https://github.com/audreyr/cookiecutter
|
||||
.. _OpenStack cookiecutter: https://github.com/openstack-dev/cookiecutter
|
||||
.. _python-watcher: https://pypi.python.org/pypi/python-watcher
|
||||
|
||||
Implementing a plugin for Watcher
|
||||
=================================
|
||||
@@ -83,7 +90,7 @@ plugins for Watcher:
|
||||
|
||||
- A :ref:`strategy plugin <implement_strategy_plugin>`
|
||||
- A :ref:`planner plugin <implement_planner_plugin>`
|
||||
- An :ref:`action plugin <implement_strategy_plugin>`
|
||||
- An :ref:`action plugin <implement_action_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
|
||||
|
||||
215
doc/source/dev/plugin/goal-plugin.rst
Normal file
@@ -0,0 +1,215 @@
|
||||
..
|
||||
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_goal_plugin:
|
||||
|
||||
================
|
||||
Build a new goal
|
||||
================
|
||||
|
||||
Watcher Decision Engine has an external :ref:`goal <goal_definition>`
|
||||
plugin interface which gives anyone the ability to integrate an external
|
||||
goal which can be achieved by a :ref:`strategy <strategy_definition>`.
|
||||
|
||||
This section gives some guidelines on how to implement and integrate custom
|
||||
goals 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 goal, please make sure that none of the existing goals fit
|
||||
your needs. Indeed, the underlying value of defining a goal is to be able to
|
||||
compare the efficacy of the action plans resulting from the various strategies
|
||||
satisfying the same goal. By doing so, Watcher can assist the administrator
|
||||
in his choices.
|
||||
|
||||
|
||||
Create a new plugin
|
||||
===================
|
||||
|
||||
In order to create a new goal, you have to:
|
||||
|
||||
- Extend the :py:class:`~.base.Goal` class.
|
||||
- Implement its :py:meth:`~.Goal.get_name` class method to return the
|
||||
**unique** ID of the new goal you want to create. This unique ID should
|
||||
be the same as the name of :ref:`the entry point you will declare later on
|
||||
<goal_plugin_add_entrypoint>`.
|
||||
- Implement its :py:meth:`~.Goal.get_display_name` class method to
|
||||
return the translated display name of the goal 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:`~.Goal.get_translatable_display_name`
|
||||
class method to return the translation key (actually the english display
|
||||
name) of your new goal. The value return should be the same as the
|
||||
string translated in :py:meth:`~.Goal.get_display_name`.
|
||||
- Implement its :py:meth:`~.Goal.get_efficacy_specification` method to return
|
||||
the :ref:`efficacy specification <efficacy_specification_definition>` for
|
||||
your goal.
|
||||
|
||||
Here is an example showing how you can define a new ``NewGoal`` goal plugin:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# filepath: thirdparty/new.py
|
||||
# import path: thirdparty.new
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.decision_engine.goal.efficacy import specs
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
class NewGoal(base.Goal):
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "new_goal" # Will be the name of the entry point
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("New Goal")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "New Goal"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
return specs.UnclassifiedStrategySpecification()
|
||||
|
||||
|
||||
As you may have noticed, the :py:meth:`~.Goal.get_efficacy_specification`
|
||||
method returns an :py:meth:`~.UnclassifiedStrategySpecification` instance which
|
||||
is provided by Watcher. This efficacy specification is useful during the
|
||||
development process of your goal as it corresponds to an empty specification.
|
||||
If you want to learn more about what efficacy specifications are used for or to
|
||||
define your own efficacy specification, please refer to the :ref:`related
|
||||
section below <implement_efficacy_specification>`.
|
||||
|
||||
|
||||
Abstract Plugin Class
|
||||
=====================
|
||||
|
||||
Here below is the abstract :py:class:`~.base.Goal` class:
|
||||
|
||||
.. autoclass:: watcher.decision_engine.goal.base.Goal
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
.. _goal_plugin_add_entrypoint:
|
||||
|
||||
Add a new entry point
|
||||
=====================
|
||||
|
||||
In order for the Watcher Decision Engine to load your new goal, the
|
||||
goal must be registered as a named entry point under the ``watcher_goals``
|
||||
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 and should be the same
|
||||
as the value returned by the :py:meth:`~.base.Goal.get_name` class method of
|
||||
your goal.
|
||||
|
||||
Here below is how you would proceed to register ``NewGoal`` using pbr_:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[entry_points]
|
||||
watcher_goals =
|
||||
new_goal = thirdparty.new:NewGoal
|
||||
|
||||
|
||||
To get a better understanding on how to implement a more advanced goal,
|
||||
have a look at the :py:class:`~.ServerConsolidation` class.
|
||||
|
||||
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||
|
||||
.. _implement_efficacy_specification:
|
||||
|
||||
Implement a customized efficacy specification
|
||||
=============================================
|
||||
|
||||
What is it for?
|
||||
---------------
|
||||
|
||||
Efficacy specifications define a set of specifications for a given goal.
|
||||
These specifications actually define a list of indicators which are to be used
|
||||
to compute a global efficacy that outlines how well a strategy performed when
|
||||
trying to achieve the goal it is associated to.
|
||||
|
||||
The idea behind such specification is to give the administrator the possibility
|
||||
to run an audit using different strategies satisfying the same goal and be able
|
||||
to judge how they performed at a glance.
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
In order to create a new efficacy specification, you have to:
|
||||
|
||||
- Extend the :py:class:`~.EfficacySpecification` class.
|
||||
- Implement :py:meth:`~.EfficacySpecification.get_indicators_specifications`
|
||||
by returning a list of :py:class:`~.IndicatorSpecification` instances.
|
||||
|
||||
* Each :py:class:`~.IndicatorSpecification` instance should actually extend
|
||||
the latter.
|
||||
* Each indicator specification should have a **unique name** which should be
|
||||
a valid Python variable name.
|
||||
* They should implement the :py:attr:`~.EfficacySpecification.schema`
|
||||
abstract property by returning a :py:class:`~.voluptuous.Schema` instance.
|
||||
This schema is the contract the strategy will have to comply with when
|
||||
setting the value associated to the indicator specification within its
|
||||
solution (see the :ref:`architecture of Watcher
|
||||
<sequence_diagrams_create_and_launch_audit>` for more information on
|
||||
the audit execution workflow).
|
||||
|
||||
- Implement the :py:meth:`~.EfficacySpecification.get_global_efficacy` method:
|
||||
it should compute the global efficacy for the goal it achieves based on the
|
||||
efficacy indicators you just defined.
|
||||
|
||||
Here below is an example of an efficacy specification containing one indicator
|
||||
specification:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.decision_engine.goal.efficacy import base as efficacy_base
|
||||
from watcher.decision_engine.goal.efficacy import indicators
|
||||
from watcher.decision_engine.solution import efficacy
|
||||
|
||||
|
||||
class IndicatorExample(IndicatorSpecification):
|
||||
def __init__(self):
|
||||
super(IndicatorExample, self).__init__(
|
||||
name="indicator_example",
|
||||
description=_("Example of indicator specification."),
|
||||
unit=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema(voluptuous.Range(min=0), required=True)
|
||||
|
||||
|
||||
class UnclassifiedStrategySpecification(efficacy_base.EfficacySpecification):
|
||||
|
||||
def get_indicators_specifications(self):
|
||||
return [IndicatorExample()]
|
||||
|
||||
def get_global_efficacy(self, indicators_map):
|
||||
return efficacy.Indicator(
|
||||
name="global_efficacy_indicator",
|
||||
description="Example of global efficacy indicator",
|
||||
unit="%",
|
||||
value=indicators_map.indicator_example % 100)
|
||||
|
||||
|
||||
To get a better understanding on how to implement an efficacy specification,
|
||||
have a look at :py:class:`~.ServerConsolidationSpecification`.
|
||||
|
||||
Also, if you want to see a concrete example of an indicator specification,
|
||||
have a look at :py:class:`~.ReleasedComputeNodesCount`.
|
||||
@@ -11,9 +11,10 @@ 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.
|
||||
external :ref:`planner <watcher_planner_definition>` plugin interface which
|
||||
gives anyone the ability to integrate an external :ref:`planner
|
||||
<watcher_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.
|
||||
|
||||
@@ -28,8 +28,8 @@ configured so that it would provide you all the metrics you need to be able to
|
||||
use your strategy.
|
||||
|
||||
|
||||
Create a new plugin
|
||||
===================
|
||||
Create a new strategy plugin
|
||||
============================
|
||||
|
||||
In order to create a new strategy, you have to:
|
||||
|
||||
@@ -53,6 +53,8 @@ Here is an example showing how you can write a plugin called ``NewStrategy``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# filepath: thirdparty/new.py
|
||||
# import path: thirdparty.new
|
||||
import abc
|
||||
|
||||
import six
|
||||
@@ -88,9 +90,12 @@ Here is an example showing how you can write a plugin called ``NewStrategy``:
|
||||
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>`.
|
||||
solution is then processed by a :ref:`planner <watcher_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>`. This
|
||||
solution also contains the various :ref:`efficacy indicators
|
||||
<efficacy_indicator_definition>` alongside its computed :ref:`global efficacy
|
||||
<efficacy_definition>`.
|
||||
|
||||
Please note that your strategy class will expect to find the same constructor
|
||||
signature as BaseStrategy to instantiate you strategy. Therefore, you should
|
||||
@@ -98,7 +103,7 @@ ensure that your ``__init__`` signature is identical to the
|
||||
:py:class:`~.BaseStrategy` one.
|
||||
|
||||
|
||||
Create a new goal
|
||||
Strategy efficacy
|
||||
=================
|
||||
|
||||
As stated before, the ``NewStrategy`` class extends a class called
|
||||
@@ -106,126 +111,76 @@ As stated before, the ``NewStrategy`` class extends a class called
|
||||
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:
|
||||
One thing this :py:class:`~.UnclassifiedStrategy` class defines is that our
|
||||
``NewStrategy`` achieves the ``unclassified`` goal. This goal is a peculiar one
|
||||
as it does not contain any indicator nor does it calculate a global efficacy.
|
||||
This proves itself to be quite useful during the development of a new strategy
|
||||
for which the goal has yet to be defined or in case a :ref:`new goal
|
||||
<implement_goal_plugin>` has yet to be implemented.
|
||||
|
||||
- :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:
|
||||
Define Strategy Parameters
|
||||
==========================
|
||||
|
||||
- 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`.
|
||||
For each new added strategy, you can add parameters spec so that an operator
|
||||
can input strategy parameters when creating an audit to control the
|
||||
:py:meth:`~.BaseStrategy.execute` behavior of strategy. This is useful to
|
||||
define some threshold for your strategy, and tune them at runtime.
|
||||
|
||||
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:
|
||||
To define parameters, just implements :py:meth:`~.BaseStrategy.get_schema` to
|
||||
return parameters spec with `jsonschema
|
||||
<http://json-schema.org/>`_ format.
|
||||
It is strongly encouraged that provide default value for each parameter, or
|
||||
else reference fails if operator specify no parameters.
|
||||
|
||||
Here is an example showing how you can define 2 parameters for
|
||||
``DummyStrategy``:
|
||||
|
||||
.. 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):
|
||||
class DummyStrategy(base.DummyBaseStrategy):
|
||||
|
||||
@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"
|
||||
def get_schema(cls):
|
||||
return {
|
||||
"properties": {
|
||||
"para1": {
|
||||
"description": "number parameter example",
|
||||
"type": "number",
|
||||
"default": 3.2,
|
||||
"minimum": 1.0,
|
||||
"maximum": 10.2,
|
||||
},
|
||||
"para2": {
|
||||
"description": "string parameter example",
|
||||
"type": "string",
|
||||
"default": "hello",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
You can reference parameters in :py:meth:`~.BaseStrategy.execute`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from oslo_config import cfg
|
||||
class DummyStrategy(base.DummyBaseStrategy):
|
||||
|
||||
class NewStrategy(NewGoalBaseStrategy):
|
||||
def execute(self):
|
||||
para1 = self.input_parameters.para1
|
||||
para2 = self.input_parameters.para2
|
||||
|
||||
# [...]
|
||||
|
||||
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 ...
|
||||
]
|
||||
if para1 > 5:
|
||||
...
|
||||
|
||||
|
||||
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:
|
||||
Operator can specify parameters with following commands:
|
||||
|
||||
.. code-block:: ini
|
||||
.. code:: bash
|
||||
|
||||
[watcher_strategies.new_strategy]
|
||||
# Option used for testing.
|
||||
test_opt = test_value
|
||||
$ watcher audit create -a <your_audit_template> -p para1=6.0 -p para2=hi
|
||||
|
||||
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.
|
||||
Pls. check user-guide for details.
|
||||
|
||||
|
||||
Abstract Plugin Class
|
||||
@@ -249,16 +204,16 @@ strategy must be registered as a named entry point under the
|
||||
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
|
||||
as the value returned by the :py:meth:`~.BaseStrategy.get_name` class method of
|
||||
your strategy.
|
||||
|
||||
Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
|
||||
Here below is how you would proceed to register ``NewStrategy`` using pbr_:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[entry_points]
|
||||
watcher_strategies =
|
||||
dummy_strategy = thirdparty.dummy:DummyStrategy
|
||||
new_strategy = thirdparty.new:NewStrategy
|
||||
|
||||
|
||||
To get a better understanding on how to implement a more advanced strategy,
|
||||
|
||||
@@ -13,6 +13,13 @@ 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_goals:
|
||||
|
||||
Goals
|
||||
=====
|
||||
|
||||
.. drivers-doc:: watcher_goals
|
||||
|
||||
.. _watcher_strategies:
|
||||
|
||||
Strategies
|
||||
|
||||
@@ -131,7 +131,8 @@ can potentially be hosted on a dedicated machine.
|
||||
Compute node
|
||||
============
|
||||
|
||||
Please, read `the official OpenStack definition of a Compute Node <http://docs.openstack.org/openstack-ops/content/compute_nodes.html>`_.
|
||||
Please, read `the official OpenStack definition of a Compute Node
|
||||
<http://docs.openstack.org/openstack-ops/content/compute_nodes.html>`_.
|
||||
|
||||
.. _customer_definition:
|
||||
|
||||
@@ -211,7 +212,23 @@ Here are some examples of
|
||||
- `Sahara Hadoop Cluster <http://docs.openstack.org/developer/heat/template_guide/openstack.html#OS::Sahara::Cluster>`_
|
||||
- ...
|
||||
|
||||
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>`_.
|
||||
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>`_.
|
||||
|
||||
.. _efficacy_indicator_definition:
|
||||
|
||||
Efficacy Indicator
|
||||
==================
|
||||
|
||||
.. watcher-term:: watcher.api.controllers.v1.efficacy_indicator
|
||||
|
||||
.. _efficacy_specification_definition:
|
||||
|
||||
Efficacy Specification
|
||||
======================
|
||||
|
||||
.. watcher-term:: watcher.decision_engine.goal.efficacy.base
|
||||
|
||||
.. _efficacy_definition:
|
||||
|
||||
@@ -234,14 +251,15 @@ to be launched).
|
||||
|
||||
For example, if the :ref:`Goal <goal_definition>` is to lower the energy
|
||||
consumption, the :ref:`Efficacy <efficacy_definition>` will be computed
|
||||
using several indicators (KPIs):
|
||||
using several :ref:`efficacy indicators <efficacy_indicator_definition>`
|
||||
(KPIs):
|
||||
|
||||
- the percentage of energy gain (which must be the highest possible)
|
||||
- the number of :ref:`SLA violations <sla_violation_definition>`
|
||||
(which must be the lowest possible)
|
||||
- the number of virtual machine migrations (which must be the lowest possible)
|
||||
|
||||
All those indicators (KPIs) are computed within a given timeframe, which is the
|
||||
All those indicators are computed within a given timeframe, which is the
|
||||
time taken to execute the whole :ref:`Action Plan <action_plan_definition>`.
|
||||
|
||||
The efficacy also enables the :ref:`Administrator <administrator_definition>`
|
||||
@@ -259,7 +277,8 @@ OpenStack should be owned by a specific :ref:`project <project_definition>`.
|
||||
In OpenStack Identity, a :ref:`project <project_definition>` must be owned by a
|
||||
specific domain.
|
||||
|
||||
Please, read `the official OpenStack definition of a Project <http://docs.openstack.org/glossary/content/glossary.html>`_.
|
||||
Please, read `the official OpenStack definition of a Project
|
||||
<http://docs.openstack.org/glossary/content/glossary.html>`_.
|
||||
|
||||
|
||||
.. _sla_definition:
|
||||
@@ -364,4 +383,3 @@ Watcher Planner
|
||||
===============
|
||||
|
||||
.. watcher-term:: watcher.decision_engine.planner.base
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
@startuml
|
||||
|
||||
skinparam maxMessageSize 200
|
||||
|
||||
"Decision Engine" -> "Decision Engine" : Execute audit
|
||||
activate "Decision Engine"
|
||||
"Decision Engine" -> "Decision Engine" : Set the audit state to ONGOING
|
||||
|
||||
"Decision Engine" -> "Strategy selector" : Select strategy
|
||||
activate "Strategy selector"
|
||||
alt A specific strategy is provided
|
||||
"Strategy selector" -> "Strategy selector" : Load strategy and inject the \
|
||||
cluster data model
|
||||
else Only a goal is specified
|
||||
"Strategy selector" -> "Strategy selector" : select strategy
|
||||
"Strategy selector" -> "Strategy selector" : Load strategy and inject the \
|
||||
cluster data model
|
||||
end
|
||||
"Strategy selector" -> "Decision Engine" : Return loaded Strategy
|
||||
deactivate "Strategy selector"
|
||||
|
||||
"Decision Engine" -> "Strategy" : Execute the strategy
|
||||
activate "Strategy"
|
||||
"Strategy" -> "Strategy" : **pre_execute()**Checks if the strategy \
|
||||
pre-requisites are all set.
|
||||
"Strategy" -> "Strategy" : **do_execute()**Contains the logic of the strategy
|
||||
"Strategy" -> "Strategy" : **post_execute()** Set the efficacy indicators
|
||||
"Strategy" -> "Strategy" : Compute the global efficacy of the solution \
|
||||
based on the provided efficacy indicators
|
||||
"Strategy" -> "Decision Engine" : Return the solution
|
||||
deactivate "Strategy"
|
||||
|
||||
"Decision Engine" -> "Planner" : Plan the solution that was computed by the \
|
||||
strategy
|
||||
activate "Planner"
|
||||
"Planner" -> "Planner" : Store the planned solution as an action plan with its \
|
||||
related actions and efficacy indicators
|
||||
"Planner" --> "Decision Engine" : Done
|
||||
deactivate "Planner"
|
||||
"Decision Engine" -> "Decision Engine" : Update the audit state to SUCCEEDED
|
||||
|
||||
deactivate "Decision Engine"
|
||||
|
||||
@enduml
|
||||
@@ -1,31 +1,54 @@
|
||||
@startuml
|
||||
|
||||
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid)
|
||||
"Watcher Decision Engine" -> "Watcher Database" : update audit.state = ONGOING
|
||||
"AMQP Bus" <[#blue]- "Watcher Decision Engine" : notify new audit state = ONGOING
|
||||
"Watcher Decision Engine" -> "Watcher Database" : get audit parameters(goal, ...)
|
||||
"Watcher Decision Engine" <-- "Watcher Database" : audit parameters(goal, ...)
|
||||
skinparam maxMessageSize 100
|
||||
|
||||
"AMQP Bus" -> "Decision Engine" : trigger audit
|
||||
|
||||
activate "Decision Engine"
|
||||
|
||||
"Decision Engine" -> "Database" : update audit.state = ONGOING
|
||||
"AMQP Bus" <[#blue]- "Decision Engine" : notify new audit state = ONGOING
|
||||
"Decision Engine" -> "Database" : get audit parameters (goal, strategy, ...)
|
||||
"Decision Engine" <-- "Database" : audit parameters (goal, strategy, ...)
|
||||
"Decision Engine" --> "Decision Engine": select appropriate \
|
||||
optimization strategy (via the Strategy Selector)
|
||||
create Strategy
|
||||
"Watcher Decision Engine" -[#red]> "Strategy": select appropriate\noptimization strategy
|
||||
loop while enough data to build cluster data model
|
||||
"Watcher Decision Engine" -> "Nova API" : get resource state (host, instance, ...)
|
||||
"Watcher Decision Engine" <-- "Nova API" : resource state
|
||||
end
|
||||
"Watcher Decision Engine" -[#red]> "Watcher Decision Engine": build cluster_data_model
|
||||
"Watcher Decision Engine" -> "Strategy" : execute(cluster_data_model)
|
||||
"Decision Engine" -> "Strategy" : execute()
|
||||
activate "Strategy"
|
||||
create "Cluster Data Model Collector"
|
||||
"Strategy" -> "Cluster Data Model Collector" : get cluster data model
|
||||
|
||||
activate "Cluster Data Model Collector"
|
||||
loop while enough data to build cluster data model
|
||||
"Cluster Data Model Collector" -> "Nova API" : get resource state (\
|
||||
host, instance, ...)
|
||||
"Cluster Data Model Collector" <-- "Nova API" : resource state
|
||||
end
|
||||
"Cluster Data Model Collector" -> "Strategy" : cluster data model
|
||||
deactivate "Cluster Data Model Collector"
|
||||
|
||||
loop while enough history data for the strategy
|
||||
"Strategy" -> "Ceilometer API": get_aggregated_metrics\n(resource_id,meter_name,period,aggregate_method)
|
||||
"Strategy" <-- "Ceilometer API": aggregated metrics
|
||||
"Strategy" -> "Ceilometer API": get necessary metrics
|
||||
"Strategy" <-- "Ceilometer API": aggregated metrics
|
||||
end
|
||||
"Strategy" -> "Strategy" : compute solution to achieve goal
|
||||
"Watcher Decision Engine" <-- "Strategy" : solution = array of actions (i.e. not scheduled yet)
|
||||
create "Watcher Planner"
|
||||
"Watcher Decision Engine" -[#red]> "Watcher Planner": select appropriate actions scheduler (i.e. Planner implementation)
|
||||
"Watcher Decision Engine" -> "Watcher Planner": schedule(audit_id, solution)
|
||||
"Watcher Planner" -> "Watcher Planner": schedule actions according to\nscheduling rules/policies
|
||||
"Watcher Decision Engine" <-- "Watcher Planner": new action_plan
|
||||
"Watcher Decision Engine" -> "Watcher Database" : save new action_plan in database
|
||||
"Watcher Decision Engine" -> "Watcher Database" : update audit.state = SUCCEEDED
|
||||
"AMQP Bus" <[#blue]- "Watcher Decision Engine" : notify new audit state = SUCCEEDED
|
||||
"Strategy" -> "Strategy" : compute/set needed actions for the solution \
|
||||
so it achieves its goal
|
||||
"Strategy" -> "Strategy" : compute/set efficacy indicators for the solution
|
||||
"Strategy" -> "Strategy" : compute/set the solution global efficacy
|
||||
"Decision Engine" <-- "Strategy" : solution (contains a list of unordered \
|
||||
actions alongside its efficacy indicators as well as its global efficacy)
|
||||
deactivate "Strategy"
|
||||
|
||||
"Decision Engine" --> "Planner": load actions scheduler (i.e. Planner plugin)
|
||||
create "Planner"
|
||||
"Decision Engine" -> "Planner": schedule()
|
||||
"Planner" -> "Planner": schedule actions according to \
|
||||
scheduling rules/policies
|
||||
"Decision Engine" <-- "Planner": new action plan
|
||||
"Decision Engine" -> "Database" : save new action plan in database
|
||||
"Decision Engine" -> "Database" : update audit.state = SUCCEEDED
|
||||
"AMQP Bus" <[#blue]- "Decision Engine" : notify new audit state = SUCCEEDED
|
||||
|
||||
deactivate "Decision Engine"
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
@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
|
||||
123
doc/source/image_src/plantuml/watcher_db_schema_diagram.txt
Normal file
@@ -0,0 +1,123 @@
|
||||
@startuml
|
||||
!define table(x) class x << (T,#FFAAAA) >>
|
||||
!define primary_key(x) <u>x</u>
|
||||
!define foreign_key(x) <i><u>x</u></i>
|
||||
hide methods
|
||||
hide stereotypes
|
||||
|
||||
table(goal) {
|
||||
primary_key(id: Integer)
|
||||
uuid : String[36]
|
||||
name : String[63]
|
||||
display_name : String[63]
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
|
||||
table(strategy) {
|
||||
primary_key(id: Integer)
|
||||
foreign_key(goal_id : Integer)
|
||||
uuid : String[36]
|
||||
name : String[63]
|
||||
display_name : String[63]
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
|
||||
table(audit_template) {
|
||||
primary_key(id: Integer)
|
||||
foreign_key("goal_id : Integer")
|
||||
foreign_key("strategy_id : Integer, nullable")
|
||||
uuid : String[36]
|
||||
name : String[63], nullable
|
||||
description : String[255], nullable
|
||||
host_aggregate : Integer, nullable
|
||||
extra : JSONEncodedDict
|
||||
version : String[15], nullable
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
|
||||
table(audit) {
|
||||
primary_key(id: Integer)
|
||||
foreign_key("audit_template_id : Integer")
|
||||
uuid : String[36]
|
||||
audit_type : String[20]
|
||||
state : String[20], nullable
|
||||
deadline :DateTime, nullable
|
||||
interval : Integer, nullable
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
|
||||
table(action_plan) {
|
||||
primary_key(id: Integer)
|
||||
foreign_key("audit_id : Integer, nullable")
|
||||
uuid : String[36]
|
||||
first_action_id : Integer
|
||||
state : String[20], nullable
|
||||
global_efficacy : JSONEncodedDict, nullable
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
|
||||
table(action) {
|
||||
primary_key(id: Integer)
|
||||
foreign_key("action_plan_id : Integer")
|
||||
uuid : String[36]
|
||||
action_type : String[255]
|
||||
input_parameters : JSONEncodedDict, nullable
|
||||
state : String[20], nullable
|
||||
next : String[36], nullable
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
|
||||
table(efficacy_indicator) {
|
||||
primary_key(id: Integer)
|
||||
foreign_key("action_plan_id : Integer")
|
||||
uuid : String[36]
|
||||
name : String[63]
|
||||
description : String[255], nullable
|
||||
unit : String[63], nullable
|
||||
value : Numeric
|
||||
|
||||
created_at : DateTime
|
||||
updated_at : DateTime
|
||||
deleted_at : DateTime
|
||||
deleted : Integer
|
||||
}
|
||||
|
||||
"goal" <.. "strategy" : Foreign Key
|
||||
"goal" <.. "audit_template" : Foreign Key
|
||||
"strategy" <.. "audit_template" : Foreign Key
|
||||
"audit_template" <.. "audit" : Foreign Key
|
||||
"action_plan" <.. "action" : Foreign Key
|
||||
"action_plan" <.. "efficacy_indicator" : Foreign Key
|
||||
"audit" <.. "action_plan" : Foreign Key
|
||||
|
||||
@enduml
|
||||
@@ -1,139 +1,600 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
|
||||
<svg width="58cm" height="28cm" viewBox="26 8 1147 549" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="570" y="99" width="148.6" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="570" y="99" width="148.6" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="644.3" y="118">Audit Template</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="212" y="140" width="177.5" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="212" y="140" width="177.5" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="300.75" y="159">OpenStack Cluster</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="569.006,113 446,113 446,154 397.19,154 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="409.838,149 393.838,154 409.838,159 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="448" y="130.5">Applies to</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="615" y="227" width="58.4" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="615" y="227" width="58.4" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="644.2" y="246">Audit</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="644.2,225.996 644.2,188.497 644.3,188.497 644.3,133.705 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="649.3,146.353 644.3,130.353 639.3,146.353 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="644.25" y="185.497">gets configuration from</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="916" y="9" width="50.45" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="916" y="9" width="50.45" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="941.225" y="28">Goal</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="644.3,97.9927 644.3,72 941.225,72 941.225,44.7125 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="946.225,57.3599 941.225,41.3599 936.225,57.3599 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="792.763" y="69">Achieves</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="495" y="367" width="112.45" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="495" y="367" width="112.45" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="551.225" y="386">Action Plan</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="644.2,256 644.2,298.5 523,298.5 523,356.295 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="518,343.647 523,359.647 528,343.647 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="583.6" y="295.5">Generates</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="682" y="471" width="67.45" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="682" y="471" width="67.45" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="715.725" y="490">Action</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="551.225,420.945 551.225,443 715.725,443 715.725,470.029 "/>
|
||||
<polygon style="fill: #000000" points="551.225,395.773 556.025,409.773 551.225,423.773 546.425,409.773 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="551.225,395.773 556.025,409.773 551.225,423.773 546.425,409.773 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="633.475" y="440">is composed of</text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="562.225" y="407.773"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="719.725" y="466.029"></text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="749.45,499 749.45,517 862,517 862,485 749.45,485 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="805.725" y="514">Next action</text>
|
||||
<polygon style="fill: #000000" points="850.075,514 850.075,506 858.075,510 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="753.45" y="511"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="753.45" y="482"></text>
|
||||
</g>
|
||||
<g>
|
||||
<ellipse style="fill: #ffffff" cx="1036" cy="219" rx="6" ry="6"/>
|
||||
<ellipse style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" cx="1036" cy="219" rx="6" ry="6"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1012" y1="231" x2="1060" y2="231"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="225" x2="1036" y2="255"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="255" x2="1012" y2="281"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="255" x2="1060" y2="281"/>
|
||||
<text font-size="12.8" style="fill: #ff0000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="1036" y="304.9">
|
||||
<tspan x="1036" y="304.9">Administrator</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="985.869,255 834.138,255 834.138,241 681.113,241 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="693.76,236 677.76,241 693.76,246 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="836.138" y="245">Triggers</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="1038,192.993 882.3,192.993 882.3,113 725.305,113 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="737.953,108 721.953,113 737.953,118 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="884.3" y="149.997">Defines Audit configuration in</text>
|
||||
</g>
|
||||
<g>
|
||||
<ellipse style="fill: #ffffff" cx="103" cy="28" rx="6" ry="6"/>
|
||||
<ellipse style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" cx="103" cy="28" rx="6" ry="6"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="79" y1="40" x2="127" y2="40"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="34" x2="103" y2="64"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="64" x2="79" y2="90"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="64" x2="127" y2="90"/>
|
||||
<text font-size="12.8" style="fill: #ff0000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="103" y="113.9">
|
||||
<tspan x="103" y="113.9">Customer</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="137.683,64 300.75,64 300.75,133.295 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="295.75,120.647 300.75,136.647 305.75,120.647 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="219.217" y="61">Consumes resources</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="27" y="258" width="102.8" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="27" y="258" width="102.8" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="78.4" y="277">Resources</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="186.828,154 78.4,154 78.4,258 "/>
|
||||
<polygon style="fill: #000000" points="212,154 198,158.8 184,154 198,149.2 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="212,154 198,158.8 184,154 198,149.2 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="145.2" y="151"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:end;font-family:monospace;font-style:normal;font-weight:normal" x="180" y="151"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="82.4" y="254"></text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="715.724,499 715.724,540 78.4,540 78.4,292.705 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="83.4,305.353 78.4,289.353 73.4,305.353 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="397.062" y="537">Modifies</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="1036,309.94 1036,381 614.155,381 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="626.803,376 610.803,381 626.803,386 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="821.725" y="378">Launches</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="1082.9" y="43.1" width="88.25" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="1082.9" y="43.1" width="88.25" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="1127.02" y="62.1">Strategy</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="966.45,23 1020.17,23 1020.17,57.1 1075.22,57.1 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="1062.57,62.1 1078.57,57.1 1062.57,52.1 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="1022.17" y="37.05">uses</text>
|
||||
</g>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1146pt" height="548pt" viewBox="0 0 1146 548" version="1.1">
|
||||
<defs>
|
||||
<g>
|
||||
<symbol overflow="visible" id="glyph0-0">
|
||||
<path style="stroke:none;" d="M 0.796875 2.828125 L 0.796875 -11.28125 L 8.796875 -11.28125 L 8.796875 2.828125 Z M 1.703125 1.9375 L 7.90625 1.9375 L 7.90625 -10.390625 L 1.703125 -10.390625 Z M 1.703125 1.9375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-1">
|
||||
<path style="stroke:none;" d="M 8.546875 -2.125 L 3.84375 -2.125 L 3.109375 0 L 0.078125 0 L 4.40625 -11.671875 L 7.984375 -11.671875 L 12.3125 0 L 9.28125 0 Z M 4.59375 -4.296875 L 7.796875 -4.296875 L 6.203125 -8.9375 Z M 4.59375 -4.296875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-2">
|
||||
<path style="stroke:none;" d="M 1.25 -3.40625 L 1.25 -8.75 L 4.0625 -8.75 L 4.0625 -7.875 C 4.0625 -7.40625 4.054688 -6.8125 4.046875 -6.09375 C 4.046875 -5.375 4.046875 -4.894531 4.046875 -4.65625 C 4.046875 -3.957031 4.0625 -3.453125 4.09375 -3.140625 C 4.132812 -2.828125 4.203125 -2.601562 4.296875 -2.46875 C 4.410156 -2.28125 4.554688 -2.132812 4.734375 -2.03125 C 4.921875 -1.9375 5.132812 -1.890625 5.375 -1.890625 C 5.957031 -1.890625 6.414062 -2.113281 6.75 -2.5625 C 7.082031 -3.007812 7.25 -3.632812 7.25 -4.4375 L 7.25 -8.75 L 10.046875 -8.75 L 10.046875 0 L 7.25 0 L 7.25 -1.265625 C 6.832031 -0.753906 6.382812 -0.375 5.90625 -0.125 C 5.4375 0.113281 4.921875 0.234375 4.359375 0.234375 C 3.347656 0.234375 2.578125 -0.078125 2.046875 -0.703125 C 1.515625 -1.328125 1.25 -2.226562 1.25 -3.40625 Z M 1.25 -3.40625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-3">
|
||||
<path style="stroke:none;" d="M 7.296875 -7.46875 L 7.296875 -12.15625 L 10.109375 -12.15625 L 10.109375 0 L 7.296875 0 L 7.296875 -1.265625 C 6.910156 -0.753906 6.484375 -0.375 6.015625 -0.125 C 5.554688 0.113281 5.023438 0.234375 4.421875 0.234375 C 3.335938 0.234375 2.445312 -0.191406 1.75 -1.046875 C 1.0625 -1.910156 0.71875 -3.019531 0.71875 -4.375 C 0.71875 -5.71875 1.0625 -6.816406 1.75 -7.671875 C 2.445312 -8.535156 3.335938 -8.96875 4.421875 -8.96875 C 5.023438 -8.96875 5.554688 -8.84375 6.015625 -8.59375 C 6.484375 -8.351562 6.910156 -7.976562 7.296875 -7.46875 Z M 5.453125 -1.8125 C 6.054688 -1.8125 6.515625 -2.03125 6.828125 -2.46875 C 7.140625 -2.90625 7.296875 -3.539062 7.296875 -4.375 C 7.296875 -5.207031 7.140625 -5.84375 6.828125 -6.28125 C 6.515625 -6.71875 6.054688 -6.9375 5.453125 -6.9375 C 4.859375 -6.9375 4.40625 -6.71875 4.09375 -6.28125 C 3.78125 -5.84375 3.625 -5.207031 3.625 -4.375 C 3.625 -3.539062 3.78125 -2.90625 4.09375 -2.46875 C 4.40625 -2.03125 4.859375 -1.8125 5.453125 -1.8125 Z M 5.453125 -1.8125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-4">
|
||||
<path style="stroke:none;" d="M 1.34375 -8.75 L 4.140625 -8.75 L 4.140625 0 L 1.34375 0 Z M 1.34375 -12.15625 L 4.140625 -12.15625 L 4.140625 -9.875 L 1.34375 -9.875 Z M 1.34375 -12.15625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-5">
|
||||
<path style="stroke:none;" d="M 4.40625 -11.234375 L 4.40625 -8.75 L 7.28125 -8.75 L 7.28125 -6.75 L 4.40625 -6.75 L 4.40625 -3.046875 C 4.40625 -2.640625 4.484375 -2.363281 4.640625 -2.21875 C 4.804688 -2.070312 5.128906 -2 5.609375 -2 L 7.046875 -2 L 7.046875 0 L 4.640625 0 C 3.535156 0 2.753906 -0.226562 2.296875 -0.6875 C 1.835938 -1.15625 1.609375 -1.941406 1.609375 -3.046875 L 1.609375 -6.75 L 0.21875 -6.75 L 0.21875 -8.75 L 1.609375 -8.75 L 1.609375 -11.234375 Z M 4.40625 -11.234375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-6">
|
||||
<path style="stroke:none;" d=""/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-7">
|
||||
<path style="stroke:none;" d="M 0.078125 -11.671875 L 10.828125 -11.671875 L 10.828125 -9.390625 L 6.96875 -9.390625 L 6.96875 0 L 3.953125 0 L 3.953125 -9.390625 L 0.078125 -9.390625 Z M 0.078125 -11.671875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-8">
|
||||
<path style="stroke:none;" d="M 10.078125 -4.40625 L 10.078125 -3.609375 L 3.546875 -3.609375 C 3.609375 -2.953125 3.84375 -2.457031 4.25 -2.125 C 4.65625 -1.800781 5.222656 -1.640625 5.953125 -1.640625 C 6.546875 -1.640625 7.148438 -1.722656 7.765625 -1.890625 C 8.378906 -2.066406 9.015625 -2.332031 9.671875 -2.6875 L 9.671875 -0.53125 C 9.003906 -0.28125 8.335938 -0.09375 7.671875 0.03125 C 7.015625 0.164062 6.359375 0.234375 5.703125 0.234375 C 4.117188 0.234375 2.882812 -0.164062 2 -0.96875 C 1.125 -1.78125 0.6875 -2.914062 0.6875 -4.375 C 0.6875 -5.800781 1.117188 -6.921875 1.984375 -7.734375 C 2.847656 -8.554688 4.035156 -8.96875 5.546875 -8.96875 C 6.921875 -8.96875 8.019531 -8.550781 8.84375 -7.71875 C 9.664062 -6.894531 10.078125 -5.789062 10.078125 -4.40625 Z M 7.203125 -5.328125 C 7.203125 -5.859375 7.046875 -6.285156 6.734375 -6.609375 C 6.429688 -6.941406 6.03125 -7.109375 5.53125 -7.109375 C 4.988281 -7.109375 4.546875 -6.953125 4.203125 -6.640625 C 3.867188 -6.335938 3.660156 -5.898438 3.578125 -5.328125 Z M 7.203125 -5.328125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-9">
|
||||
<path style="stroke:none;" d="M 9.453125 -7.296875 C 9.804688 -7.835938 10.226562 -8.25 10.71875 -8.53125 C 11.207031 -8.820312 11.742188 -8.96875 12.328125 -8.96875 C 13.328125 -8.96875 14.085938 -8.65625 14.609375 -8.03125 C 15.140625 -7.414062 15.40625 -6.515625 15.40625 -5.328125 L 15.40625 0 L 12.59375 0 L 12.59375 -4.5625 C 12.601562 -4.632812 12.609375 -4.707031 12.609375 -4.78125 C 12.609375 -4.851562 12.609375 -4.957031 12.609375 -5.09375 C 12.609375 -5.707031 12.515625 -6.15625 12.328125 -6.4375 C 12.148438 -6.71875 11.859375 -6.859375 11.453125 -6.859375 C 10.921875 -6.859375 10.507812 -6.640625 10.21875 -6.203125 C 9.9375 -5.765625 9.789062 -5.128906 9.78125 -4.296875 L 9.78125 0 L 6.96875 0 L 6.96875 -4.5625 C 6.96875 -5.53125 6.882812 -6.15625 6.71875 -6.4375 C 6.550781 -6.71875 6.253906 -6.859375 5.828125 -6.859375 C 5.285156 -6.859375 4.867188 -6.632812 4.578125 -6.1875 C 4.285156 -5.75 4.140625 -5.125 4.140625 -4.3125 L 4.140625 0 L 1.328125 0 L 1.328125 -8.75 L 4.140625 -8.75 L 4.140625 -7.46875 C 4.484375 -7.96875 4.878906 -8.34375 5.328125 -8.59375 C 5.773438 -8.84375 6.265625 -8.96875 6.796875 -8.96875 C 7.398438 -8.96875 7.929688 -8.820312 8.390625 -8.53125 C 8.859375 -8.238281 9.210938 -7.828125 9.453125 -7.296875 Z M 9.453125 -7.296875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-10">
|
||||
<path style="stroke:none;" d="M 4.140625 -1.265625 L 4.140625 3.328125 L 1.34375 3.328125 L 1.34375 -8.75 L 4.140625 -8.75 L 4.140625 -7.46875 C 4.523438 -7.976562 4.953125 -8.351562 5.421875 -8.59375 C 5.890625 -8.84375 6.429688 -8.96875 7.046875 -8.96875 C 8.117188 -8.96875 9 -8.535156 9.6875 -7.671875 C 10.382812 -6.816406 10.734375 -5.71875 10.734375 -4.375 C 10.734375 -3.019531 10.382812 -1.910156 9.6875 -1.046875 C 9 -0.191406 8.117188 0.234375 7.046875 0.234375 C 6.429688 0.234375 5.890625 0.113281 5.421875 -0.125 C 4.953125 -0.375 4.523438 -0.753906 4.140625 -1.265625 Z M 6 -6.9375 C 5.40625 -6.9375 4.945312 -6.710938 4.625 -6.265625 C 4.300781 -5.828125 4.140625 -5.195312 4.140625 -4.375 C 4.140625 -3.539062 4.300781 -2.90625 4.625 -2.46875 C 4.945312 -2.03125 5.40625 -1.8125 6 -1.8125 C 6.601562 -1.8125 7.0625 -2.03125 7.375 -2.46875 C 7.6875 -2.90625 7.84375 -3.539062 7.84375 -4.375 C 7.84375 -5.207031 7.6875 -5.84375 7.375 -6.28125 C 7.0625 -6.71875 6.601562 -6.9375 6 -6.9375 Z M 6 -6.9375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-11">
|
||||
<path style="stroke:none;" d="M 1.34375 -12.15625 L 4.140625 -12.15625 L 4.140625 0 L 1.34375 0 Z M 1.34375 -12.15625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-12">
|
||||
<path style="stroke:none;" d="M 5.265625 -3.9375 C 4.679688 -3.9375 4.242188 -3.835938 3.953125 -3.640625 C 3.660156 -3.441406 3.515625 -3.148438 3.515625 -2.765625 C 3.515625 -2.410156 3.628906 -2.132812 3.859375 -1.9375 C 4.097656 -1.738281 4.429688 -1.640625 4.859375 -1.640625 C 5.378906 -1.640625 5.816406 -1.828125 6.171875 -2.203125 C 6.535156 -2.578125 6.71875 -3.050781 6.71875 -3.625 L 6.71875 -3.9375 Z M 9.546875 -5 L 9.546875 0 L 6.71875 0 L 6.71875 -1.296875 C 6.34375 -0.765625 5.921875 -0.375 5.453125 -0.125 C 4.984375 0.113281 4.414062 0.234375 3.75 0.234375 C 2.84375 0.234375 2.101562 -0.03125 1.53125 -0.5625 C 0.96875 -1.09375 0.6875 -1.78125 0.6875 -2.625 C 0.6875 -3.65625 1.039062 -4.410156 1.75 -4.890625 C 2.457031 -5.367188 3.566406 -5.609375 5.078125 -5.609375 L 6.71875 -5.609375 L 6.71875 -5.828125 C 6.71875 -6.265625 6.539062 -6.585938 6.1875 -6.796875 C 5.84375 -7.003906 5.300781 -7.109375 4.5625 -7.109375 C 3.96875 -7.109375 3.410156 -7.046875 2.890625 -6.921875 C 2.378906 -6.804688 1.898438 -6.628906 1.453125 -6.390625 L 1.453125 -8.515625 C 2.054688 -8.660156 2.660156 -8.769531 3.265625 -8.84375 C 3.867188 -8.925781 4.472656 -8.96875 5.078125 -8.96875 C 6.648438 -8.96875 7.785156 -8.65625 8.484375 -8.03125 C 9.191406 -7.40625 9.546875 -6.394531 9.546875 -5 Z M 9.546875 -5 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-13">
|
||||
<path style="stroke:none;" d="M 6.796875 -9.703125 C 5.878906 -9.703125 5.164062 -9.363281 4.65625 -8.6875 C 4.15625 -8.007812 3.90625 -7.054688 3.90625 -5.828125 C 3.90625 -4.597656 4.15625 -3.644531 4.65625 -2.96875 C 5.164062 -2.289062 5.878906 -1.953125 6.796875 -1.953125 C 7.722656 -1.953125 8.4375 -2.289062 8.9375 -2.96875 C 9.445312 -3.644531 9.703125 -4.597656 9.703125 -5.828125 C 9.703125 -7.054688 9.445312 -8.007812 8.9375 -8.6875 C 8.4375 -9.363281 7.722656 -9.703125 6.796875 -9.703125 Z M 6.796875 -11.875 C 8.671875 -11.875 10.140625 -11.335938 11.203125 -10.265625 C 12.265625 -9.191406 12.796875 -7.710938 12.796875 -5.828125 C 12.796875 -3.941406 12.265625 -2.457031 11.203125 -1.375 C 10.140625 -0.300781 8.671875 0.234375 6.796875 0.234375 C 4.929688 0.234375 3.460938 -0.300781 2.390625 -1.375 C 1.328125 -2.457031 0.796875 -3.941406 0.796875 -5.828125 C 0.796875 -7.710938 1.328125 -9.191406 2.390625 -10.265625 C 3.460938 -11.335938 4.929688 -11.875 6.796875 -11.875 Z M 6.796875 -11.875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-14">
|
||||
<path style="stroke:none;" d="M 10.140625 -5.328125 L 10.140625 0 L 7.328125 0 L 7.328125 -4.078125 C 7.328125 -4.835938 7.3125 -5.359375 7.28125 -5.640625 C 7.25 -5.929688 7.191406 -6.144531 7.109375 -6.28125 C 6.992188 -6.457031 6.84375 -6.597656 6.65625 -6.703125 C 6.46875 -6.804688 6.253906 -6.859375 6.015625 -6.859375 C 5.429688 -6.859375 4.972656 -6.628906 4.640625 -6.171875 C 4.304688 -5.722656 4.140625 -5.101562 4.140625 -4.3125 L 4.140625 0 L 1.34375 0 L 1.34375 -8.75 L 4.140625 -8.75 L 4.140625 -7.46875 C 4.566406 -7.976562 5.015625 -8.351562 5.484375 -8.59375 C 5.960938 -8.84375 6.488281 -8.96875 7.0625 -8.96875 C 8.070312 -8.96875 8.835938 -8.65625 9.359375 -8.03125 C 9.878906 -7.414062 10.140625 -6.515625 10.140625 -5.328125 Z M 10.140625 -5.328125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-15">
|
||||
<path style="stroke:none;" d="M 9.59375 -11.296875 L 9.59375 -8.828125 C 8.945312 -9.117188 8.316406 -9.335938 7.703125 -9.484375 C 7.097656 -9.628906 6.523438 -9.703125 5.984375 -9.703125 C 5.265625 -9.703125 4.734375 -9.601562 4.390625 -9.40625 C 4.046875 -9.207031 3.875 -8.898438 3.875 -8.484375 C 3.875 -8.171875 3.988281 -7.925781 4.21875 -7.75 C 4.457031 -7.570312 4.878906 -7.421875 5.484375 -7.296875 L 6.765625 -7.046875 C 8.066406 -6.785156 8.988281 -6.390625 9.53125 -5.859375 C 10.082031 -5.328125 10.359375 -4.570312 10.359375 -3.59375 C 10.359375 -2.300781 9.972656 -1.335938 9.203125 -0.703125 C 8.441406 -0.078125 7.28125 0.234375 5.71875 0.234375 C 4.976562 0.234375 4.234375 0.160156 3.484375 0.015625 C 2.742188 -0.128906 2 -0.335938 1.25 -0.609375 L 1.25 -3.15625 C 2 -2.757812 2.71875 -2.457031 3.40625 -2.25 C 4.101562 -2.050781 4.773438 -1.953125 5.421875 -1.953125 C 6.078125 -1.953125 6.578125 -2.0625 6.921875 -2.28125 C 7.273438 -2.5 7.453125 -2.8125 7.453125 -3.21875 C 7.453125 -3.582031 7.332031 -3.863281 7.09375 -4.0625 C 6.863281 -4.257812 6.394531 -4.4375 5.6875 -4.59375 L 4.515625 -4.859375 C 3.347656 -5.109375 2.492188 -5.503906 1.953125 -6.046875 C 1.421875 -6.597656 1.15625 -7.335938 1.15625 -8.265625 C 1.15625 -9.421875 1.53125 -10.3125 2.28125 -10.9375 C 3.03125 -11.5625 4.109375 -11.875 5.515625 -11.875 C 6.148438 -11.875 6.804688 -11.828125 7.484375 -11.734375 C 8.160156 -11.640625 8.863281 -11.492188 9.59375 -11.296875 Z M 9.59375 -11.296875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-16">
|
||||
<path style="stroke:none;" d="M 8.421875 -8.484375 L 8.421875 -6.203125 C 8.035156 -6.460938 7.648438 -6.65625 7.265625 -6.78125 C 6.890625 -6.90625 6.492188 -6.96875 6.078125 -6.96875 C 5.296875 -6.96875 4.6875 -6.738281 4.25 -6.28125 C 3.820312 -5.820312 3.609375 -5.1875 3.609375 -4.375 C 3.609375 -3.550781 3.820312 -2.910156 4.25 -2.453125 C 4.6875 -2.003906 5.296875 -1.78125 6.078125 -1.78125 C 6.515625 -1.78125 6.929688 -1.84375 7.328125 -1.96875 C 7.722656 -2.101562 8.085938 -2.296875 8.421875 -2.546875 L 8.421875 -0.265625 C 7.984375 -0.0976562 7.535156 0.0234375 7.078125 0.109375 C 6.628906 0.191406 6.179688 0.234375 5.734375 0.234375 C 4.148438 0.234375 2.910156 -0.171875 2.015625 -0.984375 C 1.128906 -1.796875 0.6875 -2.925781 0.6875 -4.375 C 0.6875 -5.8125 1.128906 -6.9375 2.015625 -7.75 C 2.910156 -8.5625 4.148438 -8.96875 5.734375 -8.96875 C 6.191406 -8.96875 6.640625 -8.925781 7.078125 -8.84375 C 7.523438 -8.757812 7.972656 -8.640625 8.421875 -8.484375 Z M 8.421875 -8.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-17">
|
||||
<path style="stroke:none;" d="M 1.34375 -12.15625 L 4.140625 -12.15625 L 4.140625 -5.546875 L 7.359375 -8.75 L 10.609375 -8.75 L 6.34375 -4.734375 L 10.953125 0 L 7.5625 0 L 4.140625 -3.65625 L 4.140625 0 L 1.34375 0 Z M 1.34375 -12.15625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-18">
|
||||
<path style="stroke:none;" d="M 10.71875 -0.640625 C 10.164062 -0.359375 9.585938 -0.144531 8.984375 0 C 8.390625 0.15625 7.769531 0.234375 7.125 0.234375 C 5.175781 0.234375 3.632812 -0.304688 2.5 -1.390625 C 1.363281 -2.484375 0.796875 -3.960938 0.796875 -5.828125 C 0.796875 -7.691406 1.363281 -9.164062 2.5 -10.25 C 3.632812 -11.332031 5.175781 -11.875 7.125 -11.875 C 7.769531 -11.875 8.390625 -11.800781 8.984375 -11.65625 C 9.585938 -11.507812 10.164062 -11.296875 10.71875 -11.015625 L 10.71875 -8.59375 C 10.164062 -8.976562 9.617188 -9.257812 9.078125 -9.4375 C 8.535156 -9.613281 7.960938 -9.703125 7.359375 -9.703125 C 6.285156 -9.703125 5.441406 -9.359375 4.828125 -8.671875 C 4.210938 -7.984375 3.90625 -7.035156 3.90625 -5.828125 C 3.90625 -4.617188 4.210938 -3.671875 4.828125 -2.984375 C 5.441406 -2.296875 6.285156 -1.953125 7.359375 -1.953125 C 7.960938 -1.953125 8.535156 -2.039062 9.078125 -2.21875 C 9.617188 -2.394531 10.164062 -2.675781 10.71875 -3.0625 Z M 10.71875 -0.640625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-19">
|
||||
<path style="stroke:none;" d="M 8.1875 -8.484375 L 8.1875 -6.359375 C 7.582031 -6.609375 7 -6.796875 6.4375 -6.921875 C 5.882812 -7.046875 5.363281 -7.109375 4.875 -7.109375 C 4.34375 -7.109375 3.945312 -7.039062 3.6875 -6.90625 C 3.425781 -6.769531 3.296875 -6.566406 3.296875 -6.296875 C 3.296875 -6.066406 3.394531 -5.890625 3.59375 -5.765625 C 3.789062 -5.648438 4.140625 -5.566406 4.640625 -5.515625 L 5.140625 -5.4375 C 6.566406 -5.257812 7.523438 -4.960938 8.015625 -4.546875 C 8.515625 -4.128906 8.765625 -3.472656 8.765625 -2.578125 C 8.765625 -1.648438 8.421875 -0.945312 7.734375 -0.46875 C 7.046875 0 6.019531 0.234375 4.65625 0.234375 C 4.082031 0.234375 3.484375 0.1875 2.859375 0.09375 C 2.242188 0 1.613281 -0.140625 0.96875 -0.328125 L 0.96875 -2.453125 C 1.519531 -2.179688 2.085938 -1.976562 2.671875 -1.84375 C 3.265625 -1.707031 3.863281 -1.640625 4.46875 -1.640625 C 5.007812 -1.640625 5.414062 -1.710938 5.6875 -1.859375 C 5.96875 -2.015625 6.109375 -2.238281 6.109375 -2.53125 C 6.109375 -2.78125 6.015625 -2.96875 5.828125 -3.09375 C 5.640625 -3.21875 5.257812 -3.3125 4.6875 -3.375 L 4.203125 -3.4375 C 2.953125 -3.59375 2.078125 -3.878906 1.578125 -4.296875 C 1.078125 -4.722656 0.828125 -5.367188 0.828125 -6.234375 C 0.828125 -7.160156 1.144531 -7.847656 1.78125 -8.296875 C 2.414062 -8.742188 3.390625 -8.96875 4.703125 -8.96875 C 5.222656 -8.96875 5.765625 -8.925781 6.328125 -8.84375 C 6.898438 -8.769531 7.519531 -8.648438 8.1875 -8.484375 Z M 8.1875 -8.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-20">
|
||||
<path style="stroke:none;" d="M 7.84375 -6.375 C 7.601562 -6.488281 7.359375 -6.570312 7.109375 -6.625 C 6.867188 -6.675781 6.628906 -6.703125 6.390625 -6.703125 C 5.671875 -6.703125 5.113281 -6.472656 4.71875 -6.015625 C 4.332031 -5.554688 4.140625 -4.894531 4.140625 -4.03125 L 4.140625 0 L 1.34375 0 L 1.34375 -8.75 L 4.140625 -8.75 L 4.140625 -7.3125 C 4.503906 -7.882812 4.914062 -8.300781 5.375 -8.5625 C 5.84375 -8.832031 6.40625 -8.96875 7.0625 -8.96875 C 7.15625 -8.96875 7.253906 -8.960938 7.359375 -8.953125 C 7.472656 -8.941406 7.632812 -8.925781 7.84375 -8.90625 Z M 7.84375 -6.375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-21">
|
||||
<path style="stroke:none;" d="M 11.953125 -0.875 C 11.203125 -0.507812 10.421875 -0.234375 9.609375 -0.046875 C 8.804688 0.140625 7.976562 0.234375 7.125 0.234375 C 5.175781 0.234375 3.632812 -0.304688 2.5 -1.390625 C 1.363281 -2.484375 0.796875 -3.960938 0.796875 -5.828125 C 0.796875 -7.703125 1.375 -9.175781 2.53125 -10.25 C 3.6875 -11.332031 5.269531 -11.875 7.28125 -11.875 C 8.0625 -11.875 8.804688 -11.800781 9.515625 -11.65625 C 10.222656 -11.507812 10.894531 -11.296875 11.53125 -11.015625 L 11.53125 -8.59375 C 10.875 -8.96875 10.222656 -9.242188 9.578125 -9.421875 C 8.941406 -9.609375 8.300781 -9.703125 7.65625 -9.703125 C 6.457031 -9.703125 5.53125 -9.363281 4.875 -8.6875 C 4.226562 -8.019531 3.90625 -7.066406 3.90625 -5.828125 C 3.90625 -4.585938 4.21875 -3.628906 4.84375 -2.953125 C 5.46875 -2.285156 6.359375 -1.953125 7.515625 -1.953125 C 7.828125 -1.953125 8.113281 -1.972656 8.375 -2.015625 C 8.644531 -2.054688 8.890625 -2.117188 9.109375 -2.203125 L 9.109375 -4.46875 L 7.265625 -4.46875 L 7.265625 -6.484375 L 11.953125 -6.484375 Z M 11.953125 -0.875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-22">
|
||||
<path style="stroke:none;" d="M 5.515625 -6.96875 C 4.890625 -6.96875 4.414062 -6.742188 4.09375 -6.296875 C 3.769531 -5.847656 3.609375 -5.207031 3.609375 -4.375 C 3.609375 -3.53125 3.769531 -2.882812 4.09375 -2.4375 C 4.414062 -2 4.890625 -1.78125 5.515625 -1.78125 C 6.117188 -1.78125 6.582031 -2 6.90625 -2.4375 C 7.226562 -2.882812 7.390625 -3.53125 7.390625 -4.375 C 7.390625 -5.207031 7.226562 -5.847656 6.90625 -6.296875 C 6.582031 -6.742188 6.117188 -6.96875 5.515625 -6.96875 Z M 5.515625 -8.96875 C 7.015625 -8.96875 8.1875 -8.5625 9.03125 -7.75 C 9.882812 -6.9375 10.3125 -5.8125 10.3125 -4.375 C 10.3125 -2.9375 9.882812 -1.804688 9.03125 -0.984375 C 8.1875 -0.171875 7.015625 0.234375 5.515625 0.234375 C 4.003906 0.234375 2.820312 -0.171875 1.96875 -0.984375 C 1.113281 -1.804688 0.6875 -2.9375 0.6875 -4.375 C 0.6875 -5.8125 1.113281 -6.9375 1.96875 -7.75 C 2.820312 -8.5625 4.003906 -8.96875 5.515625 -8.96875 Z M 5.515625 -8.96875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-23">
|
||||
<path style="stroke:none;" d="M 1.46875 -11.671875 L 6.46875 -11.671875 C 7.945312 -11.671875 9.082031 -11.335938 9.875 -10.671875 C 10.675781 -10.015625 11.078125 -9.078125 11.078125 -7.859375 C 11.078125 -6.640625 10.675781 -5.695312 9.875 -5.03125 C 9.082031 -4.375 7.945312 -4.046875 6.46875 -4.046875 L 4.484375 -4.046875 L 4.484375 0 L 1.46875 0 Z M 4.484375 -9.484375 L 4.484375 -6.234375 L 6.140625 -6.234375 C 6.722656 -6.234375 7.171875 -6.375 7.484375 -6.65625 C 7.804688 -6.9375 7.96875 -7.335938 7.96875 -7.859375 C 7.96875 -8.378906 7.804688 -8.78125 7.484375 -9.0625 C 7.171875 -9.34375 6.722656 -9.484375 6.140625 -9.484375 Z M 4.484375 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-24">
|
||||
<path style="stroke:none;" d="M 5.75 -6.5 C 6.375 -6.5 6.820312 -6.613281 7.09375 -6.84375 C 7.375 -7.082031 7.515625 -7.46875 7.515625 -8 C 7.515625 -8.53125 7.375 -8.910156 7.09375 -9.140625 C 6.820312 -9.367188 6.375 -9.484375 5.75 -9.484375 L 4.484375 -9.484375 L 4.484375 -6.5 Z M 4.484375 -4.421875 L 4.484375 0 L 1.46875 0 L 1.46875 -11.671875 L 6.0625 -11.671875 C 7.601562 -11.671875 8.726562 -11.410156 9.4375 -10.890625 C 10.15625 -10.378906 10.515625 -9.566406 10.515625 -8.453125 C 10.515625 -7.679688 10.328125 -7.046875 9.953125 -6.546875 C 9.585938 -6.054688 9.03125 -5.691406 8.28125 -5.453125 C 8.6875 -5.359375 9.050781 -5.144531 9.375 -4.8125 C 9.707031 -4.488281 10.039062 -3.988281 10.375 -3.3125 L 12 0 L 8.796875 0 L 7.375 -2.90625 C 7.09375 -3.488281 6.800781 -3.882812 6.5 -4.09375 C 6.207031 -4.3125 5.816406 -4.421875 5.328125 -4.421875 Z M 4.484375 -4.421875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-25">
|
||||
<path style="stroke:none;" d="M 7.296875 -1.484375 C 6.910156 -0.972656 6.484375 -0.597656 6.015625 -0.359375 C 5.554688 -0.117188 5.023438 0 4.421875 0 C 3.347656 0 2.460938 -0.421875 1.765625 -1.265625 C 1.066406 -2.109375 0.71875 -3.179688 0.71875 -4.484375 C 0.71875 -5.785156 1.066406 -6.851562 1.765625 -7.6875 C 2.460938 -8.53125 3.347656 -8.953125 4.421875 -8.953125 C 5.023438 -8.953125 5.554688 -8.832031 6.015625 -8.59375 C 6.484375 -8.351562 6.910156 -7.972656 7.296875 -7.453125 L 7.296875 -8.75 L 10.109375 -8.75 L 10.109375 -0.890625 C 10.109375 0.523438 9.664062 1.601562 8.78125 2.34375 C 7.894531 3.082031 6.609375 3.453125 4.921875 3.453125 C 4.367188 3.453125 3.835938 3.410156 3.328125 3.328125 C 2.816406 3.242188 2.304688 3.117188 1.796875 2.953125 L 1.796875 0.765625 C 2.285156 1.046875 2.765625 1.253906 3.234375 1.390625 C 3.703125 1.535156 4.171875 1.609375 4.640625 1.609375 C 5.554688 1.609375 6.226562 1.40625 6.65625 1 C 7.082031 0.601562 7.296875 -0.0234375 7.296875 -0.890625 Z M 5.453125 -6.9375 C 4.878906 -6.9375 4.429688 -6.722656 4.109375 -6.296875 C 3.785156 -5.867188 3.625 -5.265625 3.625 -4.484375 C 3.625 -3.679688 3.78125 -3.070312 4.09375 -2.65625 C 4.40625 -2.238281 4.859375 -2.03125 5.453125 -2.03125 C 6.035156 -2.03125 6.488281 -2.242188 6.8125 -2.671875 C 7.132812 -3.097656 7.296875 -3.703125 7.296875 -4.484375 C 7.296875 -5.265625 7.132812 -5.867188 6.8125 -6.296875 C 6.488281 -6.722656 6.035156 -6.9375 5.453125 -6.9375 Z M 5.453125 -6.9375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-26">
|
||||
<path style="stroke:none;" d="M 0.203125 -8.75 L 3 -8.75 L 5.34375 -2.8125 L 7.34375 -8.75 L 10.140625 -8.75 L 6.46875 0.828125 C 6.09375 1.804688 5.660156 2.488281 5.171875 2.875 C 4.679688 3.257812 4.03125 3.453125 3.21875 3.453125 L 1.609375 3.453125 L 1.609375 1.625 L 2.484375 1.625 C 2.953125 1.625 3.296875 1.546875 3.515625 1.390625 C 3.734375 1.242188 3.898438 0.972656 4.015625 0.578125 L 4.09375 0.34375 Z M 0.203125 -8.75 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-27">
|
||||
<path style="stroke:none;" d="M 1.46875 -11.671875 L 9.59375 -11.671875 L 9.59375 -9.390625 L 4.484375 -9.390625 L 4.484375 -7.21875 L 9.28125 -7.21875 L 9.28125 -4.953125 L 4.484375 -4.953125 L 4.484375 -2.28125 L 9.765625 -2.28125 L 9.765625 0 L 1.46875 0 Z M 1.46875 -11.671875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-28">
|
||||
<path style="stroke:none;" d="M 7.109375 -12.15625 L 7.109375 -10.328125 L 5.5625 -10.328125 C 5.164062 -10.328125 4.890625 -10.253906 4.734375 -10.109375 C 4.578125 -9.960938 4.5 -9.710938 4.5 -9.359375 L 4.5 -8.75 L 7.703125 -8.75 L 7.703125 -9.359375 C 7.703125 -10.316406 7.96875 -11.019531 8.5 -11.46875 C 9.03125 -11.925781 9.851562 -12.15625 10.96875 -12.15625 L 13.109375 -12.15625 L 13.109375 -10.328125 L 11.5625 -10.328125 C 11.164062 -10.328125 10.894531 -10.253906 10.75 -10.109375 C 10.59375 -9.960938 10.515625 -9.710938 10.515625 -9.359375 L 10.515625 -8.75 L 16.5 -8.75 L 16.5 0 L 13.6875 0 L 13.6875 -6.75 L 10.515625 -6.75 L 10.515625 0 L 7.703125 0 L 7.703125 -6.75 L 4.5 -6.75 L 4.5 0 L 1.703125 0 L 1.703125 -6.75 L 0.3125 -6.75 L 0.3125 -8.75 L 1.703125 -8.75 L 1.703125 -9.359375 C 1.703125 -10.316406 1.96875 -11.019531 2.5 -11.46875 C 3.03125 -11.925781 3.851562 -12.15625 4.96875 -12.15625 Z M 13.6875 -12.15625 L 16.5 -12.15625 L 16.5 -9.875 L 13.6875 -9.875 Z M 13.6875 -12.15625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph0-29">
|
||||
<path style="stroke:none;" d="M 1.46875 -11.671875 L 4.484375 -11.671875 L 4.484375 0 L 1.46875 0 Z M 1.46875 -11.671875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-0">
|
||||
<path style="stroke:none;" d="M 0.65625 2.296875 L 0.65625 -9.171875 L 7.15625 -9.171875 L 7.15625 2.296875 Z M 1.390625 1.578125 L 6.4375 1.578125 L 6.4375 -8.4375 L 1.390625 -8.4375 Z M 1.390625 1.578125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-1">
|
||||
<path style="stroke:none;" d="M 3.90625 -8.34375 L 2.5625 -3.5 L 5.265625 -3.5 Z M 3.140625 -9.484375 L 4.6875 -9.484375 L 7.59375 0 L 6.265625 0 L 5.5625 -2.46875 L 2.25 -2.46875 L 1.5625 0 L 0.234375 0 Z M 3.140625 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-2">
|
||||
<path style="stroke:none;" d="M 2.375 -0.890625 L 2.375 2.703125 L 1.203125 2.703125 L 1.203125 -7.109375 L 2.375 -7.109375 L 2.375 -6.203125 C 2.570312 -6.554688 2.832031 -6.820312 3.15625 -7 C 3.476562 -7.1875 3.851562 -7.28125 4.28125 -7.28125 C 5.132812 -7.28125 5.804688 -6.945312 6.296875 -6.28125 C 6.785156 -5.613281 7.03125 -4.691406 7.03125 -3.515625 C 7.03125 -2.367188 6.785156 -1.460938 6.296875 -0.796875 C 5.804688 -0.140625 5.132812 0.1875 4.28125 0.1875 C 3.84375 0.1875 3.460938 0.09375 3.140625 -0.09375 C 2.816406 -0.28125 2.5625 -0.546875 2.375 -0.890625 Z M 5.8125 -3.546875 C 5.8125 -4.453125 5.664062 -5.132812 5.375 -5.59375 C 5.09375 -6.0625 4.671875 -6.296875 4.109375 -6.296875 C 3.535156 -6.296875 3.101562 -6.0625 2.8125 -5.59375 C 2.519531 -5.132812 2.375 -4.453125 2.375 -3.546875 C 2.375 -2.648438 2.519531 -1.96875 2.8125 -1.5 C 3.101562 -1.039062 3.535156 -0.8125 4.109375 -0.8125 C 4.671875 -0.8125 5.09375 -1.039062 5.375 -1.5 C 5.664062 -1.957031 5.8125 -2.640625 5.8125 -3.546875 Z M 5.8125 -3.546875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-3">
|
||||
<path style="stroke:none;" d="M 4.0625 -2.578125 C 4.0625 -2.054688 4.15625 -1.660156 4.34375 -1.390625 C 4.539062 -1.117188 4.828125 -0.984375 5.203125 -0.984375 L 6.5625 -0.984375 L 6.5625 0 L 5.078125 0 C 4.378906 0 3.835938 -0.222656 3.453125 -0.671875 C 3.078125 -1.117188 2.890625 -1.753906 2.890625 -2.578125 L 2.890625 -9.03125 L 1.015625 -9.03125 L 1.015625 -9.953125 L 4.0625 -9.953125 Z M 4.0625 -2.578125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-4">
|
||||
<path style="stroke:none;" d="M 1.625 -7.109375 L 4.609375 -7.109375 L 4.609375 -0.90625 L 6.9375 -0.90625 L 6.9375 0 L 1.125 0 L 1.125 -0.90625 L 3.453125 -0.90625 L 3.453125 -6.203125 L 1.625 -6.203125 Z M 3.453125 -9.875 L 4.609375 -9.875 L 4.609375 -8.390625 L 3.453125 -8.390625 Z M 3.453125 -9.875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-5">
|
||||
<path style="stroke:none;" d="M 7.0625 -3.84375 L 7.0625 -3.28125 L 2 -3.28125 L 2 -3.234375 C 2 -2.460938 2.203125 -1.863281 2.609375 -1.4375 C 3.015625 -1.019531 3.582031 -0.8125 4.3125 -0.8125 C 4.6875 -0.8125 5.078125 -0.867188 5.484375 -0.984375 C 5.890625 -1.097656 6.320312 -1.28125 6.78125 -1.53125 L 6.78125 -0.359375 C 6.34375 -0.179688 5.914062 -0.046875 5.5 0.046875 C 5.082031 0.140625 4.679688 0.1875 4.296875 0.1875 C 3.191406 0.1875 2.328125 -0.140625 1.703125 -0.796875 C 1.085938 -1.460938 0.78125 -2.378906 0.78125 -3.546875 C 0.78125 -4.679688 1.082031 -5.585938 1.6875 -6.265625 C 2.300781 -6.941406 3.113281 -7.28125 4.125 -7.28125 C 5.03125 -7.28125 5.742188 -6.972656 6.265625 -6.359375 C 6.796875 -5.742188 7.0625 -4.90625 7.0625 -3.84375 Z M 5.890625 -4.1875 C 5.867188 -4.875 5.703125 -5.394531 5.390625 -5.75 C 5.085938 -6.113281 4.648438 -6.296875 4.078125 -6.296875 C 3.515625 -6.296875 3.050781 -6.109375 2.6875 -5.734375 C 2.320312 -5.359375 2.109375 -4.84375 2.046875 -4.1875 Z M 5.890625 -4.1875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-6">
|
||||
<path style="stroke:none;" d="M 6.171875 -6.859375 L 6.171875 -5.71875 C 5.835938 -5.914062 5.5 -6.0625 5.15625 -6.15625 C 4.820312 -6.25 4.476562 -6.296875 4.125 -6.296875 C 3.601562 -6.296875 3.210938 -6.210938 2.953125 -6.046875 C 2.691406 -5.878906 2.5625 -5.617188 2.5625 -5.265625 C 2.5625 -4.941406 2.65625 -4.703125 2.84375 -4.546875 C 3.039062 -4.390625 3.523438 -4.238281 4.296875 -4.09375 L 4.78125 -4 C 5.351562 -3.894531 5.785156 -3.675781 6.078125 -3.34375 C 6.378906 -3.007812 6.53125 -2.582031 6.53125 -2.0625 C 6.53125 -1.351562 6.28125 -0.800781 5.78125 -0.40625 C 5.289062 -0.0078125 4.597656 0.1875 3.703125 0.1875 C 3.359375 0.1875 2.992188 0.148438 2.609375 0.078125 C 2.222656 0.00390625 1.804688 -0.109375 1.359375 -0.265625 L 1.359375 -1.46875 C 1.785156 -1.238281 2.195312 -1.066406 2.59375 -0.953125 C 3 -0.847656 3.378906 -0.796875 3.734375 -0.796875 C 4.242188 -0.796875 4.640625 -0.898438 4.921875 -1.109375 C 5.210938 -1.316406 5.359375 -1.609375 5.359375 -1.984375 C 5.359375 -2.523438 4.835938 -2.898438 3.796875 -3.109375 L 3.75 -3.125 L 3.3125 -3.21875 C 2.632812 -3.34375 2.140625 -3.5625 1.828125 -3.875 C 1.523438 -4.1875 1.375 -4.609375 1.375 -5.140625 C 1.375 -5.828125 1.601562 -6.351562 2.0625 -6.71875 C 2.53125 -7.09375 3.191406 -7.28125 4.046875 -7.28125 C 4.421875 -7.28125 4.785156 -7.242188 5.140625 -7.171875 C 5.492188 -7.109375 5.835938 -7.003906 6.171875 -6.859375 Z M 6.171875 -6.859375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-7">
|
||||
<path style="stroke:none;" d=""/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-8">
|
||||
<path style="stroke:none;" d="M 3.890625 -9.125 L 3.890625 -7.109375 L 6.546875 -7.109375 L 6.546875 -6.203125 L 3.890625 -6.203125 L 3.890625 -2.34375 C 3.890625 -1.820312 3.988281 -1.457031 4.1875 -1.25 C 4.394531 -1.039062 4.742188 -0.9375 5.234375 -0.9375 L 6.546875 -0.9375 L 6.546875 0 L 5.125 0 C 4.25 0 3.628906 -0.171875 3.265625 -0.515625 C 2.910156 -0.867188 2.734375 -1.476562 2.734375 -2.34375 L 2.734375 -6.203125 L 0.828125 -6.203125 L 0.828125 -7.109375 L 2.734375 -7.109375 L 2.734375 -9.125 Z M 3.890625 -9.125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-9">
|
||||
<path style="stroke:none;" d="M 3.90625 -6.296875 C 3.3125 -6.296875 2.863281 -6.0625 2.5625 -5.59375 C 2.257812 -5.132812 2.109375 -4.453125 2.109375 -3.546875 C 2.109375 -2.648438 2.257812 -1.96875 2.5625 -1.5 C 2.863281 -1.039062 3.3125 -0.8125 3.90625 -0.8125 C 4.507812 -0.8125 4.960938 -1.039062 5.265625 -1.5 C 5.566406 -1.96875 5.71875 -2.648438 5.71875 -3.546875 C 5.71875 -4.453125 5.566406 -5.132812 5.265625 -5.59375 C 4.960938 -6.0625 4.507812 -6.296875 3.90625 -6.296875 Z M 3.90625 -7.28125 C 4.894531 -7.28125 5.648438 -6.957031 6.171875 -6.3125 C 6.691406 -5.675781 6.953125 -4.753906 6.953125 -3.546875 C 6.953125 -2.335938 6.691406 -1.410156 6.171875 -0.765625 C 5.648438 -0.128906 4.894531 0.1875 3.90625 0.1875 C 2.925781 0.1875 2.175781 -0.128906 1.65625 -0.765625 C 1.132812 -1.410156 0.875 -2.335938 0.875 -3.546875 C 0.875 -4.753906 1.132812 -5.675781 1.65625 -6.3125 C 2.175781 -6.957031 2.925781 -7.28125 3.90625 -7.28125 Z M 3.90625 -7.28125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-10">
|
||||
<path style="stroke:none;" d="M 5.453125 -3.609375 C 5.453125 -4.484375 5.304688 -5.148438 5.015625 -5.609375 C 4.734375 -6.066406 4.316406 -6.296875 3.765625 -6.296875 C 3.191406 -6.296875 2.753906 -6.066406 2.453125 -5.609375 C 2.160156 -5.148438 2.015625 -4.484375 2.015625 -3.609375 C 2.015625 -2.734375 2.164062 -2.066406 2.46875 -1.609375 C 2.769531 -1.148438 3.207031 -0.921875 3.78125 -0.921875 C 4.320312 -0.921875 4.734375 -1.148438 5.015625 -1.609375 C 5.304688 -2.066406 5.453125 -2.734375 5.453125 -3.609375 Z M 6.609375 -0.453125 C 6.609375 0.609375 6.359375 1.414062 5.859375 1.96875 C 5.359375 2.519531 4.617188 2.796875 3.640625 2.796875 C 3.316406 2.796875 2.976562 2.765625 2.625 2.703125 C 2.269531 2.640625 1.921875 2.550781 1.578125 2.4375 L 1.578125 1.28125 C 1.992188 1.476562 2.367188 1.625 2.703125 1.71875 C 3.046875 1.8125 3.359375 1.859375 3.640625 1.859375 C 4.265625 1.859375 4.722656 1.6875 5.015625 1.34375 C 5.304688 1 5.453125 0.457031 5.453125 -0.28125 L 5.453125 -1.125 C 5.265625 -0.726562 5.007812 -0.429688 4.6875 -0.234375 C 4.363281 -0.046875 3.972656 0.046875 3.515625 0.046875 C 2.679688 0.046875 2.015625 -0.28125 1.515625 -0.9375 C 1.023438 -1.601562 0.78125 -2.492188 0.78125 -3.609375 C 0.78125 -4.722656 1.023438 -5.613281 1.515625 -6.28125 C 2.015625 -6.945312 2.679688 -7.28125 3.515625 -7.28125 C 3.972656 -7.28125 4.359375 -7.1875 4.671875 -7 C 4.984375 -6.820312 5.242188 -6.539062 5.453125 -6.15625 L 5.453125 -7.078125 L 6.609375 -7.078125 Z M 6.609375 -0.453125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-11">
|
||||
<path style="stroke:none;" d="M 6.734375 -0.359375 C 6.421875 -0.179688 6.097656 -0.046875 5.765625 0.046875 C 5.429688 0.140625 5.09375 0.1875 4.75 0.1875 C 3.644531 0.1875 2.78125 -0.140625 2.15625 -0.796875 C 1.539062 -1.460938 1.234375 -2.378906 1.234375 -3.546875 C 1.234375 -4.710938 1.539062 -5.625 2.15625 -6.28125 C 2.78125 -6.945312 3.644531 -7.28125 4.75 -7.28125 C 5.09375 -7.28125 5.425781 -7.234375 5.75 -7.140625 C 6.070312 -7.054688 6.398438 -6.921875 6.734375 -6.734375 L 6.734375 -5.515625 C 6.421875 -5.785156 6.109375 -5.984375 5.796875 -6.109375 C 5.492188 -6.234375 5.144531 -6.296875 4.75 -6.296875 C 4.019531 -6.296875 3.457031 -6.054688 3.0625 -5.578125 C 2.664062 -5.109375 2.46875 -4.429688 2.46875 -3.546875 C 2.46875 -2.671875 2.664062 -1.992188 3.0625 -1.515625 C 3.457031 -1.046875 4.019531 -0.8125 4.75 -0.8125 C 5.15625 -0.8125 5.519531 -0.875 5.84375 -1 C 6.164062 -1.125 6.460938 -1.316406 6.734375 -1.578125 Z M 6.734375 -0.359375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-12">
|
||||
<path style="stroke:none;" d="M 6.671875 -4.40625 L 6.671875 0 L 5.5 0 L 5.5 -4.40625 C 5.5 -5.039062 5.382812 -5.507812 5.15625 -5.8125 C 4.9375 -6.113281 4.585938 -6.265625 4.109375 -6.265625 C 3.554688 -6.265625 3.132812 -6.070312 2.84375 -5.6875 C 2.550781 -5.300781 2.40625 -4.742188 2.40625 -4.015625 L 2.40625 0 L 1.234375 0 L 1.234375 -7.109375 L 2.40625 -7.109375 L 2.40625 -6.046875 C 2.613281 -6.453125 2.894531 -6.757812 3.25 -6.96875 C 3.601562 -7.175781 4.023438 -7.28125 4.515625 -7.28125 C 5.234375 -7.28125 5.769531 -7.039062 6.125 -6.5625 C 6.488281 -6.09375 6.671875 -5.375 6.671875 -4.40625 Z M 6.671875 -4.40625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-13">
|
||||
<path style="stroke:none;" d="M 6.75 -9.875 L 6.75 -8.90625 L 5.421875 -8.90625 C 5.003906 -8.90625 4.710938 -8.816406 4.546875 -8.640625 C 4.378906 -8.472656 4.296875 -8.171875 4.296875 -7.734375 L 4.296875 -7.109375 L 6.75 -7.109375 L 6.75 -6.203125 L 4.296875 -6.203125 L 4.296875 0 L 3.140625 0 L 3.140625 -6.203125 L 1.234375 -6.203125 L 1.234375 -7.109375 L 3.140625 -7.109375 L 3.140625 -7.609375 C 3.140625 -8.378906 3.316406 -8.945312 3.671875 -9.3125 C 4.023438 -9.6875 4.582031 -9.875 5.34375 -9.875 Z M 6.75 -9.875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-14">
|
||||
<path style="stroke:none;" d="M 1.234375 -2.6875 L 1.234375 -7.09375 L 2.40625 -7.09375 L 2.40625 -2.6875 C 2.40625 -2.050781 2.515625 -1.582031 2.734375 -1.28125 C 2.960938 -0.976562 3.316406 -0.828125 3.796875 -0.828125 C 4.347656 -0.828125 4.769531 -1.019531 5.0625 -1.40625 C 5.351562 -1.800781 5.5 -2.359375 5.5 -3.078125 L 5.5 -7.09375 L 6.671875 -7.09375 L 6.671875 0 L 5.5 0 L 5.5 -1.0625 C 5.289062 -0.65625 5.003906 -0.34375 4.640625 -0.125 C 4.285156 0.0820312 3.867188 0.1875 3.390625 0.1875 C 2.660156 0.1875 2.117188 -0.0507812 1.765625 -0.53125 C 1.410156 -1.007812 1.234375 -1.726562 1.234375 -2.6875 Z M 1.234375 -2.6875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-15">
|
||||
<path style="stroke:none;" d="M 7.328125 -5.640625 C 7.078125 -5.835938 6.820312 -5.976562 6.5625 -6.0625 C 6.3125 -6.15625 6.03125 -6.203125 5.71875 -6.203125 C 4.988281 -6.203125 4.429688 -5.972656 4.046875 -5.515625 C 3.660156 -5.054688 3.46875 -4.394531 3.46875 -3.53125 L 3.46875 0 L 2.296875 0 L 2.296875 -7.109375 L 3.46875 -7.109375 L 3.46875 -5.71875 C 3.664062 -6.21875 3.96875 -6.601562 4.375 -6.875 C 4.78125 -7.144531 5.257812 -7.28125 5.8125 -7.28125 C 6.09375 -7.28125 6.359375 -7.242188 6.609375 -7.171875 C 6.859375 -7.097656 7.097656 -6.988281 7.328125 -6.84375 Z M 7.328125 -5.640625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-16">
|
||||
<path style="stroke:none;" d="M 4.453125 -3.578125 L 4.0625 -3.578125 C 3.382812 -3.578125 2.875 -3.457031 2.53125 -3.21875 C 2.1875 -2.976562 2.015625 -2.617188 2.015625 -2.140625 C 2.015625 -1.710938 2.140625 -1.378906 2.390625 -1.140625 C 2.648438 -0.910156 3.007812 -0.796875 3.46875 -0.796875 C 4.113281 -0.796875 4.617188 -1.019531 4.984375 -1.46875 C 5.359375 -1.914062 5.546875 -2.53125 5.546875 -3.3125 L 5.546875 -3.578125 Z M 6.71875 -4.0625 L 6.71875 0 L 5.546875 0 L 5.546875 -1.046875 C 5.296875 -0.628906 4.976562 -0.316406 4.59375 -0.109375 C 4.21875 0.0859375 3.757812 0.1875 3.21875 0.1875 C 2.5 0.1875 1.921875 -0.015625 1.484375 -0.421875 C 1.054688 -0.835938 0.84375 -1.382812 0.84375 -2.0625 C 0.84375 -2.851562 1.109375 -3.453125 1.640625 -3.859375 C 2.171875 -4.273438 2.953125 -4.484375 3.984375 -4.484375 L 5.546875 -4.484375 L 5.546875 -4.671875 C 5.546875 -5.234375 5.398438 -5.644531 5.109375 -5.90625 C 4.828125 -6.164062 4.378906 -6.296875 3.765625 -6.296875 C 3.359375 -6.296875 2.953125 -6.238281 2.546875 -6.125 C 2.140625 -6.007812 1.742188 -5.84375 1.359375 -5.625 L 1.359375 -6.78125 C 1.796875 -6.945312 2.210938 -7.070312 2.609375 -7.15625 C 3.003906 -7.238281 3.390625 -7.28125 3.765625 -7.28125 C 4.347656 -7.28125 4.847656 -7.191406 5.265625 -7.015625 C 5.679688 -6.847656 6.019531 -6.585938 6.28125 -6.234375 C 6.4375 -6.023438 6.546875 -5.765625 6.609375 -5.453125 C 6.679688 -5.140625 6.71875 -4.675781 6.71875 -4.0625 Z M 6.71875 -4.0625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-17">
|
||||
<path style="stroke:none;" d="M 4.296875 -6.390625 C 4.429688 -6.691406 4.609375 -6.914062 4.828125 -7.0625 C 5.054688 -7.207031 5.328125 -7.28125 5.640625 -7.28125 C 6.210938 -7.28125 6.613281 -7.054688 6.84375 -6.609375 C 7.082031 -6.171875 7.203125 -5.34375 7.203125 -4.125 L 7.203125 0 L 6.140625 0 L 6.140625 -4.0625 C 6.140625 -5.070312 6.082031 -5.695312 5.96875 -5.9375 C 5.851562 -6.175781 5.648438 -6.296875 5.359375 -6.296875 C 5.015625 -6.296875 4.78125 -6.164062 4.65625 -5.90625 C 4.53125 -5.644531 4.46875 -5.03125 4.46875 -4.0625 L 4.46875 0 L 3.40625 0 L 3.40625 -4.0625 C 3.40625 -5.082031 3.34375 -5.707031 3.21875 -5.9375 C 3.101562 -6.175781 2.890625 -6.296875 2.578125 -6.296875 C 2.265625 -6.296875 2.046875 -6.164062 1.921875 -5.90625 C 1.804688 -5.644531 1.75 -5.03125 1.75 -4.0625 L 1.75 0 L 0.6875 0 L 0.6875 -7.109375 L 1.75 -7.109375 L 1.75 -6.5 C 1.894531 -6.75 2.070312 -6.941406 2.28125 -7.078125 C 2.488281 -7.210938 2.722656 -7.28125 2.984375 -7.28125 C 3.304688 -7.28125 3.570312 -7.207031 3.78125 -7.0625 C 4 -6.914062 4.171875 -6.691406 4.296875 -6.390625 Z M 4.296875 -6.390625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-18">
|
||||
<path style="stroke:none;" d="M 6.671875 -4.40625 L 6.671875 0 L 5.5 0 L 5.5 -4.40625 C 5.5 -5.039062 5.382812 -5.507812 5.15625 -5.8125 C 4.9375 -6.113281 4.585938 -6.265625 4.109375 -6.265625 C 3.554688 -6.265625 3.132812 -6.070312 2.84375 -5.6875 C 2.550781 -5.300781 2.40625 -4.742188 2.40625 -4.015625 L 2.40625 0 L 1.234375 0 L 1.234375 -9.875 L 2.40625 -9.875 L 2.40625 -6.046875 C 2.613281 -6.453125 2.894531 -6.757812 3.25 -6.96875 C 3.601562 -7.175781 4.023438 -7.28125 4.515625 -7.28125 C 5.234375 -7.28125 5.769531 -7.039062 6.125 -6.5625 C 6.488281 -6.09375 6.671875 -5.375 6.671875 -4.40625 Z M 6.671875 -4.40625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-19">
|
||||
<path style="stroke:none;" d="M 0.640625 -7.109375 L 1.84375 -7.109375 L 3.90625 -1.140625 L 5.984375 -7.109375 L 7.1875 -7.109375 L 4.671875 0 L 3.15625 0 Z M 0.640625 -7.109375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-20">
|
||||
<path style="stroke:none;" d="M 7.015625 -0.78125 C 6.671875 -0.46875 6.28125 -0.226562 5.84375 -0.0625 C 5.414062 0.101562 4.953125 0.1875 4.453125 0.1875 C 3.253906 0.1875 2.316406 -0.242188 1.640625 -1.109375 C 0.972656 -1.972656 0.640625 -3.179688 0.640625 -4.734375 C 0.640625 -6.273438 0.976562 -7.476562 1.65625 -8.34375 C 2.332031 -9.21875 3.273438 -9.65625 4.484375 -9.65625 C 4.878906 -9.65625 5.257812 -9.597656 5.625 -9.484375 C 5.988281 -9.367188 6.34375 -9.195312 6.6875 -8.96875 L 6.6875 -7.65625 C 6.34375 -7.976562 5.988281 -8.21875 5.625 -8.375 C 5.269531 -8.53125 4.890625 -8.609375 4.484375 -8.609375 C 3.648438 -8.609375 3.023438 -8.285156 2.609375 -7.640625 C 2.191406 -6.992188 1.984375 -6.023438 1.984375 -4.734375 C 1.984375 -3.410156 2.1875 -2.429688 2.59375 -1.796875 C 3 -1.171875 3.617188 -0.859375 4.453125 -0.859375 C 4.734375 -0.859375 4.976562 -0.890625 5.1875 -0.953125 C 5.40625 -1.015625 5.601562 -1.117188 5.78125 -1.265625 L 5.78125 -3.8125 L 4.40625 -3.8125 L 4.40625 -4.859375 L 7.015625 -4.859375 Z M 7.015625 -0.78125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-21">
|
||||
<path style="stroke:none;" d="M 5.453125 -6.203125 L 5.453125 -9.875 L 6.609375 -9.875 L 6.609375 0 L 5.453125 0 L 5.453125 -0.890625 C 5.253906 -0.546875 4.992188 -0.28125 4.671875 -0.09375 C 4.347656 0.09375 3.972656 0.1875 3.546875 0.1875 C 2.691406 0.1875 2.015625 -0.144531 1.515625 -0.8125 C 1.023438 -1.476562 0.78125 -2.398438 0.78125 -3.578125 C 0.78125 -4.734375 1.023438 -5.640625 1.515625 -6.296875 C 2.015625 -6.953125 2.691406 -7.28125 3.546875 -7.28125 C 3.972656 -7.28125 4.347656 -7.1875 4.671875 -7 C 5.003906 -6.820312 5.265625 -6.554688 5.453125 -6.203125 Z M 2.015625 -3.546875 C 2.015625 -2.640625 2.15625 -1.957031 2.4375 -1.5 C 2.726562 -1.039062 3.15625 -0.8125 3.71875 -0.8125 C 4.28125 -0.8125 4.707031 -1.039062 5 -1.5 C 5.300781 -1.96875 5.453125 -2.648438 5.453125 -3.546875 C 5.453125 -4.453125 5.300781 -5.132812 5 -5.59375 C 4.707031 -6.0625 4.28125 -6.296875 3.71875 -6.296875 C 3.15625 -6.296875 2.726562 -6.0625 2.4375 -5.59375 C 2.15625 -5.132812 2.015625 -4.453125 2.015625 -3.546875 Z M 2.015625 -3.546875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-22">
|
||||
<path style="stroke:none;" d="M 0.875 -9.484375 L 2.5 -9.484375 L 5.703125 -1.671875 L 5.703125 -9.484375 L 6.9375 -9.484375 L 6.9375 0 L 5.3125 0 L 2.125 -7.796875 L 2.125 0 L 0.875 0 Z M 0.875 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-23">
|
||||
<path style="stroke:none;" d="M 7.09375 -7.109375 L 4.546875 -3.703125 L 7.34375 0 L 6 0 L 3.90625 -2.84375 L 1.828125 0 L 0.484375 0 L 3.28125 -3.703125 L 0.734375 -7.109375 L 2.03125 -7.109375 L 3.90625 -4.53125 L 5.78125 -7.109375 Z M 7.09375 -7.109375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-24">
|
||||
<path style="stroke:none;" d="M 0.296875 -9.484375 L 7.53125 -9.484375 L 7.53125 -8.390625 L 4.5625 -8.390625 L 4.5625 0 L 3.28125 0 L 3.28125 -8.390625 L 0.296875 -8.390625 Z M 0.296875 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-25">
|
||||
<path style="stroke:none;" d="M 2.765625 -1.046875 C 3.847656 -1.046875 4.601562 -1.3125 5.03125 -1.84375 C 5.457031 -2.375 5.671875 -3.335938 5.671875 -4.734375 C 5.671875 -6.128906 5.457031 -7.09375 5.03125 -7.625 C 4.601562 -8.15625 3.847656 -8.421875 2.765625 -8.421875 L 2.15625 -8.421875 L 2.15625 -1.046875 Z M 2.796875 -9.484375 C 4.242188 -9.484375 5.304688 -9.097656 5.984375 -8.328125 C 6.671875 -7.554688 7.015625 -6.359375 7.015625 -4.734375 C 7.015625 -3.109375 6.671875 -1.910156 5.984375 -1.140625 C 5.304688 -0.378906 4.242188 0 2.796875 0 L 0.875 0 L 0.875 -9.484375 Z M 2.796875 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-26">
|
||||
<path style="stroke:none;" d="M 6.8125 -0.34375 C 6.488281 -0.164062 6.15625 -0.0351562 5.8125 0.046875 C 5.46875 0.140625 5.101562 0.1875 4.71875 0.1875 C 3.5 0.1875 2.550781 -0.238281 1.875 -1.09375 C 1.207031 -1.957031 0.875 -3.171875 0.875 -4.734375 C 0.875 -6.273438 1.210938 -7.476562 1.890625 -8.34375 C 2.566406 -9.21875 3.507812 -9.65625 4.71875 -9.65625 C 5.101562 -9.65625 5.46875 -9.609375 5.8125 -9.515625 C 6.15625 -9.429688 6.488281 -9.300781 6.8125 -9.125 L 6.8125 -7.8125 C 6.5 -8.070312 6.160156 -8.269531 5.796875 -8.40625 C 5.441406 -8.539062 5.082031 -8.609375 4.71875 -8.609375 C 3.882812 -8.609375 3.257812 -8.285156 2.84375 -7.640625 C 2.425781 -6.992188 2.21875 -6.023438 2.21875 -4.734375 C 2.21875 -3.429688 2.425781 -2.457031 2.84375 -1.8125 C 3.257812 -1.175781 3.882812 -0.859375 4.71875 -0.859375 C 5.09375 -0.859375 5.457031 -0.925781 5.8125 -1.0625 C 6.164062 -1.195312 6.5 -1.394531 6.8125 -1.65625 Z M 6.8125 -0.34375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-27">
|
||||
<path style="stroke:none;" d="M 0.546875 -9.484375 L 2.265625 -9.484375 L 3.890625 -4.65625 L 5.546875 -9.484375 L 7.265625 -9.484375 L 7.265625 0 L 6.078125 0 L 6.078125 -8.375 L 4.390625 -3.375 L 3.421875 -3.375 L 1.734375 -8.375 L 1.734375 0 L 0.546875 0 Z M 0.546875 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph1-28">
|
||||
<path style="stroke:none;" d="M 1.359375 -9.484375 L 2.65625 -9.484375 L 2.65625 -1.078125 L 7.234375 -1.078125 L 7.234375 0 L 1.359375 0 Z M 1.359375 -9.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-0">
|
||||
<path style="stroke:none;" d="M 0.640625 2.296875 L 0.640625 -9.171875 L 7.140625 -9.171875 L 7.140625 2.296875 Z M 1.375 1.578125 L 6.421875 1.578125 L 6.421875 -8.4375 L 1.375 -8.4375 Z M 1.375 1.578125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-1">
|
||||
<path style="stroke:none;" d="M 6.9375 -1.734375 L 3.125 -1.734375 L 2.515625 0 L 0.0625 0 L 3.578125 -9.484375 L 6.484375 -9.484375 L 10 0 L 7.546875 0 Z M 3.734375 -3.484375 L 6.328125 -3.484375 L 5.03125 -7.25 Z M 3.734375 -3.484375 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-2">
|
||||
<path style="stroke:none;" d="M 5.921875 -6.0625 L 5.921875 -9.875 L 8.21875 -9.875 L 8.21875 0 L 5.921875 0 L 5.921875 -1.03125 C 5.609375 -0.613281 5.265625 -0.304688 4.890625 -0.109375 C 4.515625 0.0859375 4.082031 0.1875 3.59375 0.1875 C 2.707031 0.1875 1.984375 -0.160156 1.421875 -0.859375 C 0.859375 -1.554688 0.578125 -2.453125 0.578125 -3.546875 C 0.578125 -4.640625 0.859375 -5.535156 1.421875 -6.234375 C 1.984375 -6.929688 2.707031 -7.28125 3.59375 -7.28125 C 4.082031 -7.28125 4.515625 -7.179688 4.890625 -6.984375 C 5.265625 -6.785156 5.609375 -6.476562 5.921875 -6.0625 Z M 4.4375 -1.46875 C 4.914062 -1.46875 5.28125 -1.644531 5.53125 -2 C 5.789062 -2.351562 5.921875 -2.867188 5.921875 -3.546875 C 5.921875 -4.222656 5.789062 -4.738281 5.53125 -5.09375 C 5.28125 -5.445312 4.914062 -5.625 4.4375 -5.625 C 3.945312 -5.625 3.570312 -5.445312 3.3125 -5.09375 C 3.0625 -4.738281 2.9375 -4.222656 2.9375 -3.546875 C 2.9375 -2.867188 3.0625 -2.351562 3.3125 -2 C 3.570312 -1.644531 3.945312 -1.46875 4.4375 -1.46875 Z M 4.4375 -1.46875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-3">
|
||||
<path style="stroke:none;" d="M 7.6875 -5.921875 C 7.96875 -6.367188 8.304688 -6.707031 8.703125 -6.9375 C 9.097656 -7.164062 9.535156 -7.28125 10.015625 -7.28125 C 10.828125 -7.28125 11.445312 -7.023438 11.875 -6.515625 C 12.300781 -6.015625 12.515625 -5.285156 12.515625 -4.328125 L 12.515625 0 L 10.234375 0 L 10.234375 -3.703125 C 10.234375 -3.765625 10.234375 -3.820312 10.234375 -3.875 C 10.242188 -3.9375 10.25 -4.019531 10.25 -4.125 C 10.25 -4.632812 10.171875 -5 10.015625 -5.21875 C 9.867188 -5.445312 9.632812 -5.5625 9.3125 -5.5625 C 8.875 -5.5625 8.535156 -5.382812 8.296875 -5.03125 C 8.066406 -4.675781 7.945312 -4.160156 7.9375 -3.484375 L 7.9375 0 L 5.65625 0 L 5.65625 -3.703125 C 5.65625 -4.492188 5.585938 -5 5.453125 -5.21875 C 5.316406 -5.445312 5.078125 -5.5625 4.734375 -5.5625 C 4.296875 -5.5625 3.957031 -5.382812 3.71875 -5.03125 C 3.476562 -4.675781 3.359375 -4.164062 3.359375 -3.5 L 3.359375 0 L 1.078125 0 L 1.078125 -7.109375 L 3.359375 -7.109375 L 3.359375 -6.0625 C 3.640625 -6.46875 3.960938 -6.769531 4.328125 -6.96875 C 4.691406 -7.175781 5.085938 -7.28125 5.515625 -7.28125 C 6.015625 -7.28125 6.453125 -7.160156 6.828125 -6.921875 C 7.203125 -6.679688 7.488281 -6.347656 7.6875 -5.921875 Z M 7.6875 -5.921875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-4">
|
||||
<path style="stroke:none;" d="M 1.09375 -7.109375 L 3.359375 -7.109375 L 3.359375 0 L 1.09375 0 Z M 1.09375 -9.875 L 3.359375 -9.875 L 3.359375 -8.03125 L 1.09375 -8.03125 Z M 1.09375 -9.875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-5">
|
||||
<path style="stroke:none;" d="M 8.234375 -4.328125 L 8.234375 0 L 5.953125 0 L 5.953125 -3.3125 C 5.953125 -3.925781 5.9375 -4.347656 5.90625 -4.578125 C 5.882812 -4.816406 5.835938 -4.988281 5.765625 -5.09375 C 5.679688 -5.238281 5.5625 -5.351562 5.40625 -5.4375 C 5.257812 -5.519531 5.085938 -5.5625 4.890625 -5.5625 C 4.410156 -5.5625 4.035156 -5.378906 3.765625 -5.015625 C 3.492188 -4.648438 3.359375 -4.144531 3.359375 -3.5 L 3.359375 0 L 1.09375 0 L 1.09375 -7.109375 L 3.359375 -7.109375 L 3.359375 -6.0625 C 3.703125 -6.476562 4.066406 -6.785156 4.453125 -6.984375 C 4.835938 -7.179688 5.265625 -7.28125 5.734375 -7.28125 C 6.554688 -7.28125 7.175781 -7.023438 7.59375 -6.515625 C 8.019531 -6.015625 8.234375 -5.285156 8.234375 -4.328125 Z M 8.234375 -4.328125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-6">
|
||||
<path style="stroke:none;" d="M 6.640625 -6.890625 L 6.640625 -5.15625 C 6.160156 -5.363281 5.691406 -5.515625 5.234375 -5.609375 C 4.785156 -5.710938 4.359375 -5.765625 3.953125 -5.765625 C 3.523438 -5.765625 3.203125 -5.710938 2.984375 -5.609375 C 2.773438 -5.503906 2.671875 -5.335938 2.671875 -5.109375 C 2.671875 -4.929688 2.75 -4.789062 2.90625 -4.6875 C 3.070312 -4.59375 3.359375 -4.519531 3.765625 -4.46875 L 4.171875 -4.421875 C 5.335938 -4.273438 6.117188 -4.03125 6.515625 -3.6875 C 6.921875 -3.351562 7.125 -2.820312 7.125 -2.09375 C 7.125 -1.332031 6.84375 -0.757812 6.28125 -0.375 C 5.726562 0 4.894531 0.1875 3.78125 0.1875 C 3.3125 0.1875 2.828125 0.148438 2.328125 0.078125 C 1.828125 0.00390625 1.3125 -0.109375 0.78125 -0.265625 L 0.78125 -1.984375 C 1.226562 -1.765625 1.691406 -1.597656 2.171875 -1.484375 C 2.648438 -1.378906 3.132812 -1.328125 3.625 -1.328125 C 4.070312 -1.328125 4.40625 -1.382812 4.625 -1.5 C 4.851562 -1.625 4.96875 -1.8125 4.96875 -2.0625 C 4.96875 -2.257812 4.890625 -2.40625 4.734375 -2.5 C 4.578125 -2.601562 4.269531 -2.6875 3.8125 -2.75 L 3.40625 -2.796875 C 2.394531 -2.921875 1.6875 -3.15625 1.28125 -3.5 C 0.875 -3.84375 0.671875 -4.363281 0.671875 -5.0625 C 0.671875 -5.8125 0.925781 -6.367188 1.4375 -6.734375 C 1.957031 -7.097656 2.753906 -7.28125 3.828125 -7.28125 C 4.242188 -7.28125 4.679688 -7.25 5.140625 -7.1875 C 5.597656 -7.125 6.097656 -7.023438 6.640625 -6.890625 Z M 6.640625 -6.890625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-7">
|
||||
<path style="stroke:none;" d="M 3.578125 -9.125 L 3.578125 -7.109375 L 5.921875 -7.109375 L 5.921875 -5.484375 L 3.578125 -5.484375 L 3.578125 -2.46875 C 3.578125 -2.132812 3.640625 -1.910156 3.765625 -1.796875 C 3.898438 -1.679688 4.160156 -1.625 4.546875 -1.625 L 5.71875 -1.625 L 5.71875 0 L 3.765625 0 C 2.867188 0 2.234375 -0.1875 1.859375 -0.5625 C 1.484375 -0.9375 1.296875 -1.570312 1.296875 -2.46875 L 1.296875 -5.484375 L 0.171875 -5.484375 L 0.171875 -7.109375 L 1.296875 -7.109375 L 1.296875 -9.125 Z M 3.578125 -9.125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-8">
|
||||
<path style="stroke:none;" d="M 6.375 -5.171875 C 6.175781 -5.265625 5.976562 -5.332031 5.78125 -5.375 C 5.582031 -5.425781 5.382812 -5.453125 5.1875 -5.453125 C 4.601562 -5.453125 4.148438 -5.265625 3.828125 -4.890625 C 3.515625 -4.515625 3.359375 -3.976562 3.359375 -3.28125 L 3.359375 0 L 1.09375 0 L 1.09375 -7.109375 L 3.359375 -7.109375 L 3.359375 -5.9375 C 3.648438 -6.40625 3.984375 -6.742188 4.359375 -6.953125 C 4.742188 -7.171875 5.203125 -7.28125 5.734375 -7.28125 C 5.804688 -7.28125 5.882812 -7.273438 5.96875 -7.265625 C 6.0625 -7.265625 6.191406 -7.253906 6.359375 -7.234375 Z M 6.375 -5.171875 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-9">
|
||||
<path style="stroke:none;" d="M 4.28125 -3.203125 C 3.800781 -3.203125 3.441406 -3.117188 3.203125 -2.953125 C 2.960938 -2.796875 2.84375 -2.5625 2.84375 -2.25 C 2.84375 -1.957031 2.9375 -1.726562 3.125 -1.5625 C 3.320312 -1.40625 3.59375 -1.328125 3.9375 -1.328125 C 4.363281 -1.328125 4.722656 -1.476562 5.015625 -1.78125 C 5.304688 -2.09375 5.453125 -2.476562 5.453125 -2.9375 L 5.453125 -3.203125 Z M 7.75 -4.0625 L 7.75 0 L 5.453125 0 L 5.453125 -1.046875 C 5.148438 -0.617188 4.804688 -0.304688 4.421875 -0.109375 C 4.046875 0.0859375 3.585938 0.1875 3.046875 0.1875 C 2.304688 0.1875 1.707031 -0.0234375 1.25 -0.453125 C 0.789062 -0.890625 0.5625 -1.453125 0.5625 -2.140625 C 0.5625 -2.972656 0.847656 -3.582031 1.421875 -3.96875 C 1.992188 -4.351562 2.894531 -4.546875 4.125 -4.546875 L 5.453125 -4.546875 L 5.453125 -4.734375 C 5.453125 -5.085938 5.3125 -5.347656 5.03125 -5.515625 C 4.75 -5.679688 4.304688 -5.765625 3.703125 -5.765625 C 3.222656 -5.765625 2.769531 -5.71875 2.34375 -5.625 C 1.925781 -5.53125 1.539062 -5.382812 1.1875 -5.1875 L 1.1875 -6.921875 C 1.664062 -7.035156 2.148438 -7.125 2.640625 -7.1875 C 3.140625 -7.25 3.632812 -7.28125 4.125 -7.28125 C 5.40625 -7.28125 6.328125 -7.023438 6.890625 -6.515625 C 7.460938 -6.015625 7.75 -5.195312 7.75 -4.0625 Z M 7.75 -4.0625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-10">
|
||||
<path style="stroke:none;" d="M 4.46875 -5.65625 C 3.96875 -5.65625 3.582031 -5.472656 3.3125 -5.109375 C 3.050781 -4.753906 2.921875 -4.234375 2.921875 -3.546875 C 2.921875 -2.867188 3.050781 -2.347656 3.3125 -1.984375 C 3.582031 -1.617188 3.96875 -1.4375 4.46875 -1.4375 C 4.96875 -1.4375 5.347656 -1.617188 5.609375 -1.984375 C 5.867188 -2.347656 6 -2.867188 6 -3.546875 C 6 -4.234375 5.867188 -4.753906 5.609375 -5.109375 C 5.347656 -5.472656 4.96875 -5.65625 4.46875 -5.65625 Z M 4.46875 -7.28125 C 5.695312 -7.28125 6.65625 -6.945312 7.34375 -6.28125 C 8.03125 -5.625 8.375 -4.710938 8.375 -3.546875 C 8.375 -2.378906 8.03125 -1.460938 7.34375 -0.796875 C 6.65625 -0.140625 5.695312 0.1875 4.46875 0.1875 C 3.25 0.1875 2.289062 -0.140625 1.59375 -0.796875 C 0.90625 -1.460938 0.5625 -2.378906 0.5625 -3.546875 C 0.5625 -4.710938 0.90625 -5.625 1.59375 -6.28125 C 2.289062 -6.945312 3.25 -7.28125 4.46875 -7.28125 Z M 4.46875 -7.28125 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-11">
|
||||
<path style="stroke:none;" d="M 8.703125 -0.515625 C 8.253906 -0.285156 7.785156 -0.113281 7.296875 0 C 6.816406 0.125 6.3125 0.1875 5.78125 0.1875 C 4.207031 0.1875 2.957031 -0.253906 2.03125 -1.140625 C 1.101562 -2.023438 0.640625 -3.222656 0.640625 -4.734375 C 0.640625 -6.242188 1.101562 -7.441406 2.03125 -8.328125 C 2.957031 -9.210938 4.207031 -9.65625 5.78125 -9.65625 C 6.3125 -9.65625 6.816406 -9.59375 7.296875 -9.46875 C 7.785156 -9.351562 8.253906 -9.175781 8.703125 -8.9375 L 8.703125 -6.984375 C 8.253906 -7.296875 7.804688 -7.519531 7.359375 -7.65625 C 6.921875 -7.800781 6.460938 -7.875 5.984375 -7.875 C 5.109375 -7.875 4.421875 -7.59375 3.921875 -7.03125 C 3.421875 -6.476562 3.171875 -5.710938 3.171875 -4.734375 C 3.171875 -3.753906 3.421875 -2.984375 3.921875 -2.421875 C 4.421875 -1.867188 5.109375 -1.59375 5.984375 -1.59375 C 6.460938 -1.59375 6.921875 -1.660156 7.359375 -1.796875 C 7.804688 -1.941406 8.253906 -2.171875 8.703125 -2.484375 Z M 8.703125 -0.515625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-12">
|
||||
<path style="stroke:none;" d="M 1.015625 -2.765625 L 1.015625 -7.109375 L 3.296875 -7.109375 L 3.296875 -6.40625 C 3.296875 -6.019531 3.289062 -5.535156 3.28125 -4.953125 C 3.28125 -4.367188 3.28125 -3.976562 3.28125 -3.78125 C 3.28125 -3.207031 3.296875 -2.796875 3.328125 -2.546875 C 3.359375 -2.296875 3.410156 -2.113281 3.484375 -2 C 3.578125 -1.851562 3.695312 -1.738281 3.84375 -1.65625 C 4 -1.570312 4.175781 -1.53125 4.375 -1.53125 C 4.84375 -1.53125 5.210938 -1.710938 5.484375 -2.078125 C 5.753906 -2.441406 5.890625 -2.945312 5.890625 -3.59375 L 5.890625 -7.109375 L 8.15625 -7.109375 L 8.15625 0 L 5.890625 0 L 5.890625 -1.03125 C 5.546875 -0.613281 5.179688 -0.304688 4.796875 -0.109375 C 4.421875 0.0859375 4 0.1875 3.53125 0.1875 C 2.707031 0.1875 2.082031 -0.0625 1.65625 -0.5625 C 1.226562 -1.070312 1.015625 -1.804688 1.015625 -2.765625 Z M 1.015625 -2.765625 "/>
|
||||
</symbol>
|
||||
<symbol overflow="visible" id="glyph2-13">
|
||||
<path style="stroke:none;" d="M 8.1875 -3.578125 L 8.1875 -2.921875 L 2.875 -2.921875 C 2.925781 -2.390625 3.117188 -1.988281 3.453125 -1.71875 C 3.785156 -1.457031 4.25 -1.328125 4.84375 -1.328125 C 5.3125 -1.328125 5.796875 -1.394531 6.296875 -1.53125 C 6.804688 -1.675781 7.328125 -1.894531 7.859375 -2.1875 L 7.859375 -0.4375 C 7.316406 -0.226562 6.773438 -0.0703125 6.234375 0.03125 C 5.703125 0.132812 5.164062 0.1875 4.625 0.1875 C 3.34375 0.1875 2.34375 -0.140625 1.625 -0.796875 C 0.914062 -1.453125 0.5625 -2.367188 0.5625 -3.546875 C 0.5625 -4.703125 0.910156 -5.613281 1.609375 -6.28125 C 2.304688 -6.945312 3.269531 -7.28125 4.5 -7.28125 C 5.613281 -7.28125 6.503906 -6.941406 7.171875 -6.265625 C 7.847656 -5.597656 8.1875 -4.703125 8.1875 -3.578125 Z M 5.859375 -4.328125 C 5.859375 -4.765625 5.726562 -5.113281 5.46875 -5.375 C 5.21875 -5.632812 4.890625 -5.765625 4.484375 -5.765625 C 4.046875 -5.765625 3.6875 -5.640625 3.40625 -5.390625 C 3.132812 -5.148438 2.96875 -4.796875 2.90625 -4.328125 Z M 5.859375 -4.328125 "/>
|
||||
</symbol>
|
||||
</g>
|
||||
</defs>
|
||||
<g id="surface25169">
|
||||
<rect x="0" y="0" width="1146" height="548" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.5 4.95 L 35.825 4.95 L 35.825 6.35 L 28.5 6.35 Z M 28.5 4.95 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-1" x="537.601562" y="110"/>
|
||||
<use xlink:href="#glyph0-2" x="549.984375" y="110"/>
|
||||
<use xlink:href="#glyph0-3" x="561.371094" y="110"/>
|
||||
<use xlink:href="#glyph0-4" x="572.816406" y="110"/>
|
||||
<use xlink:href="#glyph0-5" x="578.304688" y="110"/>
|
||||
<use xlink:href="#glyph0-6" x="585.960938" y="110"/>
|
||||
<use xlink:href="#glyph0-7" x="623.109375" y="110"/>
|
||||
<use xlink:href="#glyph0-8" x="623.109375" y="110"/>
|
||||
<use xlink:href="#glyph0-9" x="633.96875" y="110"/>
|
||||
<use xlink:href="#glyph0-10" x="650.648438" y="110"/>
|
||||
<use xlink:href="#glyph0-11" x="662.09375" y="110"/>
|
||||
<use xlink:href="#glyph0-12" x="667.582031" y="110"/>
|
||||
<use xlink:href="#glyph0-5" x="678.382812" y="110"/>
|
||||
<use xlink:href="#glyph0-8" x="686.039062" y="110"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.6 7 L 19.475 7 L 19.475 8.4 L 10.6 8.4 Z M 10.6 7 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-13" x="191" y="151"/>
|
||||
<use xlink:href="#glyph0-10" x="204.59375" y="151"/>
|
||||
<use xlink:href="#glyph0-8" x="216.039063" y="151"/>
|
||||
<use xlink:href="#glyph0-14" x="226.898438" y="151"/>
|
||||
<use xlink:href="#glyph0-15" x="238.285156" y="151"/>
|
||||
<use xlink:href="#glyph0-5" x="249.808594" y="151"/>
|
||||
<use xlink:href="#glyph0-12" x="257.464844" y="151"/>
|
||||
<use xlink:href="#glyph0-16" x="268.265625" y="151"/>
|
||||
<use xlink:href="#glyph0-17" x="277.757812" y="151"/>
|
||||
<use xlink:href="#glyph0-6" x="288.402344" y="151"/>
|
||||
<use xlink:href="#glyph0-18" x="293.96875" y="151"/>
|
||||
<use xlink:href="#glyph0-11" x="305.707031" y="151"/>
|
||||
<use xlink:href="#glyph0-2" x="311.195312" y="151"/>
|
||||
<use xlink:href="#glyph0-19" x="322.582031" y="151"/>
|
||||
<use xlink:href="#glyph0-5" x="332.113281" y="151"/>
|
||||
<use xlink:href="#glyph0-8" x="339.769531" y="151"/>
|
||||
<use xlink:href="#glyph0-20" x="350.628906" y="151"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 28.451953 5.65 L 22.3 5.65 L 22.3 7.7 L 19.85957 7.7 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.491797 7.45 L 19.691797 7.7 L 20.491797 7.95 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-1" x="422" y="121.5"/>
|
||||
<use xlink:href="#glyph1-2" x="429.695313" y="121.5"/>
|
||||
<use xlink:href="#glyph1-2" x="437.390625" y="121.5"/>
|
||||
<use xlink:href="#glyph1-3" x="445.085938" y="121.5"/>
|
||||
<use xlink:href="#glyph1-4" x="452.78125" y="121.5"/>
|
||||
<use xlink:href="#glyph1-5" x="460.476563" y="121.5"/>
|
||||
<use xlink:href="#glyph1-6" x="468.171875" y="121.5"/>
|
||||
<use xlink:href="#glyph1-7" x="475.867188" y="121.5"/>
|
||||
<use xlink:href="#glyph1-8" x="483.5625" y="121.5"/>
|
||||
<use xlink:href="#glyph1-9" x="491.257812" y="121.5"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.75 11.35 L 33.669922 11.35 L 33.669922 12.75 L 30.75 12.75 Z M 30.75 11.35 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-1" x="594.019531" y="238"/>
|
||||
<use xlink:href="#glyph0-2" x="606.402344" y="238"/>
|
||||
<use xlink:href="#glyph0-3" x="617.789062" y="238"/>
|
||||
<use xlink:href="#glyph0-4" x="629.234375" y="238"/>
|
||||
<use xlink:href="#glyph0-5" x="634.722656" y="238"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 32.209961 11.299805 L 32.209961 9.424805 L 32.215039 9.424805 L 32.215039 6.685352 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 32.465039 7.317578 L 32.215039 6.517578 L 31.965039 7.317578 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-10" x="529.753906" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-5" x="537.449219" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-8" x="545.144531" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-6" x="552.839844" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-7" x="560.535156" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-11" x="568.230469" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-9" x="575.925781" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-12" x="583.621094" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-13" x="591.316406" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-4" x="599.011719" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-10" x="606.707031" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-14" x="614.402344" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-15" x="622.097656" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-16" x="629.792969" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-8" x="637.488281" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-4" x="645.183594" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-9" x="652.878906" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-12" x="660.574219" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-7" x="668.269531" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-13" x="675.964844" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-15" x="683.660156" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-9" x="691.355469" y="176.496094"/>
|
||||
<use xlink:href="#glyph1-17" x="699.050781" y="176.496094"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 45.8 0.45 L 48.322461 0.45 L 48.322461 1.85 L 45.8 1.85 Z M 45.8 0.45 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-21" x="895.03125" y="20"/>
|
||||
<use xlink:href="#glyph0-22" x="908.15625" y="20"/>
|
||||
<use xlink:href="#glyph0-12" x="919.152344" y="20"/>
|
||||
<use xlink:href="#glyph0-11" x="929.953125" y="20"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 32.1625 4.899609 L 32.1625 3.6 L 47.061328 3.6 L 47.061328 2.235547 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 47.311328 2.867969 L 47.061328 2.067969 L 46.811328 2.867969 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-1" x="735.457031" y="60"/>
|
||||
<use xlink:href="#glyph1-11" x="743.152344" y="60"/>
|
||||
<use xlink:href="#glyph1-18" x="750.847656" y="60"/>
|
||||
<use xlink:href="#glyph1-4" x="758.542969" y="60"/>
|
||||
<use xlink:href="#glyph1-5" x="766.238281" y="60"/>
|
||||
<use xlink:href="#glyph1-19" x="773.933594" y="60"/>
|
||||
<use xlink:href="#glyph1-5" x="781.628906" y="60"/>
|
||||
<use xlink:href="#glyph1-6" x="789.324219" y="60"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.75 18.35 L 30.372461 18.35 L 30.372461 19.75 L 24.75 19.75 Z M 24.75 18.35 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-1" x="474.054688" y="378"/>
|
||||
<use xlink:href="#glyph0-16" x="486.4375" y="378"/>
|
||||
<use xlink:href="#glyph0-5" x="495.929688" y="378"/>
|
||||
<use xlink:href="#glyph0-4" x="503.585938" y="378"/>
|
||||
<use xlink:href="#glyph0-22" x="509.074219" y="378"/>
|
||||
<use xlink:href="#glyph0-14" x="520.070312" y="378"/>
|
||||
<use xlink:href="#glyph0-6" x="531.457031" y="378"/>
|
||||
<use xlink:href="#glyph0-23" x="537.023438" y="378"/>
|
||||
<use xlink:href="#glyph0-11" x="548.742188" y="378"/>
|
||||
<use xlink:href="#glyph0-12" x="554.230469" y="378"/>
|
||||
<use xlink:href="#glyph0-14" x="565.03125" y="378"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 32.209961 12.8 L 32.209961 14.925 L 26.15 14.925 L 26.15 17.814648 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.9 17.182422 L 26.15 17.982422 L 26.4 17.182422 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-20" x="522.972656" y="286.5"/>
|
||||
<use xlink:href="#glyph1-5" x="530.667969" y="286.5"/>
|
||||
<use xlink:href="#glyph1-12" x="538.363281" y="286.5"/>
|
||||
<use xlink:href="#glyph1-5" x="546.058594" y="286.5"/>
|
||||
<use xlink:href="#glyph1-15" x="553.753906" y="286.5"/>
|
||||
<use xlink:href="#glyph1-16" x="561.449219" y="286.5"/>
|
||||
<use xlink:href="#glyph1-8" x="569.144531" y="286.5"/>
|
||||
<use xlink:href="#glyph1-5" x="576.839844" y="286.5"/>
|
||||
<use xlink:href="#glyph1-6" x="584.535156" y="286.5"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 34.1 23.55 L 37.472461 23.55 L 37.472461 24.95 L 34.1 24.95 Z M 34.1 23.55 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-1" x="661.035156" y="482"/>
|
||||
<use xlink:href="#glyph0-16" x="673.417969" y="482"/>
|
||||
<use xlink:href="#glyph0-5" x="682.910156" y="482"/>
|
||||
<use xlink:href="#glyph0-4" x="690.566406" y="482"/>
|
||||
<use xlink:href="#glyph0-22" x="696.054688" y="482"/>
|
||||
<use xlink:href="#glyph0-14" x="707.050781" y="482"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.561328 21.047266 L 27.561328 22.15 L 35.786328 22.15 L 35.786328 23.501562 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.561328 19.788672 L 27.801172 20.488672 L 27.561328 21.188672 L 27.321289 20.488672 Z M 27.561328 19.788672 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-4" x="553.609375" y="431"/>
|
||||
<use xlink:href="#glyph1-6" x="561.304688" y="431"/>
|
||||
<use xlink:href="#glyph1-7" x="569" y="431"/>
|
||||
<use xlink:href="#glyph1-11" x="576.695313" y="431"/>
|
||||
<use xlink:href="#glyph1-9" x="584.390625" y="431"/>
|
||||
<use xlink:href="#glyph1-17" x="592.085938" y="431"/>
|
||||
<use xlink:href="#glyph1-2" x="599.78125" y="431"/>
|
||||
<use xlink:href="#glyph1-9" x="607.476563" y="431"/>
|
||||
<use xlink:href="#glyph1-6" x="615.171875" y="431"/>
|
||||
<use xlink:href="#glyph1-5" x="622.867188" y="431"/>
|
||||
<use xlink:href="#glyph1-21" x="630.5625" y="431"/>
|
||||
<use xlink:href="#glyph1-7" x="638.257812" y="431"/>
|
||||
<use xlink:href="#glyph1-9" x="645.953125" y="431"/>
|
||||
<use xlink:href="#glyph1-13" x="653.648438" y="431"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 37.472461 24.95 L 37.472461 25.85 L 43.1 25.85 L 43.1 24.25 L 37.472461 24.25 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-22" x="737.402344" y="505"/>
|
||||
<use xlink:href="#glyph1-5" x="745.097656" y="505"/>
|
||||
<use xlink:href="#glyph1-23" x="752.792969" y="505"/>
|
||||
<use xlink:href="#glyph1-8" x="760.488281" y="505"/>
|
||||
<use xlink:href="#glyph1-7" x="768.183594" y="505"/>
|
||||
<use xlink:href="#glyph1-16" x="775.878906" y="505"/>
|
||||
<use xlink:href="#glyph1-11" x="783.574219" y="505"/>
|
||||
<use xlink:href="#glyph1-8" x="791.269531" y="505"/>
|
||||
<use xlink:href="#glyph1-4" x="798.964844" y="505"/>
|
||||
<use xlink:href="#glyph1-9" x="806.660156" y="505"/>
|
||||
<use xlink:href="#glyph1-12" x="814.355469" y="505"/>
|
||||
</g>
|
||||
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 824.074219 505 L 824.074219 497 L 832.074219 501 Z M 824.074219 505 "/>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 52.1 10.95 C 52.1 11.115625 51.965625 11.25 51.8 11.25 C 51.634375 11.25 51.5 11.115625 51.5 10.95 C 51.5 10.784375 51.634375 10.65 51.8 10.65 C 51.965625 10.65 52.1 10.784375 52.1 10.95 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 50.6 11.55 L 53 11.55 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 51.8 11.25 L 51.8 12.75 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 51.8 12.75 L 50.6 14.05 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 51.8 12.75 L 53 14.05 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(100%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph2-1" x="959.921875" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-2" x="969.824219" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-3" x="978.984375" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-4" x="992.324219" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-5" x="996.71875" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-4" x="1005.820312" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-6" x="1010.214844" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-7" x="1017.832031" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-8" x="1023.945312" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-9" x="1030.253906" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-7" x="1038.886719" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-10" x="1045" y="296.898438"/>
|
||||
<use xlink:href="#glyph2-8" x="1053.789062" y="296.898438"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 49.295898 12.75 L 41.708203 12.75 L 41.708203 12.05 L 34.055664 12.05 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 34.688086 11.8 L 33.888086 12.05 L 34.688086 12.3 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-24" x="810.164062" y="236"/>
|
||||
<use xlink:href="#glyph1-15" x="817.859375" y="236"/>
|
||||
<use xlink:href="#glyph1-4" x="825.554688" y="236"/>
|
||||
<use xlink:href="#glyph1-10" x="833.25" y="236"/>
|
||||
<use xlink:href="#glyph1-10" x="840.945312" y="236"/>
|
||||
<use xlink:href="#glyph1-5" x="848.640625" y="236"/>
|
||||
<use xlink:href="#glyph1-15" x="856.335938" y="236"/>
|
||||
<use xlink:href="#glyph1-6" x="864.03125" y="236"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 51.9 9.649609 L 44.0625 9.649609 L 44.0625 5.65 L 36.160352 5.65 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.792578 5.4 L 35.992578 5.65 L 36.792578 5.9 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-25" x="857.25" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-5" x="864.945312" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-13" x="872.640625" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-4" x="880.335938" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-12" x="888.03125" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-5" x="895.726562" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-6" x="903.421875" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-7" x="911.117188" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-1" x="918.8125" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-14" x="926.507812" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-21" x="934.203125" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-4" x="941.898438" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-8" x="949.59375" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-7" x="957.289062" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-11" x="964.984375" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-9" x="972.679688" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-12" x="980.375" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-13" x="988.070312" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-4" x="995.765625" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-10" x="1003.460938" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-14" x="1011.15625" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-15" x="1018.851562" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-16" x="1026.546875" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-8" x="1034.242188" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-4" x="1041.9375" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-9" x="1049.632812" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-12" x="1057.328125" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-7" x="1065.023438" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-4" x="1072.71875" y="140.996094"/>
|
||||
<use xlink:href="#glyph1-12" x="1080.414062" y="140.996094"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 5.45 1.4 C 5.45 1.565625 5.315625 1.7 5.15 1.7 C 4.984375 1.7 4.85 1.565625 4.85 1.4 C 4.85 1.234375 4.984375 1.1 5.15 1.1 C 5.315625 1.1 5.45 1.234375 5.45 1.4 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 3.95 2 L 6.35 2 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 5.15 1.7 L 5.15 3.2 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 5.15 3.2 L 3.95 4.5 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 5.15 3.2 L 6.35 4.5 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(100%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph2-11" x="42.332031" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-12" x="51.726562" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-6" x="60.828125" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-7" x="68.445312" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-10" x="74.558594" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-3" x="83.347656" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-13" x="96.6875" y="105.898438"/>
|
||||
<use xlink:href="#glyph2-8" x="105.359375" y="105.898438"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 6.88418 3.2 L 15.0375 3.2 L 15.0375 6.664648 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 14.7875 6.032422 L 15.0375 6.832422 L 15.2875 6.032422 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-26" x="123.957031" y="52"/>
|
||||
<use xlink:href="#glyph1-9" x="131.652344" y="52"/>
|
||||
<use xlink:href="#glyph1-12" x="139.347656" y="52"/>
|
||||
<use xlink:href="#glyph1-6" x="147.042969" y="52"/>
|
||||
<use xlink:href="#glyph1-14" x="154.738281" y="52"/>
|
||||
<use xlink:href="#glyph1-17" x="162.433594" y="52"/>
|
||||
<use xlink:href="#glyph1-5" x="170.128906" y="52"/>
|
||||
<use xlink:href="#glyph1-6" x="177.824219" y="52"/>
|
||||
<use xlink:href="#glyph1-7" x="185.519531" y="52"/>
|
||||
<use xlink:href="#glyph1-15" x="193.214844" y="52"/>
|
||||
<use xlink:href="#glyph1-5" x="200.910156" y="52"/>
|
||||
<use xlink:href="#glyph1-6" x="208.605469" y="52"/>
|
||||
<use xlink:href="#glyph1-9" x="216.300781" y="52"/>
|
||||
<use xlink:href="#glyph1-14" x="223.996094" y="52"/>
|
||||
<use xlink:href="#glyph1-15" x="231.691406" y="52"/>
|
||||
<use xlink:href="#glyph1-11" x="239.386719" y="52"/>
|
||||
<use xlink:href="#glyph1-5" x="247.082031" y="52"/>
|
||||
<use xlink:href="#glyph1-6" x="254.777344" y="52"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 1.35 12.9 L 6.490039 12.9 L 6.490039 14.3 L 1.35 14.3 Z M 1.35 12.9 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-24" x="5.972656" y="269"/>
|
||||
<use xlink:href="#glyph0-8" x="18.296875" y="269"/>
|
||||
<use xlink:href="#glyph0-19" x="29.15625" y="269"/>
|
||||
<use xlink:href="#glyph0-22" x="38.6875" y="269"/>
|
||||
<use xlink:href="#glyph0-2" x="49.683594" y="269"/>
|
||||
<use xlink:href="#glyph0-20" x="61.070312" y="269"/>
|
||||
<use xlink:href="#glyph0-16" x="68.960938" y="269"/>
|
||||
<use xlink:href="#glyph0-8" x="78.453125" y="269"/>
|
||||
<use xlink:href="#glyph0-19" x="89.3125" y="269"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 9.341406 7.7 L 3.919922 7.7 L 3.919922 12.9 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.6 7.7 L 9.9 7.940039 L 9.2 7.7 L 9.9 7.459961 Z M 10.6 7.7 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 35.786133 24.95 L 35.786133 27 L 3.919922 27 L 3.919922 14.635352 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4.169922 15.267578 L 3.919922 14.467578 L 3.669922 15.267578 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-27" x="340.28125" y="528"/>
|
||||
<use xlink:href="#glyph1-9" x="347.976563" y="528"/>
|
||||
<use xlink:href="#glyph1-21" x="355.671875" y="528"/>
|
||||
<use xlink:href="#glyph1-4" x="363.367188" y="528"/>
|
||||
<use xlink:href="#glyph1-13" x="371.0625" y="528"/>
|
||||
<use xlink:href="#glyph1-4" x="378.757813" y="528"/>
|
||||
<use xlink:href="#glyph1-5" x="386.453125" y="528"/>
|
||||
<use xlink:href="#glyph1-6" x="394.148438" y="528"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 51.8 15.49707 L 51.8 19.05 L 30.707813 19.05 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.340039 18.8 L 30.540039 19.05 L 31.340039 19.3 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-28" x="764.945312" y="369"/>
|
||||
<use xlink:href="#glyph1-16" x="772.640625" y="369"/>
|
||||
<use xlink:href="#glyph1-14" x="780.335938" y="369"/>
|
||||
<use xlink:href="#glyph1-12" x="788.03125" y="369"/>
|
||||
<use xlink:href="#glyph1-11" x="795.726562" y="369"/>
|
||||
<use xlink:href="#glyph1-18" x="803.421875" y="369"/>
|
||||
<use xlink:href="#glyph1-5" x="811.117188" y="369"/>
|
||||
<use xlink:href="#glyph1-6" x="818.8125" y="369"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 54.144922 2.155078 L 58.557422 2.155078 L 58.557422 3.555078 L 54.144922 3.555078 Z M 54.144922 2.155078 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-15" x="1061.902344" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-5" x="1073.425781" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-20" x="1081.082031" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-12" x="1088.972656" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-5" x="1099.773438" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-8" x="1107.429688" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-25" x="1118.289062" y="54.101562"/>
|
||||
<use xlink:href="#glyph0-26" x="1129.734375" y="54.101562"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.4,0.4;stroke-miterlimit:10;" d="M 48.322461 1.15 L 51.008594 1.15 L 51.008594 2.855078 L 53.760742 2.855078 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 53.12832 3.105078 L 53.92832 2.855078 L 53.12832 2.605078 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-14" x="996.171875" y="28.050781"/>
|
||||
<use xlink:href="#glyph1-6" x="1003.867188" y="28.050781"/>
|
||||
<use xlink:href="#glyph1-5" x="1011.5625" y="28.050781"/>
|
||||
<use xlink:href="#glyph1-6" x="1019.257812" y="28.050781"/>
|
||||
</g>
|
||||
<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.561328 21.058594 L 27.561328 22.15 L 19.345703 22.15 L 19.345703 23.652734 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.561328 19.8 L 27.801172 20.5 L 27.561328 21.2 L 27.321289 20.5 Z M 27.561328 19.8 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph1-4" x="389.203125" y="431"/>
|
||||
<use xlink:href="#glyph1-6" x="396.898438" y="431"/>
|
||||
<use xlink:href="#glyph1-7" x="404.59375" y="431"/>
|
||||
<use xlink:href="#glyph1-11" x="412.289062" y="431"/>
|
||||
<use xlink:href="#glyph1-9" x="419.984375" y="431"/>
|
||||
<use xlink:href="#glyph1-17" x="427.679688" y="431"/>
|
||||
<use xlink:href="#glyph1-2" x="435.375" y="431"/>
|
||||
<use xlink:href="#glyph1-9" x="443.070312" y="431"/>
|
||||
<use xlink:href="#glyph1-6" x="450.765625" y="431"/>
|
||||
<use xlink:href="#glyph1-5" x="458.460938" y="431"/>
|
||||
<use xlink:href="#glyph1-21" x="466.15625" y="431"/>
|
||||
<use xlink:href="#glyph1-7" x="473.851562" y="431"/>
|
||||
<use xlink:href="#glyph1-9" x="481.546875" y="431"/>
|
||||
<use xlink:href="#glyph1-13" x="489.242188" y="431"/>
|
||||
</g>
|
||||
<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.201953 23.652734 L 23.489453 23.652734 L 23.489453 25.052734 L 15.201953 25.052734 Z M 15.201953 23.652734 " transform="matrix(20,0,0,20,-26,-8)"/>
|
||||
<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
|
||||
<use xlink:href="#glyph0-27" x="283.082031" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-28" x="294.019531" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-16" x="311.871094" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-12" x="321.363281" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-16" x="332.164062" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-26" x="341.65625" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-6" x="352.085938" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-29" x="357.652344" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-14" x="363.609375" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-3" x="374.996094" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-4" x="386.441406" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-16" x="391.929688" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-12" x="401.421875" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-5" x="412.222656" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-22" x="419.878906" y="484.054688"/>
|
||||
<use xlink:href="#glyph0-20" x="430.875" y="484.054688"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 89 KiB |
BIN
doc/source/images/watcher_db_schema_diagram.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
@@ -73,6 +73,7 @@ Plugins
|
||||
:maxdepth: 1
|
||||
|
||||
dev/plugin/base-setup
|
||||
dev/plugin/goal-plugin
|
||||
dev/plugin/strategy-plugin
|
||||
dev/plugin/action-plugin
|
||||
dev/plugin/planner-plugin
|
||||
@@ -90,6 +91,7 @@ Introduction
|
||||
|
||||
deploy/installation
|
||||
deploy/user-guide
|
||||
deploy/policy
|
||||
deploy/gmr
|
||||
|
||||
Watcher Manual Pages
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
{
|
||||
"admin_api": "role:admin or role:administrator",
|
||||
"show_password": "!",
|
||||
"default": "rule:admin_api"
|
||||
"default": "rule:admin_api",
|
||||
|
||||
"action:detail": "rule:default",
|
||||
"action:get": "rule:default",
|
||||
"action:get_all": "rule:default",
|
||||
|
||||
"action_plan:delete": "rule:default",
|
||||
"action_plan:detail": "rule:default",
|
||||
"action_plan:get": "rule:default",
|
||||
"action_plan:get_all": "rule:default",
|
||||
"action_plan:update": "rule:default",
|
||||
|
||||
"audit:create": "rule:default",
|
||||
"audit:delete": "rule:default",
|
||||
"audit:detail": "rule:default",
|
||||
"audit:get": "rule:default",
|
||||
"audit:get_all": "rule:default",
|
||||
"audit:update": "rule:default",
|
||||
|
||||
"audit_template:create": "rule:default",
|
||||
"audit_template:delete": "rule:default",
|
||||
"audit_template:detail": "rule:default",
|
||||
"audit_template:get": "rule:default",
|
||||
"audit_template:get_all": "rule:default",
|
||||
"audit_template:update": "rule:default",
|
||||
|
||||
"goal:detail": "rule:default",
|
||||
"goal:get": "rule:default",
|
||||
"goal:get_all": "rule:default",
|
||||
|
||||
"strategy:detail": "rule:default",
|
||||
"strategy:get": "rule:default",
|
||||
"strategy:get_all": "rule:default"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,13 @@ wrap_width = 79
|
||||
|
||||
namespace = watcher
|
||||
namespace = keystonemiddleware.auth_token
|
||||
namespace = oslo.log
|
||||
namespace = oslo.cache
|
||||
namespace = oslo.concurrency
|
||||
namespace = oslo.db
|
||||
namespace = oslo.log
|
||||
namespace = oslo.messaging
|
||||
namespace = oslo.policy
|
||||
namespace = oslo.reports
|
||||
namespace = oslo.service.periodic_task
|
||||
namespace = oslo.service.service
|
||||
namespace = oslo.service.wsgi
|
||||
|
||||
244
releasenotes/source/conf.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# watcher documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Jun 3 11:37:52 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
from watcher import version as watcher_version
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['reno.sphinxext',
|
||||
'oslosphinx']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'watcher'
|
||||
copyright = u'2016, Watcher developers'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = watcher_version.version_info.release_string()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = watcher_version.version_info.version_string()
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'watcherdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output -------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual])
|
||||
latex_documents = [
|
||||
('index', 'watcher.tex', u'Watcher Documentation',
|
||||
u'Watcher developers', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output -------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'watcher', u'Watcher Documentation',
|
||||
[u'Watcher developers'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'watcher', u'Watcher Documentation',
|
||||
u'Watcher developers', 'watcher', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
10
releasenotes/source/index.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Welcome to watcher's Release Notes documentation!
|
||||
=================================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased.rst
|
||||
|
||||
5
releasenotes/source/unreleased.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
==============================
|
||||
Current Series Release Notes
|
||||
==============================
|
||||
|
||||
.. release-notes::
|
||||
@@ -2,29 +2,30 @@
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
apscheduler # MIT License
|
||||
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
|
||||
keystoneauth1>=2.7.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.config>=3.10.0 # Apache-2.0
|
||||
oslo.context>=2.4.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.messaging>=5.2.0 # Apache-2.0
|
||||
oslo.policy>=1.9.0 # Apache-2.0
|
||||
oslo.reports>=0.6.0 # Apache-2.0
|
||||
oslo.service>=1.10.0 # Apache-2.0
|
||||
oslo.utils>=3.5.0 # Apache-2.0
|
||||
oslo.utils>=3.14.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-cinderclient!=1.7.0,!=1.7.1,>=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
|
||||
|
||||
11
setup.cfg
@@ -45,13 +45,22 @@ tempest.test_plugins =
|
||||
watcher.database.migration_backend =
|
||||
sqlalchemy = watcher.db.sqlalchemy.migration
|
||||
|
||||
watcher_goals =
|
||||
unclassified = watcher.decision_engine.goal.goals:Unclassified
|
||||
dummy = watcher.decision_engine.goal.goals:Dummy
|
||||
server_consolidation = watcher.decision_engine.goal.goals:ServerConsolidation
|
||||
thermal_optimization = watcher.decision_engine.goal.goals:ThermalOptimization
|
||||
workload_balancing = watcher.decision_engine.goal.goals:WorkloadBalancing
|
||||
airflow_optimization = watcher.decision_engine.goal.goals:AirflowOptimization
|
||||
|
||||
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
|
||||
outlet_temperature = 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
|
||||
uniform_airflow = watcher.decision_engine.strategy.strategies.uniform_airflow:UniformAirflow
|
||||
|
||||
watcher_actions =
|
||||
migrate = watcher.applier.actions.migration:Migrate
|
||||
|
||||
@@ -9,7 +9,7 @@ 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
|
||||
os-testr>=0.7.0 # 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
|
||||
@@ -17,5 +17,11 @@ testtools>=1.4.0 # MIT
|
||||
|
||||
# Doc requirements
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
|
||||
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
|
||||
sphinxcontrib-pecanwsme>=0.8 # Apache-2.0
|
||||
|
||||
# releasenotes
|
||||
reno>=1.8.0 # Apache2
|
||||
|
||||
# bandit
|
||||
bandit>=1.0.1 # Apache-2.0
|
||||
|
||||
10
tox.ini
@@ -20,6 +20,7 @@ commands =
|
||||
commands =
|
||||
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
|
||||
flake8
|
||||
bandit -r watcher -x tests -n5 -ll
|
||||
|
||||
[testenv:venv]
|
||||
setenv = PYTHONHASHSEED=0
|
||||
@@ -46,7 +47,7 @@ commands =
|
||||
show-source=True
|
||||
ignore=
|
||||
builtins= _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/,releasenotes
|
||||
|
||||
[testenv:wheel]
|
||||
commands = python setup.py bdist_wheel
|
||||
@@ -58,3 +59,10 @@ import_exceptions = watcher._i18n
|
||||
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,doc/source/api
|
||||
|
||||
[testenv:releasenotes]
|
||||
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[testenv:bandit]
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = bandit -r watcher -x tests -n5 -ll
|
||||
|
||||
@@ -30,7 +30,7 @@ API_SERVICE_OPTS = [
|
||||
default=9322,
|
||||
help=_('The port for the watcher API server')),
|
||||
cfg.StrOpt('host',
|
||||
default='0.0.0.0',
|
||||
default='127.0.0.1',
|
||||
help=_('The listen IP for the watcher API server')),
|
||||
cfg.IntOpt('max_limit',
|
||||
default=1000,
|
||||
|
||||
@@ -22,7 +22,7 @@ from watcher.api import hooks
|
||||
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
|
||||
server = {
|
||||
'port': '9322',
|
||||
'host': '0.0.0.0'
|
||||
'host': '127.0.0.1'
|
||||
}
|
||||
|
||||
# Pecan Application Configurations
|
||||
|
||||
@@ -167,4 +167,5 @@ class Controller(rest.RestController):
|
||||
# the request object to make the links.
|
||||
return V1.convert()
|
||||
|
||||
__all__ = (Controller)
|
||||
|
||||
__all__ = ("Controller", )
|
||||
|
||||
@@ -63,12 +63,14 @@ 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 exception
|
||||
from watcher.common import policy
|
||||
from watcher import objects
|
||||
|
||||
|
||||
@@ -302,6 +304,10 @@ class ActionsController(rest.RestController):
|
||||
:param audit_uuid: Optional UUID of an audit,
|
||||
to get only actions for that audit.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'action:get_all',
|
||||
action='action:get_all')
|
||||
|
||||
if action_plan_uuid and audit_uuid:
|
||||
raise exception.ActionFilterCombinationProhibited
|
||||
|
||||
@@ -326,6 +332,10 @@ class ActionsController(rest.RestController):
|
||||
:param audit_uuid: Optional UUID of an audit,
|
||||
to get only actions for that audit.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'action:detail',
|
||||
action='action:detail')
|
||||
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "actions":
|
||||
@@ -349,8 +359,10 @@ class ActionsController(rest.RestController):
|
||||
if self.from_actions:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action = objects.Action.get_by_uuid(pecan.request.context,
|
||||
action_uuid)
|
||||
context = pecan.request.context
|
||||
action = api_utils.get_resource('Action', action_uuid)
|
||||
policy.enforce(context, 'action:get', action, action='action:get')
|
||||
|
||||
return Action.convert_with_links(action)
|
||||
|
||||
@wsme_pecan.wsexpose(Action, body=Action, status_code=201)
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An :ref:`Action Plan <action_plan_definition>` is a flow of
|
||||
An :ref:`Action Plan <action_plan_definition>` specifies a flow of
|
||||
:ref:`Actions <action_definition>` that should be executed in order to satisfy
|
||||
a given :ref:`Goal <goal_definition>`.
|
||||
a given :ref:`Goal <goal_definition>`. It also contains an estimated
|
||||
:ref:`global efficacy <efficacy_definition>` alongside a set of
|
||||
:ref:`efficacy indicators <efficacy_indicator_definition>`.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
|
||||
:ref:`Audit <audit_definition>` is successful which implies that the
|
||||
@@ -26,16 +28,13 @@ An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
|
||||
which was used has found a :ref:`Solution <solution_definition>` to achieve the
|
||||
:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`.
|
||||
|
||||
In the default implementation of Watcher, an
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
is only composed of successive :ref:`Actions <action_definition>`
|
||||
(i.e., a Workflow of :ref:`Actions <action_definition>` belonging to a unique
|
||||
branch).
|
||||
In the default implementation of Watcher, an action plan is composed of
|
||||
a list of successive :ref:`Actions <action_definition>` (i.e., a Workflow of
|
||||
:ref:`Actions <action_definition>` belonging to a unique branch).
|
||||
|
||||
However, Watcher provides abstract interfaces for many of its components,
|
||||
allowing other implementations to generate and handle more complex
|
||||
:ref:`Action Plan(s) <action_plan_definition>`
|
||||
composed of two types of Action Item(s):
|
||||
allowing other implementations to generate and handle more complex :ref:`Action
|
||||
Plan(s) <action_plan_definition>` composed of two types of Action Item(s):
|
||||
|
||||
- simple :ref:`Actions <action_definition>`: atomic tasks, which means it
|
||||
can not be split into smaller tasks or commands from an OpenStack point of
|
||||
@@ -46,16 +45,18 @@ composed of two types of Action Item(s):
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` may be described using
|
||||
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/>`_.
|
||||
`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/>`_.
|
||||
|
||||
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
|
||||
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>`.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from oslo_log import log
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import wsme
|
||||
@@ -66,13 +67,17 @@ 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 efficacy_indicator as efficacyindicator
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils as api_utils
|
||||
from watcher.applier import rpcapi
|
||||
from watcher.common import exception
|
||||
from watcher.common import policy
|
||||
from watcher import objects
|
||||
from watcher.objects import action_plan as ap_objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionPlanPatchType(types.JsonPatchType):
|
||||
|
||||
@@ -113,6 +118,7 @@ class ActionPlan(base.APIBase):
|
||||
|
||||
_audit_uuid = None
|
||||
_first_action_uuid = None
|
||||
_efficacy_indicators = None
|
||||
|
||||
def _get_audit_uuid(self):
|
||||
return self._audit_uuid
|
||||
@@ -143,6 +149,34 @@ class ActionPlan(base.APIBase):
|
||||
except exception.ActionNotFound:
|
||||
self._first_action_uuid = None
|
||||
|
||||
def _get_efficacy_indicators(self):
|
||||
if self._efficacy_indicators is None:
|
||||
self._set_efficacy_indicators(wtypes.Unset)
|
||||
return self._efficacy_indicators
|
||||
|
||||
def _set_efficacy_indicators(self, value):
|
||||
efficacy_indicators = []
|
||||
if value == wtypes.Unset and not self._efficacy_indicators:
|
||||
try:
|
||||
_efficacy_indicators = objects.EfficacyIndicator.list(
|
||||
pecan.request.context,
|
||||
filters={"action_plan_uuid": self.uuid})
|
||||
|
||||
for indicator in _efficacy_indicators:
|
||||
efficacy_indicator = efficacyindicator.EfficacyIndicator(
|
||||
context=pecan.request.context,
|
||||
name=indicator.name,
|
||||
description=indicator.description,
|
||||
unit=indicator.unit,
|
||||
value=indicator.value,
|
||||
)
|
||||
efficacy_indicators.append(efficacy_indicator.as_dict())
|
||||
self._efficacy_indicators = efficacy_indicators
|
||||
except exception.EfficacyIndicatorNotFound as exc:
|
||||
LOG.exception(exc)
|
||||
elif value and self._efficacy_indicators != value:
|
||||
self._efficacy_indicators = value
|
||||
|
||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||
"""Unique UUID for this action plan"""
|
||||
|
||||
@@ -155,6 +189,14 @@ class ActionPlan(base.APIBase):
|
||||
mandatory=True)
|
||||
"""The UUID of the audit this port belongs to"""
|
||||
|
||||
efficacy_indicators = wsme.wsproperty(
|
||||
types.jsontype, _get_efficacy_indicators, _set_efficacy_indicators,
|
||||
mandatory=True)
|
||||
"""The list of efficacy indicators associated to this action plan"""
|
||||
|
||||
global_efficacy = wtypes.wsattr(types.jsontype, readonly=True)
|
||||
"""The global efficacy of this action plan"""
|
||||
|
||||
state = wtypes.text
|
||||
"""This action plan state"""
|
||||
|
||||
@@ -163,7 +205,6 @@ class ActionPlan(base.APIBase):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ActionPlan, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
fields = list(objects.ActionPlan.fields)
|
||||
for field in fields:
|
||||
@@ -175,6 +216,7 @@ class ActionPlan(base.APIBase):
|
||||
|
||||
self.fields.append('audit_uuid')
|
||||
self.fields.append('first_action_uuid')
|
||||
self.fields.append('efficacy_indicators')
|
||||
|
||||
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
|
||||
setattr(self, 'first_action_uuid',
|
||||
@@ -184,12 +226,13 @@ class ActionPlan(base.APIBase):
|
||||
def _convert_with_links(action_plan, url, expand=True):
|
||||
if not expand:
|
||||
action_plan.unset_fields_except(
|
||||
['uuid', 'state', 'updated_at',
|
||||
'audit_uuid', 'first_action_uuid'])
|
||||
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
|
||||
'updated_at', 'audit_uuid', 'first_action_uuid'])
|
||||
|
||||
action_plan.links = [link.Link.make_link(
|
||||
'self', url,
|
||||
'action_plans', action_plan.uuid),
|
||||
action_plan.links = [
|
||||
link.Link.make_link(
|
||||
'self', url,
|
||||
'action_plans', action_plan.uuid),
|
||||
link.Link.make_link(
|
||||
'bookmark', url,
|
||||
'action_plans', action_plan.uuid,
|
||||
@@ -211,6 +254,12 @@ class ActionPlan(base.APIBase):
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720'
|
||||
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
|
||||
sample._efficacy_indicators = [{'description': 'Test indicator',
|
||||
'name': 'test_indicator',
|
||||
'unit': '%'}]
|
||||
sample._global_efficacy = {'description': 'Global efficacy',
|
||||
'name': 'test_global_efficacy',
|
||||
'unit': '%'}
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
@@ -310,6 +359,10 @@ class ActionPlansController(rest.RestController):
|
||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||
for that audit.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'action_plan:get_all',
|
||||
action='action_plan:get_all')
|
||||
|
||||
return self._get_action_plans_collection(
|
||||
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid)
|
||||
|
||||
@@ -326,6 +379,10 @@ class ActionPlansController(rest.RestController):
|
||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||
for that audit.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'action_plan:detail',
|
||||
action='action_plan:detail')
|
||||
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "action_plans":
|
||||
@@ -347,8 +404,11 @@ class ActionPlansController(rest.RestController):
|
||||
if self.from_actionsPlans:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action_plan = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context, action_plan_uuid)
|
||||
context = pecan.request.context
|
||||
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid)
|
||||
policy.enforce(
|
||||
context, 'action_plan:get', action_plan, action='action_plan:get')
|
||||
|
||||
return ActionPlan.convert_with_links(action_plan)
|
||||
|
||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||
@@ -357,11 +417,12 @@ class ActionPlansController(rest.RestController):
|
||||
|
||||
:param action_plan_uuid: UUID of a action.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid)
|
||||
policy.enforce(context, 'action_plan:delete', action_plan,
|
||||
action='action_plan:delete')
|
||||
|
||||
action_plan_to_delete = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context,
|
||||
action_plan_uuid)
|
||||
action_plan_to_delete.soft_delete()
|
||||
action_plan.soft_delete()
|
||||
|
||||
@wsme.validate(types.uuid, [ActionPlanPatchType])
|
||||
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
|
||||
@@ -376,9 +437,12 @@ class ActionPlansController(rest.RestController):
|
||||
if self.from_actionsPlans:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action_plan_to_update = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context,
|
||||
action_plan_uuid)
|
||||
context = pecan.request.context
|
||||
action_plan_to_update = api_utils.get_resource('ActionPlan',
|
||||
action_plan_uuid)
|
||||
policy.enforce(context, 'action_plan:update', action_plan_to_update,
|
||||
action='action_plan:update')
|
||||
|
||||
try:
|
||||
action_plan_dict = action_plan_to_update.as_dict()
|
||||
action_plan = ActionPlan(**api_utils.apply_jsonpatch(
|
||||
|
||||
@@ -44,6 +44,7 @@ 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 policy
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine import rpcapi
|
||||
from watcher import objects
|
||||
@@ -53,29 +54,54 @@ class AuditPostType(wtypes.Base):
|
||||
|
||||
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=True)
|
||||
|
||||
type = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
audit_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)
|
||||
|
||||
parameters = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False,
|
||||
default={})
|
||||
interval = wsme.wsattr(int, mandatory=False)
|
||||
|
||||
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)
|
||||
if self.audit_type not in audit_type_values:
|
||||
raise exception.AuditTypeNotFound(audit_type=self.audit_type)
|
||||
|
||||
if (self.audit_type == objects.audit.AuditType.ONESHOT.value and
|
||||
self.interval != wtypes.Unset):
|
||||
raise exception.AuditIntervalNotAllowed(audit_type=self.audit_type)
|
||||
|
||||
if (self.audit_type == objects.audit.AuditType.CONTINUOUS.value and
|
||||
self.interval == wtypes.Unset):
|
||||
raise exception.AuditIntervalNotSpecified(
|
||||
audit_type=self.audit_type)
|
||||
|
||||
return Audit(
|
||||
audit_template_id=self.audit_template_uuid,
|
||||
type=self.type,
|
||||
deadline=self.deadline)
|
||||
audit_type=self.audit_type,
|
||||
deadline=self.deadline,
|
||||
parameters=self.parameters,
|
||||
interval=self.interval)
|
||||
|
||||
|
||||
class AuditPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return ['/audit_template_uuid']
|
||||
return ['/audit_template_uuid', '/type']
|
||||
|
||||
@staticmethod
|
||||
def validate(patch):
|
||||
serialized_patch = {'path': patch.path, 'op': patch.op}
|
||||
if patch.path in AuditPatchType.mandatory_attrs():
|
||||
msg = _("%(field)s can't be updated.")
|
||||
raise exception.PatchError(
|
||||
patch=serialized_patch,
|
||||
reason=msg % dict(field=patch.path))
|
||||
return types.JsonPatchType.validate(patch)
|
||||
|
||||
|
||||
class Audit(base.APIBase):
|
||||
@@ -127,7 +153,7 @@ class Audit(base.APIBase):
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this audit"""
|
||||
|
||||
type = wtypes.text
|
||||
audit_type = wtypes.text
|
||||
"""Type of this audit"""
|
||||
|
||||
deadline = datetime.datetime
|
||||
@@ -148,9 +174,15 @@ class Audit(base.APIBase):
|
||||
mandatory=False)
|
||||
"""The name of the audit template this audit refers to"""
|
||||
|
||||
parameters = {wtypes.text: types.jsontype}
|
||||
"""The strategy parameters for this audit"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated audit links"""
|
||||
|
||||
interval = wsme.wsattr(int, mandatory=False)
|
||||
"""Launch audit periodically (in seconds)"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fields = []
|
||||
fields = list(objects.Audit.fields)
|
||||
@@ -176,9 +208,9 @@ class Audit(base.APIBase):
|
||||
@staticmethod
|
||||
def _convert_with_links(audit, url, expand=True):
|
||||
if not expand:
|
||||
audit.unset_fields_except(['uuid', 'type', 'deadline',
|
||||
audit.unset_fields_except(['uuid', 'audit_type', 'deadline',
|
||||
'state', 'audit_template_uuid',
|
||||
'audit_template_name'])
|
||||
'audit_template_name', 'interval'])
|
||||
|
||||
# The numeric ID should not be exposed to
|
||||
# the user, it's internal only.
|
||||
@@ -201,12 +233,13 @@ class Audit(base.APIBase):
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
type='ONESHOT',
|
||||
audit_type='ONESHOT',
|
||||
state='PENDING',
|
||||
deadline=None,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
updated_at=datetime.datetime.utcnow(),
|
||||
interval=7200)
|
||||
sample._audit_template_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
@@ -295,34 +328,41 @@ class AuditsController(rest.RestController):
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
|
||||
@wsme_pecan.wsexpose(AuditCollection, wtypes.text, types.uuid, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def get_all(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_template=None):
|
||||
def get_all(self, audit_template=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audits.
|
||||
|
||||
:param audit_template: Optional UUID or name of an audit
|
||||
: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.
|
||||
:param audit_template: Optional UUID or name of an audit
|
||||
template, to get only audits for that audit template.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'audit:get_all',
|
||||
action='audit:get_all')
|
||||
return self._get_audits_collection(marker, limit, sort_key,
|
||||
sort_dir,
|
||||
audit_template=audit_template)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
|
||||
wtypes.text)
|
||||
def detail(self, marker=None, limit=None,
|
||||
@wsme_pecan.wsexpose(AuditCollection, wtypes.text, types.uuid, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def detail(self, audit_template=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audits with detail.
|
||||
|
||||
:param audit_template: Optional UUID or name of an audit
|
||||
: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.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'audit:detail',
|
||||
action='audit:detail')
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "audits":
|
||||
@@ -332,7 +372,8 @@ class AuditsController(rest.RestController):
|
||||
resource_url = '/'.join(['audits', 'detail'])
|
||||
return self._get_audits_collection(marker, limit,
|
||||
sort_key, sort_dir, expand,
|
||||
resource_url)
|
||||
resource_url,
|
||||
audit_template=audit_template)
|
||||
|
||||
@wsme_pecan.wsexpose(Audit, types.uuid)
|
||||
def get_one(self, audit_uuid):
|
||||
@@ -343,8 +384,10 @@ class AuditsController(rest.RestController):
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
rpc_audit = objects.Audit.get_by_uuid(pecan.request.context,
|
||||
audit_uuid)
|
||||
context = pecan.request.context
|
||||
rpc_audit = api_utils.get_resource('Audit', audit_uuid)
|
||||
policy.enforce(context, 'audit:get', rpc_audit, action='audit:get')
|
||||
|
||||
return Audit.convert_with_links(rpc_audit)
|
||||
|
||||
@wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201)
|
||||
@@ -353,6 +396,10 @@ class AuditsController(rest.RestController):
|
||||
|
||||
:param audit_p: a audit within the request body.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'audit:create',
|
||||
action='audit:create')
|
||||
|
||||
audit = audit_p.as_audit()
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
@@ -362,6 +409,25 @@ class AuditsController(rest.RestController):
|
||||
message=_('The audit template UUID or name specified is '
|
||||
'invalid'))
|
||||
|
||||
audit_template = objects.AuditTemplate.get(pecan.request.context,
|
||||
audit._audit_template_uuid)
|
||||
strategy_id = audit_template.strategy_id
|
||||
no_schema = True
|
||||
if strategy_id is not None:
|
||||
# validate parameter when predefined strategy in audit template
|
||||
strategy = objects.Strategy.get(pecan.request.context, strategy_id)
|
||||
schema = strategy.parameters_spec
|
||||
if schema:
|
||||
# validate input parameter with default value feedback
|
||||
no_schema = False
|
||||
utils.DefaultValidatingDraft4Validator(schema).validate(
|
||||
audit.parameters)
|
||||
|
||||
if no_schema and audit.parameters:
|
||||
raise exception.Invalid(_('Specify parameters but no predefined '
|
||||
'strategy for audit template, or no '
|
||||
'parameter spec in predefined strategy'))
|
||||
|
||||
audit_dict = audit.as_dict()
|
||||
context = pecan.request.context
|
||||
new_audit = objects.Audit(context, **audit_dict)
|
||||
@@ -372,8 +438,9 @@ class AuditsController(rest.RestController):
|
||||
|
||||
# trigger decision-engine to run the audit
|
||||
|
||||
dc_client = rpcapi.DecisionEngineAPI()
|
||||
dc_client.trigger_audit(context, new_audit.uuid)
|
||||
if new_audit.audit_type == objects.audit.AuditType.ONESHOT.value:
|
||||
dc_client = rpcapi.DecisionEngineAPI()
|
||||
dc_client.trigger_audit(context, new_audit.uuid)
|
||||
|
||||
return Audit.convert_with_links(new_audit)
|
||||
|
||||
@@ -388,6 +455,12 @@ class AuditsController(rest.RestController):
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
context = pecan.request.context
|
||||
audit_to_update = api_utils.get_resource('Audit',
|
||||
audit_uuid)
|
||||
policy.enforce(context, 'audit:update', audit_to_update,
|
||||
action='audit:update')
|
||||
|
||||
audit_to_update = objects.Audit.get_by_uuid(pecan.request.context,
|
||||
audit_uuid)
|
||||
try:
|
||||
@@ -417,8 +490,9 @@ class AuditsController(rest.RestController):
|
||||
|
||||
:param audit_uuid: UUID of a audit.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
audit_to_delete = api_utils.get_resource('Audit', audit_uuid)
|
||||
policy.enforce(context, 'audit:update', audit_to_delete,
|
||||
action='audit:update')
|
||||
|
||||
audit_to_delete = objects.Audit.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_uuid)
|
||||
audit_to_delete.soft_delete()
|
||||
|
||||
@@ -64,6 +64,7 @@ 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 policy
|
||||
from watcher.common import utils as common_utils
|
||||
from watcher import objects
|
||||
|
||||
@@ -493,6 +494,9 @@ class AuditTemplatesController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'audit_template:get_all',
|
||||
action='audit_template:get_all')
|
||||
filters = {}
|
||||
if goal:
|
||||
if common_utils.is_uuid_like(goal):
|
||||
@@ -522,6 +526,10 @@ class AuditTemplatesController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'audit_template:detail',
|
||||
action='audit_template:detail')
|
||||
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "audit_templates":
|
||||
@@ -555,14 +563,11 @@ class AuditTemplatesController(rest.RestController):
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
if common_utils.is_uuid_like(audit_template):
|
||||
rpc_audit_template = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
else:
|
||||
rpc_audit_template = objects.AuditTemplate.get_by_name(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
context = pecan.request.context
|
||||
rpc_audit_template = api_utils.get_resource('AuditTemplate',
|
||||
audit_template)
|
||||
policy.enforce(context, 'audit_template:get', rpc_audit_template,
|
||||
action='audit_template:get')
|
||||
|
||||
return AuditTemplate.convert_with_links(rpc_audit_template)
|
||||
|
||||
@@ -578,6 +583,10 @@ class AuditTemplatesController(rest.RestController):
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'audit_template:create',
|
||||
action='audit_template:create')
|
||||
|
||||
context = pecan.request.context
|
||||
audit_template = audit_template_postdata.as_audit_template()
|
||||
audit_template_dict = audit_template.as_dict()
|
||||
@@ -602,6 +611,13 @@ class AuditTemplatesController(rest.RestController):
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
context = pecan.request.context
|
||||
audit_template_to_update = api_utils.get_resource('AuditTemplate',
|
||||
audit_template)
|
||||
policy.enforce(context, 'audit_template:update',
|
||||
audit_template_to_update,
|
||||
action='audit_template:update')
|
||||
|
||||
if common_utils.is_uuid_like(audit_template):
|
||||
audit_template_to_update = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
@@ -639,14 +655,11 @@ class AuditTemplatesController(rest.RestController):
|
||||
|
||||
:param audit template_uuid: UUID or name of an audit template.
|
||||
"""
|
||||
|
||||
if common_utils.is_uuid_like(audit_template):
|
||||
audit_template_to_delete = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
else:
|
||||
audit_template_to_delete = objects.AuditTemplate.get_by_name(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
context = pecan.request.context
|
||||
audit_template_to_delete = api_utils.get_resource('AuditTemplate',
|
||||
audit_template)
|
||||
policy.enforce(context, 'audit_template:update',
|
||||
audit_template_to_delete,
|
||||
action='audit_template:update')
|
||||
|
||||
audit_template_to_delete.soft_delete()
|
||||
|
||||
72
watcher/api/controllers/v1/efficacy_indicator.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- 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.
|
||||
|
||||
"""
|
||||
An efficacy indicator is a single value that gives an indication on how the
|
||||
:ref:`solution <solution_definition>` produced by a given :ref:`strategy
|
||||
<strategy_definition>` performed. These efficacy indicators are specific to a
|
||||
given :ref:`goal <goal_definition>` and are usually used to compute the
|
||||
:ref:`gobal efficacy <efficacy_definition>` of the resulting :ref:`action plan
|
||||
<action_plan_definition>`.
|
||||
|
||||
In Watcher, these efficacy indicators are specified alongside the goal they
|
||||
relate to. When a strategy (which always relates to a goal) is executed, it
|
||||
produces a solution containing the efficacy indicators specified by the goal.
|
||||
This solution, which has been translated by the :ref:`Watcher Planner
|
||||
<watcher_planner_definition>` into an action plan, will see its indicators and
|
||||
global efficacy stored and would now be accessible through the :ref:`Watcher
|
||||
API <archi_watcher_api_definition>`.
|
||||
"""
|
||||
|
||||
import numbers
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from watcher.api.controllers import base
|
||||
from watcher import objects
|
||||
|
||||
|
||||
class EfficacyIndicator(base.APIBase):
|
||||
"""API representation of a efficacy indicator.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of an
|
||||
efficacy indicator.
|
||||
"""
|
||||
|
||||
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
"""Name of this efficacy indicator"""
|
||||
|
||||
description = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||
"""Description of this efficacy indicator"""
|
||||
|
||||
unit = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||
"""Unit of this efficacy indicator"""
|
||||
|
||||
value = wtypes.wsattr(numbers.Number, mandatory=True)
|
||||
"""Value of this efficacy indicator"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EfficacyIndicator, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
fields = list(objects.EfficacyIndicator.fields)
|
||||
for field in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
continue
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
@@ -46,7 +46,7 @@ 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.common import policy
|
||||
from watcher import objects
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -68,24 +68,28 @@ class Goal(base.APIBase):
|
||||
display_name = wtypes.text
|
||||
"""Localized name of the goal"""
|
||||
|
||||
efficacy_specification = wtypes.wsattr(types.jsontype, readonly=True)
|
||||
"""Efficacy specification for this goal"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""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('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))
|
||||
fields = list(objects.Goal.fields)
|
||||
|
||||
for k in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, k):
|
||||
continue
|
||||
self.fields.append(k)
|
||||
setattr(self, k, kwargs.get(k, wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(goal, url, expand=True):
|
||||
if not expand:
|
||||
goal.unset_fields_except(['uuid', 'name', 'display_name'])
|
||||
goal.unset_fields_except(['uuid', 'name', 'display_name',
|
||||
'efficacy_specification'])
|
||||
|
||||
goal.links = [link.Link.make_link('self', url,
|
||||
'goals', goal.uuid),
|
||||
@@ -101,9 +105,16 @@ class Goal(base.APIBase):
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
name='DUMMY',
|
||||
display_name='Dummy strategy')
|
||||
sample = cls(
|
||||
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
name='DUMMY',
|
||||
display_name='Dummy strategy',
|
||||
efficacy_specification=[
|
||||
{'description': 'Dummy indicator', 'name': 'dummy',
|
||||
'schema': 'Range(min=0, max=100, min_included=True, '
|
||||
'max_included=True, msg=None)',
|
||||
'unit': '%'}
|
||||
])
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
@@ -190,6 +201,9 @@ class GoalsController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'goal:get_all',
|
||||
action='goal:get_all')
|
||||
return self._get_goals_collection(marker, limit, sort_key, sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
|
||||
@@ -202,6 +216,9 @@ class GoalsController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'goal:detail',
|
||||
action='goal:detail')
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "goals":
|
||||
@@ -220,11 +237,8 @@ class GoalsController(rest.RestController):
|
||||
if self.from_goals:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
if common_utils.is_uuid_like(goal):
|
||||
get_goal_func = objects.Goal.get_by_uuid
|
||||
else:
|
||||
get_goal_func = objects.Goal.get_by_name
|
||||
|
||||
rpc_goal = get_goal_func(pecan.request.context, goal)
|
||||
context = pecan.request.context
|
||||
rpc_goal = api_utils.get_resource('Goal', goal)
|
||||
policy.enforce(context, 'goal:get', rpc_goal, action='goal:get')
|
||||
|
||||
return Goal.convert_with_links(rpc_goal)
|
||||
|
||||
@@ -41,6 +41,7 @@ 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 policy
|
||||
from watcher.common import utils as common_utils
|
||||
from watcher import objects
|
||||
|
||||
@@ -54,6 +55,7 @@ class Strategy(base.APIBase):
|
||||
between the internal object model and the API representation of a strategy.
|
||||
"""
|
||||
_goal_uuid = None
|
||||
_goal_name = None
|
||||
|
||||
def _get_goal(self, value):
|
||||
if value == wtypes.Unset:
|
||||
@@ -81,6 +83,16 @@ class Strategy(base.APIBase):
|
||||
if goal:
|
||||
self._goal_uuid = goal.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
|
||||
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this strategy"""
|
||||
|
||||
@@ -97,6 +109,13 @@ class Strategy(base.APIBase):
|
||||
mandatory=True)
|
||||
"""The UUID of the goal this audit refers to"""
|
||||
|
||||
goal_name = wsme.wsproperty(wtypes.text, _get_goal_name, _set_goal_name,
|
||||
mandatory=False)
|
||||
"""The name of the goal this audit refers to"""
|
||||
|
||||
parameters_spec = {wtypes.text: types.jsontype}
|
||||
""" Parameters spec dict"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Strategy, self).__init__()
|
||||
|
||||
@@ -105,16 +124,21 @@ class Strategy(base.APIBase):
|
||||
self.fields.append('name')
|
||||
self.fields.append('display_name')
|
||||
self.fields.append('goal_uuid')
|
||||
self.fields.append('goal_name')
|
||||
self.fields.append('parameters_spec')
|
||||
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))
|
||||
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
|
||||
setattr(self, 'parameters_spec', kwargs.get('parameters_spec',
|
||||
wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(strategy, url, expand=True):
|
||||
if not expand:
|
||||
strategy.unset_fields_except(
|
||||
['uuid', 'name', 'display_name', 'goal_uuid'])
|
||||
['uuid', 'name', 'display_name', 'goal_uuid', 'goal_name'])
|
||||
|
||||
strategy.links = [
|
||||
link.Link.make_link('self', url, 'strategies', strategy.uuid),
|
||||
@@ -223,6 +247,9 @@ class StrategiesController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'strategy:get_all',
|
||||
action='strategy:get_all')
|
||||
filters = {}
|
||||
if goal:
|
||||
if common_utils.is_uuid_like(goal):
|
||||
@@ -245,6 +272,9 @@ class StrategiesController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'strategy:detail',
|
||||
action='strategy:detail')
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "strategies":
|
||||
@@ -271,11 +301,9 @@ class StrategiesController(rest.RestController):
|
||||
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)
|
||||
context = pecan.request.context
|
||||
rpc_strategy = api_utils.get_resource('Strategy', strategy)
|
||||
policy.enforce(context, 'strategy:get', rpc_strategy,
|
||||
action='strategy:get')
|
||||
|
||||
return Strategy.convert_with_links(rpc_strategy)
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
|
||||
import jsonpatch
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
import wsme
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher import objects
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@@ -75,3 +78,19 @@ def as_filters_dict(**filters):
|
||||
filters_dict[filter_name] = filter_value
|
||||
|
||||
return filters_dict
|
||||
|
||||
|
||||
def get_resource(resource, resource_ident):
|
||||
"""Get the resource from the uuid or logical name.
|
||||
|
||||
:param resource: the resource type.
|
||||
:param resource_ident: the UUID or logical name of the resource.
|
||||
|
||||
:returns: The resource.
|
||||
"""
|
||||
resource = getattr(objects, resource)
|
||||
|
||||
if uuidutils.is_uuid_like(resource_ident):
|
||||
return resource.get_by_uuid(pecan.request.context, resource_ident)
|
||||
|
||||
return resource.get_by_name(pecan.request.context, resource_ident)
|
||||
|
||||
@@ -56,6 +56,8 @@ class ContextHook(hooks.PecanHook):
|
||||
auth_token = headers.get('X-Auth-Token', auth_token)
|
||||
show_deleted = headers.get('X-Show-Deleted')
|
||||
auth_token_info = state.request.environ.get('keystone.token_info')
|
||||
roles = (headers.get('X-Roles', None) and
|
||||
headers.get('X-Roles').split(','))
|
||||
|
||||
auth_url = headers.get('X-Auth-Url')
|
||||
if auth_url is None:
|
||||
@@ -72,7 +74,8 @@ class ContextHook(hooks.PecanHook):
|
||||
project_id=project_id,
|
||||
domain_id=domain_id,
|
||||
domain_name=domain_name,
|
||||
show_deleted=show_deleted)
|
||||
show_deleted=show_deleted,
|
||||
roles=roles)
|
||||
|
||||
|
||||
class NoExceptionTracebackHook(hooks.PecanHook):
|
||||
|
||||
@@ -19,7 +19,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.actions.loading import default
|
||||
from watcher.applier.loading import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier import base
|
||||
from watcher.applier.workflow_engine.loading import default
|
||||
from watcher.applier.loading import default
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
@@ -56,9 +56,7 @@ class DefaultApplier(base.BaseApplier):
|
||||
|
||||
def execute(self, action_plan_uuid):
|
||||
LOG.debug("Executing action plan %s ", action_plan_uuid)
|
||||
action_plan = objects.ActionPlan.get_by_uuid(self.context,
|
||||
action_plan_uuid)
|
||||
# todo(jed) remove direct access to dbapi need filter in object
|
||||
filters = {'action_plan_id': action_plan.id}
|
||||
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||
filters = {'action_plan_uuid': action_plan_uuid}
|
||||
actions = objects.Action.list(self.context,
|
||||
filters=filters)
|
||||
return self.engine.execute(actions)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- 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
|
||||
@@ -13,10 +10,10 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from watcher.common.loader import default
|
||||
|
||||
|
||||
@@ -24,3 +21,9 @@ class DefaultWorkFlowEngineLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultWorkFlowEngineLoader, self).__init__(
|
||||
namespace='watcher_workflow_engines')
|
||||
|
||||
|
||||
class DefaultActionLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultActionLoader, self).__init__(
|
||||
namespace='watcher_actions')
|
||||
@@ -38,8 +38,8 @@ def main():
|
||||
server = service.WSGIService(
|
||||
'watcher-api', CONF.api.enable_ssl_api)
|
||||
|
||||
if host == '0.0.0.0':
|
||||
LOG.info(_LI('serving on 0.0.0.0:%(port)s, '
|
||||
if host == '127.0.0.1':
|
||||
LOG.info(_LI('serving on 127.0.0.1:%(port)s, '
|
||||
'view at %(protocol)s://127.0.0.1:%(port)s') %
|
||||
dict(protocol=protocol, port=port))
|
||||
else:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from ceilometerclient.exc import HTTPUnauthorized
|
||||
from ceilometerclient import exc
|
||||
|
||||
from watcher.common import clients
|
||||
|
||||
@@ -68,7 +68,7 @@ class CeilometerHelper(object):
|
||||
def query_retry(self, f, *args, **kargs):
|
||||
try:
|
||||
return f(*args, **kargs)
|
||||
except HTTPUnauthorized:
|
||||
except exc.HTTPUnauthorized:
|
||||
self.osc.reset_clients()
|
||||
self.ceilometer = self.osc.ceilometer()
|
||||
return f(*args, **kargs)
|
||||
|
||||
@@ -20,7 +20,7 @@ class RequestContext(context.RequestContext):
|
||||
domain_name=None, user=None, user_id=None, project=None,
|
||||
project_id=None, is_admin=False, is_public_api=False,
|
||||
read_only=False, show_deleted=False, request_id=None,
|
||||
trust_id=None, auth_token_info=None):
|
||||
trust_id=None, auth_token_info=None, roles=None):
|
||||
"""Stores several additional request parameters:
|
||||
|
||||
:param domain_id: The ID of the domain.
|
||||
@@ -44,7 +44,8 @@ class RequestContext(context.RequestContext):
|
||||
is_admin=is_admin,
|
||||
read_only=read_only,
|
||||
show_deleted=show_deleted,
|
||||
request_id=request_id)
|
||||
request_id=request_id,
|
||||
roles=roles)
|
||||
|
||||
def to_dict(self):
|
||||
return {'auth_token': self.auth_token,
|
||||
@@ -61,7 +62,8 @@ class RequestContext(context.RequestContext):
|
||||
'show_deleted': self.show_deleted,
|
||||
'request_id': self.request_id,
|
||||
'trust_id': self.trust_id,
|
||||
'auth_token_info': self.auth_token_info}
|
||||
'auth_token_info': self.auth_token_info,
|
||||
'roles': self.roles}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, values):
|
||||
|
||||
@@ -123,6 +123,10 @@ class NotAuthorized(WatcherException):
|
||||
code = 403
|
||||
|
||||
|
||||
class PolicyNotAuthorized(NotAuthorized):
|
||||
msg_fmt = _("Policy doesn't allow %(action)s to be performed.")
|
||||
|
||||
|
||||
class OperationNotPermitted(NotAuthorized):
|
||||
msg_fmt = _("Operation not permitted")
|
||||
|
||||
@@ -150,6 +154,11 @@ class InvalidIdentity(Invalid):
|
||||
msg_fmt = _("Expected a uuid or int but received %(identity)s")
|
||||
|
||||
|
||||
class InvalidOperator(Invalid):
|
||||
msg_fmt = _("Filter operator is not valid: %(operator)s not "
|
||||
"in %(valid_operators)s")
|
||||
|
||||
|
||||
class InvalidGoal(Invalid):
|
||||
msg_fmt = _("Goal %(goal)s is invalid")
|
||||
|
||||
@@ -212,6 +221,14 @@ class AuditAlreadyExists(Conflict):
|
||||
msg_fmt = _("An audit with UUID %(uuid)s already exists")
|
||||
|
||||
|
||||
class AuditIntervalNotSpecified(Invalid):
|
||||
msg_fmt = _("Interval of audit must be specified for %(audit_type)s.")
|
||||
|
||||
|
||||
class AuditIntervalNotAllowed(Invalid):
|
||||
msg_fmt = _("Interval of audit must not be set for %(audit_type)s.")
|
||||
|
||||
|
||||
class AuditReferenced(Invalid):
|
||||
msg_fmt = _("Audit %(audit)s is referenced by one or multiple action "
|
||||
"plans")
|
||||
@@ -248,6 +265,14 @@ class ActionFilterCombinationProhibited(Invalid):
|
||||
"prohibited")
|
||||
|
||||
|
||||
class EfficacyIndicatorNotFound(ResourceNotFound):
|
||||
msg_fmt = _("Efficacy indicator %(efficacy_indicator)s could not be found")
|
||||
|
||||
|
||||
class EfficacyIndicatorAlreadyExists(Conflict):
|
||||
msg_fmt = _("An action with UUID %(uuid)s already exists")
|
||||
|
||||
|
||||
class HTTPNotFound(ResourceNotFound):
|
||||
pass
|
||||
|
||||
@@ -298,6 +323,16 @@ class NoAvailableStrategyForGoal(WatcherException):
|
||||
msg_fmt = _("No strategy could be found to achieve the '%(goal)s' goal.")
|
||||
|
||||
|
||||
class InvalidIndicatorValue(WatcherException):
|
||||
msg_fmt = _("The indicator '%(name)s' with value '%(value)s' "
|
||||
"and spec type '%(spec_type)s' is invalid.")
|
||||
|
||||
|
||||
class GlobalEfficacyComputationError(WatcherException):
|
||||
msg_fmt = _("Could not compute the global efficacy for the '%(goal)s' "
|
||||
"goal using the '%(strategy)s' strategy.")
|
||||
|
||||
|
||||
class NoMetricValuesForVM(WatcherException):
|
||||
msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.")
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ class DefaultLoader(base.BaseLoader):
|
||||
"""Load the config of the plugin"""
|
||||
config = utils.Struct()
|
||||
config_opts = driver_cls.get_config_opts()
|
||||
|
||||
if not config_opts:
|
||||
return config
|
||||
|
||||
|
||||
@@ -15,55 +15,80 @@
|
||||
|
||||
"""Policy Engine For Watcher."""
|
||||
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
|
||||
from oslo_policy import policy
|
||||
|
||||
from watcher.common import exception
|
||||
|
||||
|
||||
_ENFORCER = None
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@lockutils.synchronized('policy_enforcer', 'watcher-')
|
||||
def init_enforcer(policy_file=None, rules=None,
|
||||
default_rule=None, use_conf=True):
|
||||
"""Synchronously initializes the policy enforcer
|
||||
|
||||
:param policy_file: Custom policy file to use, if none is specified,
|
||||
`CONF.policy_file` will be used.
|
||||
:param rules: Default dictionary / Rules to use. It will be
|
||||
considered just in the first instantiation.
|
||||
:param default_rule: Default rule to use, CONF.default_rule will
|
||||
be used if none is specified.
|
||||
:param use_conf: Whether to load rules from config file.
|
||||
# we can get a policy enforcer by this init.
|
||||
# oslo policy support change policy rule dynamically.
|
||||
# at present, policy.enforce will reload the policy rules when it checks
|
||||
# the policy files have been touched.
|
||||
def init(policy_file=None, rules=None,
|
||||
default_rule=None, use_conf=True, overwrite=True):
|
||||
"""Init an Enforcer class.
|
||||
|
||||
:param policy_file: Custom policy file to use, if none is
|
||||
specified, ``conf.policy_file`` will be
|
||||
used.
|
||||
:param rules: Default dictionary / Rules to use. It will be
|
||||
considered just in the first instantiation. If
|
||||
:meth:`load_rules` with ``force_reload=True``,
|
||||
:meth:`clear` or :meth:`set_rules` with
|
||||
``overwrite=True`` is called this will be overwritten.
|
||||
:param default_rule: Default rule to use, conf.default_rule will
|
||||
be used if none is specified.
|
||||
:param use_conf: Whether to load rules from cache or config file.
|
||||
:param overwrite: Whether to overwrite existing rules when reload rules
|
||||
from config file.
|
||||
"""
|
||||
global _ENFORCER
|
||||
|
||||
if _ENFORCER:
|
||||
return
|
||||
|
||||
_ENFORCER = policy.Enforcer(policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf)
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
"""Provides access to the single instance of Policy enforcer."""
|
||||
|
||||
if not _ENFORCER:
|
||||
init_enforcer()
|
||||
|
||||
# http://docs.openstack.org/developer/oslo.policy/usage.html
|
||||
_ENFORCER = policy.Enforcer(CONF,
|
||||
policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf,
|
||||
overwrite=overwrite)
|
||||
return _ENFORCER
|
||||
|
||||
|
||||
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs):
|
||||
"""A shortcut for policy.Enforcer.enforce()
|
||||
def enforce(context, rule=None, target=None,
|
||||
do_raise=True, exc=None, *args, **kwargs):
|
||||
|
||||
Checks authorization of a rule against the target and credentials.
|
||||
"""Checks authorization of a rule against the target and credentials.
|
||||
|
||||
:param dict context: As much information about the user performing the
|
||||
action as possible.
|
||||
:param rule: The rule to evaluate.
|
||||
:param dict target: As much information about the object being operated
|
||||
on as possible.
|
||||
:param do_raise: Whether to raise an exception or not if check
|
||||
fails.
|
||||
:param exc: Class of the exception to raise if the check fails.
|
||||
Any remaining arguments passed to :meth:`enforce` (both
|
||||
positional and keyword arguments) will be passed to
|
||||
the exception class. If not specified,
|
||||
:class:`PolicyNotAuthorized` will be used.
|
||||
|
||||
:return: ``False`` if the policy does not allow the action and `exc` is
|
||||
not provided; otherwise, returns a value that evaluates to
|
||||
``True``. Note: for rules using the "case" expression, this
|
||||
``True`` value will be the specified string from the
|
||||
expression.
|
||||
"""
|
||||
enforcer = get_enforcer()
|
||||
return enforcer.enforce(rule, target, creds, do_raise=do_raise,
|
||||
exc=exc, *args, **kwargs)
|
||||
enforcer = init()
|
||||
credentials = context.to_dict()
|
||||
if not exc:
|
||||
exc = exception.PolicyNotAuthorized
|
||||
if target is None:
|
||||
target = {'project_id': context.project_id,
|
||||
'user_id': context.user_id}
|
||||
return enforcer.enforce(rule, target, credentials,
|
||||
do_raise=do_raise, exc=exc, *args, **kwargs)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
from jsonschema import validators
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
@@ -129,3 +130,25 @@ def get_cls_import_path(cls):
|
||||
if module is None or module == str.__module__:
|
||||
return cls.__name__
|
||||
return module + '.' + cls.__name__
|
||||
|
||||
|
||||
# Default value feedback extension as jsonschema doesn't support it
|
||||
def extend_with_default(validator_class):
|
||||
validate_properties = validator_class.VALIDATORS["properties"]
|
||||
|
||||
def set_defaults(validator, properties, instance, schema):
|
||||
for prop, subschema in properties.items():
|
||||
if "default" in subschema:
|
||||
instance.setdefault(prop, subschema["default"])
|
||||
|
||||
for error in validate_properties(
|
||||
validator, properties, instance, schema,
|
||||
):
|
||||
yield error
|
||||
|
||||
return validators.extend(
|
||||
validator_class, {"properties": set_defaults},
|
||||
)
|
||||
|
||||
DefaultValidatingDraft4Validator = extend_with_default(
|
||||
validators.Draft4Validator)
|
||||
|
||||
@@ -139,8 +139,6 @@ class BaseConnection(object):
|
||||
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.
|
||||
@@ -221,7 +219,7 @@ class BaseConnection(object):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_template_list(self, context, columns=None, filters=None,
|
||||
def get_audit_template_list(self, context, filters=None,
|
||||
limit=None, marker=None, sort_key=None,
|
||||
sort_dir=None):
|
||||
"""Get specific columns for matching audit templates.
|
||||
@@ -230,8 +228,6 @@ class BaseConnection(object):
|
||||
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 audit templates to return.
|
||||
@@ -320,7 +316,7 @@ class BaseConnection(object):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_list(self, context, columns=None, filters=None, limit=None,
|
||||
def get_audit_list(self, context, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching audits.
|
||||
|
||||
@@ -328,8 +324,6 @@ class BaseConnection(object):
|
||||
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 audits to return.
|
||||
@@ -407,7 +401,7 @@ class BaseConnection(object):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_list(self, context, columns=None, filters=None, limit=None,
|
||||
def get_action_list(self, context, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching actions.
|
||||
|
||||
@@ -415,8 +409,6 @@ class BaseConnection(object):
|
||||
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 actions to return.
|
||||
@@ -490,7 +482,7 @@ class BaseConnection(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_plan_list(
|
||||
self, context, columns=None, filters=None, limit=None,
|
||||
self, context, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching action plans.
|
||||
|
||||
@@ -498,8 +490,6 @@ class BaseConnection(object):
|
||||
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 audits to return.
|
||||
@@ -560,3 +550,93 @@ class BaseConnection(object):
|
||||
:raises: :py:class:`~.ActionPlanReferenced`
|
||||
:raises: :py:class:`~.Invalid`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_efficacy_indicator_list(self, context, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching efficacy indicators.
|
||||
|
||||
Return a list of the specified columns for all efficacy indicators 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 efficacy indicators 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_efficacy_indicator(self, values):
|
||||
"""Create a new efficacy indicator.
|
||||
|
||||
:param values: A dict containing items used to identify
|
||||
and track the efficacy indicator. For example:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'id': 1,
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': 'my_efficacy_indicator',
|
||||
'display_name': 'My efficacy indicator',
|
||||
'goal_uuid': utils.generate_uuid(),
|
||||
}
|
||||
:returns: An efficacy_indicator
|
||||
:raises: :py:class:`~.EfficacyIndicatorAlreadyExists`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id):
|
||||
"""Return an efficacy indicator given its ID.
|
||||
|
||||
:param context: The security context
|
||||
:param efficacy_indicator_id: The ID of an efficacy indicator
|
||||
:returns: An efficacy indicator
|
||||
:raises: :py:class:`~.EfficacyIndicatorNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid):
|
||||
"""Return an efficacy indicator given its UUID.
|
||||
|
||||
:param context: The security context
|
||||
:param efficacy_indicator_uuid: The UUID of an efficacy indicator
|
||||
:returns: An efficacy indicator
|
||||
:raises: :py:class:`~.EfficacyIndicatorNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name):
|
||||
"""Return an efficacy indicator given its name.
|
||||
|
||||
:param context: The security context
|
||||
:param efficacy_indicator_name: The name of an efficacy indicator
|
||||
:returns: An efficacy indicator
|
||||
:raises: :py:class:`~.EfficacyIndicatorNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_efficacy_indicator(self, efficacy_indicator_uuid):
|
||||
"""Destroy an efficacy indicator.
|
||||
|
||||
:param efficacy_indicator_uuid: The UUID of an efficacy indicator
|
||||
:raises: :py:class:`~.EfficacyIndicatorNotFound`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_efficacy_indicator(self, efficacy_indicator_uuid, values):
|
||||
"""Update properties of an efficacy indicator.
|
||||
|
||||
:param efficacy_indicator_uuid: The UUID of an efficacy indicator
|
||||
:returns: An efficacy indicator
|
||||
:raises: :py:class:`~.EfficacyIndicatorNotFound`
|
||||
:raises: :py:class:`~.Invalid`
|
||||
"""
|
||||
|
||||
@@ -14,16 +14,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
"""SQLAlchemy storage backend."""
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import operator
|
||||
|
||||
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 sqlalchemy.orm import exc
|
||||
|
||||
from watcher import _i18n
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
from watcher.db import api
|
||||
@@ -34,7 +37,6 @@ from watcher.objects import audit as audit_objects
|
||||
from watcher.objects import utils as objutils
|
||||
|
||||
CONF = cfg.CONF
|
||||
_ = _i18n._
|
||||
|
||||
_FACADE = None
|
||||
|
||||
@@ -102,119 +104,132 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None,
|
||||
return query.all()
|
||||
|
||||
|
||||
class JoinMap(utils.Struct):
|
||||
"""Mapping for the Join-based queries"""
|
||||
|
||||
|
||||
NaturalJoinFilter = collections.namedtuple(
|
||||
'NaturalJoinFilter', ['join_fieldname', 'join_model'])
|
||||
|
||||
|
||||
class Connection(api.BaseConnection):
|
||||
"""SqlAlchemy connection."""
|
||||
|
||||
valid_operators = {
|
||||
"": operator.eq,
|
||||
"eq": operator.eq,
|
||||
"neq": operator.ne,
|
||||
"gt": operator.gt,
|
||||
"gte": operator.ge,
|
||||
"lt": operator.lt,
|
||||
"lte": operator.le,
|
||||
"in": lambda field, choices: field.in_(choices),
|
||||
"notin": lambda field, choices: field.notin_(choices),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
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']))
|
||||
def __add_simple_filter(self, query, model, fieldname, value, operator_):
|
||||
field = getattr(model, fieldname)
|
||||
|
||||
return query
|
||||
if field.type.python_type is datetime.datetime:
|
||||
value = objutils.datetime_or_str_or_none(value)
|
||||
|
||||
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']))
|
||||
return query.filter(self.valid_operators[operator_](field, value))
|
||||
|
||||
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']))
|
||||
def __add_join_filter(self, query, model, fieldname, value, operator_):
|
||||
query = query.join(model)
|
||||
return self.__add_simple_filter(query, model, fieldname,
|
||||
value, operator_)
|
||||
|
||||
return query
|
||||
def __decompose_filter(self, raw_fieldname):
|
||||
"""Decompose a filter name into its 2 subparts
|
||||
|
||||
def __add_simple_filter(self, query, model, fieldname, value):
|
||||
return query.filter(getattr(model, fieldname) == value)
|
||||
A filter can take 2 forms:
|
||||
|
||||
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)
|
||||
- "<FIELDNAME>" which is a syntactic sugar for "<FIELDNAME>__eq"
|
||||
- "<FIELDNAME>__<OPERATOR>" where <OPERATOR> is the comparison operator
|
||||
to be used.
|
||||
|
||||
Available operators are:
|
||||
|
||||
- eq
|
||||
- neq
|
||||
- gt
|
||||
- gte
|
||||
- lt
|
||||
- lte
|
||||
- in
|
||||
- notin
|
||||
"""
|
||||
separator = '__'
|
||||
fieldname, separator, operator_ = raw_fieldname.partition(separator)
|
||||
|
||||
if operator_ and operator_ not in self.valid_operators:
|
||||
raise exception.InvalidOperator(
|
||||
operator=operator_, valid_operators=self.valid_operators)
|
||||
|
||||
return fieldname, operator_
|
||||
|
||||
def _add_filters(self, query, model, filters=None,
|
||||
plain_fields=None, join_fieldmap=None):
|
||||
"""Generic way to add filters to a Watcher model
|
||||
|
||||
Each filter key provided by the `filters` parameter will be decomposed
|
||||
into 2 pieces: the field name and the comparison operator
|
||||
|
||||
- "": By default, the "eq" is applied if no operator is provided
|
||||
- "eq", which stands for "equal" : e.g. {"state__eq": "PENDING"}
|
||||
will result in the "WHERE state = 'PENDING'" clause.
|
||||
- "neq", which stands for "not equal" : e.g. {"state__neq": "PENDING"}
|
||||
will result in the "WHERE state != 'PENDING'" clause.
|
||||
- "gt", which stands for "greater than" : e.g.
|
||||
{"created_at__gt": "2016-06-06T10:33:22.063176"} will result in the
|
||||
"WHERE created_at > '2016-06-06T10:33:22.063176'" clause.
|
||||
- "gte", which stands for "greater than or equal to" : e.g.
|
||||
{"created_at__gte": "2016-06-06T10:33:22.063176"} will result in the
|
||||
"WHERE created_at >= '2016-06-06T10:33:22.063176'" clause.
|
||||
- "lt", which stands for "less than" : e.g.
|
||||
{"created_at__lt": "2016-06-06T10:33:22.063176"} will result in the
|
||||
"WHERE created_at < '2016-06-06T10:33:22.063176'" clause.
|
||||
- "lte", which stands for "less than or equal to" : e.g.
|
||||
{"created_at__lte": "2016-06-06T10:33:22.063176"} will result in the
|
||||
"WHERE created_at <= '2016-06-06T10:33:22.063176'" clause.
|
||||
- "in": e.g. {"state__in": ('SUCCEEDED', 'FAILED')} will result in the
|
||||
"WHERE state IN ('SUCCEEDED', 'FAILED')" clause.
|
||||
|
||||
: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
|
||||
|
||||
"""
|
||||
soft_delete_mixin_fields = ['deleted', 'deleted_at']
|
||||
timestamp_mixin_fields = ['created_at', 'updated_at']
|
||||
filters = filters or {}
|
||||
plain_fields = plain_fields or ()
|
||||
|
||||
# Special case for 'deleted' because it is a non-boolean flag
|
||||
if 'deleted' in filters:
|
||||
deleted_filter = filters.pop('deleted')
|
||||
op = 'eq' if not bool(deleted_filter) else 'neq'
|
||||
filters['deleted__%s' % op] = 0
|
||||
|
||||
plain_fields = tuple(
|
||||
(list(plain_fields) or []) +
|
||||
soft_delete_mixin_fields +
|
||||
timestamp_mixin_fields)
|
||||
join_fieldmap = join_fieldmap or {}
|
||||
|
||||
for fieldname, value in filters.items():
|
||||
for raw_fieldname, value in filters.items():
|
||||
fieldname, operator_ = self.__decompose_filter(raw_fieldname)
|
||||
if fieldname in plain_fields:
|
||||
query = self.__add_simple_filter(
|
||||
query, model, fieldname, value)
|
||||
query, model, fieldname, value, operator_)
|
||||
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)
|
||||
query, join_model, join_field, value, operator_)
|
||||
|
||||
return query
|
||||
|
||||
@@ -281,11 +296,11 @@ class Connection(api.BaseConnection):
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
join_fieldmap = JoinMap(
|
||||
goal_uuid=NaturalJoinFilter(
|
||||
join_fieldname="uuid", join_model=models.Goal),
|
||||
goal_name=NaturalJoinFilter(
|
||||
join_fieldname="name", join_model=models.Goal))
|
||||
return self._add_filters(
|
||||
query=query, model=models.Strategy, filters=filters,
|
||||
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||
@@ -296,12 +311,16 @@ class Connection(api.BaseConnection):
|
||||
|
||||
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),
|
||||
}
|
||||
join_fieldmap = JoinMap(
|
||||
goal_uuid=NaturalJoinFilter(
|
||||
join_fieldname="uuid", join_model=models.Goal),
|
||||
goal_name=NaturalJoinFilter(
|
||||
join_fieldname="name", join_model=models.Goal),
|
||||
strategy_uuid=NaturalJoinFilter(
|
||||
join_fieldname="uuid", join_model=models.Strategy),
|
||||
strategy_name=NaturalJoinFilter(
|
||||
join_fieldname="name", join_model=models.Strategy),
|
||||
)
|
||||
|
||||
return self._add_filters(
|
||||
query=query, model=models.AuditTemplate, filters=filters,
|
||||
@@ -309,74 +328,44 @@ class Connection(api.BaseConnection):
|
||||
|
||||
def _add_audits_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = []
|
||||
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:
|
||||
query = query.filter_by(state=filters['state'])
|
||||
if 'audit_template_id' in filters:
|
||||
query = query.filter_by(
|
||||
audit_template_id=filters['audit_template_id'])
|
||||
if 'audit_template_uuid' in filters:
|
||||
query = query.join(
|
||||
models.AuditTemplate,
|
||||
models.Audit.audit_template_id == models.AuditTemplate.id)
|
||||
query = query.filter(
|
||||
models.AuditTemplate.uuid == filters['audit_template_uuid'])
|
||||
if 'audit_template_name' in filters:
|
||||
query = query.join(
|
||||
models.AuditTemplate,
|
||||
models.Audit.audit_template_id == models.AuditTemplate.id)
|
||||
query = query.filter(
|
||||
models.AuditTemplate.name ==
|
||||
filters['audit_template_name'])
|
||||
plain_fields = ['uuid', 'audit_type', 'state', 'audit_template_id']
|
||||
join_fieldmap = {
|
||||
'audit_template_uuid': ("uuid", models.AuditTemplate),
|
||||
'audit_template_name': ("name", models.AuditTemplate),
|
||||
}
|
||||
|
||||
query = self.__add_soft_delete_mixin_filters(
|
||||
query, filters, models.Audit)
|
||||
query = self.__add_timestamp_mixin_filters(
|
||||
query, filters, models.Audit)
|
||||
|
||||
return query
|
||||
return self._add_filters(
|
||||
query=query, model=models.Audit, filters=filters,
|
||||
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||
|
||||
def _add_action_plans_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = []
|
||||
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:
|
||||
query = query.filter_by(audit_id=filters['audit_id'])
|
||||
if 'audit_uuid' in filters:
|
||||
query = query.join(models.Audit,
|
||||
models.ActionPlan.audit_id == models.Audit.id)
|
||||
query = query.filter(models.Audit.uuid == filters['audit_uuid'])
|
||||
plain_fields = ['uuid', 'state', 'audit_id']
|
||||
join_fieldmap = {
|
||||
'audit_uuid': ("uuid", models.Audit),
|
||||
}
|
||||
|
||||
query = self.__add_soft_delete_mixin_filters(
|
||||
query, filters, models.ActionPlan)
|
||||
query = self.__add_timestamp_mixin_filters(
|
||||
query, filters, models.ActionPlan)
|
||||
|
||||
return query
|
||||
return self._add_filters(
|
||||
query=query, model=models.ActionPlan, filters=filters,
|
||||
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||
|
||||
def _add_actions_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = []
|
||||
filters = {}
|
||||
|
||||
plain_fields = ['uuid', 'state', 'action_plan_id']
|
||||
join_fieldmap = {
|
||||
'action_plan_uuid': ("uuid", models.ActionPlan),
|
||||
}
|
||||
|
||||
query = self._add_filters(
|
||||
query=query, model=models.Action, filters=filters,
|
||||
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||
|
||||
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:
|
||||
query = query.join(
|
||||
models.ActionPlan,
|
||||
models.Action.action_plan_id == models.ActionPlan.id)
|
||||
query = query.filter(
|
||||
models.ActionPlan.uuid == filters['action_plan_uuid'])
|
||||
if 'audit_uuid' in filters:
|
||||
stmt = model_query(models.ActionPlan).join(
|
||||
models.Audit,
|
||||
@@ -384,16 +373,22 @@ class Connection(api.BaseConnection):
|
||||
.filter_by(uuid=filters['audit_uuid']).subquery()
|
||||
query = query.filter_by(action_plan_id=stmt.c.id)
|
||||
|
||||
if 'state' in filters:
|
||||
query = query.filter_by(state=filters['state'])
|
||||
|
||||
query = self.__add_soft_delete_mixin_filters(
|
||||
query, filters, models.Action)
|
||||
query = self.__add_timestamp_mixin_filters(
|
||||
query, filters, models.Action)
|
||||
|
||||
return query
|
||||
|
||||
def _add_efficacy_indicators_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = {}
|
||||
|
||||
plain_fields = ['uuid', 'name', 'unit', 'schema', 'action_plan_id']
|
||||
join_fieldmap = JoinMap(
|
||||
action_plan_uuid=NaturalJoinFilter(
|
||||
join_fieldname="uuid", join_model=models.ActionPlan),
|
||||
)
|
||||
|
||||
return self._add_filters(
|
||||
query=query, model=models.EfficacyIndicator, filters=filters,
|
||||
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
|
||||
|
||||
# ### GOALS ### #
|
||||
|
||||
def get_goal_list(self, context, filters=None, limit=None,
|
||||
@@ -688,33 +683,16 @@ class Connection(api.BaseConnection):
|
||||
message=_("Cannot overwrite UUID for an existing "
|
||||
"Audit."))
|
||||
|
||||
return self._do_update_audit(audit_id, values)
|
||||
|
||||
def _do_update_audit(self, audit_id, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.Audit, session=session)
|
||||
query = add_identity_filter(query, audit_id)
|
||||
try:
|
||||
ref = query.with_lockmode('update').one()
|
||||
except exc.NoResultFound:
|
||||
raise exception.AuditNotFound(audit=audit_id)
|
||||
|
||||
ref.update(values)
|
||||
return ref
|
||||
try:
|
||||
return self._update(models.Audit, audit_id, values)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.AuditNotFound(audit=audit_id)
|
||||
|
||||
def soft_delete_audit(self, audit_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.Audit, session=session)
|
||||
query = add_identity_filter(query, audit_id)
|
||||
|
||||
try:
|
||||
query.one()
|
||||
except exc.NoResultFound:
|
||||
raise exception.AuditNotFound(audit=audit_id)
|
||||
|
||||
query.soft_delete()
|
||||
try:
|
||||
self._soft_delete(models.Audit, audit_id)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.AuditNotFound(audit=audit_id)
|
||||
|
||||
# ### ACTIONS ### #
|
||||
|
||||
@@ -814,7 +792,7 @@ class Connection(api.BaseConnection):
|
||||
# ### ACTION PLANS ### #
|
||||
|
||||
def get_action_plan_list(
|
||||
self, context, columns=None, filters=None, limit=None,
|
||||
self, context, 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)
|
||||
@@ -923,3 +901,76 @@ class Connection(api.BaseConnection):
|
||||
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
|
||||
|
||||
query.soft_delete()
|
||||
|
||||
# ### EFFICACY INDICATORS ### #
|
||||
|
||||
def get_efficacy_indicator_list(self, context, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
|
||||
query = model_query(models.EfficacyIndicator)
|
||||
query = self._add_efficacy_indicators_filters(query, filters)
|
||||
if not context.show_deleted:
|
||||
query = query.filter_by(deleted_at=None)
|
||||
return _paginate_query(models.EfficacyIndicator, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
def create_efficacy_indicator(self, values):
|
||||
# ensure defaults are present for new efficacy indicators
|
||||
if not values.get('uuid'):
|
||||
values['uuid'] = utils.generate_uuid()
|
||||
|
||||
efficacy_indicator = models.EfficacyIndicator()
|
||||
efficacy_indicator.update(values)
|
||||
|
||||
try:
|
||||
efficacy_indicator.save()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.EfficacyIndicatorAlreadyExists(uuid=values['uuid'])
|
||||
return efficacy_indicator
|
||||
|
||||
def _get_efficacy_indicator(self, context, fieldname, value):
|
||||
try:
|
||||
return self._get(context, model=models.EfficacyIndicator,
|
||||
fieldname=fieldname, value=value)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.EfficacyIndicatorNotFound(efficacy_indicator=value)
|
||||
|
||||
def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id):
|
||||
return self._get_efficacy_indicator(
|
||||
context, fieldname="id", value=efficacy_indicator_id)
|
||||
|
||||
def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid):
|
||||
return self._get_efficacy_indicator(
|
||||
context, fieldname="uuid", value=efficacy_indicator_uuid)
|
||||
|
||||
def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name):
|
||||
return self._get_efficacy_indicator(
|
||||
context, fieldname="name", value=efficacy_indicator_name)
|
||||
|
||||
def update_efficacy_indicator(self, efficacy_indicator_id, values):
|
||||
if 'uuid' in values:
|
||||
raise exception.Invalid(
|
||||
message=_("Cannot overwrite UUID for an existing "
|
||||
"efficacy indicator."))
|
||||
|
||||
try:
|
||||
return self._update(
|
||||
models.EfficacyIndicator, efficacy_indicator_id, values)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.EfficacyIndicatorNotFound(
|
||||
efficacy_indicator=efficacy_indicator_id)
|
||||
|
||||
def soft_delete_efficacy_indicator(self, efficacy_indicator_id):
|
||||
try:
|
||||
self._soft_delete(models.EfficacyIndicator, efficacy_indicator_id)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.EfficacyIndicatorNotFound(
|
||||
efficacy_indicator=efficacy_indicator_id)
|
||||
|
||||
def destroy_efficacy_indicator(self, efficacy_indicator_id):
|
||||
try:
|
||||
return self._destroy(
|
||||
models.EfficacyIndicator, efficacy_indicator_id)
|
||||
except exception.ResourceNotFound:
|
||||
raise exception.EfficacyIndicatorNotFound(
|
||||
efficacy_indicator=efficacy_indicator_id)
|
||||
|
||||
@@ -27,6 +27,7 @@ from sqlalchemy import DateTime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import Numeric
|
||||
from sqlalchemy import schema
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
@@ -123,6 +124,7 @@ class Strategy(Base):
|
||||
name = Column(String(63), nullable=False)
|
||||
display_name = Column(String(63), nullable=False)
|
||||
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
|
||||
parameters_spec = Column(JSONEncodedDict, nullable=True)
|
||||
|
||||
|
||||
class Goal(Base):
|
||||
@@ -137,6 +139,7 @@ class Goal(Base):
|
||||
uuid = Column(String(36))
|
||||
name = Column(String(63), nullable=False)
|
||||
display_name = Column(String(63), nullable=False)
|
||||
efficacy_specification = Column(JSONEncodedList, nullable=False)
|
||||
|
||||
|
||||
class AuditTemplate(Base):
|
||||
@@ -168,11 +171,13 @@ class Audit(Base):
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36))
|
||||
type = Column(String(20))
|
||||
audit_type = Column(String(20))
|
||||
state = Column(String(20), nullable=True)
|
||||
deadline = Column(DateTime, nullable=True)
|
||||
audit_template_id = Column(Integer, ForeignKey('audit_templates.id'),
|
||||
nullable=False)
|
||||
parameters = Column(JSONEncodedDict, nullable=True)
|
||||
interval = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class Action(Base):
|
||||
@@ -205,6 +210,24 @@ class ActionPlan(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36))
|
||||
first_action_id = Column(Integer)
|
||||
audit_id = Column(Integer, ForeignKey('audits.id'),
|
||||
nullable=True)
|
||||
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=True)
|
||||
state = Column(String(20), nullable=True)
|
||||
global_efficacy = Column(JSONEncodedDict, nullable=True)
|
||||
|
||||
|
||||
class EfficacyIndicator(Base):
|
||||
"""Represents an efficacy indicator."""
|
||||
|
||||
__tablename__ = 'efficacy_indicators'
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('uuid', name='uniq_efficacy_indicators0uuid'),
|
||||
table_args()
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36))
|
||||
name = Column(String(63))
|
||||
description = Column(String(255), nullable=True)
|
||||
unit = Column(String(63), nullable=True)
|
||||
value = Column(Numeric())
|
||||
action_plan_id = Column(Integer, ForeignKey('action_plans.id'),
|
||||
nullable=False)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
# Alexander Chadin <a.chadin@servionica.ru>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +20,92 @@
|
||||
import abc
|
||||
import six
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.messaging.events import event as watcher_event
|
||||
from watcher.decision_engine.messaging import events as de_events
|
||||
from watcher.decision_engine.planner import manager as planner_manager
|
||||
from watcher.decision_engine.strategy.context import default as default_context
|
||||
from watcher.objects import audit as audit_objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAuditHandler(object):
|
||||
@abc.abstractmethod
|
||||
def execute(self, audit_uuid, request_context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def pre_execute(self, audit_uuid, request_context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def do_execute(self, audit, request_context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def post_execute(self, audit, solution, request_context):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AuditHandler(BaseAuditHandler):
|
||||
def __init__(self, messaging):
|
||||
self._messaging = messaging
|
||||
self._strategy_context = default_context.DefaultStrategyContext()
|
||||
self._planner_manager = planner_manager.PlannerManager()
|
||||
self._planner = None
|
||||
|
||||
@property
|
||||
def planner(self):
|
||||
if self._planner is None:
|
||||
self._planner = self._planner_manager.load()
|
||||
return self._planner
|
||||
|
||||
@property
|
||||
def messaging(self):
|
||||
return self._messaging
|
||||
|
||||
@property
|
||||
def strategy_context(self):
|
||||
return self._strategy_context
|
||||
|
||||
def notify(self, audit_uuid, event_type, status):
|
||||
event = watcher_event.Event()
|
||||
event.type = event_type
|
||||
event.data = {}
|
||||
payload = {'audit_uuid': audit_uuid,
|
||||
'audit_status': status}
|
||||
self.messaging.status_topic_handler.publish_event(
|
||||
event.type.name, payload)
|
||||
|
||||
def update_audit_state(self, request_context, audit, state):
|
||||
LOG.debug("Update audit state: %s", state)
|
||||
audit.state = state
|
||||
audit.save()
|
||||
self.notify(audit.uuid, de_events.Events.TRIGGER_AUDIT, state)
|
||||
|
||||
def pre_execute(self, audit, request_context):
|
||||
LOG.debug("Trigger audit %s", audit.uuid)
|
||||
# change state of the audit to ONGOING
|
||||
self.update_audit_state(request_context, audit,
|
||||
audit_objects.State.ONGOING)
|
||||
|
||||
def post_execute(self, audit, solution, request_context):
|
||||
self.planner.schedule(request_context, audit.id, solution)
|
||||
|
||||
# change state of the audit to SUCCEEDED
|
||||
self.update_audit_state(request_context, audit,
|
||||
audit_objects.State.SUCCEEDED)
|
||||
|
||||
def execute(self, audit, request_context):
|
||||
try:
|
||||
self.pre_execute(audit, request_context)
|
||||
solution = self.do_execute(audit, request_context)
|
||||
self.post_execute(audit, solution, request_context)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
self.update_audit_state(request_context, audit,
|
||||
audit_objects.State.FAILED)
|
||||
|
||||
126
watcher/decision_engine/audit/continuous.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 Servionica LTD
|
||||
#
|
||||
# Authors: Alexander Chadin <a.chadin@servionica.ru>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
from apscheduler.schedulers import background
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.common import context
|
||||
from watcher.decision_engine.audit import base
|
||||
from watcher.objects import action_plan as action_objects
|
||||
from watcher.objects import audit as audit_objects
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
WATCHER_CONTINUOUS_OPTS = [
|
||||
cfg.IntOpt('continuous_audit_interval',
|
||||
default=10,
|
||||
help='Interval, in seconds, for checking new created'
|
||||
'continuous audit.')
|
||||
]
|
||||
|
||||
CONF.register_opts(WATCHER_CONTINUOUS_OPTS, 'watcher_decision_engine')
|
||||
|
||||
|
||||
class ContinuousAuditHandler(base.AuditHandler):
|
||||
def __init__(self, messaging):
|
||||
super(ContinuousAuditHandler, self).__init__(messaging)
|
||||
self._scheduler = None
|
||||
self.jobs = []
|
||||
self._start()
|
||||
self.context_show_deleted = context.RequestContext(is_admin=True,
|
||||
show_deleted=True)
|
||||
|
||||
@property
|
||||
def scheduler(self):
|
||||
if self._scheduler is None:
|
||||
self._scheduler = background.BackgroundScheduler()
|
||||
return self._scheduler
|
||||
|
||||
def _is_audit_inactive(self, audit):
|
||||
audit = audit_objects.Audit.get_by_uuid(self.context_show_deleted,
|
||||
audit.uuid)
|
||||
if audit.state in (audit_objects.State.CANCELLED,
|
||||
audit_objects.State.DELETED,
|
||||
audit_objects.State.FAILED):
|
||||
# if audit isn't in active states, audit's job must be removed to
|
||||
# prevent using of inactive audit in future.
|
||||
job_to_delete = [job for job in self.jobs
|
||||
if job.keys()[0] == audit.uuid][0]
|
||||
self.jobs.remove(job_to_delete)
|
||||
job_to_delete[audit.uuid].remove()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def do_execute(self, audit, request_context):
|
||||
# execute the strategy
|
||||
solution = self.strategy_context.execute_strategy(audit.uuid,
|
||||
request_context)
|
||||
|
||||
if audit.audit_type == audit_objects.AuditType.CONTINUOUS.value:
|
||||
a_plan_filters = {'audit_uuid': audit.uuid,
|
||||
'state': action_objects.State.RECOMMENDED}
|
||||
action_plans = action_objects.ActionPlan.list(
|
||||
request_context,
|
||||
filters=a_plan_filters)
|
||||
for plan in action_plans:
|
||||
plan.state = action_objects.State.CANCELLED
|
||||
plan.save()
|
||||
return solution
|
||||
|
||||
def execute_audit(self, audit, request_context):
|
||||
if not self._is_audit_inactive(audit):
|
||||
self.execute(audit, request_context)
|
||||
|
||||
def post_execute(self, audit, solution, request_context):
|
||||
self.planner.schedule(request_context, audit.id, solution)
|
||||
|
||||
def launch_audits_periodically(self):
|
||||
audit_context = context.RequestContext(is_admin=True)
|
||||
audit_filters = {
|
||||
'audit_type': audit_objects.AuditType.CONTINUOUS.value,
|
||||
'state__in': (audit_objects.State.PENDING,
|
||||
audit_objects.State.ONGOING,
|
||||
audit_objects.State.SUCCEEDED)
|
||||
}
|
||||
audits = audit_objects.Audit.list(audit_context,
|
||||
filters=audit_filters)
|
||||
scheduler_job_args = [job.args for job in self.scheduler.get_jobs()
|
||||
if job.name == 'execute_audit']
|
||||
for audit in audits:
|
||||
if audit.uuid not in [arg[0].uuid for arg in scheduler_job_args]:
|
||||
job = self.scheduler.add_job(
|
||||
self.execute_audit, 'interval',
|
||||
args=[audit, audit_context],
|
||||
seconds=audit.interval,
|
||||
name='execute_audit',
|
||||
next_run_time=datetime.datetime.now())
|
||||
self.jobs.append({audit.uuid: job})
|
||||
|
||||
def _start(self):
|
||||
self.scheduler.add_job(
|
||||
self.launch_audits_periodically,
|
||||
'interval',
|
||||
seconds=CONF.watcher_decision_engine.continuous_audit_interval,
|
||||
next_run_time=datetime.datetime.now())
|
||||
self.scheduler.start()
|
||||
@@ -1,87 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.messaging.events import event as watcher_event
|
||||
from watcher.decision_engine.audit import base
|
||||
from watcher.decision_engine.messaging import events as de_events
|
||||
from watcher.decision_engine.planner import manager as planner_manager
|
||||
from watcher.decision_engine.strategy.context import default as default_context
|
||||
from watcher.objects import audit as audit_objects
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultAuditHandler(base.BaseAuditHandler):
|
||||
def __init__(self, messaging):
|
||||
super(DefaultAuditHandler, self).__init__()
|
||||
self._messaging = messaging
|
||||
self._strategy_context = default_context.DefaultStrategyContext()
|
||||
self._planner_manager = planner_manager.PlannerManager()
|
||||
self._planner = None
|
||||
|
||||
@property
|
||||
def planner(self):
|
||||
if self._planner is None:
|
||||
self._planner = self._planner_manager.load()
|
||||
return self._planner
|
||||
|
||||
@property
|
||||
def messaging(self):
|
||||
return self._messaging
|
||||
|
||||
@property
|
||||
def strategy_context(self):
|
||||
return self._strategy_context
|
||||
|
||||
def notify(self, audit_uuid, event_type, status):
|
||||
event = watcher_event.Event()
|
||||
event.type = event_type
|
||||
event.data = {}
|
||||
payload = {'audit_uuid': audit_uuid,
|
||||
'audit_status': status}
|
||||
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)
|
||||
audit = audit_objects.Audit.get_by_uuid(request_context, audit_uuid)
|
||||
audit.state = state
|
||||
audit.save()
|
||||
self.notify(audit_uuid, de_events.Events.TRIGGER_AUDIT, state)
|
||||
return audit
|
||||
|
||||
def execute(self, audit_uuid, request_context):
|
||||
try:
|
||||
LOG.debug("Trigger audit %s", audit_uuid)
|
||||
# change state of the audit to ONGOING
|
||||
audit = self.update_audit_state(request_context, audit_uuid,
|
||||
audit_objects.State.ONGOING)
|
||||
|
||||
# execute the strategy
|
||||
solution = self.strategy_context.execute_strategy(audit_uuid,
|
||||
request_context)
|
||||
|
||||
self.planner.schedule(request_context, audit.id, solution)
|
||||
|
||||
# change state of the audit to SUCCEEDED
|
||||
self.update_audit_state(request_context, audit_uuid,
|
||||
audit_objects.State.SUCCEEDED)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
self.update_audit_state(request_context, audit_uuid,
|
||||
audit_objects.State.FAILED)
|
||||
@@ -13,14 +13,14 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from watcher.common.loader import default
|
||||
from watcher.decision_engine.audit import base
|
||||
|
||||
|
||||
class DefaultPlannerLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultPlannerLoader, self).__init__(
|
||||
namespace='watcher_planners')
|
||||
class OneShotAuditHandler(base.AuditHandler):
|
||||
def do_execute(self, audit, request_context):
|
||||
# execute the strategy
|
||||
solution = self.strategy_context.execute_strategy(audit.uuid,
|
||||
request_context)
|
||||
|
||||
return solution
|
||||
@@ -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,13 +13,14 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from watcher.decision_engine.goal import goals
|
||||
|
||||
from watcher.common.loader import default
|
||||
Dummy = goals.Dummy
|
||||
ServerConsolidation = goals.ServerConsolidation
|
||||
ThermalOptimization = goals.ThermalOptimization
|
||||
Unclassified = goals.Unclassified
|
||||
WorkloadBalancing = goals.WorkloadBalancing
|
||||
|
||||
|
||||
class DefaultActionLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultActionLoader, self).__init__(namespace='watcher_actions')
|
||||
__all__ = ("Dummy", "ServerConsolidation", "ThermalOptimization",
|
||||
"Unclassified", "WorkloadBalancing", )
|
||||
68
watcher/decision_engine/goal/base.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.loader import loadable
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Goal(loadable.Loadable):
|
||||
|
||||
def __init__(self, config):
|
||||
super(Goal, self).__init__(config)
|
||||
self.name = self.get_name()
|
||||
self.display_name = self.get_display_name()
|
||||
self.efficacy_specification = self.get_efficacy_specification()
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_name(cls):
|
||||
"""Name of the goal: should be identical to the related entry point"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_display_name(cls):
|
||||
"""The goal display name for the goal"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_translatable_display_name(cls):
|
||||
"""The translatable msgid of the goal"""
|
||||
# Note(v-francoise): Defined here to be used as the translation key for
|
||||
# other services
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
"""Defines the configuration options to be associated to this loadable
|
||||
|
||||
:return: A list of configuration options relative to this Loadable
|
||||
:rtype: list of :class:`oslo_config.cfg.Opt` instances
|
||||
"""
|
||||
return []
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
raise NotImplementedError()
|
||||
84
watcher/decision_engine/goal/efficacy/base.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- 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.
|
||||
|
||||
"""
|
||||
An efficacy specfication is a contract that is associated to each :ref:`Goal
|
||||
<goal_definition>` that defines the various :ref:`efficacy indicators
|
||||
<efficacy_indicator_definition>` a strategy achieving the associated goal
|
||||
should provide within its :ref:`solution <solution_definition>`. Indeed, each
|
||||
solution proposed by a strategy will be validated against this contract before
|
||||
calculating its :ref:`global efficacy <efficacy_definition>`.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import json
|
||||
|
||||
import six
|
||||
import voluptuous
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class EfficacySpecification(object):
|
||||
|
||||
def __init__(self):
|
||||
self._indicators_specs = self.get_indicators_specifications()
|
||||
|
||||
@property
|
||||
def indicators_specs(self):
|
||||
return self._indicators_specs
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_indicators_specifications(self):
|
||||
"""List the specifications of the indicator for this efficacy spec
|
||||
|
||||
:return: Tuple of indicator specifications
|
||||
:rtype: Tuple of :py:class:`~.IndicatorSpecification` instances
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_global_efficacy_indicator(self, indicators_map):
|
||||
"""Compute the global efficacy for the goal it achieves
|
||||
|
||||
:param indicators_map: dict-like object containing the
|
||||
efficacy indicators related to this spec
|
||||
:type indicators_map: :py:class:`~.IndicatorsMap` instance
|
||||
:raises: NotImplementedError
|
||||
:returns: :py:class:`~.Indicator` instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
"""Combined schema from the schema of the indicators"""
|
||||
schema = voluptuous.Schema({}, required=True)
|
||||
for indicator in self.indicators_specs:
|
||||
key_constraint = (voluptuous.Required
|
||||
if indicator.required else voluptuous.Optional)
|
||||
schema = schema.extend(
|
||||
{key_constraint(indicator.name): indicator.schema.schema})
|
||||
|
||||
return schema
|
||||
|
||||
def validate_efficacy_indicators(self, indicators_map):
|
||||
return self.schema(indicators_map)
|
||||
|
||||
def get_indicators_specs_dicts(self):
|
||||
return [indicator.to_dict()
|
||||
for indicator in self.indicators_specs]
|
||||
|
||||
def serialize_indicators_specs(self):
|
||||
return json.dumps(self.get_indicators_specs_dicts())
|
||||
132
watcher/decision_engine/goal/efficacy/indicators.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
from oslo_log import log
|
||||
import voluptuous
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IndicatorSpecification(object):
|
||||
|
||||
def __init__(self, name=None, description=None, unit=None, required=True):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
self.required = required
|
||||
|
||||
@abc.abstractproperty
|
||||
def schema(self):
|
||||
"""Schema used to validate the indicator value
|
||||
|
||||
:return: A Voplutuous Schema
|
||||
:rtype: :py:class:`.voluptuous.Schema` instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def validate(cls, solution):
|
||||
"""Validate the given solution
|
||||
|
||||
:raises: :py:class:`~.InvalidIndicatorValue` when the validation fails
|
||||
"""
|
||||
indicator = cls()
|
||||
value = None
|
||||
try:
|
||||
value = getattr(solution, indicator.name)
|
||||
indicator.schema(value)
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise exception.InvalidIndicatorValue(
|
||||
name=indicator.name, value=value, spec_type=type(indicator))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"unit": self.unit,
|
||||
"schema": str(self.schema.schema) if self.schema else None,
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_dict())
|
||||
|
||||
|
||||
class AverageCpuLoad(IndicatorSpecification):
|
||||
|
||||
def __init__(self):
|
||||
super(AverageCpuLoad, self).__init__(
|
||||
name="avg_cpu_percent",
|
||||
description=_("Average CPU load as a percentage of the CPU time."),
|
||||
unit="%",
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema(
|
||||
voluptuous.Range(min=0, max=100), required=True)
|
||||
|
||||
|
||||
class MigrationEfficacy(IndicatorSpecification):
|
||||
|
||||
def __init__(self):
|
||||
super(MigrationEfficacy, self).__init__(
|
||||
name="migration_efficacy",
|
||||
description=_("Represents the percentage of released nodes out of "
|
||||
"the total number of migrations."),
|
||||
unit="%",
|
||||
required=True
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema(
|
||||
voluptuous.Range(min=0, max=100), required=True)
|
||||
|
||||
|
||||
class ReleasedComputeNodesCount(IndicatorSpecification):
|
||||
def __init__(self):
|
||||
super(ReleasedComputeNodesCount, self).__init__(
|
||||
name="released_compute_nodes_count",
|
||||
description=_("The number of compute nodes to be released."),
|
||||
unit=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema(
|
||||
voluptuous.Range(min=0), required=True)
|
||||
|
||||
|
||||
class VmMigrationsCount(IndicatorSpecification):
|
||||
def __init__(self):
|
||||
super(VmMigrationsCount, self).__init__(
|
||||
name="vm_migrations_count",
|
||||
description=_("The number of migrations to be performed."),
|
||||
unit=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema(
|
||||
voluptuous.Range(min=0), required=True)
|
||||
52
watcher/decision_engine/goal/efficacy/specs.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.decision_engine.goal.efficacy import base
|
||||
from watcher.decision_engine.goal.efficacy import indicators
|
||||
from watcher.decision_engine.solution import efficacy
|
||||
|
||||
|
||||
class Unclassified(base.EfficacySpecification):
|
||||
|
||||
def get_indicators_specifications(self):
|
||||
return ()
|
||||
|
||||
def get_global_efficacy_indicator(self, indicators_map):
|
||||
return None
|
||||
|
||||
|
||||
class ServerConsolidation(base.EfficacySpecification):
|
||||
|
||||
def get_indicators_specifications(self):
|
||||
return [
|
||||
indicators.ReleasedComputeNodesCount(),
|
||||
indicators.VmMigrationsCount(),
|
||||
]
|
||||
|
||||
def get_global_efficacy_indicator(self, indicators_map):
|
||||
value = 0
|
||||
if indicators_map.vm_migrations_count > 0:
|
||||
value = (float(indicators_map.released_compute_nodes_count) /
|
||||
float(indicators_map.vm_migrations_count)) * 100
|
||||
|
||||
return efficacy.Indicator(
|
||||
name="released_nodes_ratio",
|
||||
description=_("Ratio of released compute nodes divided by the "
|
||||
"number of VM migrations."),
|
||||
unit='%',
|
||||
value=value,
|
||||
)
|
||||
164
watcher/decision_engine/goal/goals.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.decision_engine.goal import base
|
||||
from watcher.decision_engine.goal.efficacy import specs
|
||||
|
||||
|
||||
class Dummy(base.Goal):
|
||||
"""Dummy
|
||||
|
||||
Reserved goal that is used for testing purposes.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "dummy"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Dummy goal")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Dummy goal"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
return specs.Unclassified()
|
||||
|
||||
|
||||
class Unclassified(base.Goal):
|
||||
"""Unclassified
|
||||
|
||||
This goal is used to ease the development process of a strategy. Containing
|
||||
no actual indicator specification, this goal can be used whenever a
|
||||
strategy has yet to be formally associated with an existing goal. If the
|
||||
goal achieve has been identified but there is no available implementation,
|
||||
this Goal can also be used as a transitional stage.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "unclassified"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Unclassified")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Unclassified"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
return specs.Unclassified()
|
||||
|
||||
|
||||
class ServerConsolidation(base.Goal):
|
||||
"""ServerConsolidation
|
||||
|
||||
This goal is for efficient usage of compute server resources in order to
|
||||
reduce the total number of servers.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "server_consolidation"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Server consolidation")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Server consolidation"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
return specs.ServerConsolidation()
|
||||
|
||||
|
||||
class ThermalOptimization(base.Goal):
|
||||
"""ThermalOptimization
|
||||
|
||||
This goal is used to balance the temperature across different servers.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "thermal_optimization"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Thermal optimization")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Thermal optimization"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
return specs.Unclassified()
|
||||
|
||||
|
||||
class WorkloadBalancing(base.Goal):
|
||||
"""WorkloadBalancing
|
||||
|
||||
This goal is used to evenly distribute workloads across different servers.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "workload_balancing"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Workload balancing")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Workload balancing"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
return specs.Unclassified()
|
||||
|
||||
|
||||
class AirflowOptimization(base.Goal):
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "airflow_optimization"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Airflow optimization")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Airflow optimization"
|
||||
|
||||
@classmethod
|
||||
def get_efficacy_specification(cls):
|
||||
"""The efficacy spec for the current goal"""
|
||||
return specs.Unclassified()
|
||||
0
watcher/decision_engine/loading/__init__.py
Normal file
@@ -27,3 +27,15 @@ class DefaultStrategyLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultStrategyLoader, self).__init__(
|
||||
namespace='watcher_strategies')
|
||||
|
||||
|
||||
class DefaultGoalLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultGoalLoader, self).__init__(
|
||||
namespace='watcher_goals')
|
||||
|
||||
|
||||
class DefaultPlannerLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultPlannerLoader, self).__init__(
|
||||
namespace='watcher_planners')
|
||||
@@ -21,7 +21,9 @@ from concurrent import futures
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.decision_engine.audit import default
|
||||
from watcher.decision_engine.audit import continuous as continuous_handler
|
||||
from watcher.decision_engine.audit import oneshot as oneshot_handler
|
||||
from watcher.objects import audit as audit_objects
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
@@ -32,6 +34,10 @@ class AuditEndpoint(object):
|
||||
self._messaging = messaging
|
||||
self._executor = futures.ThreadPoolExecutor(
|
||||
max_workers=CONF.watcher_decision_engine.max_workers)
|
||||
self._oneshot_handler = oneshot_handler.OneShotAuditHandler(
|
||||
self.messaging)
|
||||
self._continuous_handler = continuous_handler.ContinuousAuditHandler(
|
||||
self.messaging)
|
||||
|
||||
@property
|
||||
def executor(self):
|
||||
@@ -42,8 +48,8 @@ class AuditEndpoint(object):
|
||||
return self._messaging
|
||||
|
||||
def do_trigger_audit(self, context, audit_uuid):
|
||||
audit = default.DefaultAuditHandler(self.messaging)
|
||||
audit.execute(audit_uuid, context)
|
||||
audit = audit_objects.Audit.get_by_uuid(context, audit_uuid)
|
||||
self._oneshot_handler.execute(audit, context)
|
||||
|
||||
def trigger_audit(self, context, audit_uuid):
|
||||
LOG.debug("Trigger audit %s" % audit_uuid)
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from enum import Enum
|
||||
import enum
|
||||
|
||||
|
||||
class Events(Enum):
|
||||
class Events(enum.Enum):
|
||||
ALL = '*',
|
||||
ACTION_PLAN = "action_plan"
|
||||
TRIGGER_AUDIT = "trigger_audit"
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from watcher.decision_engine.model.compute_resource import ComputeResource
|
||||
from watcher.decision_engine.model.hypervisor_state import HypervisorState
|
||||
from watcher.decision_engine.model.power_state import PowerState
|
||||
from watcher.decision_engine.model import compute_resource
|
||||
from watcher.decision_engine.model import hypervisor_state
|
||||
from watcher.decision_engine.model import power_state
|
||||
|
||||
|
||||
class Hypervisor(ComputeResource):
|
||||
class Hypervisor(compute_resource.ComputeResource):
|
||||
def __init__(self):
|
||||
super(Hypervisor, self).__init__()
|
||||
self._state = HypervisorState.ONLINE
|
||||
self._status = HypervisorState.ENABLED
|
||||
self._power_state = PowerState.g0
|
||||
self._state = hypervisor_state.HypervisorState.ONLINE
|
||||
self._status = hypervisor_state.HypervisorState.ENABLED
|
||||
self._power_state = power_state.PowerState.g0
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from enum import Enum
|
||||
import enum
|
||||
|
||||
|
||||
class HypervisorState(Enum):
|
||||
class HypervisorState(enum.Enum):
|
||||
ONLINE = 'up'
|
||||
OFFLINE = 'down'
|
||||
ENABLED = 'enabled'
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from oslo_log import log
|
||||
from threading import Lock
|
||||
import threading
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
@@ -24,7 +24,7 @@ class Mapping(object):
|
||||
self.model = model
|
||||
self._mapping_hypervisors = {}
|
||||
self.mapping_vm = {}
|
||||
self.lock = Lock()
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def map(self, hypervisor, vm):
|
||||
"""Select the hypervisor where the instance are launched
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from enum import Enum
|
||||
import enum
|
||||
|
||||
|
||||
class PowerState(Enum):
|
||||
class PowerState(enum.Enum):
|
||||
# away mode
|
||||
g0 = "g0"
|
||||
# power on suspend (processor caches are flushed)
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from enum import Enum
|
||||
import enum
|
||||
|
||||
|
||||
class ResourceType(Enum):
|
||||
class ResourceType(enum.Enum):
|
||||
cpu_cores = 'num_cores'
|
||||
memory = 'memory'
|
||||
disk = 'disk'
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from watcher.decision_engine.model.compute_resource import ComputeResource
|
||||
from watcher.decision_engine.model.vm_state import VMState
|
||||
from watcher.decision_engine.model import compute_resource
|
||||
from watcher.decision_engine.model import vm_state
|
||||
|
||||
|
||||
class VM(ComputeResource):
|
||||
class VM(compute_resource.ComputeResource):
|
||||
def __init__(self):
|
||||
super(VM, self).__init__()
|
||||
self._state = VMState.ACTIVE.value
|
||||
self._state = vm_state.VMState.ACTIVE.value
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from enum import Enum
|
||||
import enum
|
||||
|
||||
|
||||
class VMState(Enum):
|
||||
class VMState(enum.Enum):
|
||||
ACTIVE = 'active' # VM is running
|
||||
BUILDING = 'building' # VM only exists in DB
|
||||
PAUSED = 'paused'
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _LW
|
||||
@@ -30,18 +31,29 @@ LOG = log.getLogger(__name__)
|
||||
class DefaultPlanner(base.BasePlanner):
|
||||
"""Default planner implementation
|
||||
|
||||
This implementation comes with basic rules with a fixed set of action types
|
||||
that are weighted. An action having a lower weight will be scheduled before
|
||||
the other ones.
|
||||
This implementation comes with basic rules with a set of action types that
|
||||
are weighted. An action having a lower weight will be scheduled before the
|
||||
other ones. The set of action types can be specified by 'weights' in the
|
||||
``watcher.conf``. You need to associate a different weight to all available
|
||||
actions into the configuration file, otherwise you will get an error when
|
||||
the new action will be referenced in the solution produced by a strategy.
|
||||
"""
|
||||
|
||||
priorities = {
|
||||
weights_dict = {
|
||||
'nop': 0,
|
||||
'sleep': 1,
|
||||
'change_nova_service_state': 2,
|
||||
'migrate': 3,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
return [cfg.DictOpt(
|
||||
'weights',
|
||||
help="These weights are used to schedule the actions",
|
||||
default=cls.weights_dict),
|
||||
]
|
||||
|
||||
def create_action(self,
|
||||
action_plan_id,
|
||||
action_type,
|
||||
@@ -59,19 +71,22 @@ class DefaultPlanner(base.BasePlanner):
|
||||
|
||||
def schedule(self, context, audit_id, solution):
|
||||
LOG.debug('Create an action plan for the audit uuid: %s ', audit_id)
|
||||
action_plan = self._create_action_plan(context, audit_id)
|
||||
priorities = self.config.weights
|
||||
action_plan = self._create_action_plan(context, audit_id, solution)
|
||||
|
||||
actions = list(solution.actions)
|
||||
to_schedule = []
|
||||
for action in actions:
|
||||
json_action = self.create_action(action_plan_id=action_plan.id,
|
||||
action_type=action.get(
|
||||
'action_type'),
|
||||
input_parameters=action.get(
|
||||
'input_parameters'))
|
||||
to_schedule.append((self.priorities[action.get('action_type')],
|
||||
json_action = self.create_action(
|
||||
action_plan_id=action_plan.id,
|
||||
action_type=action.get('action_type'),
|
||||
input_parameters=action.get('input_parameters'))
|
||||
to_schedule.append((priorities[action.get('action_type')],
|
||||
json_action))
|
||||
|
||||
self._create_efficacy_indicators(
|
||||
context, action_plan.id, solution.efficacy_indicators)
|
||||
|
||||
# scheduling
|
||||
scheduled = sorted(to_schedule, key=lambda x: (x[0]))
|
||||
if len(scheduled) == 0:
|
||||
@@ -96,19 +111,38 @@ class DefaultPlanner(base.BasePlanner):
|
||||
|
||||
return action_plan
|
||||
|
||||
def _create_action_plan(self, context, audit_id):
|
||||
def _create_action_plan(self, context, audit_id, solution):
|
||||
action_plan_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'audit_id': audit_id,
|
||||
'first_action_id': None,
|
||||
'state': objects.action_plan.State.RECOMMENDED
|
||||
'state': objects.action_plan.State.RECOMMENDED,
|
||||
'global_efficacy': solution.global_efficacy,
|
||||
}
|
||||
|
||||
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||
new_action_plan.create(context)
|
||||
new_action_plan.save()
|
||||
|
||||
return new_action_plan
|
||||
|
||||
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
||||
efficacy_indicators = []
|
||||
for indicator in indicators:
|
||||
efficacy_indicator_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': indicator.name,
|
||||
'description': indicator.description,
|
||||
'unit': indicator.unit,
|
||||
'value': indicator.value,
|
||||
'action_plan_id': action_plan_id,
|
||||
}
|
||||
new_efficacy_indicator = objects.EfficacyIndicator(
|
||||
context, **efficacy_indicator_dict)
|
||||
new_efficacy_indicator.create(context)
|
||||
|
||||
efficacy_indicators.append(new_efficacy_indicator)
|
||||
return efficacy_indicators
|
||||
|
||||
def _create_action(self, context, _action, parent_action):
|
||||
try:
|
||||
LOG.debug("Creating the %s in watcher db",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.decision_engine.planner.loading import default as loader
|
||||
from watcher.decision_engine.loading import default as loader
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
@@ -18,10 +18,17 @@
|
||||
#
|
||||
|
||||
"""
|
||||
A :ref:`Solution <solution_definition>` is a set of
|
||||
:ref:`Actions <action_definition>` generated by a
|
||||
:ref:`Strategy <strategy_definition>` (i.e., an algorithm) in order to achieve
|
||||
the :ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
|
||||
A :ref:`Solution <solution_definition>` is the result of execution of a
|
||||
:ref:`strategy <strategy_definition>` (i.e., an algorithm).
|
||||
Each solution is composed of many pieces of information:
|
||||
|
||||
- A set of :ref:`actions <action_definition>` generated by the strategy in
|
||||
order to achieve the :ref:`goal <goal_definition>` of an associated
|
||||
:ref:`audit <audit_definition>`.
|
||||
- A set of :ref:`efficacy indicators <efficacy_indicator_definition>` as
|
||||
defined by the associated goal
|
||||
- A :ref:`global efficacy <efficacy_definition>` which is computed by the
|
||||
associated goal using the aforementioned efficacy indicators.
|
||||
|
||||
A :ref:`Solution <solution_definition>` is different from an
|
||||
:ref:`Action Plan <action_plan_definition>` because it contains the
|
||||
@@ -37,59 +44,62 @@ applied.
|
||||
|
||||
Two approaches to dealing with this can be envisaged:
|
||||
|
||||
- **fully automated mode**: only the :ref:`Solution <solution_definition>`
|
||||
with the highest ranking (i.e., the highest
|
||||
:ref:`Optimization Efficiency <efficiency_definition>`)
|
||||
will be sent to the :ref:`Watcher Planner <watcher_planner_definition>` and
|
||||
translated into concrete :ref:`Actions <action_definition>`.
|
||||
- **manual mode**: several :ref:`Solutions <solution_definition>` are proposed
|
||||
to the :ref:`Administrator <administrator_definition>` with a detailed
|
||||
measurement of the estimated
|
||||
:ref:`Optimization Efficiency <efficiency_definition>` and he/she decides
|
||||
which one will be launched.
|
||||
- **fully automated mode**: only the :ref:`Solution <solution_definition>`
|
||||
with the highest ranking (i.e., the highest
|
||||
:ref:`Optimization Efficacy <efficacy_definition>`) will be sent to the
|
||||
:ref:`Watcher Planner <watcher_planner_definition>` and translated into
|
||||
concrete :ref:`Actions <action_definition>`.
|
||||
- **manual mode**: several :ref:`Solutions <solution_definition>` are proposed
|
||||
to the :ref:`Administrator <administrator_definition>` with a detailed
|
||||
measurement of the estimated :ref:`Optimization Efficacy
|
||||
<efficacy_definition>` and he/she decides which one will be launched.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
from watcher.decision_engine.solution import efficacy
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseSolution(object):
|
||||
def __init__(self):
|
||||
self._origin = None
|
||||
self._model = None
|
||||
self._efficacy = 0
|
||||
def __init__(self, goal, strategy):
|
||||
"""Base Solution constructor
|
||||
|
||||
:param goal: Goal associated to this solution
|
||||
:type goal: :py:class:`~.base.Goal` instance
|
||||
:param strategy: Strategy associated to this solution
|
||||
:type strategy: :py:class:`~.BaseStrategy` instance
|
||||
"""
|
||||
self.goal = goal
|
||||
self.strategy = strategy
|
||||
self.origin = None
|
||||
self.model = None
|
||||
self.efficacy = efficacy.Efficacy(self.goal, self.strategy)
|
||||
|
||||
@property
|
||||
def efficacy(self):
|
||||
return self._efficacy
|
||||
|
||||
@efficacy.setter
|
||||
def efficacy(self, e):
|
||||
self._efficacy = e
|
||||
def global_efficacy(self):
|
||||
return self.efficacy.global_efficacy
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self._model
|
||||
def efficacy_indicators(self):
|
||||
return self.efficacy.indicators
|
||||
|
||||
@model.setter
|
||||
def model(self, m):
|
||||
self._model = m
|
||||
def compute_global_efficacy(self):
|
||||
"""Compute the global efficacy given a map of efficacy indicators"""
|
||||
self.efficacy.compute_global_efficacy()
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self._origin
|
||||
def set_efficacy_indicators(self, **indicators_map):
|
||||
"""Set the efficacy indicators mapping (no validation)
|
||||
|
||||
@origin.setter
|
||||
def origin(self, m):
|
||||
self._origin = m
|
||||
:param indicators_map: mapping between the indicator name and its value
|
||||
:type indicators_map: dict {`str`: `object`}
|
||||
"""
|
||||
self.efficacy.set_efficacy_indicators(**indicators_map)
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_action(self,
|
||||
action_type,
|
||||
resource_id,
|
||||
input_parameters=None):
|
||||
"""Add a new Action in the Action Plan
|
||||
def add_action(self, action_type, resource_id, input_parameters=None):
|
||||
"""Add a new Action in the Solution
|
||||
|
||||
:param action_type: the unique id of an action type defined in
|
||||
entry point 'watcher_actions'
|
||||
|
||||
@@ -22,19 +22,21 @@ from watcher.decision_engine.solution import base
|
||||
|
||||
|
||||
class DefaultSolution(base.BaseSolution):
|
||||
def __init__(self):
|
||||
def __init__(self, goal, strategy):
|
||||
"""Stores a set of actions generated by a strategy
|
||||
|
||||
The DefaultSolution class store a set of actions generated by a
|
||||
strategy in order to achieve the goal.
|
||||
|
||||
:param goal: Goal associated to this solution
|
||||
:type goal: :py:class:`~.base.Goal` instance
|
||||
:param strategy: Strategy associated to this solution
|
||||
:type strategy: :py:class:`~.BaseStrategy` instance
|
||||
"""
|
||||
super(DefaultSolution, self).__init__()
|
||||
super(DefaultSolution, self).__init__(goal, strategy)
|
||||
self._actions = []
|
||||
|
||||
def add_action(self, action_type,
|
||||
input_parameters=None,
|
||||
resource_id=None):
|
||||
|
||||
def add_action(self, action_type, input_parameters=None, resource_id=None):
|
||||
if input_parameters is not None:
|
||||
if baction.BaseAction.RESOURCE_ID in input_parameters.keys():
|
||||
raise exception.ReservedWord(name=baction.BaseAction.
|
||||
|
||||
105
watcher/decision_engine/solution/efficacy.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import numbers
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndicatorsMap(utils.Struct):
|
||||
pass
|
||||
|
||||
|
||||
class Indicator(utils.Struct):
|
||||
|
||||
def __init__(self, name, description, unit, value):
|
||||
super(Indicator, self).__init__()
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
if not isinstance(value, numbers.Number):
|
||||
raise exception.InvalidIndicatorValue(
|
||||
_("An indicator value should be a number"))
|
||||
self.value = value
|
||||
|
||||
|
||||
class Efficacy(object):
|
||||
"""Solution efficacy"""
|
||||
|
||||
def __init__(self, goal, strategy):
|
||||
"""Solution efficacy
|
||||
|
||||
:param goal: Goal associated to this solution
|
||||
:type goal: :py:class:`~.base.Goal` instance
|
||||
:param strategy: Strategy associated to this solution
|
||||
:type strategy: :py:class:`~.BaseStrategy` instance
|
||||
"""
|
||||
self.goal = goal
|
||||
self.strategy = strategy
|
||||
|
||||
self._efficacy_spec = self.goal.efficacy_specification
|
||||
|
||||
# Used to store in DB the info related to the efficacy indicators
|
||||
self.indicators = []
|
||||
# Used to compute the global efficacy
|
||||
self._indicators_mapping = IndicatorsMap()
|
||||
self.global_efficacy = None
|
||||
|
||||
def set_efficacy_indicators(self, **indicators_map):
|
||||
"""Set the efficacy indicators
|
||||
|
||||
:param indicators_map: kwargs where the key is the name of the efficacy
|
||||
indicator as defined in the associated
|
||||
:py:class:`~.IndicatorSpecification` and the
|
||||
value is a number.
|
||||
:type indicators_map: dict {str: numerical value}
|
||||
"""
|
||||
self._indicators_mapping.update(indicators_map)
|
||||
|
||||
def compute_global_efficacy(self):
|
||||
self._efficacy_spec.validate_efficacy_indicators(
|
||||
self._indicators_mapping)
|
||||
try:
|
||||
self.global_efficacy = (
|
||||
self._efficacy_spec.get_global_efficacy_indicator(
|
||||
self._indicators_mapping))
|
||||
|
||||
indicators_specs_map = {
|
||||
indicator_spec.name: indicator_spec
|
||||
for indicator_spec in self._efficacy_spec.indicators_specs}
|
||||
|
||||
indicators = []
|
||||
for indicator_name, value in self._indicators_mapping.items():
|
||||
related_indicator_spec = indicators_specs_map[indicator_name]
|
||||
indicators.append(
|
||||
Indicator(
|
||||
name=related_indicator_spec.name,
|
||||
description=related_indicator_spec.description,
|
||||
unit=related_indicator_spec.unit,
|
||||
value=value))
|
||||
|
||||
self.indicators = indicators
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise exception.GlobalEfficacyComputationError(
|
||||
goal=self.goal.name,
|
||||
strategy=self.strategy.name)
|
||||
@@ -16,9 +16,9 @@
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common import clients
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.strategy.context import base
|
||||
from watcher.decision_engine.strategy.selection import default
|
||||
from watcher.metrics_engine.cluster_model_collector import manager
|
||||
|
||||
from watcher import objects
|
||||
|
||||
@@ -26,15 +26,9 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultStrategyContext(base.BaseStrategyContext):
|
||||
|
||||
def __init__(self):
|
||||
super(DefaultStrategyContext, self).__init__()
|
||||
LOG.debug("Initializing Strategy Context")
|
||||
self._collector_manager = manager.CollectorManager()
|
||||
|
||||
@property
|
||||
def collector(self):
|
||||
return self._collector_manager
|
||||
|
||||
def execute_strategy(self, audit_uuid, request_context):
|
||||
audit = objects.Audit.get_by_uuid(request_context, audit_uuid)
|
||||
@@ -46,18 +40,33 @@ class DefaultStrategyContext(base.BaseStrategyContext):
|
||||
osc = clients.OpenStackClients()
|
||||
# todo(jed) retrieve in audit_template parameters (threshold,...)
|
||||
# todo(jed) create ActionPlan
|
||||
collector_manager = self.collector.get_cluster_model_collector(osc=osc)
|
||||
|
||||
# todo(jed) remove call to get_latest_cluster_data_model
|
||||
cluster_data_model = collector_manager.get_latest_cluster_data_model()
|
||||
goal = objects.Goal.get_by_id(request_context, audit_template.goal_id)
|
||||
|
||||
# NOTE(jed56) In the audit_template object, the 'strategy_id' attribute
|
||||
# is optional. If the admin wants to force the trigger of a Strategy
|
||||
# it could specify the Strategy uuid in the Audit Template.
|
||||
strategy_name = None
|
||||
if audit_template.strategy_id:
|
||||
strategy = objects.Strategy.get_by_id(request_context,
|
||||
audit_template.strategy_id)
|
||||
strategy_name = strategy.name
|
||||
|
||||
strategy_selector = default.DefaultStrategySelector(
|
||||
goal_name=objects.Goal.get_by_id(
|
||||
request_context, audit_template.goal_id).name,
|
||||
strategy_name=None,
|
||||
goal_name=goal.name,
|
||||
strategy_name=strategy_name,
|
||||
osc=osc)
|
||||
|
||||
selected_strategy = strategy_selector.select()
|
||||
|
||||
# todo(jed) add parameters and remove cluster_data_model
|
||||
return selected_strategy.execute(cluster_data_model)
|
||||
schema = selected_strategy.get_schema()
|
||||
if not audit.parameters and schema:
|
||||
# Default value feedback if no predefined strategy
|
||||
utils.DefaultValidatingDraft4Validator(schema).validate(
|
||||
audit.parameters)
|
||||
|
||||
selected_strategy.input_parameters.update({
|
||||
name: value for name, value in audit.parameters.items()
|
||||
})
|
||||
|
||||
return selected_strategy.execute()
|
||||
|
||||
@@ -19,7 +19,7 @@ from oslo_log import log
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.strategy.loading import default
|
||||
from watcher.decision_engine.loading import default
|
||||
from watcher.decision_engine.strategy.selection import base
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
@@ -18,16 +18,20 @@
|
||||
from watcher.decision_engine.strategy.strategies import basic_consolidation
|
||||
from watcher.decision_engine.strategy.strategies import dummy_strategy
|
||||
from watcher.decision_engine.strategy.strategies import outlet_temp_control
|
||||
from watcher.decision_engine.strategy.strategies import uniform_airflow
|
||||
from watcher.decision_engine.strategy.strategies import \
|
||||
vm_workload_consolidation
|
||||
from watcher.decision_engine.strategy.strategies import workload_balance
|
||||
from watcher.decision_engine.strategy.strategies import workload_stabilization
|
||||
|
||||
BasicConsolidation = basic_consolidation.BasicConsolidation
|
||||
OutletTempControl = outlet_temp_control.OutletTempControl
|
||||
DummyStrategy = dummy_strategy.DummyStrategy
|
||||
VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation
|
||||
WorkloadBalance = workload_balance.WorkloadBalance
|
||||
WorkloadStabilization = workload_stabilization.WorkloadStabilization
|
||||
UniformAirflow = uniform_airflow.UniformAirflow
|
||||
|
||||
__all__ = ("BasicConsolidation", "OutletTempControl",
|
||||
"DummyStrategy", "VMWorkloadConsolidation",
|
||||
"WorkloadStabilization")
|
||||
__all__ = ("BasicConsolidation", "OutletTempControl", "DummyStrategy",
|
||||
"VMWorkloadConsolidation", "WorkloadBalance",
|
||||
"WorkloadStabilization", "UniformAirflow")
|
||||
|
||||
@@ -39,11 +39,13 @@ which are dynamically loaded by Watcher at launch time.
|
||||
import abc
|
||||
import six
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import clients
|
||||
from watcher.common.loader import loadable
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.loading import default as loading
|
||||
from watcher.decision_engine.solution import default
|
||||
from watcher.decision_engine.strategy.common import level
|
||||
from watcher.metrics_engine.cluster_model_collector import manager
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
@@ -55,16 +57,27 @@ class BaseStrategy(loadable.Loadable):
|
||||
"""
|
||||
|
||||
def __init__(self, config, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
"""Constructor: the signature should be identical within the subclasses
|
||||
|
||||
:param config: Configuration related to this plugin
|
||||
:type config: :py:class:`~.Struct`
|
||||
:param osc: An OpenStackClients instance
|
||||
:type osc: :py:class:`~.OpenStackClients` instance
|
||||
"""
|
||||
super(BaseStrategy, self).__init__(config)
|
||||
self._name = self.get_name()
|
||||
self._display_name = self.get_display_name()
|
||||
self._goal = self.get_goal()
|
||||
# default strategy level
|
||||
self._strategy_level = level.StrategyLevel.conservative
|
||||
self._cluster_state_collector = None
|
||||
# the solution given by the strategy
|
||||
self._solution = default.DefaultSolution()
|
||||
self._solution = default.DefaultSolution(goal=self.goal, strategy=self)
|
||||
self._osc = osc
|
||||
self._collector_manager = None
|
||||
self._model = None
|
||||
self._goal = None
|
||||
self._input_parameters = utils.Struct()
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
@@ -89,22 +102,14 @@ class BaseStrategy(loadable.Loadable):
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_goal_name(cls):
|
||||
"""The goal name for the strategy"""
|
||||
"""The goal name the strategy achieves"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_goal_display_name(cls):
|
||||
"""The translated display name related to the goal of the strategy"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
"""The translatable msgid related to the goal of the strategy"""
|
||||
# Note(v-francoise): Defined here to be used as the translation key for
|
||||
# other services
|
||||
raise NotImplementedError()
|
||||
def get_goal(cls):
|
||||
"""The goal the strategy achieves"""
|
||||
goal_loader = loading.DefaultGoalLoader()
|
||||
return goal_loader.load(cls.get_goal_name())
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
@@ -116,14 +121,79 @@ class BaseStrategy(loadable.Loadable):
|
||||
return []
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self, original_model):
|
||||
def pre_execute(self):
|
||||
"""Pre-execution phase
|
||||
|
||||
This can be used to fetch some pre-requisites or data.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def do_execute(self):
|
||||
"""Strategy execution phase
|
||||
|
||||
This phase is where you should put the main logic of your strategy.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def post_execute(self):
|
||||
"""Post-execution phase
|
||||
|
||||
This can be used to compute the global efficacy
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute(self):
|
||||
"""Execute a strategy
|
||||
|
||||
:param original_model: The model the strategy is executed on
|
||||
:type model: str
|
||||
:return: A computed solution (via a placement algorithm)
|
||||
:rtype: :class:`watcher.decision_engine.solution.base.BaseSolution`
|
||||
:rtype: :py:class:`~.BaseSolution` instance
|
||||
"""
|
||||
self.pre_execute()
|
||||
self.do_execute()
|
||||
self.post_execute()
|
||||
|
||||
self.solution.compute_global_efficacy()
|
||||
|
||||
return self.solution
|
||||
|
||||
@property
|
||||
def collector(self):
|
||||
if self._collector_manager is None:
|
||||
self._collector_manager = manager.CollectorManager()
|
||||
return self._collector_manager
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Cluster data model
|
||||
|
||||
:returns: Cluster data model the strategy is executed on
|
||||
:rtype model: :py:class:`~.ModelRoot` instance
|
||||
"""
|
||||
if self._model is None:
|
||||
collector = self.collector.get_cluster_model_collector(
|
||||
osc=self.osc)
|
||||
self._model = collector.get_latest_cluster_data_model()
|
||||
|
||||
return self._model
|
||||
|
||||
@classmethod
|
||||
def get_schema(cls):
|
||||
"""Defines a Schema that the input parameters shall comply to
|
||||
|
||||
:return: A jsonschema format (mandatory default setting)
|
||||
:rtype: dict
|
||||
"""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def input_parameters(self):
|
||||
return self._input_parameters
|
||||
|
||||
@input_parameters.setter
|
||||
def input_parameters(self, p):
|
||||
self._input_parameters = p
|
||||
|
||||
@property
|
||||
def osc(self):
|
||||
@@ -140,13 +210,17 @@ class BaseStrategy(loadable.Loadable):
|
||||
self._solution = s
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._display_name
|
||||
|
||||
@property
|
||||
def goal(self):
|
||||
return self._goal
|
||||
|
||||
@property
|
||||
def strategy_level(self):
|
||||
return self._strategy_level
|
||||
@@ -169,15 +243,7 @@ class DummyBaseStrategy(BaseStrategy):
|
||||
|
||||
@classmethod
|
||||
def get_goal_name(cls):
|
||||
return "DUMMY"
|
||||
|
||||
@classmethod
|
||||
def get_goal_display_name(cls):
|
||||
return _("Dummy goal")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
return "Dummy goal"
|
||||
return "dummy"
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
@@ -192,15 +258,7 @@ class UnclassifiedStrategy(BaseStrategy):
|
||||
|
||||
@classmethod
|
||||
def get_goal_name(cls):
|
||||
return "UNCLASSIFIED"
|
||||
|
||||
@classmethod
|
||||
def get_goal_display_name(cls):
|
||||
return _("Unclassified")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
return "Unclassified"
|
||||
return "unclassified"
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
@@ -208,15 +266,7 @@ class ServerConsolidationBaseStrategy(BaseStrategy):
|
||||
|
||||
@classmethod
|
||||
def get_goal_name(cls):
|
||||
return "SERVER_CONSOLIDATION"
|
||||
|
||||
@classmethod
|
||||
def get_goal_display_name(cls):
|
||||
return _("Server consolidation")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
return "Server consolidation"
|
||||
return "server_consolidation"
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
@@ -224,15 +274,7 @@ class ThermalOptimizationBaseStrategy(BaseStrategy):
|
||||
|
||||
@classmethod
|
||||
def get_goal_name(cls):
|
||||
return "THERMAL_OPTIMIZATION"
|
||||
|
||||
@classmethod
|
||||
def get_goal_display_name(cls):
|
||||
return _("Thermal optimization")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
return "Thermal optimization"
|
||||
return "thermal_optimization"
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
@@ -240,12 +282,4 @@ class WorkloadStabilizationBaseStrategy(BaseStrategy):
|
||||
|
||||
@classmethod
|
||||
def get_goal_name(cls):
|
||||
return "WORKLOAD_BALANCING"
|
||||
|
||||
@classmethod
|
||||
def get_goal_display_name(cls):
|
||||
return _("Workload balancing")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
return "Workload balancing"
|
||||
return "workload_balancing"
|
||||
|
||||
@@ -71,11 +71,11 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
MIGRATION = "migrate"
|
||||
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
|
||||
|
||||
def __init__(self, config=None, osc=None):
|
||||
def __init__(self, config, osc=None):
|
||||
"""Basic offline Consolidation using live migration
|
||||
|
||||
:param config: A mapping containing the configuration of this strategy
|
||||
:type config: dict
|
||||
:type config: :py:class:`~.Struct` instance
|
||||
:param osc: :py:class:`~.OpenStackClients` instance
|
||||
"""
|
||||
super(BasicConsolidation, self).__init__(config, osc)
|
||||
@@ -134,17 +134,13 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
def compute_attempts(self, size_cluster):
|
||||
"""Upper bound of the number of migration
|
||||
|
||||
:param size_cluster:
|
||||
:param size_cluster: The size of the cluster
|
||||
"""
|
||||
self.migration_attempts = size_cluster * self.bound_migration
|
||||
|
||||
def check_migration(self, cluster_data_model,
|
||||
src_hypervisor,
|
||||
dest_hypervisor,
|
||||
vm_to_mig):
|
||||
"""check if the migration is possible
|
||||
def check_migration(self, src_hypervisor, dest_hypervisor, vm_to_mig):
|
||||
"""Check if the migration is possible
|
||||
|
||||
:param cluster_data_model: the current state of the cluster
|
||||
:param src_hypervisor: the current node of the virtual machine
|
||||
:param dest_hypervisor: the destination of the virtual machine
|
||||
:param vm_to_mig: the virtual machine
|
||||
@@ -153,24 +149,22 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
if src_hypervisor == dest_hypervisor:
|
||||
return False
|
||||
|
||||
LOG.debug('Migrate VM {0} from {1} to {2} '.format(vm_to_mig,
|
||||
src_hypervisor,
|
||||
dest_hypervisor,
|
||||
))
|
||||
LOG.debug('Migrate VM %s from %s to %s',
|
||||
vm_to_mig, src_hypervisor, dest_hypervisor)
|
||||
|
||||
total_cores = 0
|
||||
total_disk = 0
|
||||
total_mem = 0
|
||||
cpu_capacity = cluster_data_model.get_resource_from_id(
|
||||
cpu_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores)
|
||||
disk_capacity = cluster_data_model.get_resource_from_id(
|
||||
disk_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.disk)
|
||||
memory_capacity = cluster_data_model.get_resource_from_id(
|
||||
memory_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.memory)
|
||||
|
||||
for vm_id in cluster_data_model. \
|
||||
for vm_id in self.model. \
|
||||
get_mapping().get_node_vms(dest_hypervisor):
|
||||
vm = cluster_data_model.get_vm_from_id(vm_id)
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
total_cores += cpu_capacity.get_capacity(vm)
|
||||
total_disk += disk_capacity.get_capacity(vm)
|
||||
total_mem += memory_capacity.get_capacity(vm)
|
||||
@@ -180,42 +174,32 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
total_disk += disk_capacity.get_capacity(vm_to_mig)
|
||||
total_mem += memory_capacity.get_capacity(vm_to_mig)
|
||||
|
||||
return self.check_threshold(cluster_data_model,
|
||||
dest_hypervisor,
|
||||
total_cores,
|
||||
total_disk,
|
||||
return self.check_threshold(dest_hypervisor, total_cores, total_disk,
|
||||
total_mem)
|
||||
|
||||
def check_threshold(self, cluster_data_model,
|
||||
dest_hypervisor,
|
||||
total_cores,
|
||||
total_disk,
|
||||
total_mem):
|
||||
def check_threshold(self, dest_hypervisor, total_cores,
|
||||
total_disk, total_mem):
|
||||
"""Check threshold
|
||||
|
||||
check the threshold value defined by the ratio of
|
||||
aggregated CPU capacity of VMs on one node to CPU capacity
|
||||
of this node must not exceed the threshold value.
|
||||
:param cluster_data_model: the current state of the cluster
|
||||
:param dest_hypervisor: the destination of the virtual machine
|
||||
:param total_cores
|
||||
:param total_disk
|
||||
:param total_mem
|
||||
:return: True if the threshold is not exceed
|
||||
"""
|
||||
cpu_capacity = cluster_data_model.get_resource_from_id(
|
||||
cpu_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores).get_capacity(dest_hypervisor)
|
||||
disk_capacity = cluster_data_model.get_resource_from_id(
|
||||
disk_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.disk).get_capacity(dest_hypervisor)
|
||||
memory_capacity = cluster_data_model.get_resource_from_id(
|
||||
memory_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.memory).get_capacity(dest_hypervisor)
|
||||
|
||||
if (cpu_capacity >= total_cores * self.threshold_cores and
|
||||
return (cpu_capacity >= total_cores * self.threshold_cores and
|
||||
disk_capacity >= total_disk * self.threshold_disk and
|
||||
memory_capacity >= total_mem * self.threshold_mem):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
memory_capacity >= total_mem * self.threshold_mem)
|
||||
|
||||
def get_allowed_migration_attempts(self):
|
||||
"""Allowed migration
|
||||
@@ -226,37 +210,23 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
"""
|
||||
return self.migration_attempts
|
||||
|
||||
def get_threshold_cores(self):
|
||||
return self.threshold_cores
|
||||
|
||||
def set_threshold_cores(self, threshold):
|
||||
self.threshold_cores = threshold
|
||||
|
||||
def get_number_of_released_nodes(self):
|
||||
return self.number_of_released_nodes
|
||||
|
||||
def get_number_of_migrations(self):
|
||||
return self.number_of_migrations
|
||||
|
||||
def calculate_weight(self, cluster_data_model, element,
|
||||
total_cores_used, total_disk_used,
|
||||
def calculate_weight(self, element, total_cores_used, total_disk_used,
|
||||
total_memory_used):
|
||||
"""Calculate weight of every resource
|
||||
|
||||
:param cluster_data_model:
|
||||
:param element:
|
||||
:param total_cores_used:
|
||||
:param total_disk_used:
|
||||
:param total_memory_used:
|
||||
:return:
|
||||
"""
|
||||
cpu_capacity = cluster_data_model.get_resource_from_id(
|
||||
cpu_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores).get_capacity(element)
|
||||
|
||||
disk_capacity = cluster_data_model.get_resource_from_id(
|
||||
disk_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.disk).get_capacity(element)
|
||||
|
||||
memory_capacity = cluster_data_model.get_resource_from_id(
|
||||
memory_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.memory).get_capacity(element)
|
||||
|
||||
score_cores = (1 - (float(cpu_capacity) - float(total_cores_used)) /
|
||||
@@ -275,20 +245,19 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
# todo(jed) take in account weight
|
||||
return (score_cores + score_disk + score_memory) / 3
|
||||
|
||||
def calculate_score_node(self, hypervisor, model):
|
||||
"""calculate the score that represent the utilization level
|
||||
def calculate_score_node(self, hypervisor):
|
||||
"""Calculate the score that represent the utilization level
|
||||
|
||||
:param hypervisor:
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
:param hypervisor:
|
||||
:return:
|
||||
"""
|
||||
resource_id = "%s_%s" % (hypervisor.uuid, hypervisor.hostname)
|
||||
host_avg_cpu_util = self.ceilometer. \
|
||||
statistic_aggregation(resource_id=resource_id,
|
||||
meter_name=self.HOST_CPU_USAGE_METRIC_NAME,
|
||||
period="7200",
|
||||
aggregate='avg'
|
||||
)
|
||||
aggregate='avg')
|
||||
|
||||
if host_avg_cpu_util is None:
|
||||
LOG.error(
|
||||
_LE("No values returned by %(resource_id)s "
|
||||
@@ -298,14 +267,12 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
)
|
||||
host_avg_cpu_util = 100
|
||||
|
||||
cpu_capacity = model.get_resource_from_id(
|
||||
cpu_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores).get_capacity(hypervisor)
|
||||
|
||||
total_cores_used = cpu_capacity * (host_avg_cpu_util / 100)
|
||||
|
||||
return self.calculate_weight(model, hypervisor, total_cores_used,
|
||||
0,
|
||||
0)
|
||||
return self.calculate_weight(hypervisor, total_cores_used, 0, 0)
|
||||
|
||||
def calculate_migration_efficacy(self):
|
||||
"""Calculate migration efficacy
|
||||
@@ -319,16 +286,13 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
else:
|
||||
return 0
|
||||
|
||||
def calculate_score_vm(self, vm, cluster_data_model):
|
||||
def calculate_score_vm(self, vm):
|
||||
"""Calculate Score of virtual machine
|
||||
|
||||
:param vm: the virtual machine
|
||||
:param cluster_data_model: the cluster model
|
||||
:param self.model: the cluster model
|
||||
:return: score
|
||||
"""
|
||||
if cluster_data_model is None:
|
||||
raise exception.ClusterStateNotDefined()
|
||||
|
||||
vm_cpu_utilization = self.ceilometer. \
|
||||
statistic_aggregation(
|
||||
resource_id=vm.uuid,
|
||||
@@ -345,13 +309,12 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
)
|
||||
vm_cpu_utilization = 100
|
||||
|
||||
cpu_capacity = cluster_data_model.get_resource_from_id(
|
||||
cpu_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores).get_capacity(vm)
|
||||
|
||||
total_cores_used = cpu_capacity * (vm_cpu_utilization / 100.0)
|
||||
|
||||
return self.calculate_weight(cluster_data_model, vm,
|
||||
total_cores_used, 0, 0)
|
||||
return self.calculate_weight(vm, total_cores_used, 0, 0)
|
||||
|
||||
def add_change_service_state(self, resource_id, state):
|
||||
parameters = {'state': state}
|
||||
@@ -371,48 +334,47 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
resource_id=resource_id,
|
||||
input_parameters=parameters)
|
||||
|
||||
def score_of_nodes(self, cluster_data_model, score):
|
||||
def score_of_nodes(self, score):
|
||||
"""Calculate score of nodes based on load by VMs"""
|
||||
for hypervisor_id in cluster_data_model.get_all_hypervisors():
|
||||
hypervisor = cluster_data_model. \
|
||||
for hypervisor_id in self.model.get_all_hypervisors():
|
||||
hypervisor = self.model. \
|
||||
get_hypervisor_from_id(hypervisor_id)
|
||||
count = cluster_data_model.get_mapping(). \
|
||||
count = self.model.get_mapping(). \
|
||||
get_node_vms_from_id(hypervisor_id)
|
||||
if len(count) > 0:
|
||||
result = self.calculate_score_node(hypervisor,
|
||||
cluster_data_model)
|
||||
result = self.calculate_score_node(hypervisor)
|
||||
else:
|
||||
''' the hypervisor has not VMs '''
|
||||
# The hypervisor has not VMs
|
||||
result = 0
|
||||
if len(count) > 0:
|
||||
score.append((hypervisor_id, result))
|
||||
return score
|
||||
|
||||
def node_and_vm_score(self, sorted_score, score, current_model):
|
||||
def node_and_vm_score(self, sorted_score, score):
|
||||
"""Get List of VMs from Node"""
|
||||
node_to_release = sorted_score[len(score) - 1][0]
|
||||
vms_to_mig = current_model.get_mapping().get_node_vms_from_id(
|
||||
vms_to_mig = self.model.get_mapping().get_node_vms_from_id(
|
||||
node_to_release)
|
||||
|
||||
vm_score = []
|
||||
for vm_id in vms_to_mig:
|
||||
vm = current_model.get_vm_from_id(vm_id)
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
if vm.state == vm_state.VMState.ACTIVE.value:
|
||||
vm_score.append(
|
||||
(vm_id, self.calculate_score_vm(vm, current_model)))
|
||||
(vm_id, self.calculate_score_vm(vm)))
|
||||
|
||||
return node_to_release, vm_score
|
||||
|
||||
def create_migration_vm(self, current_model, mig_vm, mig_src_hypervisor,
|
||||
def create_migration_vm(self, mig_vm, mig_src_hypervisor,
|
||||
mig_dst_hypervisor):
|
||||
"""Create migration VM """
|
||||
if current_model.get_mapping().migrate_vm(
|
||||
"""Create migration VM"""
|
||||
if self.model.get_mapping().migrate_vm(
|
||||
mig_vm, mig_src_hypervisor, mig_dst_hypervisor):
|
||||
self.add_migration(mig_vm.uuid, 'live',
|
||||
mig_src_hypervisor.uuid,
|
||||
mig_dst_hypervisor.uuid)
|
||||
|
||||
if len(current_model.get_mapping().get_node_vms(
|
||||
if len(self.model.get_mapping().get_node_vms(
|
||||
mig_src_hypervisor)) == 0:
|
||||
self.add_change_service_state(mig_src_hypervisor.
|
||||
uuid,
|
||||
@@ -420,24 +382,22 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
DISABLED.value)
|
||||
self.number_of_released_nodes += 1
|
||||
|
||||
def calculate_num_migrations(self, sorted_vms, current_model,
|
||||
node_to_release, sorted_score):
|
||||
def calculate_num_migrations(self, sorted_vms, node_to_release,
|
||||
sorted_score):
|
||||
number_migrations = 0
|
||||
for vm in sorted_vms:
|
||||
for j in range(0, len(sorted_score)):
|
||||
mig_vm = current_model.get_vm_from_id(vm[0])
|
||||
mig_src_hypervisor = current_model.get_hypervisor_from_id(
|
||||
mig_vm = self.model.get_vm_from_id(vm[0])
|
||||
mig_src_hypervisor = self.model.get_hypervisor_from_id(
|
||||
node_to_release)
|
||||
mig_dst_hypervisor = current_model.get_hypervisor_from_id(
|
||||
mig_dst_hypervisor = self.model.get_hypervisor_from_id(
|
||||
sorted_score[j][0])
|
||||
|
||||
result = self.check_migration(current_model,
|
||||
mig_src_hypervisor,
|
||||
mig_dst_hypervisor, mig_vm)
|
||||
result = self.check_migration(
|
||||
mig_src_hypervisor, mig_dst_hypervisor, mig_vm)
|
||||
if result:
|
||||
self.create_migration_vm(
|
||||
current_model, mig_vm,
|
||||
mig_src_hypervisor, mig_dst_hypervisor)
|
||||
mig_vm, mig_src_hypervisor, mig_dst_hypervisor)
|
||||
number_migrations += 1
|
||||
break
|
||||
return number_migrations
|
||||
@@ -450,28 +410,26 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
else:
|
||||
return unsuccessful_migration + 1
|
||||
|
||||
def execute(self, original_model):
|
||||
def pre_execute(self):
|
||||
LOG.info(_LI("Initializing Sercon Consolidation"))
|
||||
|
||||
if original_model is None:
|
||||
if self.model is None:
|
||||
raise exception.ClusterStateNotDefined()
|
||||
|
||||
def do_execute(self):
|
||||
# todo(jed) clone model
|
||||
current_model = original_model
|
||||
|
||||
self.efficacy = 100
|
||||
unsuccessful_migration = 0
|
||||
|
||||
first_migration = True
|
||||
size_cluster = len(current_model.get_all_hypervisors())
|
||||
size_cluster = len(self.model.get_all_hypervisors())
|
||||
if size_cluster == 0:
|
||||
raise exception.ClusterEmpty()
|
||||
|
||||
self.compute_attempts(size_cluster)
|
||||
|
||||
for hypervisor_id in current_model.get_all_hypervisors():
|
||||
hypervisor = current_model.get_hypervisor_from_id(hypervisor_id)
|
||||
count = current_model.get_mapping(). \
|
||||
for hypervisor_id in self.model.get_all_hypervisors():
|
||||
hypervisor = self.model.get_hypervisor_from_id(hypervisor_id)
|
||||
count = self.model.get_mapping(). \
|
||||
get_node_vms_from_id(hypervisor_id)
|
||||
if len(count) == 0:
|
||||
if hypervisor.state == hyper_state.HypervisorState.ENABLED:
|
||||
@@ -487,13 +445,13 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
first_migration = False
|
||||
score = []
|
||||
|
||||
score = self.score_of_nodes(current_model, score)
|
||||
score = self.score_of_nodes(score)
|
||||
|
||||
''' sort compute nodes by Score decreasing '''''
|
||||
# Sort compute nodes by Score decreasing
|
||||
sorted_score = sorted(score, reverse=True, key=lambda x: (x[1]))
|
||||
LOG.debug("Hypervisor(s) BFD {0}".format(sorted_score))
|
||||
LOG.debug("Hypervisor(s) BFD %s", sorted_score)
|
||||
|
||||
''' get Node to be released '''
|
||||
# Get Node to be released
|
||||
if len(score) == 0:
|
||||
LOG.warning(_LW(
|
||||
"The workloads of the compute nodes"
|
||||
@@ -501,15 +459,15 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
break
|
||||
|
||||
node_to_release, vm_score = self.node_and_vm_score(
|
||||
sorted_score, score, current_model)
|
||||
sorted_score, score)
|
||||
|
||||
''' sort VMs by Score '''
|
||||
# Sort VMs by Score
|
||||
sorted_vms = sorted(vm_score, reverse=True, key=lambda x: (x[1]))
|
||||
# BFD: Best Fit Decrease
|
||||
LOG.debug("VM(s) BFD {0}".format(sorted_vms))
|
||||
LOG.debug("VM(s) BFD %s", sorted_vms)
|
||||
|
||||
migrations = self.calculate_num_migrations(
|
||||
sorted_vms, current_model, node_to_release, sorted_score)
|
||||
sorted_vms, node_to_release, sorted_score)
|
||||
|
||||
unsuccessful_migration = self.unsuccessful_migration_actualization(
|
||||
migrations, unsuccessful_migration)
|
||||
@@ -519,6 +477,9 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
"efficacy": self.efficacy
|
||||
}
|
||||
LOG.debug(infos)
|
||||
self.solution.model = current_model
|
||||
self.solution.efficacy = self.efficacy
|
||||
return self.solution
|
||||
|
||||
def post_execute(self):
|
||||
self.solution.set_efficacy_indicators(
|
||||
released_compute_nodes_count=self.number_of_migrations,
|
||||
vm_migrations_count=self.number_of_released_nodes,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
@@ -48,17 +49,15 @@ class DummyStrategy(base.DummyBaseStrategy):
|
||||
NOP = "nop"
|
||||
SLEEP = "sleep"
|
||||
|
||||
def __init__(self, config=None, osc=None):
|
||||
"""Dummy Strategy implemented for demo and testing purposes
|
||||
def pre_execute(self):
|
||||
if self.model is None:
|
||||
raise exception.ClusterStateNotDefined()
|
||||
|
||||
:param config: A mapping containing the configuration of this strategy
|
||||
:type config: dict
|
||||
:param osc: :py:class:`~.OpenStackClients` instance
|
||||
"""
|
||||
super(DummyStrategy, self).__init__(config, osc)
|
||||
|
||||
def execute(self, original_model):
|
||||
LOG.debug("Executing Dummy strategy")
|
||||
def do_execute(self):
|
||||
para1 = self.input_parameters.para1
|
||||
para2 = self.input_parameters.para2
|
||||
LOG.debug("Executing Dummy strategy with para1=%(p1)f, para2=%(p2)s",
|
||||
{'p1': para1, 'p2': para2})
|
||||
parameters = {'message': 'hello World'}
|
||||
self.solution.add_action(action_type=self.NOP,
|
||||
input_parameters=parameters)
|
||||
@@ -69,7 +68,9 @@ class DummyStrategy(base.DummyBaseStrategy):
|
||||
|
||||
self.solution.add_action(action_type=self.SLEEP,
|
||||
input_parameters={'duration': 5.0})
|
||||
return self.solution
|
||||
|
||||
def post_execute(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
@@ -82,3 +83,23 @@ class DummyStrategy(base.DummyBaseStrategy):
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Dummy strategy"
|
||||
|
||||
@classmethod
|
||||
def get_schema(cls):
|
||||
# Mandatory default setting for each element
|
||||
return {
|
||||
"properties": {
|
||||
"para1": {
|
||||
"description": "number parameter example",
|
||||
"type": "number",
|
||||
"default": 3.2,
|
||||
"minimum": 1.0,
|
||||
"maximum": 10.2,
|
||||
},
|
||||
"para2": {
|
||||
"description": "string parameter example",
|
||||
"type": "string",
|
||||
"default": "hello"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ telemetries to measure thermal/workload status of server.
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _, _LE
|
||||
from watcher._i18n import _, _LE, _LI
|
||||
from watcher.common import exception as wexc
|
||||
from watcher.decision_engine.model import resource
|
||||
from watcher.decision_engine.model import vm_state
|
||||
@@ -73,12 +73,10 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
|
||||
# The meter to report outlet temperature in ceilometer
|
||||
METER_NAME = "hardware.ipmi.node.outlet_temperature"
|
||||
# Unit: degree C
|
||||
THRESHOLD = 35.0
|
||||
|
||||
MIGRATION = "migrate"
|
||||
|
||||
def __init__(self, config=None, osc=None):
|
||||
def __init__(self, config, osc=None):
|
||||
"""Outlet temperature control using live migration
|
||||
|
||||
:param config: A mapping containing the configuration of this strategy
|
||||
@@ -87,10 +85,6 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
:type osc: :py:class:`~.OpenStackClients` instance, optional
|
||||
"""
|
||||
super(OutletTempControl, self).__init__(config, osc)
|
||||
# the migration plan will be triggered when the outlet temperature
|
||||
# reaches threshold
|
||||
# TODO(zhenzanz): Threshold should be configurable for each audit
|
||||
self.threshold = self.THRESHOLD
|
||||
self._meter = self.METER_NAME
|
||||
self._ceilometer = None
|
||||
|
||||
@@ -106,6 +100,19 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
def get_translatable_display_name(cls):
|
||||
return "Outlet temperature based strategy"
|
||||
|
||||
@classmethod
|
||||
def get_schema(cls):
|
||||
# Mandatory default setting for each element
|
||||
return {
|
||||
"properties": {
|
||||
"threshold": {
|
||||
"description": "temperature threshold for migration",
|
||||
"type": "number",
|
||||
"default": 35.0
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
@@ -116,26 +123,25 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
def ceilometer(self, c):
|
||||
self._ceilometer = c
|
||||
|
||||
def calc_used_res(self, cluster_data_model, hypervisor, cpu_capacity,
|
||||
def calc_used_res(self, hypervisor, cpu_capacity,
|
||||
memory_capacity, disk_capacity):
|
||||
'''calculate the used vcpus, memory and disk based on VM flavors'''
|
||||
vms = cluster_data_model.get_mapping().get_node_vms(hypervisor)
|
||||
"""Calculate the used vcpus, memory and disk based on VM flavors"""
|
||||
vms = self.model.get_mapping().get_node_vms(hypervisor)
|
||||
vcpus_used = 0
|
||||
memory_mb_used = 0
|
||||
disk_gb_used = 0
|
||||
if len(vms) > 0:
|
||||
for vm_id in vms:
|
||||
vm = cluster_data_model.get_vm_from_id(vm_id)
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
vcpus_used += cpu_capacity.get_capacity(vm)
|
||||
memory_mb_used += memory_capacity.get_capacity(vm)
|
||||
disk_gb_used += disk_capacity.get_capacity(vm)
|
||||
|
||||
return vcpus_used, memory_mb_used, disk_gb_used
|
||||
|
||||
def group_hosts_by_outlet_temp(self, cluster_data_model):
|
||||
def group_hosts_by_outlet_temp(self):
|
||||
"""Group hosts based on outlet temp meters"""
|
||||
|
||||
hypervisors = cluster_data_model.get_all_hypervisors()
|
||||
hypervisors = self.model.get_all_hypervisors()
|
||||
size_cluster = len(hypervisors)
|
||||
if size_cluster == 0:
|
||||
raise wexc.ClusterEmpty()
|
||||
@@ -143,7 +149,7 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
hosts_need_release = []
|
||||
hosts_target = []
|
||||
for hypervisor_id in hypervisors:
|
||||
hypervisor = cluster_data_model.get_hypervisor_from_id(
|
||||
hypervisor = self.model.get_hypervisor_from_id(
|
||||
hypervisor_id)
|
||||
resource_id = hypervisor.uuid
|
||||
|
||||
@@ -166,37 +172,35 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
hosts_target.append(hvmap)
|
||||
return hosts_need_release, hosts_target
|
||||
|
||||
def choose_vm_to_migrate(self, cluster_data_model, hosts):
|
||||
"""pick up an active vm instance to migrate from provided hosts"""
|
||||
|
||||
def choose_vm_to_migrate(self, hosts):
|
||||
"""Pick up an active vm instance to migrate from provided hosts"""
|
||||
for hvmap in hosts:
|
||||
mig_src_hypervisor = hvmap['hv']
|
||||
vms_of_src = cluster_data_model.get_mapping().get_node_vms(
|
||||
vms_of_src = self.model.get_mapping().get_node_vms(
|
||||
mig_src_hypervisor)
|
||||
if len(vms_of_src) > 0:
|
||||
for vm_id in vms_of_src:
|
||||
try:
|
||||
# select the first active VM to migrate
|
||||
vm = cluster_data_model.get_vm_from_id(vm_id)
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
if vm.state != vm_state.VMState.ACTIVE.value:
|
||||
LOG.info(_LE("VM not active, skipped: %s"),
|
||||
vm.uuid)
|
||||
continue
|
||||
return mig_src_hypervisor, vm
|
||||
except wexc.InstanceNotFound as e:
|
||||
LOG.info("VM not found Error: %s" % e.message)
|
||||
pass
|
||||
LOG.exception(e)
|
||||
LOG.info(_LI("VM not found"))
|
||||
|
||||
return None
|
||||
|
||||
def filter_dest_servers(self, cluster_data_model, hosts, vm_to_migrate):
|
||||
def filter_dest_servers(self, hosts, vm_to_migrate):
|
||||
"""Only return hosts with sufficient available resources"""
|
||||
|
||||
cpu_capacity = cluster_data_model.get_resource_from_id(
|
||||
cpu_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores)
|
||||
disk_capacity = cluster_data_model.get_resource_from_id(
|
||||
disk_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.disk)
|
||||
memory_capacity = cluster_data_model.get_resource_from_id(
|
||||
memory_capacity = self.model.get_resource_from_id(
|
||||
resource.ResourceType.memory)
|
||||
|
||||
required_cores = cpu_capacity.get_capacity(vm_to_migrate)
|
||||
@@ -209,8 +213,7 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
host = hvmap['hv']
|
||||
# available
|
||||
cores_used, mem_used, disk_used = self.calc_used_res(
|
||||
cluster_data_model, host, cpu_capacity, memory_capacity,
|
||||
disk_capacity)
|
||||
host, cpu_capacity, memory_capacity, disk_capacity)
|
||||
cores_available = cpu_capacity.get_capacity(host) - cores_used
|
||||
disk_available = disk_capacity.get_capacity(host) - disk_used
|
||||
mem_available = memory_capacity.get_capacity(host) - mem_used
|
||||
@@ -221,15 +224,19 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
|
||||
return dest_servers
|
||||
|
||||
def execute(self, original_model):
|
||||
def pre_execute(self):
|
||||
LOG.debug("Initializing Outlet temperature strategy")
|
||||
|
||||
if original_model is None:
|
||||
if self.model is None:
|
||||
raise wexc.ClusterStateNotDefined()
|
||||
|
||||
current_model = original_model
|
||||
hosts_need_release, hosts_target = self.group_hosts_by_outlet_temp(
|
||||
current_model)
|
||||
def do_execute(self):
|
||||
# the migration plan will be triggered when the outlet temperature
|
||||
# reaches threshold
|
||||
self.threshold = self.input_parameters.threshold
|
||||
LOG.debug("Initializing Outlet temperature strategy with threshold=%d",
|
||||
self.threshold)
|
||||
hosts_need_release, hosts_target = self.group_hosts_by_outlet_temp()
|
||||
|
||||
if len(hosts_need_release) == 0:
|
||||
# TODO(zhenzanz): return something right if there's no hot servers
|
||||
@@ -245,16 +252,13 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
reverse=True,
|
||||
key=lambda x: (x["outlet_temp"]))
|
||||
|
||||
vm_to_migrate = self.choose_vm_to_migrate(current_model,
|
||||
hosts_need_release)
|
||||
vm_to_migrate = self.choose_vm_to_migrate(hosts_need_release)
|
||||
# calculate the vm's cpu cores,memory,disk needs
|
||||
if vm_to_migrate is None:
|
||||
return self.solution
|
||||
|
||||
mig_src_hypervisor, vm_src = vm_to_migrate
|
||||
dest_servers = self.filter_dest_servers(current_model,
|
||||
hosts_target,
|
||||
vm_src)
|
||||
dest_servers = self.filter_dest_servers(hosts_target, vm_src)
|
||||
# sort the filtered result by outlet temp
|
||||
# pick up the lowest one as dest server
|
||||
if len(dest_servers) == 0:
|
||||
@@ -267,9 +271,8 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
# always use the host with lowerest outlet temperature
|
||||
mig_dst_hypervisor = dest_servers[0]['hv']
|
||||
# generate solution to migrate the vm to the dest server,
|
||||
if current_model.get_mapping().migrate_vm(vm_src,
|
||||
mig_src_hypervisor,
|
||||
mig_dst_hypervisor):
|
||||
if self.model.get_mapping().migrate_vm(
|
||||
vm_src, mig_src_hypervisor, mig_dst_hypervisor):
|
||||
parameters = {'migration_type': 'live',
|
||||
'src_hypervisor': mig_src_hypervisor.uuid,
|
||||
'dst_hypervisor': mig_dst_hypervisor.uuid}
|
||||
@@ -277,6 +280,6 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
resource_id=vm_src.uuid,
|
||||
input_parameters=parameters)
|
||||
|
||||
self.solution.model = current_model
|
||||
|
||||
return self.solution
|
||||
def post_execute(self):
|
||||
self.solution.model = self.model
|
||||
# TODO(v-francoise): Add the indicators to the solution
|
||||
|
||||
326
watcher/decision_engine/strategy/strategies/uniform_airflow.py
Normal file
@@ -0,0 +1,326 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 Intel Corp
|
||||
#
|
||||
# Authors: Junjie-Huang <junjie.huang@intel.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _, _LE, _LI, _LW
|
||||
from watcher.common import exception as wexc
|
||||
from watcher.decision_engine.model import resource
|
||||
from watcher.decision_engine.model import vm_state
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
from watcher.metrics_engine.cluster_history import ceilometer as ceil
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class UniformAirflow(base.BaseStrategy):
|
||||
"""[PoC]Uniform Airflow using live migration
|
||||
|
||||
*Description*
|
||||
|
||||
It is a migration strategy based on the Airflow of physical
|
||||
servers. It generates solutions to move vm whenever a server's
|
||||
Airflow is higher than the specified threshold.
|
||||
|
||||
*Requirements*
|
||||
|
||||
* Hardware: compute node with NodeManager3.0 support
|
||||
* Software: Ceilometer component ceilometer-agent-compute running
|
||||
in each compute node, and Ceilometer API can report such telemetry
|
||||
"airflow, system power, inlet temperature" successfully.
|
||||
* You must have at least 2 physical compute nodes to run this strategy
|
||||
|
||||
*Limitations*
|
||||
|
||||
- This is a proof of concept that is not meant to be used in production
|
||||
- We cannot forecast how many servers should be migrated. This is the
|
||||
reason why we only plan a single virtual machine migration at a time.
|
||||
So it's better to use this algorithm with `CONTINUOUS` audits.
|
||||
- It assume that live migrations are possible
|
||||
|
||||
"""
|
||||
|
||||
# The meter to report Airflow of physical server in ceilometer
|
||||
METER_NAME_AIRFLOW = "hardware.ipmi.node.airflow"
|
||||
# The meter to report inlet temperature of physical server in ceilometer
|
||||
METER_NAME_INLET_T = "hardware.ipmi.node.temperature"
|
||||
# The meter to report system power of physical server in ceilometer
|
||||
METER_NAME_POWER = "hardware.ipmi.node.power"
|
||||
# TODO(Junjie): make below thresholds configurable
|
||||
# Unit: 0.1 CFM
|
||||
THRESHOLD_AIRFLOW = 400.0
|
||||
# Unit: degree C
|
||||
THRESHOLD_INLET_T = 28.0
|
||||
# Unit: watts
|
||||
THRESHOLD_POWER = 350.0
|
||||
# choose 300 seconds as the default duration of meter aggregation
|
||||
# TODO(Junjie): make it configurable
|
||||
PERIOD = 300
|
||||
|
||||
MIGRATION = "migrate"
|
||||
|
||||
def __init__(self, config, osc=None):
|
||||
"""Using live migration
|
||||
|
||||
:param config: A mapping containing the configuration of this strategy
|
||||
:type config: dict
|
||||
:param osc: an OpenStackClients object
|
||||
"""
|
||||
super(UniformAirflow, self).__init__(config, osc)
|
||||
# The migration plan will be triggered when the Ariflow reaches
|
||||
# threshold
|
||||
# TODO(Junjie): Threshold should be configurable for each audit
|
||||
self.threshold_airflow = self.THRESHOLD_AIRFLOW
|
||||
self.threshold_inlet_t = self.THRESHOLD_INLET_T
|
||||
self.threshold_power = self.THRESHOLD_POWER
|
||||
self.meter_name_airflow = self.METER_NAME_AIRFLOW
|
||||
self.meter_name_inlet_t = self.METER_NAME_INLET_T
|
||||
self.meter_name_power = self.METER_NAME_POWER
|
||||
self._ceilometer = None
|
||||
self._period = self.PERIOD
|
||||
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@ceilometer.setter
|
||||
def ceilometer(self, c):
|
||||
self._ceilometer = c
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "uniform_airflow"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("uniform airflow migration strategy")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "uniform airflow migration strategy"
|
||||
|
||||
@classmethod
|
||||
def get_goal_name(cls):
|
||||
return "airflow_optimization"
|
||||
|
||||
@classmethod
|
||||
def get_goal_display_name(cls):
|
||||
return _("AIRFLOW optimization")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_goal_display_name(cls):
|
||||
return "Airflow optimization"
|
||||
|
||||
def calculate_used_resource(self, hypervisor, cap_cores, cap_mem,
|
||||
cap_disk):
|
||||
'''calculate the used vcpus, memory and disk based on VM flavors'''
|
||||
vms = self.model.get_mapping().get_node_vms(hypervisor)
|
||||
vcpus_used = 0
|
||||
memory_mb_used = 0
|
||||
disk_gb_used = 0
|
||||
for vm_id in vms:
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
vcpus_used += cap_cores.get_capacity(vm)
|
||||
memory_mb_used += cap_mem.get_capacity(vm)
|
||||
disk_gb_used += cap_disk.get_capacity(vm)
|
||||
|
||||
return vcpus_used, memory_mb_used, disk_gb_used
|
||||
|
||||
def choose_vm_to_migrate(self, hosts):
|
||||
"""pick up an active vm instance to migrate from provided hosts
|
||||
|
||||
:param hosts: the array of dict which contains hypervisor object
|
||||
"""
|
||||
vms_tobe_migrate = []
|
||||
for hvmap in hosts:
|
||||
source_hypervisor = hvmap['hv']
|
||||
source_vms = self.model.get_mapping().get_node_vms(
|
||||
source_hypervisor)
|
||||
if source_vms:
|
||||
inlet_t = self.ceilometer.statistic_aggregation(
|
||||
resource_id=source_hypervisor.uuid,
|
||||
meter_name=self.meter_name_inlet_t,
|
||||
period=self._period,
|
||||
aggregate='avg')
|
||||
power = self.ceilometer.statistic_aggregation(
|
||||
resource_id=source_hypervisor.uuid,
|
||||
meter_name=self.meter_name_power,
|
||||
period=self._period,
|
||||
aggregate='avg')
|
||||
if (power < self.threshold_power and
|
||||
inlet_t < self.threshold_inlet_t):
|
||||
# hardware issue, migrate all vms from this hypervisor
|
||||
for vm_id in source_vms:
|
||||
try:
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
vms_tobe_migrate.append(vm)
|
||||
except wexc.InstanceNotFound:
|
||||
LOG.error(_LE("VM not found Error: %s"), vm_id)
|
||||
return source_hypervisor, vms_tobe_migrate
|
||||
else:
|
||||
# migrate the first active vm
|
||||
for vm_id in source_vms:
|
||||
try:
|
||||
vm = self.model.get_vm_from_id(vm_id)
|
||||
if vm.state != vm_state.VMState.ACTIVE.value:
|
||||
LOG.info(_LE("VM not active, skipped: %s"),
|
||||
vm.uuid)
|
||||
continue
|
||||
vms_tobe_migrate.append(vm)
|
||||
return source_hypervisor, vms_tobe_migrate
|
||||
except wexc.InstanceNotFound:
|
||||
LOG.error(_LE("VM not found Error: %s"), vm_id)
|
||||
else:
|
||||
LOG.info(_LI("VM not found from hypervisor: %s"),
|
||||
source_hypervisor.uuid)
|
||||
|
||||
def filter_destination_hosts(self, hosts, vms_to_migrate):
|
||||
'''return vm and host with sufficient available resources'''
|
||||
|
||||
cap_cores = self.model.get_resource_from_id(
|
||||
resource.ResourceType.cpu_cores)
|
||||
cap_disk = self.model.get_resource_from_id(resource.ResourceType.disk)
|
||||
cap_mem = self.model.get_resource_from_id(
|
||||
resource.ResourceType.memory)
|
||||
# large vm go first
|
||||
vms_to_migrate = sorted(vms_to_migrate, reverse=True,
|
||||
key=lambda x: (cap_cores.get_capacity(x)))
|
||||
# find hosts for VMs
|
||||
destination_hosts = []
|
||||
for vm_to_migrate in vms_to_migrate:
|
||||
required_cores = cap_cores.get_capacity(vm_to_migrate)
|
||||
required_disk = cap_disk.get_capacity(vm_to_migrate)
|
||||
required_mem = cap_mem.get_capacity(vm_to_migrate)
|
||||
dest_migrate_info = {}
|
||||
for hvmap in hosts:
|
||||
host = hvmap['hv']
|
||||
if 'cores_used' not in hvmap:
|
||||
# calculate the available resources
|
||||
hvmap['cores_used'], hvmap['mem_used'],\
|
||||
hvmap['disk_used'] = self.calculate_used_resource(
|
||||
host, cap_cores, cap_mem, cap_disk)
|
||||
cores_available = (cap_cores.get_capacity(host) -
|
||||
hvmap['cores_used'])
|
||||
disk_available = (cap_disk.get_capacity(host) -
|
||||
hvmap['disk_used'])
|
||||
mem_available = cap_mem.get_capacity(host) - hvmap['mem_used']
|
||||
if (cores_available >= required_cores and
|
||||
disk_available >= required_disk and
|
||||
mem_available >= required_mem):
|
||||
dest_migrate_info['vm'] = vm_to_migrate
|
||||
dest_migrate_info['hv'] = host
|
||||
hvmap['cores_used'] += required_cores
|
||||
hvmap['mem_used'] += required_mem
|
||||
hvmap['disk_used'] += required_disk
|
||||
destination_hosts.append(dest_migrate_info)
|
||||
break
|
||||
# check if all vms have target hosts
|
||||
if len(destination_hosts) != len(vms_to_migrate):
|
||||
LOG.warning(_LW("Not all target hosts could be found, it might "
|
||||
"be because of there's no enough resource"))
|
||||
return None
|
||||
return destination_hosts
|
||||
|
||||
def group_hosts_by_airflow(self):
|
||||
"""Group hosts based on airflow meters"""
|
||||
|
||||
hypervisors = self.model.get_all_hypervisors()
|
||||
if not hypervisors:
|
||||
raise wexc.ClusterEmpty()
|
||||
overload_hosts = []
|
||||
nonoverload_hosts = []
|
||||
for hypervisor_id in hypervisors:
|
||||
hypervisor = self.model.get_hypervisor_from_id(hypervisor_id)
|
||||
resource_id = hypervisor.uuid
|
||||
airflow = self.ceilometer.statistic_aggregation(
|
||||
resource_id=resource_id,
|
||||
meter_name=self.meter_name_airflow,
|
||||
period=self._period,
|
||||
aggregate='avg')
|
||||
# some hosts may not have airflow meter, remove from target
|
||||
if airflow is None:
|
||||
LOG.warning(_LE("%s: no airflow data"), resource_id)
|
||||
continue
|
||||
|
||||
LOG.debug("%s: airflow %f" % (resource_id, airflow))
|
||||
hvmap = {'hv': hypervisor, 'airflow': airflow}
|
||||
if airflow >= self.threshold_airflow:
|
||||
# mark the hypervisor to release resources
|
||||
overload_hosts.append(hvmap)
|
||||
else:
|
||||
nonoverload_hosts.append(hvmap)
|
||||
return overload_hosts, nonoverload_hosts
|
||||
|
||||
def pre_execute(self):
|
||||
LOG.debug("Initializing Uniform Airflow Strategy")
|
||||
|
||||
if self.model is None:
|
||||
raise wexc.ClusterStateNotDefined()
|
||||
|
||||
def do_execute(self):
|
||||
src_hypervisors, target_hypervisors = (
|
||||
self.group_hosts_by_airflow())
|
||||
|
||||
if not src_hypervisors:
|
||||
LOG.debug("No hosts require optimization")
|
||||
return self.solution
|
||||
|
||||
if not target_hypervisors:
|
||||
LOG.warning(_LW("No hosts current have airflow under %s "
|
||||
", therefore there are no possible target "
|
||||
"hosts for any migration"),
|
||||
self.threshold_airflow)
|
||||
return self.solution
|
||||
|
||||
# migrate the vm from server with largest airflow first
|
||||
src_hypervisors = sorted(src_hypervisors,
|
||||
reverse=True,
|
||||
key=lambda x: (x["airflow"]))
|
||||
vms_to_migrate = self.choose_vm_to_migrate(src_hypervisors)
|
||||
if not vms_to_migrate:
|
||||
return self.solution
|
||||
source_hypervisor, vms_src = vms_to_migrate
|
||||
# sort host with airflow
|
||||
target_hypervisors = sorted(target_hypervisors,
|
||||
key=lambda x: (x["airflow"]))
|
||||
# find the hosts that have enough resource for the VM to be migrated
|
||||
destination_hosts = self.filter_destination_hosts(target_hypervisors,
|
||||
vms_src)
|
||||
if not destination_hosts:
|
||||
LOG.warning(_LW("No proper target host could be found, it might "
|
||||
"be because of there's no enough resource"))
|
||||
return self.solution
|
||||
# generate solution to migrate the vm to the dest server,
|
||||
for info in destination_hosts:
|
||||
vm_src = info['vm']
|
||||
mig_dst_hypervisor = info['hv']
|
||||
if self.model.get_mapping().migrate_vm(vm_src,
|
||||
source_hypervisor,
|
||||
mig_dst_hypervisor):
|
||||
parameters = {'migration_type': 'live',
|
||||
'src_hypervisor': source_hypervisor.uuid,
|
||||
'dst_hypervisor': mig_dst_hypervisor.uuid}
|
||||
self.solution.add_action(action_type=self.MIGRATION,
|
||||
resource_id=vm_src.uuid,
|
||||
input_parameters=parameters)
|
||||
|
||||
def post_execute(self):
|
||||
self.solution.model = self.model
|
||||
# TODO(v-francoise): Add the indicators to the solution
|
||||
@@ -50,7 +50,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
* Offload phase - handling over-utilized resources
|
||||
* Consolidation phase - handling under-utilized resources
|
||||
* Solution optimization - reducing number of migrations
|
||||
* Deactivation of unused hypervisors
|
||||
* Disability of unused hypervisors
|
||||
|
||||
A capacity coefficients (cc) might be used to adjust optimization
|
||||
thresholds. Different resources may require different coefficient
|
||||
@@ -84,7 +84,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/implemented/zhaw-load-consolidation.rst
|
||||
""" # noqa
|
||||
|
||||
def __init__(self, config=None, osc=None):
|
||||
def __init__(self, config, osc=None):
|
||||
super(VMWorkloadConsolidation, self).__init__(config, osc)
|
||||
self._ceilometer = None
|
||||
self.number_of_migrations = 0
|
||||
@@ -121,8 +121,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
"""
|
||||
if isinstance(state, six.string_types):
|
||||
return state
|
||||
elif (type(state) == hyper_state.HypervisorState or
|
||||
type(state) == vm_state.VMState):
|
||||
elif isinstance(state, (vm_state.VMState,
|
||||
hyper_state.HypervisorState)):
|
||||
return state.value
|
||||
else:
|
||||
LOG.error(_LE('Unexpexted resource state type, '
|
||||
@@ -131,26 +131,26 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
st=type(state))
|
||||
raise exception.WatcherException
|
||||
|
||||
def add_action_activate_hypervisor(self, hypervisor):
|
||||
"""Add an action for hypervisor activation into the solution.
|
||||
def add_action_enable_hypervisor(self, hypervisor):
|
||||
"""Add an action for hypervisor enabler into the solution.
|
||||
|
||||
:param hypervisor: hypervisor object
|
||||
:return: None
|
||||
"""
|
||||
params = {'state': hyper_state.HypervisorState.ONLINE.value}
|
||||
params = {'state': hyper_state.HypervisorState.ENABLED.value}
|
||||
self.solution.add_action(
|
||||
action_type='change_nova_service_state',
|
||||
resource_id=hypervisor.uuid,
|
||||
input_parameters=params)
|
||||
self.number_of_released_hypervisors -= 1
|
||||
|
||||
def add_action_deactivate_hypervisor(self, hypervisor):
|
||||
"""Add an action for hypervisor deactivation into the solution.
|
||||
def add_action_disable_hypervisor(self, hypervisor):
|
||||
"""Add an action for hypervisor disablity into the solution.
|
||||
|
||||
:param hypervisor: hypervisor object
|
||||
:return: None
|
||||
"""
|
||||
params = {'state': hyper_state.HypervisorState.OFFLINE.value}
|
||||
params = {'state': hyper_state.HypervisorState.DISABLED.value}
|
||||
self.solution.add_action(
|
||||
action_type='change_nova_service_state',
|
||||
resource_id=hypervisor.uuid,
|
||||
@@ -171,12 +171,10 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
|
||||
vm_state_str = self.get_state_str(vm.state)
|
||||
if vm_state_str != vm_state.VMState.ACTIVE.value:
|
||||
'''
|
||||
Watcher curently only supports live VM migration and block live
|
||||
VM migration which both requires migrated VM to be active.
|
||||
When supported, the cold migration may be used as a fallback
|
||||
migration mechanism to move non active VMs.
|
||||
'''
|
||||
# Watcher curently only supports live VM migration and block live
|
||||
# VM migration which both requires migrated VM to be active.
|
||||
# When supported, the cold migration may be used as a fallback
|
||||
# migration mechanism to move non active VMs.
|
||||
LOG.error(_LE('Cannot live migrate: vm_uuid=%(vm_uuid)s, '
|
||||
'state=%(vm_state)s.'),
|
||||
vm_uuid=vm_uuid,
|
||||
@@ -186,8 +184,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
migration_type = 'live'
|
||||
|
||||
dst_hyper_state_str = self.get_state_str(dst_hypervisor.state)
|
||||
if dst_hyper_state_str == hyper_state.HypervisorState.OFFLINE.value:
|
||||
self.add_action_activate_hypervisor(dst_hypervisor)
|
||||
if dst_hyper_state_str == hyper_state.HypervisorState.DISABLED.value:
|
||||
self.add_action_enable_hypervisor(dst_hypervisor)
|
||||
model.get_mapping().unmap(src_hypervisor, vm)
|
||||
model.get_mapping().map(dst_hypervisor, vm)
|
||||
|
||||
@@ -199,23 +197,25 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
input_parameters=params)
|
||||
self.number_of_migrations += 1
|
||||
|
||||
def deactivate_unused_hypervisors(self, model):
|
||||
"""Generate actions for deactivation of unused hypervisors.
|
||||
def disable_unused_hypervisors(self, model):
|
||||
"""Generate actions for disablity of unused hypervisors.
|
||||
|
||||
:param model: model_root object
|
||||
:return: None
|
||||
"""
|
||||
for hypervisor in model.get_all_hypervisors().values():
|
||||
if len(model.get_mapping().get_node_vms(hypervisor)) == 0:
|
||||
self.add_action_deactivate_hypervisor(hypervisor)
|
||||
if (len(model.get_mapping().get_node_vms(hypervisor)) == 0 and
|
||||
hypervisor.status !=
|
||||
hyper_state.HypervisorState.DISABLED.value):
|
||||
self.add_action_disable_hypervisor(hypervisor)
|
||||
|
||||
def get_prediction_model(self, model):
|
||||
def get_prediction_model(self):
|
||||
"""Return a deepcopy of a model representing current cluster state.
|
||||
|
||||
:param model: model_root object
|
||||
:return: model_root object
|
||||
"""
|
||||
return deepcopy(model)
|
||||
return deepcopy(self.model)
|
||||
|
||||
def get_vm_utilization(self, vm_uuid, model, period=3600, aggr='avg'):
|
||||
"""Collect cpu, ram and disk utilization statistics of a VM.
|
||||
@@ -339,7 +339,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
counters = {}
|
||||
for hypervisor in hypervisors:
|
||||
hyper_state_str = self.get_state_str(hypervisor.state)
|
||||
if hyper_state_str == hyper_state.HypervisorState.ONLINE.value:
|
||||
if hyper_state_str == hyper_state.HypervisorState.ENABLED.value:
|
||||
rhu = self.get_relative_hypervisor_utilization(
|
||||
hypervisor, model)
|
||||
for k in rhu.keys():
|
||||
@@ -437,12 +437,12 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
the least CPU utilized VM first as live migration these
|
||||
generaly causes less troubles. This phase results in a cluster
|
||||
with no overloaded hypervisors.
|
||||
* This phase is be able to activate turned off hypervisors (if needed
|
||||
* This phase is be able to enable disabled hypervisors (if needed
|
||||
and any available) in the case of the resource capacity provided by
|
||||
active hypervisors is not able to accomodate all the load.
|
||||
As the offload phase is later followed by the consolidation phase,
|
||||
the hypervisor activation in this phase doesn't necessarily results
|
||||
in more activated hypervisors in the final solution.
|
||||
the hypervisor enabler in this phase doesn't necessarily results
|
||||
in more enabled hypervisors in the final solution.
|
||||
|
||||
:param model: model_root object
|
||||
:param cc: dictionary containing resource capacity coefficients
|
||||
@@ -452,9 +452,11 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
key=lambda x: self.get_hypervisor_utilization(x, model)['cpu'])
|
||||
for hypervisor in reversed(sorted_hypervisors):
|
||||
if self.is_overloaded(hypervisor, model, cc):
|
||||
for vm in sorted(model.get_mapping().get_node_vms(hypervisor),
|
||||
key=lambda x: self.get_vm_utilization(
|
||||
x, model)['cpu']):
|
||||
for vm in sorted(
|
||||
model.get_mapping().get_node_vms(hypervisor),
|
||||
key=lambda x: self.get_vm_utilization(
|
||||
x, model)['cpu']
|
||||
):
|
||||
for dst_hypervisor in reversed(sorted_hypervisors):
|
||||
if self.vm_fits(vm, dst_hypervisor, model, cc):
|
||||
self.add_migration(vm, hypervisor,
|
||||
@@ -498,7 +500,11 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
dsc -= 1
|
||||
asc += 1
|
||||
|
||||
def execute(self, original_model):
|
||||
def pre_execute(self):
|
||||
if self.model is None:
|
||||
raise exception.ClusterStateNotDefined()
|
||||
|
||||
def do_execute(self):
|
||||
"""Execute strategy.
|
||||
|
||||
This strategy produces a solution resulting in more
|
||||
@@ -508,12 +514,12 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
* Offload phase - handling over-utilized resources
|
||||
* Consolidation phase - handling under-utilized resources
|
||||
* Solution optimization - reducing number of migrations
|
||||
* Deactivation of unused hypervisors
|
||||
* Disability of unused hypervisors
|
||||
|
||||
:param original_model: root_model object
|
||||
"""
|
||||
LOG.info(_LI('Executing Smart Strategy'))
|
||||
model = self.get_prediction_model(original_model)
|
||||
model = self.get_prediction_model()
|
||||
rcu = self.get_relative_cluster_utilization(model)
|
||||
self.ceilometer_vm_data_cache = dict()
|
||||
|
||||
@@ -528,8 +534,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
# Optimize solution
|
||||
self.optimize_solution(model)
|
||||
|
||||
# Deactivate unused hypervisors
|
||||
self.deactivate_unused_hypervisors(model)
|
||||
# disable unused hypervisors
|
||||
self.disable_unused_hypervisors(model)
|
||||
|
||||
rcu_after = self.get_relative_cluster_utilization(model)
|
||||
info = {
|
||||
@@ -542,7 +548,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
|
||||
LOG.debug(info)
|
||||
|
||||
self.solution.model = model
|
||||
self.solution.efficacy = rcu_after['cpu']
|
||||
|
||||
return self.solution
|
||||
def post_execute(self):
|
||||
# self.solution.efficacy = rcu_after['cpu']
|
||||
self.solution.set_efficacy_indicators(
|
||||
released_compute_nodes_count=self.number_of_migrations,
|
||||
vm_migrations_count=self.number_of_released_hypervisors,
|
||||
)
|
||||
|
||||