Compare commits

...

168 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
Jenkins
3d398b4d22 Merge "Documentation for plugins-parameters" 2016-05-30 09:41:53 +00:00
Jenkins
585fbeb9ee Merge "Watcher plugins table in Guru meditation reports" 2016-05-30 09:41:49 +00:00
Jenkins
e9f237dc80 Merge "Enabled config parameters to plugins" 2016-05-30 09:40:30 +00:00
Vincent Françoise
38f6700144 Documentation for plugins-parameters
In this changeset, I updated the documentation to explain how to
add configuration options for each type of plugin.

Partially Implements: plugins-parameters

Change-Id: Ifd373da64207110492b4a62f1cb7f13b029a45d2
2016-05-30 10:28:29 +02:00
junjie huang
30bdf29002 Workload balance migration strategy implementation
This is one of the algorithm of Intel thermal POC.
It's based on the VM workloads of hypervisors.

Change-Id: I45ab0cf0f05786e6f68025bdd315f38381900a68
blueprint: workload-balance-migration-strategy
2016-05-30 07:52:05 +00:00
Vincent Françoise
e6f147d81d Watcher plugins table in Guru meditation reports
In this changeset, I added the list of all the available plugins
for the current instance of any given Watcher service.

Partially Implements: blueprint plugins-parameters

Change-Id: I58c9724a229712b0322a578f0f89a61b38dfd80a
2016-05-30 09:48:37 +02:00
Vincent Françoise
5aa6b16238 Enabled config parameters to plugins
In this changeset, I added the possibility for all plugins to define
configuration parameters for themselves.

Partially Implements: blueprint plugins-parameters

Change-Id: I676b2583b3b4841c64c862b2b0c234b4eb5fd0fd
2016-05-30 09:48:34 +02:00
Jenkins
dcb5c1f9fc Merge "Add Overload standard deviation strategy" 2016-05-30 07:10:17 +00:00
Jenkins
4ba01cbbcf Merge "Update Watcher documentation" 2016-05-27 15:58:01 +00:00
Jenkins
d91d72d2c2 Merge "Add goal name as filter for strategy list cmd" 2016-05-27 15:51:22 +00:00
Jenkins
083bc2bed4 Merge "Add goal_name & strategy_name in /audit_templates" 2016-05-27 15:51:14 +00:00
Alexander Chadin
9d3671af37 Add Overload standard deviation strategy
The main purpose of this strategy is to choose the pair VM:dest_host that
minimizes the standard deviation in a cluster best.

Change-Id: I95a31b7bcab83411ef6b6e1e01818ca21ef96883
Implements: blueprint watcher-overload-sd
2016-05-27 16:16:36 +03:00
Jenkins
3b88e37680 Merge "Added cold VM migration support" 2016-05-27 12:33:46 +00:00
David TARDIVEL
6eee64502f Add goal name as filter for strategy list cmd
This changeset add the possibility to use the goal name as a
stragegy list filter.

Change-Id: Ibaf45e694f115308f19e9bcd3023fe2e6d1750cd
2016-05-27 11:20:55 +02:00
David TARDIVEL
b9231f65cc Update Watcher documentation
We introduced a new watcher plugin for OpenStack CLI. This patchset
updates accordingly the watcher documentation and schemas.

Partially Implements: blueprint openstackclient-plugin

Change-Id: Ib00469c8645fff21f5ba95951379827dbd359c69
2016-05-27 09:40:06 +02:00
OpenStack Proposal Bot
b9b505a518 Updated from global requirements
Change-Id: Ia90ce890fac40ddb6d38effd022ca71e9a7fc52f
2016-05-26 17:07:34 +00:00
cima
388ef9f11c Added cold VM migration support
Cold migration enables migrating some of the VMs which are not in active state (e.g. stopped). Cold migration can also be used for migrating active VM, although VM is shut down and hence unaccessible while migrating.

Change-Id: I89ad0a04d41282431c9773f6ae7feb41573368e3
Closes-Bug: #1564297
2016-05-24 13:26:45 +02:00
Jenkins
4e3caaa157 Merge "Fixed flaky tempest test" 2016-05-24 09:25:15 +00:00
Vincent Françoise
8c6bf734af Add goal_name & strategy_name in /audit_templates
In this changeset, I added both the 'goal_name' and the 'strategy_name'
field.

Change-Id: Ic164df84d4e23ec75b2b2f4b358cf827d0ad7fa5
Related-Bug: #1573582
2016-05-24 11:09:48 +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
Vincent Françoise
277a749ca0 Fix lazy translation issue with watcher-db-manage
In this changeset, I fix the issue caused by the use of lazy
translations within the 'watcher-db-manage purge' subcommand.
This is caused by the PrettyTable dependency which performs
addition operations to format its tables and the __add__ magic
method is not supported by oslo_i18n._message.Message objects.

Change-Id: Idd590e882c697957cfaf1849c3d51b52797230f6
Closes-Bug: #1584652
2016-05-23 14:35:17 +02:00
Vincent Françoise
8401b5e479 Fixed flaky tempest test
In this changeset, I fixed the test_create_audit_with_no_state
tempest test which was randomly failing because of a race condition.

Change-Id: Ibda49944c79fcd406fa81870dbbff6064b5dc4fa
2016-05-23 14:32:44 +02:00
Vincent Françoise
78689fbe3b Removed telemetry tag from tempest tests
Since telemetry was removed from tempest, this changeset removes the
telemetry tags from the watcher integration tests

Change-Id: I6229ee23740c3d92a66fc04c8de8b0ed25911022
2016-05-23 09:22:31 +00:00
OpenStack Proposal Bot
22abaa9c3a Updated from global requirements
Change-Id: I2506d35432748691fb53f8540aac43d1656a67a3
2016-05-21 15:53:55 +00:00
Alexander Chadin
fb82131d85 Fix for statistic_aggregation
This patch set fixes aggregate parameter for statistic_aggregation
function so we may use it with any aggregate function.

Change-Id: If586d656aadd3d098a1610a97a2f315e70351de5
Closes-Bug: #1583610
2016-05-19 16:41:07 +03:00
Jenkins
f6f5079adb Merge "Watcher DB class diagram" 2016-05-18 12:36:17 +00:00
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
Jenkins
f045f5d816 Merge "Updated from global requirements" 2016-05-13 07:06:35 +00:00
Jenkins
b77541deb2 Merge "[nova_helper] get keypair name by every admin users" 2016-05-13 06:33:42 +00:00
OpenStack Proposal Bot
3b9d72439c Updated from global requirements
Change-Id: I0f4fe97bdfa872074964a10535db868354d926da
2016-05-11 17:29:55 +00:00
Jenkins
89aa2d54df Merge "Added .pot file" 2016-05-11 15:39:13 +00:00
Jenkins
86f4cee588 Merge "Remove [watcher_goals] config section" 2016-05-11 15:32:36 +00:00
Jenkins
04ac509821 Merge "Remove watcher_goals section from devstack plugin" 2016-05-11 15:32:35 +00:00
Jenkins
4ba9d2cb73 Merge "Documentation update for get-goal-from-strategy" 2016-05-11 15:32:28 +00:00
Jenkins
a71c9be860 Merge "Updated purge to now include goals and strategies" 2016-05-11 15:32:22 +00:00
Jenkins
c2cb1a1f8e Merge "Syncer now syncs stale audit templates" 2016-05-11 15:32:17 +00:00
Jenkins
79bdcf7baf Merge "Add strategy_id & goal_id fields in audit template" 2016-05-11 15:32:10 +00:00
Jenkins
de1b1a9938 Merge "Refactored Strategy selector to select from DB" 2016-05-11 15:32:07 +00:00
Jenkins
031ebdecde Merge "Added /strategies endpoint in Watcher API" 2016-05-11 15:32:01 +00:00
Jenkins
daabe671c7 Merge "Add Goal in BaseStrategy + Goal API reads from DB" 2016-05-11 15:31:58 +00:00
Jenkins
26bc3d139d Merge "DB sync for Strategies" 2016-05-11 15:31:39 +00:00
Jenkins
4d2536b9b2 Merge "Added Strategy model" 2016-05-11 15:30:46 +00:00
Jenkins
4388780e66 Merge "Added Goal object + goal syncing" 2016-05-11 15:29:21 +00:00
Jenkins
d03a9197b0 Merge "Added Goal model into Watcher DB" 2016-05-11 15:28:28 +00:00
Jean-Emile DARTOIS
43f5ab18ba Fix documentation watcher sql database
This changeset fixes the issue with the parameter watcher-db-manage

Change-Id: I668edd85e3ea40c2a309caacbf68cf35bfd680f7
Closes-Bug: #1580617
2016-05-11 15:58:46 +02:00
Vincent Françoise
209176c3d7 Watcher DB class diagram
In this changeset, I added a class diagram reprensenting the
database schema of Watcher.

Change-Id: I2257010d0040a3f40279ec9db2967f0e69384b62
2016-05-11 15:52:54 +02:00
Vincent Françoise
1a21867735 Added .pot file
In this changeset, I just generate the .pot file for all the new
translations that were added during the implementation of this BP

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I2192508afda037510f8f91092c5cfde0115dae1d
2016-05-11 15:48:09 +02:00
Vincent Françoise
5f6a97148f Remove [watcher_goals] config section
In this changeset, I remove the now unused [watcher_goals] section.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I91e4e1ac3a58bb6f3e30b11449cf1a6eb18cd0ca
2016-05-11 15:48:09 +02:00
Vincent Françoise
e6b23a0856 Remove watcher_goals section from devstack plugin
In this changeset, I removed the now useless [watcher_goals] section
from the devstack plugin.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Iaa986f426dc47f6cbd04e74f16b67670e3563967
2016-05-11 15:48:09 +02:00
Vincent Françoise
f9a1b9d3ce Documentation update for get-goal-from-strategy
In this changeset, I updated the Watcher documentation to reflect
the changes that are introduced by this blueprint.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I40be39624097365220bf7d94cbe177bbf5bbe0ed
2016-05-11 15:48:02 +02:00
Vincent Françoise
ff611544fb Updated purge to now include goals and strategies
In this changeset, I updated the purge script to now take into
account the registered goals and strategies.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I2f1d58bb812fa45bc4bc6467760a071d8612e6a4
2016-05-11 15:31:02 +02:00
Vincent Françoise
18e5c7d844 Syncer now syncs stale audit templates
In this changeset, I introduce the syncing of audit templates.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ie394c12fe51f73eff95465fd5140d82ebd212599
2016-05-11 15:31:02 +02:00
Vincent Françoise
2966b93777 Add strategy_id & goal_id fields in audit template
In this changeset, I updated the 'goal_id' field into the AuditTemplate
to now become a mandatory foreign key towards the Goal model. I also
added the 'strategy_id' field into the AuditTemplate model to be an
optional foreign key onto the Strategy model.

This changeset also includes an update of the /audit_template
Watcher API endpoint to reflect the previous changes.

As this changeset changes the API, this should be merged alongside the
related changeset from python-watcherclient.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ic0573d036d1bbd7820f8eb963e47912d6b3ed1a9
2016-05-11 15:31:02 +02:00
Vincent Françoise
e67b532110 Refactored Strategy selector to select from DB
In this changeset, I refactored the strategy selector to now
look into the Watcher DB instead of looking into the configuration
file.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I2bcb63542f6237f26796a3e5a781c8b62820cf6f
2016-05-11 15:31:01 +02:00
Vincent Françoise
81765b9aa5 Added /strategies endpoint in Watcher API
In this changeset, I added the /strategies endpoint to the Watcher
API service.
This also includes the related Tempest tests.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I1b70836e0df2082ab0016ecc207e89fdcb0fc8b9
2016-05-11 15:31:01 +02:00
Vincent Françoise
673642e436 Add Goal in BaseStrategy + Goal API reads from DB
In this changeset, I changed the Strategy base class to add new
abstract class methods. I also added an abstract strategy class
per Goal type (dummy, server consolidation, thermal optimization).

This changeset also includes an update of the /goals Watcher API
endpoint to now use the new Goal model (DB entries) instead of
reading from the configuration file.

Partially Implements: blueprint get-goal-from-strategy
Change-Id: Iecfed58c72f3f9df4e9d27e50a3a274a1fc0a75f
2016-05-11 15:31:00 +02:00
Jenkins
1026a896e2 Merge "Log "https" if using SSL" 2016-05-11 13:24:43 +00:00
Vincent Françoise
a3ac26870a DB sync for Strategies
In this changeset, I added the ability to synchronize the strategies
into the Wather DB so that it can later be served through the Watcher
API.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ifeaa1f6e1f4ff7d7efc1b221cf57797a49dc5bc5
2016-05-11 15:19:40 +02:00
Vincent Françoise
192d8e262c Added Strategy model
In this changeset, I add the Strategy model as well as the DB
functionalities we need to manipulate strategies.

This changeset implies a DB schema update.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I438a8788844fbc514edfe1e9e3136f46ba5a82f2
2016-05-11 15:19:40 +02:00
Vincent Françoise
3b5ef15db6 Added Goal object + goal syncing
In this changeset, I added the Goal object into Watcher along with
a sync module that is responsible for syncing the goals with the
Watcher DB.

Partially Implements: blueprint get-goal-from-strategy

Change-Id: Ia3a2032dd9023d668c6f32ebbce44f8c1d77b0a3
2016-05-11 15:19:40 +02:00
Vincent Françoise
be9058f3e3 Added Goal model into Watcher DB
In this changeset, I added the Goal model into Watcher.
This implies a change into the Watcher DB schema

Partially Implements: blueprint get-goal-from-strategy

Change-Id: I5b5b0ffc7cff8affb59f17743e1af0e1277c2878
2016-05-11 15:19:40 +02:00
Jenkins
91951f3b01 Merge "Refactored DE and Applier to use oslo.service" 2016-05-11 13:14:52 +00:00
Jenkins
57a2af2685 Merge "Refactored Watcher API service" 2016-05-11 13:14:29 +00:00
Yosef Hoffman
76e3d2e2f6 Log "https" if using SSL
When starting the Watcher API service, the URI it served to is shown in
a log message. In this log message (in watcher/cmd/api.py) take into
account the case where SSL has been enabled with CONF.api.enable_ssl_api
set to True and format this log message accordingly.

Change-Id: I98541810139d9d4319ac89f21a5e0bc25454ee62
Closes-Bug: #1580044
2016-05-10 11:44:56 -04:00
Jenkins
bd5a969a26 Merge "Remove using of UUID field in POST methods of Watcher API" 2016-04-29 02:57:34 +00:00
zhangguoqing
d61bf5f053 [nova_helper] get keypair name by every admin users
Since the bug #1182965 has been fixed, allow admin users
to view any keypair.

Change-Id: I9cf948701515afd45e6720cfd15cfac6b5866aa5
2016-04-25 21:20:16 +08:00
Alexander Chadin
aaaf3f1c84 Remove using of UUID field in POST methods of Watcher API
This patch set removes the possibility of using UUID field
in POST methods of Watcher API.

Closes-Bug: #1572625

Change-Id: I88a8aa5346e937e3e9409b55da3316cbe1ed832a
2016-04-25 16:05:59 +03:00
Vincent Françoise
eb861f86ab Refactored DE and Applier to use oslo.service
In this PS, I have refactored the Decision Engine and the Applier
to use the oslo service utility.

Change-Id: If29158cc9b5e5e50f6c69d67c232cceeb07084f2
Closes-Bug: #1541850
2016-04-22 10:33:21 +02:00
Vincent Françoise
a9e7251d0d Refactored Watcher API service
This patchset introduces the use of oslo.service to run the
Watcher API service.

Change-Id: I6c38a3c1a2b4dc47388876e4c0ba61b7447690bd
Related-Bug: #1541850
2016-04-22 10:33:21 +02:00
OpenStack Proposal Bot
4ff373197c Updated from global requirements
Change-Id: I04865b9e63d6fc805802b6057ba9750116849c98
2016-04-19 12:30:29 +00:00
Jenkins
87087e9add Merge "Removed unused 'alarm' field" 2016-04-19 08:02:21 +00:00
Larry Rensing
408d6d4650 Removed unused 'alarm' field
The 'alarm' field is currently unused, so it has been removed.

Change-Id: I02fa15b06ed49dbc5dd63de54a9cde601413983c
Closes-Bug: #1550261
2016-04-18 14:12:12 +00:00
Alexander Chadin
e52dc4f8aa Add parameters verification when Audit is being created
We have to check Audit Type and Audit State to make sure
these parameters are in valid status.

Also, we provide default states for the next attributes:

- 'audit_template' is required and should be either UUID or text field
- 'state' is readonly so it raises an error if submitted in POST
  and is set by default to PENDING
- 'deadline' is optional and should be a datetime
- 'type' is a required text field

Change-Id: I2a7e0deec0ee2040e86400b500bb0efd8eade564
Closes-Bug: #1532843
Closes-Bug: #1533210
2016-04-14 15:43:26 +03:00
Jenkins
0f14b7635d Merge "correct the available disk, memory calculating Source data are misused in outlet temperature strategy. This patch fixes it." 2016-04-12 07:08:15 +00:00
junjie huang
bb77641aad correct the available disk, memory calculating
Source data are misused in outlet temperature strategy. This patch
fixes it.

Change-Id: I8ad5c974d7674ddfe6c4c9e3a6e3029d34a400db
Closes-bug: #1569114
2016-04-11 17:53:54 +00:00
Vincent Françoise
77228a0b0a Upgrade Watcher Tempest tests for multinode
Change-Id: I4b84ba9814227776232c8ab883cdaaf411930ee6
2016-04-11 16:49:10 +02:00
Jenkins
1157a8db30 Merge "Fix for deleting audit template" 2016-04-08 18:58:07 +00:00
Jenkins
18354d1b4e Merge "Update .coveragerc to ignore abstract methods" 2016-04-08 18:57:11 +00:00
Larry Rensing
8387cd10de Update .coveragerc to ignore abstract methods
Due to importing modules rather than functions and decorators directly,
@abc.abstract and 'raise NotImplementedError' were added to the
.coveragerc file.  Since abstract methods are not testable, this will
give us a more accurate representation of our coverage.

Change-Id: Id5ed5e1f5e142d10f41ad18d20228399226ec20d
Co-Authored-By: Jin Li <jl7351@att.com>
Closes-Bug: #1563717
2016-04-08 16:59:57 +00:00
OpenStack Proposal Bot
0449bae747 Updated from global requirements
Change-Id: Ieead2a4c784c248bd6af821f5e1e84c5e6cd3b5a
2016-04-07 17:25:39 +00:00
Alexander Chadin
3e07844844 Fix for deleting audit template
We need to update sqlalchemy/api and sqlalchemy/models (and appropriate tests)
to support deleting audit templates and recreating them with the same names.

Change-Id: Icf54cf1ed989a3f2ad689e25be4474b16a3a3eb2
Related-Bug: #1510179
2016-04-07 11:27:53 +03:00
zhangguoqing
a52d92be87 Remove unused logging import and LOG global var
In some modules the global LOG is not used any more. And the import
of logging is not used. This patch removes the unused logging import
and LOG vars.

Change-Id: I794ee719d76f04e70154cf67f726152fbb1ba15a
2016-04-06 10:34:39 +08:00
OpenStack Proposal Bot
96683a6133 Updated from global requirements
Change-Id: Ib98e484bb216eb31b64931db735ced8d1de738a4
2016-04-05 13:44:07 +00:00
239 changed files with 14882 additions and 3136 deletions

View File

@@ -4,6 +4,7 @@ source = watcher
omit = watcher/tests/* omit = watcher/tests/*
[report] [report]
ignore_errors = True ignore_errors = True
exclude_lines = exclude_lines =
@abstract @abc.abstract
raise NotImplementedError

7
.gitignore vendored
View File

@@ -64,3 +64,10 @@ sftp-config.json
cover cover
/demo/ /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 * Wiki: http://wiki.openstack.org/wiki/Watcher
* Source: https://github.com/openstack/watcher * Source: https://github.com/openstack/watcher
* Bugs: http://bugs.launchpad.net/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 remote console access
enable_service n-cauth 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 the Watcher plugin
enable_plugin watcher git://git.openstack.org/openstack/watcher enable_plugin watcher git://git.openstack.org/openstack/watcher
@@ -42,7 +45,3 @@ LOGDAYS=2
[[post-config|$NOVA_CONF]] [[post-config|$NOVA_CONF]]
[DEFAULT] [DEFAULT]
compute_monitors=cpu.virt_driver compute_monitors=cpu.virt_driver
[[post-config|$WATCHER_CONF]]
[watcher_goals]
goals=BASIC_CONSOLIDATION:basic,DUMMY:dummy

View File

@@ -150,11 +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 by the :ref:`Watcher API <archi_watcher_api_definition>` or the
:ref:`Watcher CLI <archi_watcher_cli_definition>`: :ref:`Watcher CLI <archi_watcher_cli_definition>`:
- :ref:`Goals <goal_definition>`
- :ref:`Strategies <strategy_definition>`
- :ref:`Audit templates <audit_template_definition>` - :ref:`Audit templates <audit_template_definition>`
- :ref:`Audits <audit_definition>` - :ref:`Audits <audit_definition>`
- :ref:`Action plans <action_plan_definition>` - :ref:`Action plans <action_plan_definition>`
- :ref:`Efficacy indicators <efficacy_indicator_definition>` via the Action
Plan API.
- :ref:`Actions <action_definition>` - :ref:`Actions <action_definition>`
- :ref:`Goals <goal_definition>`
The Watcher domain being here "*optimization of some resources provided by an The Watcher domain being here "*optimization of some resources provided by an
OpenStack system*". OpenStack system*".
@@ -196,8 +199,6 @@ Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
which provides information about the past of the which provides information about the past of the
:ref:`Cluster <cluster_definition>` :ref:`Cluster <cluster_definition>`
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
given :ref:`Goal <goal_definition>` via the main Watcher configuration file.
.. _data_model: .. _data_model:
@@ -211,6 +212,14 @@ view (Goals, Audits, Action Plans, ...):
.. image:: ./images/functional_data_model.svg .. image:: ./images/functional_data_model.svg
:width: 100% :width: 100%
Here below is a diagram representing the main objects in Watcher from a
database perspective:
.. image:: ./images/watcher_db_schema_diagram.png
:width: 100%
.. _sequence_diagrams: .. _sequence_diagrams:
Sequence diagrams Sequence diagrams
@@ -230,13 +239,15 @@ following parameters:
- A name - A name
- A goal to achieve - A goal to achieve
- An optional strategy
.. image:: ./images/sequence_create_audit_template.png .. image:: ./images/sequence_create_audit_template.png
:width: 100% :width: 100%
The `Watcher API`_ just makes sure that the goal exists (i.e. it is declared The `Watcher API`_ makes sure that both the specified goal (mandatory) and
in the Watcher configuration file) and stores a new audit template in the its associated strategy (optional) are registered inside the :ref:`Watcher
:ref:`Watcher Database <watcher_database_definition>`. Database <watcher_database_definition>` before storing a new audit template in
the :ref:`Watcher Database <watcher_database_definition>`.
.. _sequence_diagrams_create_and_launch_audit: .. _sequence_diagrams_create_and_launch_audit:
@@ -250,6 +261,13 @@ previously created :ref:`Audit template <audit_template_definition>`:
.. image:: ./images/sequence_create_and_launch_audit.png .. image:: ./images/sequence_create_and_launch_audit.png
:width: 100% :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 A message is sent on the :ref:`AMQP bus <amqp_bus_definition>` which triggers
the Audit in the the Audit in the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`: :ref:`Watcher Decision Engine <watcher_decision_engine_definition>`:
@@ -260,12 +278,11 @@ the Audit in the
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads
the Audit parameters from the the Audit parameters from the
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the :ref:`Watcher Database <watcher_database_definition>`. It instantiates the
appropriate :ref:`Strategy <strategy_definition>` (using entry points) appropriate :ref:`strategy <strategy_definition>` (using entry points)
associated to the :ref:`Goal <goal_definition>` of the given both the :ref:`goal <goal_definition>` and the strategy associated to the
:ref:`Audit <audit_definition>` (it uses the information of the Watcher parent :ref:`audit template <audit_template_definition>` of the :ref:`Audit
configuration file to find the mapping between the <audit_definition>`. If no strategy is associated to the audit template, the
:ref:`Goal <goal_definition>` and the :ref:`Strategy <strategy_definition>` strategy is dynamically selected by the Decision Engine.
python class).
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This
@@ -289,9 +306,11 @@ This method finds an appropriate scheduling of
:ref:`Actions <action_definition>` taking into account some scheduling rules :ref:`Actions <action_definition>` taking into account some scheduling rules
(such as priorities between actions). (such as priorities between actions).
It generates a new :ref:`Action Plan <action_plan_definition>` with status It generates a new :ref:`Action Plan <action_plan_definition>` with status
**RECOMMENDED** and saves it into the **RECOMMENDED** and saves it into the:ref:`Watcher Database
:ref:`Watcher Database <watcher_database_definition>`. The saved action plan is <watcher_database_definition>`. The saved action plan is now a scheduled flow
now a scheduled flow of actions. 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 If every step executed successfully, the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` updates :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` updates
@@ -300,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>` on the bus to inform other components that the :ref:`Audit <audit_definition>`
was successful. 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: .. _sequence_diagrams_launch_action_plan:

View File

@@ -182,8 +182,6 @@ The configuration file is organized into the following sections:
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients * ``[watcher_clients_auth]`` - Keystone auth configuration for clients
* ``[watcher_applier]`` - Watcher Applier module configuration * ``[watcher_applier]`` - Watcher Applier module configuration
* ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration * ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration
* ``[watcher_goals]`` - Goals mapping configuration
* ``[watcher_strategies]`` - Strategy configuration
* ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration * ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration
* ``[ceilometer_client]`` - Ceilometer client configuration * ``[ceilometer_client]`` - Ceilometer client configuration
* ``[cinder_client]`` - Cinder client configuration * ``[cinder_client]`` - Cinder client configuration

52
doc/source/deploy/gmr.rst Normal file
View File

@@ -0,0 +1,52 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
.. _watcher_gmr:
=======================
Guru Meditation Reports
=======================
Watcher contains a mechanism whereby developers and system administrators can
generate a report about the state of a running Watcher service. This report
is called a *Guru Meditation Report* (*GMR* for short).
Generating a GMR
================
A *GMR* can be generated by sending the *USR2* signal to any Watcher process
with support (see below). The *GMR* will then be outputted as standard error
for that particular process.
For example, suppose that ``watcher-api`` has process id ``8675``, and was run
with ``2>/var/log/watcher/watcher-api-err.log``. Then, ``kill -USR2 8675``
will trigger the Guru Meditation report to be printed to
``/var/log/watcher/watcher-api-err.log``.
Structure of a GMR
==================
The *GMR* is designed to be extensible; any particular service may add its
own sections. However, the base *GMR* consists of several sections:
Package
Shows information about the package to which this process belongs, including
version informations.
Threads
Shows stack traces and thread ids for each of the threads within this
process.
Green Threads
Shows stack traces for each of the green threads within this process (green
threads don't have thread ids).
Configuration
Lists all the configuration options currently accessible via the CONF object
for the current process.
Plugins
Lists all the plugins currently accessible by the Watcher service.

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. 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. 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 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 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 for an architectural overview of the different components of Watcher and how
they fit together. they fit together.
@@ -32,17 +32,17 @@ This guide assumes you have a working installation of Watcher. If you get
Please refer to the `installation guide`_. Please refer to the `installation guide`_.
In order to use Watcher, you have to configure your credentials suitable for In order to use Watcher, you have to configure your credentials suitable for
watcher command-line tools. watcher command-line tools.
If you need help on a specific command, you can use:
.. code:: bash You can interact with Watcher either by using our dedicated `Watcher CLI`_
named ``watcher``, or by using the `OpenStack CLI`_ ``openstack``.
$ watcher help COMMAND
If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon
plugin installation guide`_. plugin installation guide`_.
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient .. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
.. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html .. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html
.. _`OpenStack CLI`: http://docs.openstack.org/developer/python-openstackclient/man/openstack.html
.. _`Watcher CLI`: https://factory.b-com.com/www/watcher/doc/python-watcherclient/index.html
Seeing what the Watcher CLI can do ? Seeing what the Watcher CLI can do ?
------------------------------------ ------------------------------------
@@ -51,23 +51,76 @@ watcher binary without options.
.. code:: bash .. code:: bash
$ watcher $ watcher help
or::
$ openstack help optimize
How do I run an audit of my cluster ? How do I run an audit of my cluster ?
------------------------------------- -------------------------------------
First, you need to create an :ref:`audit template <audit_template_definition>`. First, you need to find the :ref:`goal <goal_definition>` you want to achieve:
An :ref:`audit template <audit_template_definition>` defines an optimization
:ref:`goal <goal_definition>` to achieve (i.e. the settings of your audit).
This goal should be declared in the Watcher service configuration file
**/etc/watcher/watcher.conf**.
.. code:: bash .. code:: bash
$ watcher audit-template-create my_first_audit DUMMY $ watcher goal list
If you get "*You must provide a username via either --os-username or via or::
env[OS_USERNAME]*" you may have to verify your credentials.
$ openstack optimize goal list
.. note::
If you get "*You must provide a username via either --os-username or via
env[OS_USERNAME]*" you may have to verify your credentials.
Then, you can create an :ref:`audit template <audit_template_definition>`.
An :ref:`audit template <audit_template_definition>` defines an optimization
:ref:`goal <goal_definition>` to achieve (i.e. the settings of your audit).
.. code:: bash
$ watcher audittemplate create my_first_audit_template <your_goal>
or::
$ openstack optimize audittemplate create my_first_audit_template <your_goal>
Although optional, you may want to actually set a specific strategy for your
audit template. If so, you may can search of its UUID or name using the
following command:
.. code:: bash
$ watcher strategy list --goal-uuid <your_goal_uuid>
or::
$ openstack optimize strategy list --goal-uuid <your_goal_uuid>
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:
.. code:: bash
$ watcher audittemplate create my_first_audit_template <your_goal> \
--strategy <your_strategy>
or::
$ openstack optimize audittemplate create my_first_audit_template <your_goal> \
--strategy <your_strategy>
Then, you can create an audit. An audit is a request for optimizing your Then, you can create an audit. An audit is a request for optimizing your
cluster depending on the specified :ref:`goal <goal_definition>`. cluster depending on the specified :ref:`goal <goal_definition>`.
@@ -76,19 +129,46 @@ You can launch an audit on your cluster by referencing the
:ref:`audit template <audit_template_definition>` (i.e. the settings of your :ref:`audit template <audit_template_definition>` (i.e. the settings of your
audit) that you want to use. audit) that you want to use.
- Get the :ref:`audit template <audit_template_definition>` UUID: - Get the :ref:`audit template <audit_template_definition>` UUID or name:
.. code:: bash .. code:: bash
$ watcher audit-template-list $ watcher audittemplate list
or::
$ openstack optimize audittemplate list
- Start an audit based on this :ref:`audit template - Start an audit based on this :ref:`audit template
<audit_template_definition>` settings: <audit_template_definition>` settings:
.. code:: bash .. code:: bash
$ watcher audit-create -a <your_audit_template_uuid> $ watcher audit create -a <your_audit_template>
or::
$ openstack optimize audit create -a <your_audit_template>
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>` Watcher service will compute an :ref:`Action Plan <action_plan_definition>`
composed of a list of potential optimization :ref:`actions <action_definition>` composed of a list of potential optimization :ref:`actions <action_definition>`
@@ -102,15 +182,22 @@ configuration file.
.. code:: bash .. code:: bash
$ watcher action-plan-list --audit <the_audit_uuid> $ watcher actionplan list --audit <the_audit_uuid>
or::
$ openstack optimize actionplan list --audit <the_audit_uuid>
- Have a look on the list of optimization :ref:`actions <action_definition>` - Have a look on the list of optimization :ref:`actions <action_definition>`
contained in this new :ref:`action plan <action_plan_definition>`: contained in this new :ref:`action plan <action_plan_definition>`:
.. code:: bash .. code:: bash
$ watcher action-list --action-plan <the_action_plan_uuid> $ watcher action list --action-plan <the_action_plan_uuid>
or::
$ openstack optimize action list --action-plan <the_action_plan_uuid>
Once you have learned how to create an :ref:`Action Plan Once you have learned how to create an :ref:`Action Plan
<action_plan_definition>`, it's time to go further by applying it to your <action_plan_definition>`, it's time to go further by applying it to your
@@ -120,18 +207,30 @@ cluster:
.. code:: bash .. code:: bash
$ watcher action-plan-start <the_action_plan_uuid> $ watcher actionplan start <the_action_plan_uuid>
or::
$ openstack optimize actionplan start <the_action_plan_uuid>
You can follow the states of the :ref:`actions <action_definition>` by You can follow the states of the :ref:`actions <action_definition>` by
periodically calling: periodically calling:
.. code:: bash .. code:: bash
$ watcher action-list $ watcher action list
or::
$ openstack optimize action list
You can also obtain more detailed information about a specific action: You can also obtain more detailed information about a specific action:
.. code:: bash .. code:: bash
$ watcher action-show <the_action_uuid> $ watcher action show <the_action_uuid>
or::
$ openstack optimize action show <the_action_uuid>

View File

@@ -64,8 +64,8 @@ IRC Channel
``#openstack-watcher`` (changelog_) ``#openstack-watcher`` (changelog_)
Weekly Meetings Weekly Meetings
on Wednesdays at 14:00 UTC in the ``#openstack-meeting-4`` IRC on Wednesdays at 14:00 UTC on even weeks, 9:00 UTC on odd weeks, in the
channel (`meetings logs`_) ``#openstack-meeting-4`` IRC channel (`meetings logs`_)
.. _changelog: http://eavesdrop.openstack.org/irclogs/%23openstack-watcher/ .. _changelog: http://eavesdrop.openstack.org/irclogs/%23openstack-watcher/
.. _meetings logs: http://eavesdrop.openstack.org/meetings/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`_. Documentation`_.
.. _`Git Repository`: http://git.openstack.org/cgit/openstack/watcher .. _`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 Prerequisites
============= =============
@@ -205,7 +205,7 @@ place:
$ workon watcher $ workon watcher
(watcher) $ watcher-db-manage --create_schema (watcher) $ watcher-db-manage create_schema
Running Watcher services Running Watcher services

View File

@@ -4,6 +4,8 @@
https://creativecommons.org/licenses/by/3.0/ https://creativecommons.org/licenses/by/3.0/
.. _implement_action_plugin:
================== ==================
Build a new action Build a new action
================== ==================
@@ -55,7 +57,7 @@ Here is an example showing how you can write a plugin called ``DummyAction``:
from watcher.applier.actions import base from watcher.applier.actions import base
class DummyAction(baseBaseAction): class DummyAction(base.BaseAction):
@property @property
def schema(self): def schema(self):
@@ -90,11 +92,53 @@ Input validation
As you can see in the previous example, we are using `Voluptuous`_ to validate As you can see in the previous example, we are using `Voluptuous`_ to validate
the input parameters of an action. So if you want to learn more about how to the input parameters of an action. So if you want to learn more about how to
work with `Voluptuous`_, you can have a look at their `documentation`_ here: work with `Voluptuous`_, you can have a look at their `documentation`_:
.. _Voluptuous: https://github.com/alecthomas/voluptuous .. _Voluptuous: https://github.com/alecthomas/voluptuous
.. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md .. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md
Define configuration parameters
===============================
At this point, you have a fully functional action. However, in more complex
implementation, you may want to define some configuration options so one can
tune the action to its needs. To do so, you can implement the
:py:meth:`~.Loadable.get_config_opts` class method as followed:
.. code-block:: python
from oslo_config import cfg
class DummyAction(base.BaseAction):
# [...]
def execute(self):
assert self.config.test_opt == 0
def get_config_opts(self):
return [
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
# Some more options ...
]
The configuration options defined within this class method will be included
within the global ``watcher.conf`` configuration file under a section named by
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
configuration would have to be modified as followed:
.. code-block:: ini
[watcher_actions.dummy]
# Option used for testing.
test_opt = test_value
Then, the configuration options you define within this method will then be
injected in each instantiated object via the ``config`` parameter of the
:py:meth:`~.BaseAction.__init__` method.
Abstract Plugin Class Abstract Plugin Class
===================== =====================
@@ -103,6 +147,7 @@ should implement:
.. autoclass:: watcher.applier.actions.base.BaseAction .. autoclass:: watcher.applier.actions.base.BaseAction
:members: :members:
:special-members: __init__
:noindex: :noindex:
.. py:attribute:: schema .. py:attribute:: schema
@@ -160,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 processed by the planner for creating an action plan which can then be executed
by the Watcher Applier via its workflow engine. 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 Scheduling of an action plugin
============================== ==============================

View File

@@ -71,8 +71,15 @@ structure that looks like this::
│   └── test_thirdparty.py │   └── test_thirdparty.py
└── tox.ini └── 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 .. _cookiecutter: https://github.com/audreyr/cookiecutter
.. _OpenStack cookiecutter: https://github.com/openstack-dev/cookiecutter .. _OpenStack cookiecutter: https://github.com/openstack-dev/cookiecutter
.. _python-watcher: https://pypi.python.org/pypi/python-watcher
Implementing a plugin for Watcher Implementing a plugin for Watcher
================================= =================================
@@ -83,7 +90,7 @@ plugins for Watcher:
- A :ref:`strategy plugin <implement_strategy_plugin>` - A :ref:`strategy plugin <implement_strategy_plugin>`
- A :ref:`planner plugin <implement_planner_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>` - 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 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 Watcher :ref:`Decision Engine <watcher_decision_engine_definition>` has an
external :ref:`planner <planner_definition>` plugin interface which gives external :ref:`planner <watcher_planner_definition>` plugin interface which
anyone the ability to integrate an external :ref:`planner <planner_definition>` gives anyone the ability to integrate an external :ref:`planner
in order to extend the initial set of planners Watcher provides. <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 This section gives some guidelines on how to implement and integrate custom
planners with Watcher. planners with Watcher.
@@ -69,6 +70,49 @@ examples, have a look at the implementation of planners already provided by
Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner
plugins can be found :ref:`here <watcher_planners>`. plugins can be found :ref:`here <watcher_planners>`.
Define configuration parameters
===============================
At this point, you have a fully functional planner. However, in more complex
implementation, you may want to define some configuration options so one can
tune the planner to its needs. To do so, you can implement the
:py:meth:`~.Loadable.get_config_opts` class method as followed:
.. code-block:: python
from oslo_config import cfg
class DummyPlanner(base.BasePlanner):
# [...]
def schedule(self, context, audit_uuid, solution):
assert self.config.test_opt == 0
# [...]
def get_config_opts(self):
return [
cfg.StrOpt('test_opt', help="Demo Option.", default=0),
# Some more options ...
]
The configuration options defined within this class method will be included
within the global ``watcher.conf`` configuration file under a section named by
convention: ``{namespace}.{plugin_name}``. In our case, the ``watcher.conf``
configuration would have to be modified as followed:
.. code-block:: ini
[watcher_planners.dummy]
# Option used for testing.
test_opt = test_value
Then, the configuration options you define within this method will then be
injected in each instantiated object via the ``config`` parameter of the
:py:meth:`~.BasePlanner.__init__` method.
Abstract Plugin Class Abstract Plugin Class
===================== =====================
@@ -77,6 +121,7 @@ should implement:
.. autoclass:: watcher.decision_engine.planner.base.BasePlanner .. autoclass:: watcher.decision_engine.planner.base.BasePlanner
:members: :members:
:special-members: __init__
:noindex: :noindex:

View File

@@ -15,7 +15,9 @@ plugin interface which gives anyone the ability to integrate an external
strategy in order to make use of placement algorithms. strategy in order to make use of placement algorithms.
This section gives some guidelines on how to implement and integrate custom This section gives some guidelines on how to implement and integrate custom
strategies with Watcher. strategies with Watcher. If you wish to create a third-party package for your
plugin, you can refer to our :ref:`documentation for third-party package
creation <plugin-base_setup>`.
Pre-requisites Pre-requisites
@@ -26,64 +28,172 @@ configured so that it would provide you all the metrics you need to be able to
use your strategy. use your strategy.
Creating a new plugin Create a new strategy plugin
===================== ============================
First of all you have to: In order to create a new strategy, you have to:
- Extend :py:class:`~.BaseStrategy` - Extend the :py:class:`~.UnclassifiedStrategy` class
- Implement its :py:meth:`~.BaseStrategy.execute` method - Implement its :py:meth:`~.BaseStrategy.get_name` class method to return the
**unique** ID of the new strategy you want to create. This unique ID should
be the same as the name of :ref:`the entry point we will declare later on
<strategy_plugin_add_entrypoint>`.
- Implement its :py:meth:`~.BaseStrategy.get_display_name` class method to
return the translated display name of the strategy you want to create.
Note: Do not use a variable to return the translated string so it can be
automatically collected by the translation tool.
- Implement its :py:meth:`~.BaseStrategy.get_translatable_display_name`
class method to return the translation key (actually the english display
name) of your new strategy. The value return should be the same as the
string translated in :py:meth:`~.BaseStrategy.get_display_name`.
- Implement its :py:meth:`~.BaseStrategy.execute` method to return the
solution you computed within your strategy.
Here is an example showing how you can write a plugin called ``DummyStrategy``: Here is an example showing how you can write a plugin called ``NewStrategy``:
.. code-block:: python .. code-block:: python
import uuid # filepath: thirdparty/new.py
# import path: thirdparty.new
import abc
class DummyStrategy(BaseStrategy): import six
DEFAULT_NAME = "dummy" from watcher._i18n import _
DEFAULT_DESCRIPTION = "Dummy Strategy" from watcher.decision_engine.strategy.strategies import base
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
super(DummyStrategy, self).__init__(name, description)
def execute(self, model): class NewStrategy(base.UnclassifiedStrategy):
migration_type = 'live'
src_hypervisor = 'compute-host-1' def __init__(self, osc=None):
dst_hypervisor = 'compute-host-2' super(NewStrategy, self).__init__(osc)
instance_id = uuid.uuid4()
parameters = {'migration_type': migration_type, def execute(self, original_model):
'src_hypervisor': src_hypervisor, self.solution.add_action(action_type="nop",
'dst_hypervisor': dst_hypervisor}
self.solution.add_action(action_type="migration",
resource_id=instance_id,
input_parameters=parameters) input_parameters=parameters)
# Do some more stuff here ... # Do some more stuff here ...
return self.solution return self.solution
@classmethod
def get_name(cls):
return "new_strategy"
@classmethod
def get_display_name(cls):
return _("New strategy")
@classmethod
def get_translatable_display_name(cls):
return "New strategy"
As you can see in the above example, the :py:meth:`~.BaseStrategy.execute` As you can see in the above example, the :py:meth:`~.BaseStrategy.execute`
method returns a :py:class:`~.BaseSolution` instance as required. This solution method returns a :py:class:`~.BaseSolution` instance as required. This solution
is what wraps the abstract set of actions the strategy recommends to you. This is what wraps the abstract set of actions the strategy recommends to you. This
solution is then processed by a :ref:`planner <planner_definition>` to produce solution is then processed by a :ref:`planner <watcher_planner_definition>` to
an action plan which shall contain the sequenced flow of actions to be produce an action plan which contains the sequenced flow of actions to be
executed by the :ref:`Watcher Applier <watcher_applier_definition>`. executed by the :ref:`Watcher Applier <watcher_applier_definition>`. 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 be instantiated without any Please note that your strategy class will expect to find the same constructor
parameter. Therefore, you should make sure not to make any of them required in signature as BaseStrategy to instantiate you strategy. Therefore, you should
your ``__init__`` method. ensure that your ``__init__`` signature is identical to the
:py:class:`~.BaseStrategy` one.
Strategy efficacy
=================
As stated before, the ``NewStrategy`` class extends a class called
:py:class:`~.UnclassifiedStrategy`. This class actually implements a set of
abstract methods which are defined within the :py:class:`~.BaseStrategy` parent
class.
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.
Define Strategy Parameters
==========================
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.
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
class DummyStrategy(base.DummyBaseStrategy):
@classmethod
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",
},
},
}
You can reference parameters in :py:meth:`~.BaseStrategy.execute`:
.. code-block:: python
class DummyStrategy(base.DummyBaseStrategy):
def execute(self):
para1 = self.input_parameters.para1
para2 = self.input_parameters.para2
if para1 > 5:
...
Operator can specify parameters with following commands:
.. code:: bash
$ watcher audit create -a <your_audit_template> -p para1=6.0 -p para2=hi
Pls. check user-guide for details.
Abstract Plugin Class Abstract Plugin Class
===================== =====================
Here below is the abstract :py:class:`~.BaseStrategy` class that every single Here below is the abstract :py:class:`~.BaseStrategy` class:
strategy should implement:
.. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy .. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy
:members: :members:
:special-members: __init__
:noindex: :noindex:
.. _strategy_plugin_add_entrypoint:
Add a new entry point Add a new entry point
===================== =====================
@@ -93,15 +203,17 @@ strategy must be registered as a named entry point under the
``watcher_strategies`` entry point of your ``setup.py`` file. If you are using ``watcher_strategies`` entry point of your ``setup.py`` file. If you are using
pbr_, this entry point should be placed in your ``setup.cfg`` file. pbr_, this entry point should be placed in your ``setup.cfg`` file.
The name you give to your entry point has to be unique. The name you give to your entry point has to be unique and should be the same
as the value returned by the :py:meth:`~.BaseStrategy.get_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 .. code-block:: ini
[entry_points] [entry_points]
watcher_strategies = watcher_strategies =
dummy = thirdparty.dummy:DummyStrategy new_strategy = thirdparty.new:NewStrategy
To get a better understanding on how to implement a more advanced strategy, To get a better understanding on how to implement a more advanced strategy,
@@ -117,16 +229,10 @@ plugins when it is restarted. If a Python package containing a custom plugin is
installed within the same environment as Watcher, Watcher will automatically installed within the same environment as Watcher, Watcher will automatically
make that plugin available for use. make that plugin available for use.
At this point, Watcher will use your new strategy if you reference it in the At this point, Watcher will scan and register inside the :ref:`Watcher Database
``goals`` under the ``[watcher_goals]`` section of your ``watcher.conf`` <watcher_database_definition>` all the strategies (alongside the goals they
configuration file. For example, if you want to use a ``dummy`` strategy you should satisfy) you implemented upon restarting the :ref:`Watcher Decision
just installed, you would have to associate it to a goal like this: Engine <watcher_decision_engine_definition>`.
.. code-block:: ini
[watcher_goals]
goals = BALANCE_LOAD:basic,MINIMIZE_ENERGY_CONSUMPTION:dummy
You should take care when installing strategy plugins. By their very nature, You should take care when installing strategy plugins. By their very nature,
there are no guarantees that utilizing them as is will be supported, as there are no guarantees that utilizing them as is will be supported, as
@@ -148,7 +254,6 @@ for various types of backends. A list of the available backends is located
here_. The Ceilosca project is a good example of how to create your own here_. The Ceilosca project is a good example of how to create your own
pluggable backend. pluggable backend.
Finally, if your strategy requires new metrics not covered by Ceilometer, you Finally, if your strategy requires new metrics not covered by Ceilometer, you
can add them through a Ceilometer `plugin`_. can add them through a Ceilometer `plugin`_.
@@ -191,7 +296,7 @@ Read usage metrics using the Watcher Cluster History Helper
Here below is the abstract ``BaseClusterHistory`` class of the Helper. Here below is the abstract ``BaseClusterHistory`` class of the Helper.
.. autoclass:: watcher.metrics_engine.cluster_history.api.BaseClusterHistory .. autoclass:: watcher.metrics_engine.cluster_history.base.BaseClusterHistory
:members: :members:
:noindex: :noindex:

View File

@@ -9,6 +9,17 @@
Available Plugins Available Plugins
================= =================
In this section we present all the plugins that are shipped along with Watcher.
If you want to know which plugins your Watcher services have access to, you can
use the :ref:`Guru Meditation Reports <watcher_gmr>` to display them.
.. _watcher_goals:
Goals
=====
.. drivers-doc:: watcher_goals
.. _watcher_strategies: .. _watcher_strategies:
Strategies Strategies

View File

@@ -99,14 +99,14 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
Cluster Data Model Cluster Data Model
================== ==================
.. watcher-term:: watcher.metrics_engine.cluster_model_collector.api .. watcher-term:: watcher.metrics_engine.cluster_model_collector.base
.. _cluster_history_definition: .. _cluster_history_definition:
Cluster History Cluster History
=============== ===============
.. watcher-term:: watcher.metrics_engine.cluster_history.api .. watcher-term:: watcher.metrics_engine.cluster_history.base
.. _controller_node_definition: .. _controller_node_definition:
@@ -131,7 +131,8 @@ can potentially be hosted on a dedicated machine.
Compute node 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: .. _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>`_ - `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: .. _efficacy_definition:
@@ -223,8 +240,8 @@ measure of how much of the :ref:`Goal <goal_definition>` has been achieved in
respect with constraints and :ref:`SLAs <sla_definition>` defined by the respect with constraints and :ref:`SLAs <sla_definition>` defined by the
:ref:`Customer <customer_definition>`. :ref:`Customer <customer_definition>`.
The way efficacy is evaluated will depend on the The way efficacy is evaluated will depend on the :ref:`Goal <goal_definition>`
:ref:`Goal <goal_definition>` to achieve. to achieve.
Of course, the efficacy will be relevant only as long as the Of course, the efficacy will be relevant only as long as the
:ref:`Action Plan <action_plan_definition>` is relevant :ref:`Action Plan <action_plan_definition>` is relevant
@@ -234,14 +251,15 @@ to be launched).
For example, if the :ref:`Goal <goal_definition>` is to lower the energy For example, if the :ref:`Goal <goal_definition>` is to lower the energy
consumption, the :ref:`Efficacy <efficacy_definition>` will be computed 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 percentage of energy gain (which must be the highest possible)
- the number of :ref:`SLA violations <sla_violation_definition>` - the number of :ref:`SLA violations <sla_violation_definition>`
(which must be the lowest possible) (which must be the lowest possible)
- the number of virtual machine migrations (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>`. time taken to execute the whole :ref:`Action Plan <action_plan_definition>`.
The efficacy also enables the :ref:`Administrator <administrator_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 In OpenStack Identity, a :ref:`project <project_definition>` must be owned by a
specific domain. 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: .. _sla_definition:
@@ -323,7 +342,7 @@ Solution
Strategy Strategy
======== ========
.. watcher-term:: watcher.decision_engine.strategy.strategies.base .. watcher-term:: watcher.api.controllers.v1.strategy
.. _watcher_applier_definition: .. _watcher_applier_definition:
@@ -364,4 +383,3 @@ Watcher Planner
=============== ===============
.. watcher-term:: watcher.decision_engine.planner.base .. watcher-term:: watcher.decision_engine.planner.base

View File

@@ -0,0 +1,14 @@
plantuml
========
To build an image from a source file, you have to upload the plantuml JAR file
available on http://plantuml.com/download.html.
After, just run this command to build your image:
.. code-block:: shell
$ cd doc/source/images
$ java -jar /path/to/plantuml.jar doc/source/image_src/plantuml/my_image.txt
$ ls doc/source/images/
my_image.png

View File

@@ -3,7 +3,7 @@
actor Administrator actor Administrator
Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid> Administrator -> "Watcher CLI" : watcher audit create -a <audit_template>
"Watcher CLI" -> "Watcher API" : POST audit(parameters) "Watcher CLI" -> "Watcher API" : POST audit(parameters)
"Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING) "Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING)
@@ -14,7 +14,7 @@ Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid>
Administrator <-- "Watcher CLI" : new audit uuid Administrator <-- "Watcher CLI" : new audit uuid
"Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid) "Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid)
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid) "AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid) (status=ONGOING)
ref over "Watcher Decision Engine" ref over "Watcher Decision Engine"
Trigger audit in the Trigger audit in the

View File

@@ -2,15 +2,21 @@
actor Administrator actor Administrator
Administrator -> "Watcher CLI" : watcher audit-template-create <name> <goal> Administrator -> "Watcher CLI" : watcher audittemplate create <name> <goal> \
[--strategy-uuid <strategy>]
"Watcher CLI" -> "Watcher API" : POST audit_template(parameters) "Watcher CLI" -> "Watcher API" : POST audit_template(parameters)
"Watcher API" -> "Watcher API" : make sure goal exist in configuration "Watcher API" -> "Watcher Database" : Request if goal exists in database
"Watcher API" -> "Watcher Database" : create new audit_template in database "Watcher API" <-- "Watcher Database" : OK
"Watcher API" <-- "Watcher Database" : new audit template uuid "Watcher API" -> "Watcher Database" : Request if strategy exists in database (if provided)
"Watcher CLI" <-- "Watcher API" : return new audit template URL in HTTP Location Header "Watcher API" <-- "Watcher Database" : OK
Administrator <-- "Watcher CLI" : new audit template uuid
"Watcher API" -> "Watcher Database" : Create new audit_template in database
"Watcher API" <-- "Watcher Database" : New audit template UUID
"Watcher CLI" <-- "Watcher API" : Return new audit template URL in HTTP Location Header
Administrator <-- "Watcher CLI" : New audit template UUID
@enduml @enduml

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

@@ -2,10 +2,10 @@
actor Administrator actor Administrator
Administrator -> "Watcher CLI" : watcher action-plan-start <action_plan_uuid> Administrator -> "Watcher CLI" : watcher actionplan start <action_plan_uuid>
"Watcher CLI" -> "Watcher API" : PATCH action_plan(state=TRIGGERED) "Watcher CLI" -> "Watcher API" : PATCH action_plan(state=PENDING)
"Watcher API" -> "Watcher Database" : action_plan.state=TRIGGERED "Watcher API" -> "Watcher Database" : action_plan.state=PENDING
"Watcher CLI" <-- "Watcher API" : HTTP 200 "Watcher CLI" <-- "Watcher API" : HTTP 200

View File

@@ -1,31 +1,54 @@
@startuml @startuml
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid) skinparam maxMessageSize 100
"Watcher Decision Engine" -> "Watcher Database" : update audit.state = ONGOING
"AMQP Bus" <[#blue]- "Watcher Decision Engine" : notify new audit state = ONGOING "AMQP Bus" -> "Decision Engine" : trigger audit
"Watcher Decision Engine" -> "Watcher Database" : get audit parameters(goal, ...)
"Watcher Decision Engine" <-- "Watcher Database" : audit parameters(goal, ...) 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 create Strategy
"Watcher Decision Engine" -[#red]> "Strategy": select appropriate\noptimization strategy "Decision Engine" -> "Strategy" : execute()
loop while enough data to build cluster data model activate "Strategy"
"Watcher Decision Engine" -> "Nova API" : get resource state (host, instance, ...) create "Cluster Data Model Collector"
"Watcher Decision Engine" <-- "Nova API" : resource state "Strategy" -> "Cluster Data Model Collector" : get cluster data model
end
"Watcher Decision Engine" -[#red]> "Watcher Decision Engine": build cluster_data_model activate "Cluster Data Model Collector"
"Watcher Decision Engine" -> "Strategy" : execute(cluster_data_model) 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 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": get necessary metrics
"Strategy" <-- "Ceilometer API": aggregated metrics "Strategy" <-- "Ceilometer API": aggregated metrics
end end
"Strategy" -> "Strategy" : compute solution to achieve goal "Strategy" -> "Strategy" : compute/set needed actions for the solution \
"Watcher Decision Engine" <-- "Strategy" : solution = array of actions (i.e. not scheduled yet) so it achieves its goal
create "Watcher Planner" "Strategy" -> "Strategy" : compute/set efficacy indicators for the solution
"Watcher Decision Engine" -[#red]> "Watcher Planner": select appropriate actions scheduler (i.e. Planner implementation) "Strategy" -> "Strategy" : compute/set the solution global efficacy
"Watcher Decision Engine" -> "Watcher Planner": schedule(audit_id, solution) "Decision Engine" <-- "Strategy" : solution (contains a list of unordered \
"Watcher Planner" -> "Watcher Planner": schedule actions according to\nscheduling rules/policies actions alongside its efficacy indicators as well as its global efficacy)
"Watcher Decision Engine" <-- "Watcher Planner": new action_plan deactivate "Strategy"
"Watcher Decision Engine" -> "Watcher Database" : save new action_plan in database
"Watcher Decision Engine" -> "Watcher Database" : update audit.state = SUCCEEDED "Decision Engine" --> "Planner": load actions scheduler (i.e. Planner plugin)
"AMQP Bus" <[#blue]- "Watcher Decision Engine" : notify new audit state = SUCCEEDED 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 @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"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd"> <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">
<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"> <defs>
<g> <g>
<rect style="fill: #ffffff" x="570" y="99" width="148.6" height="28"/> <symbol overflow="visible" id="glyph0-0">
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="570" y="99" width="148.6" height="28"/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph0-1">
<g> <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 "/>
<rect style="fill: #ffffff" x="212" y="140" width="177.5" height="28"/> </symbol>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="212" y="140" width="177.5" height="28"/> <symbol overflow="visible" id="glyph0-2">
<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> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph0-3">
<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 "/> <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 "/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="409.838,149 393.838,154 409.838,159 "/> </symbol>
<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> <symbol overflow="visible" id="glyph0-4">
</g> <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 "/>
<g> </symbol>
<rect style="fill: #ffffff" x="615" y="227" width="58.4" height="28"/> <symbol overflow="visible" id="glyph0-5">
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="615" y="227" width="58.4" height="28"/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph0-6">
<g> <path style="stroke:none;" d=""/>
<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 "/> </symbol>
<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 "/> <symbol overflow="visible" id="glyph0-7">
<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> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph0-8">
<rect style="fill: #ffffff" x="916" y="9" width="50.45" height="28"/> <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 "/>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="916" y="9" width="50.45" height="28"/> </symbol>
<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> <symbol overflow="visible" id="glyph0-9">
</g> <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 "/>
<g> </symbol>
<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 "/> <symbol overflow="visible" id="glyph0-10">
<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 "/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph0-11">
<g> <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 "/>
<rect style="fill: #ffffff" x="495" y="367" width="112.45" height="28"/> </symbol>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="495" y="367" width="112.45" height="28"/> <symbol overflow="visible" id="glyph0-12">
<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> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph0-13">
<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 "/> <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 "/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="518,343.647 523,359.647 528,343.647 "/> </symbol>
<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> <symbol overflow="visible" id="glyph0-14">
</g> <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 "/>
<g> </symbol>
<rect style="fill: #ffffff" x="682" y="471" width="67.45" height="28"/> <symbol overflow="visible" id="glyph0-15">
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="682" y="471" width="67.45" height="28"/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph0-16">
<g> <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 "/>
<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 "/> </symbol>
<polygon style="fill: #000000" points="551.225,395.773 556.025,409.773 551.225,423.773 546.425,409.773 "/> <symbol overflow="visible" id="glyph0-17">
<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 "/> <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 "/>
<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> </symbol>
<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> <symbol overflow="visible" id="glyph0-18">
<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> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph0-19">
<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 "/> <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 "/>
<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> </symbol>
<polygon style="fill: #000000" points="850.075,514 850.075,506 858.075,510 "/> <symbol overflow="visible" id="glyph0-20">
<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> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph0-21">
<g> <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 "/>
<ellipse style="fill: #ffffff" cx="1036" cy="219" rx="6" ry="6"/> </symbol>
<ellipse style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" cx="1036" cy="219" rx="6" ry="6"/> <symbol overflow="visible" id="glyph0-22">
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1012" y1="231" x2="1060" y2="231"/> <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 "/>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="225" x2="1036" y2="255"/> </symbol>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="255" x2="1012" y2="281"/> <symbol overflow="visible" id="glyph0-23">
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="255" x2="1060" y2="281"/> <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 "/>
<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"> </symbol>
<tspan x="1036" y="304.9">Administrator</tspan> <symbol overflow="visible" id="glyph0-24">
</text> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph0-25">
<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 "/> <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 "/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="693.76,236 677.76,241 693.76,246 "/> </symbol>
<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> <symbol overflow="visible" id="glyph0-26">
</g> <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 "/>
<g> </symbol>
<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 "/> <symbol overflow="visible" id="glyph0-27">
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="737.953,108 721.953,113 737.953,118 "/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph0-28">
<g> <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 "/>
<ellipse style="fill: #ffffff" cx="103" cy="28" rx="6" ry="6"/> </symbol>
<ellipse style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" cx="103" cy="28" rx="6" ry="6"/> <symbol overflow="visible" id="glyph0-29">
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="79" y1="40" x2="127" y2="40"/> <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 "/>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="34" x2="103" y2="64"/> </symbol>
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="64" x2="79" y2="90"/> <symbol overflow="visible" id="glyph1-0">
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="64" x2="127" y2="90"/> <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 "/>
<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"> </symbol>
<tspan x="103" y="113.9">Customer</tspan> <symbol overflow="visible" id="glyph1-1">
</text> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph1-2">
<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 "/> <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 "/>
<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 "/> </symbol>
<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> <symbol overflow="visible" id="glyph1-3">
</g> <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 "/>
<g> </symbol>
<rect style="fill: #ffffff" x="27" y="258" width="102.8" height="28"/> <symbol overflow="visible" id="glyph1-4">
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="27" y="258" width="102.8" height="28"/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph1-5">
<g> <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 "/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="186.828,154 78.4,154 78.4,258 "/> </symbol>
<polygon style="fill: #000000" points="212,154 198,158.8 184,154 198,149.2 "/> <symbol overflow="visible" id="glyph1-6">
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="212,154 198,158.8 184,154 198,149.2 "/> <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 "/>
<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> </symbol>
<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> <symbol overflow="visible" id="glyph1-7">
<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> <path style="stroke:none;" d=""/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph1-8">
<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 "/> <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 "/>
<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 "/> </symbol>
<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> <symbol overflow="visible" id="glyph1-9">
</g> <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 "/>
<g> </symbol>
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="1036,309.94 1036,381 614.155,381 "/> <symbol overflow="visible" id="glyph1-10">
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="626.803,376 610.803,381 626.803,386 "/> <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 "/>
<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> </symbol>
</g> <symbol overflow="visible" id="glyph1-11">
<g> <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 "/>
<rect style="fill: #ffffff" x="1082.9" y="43.1" width="88.25" height="28"/> </symbol>
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="1082.9" y="43.1" width="88.25" height="28"/> <symbol overflow="visible" id="glyph1-12">
<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> <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 "/>
</g> </symbol>
<g> <symbol overflow="visible" id="glyph1-13">
<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 "/> <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 "/>
<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 "/> </symbol>
<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> <symbol overflow="visible" id="glyph1-14">
</g> <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> </svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -73,6 +73,7 @@ Plugins
:maxdepth: 1 :maxdepth: 1
dev/plugin/base-setup dev/plugin/base-setup
dev/plugin/goal-plugin
dev/plugin/strategy-plugin dev/plugin/strategy-plugin
dev/plugin/action-plugin dev/plugin/action-plugin
dev/plugin/planner-plugin dev/plugin/planner-plugin
@@ -90,6 +91,8 @@ Introduction
deploy/installation deploy/installation
deploy/user-guide deploy/user-guide
deploy/policy
deploy/gmr
Watcher Manual Pages Watcher Manual Pages
==================== ====================

View File

@@ -1,5 +1,37 @@
{ {
"admin_api": "role:admin or role:administrator", "admin_api": "role:admin or role:administrator",
"show_password": "!", "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 = watcher
namespace = keystonemiddleware.auth_token namespace = keystonemiddleware.auth_token
namespace = oslo.log namespace = oslo.cache
namespace = oslo.concurrency
namespace = oslo.db namespace = oslo.db
namespace = oslo.log
namespace = oslo.messaging 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,34 +2,38 @@
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # 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 enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
jsonpatch>=1.1 # BSD jsonpatch>=1.1 # BSD
keystoneauth1>=2.1.0 # Apache-2.0 keystoneauth1>=2.7.0 # Apache-2.0
keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0 keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
oslo.config>=3.7.0 # Apache-2.0 oslo.concurrency>=3.8.0 # Apache-2.0
oslo.context>=0.2.0 # Apache-2.0 oslo.cache>=1.5.0 # Apache-2.0
oslo.config>=3.10.0 # Apache-2.0
oslo.context>=2.4.0 # Apache-2.0
oslo.db>=4.1.0 # Apache-2.0 oslo.db>=4.1.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0 oslo.log>=1.14.0 # Apache-2.0
oslo.messaging>=4.0.0 # Apache-2.0 oslo.messaging>=5.2.0 # Apache-2.0
oslo.policy>=0.5.0 # Apache-2.0 oslo.policy>=1.9.0 # Apache-2.0
oslo.service>=1.0.0 # Apache-2.0 oslo.reports>=0.6.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0 oslo.service>=1.10.0 # Apache-2.0
oslo.utils>=3.14.0 # Apache-2.0
PasteDeploy>=1.5.0 # MIT PasteDeploy>=1.5.0 # MIT
pbr>=1.6 # Apache-2.0 pbr>=1.6 # Apache-2.0
pecan>=1.0.0 # BSD pecan>=1.0.0 # BSD
PrettyTable<0.8,>=0.7 # BSD PrettyTable<0.8,>=0.7 # BSD
voluptuous>=0.8.6 # BSD License voluptuous>=0.8.9 # BSD License
python-ceilometerclient>=2.2.1 # Apache-2.0 python-ceilometerclient>=2.2.1 # Apache-2.0
python-cinderclient>=1.3.1 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0
python-glanceclient>=2.0.0 # Apache-2.0 python-glanceclient>=2.0.0 # Apache-2.0
python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
python-neutronclient!=4.1.0,>=2.6.0 # Apache-2.0 python-neutronclient>=4.2.0 # Apache-2.0
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
python-openstackclient>=2.1.0 # Apache-2.0 python-openstackclient>=2.1.0 # Apache-2.0
six>=1.9.0 # MIT six>=1.9.0 # MIT
SQLAlchemy<1.1.0,>=1.0.10 # MIT SQLAlchemy<1.1.0,>=1.0.10 # MIT
stevedore>=1.5.0 # Apache-2.0 stevedore>=1.10.0 # Apache-2.0
taskflow>=1.26.0 # Apache-2.0 taskflow>=1.26.0 # Apache-2.0
WebOb>=1.2.3 # MIT WebOb>=1.2.3 # MIT
WSME>=0.8 # MIT WSME>=0.8 # MIT

View File

@@ -45,11 +45,22 @@ tempest.test_plugins =
watcher.database.migration_backend = watcher.database.migration_backend =
sqlalchemy = watcher.db.sqlalchemy.migration 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 = watcher_strategies =
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl outlet_temperature = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
workload_stabilization = watcher.decision_engine.strategy.strategies.workload_stabilization:WorkloadStabilization
workload_balance = watcher.decision_engine.strategy.strategies.workload_balance:WorkloadBalance
uniform_airflow = watcher.decision_engine.strategy.strategies.uniform_airflow:UniformAirflow
watcher_actions = watcher_actions =
migrate = watcher.applier.actions.migration:Migrate migrate = watcher.applier.actions.migration:Migrate

View File

@@ -7,9 +7,9 @@ discover # BSD
doc8 # Apache-2.0 doc8 # Apache-2.0
freezegun # Apache-2.0 freezegun # Apache-2.0
hacking<0.11,>=0.10.2 hacking<0.11,>=0.10.2
mock>=1.2 # BSD mock>=2.0 # BSD
oslotest>=1.10.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0
os-testr>=0.4.1 # Apache-2.0 os-testr>=0.7.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD python-subunit>=0.0.18 # Apache-2.0/BSD
testrepository>=0.0.18 # Apache-2.0/BSD testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD
@@ -17,5 +17,11 @@ testtools>=1.4.0 # MIT
# Doc requirements # Doc requirements
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 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 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 = commands =
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
flake8 flake8
bandit -r watcher -x tests -n5 -ll
[testenv:venv] [testenv:venv]
setenv = PYTHONHASHSEED=0 setenv = PYTHONHASHSEED=0
@@ -46,7 +47,7 @@ commands =
show-source=True show-source=True
ignore= ignore=
builtins= _ 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] [testenv:wheel]
commands = python setup.py bdist_wheel commands = python setup.py bdist_wheel
@@ -58,3 +59,10 @@ import_exceptions = watcher._i18n
extension=.rst extension=.rst
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed # 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 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

@@ -15,6 +15,7 @@
# limitations under the License. # limitations under the License.
# #
import oslo_i18n import oslo_i18n
from oslo_i18n import _lazy
# The domain is the name of the App which is used to generate the folder # The domain is the name of the App which is used to generate the folder
# containing the translation files (i.e. the .pot file and the various locales) # containing the translation files (i.e. the .pot file and the various locales)
@@ -42,5 +43,9 @@ _LE = _translators.log_error
_LC = _translators.log_critical _LC = _translators.log_critical
def lazy_translation_enabled():
return _lazy.USE_LAZY
def get_available_languages(): def get_available_languages():
return oslo_i18n.get_available_languages(DOMAIN) return oslo_i18n.get_available_languages(DOMAIN)

View File

@@ -19,24 +19,38 @@
from oslo_config import cfg from oslo_config import cfg
import pecan import pecan
from watcher._i18n import _
from watcher.api import acl from watcher.api import acl
from watcher.api import config as api_config from watcher.api import config as api_config
from watcher.api import middleware from watcher.api import middleware
from watcher.decision_engine.strategy.selection import default \
as strategy_selector
# Register options for the service # Register options for the service
API_SERVICE_OPTS = [ API_SERVICE_OPTS = [
cfg.IntOpt('port', cfg.PortOpt('port',
default=9322, default=9322,
help='The port for the watcher API server'), help=_('The port for the watcher API server')),
cfg.StrOpt('host', cfg.StrOpt('host',
default='0.0.0.0', default='127.0.0.1',
help='The listen IP for the watcher API server'), help=_('The listen IP for the watcher API server')),
cfg.IntOpt('max_limit', cfg.IntOpt('max_limit',
default=1000, default=1000,
help='The maximum number of items returned in a single ' help=_('The maximum number of items returned in a single '
'response from a collection resource.') 'response from a collection resource')),
cfg.IntOpt('workers',
min=1,
help=_('Number of workers for Watcher API service. '
'The default is equal to the number of CPUs available '
'if that can be determined, else a default worker '
'count of 1 is returned.')),
cfg.BoolOpt('enable_ssl_api',
default=False,
help=_("Enable the integrated stand-alone API to service "
"requests via HTTPS instead of HTTP. If there is a "
"front-end service performing HTTPS offloading from "
"the service, this option should be False; note, you "
"will want to change public API endpoint to represent "
"SSL termination URL with 'public_endpoint' option.")),
] ]
CONF = cfg.CONF CONF = cfg.CONF
@@ -45,7 +59,6 @@ opt_group = cfg.OptGroup(name='api',
CONF.register_group(opt_group) CONF.register_group(opt_group)
CONF.register_opts(API_SERVICE_OPTS, opt_group) CONF.register_opts(API_SERVICE_OPTS, opt_group)
CONF.register_opts(strategy_selector.WATCHER_GOALS_OPTS)
def get_pecan_config(): def get_pecan_config():
@@ -68,3 +81,12 @@ def setup_app(config=None):
) )
return acl.install(app, CONF, config.app.acl_public_routes) return acl.install(app, CONF, config.app.acl_public_routes)
class VersionSelectorApplication(object):
def __init__(self):
pc = get_pecan_config()
self.v1 = setup_app(config=pc)
def __call__(self, environ, start_response):
return self.v1(environ, start_response)

View File

@@ -22,7 +22,7 @@ from watcher.api import hooks
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa # See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
server = { server = {
'port': '9322', 'port': '9322',
'host': '0.0.0.0' 'host': '127.0.0.1'
} }
# Pecan Application Configurations # Pecan Application Configurations

View File

@@ -34,6 +34,7 @@ from watcher.api.controllers.v1 import action_plan
from watcher.api.controllers.v1 import audit from watcher.api.controllers.v1 import audit
from watcher.api.controllers.v1 import audit_template from watcher.api.controllers.v1 import audit_template
from watcher.api.controllers.v1 import goal from watcher.api.controllers.v1 import goal
from watcher.api.controllers.v1 import strategy
class APIBase(wtypes.Base): class APIBase(wtypes.Base):
@@ -157,6 +158,7 @@ class Controller(rest.RestController):
actions = action.ActionsController() actions = action.ActionsController()
action_plans = action_plan.ActionPlansController() action_plans = action_plan.ActionPlansController()
goals = goal.GoalsController() goals = goal.GoalsController()
strategies = strategy.StrategiesController()
@wsme_pecan.wsexpose(V1) @wsme_pecan.wsexpose(V1)
def get(self): def get(self):
@@ -165,4 +167,5 @@ class Controller(rest.RestController):
# the request object to make the links. # the request object to make the links.
return V1.convert() return V1.convert()
__all__ = (Controller)
__all__ = ("Controller", )

View File

@@ -63,12 +63,14 @@ import wsme
from wsme import types as wtypes from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from watcher._i18n import _
from watcher.api.controllers import base from watcher.api.controllers import base
from watcher.api.controllers import link from watcher.api.controllers import link
from watcher.api.controllers.v1 import collection from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher import objects from watcher import objects
@@ -119,7 +121,7 @@ class Action(base.APIBase):
self.action_next_uuid = None self.action_next_uuid = None
# raise e # raise e
uuid = types.uuid uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action""" """Unique UUID for this action"""
action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid, action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid,
@@ -130,9 +132,6 @@ class Action(base.APIBase):
state = wtypes.text state = wtypes.text
"""This audit state""" """This audit state"""
alarm = types.uuid
"""An alarm UUID related to this action"""
action_type = wtypes.text action_type = wtypes.text
"""Action type""" """Action type"""
@@ -194,7 +193,6 @@ class Action(base.APIBase):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
description='action description', description='action description',
state='PENDING', state='PENDING',
alarm=None,
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
deleted_at=None, deleted_at=None,
updated_at=datetime.datetime.utcnow()) updated_at=datetime.datetime.utcnow())
@@ -306,6 +304,10 @@ class ActionsController(rest.RestController):
:param audit_uuid: Optional UUID of an audit, :param audit_uuid: Optional UUID of an audit,
to get only actions for that 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: if action_plan_uuid and audit_uuid:
raise exception.ActionFilterCombinationProhibited raise exception.ActionFilterCombinationProhibited
@@ -330,6 +332,10 @@ class ActionsController(rest.RestController):
:param audit_uuid: Optional UUID of an audit, :param audit_uuid: Optional UUID of an audit,
to get only actions for that 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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "actions": if parent != "actions":
@@ -353,8 +359,10 @@ class ActionsController(rest.RestController):
if self.from_actions: if self.from_actions:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
action = objects.Action.get_by_uuid(pecan.request.context, context = pecan.request.context
action_uuid) action = api_utils.get_resource('Action', action_uuid)
policy.enforce(context, 'action:get', action, action='action:get')
return Action.convert_with_links(action) return Action.convert_with_links(action)
@wsme_pecan.wsexpose(Action, body=Action, status_code=201) @wsme_pecan.wsexpose(Action, body=Action, status_code=201)

View File

@@ -16,9 +16,11 @@
# limitations under the License. # 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 :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 An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
:ref:`Audit <audit_definition>` is successful which implies that the :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 which was used has found a :ref:`Solution <solution_definition>` to achieve the
:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`. :ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`.
In the default implementation of Watcher, an In the default implementation of Watcher, an action plan is composed of
:ref:`Action Plan <action_plan_definition>` a list of successive :ref:`Actions <action_definition>` (i.e., a Workflow of
is only composed of successive :ref:`Actions <action_definition>` :ref:`Actions <action_definition>` belonging to a unique branch).
(i.e., a Workflow of :ref:`Actions <action_definition>` belonging to a unique
branch).
However, Watcher provides abstract interfaces for many of its components, However, Watcher provides abstract interfaces for many of its components,
allowing other implementations to generate and handle more complex allowing other implementations to generate and handle more complex :ref:`Action
:ref:`Action Plan(s) <action_plan_definition>` Plan(s) <action_plan_definition>` composed of two types of Action Item(s):
composed of two types of Action Item(s):
- simple :ref:`Actions <action_definition>`: atomic tasks, which means it - simple :ref:`Actions <action_definition>`: atomic tasks, which means it
can not be split into smaller tasks or commands from an OpenStack point of 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 An :ref:`Action Plan <action_plan_definition>` may be described using
standard workflow model description formats such as 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/>`_ `Business Process Model and Notation 2.0 (BPMN 2.0)
or `Unified Modeling Language (UML) <http://www.uml.org/>`_. <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 To see the life-cycle and description of
:ref:`Action Plan <action_plan_definition>` states, visit :ref:`the Action Plan state :ref:`Action Plan <action_plan_definition>` states, visit :ref:`the Action Plan
machine <action_plan_state_machine>`. state machine <action_plan_state_machine>`.
""" # noqa """
import datetime import datetime
from oslo_log import log
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
@@ -66,13 +67,17 @@ from watcher._i18n import _
from watcher.api.controllers import base from watcher.api.controllers import base
from watcher.api.controllers import link from watcher.api.controllers import link
from watcher.api.controllers.v1 import collection from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import efficacy_indicator as efficacyindicator
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.applier import rpcapi from watcher.applier import rpcapi
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher import objects from watcher import objects
from watcher.objects import action_plan as ap_objects from watcher.objects import action_plan as ap_objects
LOG = log.getLogger(__name__)
class ActionPlanPatchType(types.JsonPatchType): class ActionPlanPatchType(types.JsonPatchType):
@@ -113,6 +118,7 @@ class ActionPlan(base.APIBase):
_audit_uuid = None _audit_uuid = None
_first_action_uuid = None _first_action_uuid = None
_efficacy_indicators = None
def _get_audit_uuid(self): def _get_audit_uuid(self):
return self._audit_uuid return self._audit_uuid
@@ -143,7 +149,35 @@ class ActionPlan(base.APIBase):
except exception.ActionNotFound: except exception.ActionNotFound:
self._first_action_uuid = None self._first_action_uuid = None
uuid = types.uuid 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""" """Unique UUID for this action plan"""
first_action_uuid = wsme.wsproperty( first_action_uuid = wsme.wsproperty(
@@ -155,6 +189,14 @@ class ActionPlan(base.APIBase):
mandatory=True) mandatory=True)
"""The UUID of the audit this port belongs to""" """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 state = wtypes.text
"""This action plan state""" """This action plan state"""
@@ -163,7 +205,6 @@ class ActionPlan(base.APIBase):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(ActionPlan, self).__init__() super(ActionPlan, self).__init__()
self.fields = [] self.fields = []
fields = list(objects.ActionPlan.fields) fields = list(objects.ActionPlan.fields)
for field in fields: for field in fields:
@@ -175,6 +216,7 @@ class ActionPlan(base.APIBase):
self.fields.append('audit_uuid') self.fields.append('audit_uuid')
self.fields.append('first_action_uuid') self.fields.append('first_action_uuid')
self.fields.append('efficacy_indicators')
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset)) setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
setattr(self, 'first_action_uuid', setattr(self, 'first_action_uuid',
@@ -184,12 +226,13 @@ class ActionPlan(base.APIBase):
def _convert_with_links(action_plan, url, expand=True): def _convert_with_links(action_plan, url, expand=True):
if not expand: if not expand:
action_plan.unset_fields_except( action_plan.unset_fields_except(
['uuid', 'state', 'updated_at', ['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
'audit_uuid', 'first_action_uuid']) 'updated_at', 'audit_uuid', 'first_action_uuid'])
action_plan.links = [link.Link.make_link( action_plan.links = [
'self', url, link.Link.make_link(
'action_plans', action_plan.uuid), 'self', url,
'action_plans', action_plan.uuid),
link.Link.make_link( link.Link.make_link(
'bookmark', url, 'bookmark', url,
'action_plans', action_plan.uuid, 'action_plans', action_plan.uuid,
@@ -211,6 +254,12 @@ class ActionPlan(base.APIBase):
updated_at=datetime.datetime.utcnow()) updated_at=datetime.datetime.utcnow())
sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720' sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720'
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6' 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) 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 :param audit_uuid: Optional UUID of an audit, to get only actions
for that audit. 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( return self._get_action_plans_collection(
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid) 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 :param audit_uuid: Optional UUID of an audit, to get only actions
for that audit. 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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "action_plans": if parent != "action_plans":
@@ -347,8 +404,11 @@ class ActionPlansController(rest.RestController):
if self.from_actionsPlans: if self.from_actionsPlans:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
action_plan = objects.ActionPlan.get_by_uuid( context = pecan.request.context
pecan.request.context, action_plan_uuid) 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) return ActionPlan.convert_with_links(action_plan)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204) @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. :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( action_plan.soft_delete()
pecan.request.context,
action_plan_uuid)
action_plan_to_delete.soft_delete()
@wsme.validate(types.uuid, [ActionPlanPatchType]) @wsme.validate(types.uuid, [ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid, @wsme_pecan.wsexpose(ActionPlan, types.uuid,
@@ -376,9 +437,12 @@ class ActionPlansController(rest.RestController):
if self.from_actionsPlans: if self.from_actionsPlans:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
action_plan_to_update = objects.ActionPlan.get_by_uuid( context = pecan.request.context
pecan.request.context, action_plan_to_update = api_utils.get_resource('ActionPlan',
action_plan_uuid) action_plan_uuid)
policy.enforce(context, 'action_plan:update', action_plan_to_update,
action='action_plan:update')
try: try:
action_plan_dict = action_plan_to_update.as_dict() action_plan_dict = action_plan_to_update.as_dict()
action_plan = ActionPlan(**api_utils.apply_jsonpatch( action_plan = ActionPlan(**api_utils.apply_jsonpatch(

View File

@@ -44,16 +44,64 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher.common import utils from watcher.common import utils
from watcher.decision_engine import rpcapi from watcher.decision_engine import rpcapi
from watcher import objects from watcher import objects
class AuditPostType(wtypes.Base):
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=True)
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.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,
audit_type=self.audit_type,
deadline=self.deadline,
parameters=self.parameters,
interval=self.interval)
class AuditPatchType(types.JsonPatchType): class AuditPatchType(types.JsonPatchType):
@staticmethod @staticmethod
def mandatory_attrs(): 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): class Audit(base.APIBase):
@@ -105,7 +153,7 @@ class Audit(base.APIBase):
uuid = types.uuid uuid = types.uuid
"""Unique UUID for this audit""" """Unique UUID for this audit"""
type = wtypes.text audit_type = wtypes.text
"""Type of this audit""" """Type of this audit"""
deadline = datetime.datetime deadline = datetime.datetime
@@ -126,9 +174,15 @@ class Audit(base.APIBase):
mandatory=False) mandatory=False)
"""The name of the audit template this audit refers to""" """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) links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated audit links""" """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): def __init__(self, **kwargs):
self.fields = [] self.fields = []
fields = list(objects.Audit.fields) fields = list(objects.Audit.fields)
@@ -154,9 +208,9 @@ class Audit(base.APIBase):
@staticmethod @staticmethod
def _convert_with_links(audit, url, expand=True): def _convert_with_links(audit, url, expand=True):
if not expand: if not expand:
audit.unset_fields_except(['uuid', 'type', 'deadline', audit.unset_fields_except(['uuid', 'audit_type', 'deadline',
'state', 'audit_template_uuid', 'state', 'audit_template_uuid',
'audit_template_name']) 'audit_template_name', 'interval'])
# The numeric ID should not be exposed to # The numeric ID should not be exposed to
# the user, it's internal only. # the user, it's internal only.
@@ -179,12 +233,13 @@ class Audit(base.APIBase):
@classmethod @classmethod
def sample(cls, expand=True): def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
type='ONESHOT', audit_type='ONESHOT',
state='PENDING', state='PENDING',
deadline=None, deadline=None,
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
deleted_at=None, deleted_at=None,
updated_at=datetime.datetime.utcnow()) updated_at=datetime.datetime.utcnow(),
interval=7200)
sample._audit_template_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae' sample._audit_template_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
return cls._convert_with_links(sample, 'http://localhost:9322', expand) return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -273,34 +328,41 @@ class AuditsController(rest.RestController):
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) 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) wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, def get_all(self, audit_template=None, marker=None, limit=None,
sort_key='id', sort_dir='asc', audit_template=None): sort_key='id', sort_dir='asc'):
"""Retrieve a list of audits. """Retrieve a list of audits.
:param audit_template: Optional UUID or name of an audit
:param marker: pagination marker for large data sets. :param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
:param audit_template: Optional UUID or name of an audit
template, to get only audits for that audit template. 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, return self._get_audits_collection(marker, limit, sort_key,
sort_dir, sort_dir,
audit_template=audit_template) audit_template=audit_template)
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text, @wsme_pecan.wsexpose(AuditCollection, wtypes.text, types.uuid, int,
wtypes.text) wtypes.text, wtypes.text)
def detail(self, marker=None, limit=None, def detail(self, audit_template=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'): sort_key='id', sort_dir='asc'):
"""Retrieve a list of audits with detail. """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 marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
""" """
context = pecan.request.context
policy.enforce(context, 'audit:detail',
action='audit:detail')
# NOTE(lucasagomes): /detail should only work agaist collections # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audits": if parent != "audits":
@@ -310,7 +372,8 @@ class AuditsController(rest.RestController):
resource_url = '/'.join(['audits', 'detail']) resource_url = '/'.join(['audits', 'detail'])
return self._get_audits_collection(marker, limit, return self._get_audits_collection(marker, limit,
sort_key, sort_dir, expand, sort_key, sort_dir, expand,
resource_url) resource_url,
audit_template=audit_template)
@wsme_pecan.wsexpose(Audit, types.uuid) @wsme_pecan.wsexpose(Audit, types.uuid)
def get_one(self, audit_uuid): def get_one(self, audit_uuid):
@@ -321,16 +384,23 @@ class AuditsController(rest.RestController):
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
rpc_audit = objects.Audit.get_by_uuid(pecan.request.context, context = pecan.request.context
audit_uuid) 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) return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=Audit, status_code=201) @wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201)
def post(self, audit): def post(self, audit_p):
"""Create a new audit. """Create a new audit.
:param audit: a audit within the request body. :param audit_p: a audit within the request body.
""" """
context = pecan.request.context
policy.enforce(context, 'audit:create',
action='audit:create')
audit = audit_p.as_audit()
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
@@ -339,6 +409,25 @@ class AuditsController(rest.RestController):
message=_('The audit template UUID or name specified is ' message=_('The audit template UUID or name specified is '
'invalid')) '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() audit_dict = audit.as_dict()
context = pecan.request.context context = pecan.request.context
new_audit = objects.Audit(context, **audit_dict) new_audit = objects.Audit(context, **audit_dict)
@@ -349,8 +438,9 @@ class AuditsController(rest.RestController):
# trigger decision-engine to run the audit # trigger decision-engine to run the audit
dc_client = rpcapi.DecisionEngineAPI() if new_audit.audit_type == objects.audit.AuditType.ONESHOT.value:
dc_client.trigger_audit(context, new_audit.uuid) dc_client = rpcapi.DecisionEngineAPI()
dc_client.trigger_audit(context, new_audit.uuid)
return Audit.convert_with_links(new_audit) return Audit.convert_with_links(new_audit)
@@ -365,6 +455,12 @@ class AuditsController(rest.RestController):
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted 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_to_update = objects.Audit.get_by_uuid(pecan.request.context,
audit_uuid) audit_uuid)
try: try:
@@ -394,8 +490,9 @@ class AuditsController(rest.RestController):
:param audit_uuid: UUID of a audit. :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() audit_to_delete.soft_delete()

View File

@@ -50,42 +50,163 @@ provided as a list of key-value pairs.
import datetime import datetime
from oslo_config import cfg
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
from wsme import types as wtypes from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from watcher._i18n import _
from watcher.api.controllers import base from watcher.api.controllers import base
from watcher.api.controllers import link from watcher.api.controllers import link
from watcher.api.controllers.v1 import collection from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import context as context_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils from watcher.common import utils as common_utils
from watcher import objects from watcher import objects
class AuditTemplatePostType(wtypes.Base):
_ctx = context_utils.make_context()
name = wtypes.wsattr(wtypes.text, mandatory=True)
"""Name of this audit template"""
description = wtypes.wsattr(wtypes.text, mandatory=False)
"""Short description of this audit template"""
deadline = wsme.wsattr(datetime.datetime, mandatory=False)
"""deadline of the audit template"""
host_aggregate = wsme.wsattr(wtypes.IntegerType(minimum=1),
mandatory=False)
"""ID of the Nova host aggregate targeted by the audit template"""
extra = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False)
"""The metadata of the audit template"""
goal = wtypes.wsattr(wtypes.text, mandatory=True)
"""Goal UUID or name of the audit template"""
strategy = wtypes.wsattr(wtypes.text, mandatory=False)
"""Strategy UUID or name of the audit template"""
version = wtypes.text
"""Internal version of the audit template"""
def as_audit_template(self):
return AuditTemplate(
name=self.name,
description=self.description,
deadline=self.deadline,
host_aggregate=self.host_aggregate,
extra=self.extra,
goal_id=self.goal, # Dirty trick ...
goal=self.goal,
strategy_id=self.strategy, # Dirty trick ...
strategy_uuid=self.strategy,
version=self.version,
)
@staticmethod
def validate(audit_template):
available_goals = objects.Goal.list(AuditTemplatePostType._ctx)
available_goal_uuids_map = {g.uuid: g for g in available_goals}
available_goal_names_map = {g.name: g for g in available_goals}
if audit_template.goal in available_goal_uuids_map:
goal = available_goal_uuids_map[audit_template.goal]
elif audit_template.goal in available_goal_names_map:
goal = available_goal_names_map[audit_template.goal]
else:
raise exception.InvalidGoal(goal=audit_template.goal)
if audit_template.strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePostType._ctx)
available_strategies_map = {
s.uuid: s for s in available_strategies}
if audit_template.strategy not in available_strategies_map:
raise exception.InvalidStrategy(
strategy=audit_template.strategy)
strategy = available_strategies_map[audit_template.strategy]
# Check that the strategy we indicate is actually related to the
# specified goal
if strategy.goal_id != goal.id:
choices = ["'%s' (%s)" % (s.uuid, s.name)
for s in available_strategies]
raise exception.InvalidStrategy(
message=_(
"'%(strategy)s' strategy does relate to the "
"'%(goal)s' goal. Possible choices: %(choices)s")
% dict(strategy=strategy.name, goal=goal.name,
choices=", ".join(choices)))
audit_template.strategy = strategy.uuid
# We force the UUID so that we do not need to query the DB with the
# name afterwards
audit_template.goal = goal.uuid
return audit_template
class AuditTemplatePatchType(types.JsonPatchType): class AuditTemplatePatchType(types.JsonPatchType):
_ctx = context_utils.make_context()
@staticmethod @staticmethod
def mandatory_attrs(): def mandatory_attrs():
return [] return []
@staticmethod @staticmethod
def validate(patch): def validate(patch):
if patch.path == "/goal": if patch.path == "/goal" and patch.op != "remove":
AuditTemplatePatchType._validate_goal(patch) AuditTemplatePatchType._validate_goal(patch)
elif patch.path == "/goal" and patch.op == "remove":
raise exception.OperationNotPermitted(
_("Cannot remove 'goal' attribute "
"from an audit template"))
if patch.path == "/strategy":
AuditTemplatePatchType._validate_strategy(patch)
return types.JsonPatchType.validate(patch) return types.JsonPatchType.validate(patch)
@staticmethod @staticmethod
def _validate_goal(patch): def _validate_goal(patch):
serialized_patch = {'path': patch.path, 'op': patch.op} patch.path = "/goal_id"
if patch.value is not wsme.Unset: goal = patch.value
serialized_patch['value'] = patch.value
new_goal = patch.value if goal:
if new_goal and new_goal not in cfg.CONF.watcher_goals.goals.keys(): available_goals = objects.Goal.list(
raise exception.InvalidGoal(goal=new_goal) AuditTemplatePatchType._ctx)
available_goal_uuids_map = {g.uuid: g for g in available_goals}
available_goal_names_map = {g.name: g for g in available_goals}
if goal in available_goal_uuids_map:
patch.value = available_goal_uuids_map[goal].id
elif goal in available_goal_names_map:
patch.value = available_goal_names_map[goal].id
else:
raise exception.InvalidGoal(goal=goal)
@staticmethod
def _validate_strategy(patch):
patch.path = "/strategy_id"
strategy = patch.value
if strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePatchType._ctx)
available_strategy_uuids_map = {
s.uuid: s for s in available_strategies}
available_strategy_names_map = {
s.name: s for s in available_strategies}
if strategy in available_strategy_uuids_map:
patch.value = available_strategy_uuids_map[strategy].id
elif strategy in available_strategy_names_map:
patch.value = available_strategy_names_map[strategy].id
else:
raise exception.InvalidStrategy(strategy=strategy)
class AuditTemplate(base.APIBase): class AuditTemplate(base.APIBase):
@@ -95,7 +216,90 @@ class AuditTemplate(base.APIBase):
between the internal object model and the API representation of an between the internal object model and the API representation of an
audit template. audit template.
""" """
uuid = types.uuid
_goal_uuid = None
_goal_name = None
_strategy_uuid = None
_strategy_name = None
def _get_goal(self, value):
if value == wtypes.Unset:
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
goal = objects.Goal.get(
pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(
pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
self.goal_id = goal.id
return goal
def _get_strategy(self, value):
if value == wtypes.Unset:
return None
strategy = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
strategy = objects.Strategy.get(
pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
except exception.StrategyNotFound:
pass
if strategy:
self.strategy_id = strategy.id
return strategy
def _get_goal_uuid(self):
return self._goal_uuid
def _set_goal_uuid(self, value):
if value and self._goal_uuid != value:
self._goal_uuid = None
goal = self._get_goal(value)
if goal:
self._goal_uuid = goal.uuid
def _get_strategy_uuid(self):
return self._strategy_uuid
def _set_strategy_uuid(self, value):
if value and self._strategy_uuid != value:
self._strategy_uuid = None
strategy = self._get_strategy(value)
if strategy:
self._strategy_uuid = strategy.uuid
def _get_goal_name(self):
return self._goal_name
def _set_goal_name(self, value):
if value and self._goal_name != value:
self._goal_name = None
goal = self._get_goal(value)
if goal:
self._goal_name = goal.name
def _get_strategy_name(self):
return self._strategy_name
def _set_strategy_name(self, value):
if value and self._strategy_name != value:
self._strategy_name = None
strategy = self._get_strategy(value)
if strategy:
self._strategy_name = strategy.name
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this audit template""" """Unique UUID for this audit template"""
name = wtypes.text name = wtypes.text
@@ -113,8 +317,21 @@ class AuditTemplate(base.APIBase):
extra = {wtypes.text: types.jsontype} extra = {wtypes.text: types.jsontype}
"""The metadata of the audit template""" """The metadata of the audit template"""
goal = wtypes.text goal_uuid = wsme.wsproperty(
"""Goal type of the audit template""" wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
"""Goal UUID the audit template refers to"""
goal_name = wsme.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
"""The name of the goal this audit template refers to"""
strategy_uuid = wsme.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
"""Strategy UUID the audit template refers to"""
strategy_name = wsme.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
"""The name of the strategy this audit template refers to"""
version = wtypes.text version = wtypes.text
"""Internal version of the audit template""" """Internal version of the audit template"""
@@ -127,20 +344,43 @@ class AuditTemplate(base.APIBase):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(AuditTemplate, self).__init__() super(AuditTemplate, self).__init__()
self.fields = [] self.fields = []
for field in objects.AuditTemplate.fields: fields = list(objects.AuditTemplate.fields)
for k in fields:
# Skip fields we do not expose. # Skip fields we do not expose.
if not hasattr(self, field): if not hasattr(self, k):
continue continue
self.fields.append(field) self.fields.append(k)
setattr(self, field, kwargs.get(field, wtypes.Unset)) setattr(self, k, kwargs.get(k, wtypes.Unset))
self.fields.append('goal_id')
self.fields.append('strategy_id')
# goal_uuid & strategy_uuid are not part of
# objects.AuditTemplate.fields because they're API-only attributes.
self.fields.append('goal_uuid')
self.fields.append('goal_name')
self.fields.append('strategy_uuid')
self.fields.append('strategy_name')
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'strategy_uuid',
kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_name',
kwargs.get('strategy_id', wtypes.Unset))
@staticmethod @staticmethod
def _convert_with_links(audit_template, url, expand=True): def _convert_with_links(audit_template, url, expand=True):
if not expand: if not expand:
audit_template.unset_fields_except(['uuid', 'name', audit_template.unset_fields_except(
'host_aggregate', 'goal']) ['uuid', 'name', 'host_aggregate', 'goal_uuid', 'goal_name',
'strategy_uuid', 'strategy_name'])
# The numeric ID should not be exposed to
# the user, it's internal only.
audit_template.goal_id = wtypes.Unset
audit_template.strategy_id = wtypes.Unset
audit_template.links = [link.Link.make_link('self', url, audit_template.links = [link.Link.make_link('self', url,
'audit_templates', 'audit_templates',
@@ -148,8 +388,7 @@ class AuditTemplate(base.APIBase):
link.Link.make_link('bookmark', url, link.Link.make_link('bookmark', url,
'audit_templates', 'audit_templates',
audit_template.uuid, audit_template.uuid,
bookmark=True) bookmark=True)]
]
return audit_template return audit_template
@classmethod @classmethod
@@ -164,19 +403,14 @@ class AuditTemplate(base.APIBase):
name='My Audit Template', name='My Audit Template',
description='Description of my audit template', description='Description of my audit template',
host_aggregate=5, host_aggregate=5,
goal='DUMMY', goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
extra={'automatic': True}, extra={'automatic': True},
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
deleted_at=None, deleted_at=None,
updated_at=datetime.datetime.utcnow()) updated_at=datetime.datetime.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9322', expand) return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@staticmethod
def validate(audit_template):
if audit_template.goal not in cfg.CONF.watcher_goals.goals.keys():
raise exception.InvalidGoal(audit_template.goal)
return audit_template
class AuditTemplateCollection(collection.Collection): class AuditTemplateCollection(collection.Collection):
"""API representation of a collection of audit templates.""" """API representation of a collection of audit templates."""
@@ -191,12 +425,12 @@ class AuditTemplateCollection(collection.Collection):
@staticmethod @staticmethod
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False, def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
**kwargs): **kwargs):
collection = AuditTemplateCollection() at_collection = AuditTemplateCollection()
collection.audit_templates = \ at_collection.audit_templates = [
[AuditTemplate.convert_with_links(p, expand) AuditTemplate.convert_with_links(p, expand)
for p in rpc_audit_templates] for p in rpc_audit_templates]
collection.next = collection.get_next(limit, url=url, **kwargs) at_collection.next = at_collection.get_next(limit, url=url, **kwargs)
return collection return at_collection
@classmethod @classmethod
def sample(cls): def sample(cls):
@@ -222,7 +456,8 @@ class AuditTemplatesController(rest.RestController):
sort_key, sort_dir, expand=False, sort_key, sort_dir, expand=False,
resource_url=None): resource_url=None):
api_utils.validate_search_filters( api_utils.validate_search_filters(
filters, objects.audit_template.AuditTemplate.fields.keys()) filters, list(objects.audit_template.AuditTemplate.fields.keys()) +
["goal_uuid", "goal_name", "strategy_uuid", "strategy_name"])
limit = api_utils.validate_limit(limit) limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir) api_utils.validate_sort_dir(sort_dir)
@@ -246,41 +481,72 @@ class AuditTemplatesController(rest.RestController):
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) sort_dir=sort_dir)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, @wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text) types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None, def get_all(self, goal=None, strategy=None, marker=None,
sort_key='id', sort_dir='asc'): limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of audit templates. """Retrieve a list of audit templates.
:param goal: goal name to filter by (case sensitive) :param goal: goal UUID or name to filter by
:param strategy: strategy UUID or name to filter by
:param marker: pagination marker for large data sets. :param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
""" """
filters = api_utils.as_filters_dict(goal=goal) 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):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
if strategy:
if common_utils.is_uuid_like(strategy):
filters['strategy_uuid'] = strategy
else:
filters['strategy_name'] = strategy
return self._get_audit_templates_collection( return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir) filters, marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, types.uuid, int, @wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
wtypes.text, wtypes.text) types.uuid, int, wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None, def detail(self, goal=None, strategy=None, marker=None,
sort_key='id', sort_dir='asc'): limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of audit templates with detail. """Retrieve a list of audit templates with detail.
:param goal: goal name to filter by (case sensitive) :param goal: goal UUID or name to filter by
:param strategy: strategy UUID or name to filter by
:param marker: pagination marker for large data sets. :param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audit_templates": if parent != "audit_templates":
raise exception.HTTPNotFound raise exception.HTTPNotFound
filters = api_utils.as_filters_dict(goal=goal) filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
if strategy:
if common_utils.is_uuid_like(strategy):
filters['strategy_uuid'] = strategy
else:
filters['strategy_name'] = strategy
expand = True expand = True
resource_url = '/'.join(['audit_templates', 'detail']) resource_url = '/'.join(['audit_templates', 'detail'])
@@ -297,30 +563,33 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
if common_utils.is_uuid_like(audit_template): context = pecan.request.context
rpc_audit_template = objects.AuditTemplate.get_by_uuid( rpc_audit_template = api_utils.get_resource('AuditTemplate',
pecan.request.context, audit_template)
audit_template) policy.enforce(context, 'audit_template:get', rpc_audit_template,
else: action='audit_template:get')
rpc_audit_template = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
return AuditTemplate.convert_with_links(rpc_audit_template) return AuditTemplate.convert_with_links(rpc_audit_template)
@wsme.validate(types.uuid, AuditTemplate) @wsme.validate(types.uuid, AuditTemplatePostType)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplate, status_code=201) @wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
def post(self, audit_template): status_code=201)
def post(self, audit_template_postdata):
"""Create a new audit template. """Create a new audit template.
:param audit template: a audit template within the request body. :param audit_template_postdata: the audit template POST data
from the request body.
""" """
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
audit_template_dict = audit_template.as_dict()
context = pecan.request.context context = pecan.request.context
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()
new_audit_template = objects.AuditTemplate(context, new_audit_template = objects.AuditTemplate(context,
**audit_template_dict) **audit_template_dict)
new_audit_template.create(context) new_audit_template.create(context)
@@ -342,6 +611,13 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted 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): if common_utils.is_uuid_like(audit_template):
audit_template_to_update = objects.AuditTemplate.get_by_uuid( audit_template_to_update = objects.AuditTemplate.get_by_uuid(
pecan.request.context, pecan.request.context,
@@ -379,14 +655,11 @@ class AuditTemplatesController(rest.RestController):
:param audit template_uuid: UUID or name of an audit template. :param audit template_uuid: UUID or name of an audit template.
""" """
context = pecan.request.context
if common_utils.is_uuid_like(audit_template): audit_template_to_delete = api_utils.get_resource('AuditTemplate',
audit_template_to_delete = objects.AuditTemplate.get_by_uuid( audit_template)
pecan.request.context, policy.enforce(context, 'audit_template:update',
audit_template) audit_template_to_delete,
else: action='audit_template:update')
audit_template_to_delete = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
audit_template_to_delete.soft_delete() audit_template_to_delete.soft_delete()

View File

@@ -44,7 +44,7 @@ class Collection(base.APIBase):
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
'args': q_args, 'limit': limit, 'args': q_args, 'limit': limit,
'marker': self.collection[-1].uuid} 'marker': getattr(self.collection[-1], "uuid")}
return link.Link.make_link('next', pecan.request.host_url, return link.Link.make_link('next', pecan.request.host_url,
resource_url, next_args).href resource_url, next_args).href

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,61 +46,75 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher import objects
CONF = cfg.CONF CONF = cfg.CONF
class Goal(base.APIBase): class Goal(base.APIBase):
"""API representation of a action. """API representation of a goal.
This class enforces type checking and value constraints, and converts This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a action. between the internal object model and the API representation of a goal.
""" """
uuid = types.uuid
"""Unique UUID for this goal"""
name = wtypes.text name = wtypes.text
"""Name of the goal""" """Name of the goal"""
strategy = wtypes.text display_name = wtypes.text
"""The strategy associated with the goal""" """Localized name of the goal"""
uuid = types.uuid efficacy_specification = wtypes.wsattr(types.jsontype, readonly=True)
"""Unused field""" """Efficacy specification for this goal"""
links = wsme.wsattr([link.Link], readonly=True) links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated action links""" """A list containing a self link and associated audit template links"""
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Goal, self).__init__()
self.fields = [] self.fields = []
self.fields.append('name') fields = list(objects.Goal.fields)
self.fields.append('strategy')
setattr(self, 'name', kwargs.get('name', for k in fields:
wtypes.Unset)) # Skip fields we do not expose.
setattr(self, 'strategy', kwargs.get('strategy', if not hasattr(self, k):
wtypes.Unset)) continue
self.fields.append(k)
setattr(self, k, kwargs.get(k, wtypes.Unset))
@staticmethod @staticmethod
def _convert_with_links(goal, url, expand=True): def _convert_with_links(goal, url, expand=True):
if not expand: if not expand:
goal.unset_fields_except(['name', 'strategy']) goal.unset_fields_except(['uuid', 'name', 'display_name',
'efficacy_specification'])
goal.links = [link.Link.make_link('self', url, goal.links = [link.Link.make_link('self', url,
'goals', goal.name), 'goals', goal.uuid),
link.Link.make_link('bookmark', url, link.Link.make_link('bookmark', url,
'goals', goal.name, 'goals', goal.uuid,
bookmark=True)] bookmark=True)]
return goal return goal
@classmethod @classmethod
def convert_with_links(cls, goal, expand=True): def convert_with_links(cls, goal, expand=True):
goal = Goal(**goal) goal = Goal(**goal.as_dict())
return cls._convert_with_links(goal, pecan.request.host_url, expand) return cls._convert_with_links(goal, pecan.request.host_url, expand)
@classmethod @classmethod
def sample(cls, expand=True): def sample(cls, expand=True):
sample = cls(name='27e3153e-d5bf-4b7e-b517-fb518e17f34c', sample = cls(
strategy='action description') 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) return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -117,27 +131,28 @@ class GoalCollection(collection.Collection):
@staticmethod @staticmethod
def convert_with_links(goals, limit, url=None, expand=False, def convert_with_links(goals, limit, url=None, expand=False,
**kwargs): **kwargs):
goal_collection = GoalCollection()
collection = GoalCollection() goal_collection.goals = [
collection.goals = [Goal.convert_with_links(g, expand) for g in goals] Goal.convert_with_links(g, expand) for g in goals]
if 'sort_key' in kwargs: if 'sort_key' in kwargs:
reverse = False reverse = False
if kwargs['sort_key'] == 'strategy': if kwargs['sort_key'] == 'strategy':
if 'sort_dir' in kwargs: if 'sort_dir' in kwargs:
reverse = True if kwargs['sort_dir'] == 'desc' else False reverse = True if kwargs['sort_dir'] == 'desc' else False
collection.goals = sorted( goal_collection.goals = sorted(
collection.goals, goal_collection.goals,
key=lambda goal: goal.name, key=lambda goal: goal.uuid,
reverse=reverse) reverse=reverse)
collection.next = collection.get_next(limit, url=url, **kwargs) goal_collection.next = goal_collection.get_next(
return collection limit, url=url, **kwargs)
return goal_collection
@classmethod @classmethod
def sample(cls): def sample(cls):
sample = cls() sample = cls()
sample.actions = [Goal.sample(expand=False)] sample.goals = [Goal.sample(expand=False)]
return sample return sample
@@ -154,73 +169,76 @@ class GoalsController(rest.RestController):
'detail': ['GET'], 'detail': ['GET'],
} }
def _get_goals_collection(self, limit, def _get_goals_collection(self, marker, limit, sort_key, sort_dir,
sort_key, sort_dir, expand=False, expand=False, resource_url=None):
resource_url=None, goal_name=None):
limit = api_utils.validate_limit(limit) limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir) api_utils.validate_sort_dir(sort_dir)
goals = [] sort_db_key = (sort_key if sort_key in objects.Goal.fields.keys()
else None)
if not goal_name and goal_name in CONF.watcher_goals.goals.keys(): marker_obj = None
goals.append({'name': goal_name, 'strategy': goals[goal_name]}) if marker:
else: marker_obj = objects.Goal.get_by_uuid(
for name, strategy in CONF.watcher_goals.goals.items(): pecan.request.context, marker)
goals.append({'name': name, 'strategy': strategy})
return GoalCollection.convert_with_links(goals[:limit], limit, goals = objects.Goal.list(pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
return GoalCollection.convert_with_links(goals, limit,
url=resource_url, url=resource_url,
expand=expand, expand=expand,
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) sort_dir=sort_dir)
@wsme_pecan.wsexpose(GoalCollection, int, wtypes.text, wtypes.text) @wsme_pecan.wsexpose(GoalCollection, wtypes.text,
def get_all(self, limit=None, int, wtypes.text, wtypes.text)
sort_key='name', sort_dir='asc'): def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of goals. """Retrieve a list of goals.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
to get only actions for that goal.
""" """
return self._get_goals_collection(limit, sort_key, sort_dir) 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, @wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
wtypes.text, wtypes.text) wtypes.text, wtypes.text)
def detail(self, goal_name=None, limit=None, def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
sort_key='name', sort_dir='asc'): """Retrieve a list of goals with detail.
"""Retrieve a list of actions with detail.
:param goal_name: name of a goal, to get only goals for that :param marker: pagination marker for large data sets.
action.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
to get only goals for that goal.
""" """
context = pecan.request.context
policy.enforce(context, 'goal:detail',
action='goal:detail')
# NOTE(lucasagomes): /detail should only work agaist collections # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "goals": if parent != "goals":
raise exception.HTTPNotFound raise exception.HTTPNotFound
expand = True expand = True
resource_url = '/'.join(['goals', 'detail']) resource_url = '/'.join(['goals', 'detail'])
return self._get_goals_collection(limit, sort_key, sort_dir, return self._get_goals_collection(marker, limit, sort_key, sort_dir,
expand, resource_url, goal_name) expand, resource_url)
@wsme_pecan.wsexpose(Goal, wtypes.text) @wsme_pecan.wsexpose(Goal, wtypes.text)
def get_one(self, goal_name): def get_one(self, goal):
"""Retrieve information about the given goal. """Retrieve information about the given goal.
:param goal_name: name of the goal. :param goal: UUID or name of the goal.
""" """
if self.from_goals: if self.from_goals:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
goals = CONF.watcher_goals.goals context = pecan.request.context
goal = {} rpc_goal = api_utils.get_resource('Goal', goal)
if goal_name in goals.keys(): policy.enforce(context, 'goal:get', rpc_goal, action='goal:get')
goal = {'name': goal_name, 'strategy': goals[goal_name]}
return Goal.convert_with_links(goal) return Goal.convert_with_links(rpc_goal)

View File

@@ -0,0 +1,309 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is
able to find a :ref:`Solution <solution_definition>` for a given
:ref:`Goal <goal_definition>`.
There may be several potential strategies which are able to achieve the same
:ref:`Goal <goal_definition>`. This is why it is possible to configure which
specific :ref:`Strategy <strategy_definition>` should be used for each goal.
Some strategies may provide better optimization results but may take more time
to find an optimal :ref:`Solution <solution_definition>`.
"""
from oslo_config import cfg
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from watcher.api.controllers import base
from watcher.api.controllers import link
from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils
from watcher import objects
CONF = cfg.CONF
class Strategy(base.APIBase):
"""API representation of a strategy.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a strategy.
"""
_goal_uuid = None
_goal_name = None
def _get_goal(self, value):
if value == wtypes.Unset:
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
self.goal_id = goal.id
return goal
def _get_goal_uuid(self):
return self._goal_uuid
def _set_goal_uuid(self, value):
if value and self._goal_uuid != value:
self._goal_uuid = None
goal = self._get_goal(value)
if goal:
self._goal_uuid = goal.uuid
def _get_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"""
name = wtypes.text
"""Name of the strategy"""
display_name = wtypes.text
"""Localized name of the strategy"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated goal links"""
goal_uuid = wsme.wsproperty(wtypes.text, _get_goal_uuid, _set_goal_uuid,
mandatory=True)
"""The UUID of the goal this audit refers to"""
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__()
self.fields = []
self.fields.append('uuid')
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', 'goal_name'])
strategy.links = [
link.Link.make_link('self', url, 'strategies', strategy.uuid),
link.Link.make_link('bookmark', url, 'strategies', strategy.uuid,
bookmark=True)]
return strategy
@classmethod
def convert_with_links(cls, strategy, expand=True):
strategy = Strategy(**strategy.as_dict())
return cls._convert_with_links(
strategy, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='DUMMY',
display_name='Dummy strategy')
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
class StrategyCollection(collection.Collection):
"""API representation of a collection of strategies."""
strategies = [Strategy]
"""A list containing strategies objects"""
def __init__(self, **kwargs):
super(StrategyCollection, self).__init__()
self._type = 'strategies'
@staticmethod
def convert_with_links(strategies, limit, url=None, expand=False,
**kwargs):
strategy_collection = StrategyCollection()
strategy_collection.strategies = [
Strategy.convert_with_links(g, expand) for g in strategies]
if 'sort_key' in kwargs:
reverse = False
if kwargs['sort_key'] == 'strategy':
if 'sort_dir' in kwargs:
reverse = True if kwargs['sort_dir'] == 'desc' else False
strategy_collection.strategies = sorted(
strategy_collection.strategies,
key=lambda strategy: strategy.uuid,
reverse=reverse)
strategy_collection.next = strategy_collection.get_next(
limit, url=url, **kwargs)
return strategy_collection
@classmethod
def sample(cls):
sample = cls()
sample.strategies = [Strategy.sample(expand=False)]
return sample
class StrategiesController(rest.RestController):
"""REST controller for Strategies."""
def __init__(self):
super(StrategiesController, self).__init__()
from_strategies = False
"""A flag to indicate if the requests to this controller are coming
from the top-level resource Strategies."""
_custom_actions = {
'detail': ['GET'],
}
def _get_strategies_collection(self, filters, marker, limit, sort_key,
sort_dir, expand=False, resource_url=None):
api_utils.validate_search_filters(
filters, list(objects.strategy.Strategy.fields.keys()) +
["goal_uuid", "goal_name"])
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
sort_db_key = (sort_key if sort_key in objects.Strategy.fields.keys()
else None)
marker_obj = None
if marker:
marker_obj = objects.Strategy.get_by_uuid(
pecan.request.context, marker)
strategies = objects.Strategy.list(
pecan.request.context, limit, marker_obj, filters=filters,
sort_key=sort_db_key, sort_dir=sort_dir)
return StrategyCollection.convert_with_links(
strategies, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text,
int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of strategies.
:param goal: goal UUID or name to filter by.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:get_all',
action='strategy:get_all')
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, int,
wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of strategies with detail.
:param goal: goal UUID or name to filter by.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
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":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['strategies', 'detail'])
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
filters['goal_uuid'] = goal
else:
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir, expand, resource_url)
@wsme_pecan.wsexpose(Strategy, wtypes.text)
def get_one(self, strategy):
"""Retrieve information about the given strategy.
:param strategy: UUID or name of the strategy.
"""
if self.from_strategies:
raise exception.OperationNotPermitted
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

@@ -31,11 +31,6 @@ class UuidOrNameType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'uuid_or_name' name = 'uuid_or_name'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -55,11 +50,6 @@ class NameType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'name' name = 'name'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -79,11 +69,6 @@ class UuidType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'uuid' name = 'uuid'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -103,11 +88,6 @@ class BooleanType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'boolean' name = 'boolean'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod @staticmethod
def validate(value): def validate(value):
@@ -129,11 +109,6 @@ class JsonType(wtypes.UserType):
basetype = wtypes.text basetype = wtypes.text
name = 'json' name = 'json'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
def __str__(self): def __str__(self):
# These are the json serializable native types # These are the json serializable native types

View File

@@ -15,9 +15,12 @@
import jsonpatch import jsonpatch
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import uuidutils
import pecan
import wsme import wsme
from watcher._i18n import _ from watcher._i18n import _
from watcher import objects
CONF = cfg.CONF CONF = cfg.CONF
@@ -75,3 +78,19 @@ def as_filters_dict(**filters):
filters_dict[filter_name] = filter_value filters_dict[filter_name] = filter_value
return filters_dict 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) auth_token = headers.get('X-Auth-Token', auth_token)
show_deleted = headers.get('X-Show-Deleted') show_deleted = headers.get('X-Show-Deleted')
auth_token_info = state.request.environ.get('keystone.token_info') 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') auth_url = headers.get('X-Auth-Url')
if auth_url is None: if auth_url is None:
@@ -72,7 +74,8 @@ class ContextHook(hooks.PecanHook):
project_id=project_id, project_id=project_id,
domain_id=domain_id, domain_id=domain_id,
domain_name=domain_name, domain_name=domain_name,
show_deleted=show_deleted) show_deleted=show_deleted,
roles=roles)
class NoExceptionTracebackHook(hooks.PecanHook): class NoExceptionTracebackHook(hooks.PecanHook):

View File

@@ -23,18 +23,26 @@ import abc
import six import six
from watcher.common import clients from watcher.common import clients
from watcher.common.loader import loadable
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseAction(object): class BaseAction(loadable.Loadable):
# NOTE(jed) by convention we decided # NOTE(jed) by convention we decided
# that the attribute "resource_id" is the unique id of # that the attribute "resource_id" is the unique id of
# the resource to which the Action applies to allow us to use it in the # the resource to which the Action applies to allow us to use it in the
# watcher dashboard and will be nested in input_parameters # watcher dashboard and will be nested in input_parameters
RESOURCE_ID = 'resource_id' RESOURCE_ID = 'resource_id'
def __init__(self, osc=None): def __init__(self, config, osc=None):
""":param osc: an OpenStackClients instance""" """Constructor
:param config: A mapping containing the configuration of this action
:type config: dict
:param osc: an OpenStackClients instance, defaults to None
:type osc: :py:class:`~.OpenStackClients` instance, optional
"""
super(BaseAction, self).__init__(config)
self._input_parameters = {} self._input_parameters = {}
self._osc = osc self._osc = osc
@@ -56,6 +64,15 @@ class BaseAction(object):
def resource_id(self): def resource_id(self):
return self.input_parameters[self.RESOURCE_ID] return self.input_parameters[self.RESOURCE_ID]
@classmethod
def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
return []
@abc.abstractmethod @abc.abstractmethod
def execute(self): def execute(self):
"""Executes the main logic of the action """Executes the main logic of the action

View File

@@ -19,7 +19,7 @@ from __future__ import unicode_literals
from oslo_log import log from oslo_log import log
from watcher.applier.actions.loading import default from watcher.applier.loading import default
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)

View File

@@ -31,27 +31,24 @@ LOG = log.getLogger(__name__)
class Migrate(base.BaseAction): class Migrate(base.BaseAction):
"""Live-Migrates a server to a destination nova-compute host """Migrates a server to a destination nova-compute host
This action will allow you to migrate a server to another compute This action will allow you to migrate a server to another compute
destination host. As of now, only live migration can be performed using destination host.
this action. Migration type 'live' can only be used for migrating active VMs.
.. If either host uses shared storage, you can use ``live`` Migration type 'cold' can be used for migrating non-active VMs
.. as ``migration_type``. If both source and destination hosts provide as well active VMs, which will be shut down while migrating.
.. local disks, you can set the block_migration parameter to True (not
.. supported for yet).
The action schema is:: The action schema is::
schema = Schema({ schema = Schema({
'resource_id': str, # should be a UUID 'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live" only 'migration_type': str, # choices -> "live", "cold"
'dst_hypervisor': str, 'dst_hypervisor': str,
'src_hypervisor': str, 'src_hypervisor': str,
}) })
The `resource_id` is the UUID of the server to migrate. Only live migration The `resource_id` is the UUID of the server to migrate.
is supported.
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
source and the destination compute hostname (list of available compute source and the destination compute hostname (list of available compute
hosts is returned by this command: ``nova service-list --binary hosts is returned by this command: ``nova service-list --binary
@@ -61,6 +58,7 @@ class Migrate(base.BaseAction):
# input parameters constants # input parameters constants
MIGRATION_TYPE = 'migration_type' MIGRATION_TYPE = 'migration_type'
LIVE_MIGRATION = 'live' LIVE_MIGRATION = 'live'
COLD_MIGRATION = 'cold'
DST_HYPERVISOR = 'dst_hypervisor' DST_HYPERVISOR = 'dst_hypervisor'
SRC_HYPERVISOR = 'src_hypervisor' SRC_HYPERVISOR = 'src_hypervisor'
@@ -77,7 +75,8 @@ class Migrate(base.BaseAction):
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id, voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
voluptuous.Required(self.MIGRATION_TYPE, voluptuous.Required(self.MIGRATION_TYPE,
default=self.LIVE_MIGRATION): default=self.LIVE_MIGRATION):
voluptuous.Any(*[self.LIVE_MIGRATION]), voluptuous.Any(*[self.LIVE_MIGRATION,
self.COLD_MIGRATION]),
voluptuous.Required(self.DST_HYPERVISOR): voluptuous.Required(self.DST_HYPERVISOR):
voluptuous.All(voluptuous.Any(*six.string_types), voluptuous.All(voluptuous.Any(*six.string_types),
voluptuous.Length(min=1)), voluptuous.Length(min=1)),
@@ -127,14 +126,30 @@ class Migrate(base.BaseAction):
return result return result
def _cold_migrate_instance(self, nova, destination):
result = None
try:
result = nova.watcher_non_live_migrate_instance(
instance_id=self.instance_uuid,
dest_hostname=destination)
except Exception as exc:
LOG.exception(exc)
LOG.critical(_LC("Unexpected error occured. Migration failed for"
"instance %s. Leaving instance on previous "
"host."), self.instance_uuid)
return result
def migrate(self, destination): def migrate(self, destination):
nova = nova_helper.NovaHelper(osc=self.osc) nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Migrate instance %s to %s", self.instance_uuid, LOG.debug("Migrate instance %s to %s", self.instance_uuid,
destination) destination)
instance = nova.find_instance(self.instance_uuid) instance = nova.find_instance(self.instance_uuid)
if instance: if instance:
if self.migration_type == 'live': if self.migration_type == self.LIVE_MIGRATION:
return self._live_migrate_instance(nova, destination) return self._live_migrate_instance(nova, destination)
elif self.migration_type == self.COLD_MIGRATION:
return self._cold_migrate_instance(nova, destination)
else: else:
raise exception.Invalid( raise exception.Invalid(
message=(_('Migration of type %(migration_type)s is not ' message=(_('Migration of type %(migration_type)s is not '

View File

@@ -20,7 +20,7 @@ from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.applier import base from watcher.applier import base
from watcher.applier.workflow_engine.loading import default from watcher.applier.loading import default
from watcher import objects from watcher import objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -56,9 +56,7 @@ class DefaultApplier(base.BaseApplier):
def execute(self, action_plan_uuid): def execute(self, action_plan_uuid):
LOG.debug("Executing action plan %s ", action_plan_uuid) LOG.debug("Executing action plan %s ", action_plan_uuid)
action_plan = objects.ActionPlan.get_by_uuid(self.context, filters = {'action_plan_uuid': action_plan_uuid}
action_plan_uuid) actions = objects.Action.list(self.context,
# todo(jed) remove direct access to dbapi need filter in object filters=filters)
filters = {'action_plan_id': action_plan.id}
actions = objects.Action.dbapi.get_action_list(self.context, filters)
return self.engine.execute(actions) 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
@@ -13,18 +10,20 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
#
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_log import log
from watcher.common.loader import default from watcher.common.loader import default
LOG = log.getLogger(__name__)
class DefaultWorkFlowEngineLoader(default.DefaultLoader): class DefaultWorkFlowEngineLoader(default.DefaultLoader):
def __init__(self): def __init__(self):
super(DefaultWorkFlowEngineLoader, self).__init__( super(DefaultWorkFlowEngineLoader, self).__init__(
namespace='watcher_workflow_engines') namespace='watcher_workflow_engines')
class DefaultActionLoader(default.DefaultLoader):
def __init__(self):
super(DefaultActionLoader, self).__init__(
namespace='watcher_actions')

View File

@@ -21,7 +21,6 @@ from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.applier.messaging import trigger from watcher.applier.messaging import trigger
from watcher.common.messaging import messaging_core
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@@ -63,17 +62,15 @@ CONF.register_group(opt_group)
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group) CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
class ApplierManager(messaging_core.MessagingCore): class ApplierManager(object):
def __init__(self):
super(ApplierManager, self).__init__(
CONF.watcher_applier.publisher_id,
CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.status_topic,
api_version=self.API_VERSION,
)
self.conductor_topic_handler.add_endpoint(
trigger.TriggerActionPlan(self))
def join(self): API_VERSION = '1.0'
self.conductor_topic_handler.join()
self.status_topic_handler.join() conductor_endpoints = [trigger.TriggerActionPlan]
status_endpoints = []
def __init__(self):
self.publisher_id = CONF.watcher_applier.publisher_id
self.conductor_topic = CONF.watcher_applier.conductor_topic
self.status_topic = CONF.watcher_applier.status_topic
self.api_version = self.API_VERSION

View File

@@ -18,48 +18,43 @@
# #
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
import oslo_messaging as om
from watcher.applier.manager import APPLIER_MANAGER_OPTS from watcher.applier import manager
from watcher.applier.manager import opt_group
from watcher.common import exception from watcher.common import exception
from watcher.common.messaging import messaging_core
from watcher.common.messaging import notification_handler as notification from watcher.common.messaging import notification_handler as notification
from watcher.common import service
from watcher.common import utils from watcher.common import utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
CONF.register_group(opt_group) CONF.register_group(manager.opt_group)
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group) CONF.register_opts(manager.APPLIER_MANAGER_OPTS, manager.opt_group)
class ApplierAPI(messaging_core.MessagingCore): class ApplierAPI(service.Service):
def __init__(self): def __init__(self):
super(ApplierAPI, self).__init__( super(ApplierAPI, self).__init__(ApplierAPIManager)
CONF.watcher_applier.publisher_id,
CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.status_topic,
api_version=self.API_VERSION,
)
self.handler = notification.NotificationHandler(self.publisher_id)
self.handler.register_observer(self)
self.status_topic_handler.add_endpoint(self.handler)
transport = om.get_transport(CONF)
target = om.Target(
topic=CONF.watcher_applier.conductor_topic,
version=self.API_VERSION,
)
self.client = om.RPCClient(transport, target,
serializer=self.serializer)
def launch_action_plan(self, context, action_plan_uuid=None): def launch_action_plan(self, context, action_plan_uuid=None):
if not utils.is_uuid_like(action_plan_uuid): if not utils.is_uuid_like(action_plan_uuid):
raise exception.InvalidUuidOrName(name=action_plan_uuid) raise exception.InvalidUuidOrName(name=action_plan_uuid)
return self.client.call( return self.conductor_client.call(
context.to_dict(), 'launch_action_plan', context.to_dict(), 'launch_action_plan',
action_plan_uuid=action_plan_uuid) action_plan_uuid=action_plan_uuid)
class ApplierAPIManager(object):
API_VERSION = '1.0'
conductor_endpoints = []
status_endpoints = [notification.NotificationHandler]
def __init__(self):
self.publisher_id = CONF.watcher_applier.publisher_id
self.conductor_topic = CONF.watcher_applier.conductor_topic
self.status_topic = CONF.watcher_applier.status_topic
self.api_version = self.API_VERSION

View File

@@ -23,18 +23,38 @@ import six
from watcher.applier.actions import factory from watcher.applier.actions import factory
from watcher.applier.messaging import event_types from watcher.applier.messaging import event_types
from watcher.common import clients from watcher.common import clients
from watcher.common.loader import loadable
from watcher.common.messaging.events import event from watcher.common.messaging.events import event
from watcher import objects from watcher import objects
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseWorkFlowEngine(object): class BaseWorkFlowEngine(loadable.Loadable):
def __init__(self, context=None, applier_manager=None):
def __init__(self, config, context=None, applier_manager=None):
"""Constructor
:param config: A mapping containing the configuration of this
workflow engine
:type config: dict
:param osc: an OpenStackClients object, defaults to None
:type osc: :py:class:`~.OpenStackClients` instance, optional
"""
super(BaseWorkFlowEngine, self).__init__(config)
self._context = context self._context = context
self._applier_manager = applier_manager self._applier_manager = applier_manager
self._action_factory = factory.ActionFactory() self._action_factory = factory.ActionFactory()
self._osc = None self._osc = None
@classmethod
def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
return []
@property @property
def context(self): def context(self):
return self._context return self._context

View File

@@ -17,19 +17,14 @@
"""Starter script for the Watcher API service.""" """Starter script for the Watcher API service."""
import logging as std_logging
import os
import sys import sys
from wsgiref import simple_server
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from watcher._i18n import _ from watcher._i18n import _LI
from watcher.api import app as api_app
from watcher.common import service from watcher.common import service
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@@ -37,22 +32,20 @@ CONF = cfg.CONF
def main(): def main():
service.prepare_service(sys.argv) service.prepare_service(sys.argv)
app = api_app.setup_app()
# Create the WSGI server and start it
host, port = cfg.CONF.api.host, cfg.CONF.api.port host, port = cfg.CONF.api.host, cfg.CONF.api.port
srv = simple_server.make_server(host, port, app) protocol = "http" if not CONF.api.enable_ssl_api else "https"
# Build and start the WSGI app
server = service.WSGIService(
'watcher-api', CONF.api.enable_ssl_api)
LOG.info(_('Starting server in PID %s') % os.getpid()) if host == '127.0.0.1':
LOG.debug("Watcher configuration:") LOG.info(_LI('serving on 127.0.0.1:%(port)s, '
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG) 'view at %(protocol)s://127.0.0.1:%(port)s') %
dict(protocol=protocol, port=port))
if host == '0.0.0.0':
LOG.info(_('serving on 0.0.0.0:%(port)s, '
'view at http://127.0.0.1:%(port)s') %
dict(port=port))
else: else:
LOG.info(_('serving on http://%(host)s:%(port)s') % LOG.info(_LI('serving on %(protocol)s://%(host)s:%(port)s') %
dict(host=host, port=port)) dict(protocol=protocol, host=host, port=port))
srv.serve_forever() launcher = service.process_launcher()
launcher.launch_service(server, workers=server.workers)
launcher.wait()

View File

@@ -17,29 +17,26 @@
"""Starter script for the Applier service.""" """Starter script for the Applier service."""
import logging as std_logging
import os import os
import sys import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import service
from watcher import _i18n from watcher._i18n import _LI
from watcher.applier import manager from watcher.applier import manager
from watcher.common import service from watcher.common import service as watcher_service
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
_LI = _i18n._LI
def main(): def main():
service.prepare_service(sys.argv) watcher_service.prepare_service(sys.argv)
LOG.info(_LI('Starting server in PID %s') % os.getpid()) LOG.info(_LI('Starting Watcher Applier service in PID %s'), os.getpid())
LOG.debug("Configuration:")
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
server = manager.ApplierManager() applier_service = watcher_service.Service(manager.ApplierManager)
server.connect() launcher = service.launch(CONF, applier_service)
server.join() launcher.wait()

View File

@@ -17,30 +17,31 @@
"""Starter script for the Decision Engine manager service.""" """Starter script for the Decision Engine manager service."""
import logging as std_logging
import os import os
import sys import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import service
from watcher import _i18n from watcher._i18n import _LI
from watcher.common import service from watcher.common import service as watcher_service
from watcher.decision_engine import manager from watcher.decision_engine import manager
from watcher.decision_engine import sync
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
_LI = _i18n._LI
def main(): def main():
service.prepare_service(sys.argv) watcher_service.prepare_service(sys.argv)
LOG.info(_LI('Starting server in PID %s') % os.getpid()) LOG.info(_LI('Starting Watcher Decision Engine service in PID %s'),
LOG.debug("Configuration:") os.getpid())
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
server = manager.DecisionEngineManager() syncer = sync.Syncer()
server.connect() syncer.sync()
server.join()
de_service = watcher_service.Service(manager.DecisionEngineManager)
launcher = service.launch(CONF, de_service)
launcher.wait()

View File

@@ -17,7 +17,7 @@
# limitations under the License. # limitations under the License.
# #
from ceilometerclient.exc import HTTPUnauthorized from ceilometerclient import exc
from watcher.common import clients from watcher.common import clients
@@ -68,7 +68,7 @@ class CeilometerHelper(object):
def query_retry(self, f, *args, **kargs): def query_retry(self, f, *args, **kargs):
try: try:
return f(*args, **kargs) return f(*args, **kargs)
except HTTPUnauthorized: except exc.HTTPUnauthorized:
self.osc.reset_clients() self.osc.reset_clients()
self.ceilometer = self.osc.ceilometer() self.ceilometer = self.osc.ceilometer()
return f(*args, **kargs) return f(*args, **kargs)
@@ -123,7 +123,7 @@ class CeilometerHelper(object):
item_value = None item_value = None
if statistic: if statistic:
item_value = statistic[-1]._info.get('aggregate').get('avg') item_value = statistic[-1]._info.get('aggregate').get(aggregate)
return item_value return item_value
def get_last_sample_values(self, resource_id, meter_name, limit=1): def get_last_sample_values(self, resource_id, meter_name, limit=1):

View File

@@ -20,7 +20,7 @@ class RequestContext(context.RequestContext):
domain_name=None, user=None, user_id=None, project=None, domain_name=None, user=None, user_id=None, project=None,
project_id=None, is_admin=False, is_public_api=False, project_id=None, is_admin=False, is_public_api=False,
read_only=False, show_deleted=False, request_id=None, 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: """Stores several additional request parameters:
:param domain_id: The ID of the domain. :param domain_id: The ID of the domain.
@@ -44,7 +44,8 @@ class RequestContext(context.RequestContext):
is_admin=is_admin, is_admin=is_admin,
read_only=read_only, read_only=read_only,
show_deleted=show_deleted, show_deleted=show_deleted,
request_id=request_id) request_id=request_id,
roles=roles)
def to_dict(self): def to_dict(self):
return {'auth_token': self.auth_token, return {'auth_token': self.auth_token,
@@ -61,7 +62,8 @@ class RequestContext(context.RequestContext):
'show_deleted': self.show_deleted, 'show_deleted': self.show_deleted,
'request_id': self.request_id, 'request_id': self.request_id,
'trust_id': self.trust_id, 'trust_id': self.trust_id,
'auth_token_info': self.auth_token_info} 'auth_token_info': self.auth_token_info,
'roles': self.roles}
@classmethod @classmethod
def from_dict(cls, values): def from_dict(cls, values):

View File

@@ -123,6 +123,10 @@ class NotAuthorized(WatcherException):
code = 403 code = 403
class PolicyNotAuthorized(NotAuthorized):
msg_fmt = _("Policy doesn't allow %(action)s to be performed.")
class OperationNotPermitted(NotAuthorized): class OperationNotPermitted(NotAuthorized):
msg_fmt = _("Operation not permitted") msg_fmt = _("Operation not permitted")
@@ -147,11 +151,20 @@ class ResourceNotFound(ObjectNotFound):
class InvalidIdentity(Invalid): class InvalidIdentity(Invalid):
msg_fmt = _("Expected an uuid or int but received %(identity)s") msg_fmt = _("Expected a uuid or int but received %(identity)s")
class InvalidOperator(Invalid):
msg_fmt = _("Filter operator is not valid: %(operator)s not "
"in %(valid_operators)s")
class InvalidGoal(Invalid): class InvalidGoal(Invalid):
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file") msg_fmt = _("Goal %(goal)s is invalid")
class InvalidStrategy(Invalid):
msg_fmt = _("Strategy %(strategy)s is invalid")
class InvalidUUID(Invalid): class InvalidUUID(Invalid):
@@ -166,12 +179,28 @@ class InvalidUuidOrName(Invalid):
msg_fmt = _("Expected a logical name or uuid but received %(name)s") msg_fmt = _("Expected a logical name or uuid but received %(name)s")
class GoalNotFound(ResourceNotFound):
msg_fmt = _("Goal %(goal)s could not be found")
class GoalAlreadyExists(Conflict):
msg_fmt = _("A goal with UUID %(uuid)s already exists")
class StrategyNotFound(ResourceNotFound):
msg_fmt = _("Strategy %(strategy)s could not be found")
class StrategyAlreadyExists(Conflict):
msg_fmt = _("A strategy with UUID %(uuid)s already exists")
class AuditTemplateNotFound(ResourceNotFound): class AuditTemplateNotFound(ResourceNotFound):
msg_fmt = _("AuditTemplate %(audit_template)s could not be found") msg_fmt = _("AuditTemplate %(audit_template)s could not be found")
class AuditTemplateAlreadyExists(Conflict): class AuditTemplateAlreadyExists(Conflict):
msg_fmt = _("An audit_template with UUID %(uuid)s or name %(name)s " msg_fmt = _("An audit_template with UUID or name %(audit_template)s "
"already exists") "already exists")
@@ -180,6 +209,10 @@ class AuditTemplateReferenced(Invalid):
"multiple audit") "multiple audit")
class AuditTypeNotFound(Invalid):
msg_fmt = _("Audit type %(audit_type)s could not be found")
class AuditNotFound(ResourceNotFound): class AuditNotFound(ResourceNotFound):
msg_fmt = _("Audit %(audit)s could not be found") msg_fmt = _("Audit %(audit)s could not be found")
@@ -188,6 +221,14 @@ class AuditAlreadyExists(Conflict):
msg_fmt = _("An audit with UUID %(uuid)s already exists") 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): class AuditReferenced(Invalid):
msg_fmt = _("Audit %(audit)s is referenced by one or multiple action " msg_fmt = _("Audit %(audit)s is referenced by one or multiple action "
"plans") "plans")
@@ -224,6 +265,14 @@ class ActionFilterCombinationProhibited(Invalid):
"prohibited") "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): class HTTPNotFound(ResourceNotFound):
pass pass
@@ -267,7 +316,29 @@ class MetricCollectorNotDefined(WatcherException):
class ClusterStateNotDefined(WatcherException): class ClusterStateNotDefined(WatcherException):
msg_fmt = _("the cluster state is not defined") msg_fmt = _("The cluster state is not defined")
class NoAvailableStrategyForGoal(WatcherException):
msg_fmt = _("No strategy could be found to achieve the '%(goal)s' goal.")
class 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.")
class NoSuchMetricForHost(WatcherException):
msg_fmt = _("No %(metric)s metric for %(host)s found.")
# Model # Model

View File

@@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com # Copyright (c) 2016 b<>com
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -16,33 +16,81 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from oslo_config import cfg
from oslo_log import log from oslo_log import log
from stevedore.driver import DriverManager from stevedore import driver as drivermanager
from stevedore import ExtensionManager from stevedore import extension as extensionmanager
from watcher.common import exception from watcher.common import exception
from watcher.common.loader.base import BaseLoader from watcher.common.loader import base
from watcher.common import utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class DefaultLoader(BaseLoader): class DefaultLoader(base.BaseLoader):
def __init__(self, namespace):
def __init__(self, namespace, conf=cfg.CONF):
"""Entry point loader for Watcher using Stevedore
:param namespace: namespace of the entry point(s) to load or list
:type namespace: str
:param conf: ConfigOpts instance, defaults to cfg.CONF
"""
super(DefaultLoader, self).__init__() super(DefaultLoader, self).__init__()
self.namespace = namespace self.namespace = namespace
self.conf = conf
def load(self, name, **kwargs): def load(self, name, **kwargs):
try: try:
LOG.debug("Loading in namespace %s => %s ", self.namespace, name) LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
driver_manager = DriverManager(namespace=self.namespace, driver_manager = drivermanager.DriverManager(
name=name) namespace=self.namespace,
loaded = driver_manager.driver name=name,
invoke_on_load=False,
)
driver_cls = driver_manager.driver
config = self._load_plugin_config(name, driver_cls)
driver = driver_cls(config, **kwargs)
except Exception as exc: except Exception as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.LoadingError(name=name) raise exception.LoadingError(name=name)
return loaded(**kwargs) return driver
def _reload_config(self):
self.conf()
def get_entry_name(self, name):
return ".".join([self.namespace, name])
def _load_plugin_config(self, name, driver_cls):
"""Load the config of the plugin"""
config = utils.Struct()
config_opts = driver_cls.get_config_opts()
if not config_opts:
return config
group_name = self.get_entry_name(name)
self.conf.register_opts(config_opts, group=group_name)
# Finalise the opt import by re-checking the configuration
# against the provided config files
self._reload_config()
config_group = self.conf.get(group_name)
if not config_group:
raise exception.LoadingError(name=name)
config.update({
name: value for name, value in config_group.items()
})
return config
def list_available(self): def list_available(self):
extension_manager = ExtensionManager(namespace=self.namespace) extension_manager = extensionmanager.ExtensionManager(
namespace=self.namespace)
return {ext.name: ext.plugin for ext in extension_manager.extensions} return {ext.name: ext.plugin for ext in extension_manager.extensions}

View File

@@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com # Copyright (c) 2016 b<>com
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -13,16 +13,29 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import abc import abc
import six import six
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class FakeLoadable(object): class Loadable(object):
@classmethod """Generic interface for dynamically loading a driver/entry point.
def namespace(cls):
return "TESTING" This defines the contract in order to let the loader manager inject
the configuration parameters during the loading.
"""
def __init__(self, config):
self.config = config
@classmethod @classmethod
def get_name(cls): @abc.abstractmethod
return 'fake' def get_config_opts(cls):
"""Defines the configuration options to be associated to this loadable
:return: A list of configuration options relative to this Loadable
:rtype: list of :class:`oslo_config.cfg.Opt` instances
"""
raise NotImplementedError

View File

@@ -1,122 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
from oslo_log import log
import oslo_messaging as om
from watcher.common.messaging.events import event_dispatcher as dispatcher
from watcher.common.messaging import messaging_handler
from watcher.common import rpc
from watcher.objects import base
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class MessagingCore(dispatcher.EventDispatcher):
API_VERSION = '1.0'
def __init__(self, publisher_id, conductor_topic, status_topic,
api_version=API_VERSION):
super(MessagingCore, self).__init__()
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self.publisher_id = publisher_id
self.api_version = api_version
self.conductor_topic = conductor_topic
self.status_topic = status_topic
self.conductor_topic_handler = self.build_topic_handler(
conductor_topic)
self.status_topic_handler = self.build_topic_handler(status_topic)
self._conductor_client = None
self._status_client = None
@property
def conductor_client(self):
if self._conductor_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.conductor_topic,
version=self.API_VERSION,
)
self._conductor_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._conductor_client
@conductor_client.setter
def conductor_client(self, c):
self.conductor_client = c
@property
def status_client(self):
if self._status_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.status_topic,
version=self.API_VERSION,
)
self._status_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._status_client
@status_client.setter
def status_client(self, c):
self.status_client = c
def build_topic_handler(self, topic_name):
return messaging_handler.MessagingHandler(
self.publisher_id, topic_name, self,
self.api_version, self.serializer)
def connect(self):
LOG.debug("Connecting to '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.start()
self.status_topic_handler.start()
def disconnect(self):
LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.stop()
self.status_topic_handler.stop()
def publish_control(self, event, payload):
return self.conductor_topic_handler.publish_event(event, payload)
def publish_status(self, event, payload, request_id=None):
return self.status_topic_handler.publish_event(
event, payload, request_id)
def get_version(self):
return self.api_version
def check_api_version(self, context):
api_manager_version = self.conductor_client.call(
context.to_dict(), 'check_api_version',
api_version=self.api_version)
return api_manager_version
def response(self, evt, ctx, message):
payload = {
'request_id': ctx['request_id'],
'msg': message
}
self.publish_status(evt, payload)

View File

@@ -38,7 +38,7 @@ CONF = cfg.CONF
class MessagingHandler(threading.Thread): class MessagingHandler(threading.Thread):
def __init__(self, publisher_id, topic_name, endpoint, version, def __init__(self, publisher_id, topic_name, endpoints, version,
serializer=None): serializer=None):
super(MessagingHandler, self).__init__() super(MessagingHandler, self).__init__()
self.publisher_id = publisher_id self.publisher_id = publisher_id
@@ -50,10 +50,10 @@ class MessagingHandler(threading.Thread):
self.__server = None self.__server = None
self.__notifier = None self.__notifier = None
self.__transport = None self.__transport = None
self.add_endpoint(endpoint) self.add_endpoints(endpoints)
def add_endpoint(self, endpoint): def add_endpoints(self, endpoints):
self.__endpoints.append(endpoint) self.__endpoints.extend(endpoints)
def remove_endpoint(self, endpoint): def remove_endpoint(self, endpoint):
if endpoint in self.__endpoints: if endpoint in self.__endpoints:

View File

@@ -15,14 +15,12 @@
# limitations under the License. # limitations under the License.
import eventlet import eventlet
from oslo_log import log
import oslo_messaging as messaging import oslo_messaging as messaging
from watcher.common.messaging.utils import observable from watcher.common.messaging.utils import observable
eventlet.monkey_patch() eventlet.monkey_patch()
LOG = log.getLogger(__name__)
class NotificationHandler(observable.Observable): class NotificationHandler(observable.Observable):

View File

@@ -87,7 +87,6 @@ class NovaHelper(object):
return False return False
else: else:
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host") host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
# https://bugs.launchpad.net/nova/+bug/1182965
LOG.debug( LOG.debug(
"Instance %s found on host '%s'." % (instance_id, host_name)) "Instance %s found on host '%s'." % (instance_id, host_name))
@@ -532,16 +531,12 @@ class NovaHelper(object):
"Trying to create new instance '%s' " "Trying to create new instance '%s' "
"from image '%s' with flavor '%s' ..." % ( "from image '%s' with flavor '%s' ..." % (
inst_name, image_id, flavor_name)) inst_name, image_id, flavor_name))
# TODO(jed) wait feature
# Allow admin users to view any keypair try:
# https://bugs.launchpad.net/nova/+bug/1182965 self.nova.keypairs.findall(name=keypair_name)
if not self.nova.keypairs.findall(name=keypair_name): except nvexceptions.NotFound:
LOG.debug("Key pair '%s' not found with user '%s'" % ( LOG.debug("Key pair '%s' not found " % keypair_name)
keypair_name, self.user))
return return
else:
LOG.debug("Key pair '%s' found with user '%s'" % (
keypair_name, self.user))
try: try:
image = self.nova.images.get(image_id) image = self.nova.images.get(image_id)

View File

@@ -15,55 +15,80 @@
"""Policy Engine For Watcher.""" """Policy Engine For Watcher."""
from oslo_concurrency import lockutils
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import policy from oslo_policy import policy
from watcher.common import exception
_ENFORCER = None _ENFORCER = None
CONF = cfg.CONF CONF = cfg.CONF
@lockutils.synchronized('policy_enforcer', 'watcher-') # we can get a policy enforcer by this init.
def init_enforcer(policy_file=None, rules=None, # oslo policy support change policy rule dynamically.
default_rule=None, use_conf=True): # at present, policy.enforce will reload the policy rules when it checks
"""Synchronously initializes the policy enforcer # the policy files have been touched.
def init(policy_file=None, rules=None,
:param policy_file: Custom policy file to use, if none is specified, default_rule=None, use_conf=True, overwrite=True):
`CONF.policy_file` will be used. """Init an Enforcer class.
: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.
: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 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: 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 return _ENFORCER
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs): def enforce(context, rule=None, target=None,
"""A shortcut for policy.Enforcer.enforce() 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() enforcer = init()
return enforcer.enforce(rule, target, creds, do_raise=do_raise, credentials = context.to_dict()
exc=exc, *args, **kwargs) 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

@@ -15,108 +15,46 @@
# under the License. # under the License.
import logging import logging
import signal
import socket import socket
from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_log import _options from oslo_log import _options
from oslo_log import log from oslo_log import log
import oslo_messaging as messaging import oslo_messaging as om
from oslo_reports import guru_meditation_report as gmr
from oslo_reports import opts as gmr_opts
from oslo_service import service from oslo_service import service
from oslo_utils import importutils from oslo_service import wsgi
from watcher._i18n import _LE from watcher._i18n import _
from watcher._i18n import _LI from watcher.api import app
from watcher.common import config from watcher.common import config
from watcher.common import context from watcher.common.messaging.events import event_dispatcher as dispatcher
from watcher.common.messaging import messaging_handler
from watcher.common import rpc from watcher.common import rpc
from watcher.objects import base as objects_base from watcher.objects import base
from watcher import opts
from watcher import version
service_opts = [ service_opts = [
cfg.IntOpt('periodic_interval', cfg.IntOpt('periodic_interval',
default=60, default=60,
help='Seconds between running periodic tasks.'), help=_('Seconds between running periodic tasks.')),
cfg.StrOpt('host', cfg.StrOpt('host',
default=socket.getfqdn(), default=socket.getfqdn(),
help='Name of this node. This can be an opaque identifier. ' help=_('Name of this node. This can be an opaque identifier. '
'It is not necessarily a hostname, FQDN, or IP address. ' 'It is not necessarily a hostname, FQDN, or IP address. '
'However, the node name must be valid within ' 'However, the node name must be valid within '
'an AMQP key, and if using ZeroMQ, a valid ' 'an AMQP key, and if using ZeroMQ, a valid '
'hostname, FQDN, or IP address.'), 'hostname, FQDN, or IP address.')),
] ]
cfg.CONF.register_opts(service_opts) cfg.CONF.register_opts(service_opts)
CONF = cfg.CONF
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class RPCService(service.Service):
def __init__(self, host, manager_module, manager_class):
super(RPCService, self).__init__()
self.host = host
manager_module = importutils.try_import(manager_module)
manager_class = getattr(manager_module, manager_class)
self.manager = manager_class(host, manager_module.MANAGER_TOPIC)
self.topic = self.manager.topic
self.rpcserver = None
self.deregister = True
def start(self):
super(RPCService, self).start()
admin_context = context.RequestContext('admin', 'admin', is_admin=True)
target = messaging.Target(topic=self.topic, server=self.host)
endpoints = [self.manager]
serializer = objects_base.IronicObjectSerializer()
self.rpcserver = rpc.get_server(target, endpoints, serializer)
self.rpcserver.start()
self.handle_signal()
self.manager.init_host()
self.tg.add_dynamic_timer(
self.manager.periodic_tasks,
periodic_interval_max=cfg.CONF.periodic_interval,
context=admin_context)
LOG.info(_LI('Created RPC server for service %(service)s on host '
'%(host)s.'),
{'service': self.topic, 'host': self.host})
def stop(self):
try:
self.rpcserver.stop()
self.rpcserver.wait()
except Exception as e:
LOG.exception(_LE('Service error occurred when stopping the '
'RPC server. Error: %s'), e)
try:
self.manager.del_host(deregister=self.deregister)
except Exception as e:
LOG.exception(_LE('Service error occurred when cleaning up '
'the RPC manager. Error: %s'), e)
super(RPCService, self).stop(graceful=True)
LOG.info(_LI('Stopped RPC server for service %(service)s on host '
'%(host)s.'),
{'service': self.topic, 'host': self.host})
def _handle_signal(self):
LOG.info(_LI('Got signal SIGUSR1. Not deregistering on next shutdown '
'of service %(service)s on host %(host)s.'),
{'service': self.topic, 'host': self.host})
self.deregister = False
def handle_signal(self):
"""Add a signal handler for SIGUSR1.
The handler ensures that the manager is not deregistered when it is
shutdown.
"""
signal.signal(signal.SIGUSR1, self._handle_signal)
_DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO', _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
'oslo.messaging=INFO', 'sqlalchemy=WARN', 'oslo.messaging=INFO', 'sqlalchemy=WARN',
'keystoneclient=INFO', 'stevedore=INFO', 'keystoneclient=INFO', 'stevedore=INFO',
@@ -125,10 +63,165 @@ _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
'glanceclient=WARN', 'watcher.openstack.common=WARN'] 'glanceclient=WARN', 'watcher.openstack.common=WARN']
def prepare_service(argv=[], conf=cfg.CONF): class WSGIService(service.ServiceBase):
"""Provides ability to launch Watcher API from wsgi app."""
def __init__(self, name, use_ssl=False):
"""Initialize, but do not start the WSGI server.
:param name: The name of the WSGI server given to the loader.
:param use_ssl: Wraps the socket in an SSL context if True.
"""
self.name = name
self.app = app.VersionSelectorApplication()
self.workers = (CONF.api.workers or
processutils.get_worker_count())
self.server = wsgi.Server(CONF, name, self.app,
host=CONF.api.host,
port=CONF.api.port,
use_ssl=use_ssl,
logger_name=name)
def start(self):
"""Start serving this service using loaded configuration"""
self.server.start()
def stop(self):
"""Stop serving this API"""
self.server.stop()
def wait(self):
"""Wait for the service to stop serving this API"""
self.server.wait()
def reset(self):
"""Reset server greenpool size to default"""
self.server.reset()
class Service(service.ServiceBase, dispatcher.EventDispatcher):
API_VERSION = '1.0'
def __init__(self, manager_class):
super(Service, self).__init__()
self.manager = manager_class()
self.publisher_id = self.manager.publisher_id
self.api_version = self.manager.API_VERSION
self.conductor_topic = self.manager.conductor_topic
self.status_topic = self.manager.status_topic
self.conductor_endpoints = [
ep(self) for ep in self.manager.conductor_endpoints
]
self.status_endpoints = [
ep(self.publisher_id) for ep in self.manager.status_endpoints
]
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self.conductor_topic_handler = self.build_topic_handler(
self.conductor_topic, self.conductor_endpoints)
self.status_topic_handler = self.build_topic_handler(
self.status_topic, self.status_endpoints)
self._conductor_client = None
self._status_client = None
@property
def conductor_client(self):
if self._conductor_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.conductor_topic,
version=self.API_VERSION,
)
self._conductor_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._conductor_client
@conductor_client.setter
def conductor_client(self, c):
self.conductor_client = c
@property
def status_client(self):
if self._status_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.status_topic,
version=self.API_VERSION,
)
self._status_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._status_client
@status_client.setter
def status_client(self, c):
self.status_client = c
def build_topic_handler(self, topic_name, endpoints=()):
return messaging_handler.MessagingHandler(
self.publisher_id, topic_name, [self.manager] + list(endpoints),
self.api_version, self.serializer)
def start(self):
LOG.debug("Connecting to '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.start()
self.status_topic_handler.start()
def stop(self):
LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.conductor_topic_handler.stop()
self.status_topic_handler.stop()
def reset(self):
"""Reset a service in case it received a SIGHUP."""
def wait(self):
"""Wait for service to complete."""
def publish_control(self, event, payload):
return self.conductor_topic_handler.publish_event(event, payload)
def publish_status(self, event, payload, request_id=None):
return self.status_topic_handler.publish_event(
event, payload, request_id)
def get_version(self):
return self.api_version
def check_api_version(self, context):
api_manager_version = self.conductor_client.call(
context.to_dict(), 'check_api_version',
api_version=self.api_version)
return api_manager_version
def response(self, evt, ctx, message):
payload = {
'request_id': ctx['request_id'],
'msg': message
}
self.publish_status(evt, payload)
def process_launcher(conf=cfg.CONF):
return service.ProcessLauncher(conf)
def prepare_service(argv=(), conf=cfg.CONF):
log.register_options(conf) log.register_options(conf)
gmr_opts.set_defaults(conf)
config.parse_args(argv) config.parse_args(argv)
cfg.set_defaults(_options.log_opts, cfg.set_defaults(_options.log_opts,
default_log_levels=_DEFAULT_LOG_LEVELS) default_log_levels=_DEFAULT_LOG_LEVELS)
log.setup(conf, 'python-watcher') log.setup(conf, 'python-watcher')
conf.log_opt_values(LOG, logging.DEBUG) conf.log_opt_values(LOG, logging.DEBUG)
gmr.TextGuruMeditation.register_section(_('Plugins'), opts.show_plugins)
gmr.TextGuruMeditation.setup_autorun(version)

View File

@@ -16,6 +16,7 @@
"""Utilities and helper functions.""" """Utilities and helper functions."""
from jsonschema import validators
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@@ -41,6 +42,29 @@ CONF.register_opts(UTILS_OPTS)
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class Struct(dict):
"""Specialized dict where you access an item like an attribute
>>> struct = Struct()
>>> struct['a'] = 1
>>> struct.b = 2
>>> assert struct.a == 1
>>> assert struct['b'] == 2
"""
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
try:
self[name] = value
except KeyError:
raise AttributeError(name)
def safe_rstrip(value, chars=None): def safe_rstrip(value, chars=None):
"""Removes trailing characters from a string if that does not make it empty """Removes trailing characters from a string if that does not make it empty
@@ -95,6 +119,36 @@ def is_hostname_safe(hostname):
:returns: True if valid. False if not. :returns: True if valid. False if not.
""" """
m = '^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$' m = r'^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$'
return (isinstance(hostname, six.string_types) and return (isinstance(hostname, six.string_types) and
(re.match(m, hostname) is not None)) (re.match(m, hostname) is not None))
def get_cls_import_path(cls):
"""Return the import path of a given class"""
module = cls.__module__
if module is None or module == str.__module__:
return cls.__name__
return module + '.' + cls.__name__
# 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

@@ -35,7 +35,191 @@ class BaseConnection(object):
"""Base class for storage system connections.""" """Base class for storage system connections."""
@abc.abstractmethod @abc.abstractmethod
def get_audit_template_list(self, context, columns=None, filters=None, def get_goal_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching goals.
Return a list of the specified columns for all goals that
match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of goals to return.
:param marker: the last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_goal(self, values):
"""Create a new goal.
:param values: A dict containing several items used to identify
and track the goal. For example:
::
{
'uuid': utils.generate_uuid(),
'name': 'DUMMY',
'display_name': 'Dummy',
}
:returns: A goal
:raises: :py:class:`~.GoalAlreadyExists`
"""
@abc.abstractmethod
def get_goal_by_id(self, context, goal_id):
"""Return a goal given its ID.
:param context: The security context
:param goal_id: The ID of a goal
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_goal_by_uuid(self, context, goal_uuid):
"""Return a goal given its UUID.
:param context: The security context
:param goal_uuid: The UUID of a goal
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_goal_by_name(self, context, goal_name):
"""Return a goal given its name.
:param context: The security context
:param goal_name: The name of a goal
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def destroy_goal(self, goal_uuid):
"""Destroy a goal.
:param goal_uuid: The UUID of a goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def update_goal(self, goal_uuid, values):
"""Update properties of a goal.
:param goal_uuid: The UUID of a goal
:param values: A dict containing several items used to identify
and track the goal. For example:
::
{
'uuid': utils.generate_uuid(),
'name': 'DUMMY',
'display_name': 'Dummy',
}
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
:raises: :py:class:`~.Invalid`
"""
@abc.abstractmethod
def get_strategy_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching strategies.
Return a list of the specified columns for all strategies that
match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of strategies to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_strategy(self, values):
"""Create a new strategy.
:param values: A dict containing items used to identify
and track the strategy. For example:
::
{
'id': 1,
'uuid': utils.generate_uuid(),
'name': 'my_strategy',
'display_name': 'My strategy',
'goal_uuid': utils.generate_uuid(),
}
:returns: A strategy
:raises: :py:class:`~.StrategyAlreadyExists`
"""
@abc.abstractmethod
def get_strategy_by_id(self, context, strategy_id):
"""Return a strategy given its ID.
:param context: The security context
:param strategy_id: The ID of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_strategy_by_uuid(self, context, strategy_uuid):
"""Return a strategy given its UUID.
:param context: The security context
:param strategy_uuid: The UUID of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_strategy_by_name(self, context, strategy_name):
"""Return a strategy given its name.
:param context: The security context
:param strategy_name: The name of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def destroy_strategy(self, strategy_uuid):
"""Destroy a strategy.
:param strategy_uuid: The UUID of a strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def update_strategy(self, strategy_uuid, values):
"""Update properties of a strategy.
:param strategy_uuid: The UUID of a strategy
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
:raises: :py:class:`~.Invalid`
"""
@abc.abstractmethod
def get_audit_template_list(self, context, filters=None,
limit=None, marker=None, sort_key=None, limit=None, marker=None, sort_key=None,
sort_dir=None): sort_dir=None):
"""Get specific columns for matching audit templates. """Get specific columns for matching audit templates.
@@ -44,8 +228,6 @@ class BaseConnection(object):
match the specified filters. match the specified filters.
:param context: The security context :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 filters: Filters to apply. Defaults to None.
:param limit: Maximum number of audit templates to return. :param limit: Maximum number of audit templates to return.
@@ -75,7 +257,7 @@ class BaseConnection(object):
'extra': {'automatic': True} 'extra': {'automatic': True}
} }
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateAlreadyExists :raises: :py:class:`~.AuditTemplateAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -85,7 +267,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_template_id: The id of an audit template. :param audit_template_id: The id of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -95,7 +277,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_template_uuid: The uuid of an audit template. :param audit_template_uuid: The uuid of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
def get_audit_template_by_name(self, context, audit_template_name): def get_audit_template_by_name(self, context, audit_template_name):
@@ -104,7 +286,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_template_name: The name of an audit template. :param audit_template_name: The name of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -112,7 +294,7 @@ class BaseConnection(object):
"""Destroy an audit_template. """Destroy an audit_template.
:param audit_template_id: The id or uuid of an audit template. :param audit_template_id: The id or uuid of an audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -121,8 +303,8 @@ class BaseConnection(object):
:param audit_template_id: The id or uuid of an audit template. :param audit_template_id: The id or uuid of an audit template.
:returns: An audit template. :returns: An audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -130,11 +312,11 @@ class BaseConnection(object):
"""Soft delete an audit_template. """Soft delete an audit_template.
:param audit_template_id: The id or uuid of an audit template. :param audit_template_id: The id or uuid of an audit template.
:raises: AuditTemplateNotFound :raises: :py:class:`~.AuditTemplateNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
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): marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching audits. """Get specific columns for matching audits.
@@ -142,8 +324,6 @@ class BaseConnection(object):
specified filters. specified filters.
:param context: The security context :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 filters: Filters to apply. Defaults to None.
:param limit: Maximum number of audits to return. :param limit: Maximum number of audits to return.
@@ -171,7 +351,7 @@ class BaseConnection(object):
'deadline': None 'deadline': None
} }
:returns: An audit. :returns: An audit.
:raises: AuditAlreadyExists :raises: :py:class:`~.AuditAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -181,7 +361,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_id: The id of an audit. :param audit_id: The id of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -191,7 +371,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param audit_uuid: The uuid of an audit. :param audit_uuid: The uuid of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -199,7 +379,7 @@ class BaseConnection(object):
"""Destroy an audit and all associated action plans. """Destroy an audit and all associated action plans.
:param audit_id: The id or uuid of an audit. :param audit_id: The id or uuid of an audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -208,8 +388,8 @@ class BaseConnection(object):
:param audit_id: The id or uuid of an audit. :param audit_id: The id or uuid of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """
def soft_delete_audit(self, audit_id): def soft_delete_audit(self, audit_id):
@@ -217,11 +397,11 @@ class BaseConnection(object):
:param audit_id: The id or uuid of an audit. :param audit_id: The id or uuid of an audit.
:returns: An audit. :returns: An audit.
:raises: AuditNotFound :raises: :py:class:`~.AuditNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
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): marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching actions. """Get specific columns for matching actions.
@@ -229,8 +409,6 @@ class BaseConnection(object):
specified filters. specified filters.
:param context: The security context :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 filters: Filters to apply. Defaults to None.
:param limit: Maximum number of actions to return. :param limit: Maximum number of actions to return.
@@ -259,7 +437,7 @@ class BaseConnection(object):
'aggregate': 'nova aggregate name or uuid' 'aggregate': 'nova aggregate name or uuid'
} }
:returns: A action. :returns: A action.
:raises: ActionAlreadyExists :raises: :py:class:`~.ActionAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -269,7 +447,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_id: The id of a action. :param action_id: The id of a action.
:returns: A action. :returns: A action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -279,7 +457,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_uuid: The uuid of a action. :param action_uuid: The uuid of a action.
:returns: A action. :returns: A action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -287,8 +465,8 @@ class BaseConnection(object):
"""Destroy a action and all associated interfaces. """Destroy a action and all associated interfaces.
:param action_id: The id or uuid of a action. :param action_id: The id or uuid of a action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
:raises: ActionReferenced :raises: :py:class:`~.ActionReferenced`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -297,14 +475,14 @@ class BaseConnection(object):
:param action_id: The id or uuid of a action. :param action_id: The id or uuid of a action.
:returns: A action. :returns: A action.
:raises: ActionNotFound :raises: :py:class:`~.ActionNotFound`
:raises: ActionReferenced :raises: :py:class:`~.ActionReferenced`
:raises: Invalid :raises: :py:class:`~.Invalid`
""" """
@abc.abstractmethod @abc.abstractmethod
def get_action_plan_list( 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): marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching action plans. """Get specific columns for matching action plans.
@@ -312,8 +490,6 @@ class BaseConnection(object):
match the specified filters. match the specified filters.
:param context: The security context :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 filters: Filters to apply. Defaults to None.
:param limit: Maximum number of audits to return. :param limit: Maximum number of audits to return.
@@ -332,7 +508,7 @@ class BaseConnection(object):
:param values: A dict containing several items used to identify :param values: A dict containing several items used to identify
and track the action plan. and track the action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanAlreadyExists :raises: :py:class:`~.ActionPlanAlreadyExists`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -342,7 +518,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_plan_id: The id of an action plan. :param action_plan_id: The id of an action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -352,7 +528,7 @@ class BaseConnection(object):
:param context: The security context :param context: The security context
:param action_plan__uuid: The uuid of an action plan. :param action_plan__uuid: The uuid of an action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -360,8 +536,8 @@ class BaseConnection(object):
"""Destroy an action plan and all associated interfaces. """Destroy an action plan and all associated interfaces.
:param action_plan_id: The id or uuid of a action plan. :param action_plan_id: The id or uuid of a action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
:raises: ActionPlanReferenced :raises: :py:class:`~.ActionPlanReferenced`
""" """
@abc.abstractmethod @abc.abstractmethod
@@ -370,7 +546,97 @@ class BaseConnection(object):
:param action_plan_id: The id or uuid of an action plan. :param action_plan_id: The id or uuid of an action plan.
:returns: An action plan. :returns: An action plan.
:raises: ActionPlanNotFound :raises: :py:class:`~.ActionPlanNotFound`
:raises: ActionPlanReferenced :raises: :py:class:`~.ActionPlanReferenced`
:raises: Invalid :raises: :py:class:`~.Invalid`
"""
@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

@@ -28,6 +28,7 @@ import prettytable as ptable
from six.moves import input from six.moves import input
from watcher._i18n import _, _LI from watcher._i18n import _, _LI
from watcher._i18n import lazy_translation_enabled
from watcher.common import context from watcher.common import context
from watcher.common import exception from watcher.common import exception
from watcher.common import utils from watcher.common import utils
@@ -47,6 +48,8 @@ class WatcherObjectsMap(object):
# This is for generating the .pot translations # This is for generating the .pot translations
keymap = collections.OrderedDict([ keymap = collections.OrderedDict([
("goals", _("Goals")),
("strategies", _("Strategies")),
("audit_templates", _("Audit Templates")), ("audit_templates", _("Audit Templates")),
("audits", _("Audits")), ("audits", _("Audits")),
("action_plans", _("Action Plans")), ("action_plans", _("Action Plans")),
@@ -54,11 +57,11 @@ class WatcherObjectsMap(object):
]) ])
def __init__(self): def __init__(self):
for attr_name in self.__class__.keys(): for attr_name in self.keys():
setattr(self, attr_name, []) setattr(self, attr_name, [])
def values(self): def values(self):
return (getattr(self, key) for key in self.__class__.keys()) return (getattr(self, key) for key in self.keys())
@classmethod @classmethod
def keys(cls): def keys(cls):
@@ -98,8 +101,13 @@ class WatcherObjectsMap(object):
def get_count_table(self): def get_count_table(self):
headers = list(self.keymap.values()) headers = list(self.keymap.values())
headers.append(_("Total")) # We also add a total count headers.append(_("Total")) # We also add a total count
translated_headers = [
h.translate() if lazy_translation_enabled() else h
for h in headers
]
counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)] counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)]
table = ptable.PrettyTable(field_names=headers) table = ptable.PrettyTable(field_names=translated_headers)
table.add_row(counters) table.add_row(counters)
return table.get_string() return table.get_string()
@@ -143,9 +151,9 @@ class PurgeCommand(object):
query_func = None query_func = None
if not utils.is_uuid_like(uuid_or_name): if not utils.is_uuid_like(uuid_or_name):
query_func = objects.audit_template.AuditTemplate.get_by_name query_func = objects.AuditTemplate.get_by_name
else: else:
query_func = objects.audit_template.AuditTemplate.get_by_uuid query_func = objects.AuditTemplate.get_by_uuid
try: try:
audit_template = query_func(cls.ctx, uuid_or_name) audit_template = query_func(cls.ctx, uuid_or_name)
@@ -159,45 +167,64 @@ class PurgeCommand(object):
return audit_template.uuid return audit_template.uuid
def _find_goals(self, filters=None):
return objects.Goal.list(self.ctx, filters=filters)
def _find_strategies(self, filters=None):
return objects.Strategy.list(self.ctx, filters=filters)
def _find_audit_templates(self, filters=None): def _find_audit_templates(self, filters=None):
return objects.audit_template.AuditTemplate.list( return objects.AuditTemplate.list(self.ctx, filters=filters)
self.ctx, filters=filters)
def _find_audits(self, filters=None): def _find_audits(self, filters=None):
return objects.audit.Audit.list(self.ctx, filters=filters) return objects.Audit.list(self.ctx, filters=filters)
def _find_action_plans(self, filters=None): def _find_action_plans(self, filters=None):
return objects.action_plan.ActionPlan.list(self.ctx, filters=filters) return objects.ActionPlan.list(self.ctx, filters=filters)
def _find_actions(self, filters=None): def _find_actions(self, filters=None):
return objects.action.Action.list(self.ctx, filters=filters) return objects.Action.list(self.ctx, filters=filters)
def _find_orphans(self): def _find_orphans(self):
orphans = WatcherObjectsMap() orphans = WatcherObjectsMap()
filters = dict(deleted=False) filters = dict(deleted=False)
audit_templates = objects.audit_template.AuditTemplate.list( goals = objects.Goal.list(self.ctx, filters=filters)
self.ctx, filters=filters) strategies = objects.Strategy.list(self.ctx, filters=filters)
audits = objects.audit.Audit.list(self.ctx, filters=filters) audit_templates = objects.AuditTemplate.list(self.ctx, filters=filters)
action_plans = objects.action_plan.ActionPlan.list( audits = objects.Audit.list(self.ctx, filters=filters)
self.ctx, filters=filters) action_plans = objects.ActionPlan.list(self.ctx, filters=filters)
actions = objects.action.Action.list(self.ctx, filters=filters) actions = objects.Action.list(self.ctx, filters=filters)
audit_template_ids = set(at.id for at in audit_templates) goal_ids = set(g.id for g in goals)
orphans.strategies = [
strategy for strategy in strategies
if strategy.goal_id not in goal_ids]
strategy_ids = [s.id for s in (s for s in strategies
if s not in orphans.strategies)]
orphans.audit_templates = [
audit_template for audit_template in audit_templates
if audit_template.goal_id not in goal_ids or
(audit_template.strategy_id and
audit_template.strategy_id not in strategy_ids)]
audit_template_ids = [at.id for at in audit_templates
if at not in orphans.audit_templates]
orphans.audits = [ orphans.audits = [
audit for audit in audits audit for audit in audits
if audit.audit_template_id not in audit_template_ids] if audit.audit_template_id not in audit_template_ids]
# Objects with orphan parents are themselves orphans # Objects with orphan parents are themselves orphans
audit_ids = [audit.id for audit in (a for a in audits audit_ids = [audit.id for audit in audits
if a not in orphans.audits)] if audit not in orphans.audits]
orphans.action_plans = [ orphans.action_plans = [
ap for ap in action_plans ap for ap in action_plans
if ap.audit_id not in audit_ids] if ap.audit_id not in audit_ids]
# Objects with orphan parents are themselves orphans # Objects with orphan parents are themselves orphans
action_plan_ids = [ap.id for ap in (a for a in action_plans action_plan_ids = [ap.id for ap in action_plans
if a not in orphans.action_plans)] if ap not in orphans.action_plans]
orphans.actions = [ orphans.actions = [
action for action in actions action for action in actions
if action.action_plan_id not in action_plan_ids] if action.action_plan_id not in action_plan_ids]
@@ -209,18 +236,21 @@ class PurgeCommand(object):
def _find_soft_deleted_objects(self): def _find_soft_deleted_objects(self):
to_be_deleted = WatcherObjectsMap() to_be_deleted = WatcherObjectsMap()
expiry_date = self.get_expiry_date() expiry_date = self.get_expiry_date()
filters = dict(deleted=True) filters = dict(deleted=True)
if self.uuid: if self.uuid:
filters["uuid"] = self.uuid filters["uuid"] = self.uuid
if expiry_date: if expiry_date:
filters.update(dict(deleted_at__lt=expiry_date)) filters.update(dict(deleted_at__lt=expiry_date))
to_be_deleted.goals.extend(self._find_goals(filters))
to_be_deleted.strategies.extend(self._find_strategies(filters))
to_be_deleted.audit_templates.extend( to_be_deleted.audit_templates.extend(
self._find_audit_templates(filters)) self._find_audit_templates(filters))
to_be_deleted.audits.extend(self._find_audits(filters)) to_be_deleted.audits.extend(self._find_audits(filters))
to_be_deleted.action_plans.extend(self._find_action_plans(filters)) to_be_deleted.action_plans.extend(
self._find_action_plans(filters))
to_be_deleted.actions.extend(self._find_actions(filters)) to_be_deleted.actions.extend(self._find_actions(filters))
soft_deleted_objs = self._find_related_objects( soft_deleted_objs = self._find_related_objects(
@@ -233,6 +263,23 @@ class PurgeCommand(object):
def _find_related_objects(self, objects_map, base_filters=None): def _find_related_objects(self, objects_map, base_filters=None):
base_filters = base_filters or {} base_filters = base_filters or {}
for goal in objects_map.goals:
filters = {}
filters.update(base_filters)
filters.update(dict(goal_id=goal.id))
related_objs = WatcherObjectsMap()
related_objs.strategies = self._find_strategies(filters)
related_objs.audit_templates = self._find_audit_templates(filters)
objects_map += related_objs
for strategy in objects_map.strategies:
filters = {}
filters.update(base_filters)
filters.update(dict(strategy_id=strategy.id))
related_objs = WatcherObjectsMap()
related_objs.audit_templates = self._find_audit_templates(filters)
objects_map += related_objs
for audit_template in objects_map.audit_templates: for audit_template in objects_map.audit_templates:
filters = {} filters = {}
filters.update(base_filters) filters.update(base_filters)
@@ -282,21 +329,48 @@ class PurgeCommand(object):
return self._delete_up_to_max return self._delete_up_to_max
def _aggregate_objects(self): def _aggregate_objects(self):
"""Objects aggregated on a 'per audit template' basis""" """Objects aggregated on a 'per goal' basis"""
# todo: aggregate orphans as well # todo: aggregate orphans as well
aggregate = [] aggregate = []
for audit_template in self._objects_map.audit_templates: for goal in self._objects_map.goals:
related_objs = WatcherObjectsMap() related_objs = WatcherObjectsMap()
related_objs.audit_templates = [audit_template]
# goals
related_objs.goals = [goal]
# strategies
goal_ids = [goal.id]
related_objs.strategies = [
strategy for strategy in self._objects_map.strategies
if strategy.goal_id in goal_ids
]
# audit templates
strategy_ids = [
strategy.id for strategy in related_objs.strategies]
related_objs.audit_templates = [
at for at in self._objects_map.audit_templates
if at.goal_id in goal_ids or
(at.strategy_id and at.strategy_id in strategy_ids)
]
# audits
audit_template_ids = [
audit_template.id
for audit_template in related_objs.audit_templates]
related_objs.audits = [ related_objs.audits = [
audit for audit in self._objects_map.audits audit for audit in self._objects_map.audits
if audit.audit_template_id == audit_template.id if audit.audit_template_id in audit_template_ids
] ]
# action plans
audit_ids = [audit.id for audit in related_objs.audits] audit_ids = [audit.id for audit in related_objs.audits]
related_objs.action_plans = [ related_objs.action_plans = [
action_plan for action_plan in self._objects_map.action_plans action_plan for action_plan in self._objects_map.action_plans
if action_plan.audit_id in audit_ids if action_plan.audit_id in audit_ids
] ]
# actions
action_plan_ids = [ action_plan_ids = [
action_plan.id for action_plan in related_objs.action_plans action_plan.id for action_plan in related_objs.action_plans
] ]

View File

@@ -14,17 +14,19 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""SQLAlchemy storage backend.""" """SQLAlchemy storage backend."""
import collections
import datetime
import operator
from oslo_config import cfg from oslo_config import cfg
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import session as db_session from oslo_db.sqlalchemy import session as db_session
from oslo_db.sqlalchemy import utils as db_utils from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log
from sqlalchemy.orm import exc from sqlalchemy.orm import exc
from watcher import _i18n from watcher._i18n import _
from watcher.common import exception from watcher.common import exception
from watcher.common import utils from watcher.common import utils
from watcher.db import api from watcher.db import api
@@ -35,8 +37,6 @@ from watcher.objects import audit as audit_objects
from watcher.objects import utils as objutils from watcher.objects import utils as objutils
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__)
_ = _i18n._
_FACADE = None _FACADE = None
@@ -104,176 +104,268 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None,
return query.all() 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): class Connection(api.BaseConnection):
"""SqlAlchemy connection.""" """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): def __init__(self):
super(Connection, self).__init__() super(Connection, self).__init__()
def __add_soft_delete_mixin_filters(self, query, filters, model): def __add_simple_filter(self, query, model, fieldname, value, operator_):
field = getattr(model, fieldname)
if field.type.python_type is datetime.datetime:
value = objutils.datetime_or_str_or_none(value)
return query.filter(self.valid_operators[operator_](field, value))
def __add_join_filter(self, query, model, fieldname, value, operator_):
query = query.join(model)
return self.__add_simple_filter(query, model, fieldname,
value, operator_)
def __decompose_filter(self, raw_fieldname):
"""Decompose a filter name into its 2 subparts
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.
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 {}
# Special case for 'deleted' because it is a non-boolean flag
if 'deleted' in filters: if 'deleted' in filters:
if bool(filters['deleted']): deleted_filter = filters.pop('deleted')
query = query.filter(model.deleted != 0) op = 'eq' if not bool(deleted_filter) else 'neq'
else: filters['deleted__%s' % op] = 0
query = query.filter(model.deleted == 0)
if 'deleted_at__eq' in filters: plain_fields = tuple(
query = query.filter( (list(plain_fields) or []) +
model.deleted_at == objutils.datetime_or_str_or_none( soft_delete_mixin_fields +
filters['deleted_at__eq'])) timestamp_mixin_fields)
if 'deleted_at__gt' in filters: join_fieldmap = join_fieldmap or {}
query = query.filter(
model.deleted_at > objutils.datetime_or_str_or_none( for raw_fieldname, value in filters.items():
filters['deleted_at__gt'])) fieldname, operator_ = self.__decompose_filter(raw_fieldname)
if 'deleted_at__gte' in filters: if fieldname in plain_fields:
query = query.filter( query = self.__add_simple_filter(
model.deleted_at >= objutils.datetime_or_str_or_none( query, model, fieldname, value, operator_)
filters['deleted_at__gte'])) elif fieldname in join_fieldmap:
if 'deleted_at__lt' in filters: join_field, join_model = join_fieldmap[fieldname]
query = query.filter( query = self.__add_join_filter(
model.deleted_at < objutils.datetime_or_str_or_none( query, join_model, join_field, value, operator_)
filters['deleted_at__lt']))
if 'deleted_at__lte' in filters:
query = query.filter(
model.deleted_at <= objutils.datetime_or_str_or_none(
filters['deleted_at__lte']))
return query return query
def __add_timestamp_mixin_filters(self, query, filters, model): def _get(self, context, model, fieldname, value):
if 'created_at__eq' in filters: query = model_query(model)
query = query.filter( query = query.filter(getattr(model, fieldname) == value)
model.created_at == objutils.datetime_or_str_or_none( if not context.show_deleted:
filters['created_at__eq'])) query = query.filter(model.deleted_at.is_(None))
if 'created_at__gt' in filters:
query = query.filter(
model.created_at > objutils.datetime_or_str_or_none(
filters['created_at__gt']))
if 'created_at__gte' in filters:
query = query.filter(
model.created_at >= objutils.datetime_or_str_or_none(
filters['created_at__gte']))
if 'created_at__lt' in filters:
query = query.filter(
model.created_at < objutils.datetime_or_str_or_none(
filters['created_at__lt']))
if 'created_at__lte' in filters:
query = query.filter(
model.created_at <= objutils.datetime_or_str_or_none(
filters['created_at__lte']))
if 'updated_at__eq' in filters: try:
query = query.filter( obj = query.one()
model.updated_at == objutils.datetime_or_str_or_none( except exc.NoResultFound:
filters['updated_at__eq'])) raise exception.ResourceNotFound(name=model.__name__, id=value)
if 'updated_at__gt' in filters:
query = query.filter(
model.updated_at > objutils.datetime_or_str_or_none(
filters['updated_at__gt']))
if 'updated_at__gte' in filters:
query = query.filter(
model.updated_at >= objutils.datetime_or_str_or_none(
filters['updated_at__gte']))
if 'updated_at__lt' in filters:
query = query.filter(
model.updated_at < objutils.datetime_or_str_or_none(
filters['updated_at__lt']))
if 'updated_at__lte' in filters:
query = query.filter(
model.updated_at <= objutils.datetime_or_str_or_none(
filters['updated_at__lte']))
return query return obj
def _update(self, model, id_, values):
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
ref.update(values)
return ref
def _soft_delete(self, model, id_):
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
query.soft_delete()
def _destroy(self, model, id_):
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
query.delete()
def _add_goals_filters(self, query, filters):
if filters is None:
filters = {}
plain_fields = ['uuid', 'name', 'display_name']
return self._add_filters(
query=query, model=models.Goal, filters=filters,
plain_fields=plain_fields)
def _add_strategies_filters(self, query, filters):
plain_fields = ['uuid', 'name', 'display_name', 'goal_id']
join_fieldmap = 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)
def _add_audit_templates_filters(self, query, filters): def _add_audit_templates_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = {}
if 'uuid' in filters: plain_fields = ['uuid', 'name', 'host_aggregate',
query = query.filter_by(uuid=filters['uuid']) 'goal_id', 'strategy_id']
if 'name' in filters: join_fieldmap = JoinMap(
query = query.filter_by(name=filters['name']) goal_uuid=NaturalJoinFilter(
if 'host_aggregate' in filters: join_fieldname="uuid", join_model=models.Goal),
query = query.filter_by(host_aggregate=filters['host_aggregate']) goal_name=NaturalJoinFilter(
if 'goal' in filters: join_fieldname="name", join_model=models.Goal),
query = query.filter_by(goal=filters['goal']) strategy_uuid=NaturalJoinFilter(
join_fieldname="uuid", join_model=models.Strategy),
strategy_name=NaturalJoinFilter(
join_fieldname="name", join_model=models.Strategy),
)
query = self.__add_soft_delete_mixin_filters( return self._add_filters(
query, filters, models.AuditTemplate) query=query, model=models.AuditTemplate, filters=filters,
query = self.__add_timestamp_mixin_filters( plain_fields=plain_fields, join_fieldmap=join_fieldmap)
query, filters, models.AuditTemplate)
return query
def _add_audits_filters(self, query, filters): def _add_audits_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = {}
if 'uuid' in filters: plain_fields = ['uuid', 'audit_type', 'state', 'audit_template_id']
query = query.filter_by(uuid=filters['uuid']) join_fieldmap = {
if 'type' in filters: 'audit_template_uuid': ("uuid", models.AuditTemplate),
query = query.filter_by(type=filters['type']) 'audit_template_name': ("name", models.AuditTemplate),
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'])
query = self.__add_soft_delete_mixin_filters( return self._add_filters(
query, filters, models.Audit) query=query, model=models.Audit, filters=filters,
query = self.__add_timestamp_mixin_filters( plain_fields=plain_fields, join_fieldmap=join_fieldmap)
query, filters, models.Audit)
return query
def _add_action_plans_filters(self, query, filters): def _add_action_plans_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = {}
if 'uuid' in filters: plain_fields = ['uuid', 'state', 'audit_id']
query = query.filter_by(uuid=filters['uuid']) join_fieldmap = {
if 'state' in filters: 'audit_uuid': ("uuid", models.Audit),
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'])
query = self.__add_soft_delete_mixin_filters( return self._add_filters(
query, filters, models.ActionPlan) query=query, model=models.ActionPlan, filters=filters,
query = self.__add_timestamp_mixin_filters( plain_fields=plain_fields, join_fieldmap=join_fieldmap)
query, filters, models.ActionPlan)
return query
def _add_actions_filters(self, query, filters): def _add_actions_filters(self, query, filters):
if filters is None: 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: if 'audit_uuid' in filters:
stmt = model_query(models.ActionPlan).join( stmt = model_query(models.ActionPlan).join(
models.Audit, models.Audit,
@@ -281,18 +373,154 @@ class Connection(api.BaseConnection):
.filter_by(uuid=filters['audit_uuid']).subquery() .filter_by(uuid=filters['audit_uuid']).subquery()
query = query.filter_by(action_plan_id=stmt.c.id) query = query.filter_by(action_plan_id=stmt.c.id)
if 'state' in filters:
query = query.filter_by(state=filters['state'])
if 'alarm' in filters:
query = query.filter_by(alarm=filters['alarm'])
query = self.__add_soft_delete_mixin_filters(
query, filters, models.Action)
query = self.__add_timestamp_mixin_filters(
query, filters, models.Action)
return query 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,
marker=None, sort_key=None, sort_dir=None):
query = model_query(models.Goal)
query = self._add_goals_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
return _paginate_query(models.Goal, limit, marker,
sort_key, sort_dir, query)
def create_goal(self, values):
# ensure defaults are present for new goals
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
goal = models.Goal()
goal.update(values)
try:
goal.save()
except db_exc.DBDuplicateEntry:
raise exception.GoalAlreadyExists(uuid=values['uuid'])
return goal
def _get_goal(self, context, fieldname, value):
try:
return self._get(context, model=models.Goal,
fieldname=fieldname, value=value)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=value)
def get_goal_by_id(self, context, goal_id):
return self._get_goal(context, fieldname="id", value=goal_id)
def get_goal_by_uuid(self, context, goal_uuid):
return self._get_goal(context, fieldname="uuid", value=goal_uuid)
def get_goal_by_name(self, context, goal_name):
return self._get_goal(context, fieldname="name", value=goal_name)
def destroy_goal(self, goal_id):
try:
return self._destroy(models.Goal, goal_id)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=goal_id)
def update_goal(self, goal_id, values):
if 'uuid' in values:
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing Goal."))
try:
return self._update(models.Goal, goal_id, values)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=goal_id)
def soft_delete_goal(self, goal_id):
try:
self._soft_delete(models.Goal, goal_id)
except exception.ResourceNotFound:
raise exception.GoalNotFound(goal=goal_id)
# ### STRATEGIES ### #
def get_strategy_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
query = model_query(models.Strategy)
query = self._add_strategies_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
return _paginate_query(models.Strategy, limit, marker,
sort_key, sort_dir, query)
def create_strategy(self, values):
# ensure defaults are present for new strategies
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
strategy = models.Strategy()
strategy.update(values)
try:
strategy.save()
except db_exc.DBDuplicateEntry:
raise exception.StrategyAlreadyExists(uuid=values['uuid'])
return strategy
def _get_strategy(self, context, fieldname, value):
try:
return self._get(context, model=models.Strategy,
fieldname=fieldname, value=value)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=value)
def get_strategy_by_id(self, context, strategy_id):
return self._get_strategy(context, fieldname="id", value=strategy_id)
def get_strategy_by_uuid(self, context, strategy_uuid):
return self._get_strategy(
context, fieldname="uuid", value=strategy_uuid)
def get_strategy_by_name(self, context, strategy_name):
return self._get_strategy(
context, fieldname="name", value=strategy_name)
def destroy_strategy(self, strategy_id):
try:
return self._destroy(models.Strategy, strategy_id)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=strategy_id)
def update_strategy(self, strategy_id, values):
if 'uuid' in values:
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing Strategy."))
try:
return self._update(models.Strategy, strategy_id, values)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=strategy_id)
def soft_delete_strategy(self, strategy_id):
try:
self._soft_delete(models.Strategy, strategy_id)
except exception.ResourceNotFound:
raise exception.StrategyNotFound(strategy=strategy_id)
# ### AUDIT TEMPLATES ### #
def get_audit_template_list(self, context, filters=None, limit=None, def get_audit_template_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None): marker=None, sort_key=None, sort_dir=None):
@@ -308,110 +536,70 @@ class Connection(api.BaseConnection):
if not values.get('uuid'): if not values.get('uuid'):
values['uuid'] = utils.generate_uuid() values['uuid'] = utils.generate_uuid()
query = model_query(models.AuditTemplate)
query = query.filter_by(name=values.get('name'),
deleted_at=None)
if len(query.all()) > 0:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
audit_template = models.AuditTemplate() audit_template = models.AuditTemplate()
audit_template.update(values) audit_template.update(values)
try: try:
audit_template.save() audit_template.save()
except db_exc.DBDuplicateEntry: except db_exc.DBDuplicateEntry:
raise exception.AuditTemplateAlreadyExists(uuid=values['uuid'], raise exception.AuditTemplateAlreadyExists(
name=values['name']) audit_template=values['name'])
return audit_template return audit_template
def get_audit_template_by_id(self, context, audit_template_id): def _get_audit_template(self, context, fieldname, value):
query = model_query(models.AuditTemplate)
query = query.filter_by(id=audit_template_id)
try: try:
audit_template = query.one() return self._get(context, model=models.AuditTemplate,
if not context.show_deleted: fieldname=fieldname, value=value)
if audit_template.deleted_at is not None: except exception.ResourceNotFound:
raise exception.AuditTemplateNotFound( raise exception.AuditTemplateNotFound(audit_template=value)
audit_template=audit_template_id)
return audit_template def get_audit_template_by_id(self, context, audit_template_id):
except exc.NoResultFound: return self._get_audit_template(
raise exception.AuditTemplateNotFound( context, fieldname="id", value=audit_template_id)
audit_template=audit_template_id)
def get_audit_template_by_uuid(self, context, audit_template_uuid): def get_audit_template_by_uuid(self, context, audit_template_uuid):
query = model_query(models.AuditTemplate) return self._get_audit_template(
query = query.filter_by(uuid=audit_template_uuid) context, fieldname="uuid", value=audit_template_uuid)
try:
audit_template = query.one()
if not context.show_deleted:
if audit_template.deleted_at is not None:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_uuid)
return audit_template
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_uuid)
def get_audit_template_by_name(self, context, audit_template_name): def get_audit_template_by_name(self, context, audit_template_name):
query = model_query(models.AuditTemplate) return self._get_audit_template(
query = query.filter_by(name=audit_template_name) context, fieldname="name", value=audit_template_name)
try:
audit_template = query.one()
if not context.show_deleted:
if audit_template.deleted_at is not None:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_name)
return audit_template
except exc.MultipleResultsFound:
raise exception.Conflict(
_('Multiple audit templates exist with the same name.'
' Please use the audit template uuid instead'))
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_name)
def destroy_audit_template(self, audit_template_id): def destroy_audit_template(self, audit_template_id):
session = get_session() try:
with session.begin(): return self._destroy(models.AuditTemplate, audit_template_id)
query = model_query(models.AuditTemplate, session=session) except exception.ResourceNotFound:
query = add_identity_filter(query, audit_template_id) raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
try:
query.one()
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(node=audit_template_id)
query.delete()
def update_audit_template(self, audit_template_id, values): def update_audit_template(self, audit_template_id, values):
if 'uuid' in values: if 'uuid' in values:
raise exception.Invalid( raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing " message=_("Cannot overwrite UUID for an existing "
"Audit Template.")) "Audit Template."))
try:
return self._do_update_audit_template(audit_template_id, values) return self._update(
models.AuditTemplate, audit_template_id, values)
def _do_update_audit_template(self, audit_template_id, values): except exception.ResourceNotFound:
session = get_session() raise exception.AuditTemplateNotFound(
with session.begin(): audit_template=audit_template_id)
query = model_query(models.AuditTemplate, session=session)
query = add_identity_filter(query, audit_template_id)
try:
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
ref.update(values)
return ref
def soft_delete_audit_template(self, audit_template_id): def soft_delete_audit_template(self, audit_template_id):
session = get_session() try:
with session.begin(): self._soft_delete(models.AuditTemplate, audit_template_id)
query = model_query(models.AuditTemplate, session=session) except exception.ResourceNotFound:
query = add_identity_filter(query, audit_template_id) raise exception.AuditTemplateNotFound(
audit_template=audit_template_id)
try: # ### AUDITS ### #
query.one()
except exc.NoResultFound:
raise exception.AuditTemplateNotFound(node=audit_template_id)
query.soft_delete()
def get_audit_list(self, context, filters=None, limit=None, marker=None, def get_audit_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None): sort_key=None, sort_dir=None):
@@ -495,33 +683,18 @@ class Connection(api.BaseConnection):
message=_("Cannot overwrite UUID for an existing " message=_("Cannot overwrite UUID for an existing "
"Audit.")) "Audit."))
return self._do_update_audit(audit_id, values) try:
return self._update(models.Audit, audit_id, values)
def _do_update_audit(self, audit_id, values): except exception.ResourceNotFound:
session = get_session() raise exception.AuditNotFound(audit=audit_id)
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
def soft_delete_audit(self, audit_id): def soft_delete_audit(self, audit_id):
session = get_session() try:
with session.begin(): self._soft_delete(models.Audit, audit_id)
query = model_query(models.Audit, session=session) except exception.ResourceNotFound:
query = add_identity_filter(query, audit_id) raise exception.AuditNotFound(audit=audit_id)
try: # ### ACTIONS ### #
query.one()
except exc.NoResultFound:
raise exception.AuditNotFound(node=audit_id)
query.soft_delete()
def get_action_list(self, context, filters=None, limit=None, marker=None, def get_action_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None): sort_key=None, sort_dir=None):
@@ -612,12 +785,14 @@ class Connection(api.BaseConnection):
try: try:
query.one() query.one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ActionNotFound(node=action_id) raise exception.ActionNotFound(action=action_id)
query.soft_delete() query.soft_delete()
# ### ACTION PLANS ### #
def get_action_plan_list( def get_action_plan_list(
self, context, columns=None, filters=None, limit=None, self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None): marker=None, sort_key=None, sort_dir=None):
query = model_query(models.ActionPlan) query = model_query(models.ActionPlan)
query = self._add_action_plans_filters(query, filters) query = self._add_action_plans_filters(query, filters)
@@ -723,6 +898,79 @@ class Connection(api.BaseConnection):
try: try:
query.one() query.one()
except exc.NoResultFound: except exc.NoResultFound:
raise exception.ActionPlanNotFound(node=action_plan_id) raise exception.ActionPlanNotFound(action_plan=action_plan_id)
query.soft_delete() query.soft_delete()
# ### 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.ext.declarative import declarative_base
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy import Integer from sqlalchemy import Integer
from sqlalchemy import Numeric
from sqlalchemy import schema from sqlalchemy import schema
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy.types import TypeDecorator, TEXT from sqlalchemy.types import TypeDecorator, TEXT
@@ -110,13 +111,43 @@ class WatcherBase(models.SoftDeleteMixin,
Base = declarative_base(cls=WatcherBase) Base = declarative_base(cls=WatcherBase)
class Strategy(Base):
"""Represents a strategy."""
__tablename__ = 'strategies'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_strategies0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(63), nullable=False)
display_name = Column(String(63), nullable=False)
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
parameters_spec = Column(JSONEncodedDict, nullable=True)
class Goal(Base):
"""Represents a goal."""
__tablename__ = 'goals'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_goals0uuid'),
table_args(),
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(63), nullable=False)
display_name = Column(String(63), nullable=False)
efficacy_specification = Column(JSONEncodedList, nullable=False)
class AuditTemplate(Base): class AuditTemplate(Base):
"""Represents an audit template.""" """Represents an audit template."""
__tablename__ = 'audit_templates' __tablename__ = 'audit_templates'
__table_args__ = ( __table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'), schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
schema.UniqueConstraint('name', name='uniq_audit_templates0name'),
table_args() table_args()
) )
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@@ -124,7 +155,8 @@ class AuditTemplate(Base):
name = Column(String(63), nullable=True) name = Column(String(63), nullable=True)
description = Column(String(255), nullable=True) description = Column(String(255), nullable=True)
host_aggregate = Column(Integer, nullable=True) host_aggregate = Column(Integer, nullable=True)
goal = Column(String(63), nullable=True) goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
extra = Column(JSONEncodedDict) extra = Column(JSONEncodedDict)
version = Column(String(15), nullable=True) version = Column(String(15), nullable=True)
@@ -139,11 +171,13 @@ class Audit(Base):
) )
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
uuid = Column(String(36)) uuid = Column(String(36))
type = Column(String(20)) audit_type = Column(String(20))
state = Column(String(20), nullable=True) state = Column(String(20), nullable=True)
deadline = Column(DateTime, nullable=True) deadline = Column(DateTime, nullable=True)
audit_template_id = Column(Integer, ForeignKey('audit_templates.id'), audit_template_id = Column(Integer, ForeignKey('audit_templates.id'),
nullable=False) nullable=False)
parameters = Column(JSONEncodedDict, nullable=True)
interval = Column(Integer, nullable=True)
class Action(Base): class Action(Base):
@@ -162,8 +196,6 @@ class Action(Base):
action_type = Column(String(255), nullable=False) action_type = Column(String(255), nullable=False)
input_parameters = Column(JSONEncodedDict, nullable=True) input_parameters = Column(JSONEncodedDict, nullable=True)
state = Column(String(20), nullable=True) state = Column(String(20), nullable=True)
# todo(jed) remove parameter alarm
alarm = Column(String(36))
next = Column(String(36), nullable=True) next = Column(String(36), nullable=True)
@@ -178,9 +210,24 @@ class ActionPlan(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
uuid = Column(String(36)) uuid = Column(String(36))
first_action_id = Column(Integer) first_action_id = Column(Integer)
# first_action_id = Column(Integer, ForeignKeyConstraint( audit_id = Column(Integer, ForeignKey('audits.id'), nullable=True)
# ['first_action_id'], ['actions.id'], name='fk_first_action_id'),
# nullable=True)
audit_id = Column(Integer, ForeignKey('audits.id'),
nullable=True)
state = Column(String(20), nullable=True) 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 # Copyright (c) 2015 b<>com
# #
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -19,9 +20,92 @@
import abc import abc
import six 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) @six.add_metaclass(abc.ABCMeta)
class BaseAuditHandler(object): class BaseAuditHandler(object):
@abc.abstractmethod @abc.abstractmethod
def execute(self, audit_uuid, request_context): def execute(self, audit_uuid, request_context):
raise NotImplementedError() 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,17 +13,14 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
#
from __future__ import unicode_literals from watcher.decision_engine.audit import base
from oslo_log import log
from watcher.common.loader import default
LOG = log.getLogger(__name__)
class DefaultActionLoader(default.DefaultLoader): class OneShotAuditHandler(base.AuditHandler):
def __init__(self): def do_execute(self, audit, request_context):
super(DefaultActionLoader, self).__init__(namespace='watcher_actions') # execute the strategy
solution = self.strategy_context.execute_strategy(audit.uuid,
request_context)
return solution

View File

@@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com # Copyright (c) 2016 b<>com
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -13,18 +13,14 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
#
from __future__ import unicode_literals from watcher.decision_engine.goal import goals
from oslo_log import log Dummy = goals.Dummy
ServerConsolidation = goals.ServerConsolidation
ThermalOptimization = goals.ThermalOptimization
Unclassified = goals.Unclassified
WorkloadBalancing = goals.WorkloadBalancing
from watcher.common.loader.default import DefaultLoader __all__ = ("Dummy", "ServerConsolidation", "ThermalOptimization",
"Unclassified", "WorkloadBalancing", )
LOG = log.getLogger(__name__)
class DefaultPlannerLoader(DefaultLoader):
def __init__(self):
super(DefaultPlannerLoader, self).__init__(
namespace='watcher_planners')

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

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