Compare commits

...

87 Commits

Author SHA1 Message Date
Jenkins
8635beb045 Merge "Fix unittest in test_api.py" 2016-07-13 13:21:27 +00:00
Jenkins
ff1af61c1b Merge "Update unitaty tests to verify strategy and goal name" 2016-07-13 12:54:45 +00:00
Jenkins
00adcb84f6 Merge "Optimize local.conf.controller file to enable watche-dashboard" 2016-07-13 12:45:30 +00:00
David TARDIVEL
a68eb0d619 Update unitaty tests to verify strategy and goal name
strategy and goal name must be synched with entry point names.

Change-Id: I776ed98f5c13d176fdb0b8677d609fcb1ec9a235
Closes-Bug: #1602262
2016-07-12 16:54:38 +02:00
David TARDIVEL
517f1d083e Bad goal and strategy name for Airflow Optimization
The goal name 'airflow optimization' must be synched with the
entry point name.
The strategy name 'uniform airflow' must be synched with the
entry point name.

Partial-Bug: #1602262

Change-Id: Ibc6f0a7c5047e57549910818200c323b878152b0
2016-07-12 15:45:59 +02:00
licanwei
76a61e234a Fix unittest in test_api.py
tox -e py27 fails with this error:
socket.error: [Error 98] Address already in use

Change-Id: I1cf4a000fb82228bed6a7da61feed48ab679b891
Closes-Bug: #1599556
2016-07-12 17:02:43 +08:00
Jenkins
f4b631ebb3 Merge "Add new documentation section for Watcher policies rules" 2016-07-12 08:14:33 +00:00
Jenkins
e78610e7d1 Merge "Make default Planner generic to handle new action" 2016-07-12 07:35:21 +00:00
Jenkins
405cde2ff9 Merge "Uniform Airflow migration strategy implementation" 2016-07-11 07:56:26 +00:00
Jenkins
b25ecde0db Merge "Add continuously optimization" 2016-07-08 14:07:51 +00:00
Jenkins
e6b4befbf4 Merge "Fix watcher doc build error" 2016-07-08 14:07:39 +00:00
zte-hanrong
dcf52077a4 Optimize local.conf.controller file to enable watche-dashboard
Change-Id: I51cfca4d55453710685624d5cf16a92271947b6a
2016-07-08 10:06:10 +08:00
Alexander Chadin
1de00086f5 Add continuously optimization
This patch set adds implementation for CONTINUOUS type
of audit.

Change-Id: I5f4ec97b2082c8a6b3ccebe36b2a343fa4a67d19
Implements: blueprint continuously-optimization
2016-07-07 19:17:17 +03:00
Jenkins
518b4c82f1 Merge "Documentation for strategy parameters" 2016-07-07 15:10:02 +00:00
Jenkins
4423fdd231 Merge "Add Desktop Service Store to .gitignore file" 2016-07-07 15:09:53 +00:00
Jenkins
b1c6ea71a4 Merge "Fix field type to audit_type" 2016-07-07 08:57:44 +00:00
Alexander Chadin
533a4c7a34 Add Desktop Service Store to .gitignore file
This patch set adds Desktop Service Store files to .gitignore to
ignore them.

Change-Id: Iff3221eea6b0b3f51dd32097f762252b4a4ffa2d
2016-07-07 11:05:56 +03:00
Jenkins
d61c4c12d1 Merge "Fix failing Tempest tests" 2016-07-07 06:58:13 +00:00
Edwin Zhai
1d3891e1e4 Documentation for strategy parameters
Add docs of how to define and input parameters of strategy.

Change-Id: I918305b89721d141b7f37630459e6cf5999f4211
Partially-Implements: optimization-threshold
2016-07-07 00:03:43 +00:00
OpenStack Proposal Bot
f13d6dd805 Updated from global requirements
Change-Id: Ie17905661161bb34b9da611201e447117cb04544
2016-07-06 16:42:29 +00:00
Jenkins
81bdc51e21 Merge "Add policies for API access control to watcher project." 2016-07-06 16:17:43 +00:00
David TARDIVEL
e7bbf1d1b9 Add new documentation section for Watcher policies rules
We can now use policies to allow or not users to invoke Watcher API
methods. I added, in the Admin guide section, a new documentation
introducing watcher policy rules.

Change-Id: Ie1f8a9e15561756f23242c6f6b0d53cdacf3505a
blueprint: watcher-policies
2016-07-06 15:42:12 +00:00
Jenkins
e34198f35c Merge "Fix link error in base-setup.rst" 2016-07-06 14:51:17 +00:00
zte-hanrong
bc06a7d419 Add policies for API access control to watcher project.
Change-Id: Ibdbe494c636dfaeca9cf2ef8724d0dade1f19c7f
blueprint: watcher-policies
2016-07-06 17:38:44 +08:00
Edwin Zhai
1a1d9d09f4 Fix watcher doc build error
Updates the module name to reflect the right defination of efficacy
specification, and adds doc string to some class.

Change-Id: If54e65a5e98521d74d8e19a21dd900ecd37edfe5
Closes-Bug: #1599368
2016-07-06 06:06:00 +00:00
digambar
47ff9bb105 Fix field type to audit_type
There's a number of places where a field of an object is named "type",
which isn't good thing. Actually type is keyword in python, so wherever
I found type used in models/api/object/test, I have fixed it to
audit_type in the audit files

Change-Id: Iea6bd3acb0b2af2a833b3916701aad88f6064bba
Closes-Bug: #1533392
2016-07-06 06:26:15 +05:30
jinquanni
ca5b849f6b Remove duplicate unittest
There are two same unit test in test_default_planner.
We need to remove one.

TrivialFix

Change-Id: Ibeec9348779dafe4c2cec2411306e09e95728ca1
2016-07-05 14:57:47 +08:00
licanwei
2272b515bf Fix link error in base-setup.rst
line 93:An ref:'action plugin <implement_strategy_plugin>
here implement_strategy_plugin should be 'implement_action_plugin'

Change-Id: I3487578b91bd169335ccf336443e6174905728b0
Closes-Bug: #1599029
2016-07-05 14:44:01 +08:00
Vincent Françoise
f6b1b515c4 Fix failing Tempest tests
In this changeset, I fixed the 3 failing tests on the Tempest test
suite.

Change-Id: I3d31ea1c13b340a2a16b41c31198d7be4711400c
Closes-Bug: #1595534
2016-07-04 16:18:59 +02:00
Jenkins
64903ce56c Merge "Enable strategy parameters" 2016-07-04 08:57:54 +00:00
Jenkins
86cbe4bd0c Merge "Added filter operators" 2016-07-04 07:10:50 +00:00
Edwin Zhai
0b29a8394b Enable strategy parameters
Strategy provides parameters to customize algorithm behavior. End user
could query then specify parameters for their requirements.

Change-Id: Id097db5f6e79c94b57674c8e5d55b06098abf18c
Implements-bp: optimization-threshold
2016-07-01 03:01:41 +00:00
Jenkins
ec64203bee Merge "Update Docs links to docs.openstack.org" 2016-06-30 14:36:07 +00:00
Jenkins
dc01e35760 Merge "add dependency for 3rd-party plugins" 2016-06-30 07:18:13 +00:00
Palimariu Marius
117079f81e Update Docs links to docs.openstack.org
This patch updates the wrong documentation links with the new location.

Change-Id: I42b86a225ebb4b4610adf0f7f8c3286d8b24ed04
Closes-Bug: #1595180
2016-06-29 19:46:11 +00:00
licanwei
d03e1d54bd add dependency for 3rd-party plugins
In the base-setup.rst about 3rd-party plugins,
we should add the watcher dependency requirement.

Change-Id: I8f898b2e3e7e30c88f4f14f7bd3ae38b8aa3ebd3
Closes-Bug: #1594705
2016-06-29 17:44:46 +08:00
Jenkins
6bdc4cac82 Merge "Add goal_name field in strategy" 2016-06-28 15:31:00 +00:00
jinquanni
b4c5e2bb81 Make default Planner generic to handle new action
Currently we have to amend the code of the Planner
for each new added Watcher Action. This patch set will
modifying the DefaultPlanner class and  leverage the
plugins-parameters blueprint so that we can configure
weights via the configuration file.

Documentation and unittest also be update in this patch.

Co-Authored-By: Vincent Françoise <vincent.francoise@b-com.com>

Change-Id: Ib794488fcafd1e153a7d7b1f7253686136501872
blueprint: configurable-weights-default-planner
2016-06-28 07:15:36 +08:00
jinquanni
369428c313 Modify IRC weekly meeting time
modify IRC weekly meeting time in contributor.rst

Change-Id: I1e46bbf23e301559e2a92fce387c2d0f578eb814
2016-06-27 20:34:37 +08:00
junjie huang
2381a677b6 Uniform Airflow migration strategy implementation
This is one of the algorithm of Intel thermal POC.
It's based on the Airflow of hypervisors.

Change-Id: I48bbab75b20129d85b1fb15ede823474546fe399
blueprint: uniform-airflow-migration-strategy
2016-06-27 04:46:18 +00:00
OpenStack Proposal Bot
da8b24f483 Updated from global requirements
Change-Id: Ie49cb8d5fe7cee5592e6045a0145a6a7299c614b
2016-06-24 03:19:28 +00:00
Jenkins
d7a7b3ea54 Merge "Add importing modules instead of classes" 2016-06-23 15:39:08 +00:00
Tin Lam
37595e9bb7 Centralize plugin loaders in watcher/applier
Refactor all plugin loaders in watcher/applier into
watcher/applier/loading/default.py.

Change-Id: I0f0a87d4f72ead45d34aca1b14219fd2ede42a6f
Closes-Bug: #1591095
2016-06-23 02:50:55 -05:00
Alexandr Stavitskiy
cb693e4093 Add importing modules instead of classes
This patch removes import of classes and replaces them with import of modules.

Change-Id: Id2502cf96ac7227cf1036cd54a74f3d7acd83479
Closes-Bug: #1594434
2016-06-22 13:32:34 +03:00
Tin Lam
9918f59227 Centralize plugin loaders in decision engine
Refactor planner loader in watcher/decision_engine/planner/loading/default.py
to watcher/decision_engine/loading/default.py.

Change-Id: I3d4f3668d2269b5a77a35f4470a4d1c96c3128dd
Closes-Bug: #1591092
2016-06-21 23:37:38 -05:00
Vladimir Ostroverkhov
07c7ba9b2e Add goal_name field in strategy
This patch set adds goal_name field in strategy
Related-Bug: #1590416.

Change-Id: Ia381dd882b244487f8beec4d04dabddd1ac7dbc7
2016-06-21 22:16:27 +03:00
OpenStack Proposal Bot
73cf5a98ae Updated from global requirements
Change-Id: I9afbb5b257259f5110471972f98144ed6cb3f621
2016-06-21 18:07:32 +00:00
jinquanni
80abcd6fd9 Use disabled/enabled to change service state
The VM workload consolidation strategy sends 'down' instead
of 'disabled' to change nova-compute service state. This patch
will correct it. The same applies for enabling nova-compute service.

Change-Id: I257411ef711b215bd9b56d0bf0479c79a9ef61d8
Closes-Bug: #1591901
2016-06-21 18:25:14 +08:00
Jenkins
e3edc67045 Merge "Check if nova-service is already disabled" 2016-06-21 08:40:17 +00:00
Bruno Grazioli
b3c2d3af1f Check if nova-service is already disabled
This patch updates VM workload consolidation strategy adding a new
condition to check whether nova-compute service is disabled before
creating the action.

Change-Id: I1accbc7bbd62684dce14ca3b35c92121e923a73a
Closes-Bug: #1591927
2016-06-21 09:54:36 +02:00
Tin Lam
4fc3307525 Add bandit in tox -e pep8
Add bandit tox environment and amend pep8 env to run it.
Also, fix bandit errors with "0.0.0.0".

Change-Id: Ieb5785abd945663e07c07f0ddd3d9a074004f46a
Closes-Bug: #1594423
2016-06-20 19:03:02 -05:00
Vincent Françoise
77c07a466f Added filter operators
In this changeset, I refactored the DB filter system to support
comparison operators using a django-like syntax.
A filter can take 2 forms:

- "<FIELDNAME>" which is a syntactic sugar for "<FIELDNAME>__eq"
- "<FIELDNAME>__<OPERATOR>" where <OPERATOR> is the comparison operator
  to be used.

Here is the list of the supported operators:

- 'eq' (==)
- 'neq' (!=)
- 'gt' (>)
- 'gte' (>=)
- 'lt' (<)
- 'lte' (<=)
- 'in' (in)
- 'notin' (not in)

Change-Id: I53a61d50a3253342a40f0ff87cb5612ed57a3bd1
2016-06-17 10:37:44 +02:00
Jenkins
80867703ba Merge "Added audit_template filter to /audits/detail" 2016-06-17 07:44:53 +00:00
Jean-Emile DARTOIS
7b403c0d3b Fix StrategyContext to use the strategy_id in the Audit Template
This patch fixes the StrategyContext to use the optional
attribute strategy_id.

Change-Id: Ib78581f564282de6cfc7f07495c846615ec1866a
Closed-bug: #1590357
2016-06-15 13:08:28 +02:00
Jenkins
6f2c82316c Merge "Use proper theme for release notes" 2016-06-15 09:05:11 +00:00
Jenkins
b21193aaf2 Merge "Documentation on goal and efficacy" 2016-06-15 09:00:09 +00:00
Jenkins
e1a1cd79f4 Merge "Added efficacy indicators to /action_plans" 2016-06-15 09:00:03 +00:00
Jenkins
019fc498ae Merge "Added pre/post execution methods to strategies" 2016-06-15 08:59:58 +00:00
Jenkins
407a505e73 Merge "Added EfficacyIndicator object" 2016-06-15 08:59:52 +00:00
Jenkins
488c6e3f4e Merge "Added efficacy specification to /goals" 2016-06-15 08:59:46 +00:00
Jenkins
28a2422ceb Merge "Added EfficacyIndicator model in DB" 2016-06-15 08:59:41 +00:00
Jenkins
58b5baaeae Merge "Decoupled Goal from Strategy" 2016-06-15 08:59:38 +00:00
Andreas Jaeger
e8fc4a1bc8 Use proper theme for release notes
Use oslosphinx as theme - watcher is an official project and thus
should use it.

Change-Id: I0ab1ad3e99a92e6b59a4bcb29f216b4af6cbac6a
2016-06-15 10:12:29 +02:00
Andreas Jaeger
921e531adf Fix releasenotes generation
Fix several problems, tox -e releasenotes did not run at all.

Fixes:
* Fix version import so that release note building works.
* There's no stable/mitaka branch for watcher, so remove the mitaka file,
  otherwise release notes build will fail.
* Add _static directory with empty placeholder for sphinx to run.

Change-Id: Ia4dd2f7a7743dfd7f7022be04552c401ade9953f
2016-06-15 09:38:48 +02:00
Jenkins
b5cb6631cc Merge "Add reno for release notes management" 2016-06-13 01:07:10 +00:00
Vincent Françoise
45801cf9c5 Documentation on goal and efficacy
In this changeset, I wrote a documentation detailing how one can
implement a new goal plugin. I also mention to define the efficacy
specification for a given goal.

Partially Implements: blueprint efficacy-indicator

Change-Id: Iba267ae312f248b49d4600504f11678cdc225622
2016-06-10 17:08:28 +02:00
Vincent Françoise
442512cd71 Added efficacy indicators to /action_plans
I this changeset, I added the efficacy indicators both at the DB
and at the API level alongside the associated logic.

Partially Implements: blueprint efficacy-indicator

Change-Id: I824553637621da67966103c1b0c01348b09bd836
2016-06-10 09:37:14 +02:00
Vincent Françoise
2b95a4cbc4 Added pre/post execution methods to strategies
In this changeset, I broke down the execute() method to sequentially
call 3 methods:

- pre_execute()
- do_execute()
- post_execute()

This changeset also removes the cluster model parameter from the
execute() method to now become a `model` property of a strategy which
is lazy loaded whenever needed.

Partially Implements: blueprint efficacy-indicator

Change-Id: I2f697938db693acfa95b2c2fbecfdc1b733c93fd
2016-06-10 09:37:11 +02:00
Vincent Françoise
84b12f8f1e Added EfficacyIndicator object
In this changeset, I created the Watcher object for the efficacy
indicators as well as the API object which will be embedded into the
/action_plans endpoint.

Partially Implements: blueprint efficacy-indicator

Change-Id: Ibbe47613317d51a3829fe9de12540c048e8d7117
2016-06-10 09:37:08 +02:00
Vincent Françoise
f665d83657 Added efficacy specification to /goals
In this changeset, I added the new Efficacy, EfficacySpecification
and IndicatorSpecification classes which are the main components for
computing an efficacy.

Partially Implements: blueprint efficacy-indicator

Change-Id: I3a1d62569de2dd6bb6f9a52f6058313fa2b886ce
2016-06-10 09:36:59 +02:00
Antoine Cabot
5e16e032be Add reno for release notes management
Change-Id: I81337ca27fa639da74cb34a874fbbc6248026c34
2016-06-09 11:01:20 +02:00
Vincent Françoise
eab47bf182 Added EfficacyIndicator model in DB
In this changeset, I created a new model named EfficacyIndicator
which is responsible for storing information regarding the efficacy
indicators that were computed by the strategy in its solution. Every
efficacy indicator should relate to a single Action Plan.

Partially Implements: blueprint efficacy-indicator

Change-Id: Ifc14ea5e16e92f032d7912c9b3fdc270af79cab6
2016-06-08 14:05:01 +02:00
Vincent Françoise
2544327979 Decoupled Goal from Strategy
In this changeset, I decoupled the notion of Goal from the Strategy
by making it a distinct object. Goals are plugins that can be loaded
just like for the strategies.

Partially Implements: blueprint efficacy-indicator

Change-Id: I4378dccd508170b305aa968843228bbc8af78895
2016-06-08 14:00:43 +02:00
Antoine Cabot
2412df4b6c Fix broken link in doc
Change-Id: I4144a9b9d4eec4445e21749a8e71597bb1ad375e
2016-06-08 09:21:52 +02:00
Jenkins
d811dd93ae Merge "Added missing config section for autogeneration" 2016-06-07 13:34:51 +00:00
Vincent Françoise
cb342b45b7 Added missing config section for autogeneration
In this changeset, I added the missing entries for the configuration
sample file generation:

- oslo.reports
- oslo.cache
- oslo.concurrency
- oslo.policy
- oslo.service.periodic_task
- oslo.service.service
- oslo.service.wsgi

Change-Id: I826f5cc1185d75f545c96242022a07089cf3042e
2016-06-06 17:47:13 +02:00
OpenStack Proposal Bot
1a4d5a3df3 Updated from global requirements
Change-Id: I65fc08c423a43aaf5b80d5728038b91c8821da42
2016-06-03 18:21:04 +00:00
Vincent Françoise
8b8d2f01fe Added audit_template filter to /audits/detail
In this changeset, I added the 'audit_template' filter to the
/audits/detail endpoint so it has the same set of parameters as
the /audits endpoint.

Change-Id: I2568be18854bd97d4b7879532cb36a7cac532719
2016-06-03 15:05:59 +02:00
Alexander Chadin
ca37358cac Add fix for hardware.cpu.util meter in sd-strategy
This patch set removes normalizing for hardware.cpu.util meter
since the values that comes from ceilometer hardware.cpu.util
are already normalized.

Closes-Bug: #1588257

Change-Id: I9494f2cc9bbaa6dfd168fb515f679eb6d7f2398a
2016-06-02 13:12:48 +03:00
Jenkins
46091385d8 Merge "Add fix for __init__() error" 2016-06-02 08:46:55 +00:00
Jenkins
a457707e56 Merge "Replace assertEqual(None, *) with assertIsNone in tests" 2016-06-02 07:59:39 +00:00
Jenkins
6c922b43ea Merge "Updated tempest test creds retrieval mechanism" 2016-06-02 07:53:11 +00:00
Alexander Chadin
5d66f66050 Add fix for __init__() error
This patch set fixes __init__() got multiple values
for keyword argument osc error.

Closes-Bug: #1587824

Change-Id: Ieaa1250774ec0fdab5450fe9c3962bb3d1f4136b
2016-06-02 10:48:28 +03:00
Jenkins
e7e50b3955 Merge "Remove direct access to dbapi" 2016-06-02 03:10:31 +00:00
Vincent Françoise
d9a2b8809b Updated tempest test creds retrieval mechanism
Following https://review.openstack.org/#/c/263376/, I update the
way Watcher tempest test retrieve the admin credentials.

Change-Id: I9633752ff69a27edf8912227ea2fd24f358dec7e
2016-05-30 11:45:10 +02:00
sharat.sharma
164a802718 Replace assertEqual(None, *) with assertIsNone in tests
Replace assertEqual(None, *) with assertIsNone in tests to have
more clear messages in case of failure.

Change-Id: I98261ef7cca06447ea9d443a2c287c046f380f77
Closes-Bug: #1280522
2016-05-23 18:05:19 +05:30
Tin Lam
dbdf690cd0 Remove direct access to dbapi
In line [1], we directly use the dbapi in order to
filter the actions by action_plan_id.

[1] https://github.com/openstack/watcher/blob/master/watcher/applier/default.py#L63

Change-Id: I2c75528f619a5277d46702cc3b4ca4bc7280f990
Closes-Bug: #1534170
2016-05-17 18:27:41 -05:00
185 changed files with 7801 additions and 2335 deletions

7
.gitignore vendored
View File

@@ -64,3 +64,10 @@ sftp-config.json
cover
/demo/
# Files created by releasenotes build
releasenotes/build
# Desktop Service Store
*.DS_Store

View File

@@ -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/

View File

@@ -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

View File

@@ -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:

View File

@@ -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
--------------------------------

View 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.

View File

@@ -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

View File

@@ -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/

View File

@@ -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
=============

View File

@@ -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
==============================

View File

@@ -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

View 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`.

View File

@@ -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.

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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
View 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'

View File

@@ -0,0 +1,10 @@
Welcome to watcher's Release Notes documentation!
=================================================
Contents:
.. toctree::
:maxdepth: 1
unreleased.rst

View File

@@ -0,0 +1,5 @@
==============================
Current Series Release Notes
==============================
.. release-notes::

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -167,4 +167,5 @@ class Controller(rest.RestController):
# the request object to make the links.
return V1.convert()
__all__ = (Controller)
__all__ = ("Controller", )

View File

@@ -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)

View File

@@ -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(

View File

@@ -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()

View File

@@ -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()

View 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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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__)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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:

View File

@@ -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)

View File

@@ -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):

View File

@@ -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.")

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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`
"""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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", )

View 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()

View 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())

View 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)

View 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,
)

View 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()

View 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')

View File

@@ -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)

View File

@@ -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"

View File

@@ -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):

View File

@@ -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'

View File

@@ -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

View File

@@ -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)

View File

@@ -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'

View File

@@ -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):

View File

@@ -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'

View File

@@ -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",

View File

@@ -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__)

View File

@@ -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'

View File

@@ -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.

View 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)

View File

@@ -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()

View File

@@ -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__)

View File

@@ -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")

View File

@@ -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"

View File

@@ -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,
)

View File

@@ -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"
},
},
}

View File

@@ -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

View 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

View File

@@ -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,
)

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