Compare commits

...

139 Commits

Author SHA1 Message Date
Jenkins
3ebe8ab70f Merge "Added Tempest API tests for /scoring_engines" 2016-11-16 21:16:25 +00:00
Jenkins
240f849758 Merge "Implemented base + moved plugins & service conf" 2016-11-16 17:24:45 +00:00
Jenkins
46f23fac57 Merge "Add audit.planner events" 2016-11-16 16:52:13 +00:00
Jenkins
83ea0bff45 Merge "Add audit.strategy events" 2016-11-16 16:50:13 +00:00
Vincent Françoise
46f511a8c8 Implemented base + moved plugins & service conf
In this changeset, I implemented the main logic although this is
mainly a shameful copy/paste of Nova's blueprint
https://blueprints.launchpad.net/nova/+spec/centralize-config-options

Partially Implements: blueprint centralise-config-opts

Change-Id: Ib645ad5da5c706336bb6ac37e85b027d05665c32
2016-11-16 17:45:07 +01:00
Jenkins
2c5be7c974 Merge "Implemented audit.delete notification" 2016-11-16 16:43:43 +00:00
Jenkins
081ec077ad Merge "Implemented audit.create notification" 2016-11-16 16:43:37 +00:00
Jenkins
f32206d845 Merge "Implemented audit.update notification" 2016-11-16 16:43:31 +00:00
Jenkins
e476384425 Merge "Update devstack plugin to add notification param" 2016-11-16 16:42:36 +00:00
Jenkins
822fe78675 Merge "Added notification_level config option" 2016-11-16 16:30:18 +00:00
Jenkins
6fabfe2af1 Merge "Removed status_topic config parameter" 2016-11-16 16:30:13 +00:00
Jenkins
982a49b952 Merge "Remove stale notification code" 2016-11-16 16:27:09 +00:00
Jenkins
19da978a1c Merge "Added notifications documentation page" 2016-11-16 15:35:27 +00:00
Jenkins
f4bced5a79 Merge "Added support for versioned notifications" 2016-11-16 15:31:45 +00:00
Jenkins
168537a754 Merge "Added Model base class + related doc" 2016-11-16 13:55:06 +00:00
Vincent Françoise
25da1636b1 Add audit.planner events
In this changeset, I implemented the following notifications:

- audit.planner.start
- audit.planner.end
- audit.planner.error

Partially Implements: blueprint audit-versioned-notifications-api

Change-Id: I29d371f8cb3fd7948c87bec97186e24a88e8fdb6
2016-11-16 13:59:00 +01:00
Vincent Françoise
19fe0a0c56 Add audit.strategy events
In this changeset, I implemented the following notifications:

- audit.strategy.start
- audit.strategy.end
- audit.strategy.error

Partially Implements: blueprint audit-versioned-notifications-api

Change-Id: I6ae8468caf8d215bc8bc694813beb4dc94f53fdb
2016-11-16 13:59:00 +01:00
Vincent Françoise
6f9f67cacc Implemented audit.delete notification
In this changeset, I implemented the audit.delete notification.

Partially Implements: blueprint audit-versioned-notifications-api

Change-Id: I8aeb73f4b8d3273d6de7dc9c44674e3041b005ea
2016-11-16 13:59:00 +01:00
Vincent Françoise
0c0a9c84d6 Implemented audit.create notification
In this changeset, I implemented the audit.create notification.

Change-Id: Ia092ca3a3dc951e3313a07f15a98aec5818e9ab0
Partially-Implements: blueprint audit-versioned-notifications-api
2016-11-16 13:59:00 +01:00
Vincent Françoise
9405eb0806 Implemented audit.update notification
In this changeset, I implemented the sending of update notifications
whenever an audit is modified.

Change-Id: I5ccc2516ce896ae7d4ef542b133e8f052eaed602
Partially-Implements: blueprint audit-versioned-notifications-api
2016-11-16 13:59:00 +01:00
Vincent Françoise
54c45a2738 Update devstack plugin to add notification param
In this changeset, I updated the devstack plugin so it has the
necessary configuration options for it to emit the Watcher
notifications straight out of the box.

Partially Implements: blueprint watcher-notifications-ovo

Change-Id: I048e0b82cde40bd3699c90429bdf55ee235fff3b
2016-11-16 13:57:25 +01:00
Vincent Françoise
4bebf882d9 Added notification_level config option
In this changeset, I implemented the notification_level parameter
that allows you to set the level minimum level of notification to
be emitted by Watcher.

Partially Implements: blueprint watcher-notifications-ovo

Change-Id: I78b30ceb3ee7606078a87beb3a4ce5d9568af1e0
2016-11-16 13:57:25 +01:00
Vincent Françoise
cdda06c08c Removed status_topic config parameter
In this changeset, I removed the now obsolete status_topic config
option.

DocImpact
Partially Implements: blueprint watcher-notifications-ovo

Change-Id: Icfc03abd875b77fc456bfa286ac2b5774651e8fa
2016-11-16 13:57:25 +01:00
Vincent Françoise
395ccbd94c Remove stale notification code
In this changeset, I cleaned up the Watcher codebase to remove
the old notification mechanism that is actually unused.

Partially Implements: blueprint watcher-notifications-ovo

Change-Id: I1901e65f031441b98a7d6f6c9c1c0364eaaaf481
2016-11-16 13:57:25 +01:00
Vincent Françoise
cdee2719f7 Added notifications documentation page
In this changeset, I added a new Sphinx directive (from Nova) that
allows us to automatically display notification samples for of
all the upcoming notifications in Watcher.

Partially Implements: blueprint watcher-notifications-ovo

Change-Id: Id7d819ee21db6dc616c78faea483b883f77e3bd6
2016-11-16 13:57:25 +01:00
Vincent Françoise
b27e5b91b9 Added support for versioned notifications
In this changeset, I added all the required modification in order
for Watcher to enable the implementation of versioned notifications.

Change-Id: I600ecbc767583824555b016fb9fc7faf69c53b39
Partially-Implements: blueprint watcher-notifications-ovo
2016-11-16 13:57:25 +01:00
Jenkins
9dc3fce3e5 Merge "Add doc for vm_workload_consolidation strategy" 2016-11-16 09:34:06 +00:00
Bruno Grazioli
5c793894ab Add doc for vm_workload_consolidation strategy
This patch adds documentation on requirements for
vm_workload_consolidation strategy.

Change-Id: I304644138b34658b09f7cd8f29eb089757807b61
Closes-bug: #1640820
2016-11-16 09:26:31 +01:00
OpenStack Proposal Bot
3532c74001 Updated from global requirements
Change-Id: If8e1f5591d026572e8fdd187ab737801a58b9a88
2016-11-15 19:39:39 +00:00
Jenkins
c0b17105af Merge "Add doc for workload-stabilization spec" 2016-11-15 10:53:58 +00:00
Jenkins
22c424cecb Merge "optimized 'find_instance()'" 2016-11-15 10:08:21 +00:00
Jenkins
01118b72c7 Merge "Remove unused SUBMITTED audit state" 2016-11-14 15:09:57 +00:00
Alexander Chadin
9761207d8a Add doc for workload-stabilization spec
This patch set adds how-to-use documentation for
Watcher Overload standard deviation algorithm.

Change-Id: I75d7cd0ff8507ca70efb6d9668ae9fbf651a7f97
2016-11-14 18:07:11 +03:00
Jenkins
abd93a298e Merge "Fix the typo in efficacy_indicator module" 2016-11-14 14:00:30 +00:00
Jenkins
71c21c3e41 Merge "Use oslo_log instead of logging" 2016-11-14 13:53:19 +00:00
Jenkins
a487718fdc Merge "Added action_plan ObjectField for Action" 2016-11-14 10:10:10 +00:00
Jenkins
b8dddfcf67 Merge "Added audit & strategy ObjectField for ActionPlan" 2016-11-14 10:10:04 +00:00
Jenkins
2a6a96640a Merge "Added goal & strategy ObjectField for Audit" 2016-11-14 10:09:58 +00:00
Jenkins
e7d70cd279 Merge "Added goal+strategy ObjectField for AuditTemplate" 2016-11-14 10:09:02 +00:00
ericxiett
72c9d86094 Fix the typo in efficacy_indicator module
Modify typo 'gobal' to 'global'.

Change-Id: Ib8b4c65d64d767666ea1a32bc72b66559a035c5f
Closes-Bug: #1641408
2016-11-13 22:49:15 +08:00
Alexander Chadin
5e9a2f6fa6 Fix NoMetricValuesForInstance error
This patch set allows in case of NoMetricValuesForInstance exception
to continue the flow of Workload Stabilization strategy
instead of interrupting.

Closes-Bug: #1640096
Change-Id: I2de22bb97defb9c34e409102d06846275793b80d
2016-11-11 12:01:28 +03:00
howardlee
b6e17a8bc8 Use oslo_log instead of logging
1. the use of the log in the module should be unified.
2. oslo_log wrapped logging.

Change-Id: Iac48fcbcd2fc9a7002fed6a328dc74242ad770de
2016-11-10 19:22:25 +08:00
licanwei
3e392b07bf optimized 'find_instance()'
The function nova.servers.list() will return a huge context in the
large infrastructure. We can use nova.servers.get() instead of it.

Change-Id: If922c31e8aff27bfbc475c63083ee561ee7f1f67
2016-11-10 19:17:46 +08:00
Jenkins
21abbb4cd1 Merge "Added 'goal' ObjectField for Strategy object" 2016-11-10 09:38:04 +00:00
Jenkins
a539ef8c58 Merge "Refactored Watcher objects to use OVO" 2016-11-10 09:37:58 +00:00
Jenkins
0c29946590 Merge "Removed deadline, version, extra & host_aggregate" 2016-11-10 09:37:52 +00:00
Jenkins
be0a889327 Merge "Eager loading on One-to-X foreign keys" 2016-11-10 09:26:41 +00:00
Hidekazu Nakamura
ea7e6a7c94 Fix workload stabilization strategy to ignore disabled hosts
Change-Id: Ib5244da196111fac03b1ac4b756a59ad15deb778
Closes-Bug: #1640102
2016-11-10 15:55:55 +09:00
Vincent Françoise
090d5d1f1e Added Tempest API tests for /scoring_engines
In this changeset, I added tempest tests for the scoring engine
endpoint.

Change-Id: I7f36250eb9df437b518d20fd4f2e1feb223f3c82
2016-11-09 16:35:31 +05:30
Jaewoo Park
4973dd6bf6 Remove unused SUBMITTED audit state
Submitted state is not referenced nor used in the Watcher codebase,
and hence removed the references to the state from the codebase.

Change-Id: Ibb0cf5125e2d93683739abf35030b7cf5cb1b056
Closes-Bug: 1634150
2016-11-07 13:16:31 -08:00
Jenkins
ad7ae3e676 Merge "[Doc] Fix strategy list optional argument" 2016-11-07 16:11:58 +00:00
Jenkins
7ed7aade91 Merge "Added missing test on GMR plugin" 2016-11-07 14:50:13 +00:00
Jenkins
1fbc8b57bf Merge "Transform KB into MB and normalize CPU" 2016-11-07 14:24:44 +00:00
Vincent Françoise
b4b17ba395 Added action_plan ObjectField for Action
In this changeset, I added the "action_plan" ObjectField which can
either be loaded by setting the new "eager" parameter as True
or not loaded (as before) by setting it to False. The advantage of
introducing this eager parameter is that this way,
we can reduce to a minimum the overhead of DB queries whenever the
related object fields is not actually needed.

Change-Id: Iae255d9a0a919e2b6db710be24dbf9033b72166e
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:40:07 +01:00
Vincent Françoise
060c369838 Added audit & strategy ObjectField for ActionPlan
In this changeset, I added the "audit" and the "strategy "ObjectField
which can either be loaded by setting the new "eager" parameter as True
or not loaded (as before) by setting it to False.
The advantage of introducing this eager parameter is that this way,
we can reduce to a minimum the overhead of DB queries whenever the
related object fields is not actually needed.

Change-Id: Ieca6fa438cbf2267f5ae2ea0d059b2953e65076b
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:40:07 +01:00
Vincent Françoise
f9df54c555 Added goal & strategy ObjectField for Audit
In this changeset, I added the "goal" and the "strategy "ObjectField
which can either be loaded by setting the new "eager" parameter as True
or not loaded (as before) by setting it to False.
The advantage of introducing this eager parameter is that this way,
we can reduce to a minimum the overhead of DB queries whenever the
related goal is not actually needed.

Change-Id: I5a1b25e395e3d904dae954952f8e0862c3556210
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:39:20 +01:00
Vincent Françoise
54cf10b41c Added goal+strategy ObjectField for AuditTemplate
In this changeset, I added the "goal" and the "strategy "ObjectField
which can either be loaded by setting the new "eager" parameter as True
or not loaded (as before) by setting it to False.
The advantage of introducing this eager parameter is that this way,
we can reduce to a minimum the overhead of DB queries whenever the
related goal is not actually needed.

Change-Id: I97418163f68aea2f1cda80be94e4035f0b3700ae
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:39:20 +01:00
Vincent Françoise
f54aca70cc Added 'goal' ObjectField for Strategy object
In this changeset, I added the "goal" ObjectField which can
either be loaded by setting the new "eager" parameter as True or
not loaded (as before) by setting it to False.
The advantage of introducing this eager parameter is that this way,
we can reduce to a minimum the overhead of DB queries whenever the
related goal is not actually needed.

Partially-Implements: blueprint watcher-versioned-objects
Change-Id: I103c9ed161d2cedf7b43c55f9e095ef66bf44dea
2016-11-07 10:39:20 +01:00
Vincent Françoise
fc31dae7f2 Refactored Watcher objects to use OVO
In this changeset, I modified all existing Watcher objects to now
rely on oslo.versionedobjects as a base.

Change-Id: I3c9b1ca6da529d128743b99020350f28926ea1a2
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:39:19 +01:00
Vincent Françoise
ed95d621f4 Removed deadline, version, extra & host_aggregate
As we are about to version the Watcher objects, we need to make sure
that upcoming model/object modifications are additive in order to
avoid having to bump the major version of the API. Therefore,
this changeset removes 4 unused DB fields that were exposed in their
associated Watcher objects (i.e. AuditTemplate and Audit).

Change-Id: Ifb0783f21cd66db16b31e3c8e376fc9d6c07dea3
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:39:19 +01:00
Vincent Françoise
750e6bf213 Eager loading on One-to-X foreign keys
In this changeset, I added ORM relationships to the DB models
concerning the already-declared foreign keys.
I also modified the DB query building to now handle a new 'eager'
parameter that, if True, is responsible to also fetch the data
relative to these 'parent' DB entities (no cascading).

Change-Id: Ieea181af9a4b173c54621dcc6c549161f5a35aeb
Partially-Implements: blueprint watcher-versioned-objects
2016-11-07 10:37:14 +01:00
Hidekazu Nakamura
afdfd8161f [Doc] Fix strategy list optional argument
Optional argument --goal-uuid changed to --goal.
This patch fixes it.

Change-Id: I79dedc94d6437eda0363da91b5065724e22bc1c8
2016-11-04 16:29:34 +09:00
Atul Pandey
aae3f79fef Use Enum value instead of String Value
Fixing Gating Issue.

Change-Id: I64a0df9e27e09172119a3a8f5395204b7476f23d
2016-11-02 10:32:57 +00:00
Jenkins
86f4f6a979 Merge "Change "Openstack" to "OpenStack"" 2016-10-26 08:50:14 +00:00
liyanhang
b48b881c20 Change "Openstack" to "OpenStack"
According to the word choice convention in
http://docs.openstack.org/contributor-guide/writing-style/word-choice.html
We should use OpenStack instead of Openstack.

Change-Id: I4300305eabc8d7cfe0072e5ac345dab63303558c
2016-10-25 13:07:26 +00:00
qinchunhua
e681645a54 Avoid use xx=[] for parameter to initialize it's value
This patch is deprecated use xx = [] for the parameter initial value,
this parameter will only be initialized at the first call,this is not
what we expected.
Better choice is to set the initial value to None,
then initialize xx with xx= xx or [] in method body.

More details:http://effbot.org/zone/default-values.htm

Change-Id: Iee0648fd500422f48c3f7f35b5066fdc644ae308
2016-10-25 12:47:36 +00:00
Jenkins
111e04d5c3 Merge "Updated from global requirements" 2016-10-18 10:04:46 +00:00
Jenkins
26424ee670 Merge "Remove duplicate unittest" 2016-10-18 09:46:42 +00:00
Jenkins
a95b73f3bb Merge "Fix typo in docstring" 2016-10-18 09:46:34 +00:00
Alexander Chadin
ef0d133ba8 Transform KB into MB and normalize CPU
This patch set normalizes CPU for hardware.cpu.util meter and
transform KB into MB fetched from hardware.memory.used meter.

Change-Id: I6dd1d053e81f06c3adb6fb256ac77bbf55a07f3e
2016-10-18 11:21:18 +03:00
OpenStack Proposal Bot
40f98bfd07 Updated from global requirements
Change-Id: Ib8ad24fbf57fbbabc7224d2e2c203cf4aeee0bfb
2016-10-17 15:52:45 +00:00
Jenkins
10bf74041b Merge "Add strategy template doc" 2016-10-17 13:47:56 +00:00
Jenkins
b9969d4854 Merge "Fix typo in hooks.py" 2016-10-17 08:50:04 +00:00
Jenkins
989d9807e8 Merge "Test code tidy up" 2016-10-17 08:49:59 +00:00
Jenkins
751d5a00d0 Merge "Delete python bytecode file" 2016-10-17 08:49:49 +00:00
Jenkins
cc44e2a0c0 Merge "Drop MANIFEST.in - it's not needed by pbr" 2016-10-17 08:49:43 +00:00
Jenkins
0139d8537c Merge "Add service object to the watcher_db_schema" 2016-10-17 08:49:35 +00:00
qinchunhua
35e6565183 Delete python bytecode file
This patch delete python bytecode including
pyo before every test run.

Change-Id: Ie6406337072bebaaa94a2b5437a2c1cece107b56
2016-10-15 00:57:02 -04:00
Jenkins
ebc70a46db Merge "Add Audit Scope Handler" 2016-10-14 15:01:20 +00:00
David TARDIVEL
3de2d368c0 Add strategy template doc
Developer should provide a detailled documentation about his strategy
algorithm to make it even easier to use by a Watcher end user.
I propose in this changeset a template for strategy documentation.
you will find also a example with basic consolidation strategy.

Change-Id: I66da1a33b87a94b508dd23ac7dce4cae6f4e068b
2016-10-13 15:30:11 +00:00
Iswarya_Vakati
6331274708 Drop MANIFEST.in - it's not needed by pbr
watcher already uses PBR:-
setuptools.setup(
    setup_requires=['pbr>=1.8'],
    pbr=True)

This patch removes `MANIFEST.in` file as pbr generates a
sensible manifest from git files and some standard files
and it removes the need for an explicit `MANIFEST.in` file.

Change-Id: If1b80738e28d71fd9e9bb4939f94adfd721a041a
Closes-Bug:#1608980
2016-10-13 18:16:44 +05:30
Jenkins
ff81f237a7 Merge "Moved Watcher doc plugin outside main package" 2016-10-13 08:42:09 +00:00
Alexander Chadin
48cc6b2718 Add Audit Scope Handler
This patch set adds audit scope mechanism.
It also removes host_aggregate field.

Change-Id: Ia98ed180a93fc8c19599735e2b41471d322bae9a
Partially-Implements: blueprint define-the-audit-scope
2016-10-13 10:50:58 +03:00
Alexander Chadin
c34a1acbea Add service object to the watcher_db_schema
This patch set adds service object to the watcher_db_schema_diagram.txt
New watcher_db_schema_diagram.png is compiled and attached.

Partially-Implements: blueprint watcher-service-list
Change-Id: Ibaac9020b0cb9fb147259a8685a7a072216ff95d
2016-10-12 12:53:11 +00:00
Alexander Chadin
e7a1e148ca Add service supervisor
This patch set adds supervisor mechanism for Watcher services
to get ability to track states.

Partially-Implements: blueprint watcher-service-list
Change-Id: Iab1cefb971c79ed27b22b6a5d1bed8698e35f9a4
2016-10-12 15:52:06 +03:00
Jenkins
6cf796ca87 Merge "Added composite unique name constraints" 2016-10-12 12:45:53 +00:00
Jenkins
4f6d42e26a Merge "Updated from global requirements" 2016-10-12 12:13:06 +00:00
Jenkins
c3aac66add Merge "Stop adding ServiceAvailable group option" 2016-10-12 12:12:55 +00:00
OpenStack Proposal Bot
0a7a9e9ab4 Updated from global requirements
Change-Id: Ib9ba6bcb305584db7675db73850dee844e2e2c66
2016-10-11 19:17:31 +00:00
Vincent Françoise
3129f31208 Added Model base class + related doc
In this changeset I added a Model abtract base class for models
and updated the plugin docs to reference it. I also added the
CDM and CDMC acronyms to the glossary.

Change-Id: Ie7d76a9f84d545715fa6e5bf350b9d2321bab9e1
2016-10-11 17:48:14 +02:00
licanwei
cd2cd184eb Add RECOMMENDED state
The state of RECOMMENDED also can change to CANCELLED
state by Adminstrator.

Change-Id: Ia6a117367451e6fb35a55acfcbf0fb788063ea4b
2016-10-09 15:43:10 +08:00
Andreas Jaeger
f1a2de4138 Enable release notes translation
Releasenote translation publishing is being prepared. 'locale_dirs'
needs to be defined in conf.py to generate translated version of the
release notes.

Note that this repository might not get translated release notes - or
no translations at all - but we add the entry here nevertheless to
prepare for it.

Change-Id: I67b2202570e23d3f20f7d132b7d7dbb460a2af3f
2016-10-06 20:56:44 +02:00
Vincent Françoise
10cbcd2432 Added composite unique name constraints
In this changeset, I added composite contraints on models that
were currently subject to an implicit uniqueness.
This composite constraint takes as an input the name and the deleted
columns.
The 'deleted' column is 0 by default and is updated with the value of
the associated 'id' column upon soft deletion.
What this means is that this composite constraint guarantees the fact
that we can hold many soft deleted records holding the exact same name.
Indeed, the value of the deleted column will be different whilst
forbidding 2 non-soft deleted records with the same name as their
associated 'deleted' value will be 0 and subsequently violates the
constraint.

Change-Id: I3ee39e23aa4ca9bc6b4ea9af8c6b7e6d67af0136
2016-10-06 16:59:41 +02:00
Jenkins
431b54fb4c Merge "Watcher utils cleanup" 2016-10-05 15:50:11 +00:00
Vincent Françoise
f2e5702657 Added missing test on GMR plugin
The show_models() function was not covered by any test so this
changeset is adding a new unit test for it.

Change-Id: Ia861f049c1f1ee0aa623404e62fb002392c9052d
2016-10-05 15:41:56 +02:00
Vincent Françoise
96357aec04 Moved Watcher doc plugin outside main package
In this changeset, I renamed watcher/doc.py file as
doc/ext/term.py which subsequently does not get scanned for coverage
anymore. Hence, we can close this bug.

Change-Id: I9d700da6569b464ce71085fe78002521555002b9
Closes-bug: #1527163
2016-10-05 15:22:50 +02:00
Jenkins
eb0da97ea6 Merge "Updated from global requirements" 2016-10-04 16:36:36 +00:00
Nishant Kumar
631e1398a1 Stop adding ServiceAvailable group option
Service available group already exists.Therefore we don't need to
register this group here again.

Change-Id: I2096ed1f69a1f70fbf248fee5fded94e3caca6c6
Closes-Bug: #1621036
2016-10-04 20:48:11 +05:30
haris tanvir
db3b5b30b2 HasLength() rewritten to assertEqual()
HasLength() based assertions are re-written to assertEqual() based
assertion to homogenize the test code base.

For example:

assertThat(.., HasLength()) becomes assertEqual(len(),...)

Change-Id: Ia3bf24e34fffe445ff4b0ee61fb7cbc0eb36a57a
Closes-Bug:#1629898
2016-10-04 12:35:38 +05:00
OpenStack Proposal Bot
74acf2a3f2 Updated from global requirements
Change-Id: I19710f88c213dd34ed2d3bf75e208d6bfe206c0c
2016-10-03 14:17:48 +00:00
Jenkins
2642627da6 Merge "Docstrings should not start with a space" 2016-10-03 06:33:58 +00:00
Jenkins
09861a0d20 Merge "Fix capital letter in doc" 2016-10-03 06:31:03 +00:00
Jenkins
dddd29b04a Merge "Doc updates" 2016-09-28 16:12:40 +00:00
Ha Van Tu
abe37390ae Fix typo in docstring
This patch replaces typo "applicatin" by "application".

Change-Id: I3c117920966fcc4400ee30c368b61180b983dd8b
2016-09-28 12:06:44 +07:00
licanwei
8b37bef12d Remove duplicate unittest
In tests/api/test_hooks.py
the 'test_hook_without_traceback_debug'
and 'test_hook_without_traceback_debug_tracebacks'
have the same function.

Change-Id: I5209bc8b1251b77b7444d51531c3902529d11453
2016-09-28 10:47:03 +08:00
Jenkins
d3d85c4801 Merge "Fixed issue on compute nodes iteration" 2016-09-27 08:20:58 +00:00
zhangyanxian
0f9361ee6c Fix typo in hooks.py
TrivialFix

Change-Id: Iff206110433b9cd76a1a2b7c6475267a9c7a568c
2016-09-27 06:33:20 +00:00
Cao Xuan Hoang
c8d605984f Docstrings should not start with a space
As per OpenStack Docstrings guide lines [1]:
[H401] Docstrings should not start with a space.

[1] http://docs.openstack.org/developer/hacking/#docstrings

trivialfix

Change-Id: Ifdff563f3ab7559e68c7201575045013db7383a5
2016-09-27 13:01:12 +07:00
Jenkins
a5b485ae29 Merge "Refactored Tests to load scenarios from file" 2016-09-26 16:31:24 +00:00
Jenkins
0e4bae9391 Merge "Add constraint target to tox.ini and remove 1 dep" 2016-09-26 09:41:29 +00:00
Ha Van Tu
e7888b2844 Fix capital letter in doc
This patch replaces 'english' by 'English' in the Watcher doc.

Change-Id: I2de7100fbbbb4de5d2d6f9575c116986aa37982a
2016-09-26 16:10:37 +07:00
Vincent Françoise
d0ea20e4bc Doc updates
Updated inconsistent docs.

Change-Id: I4be05f662fee6ebdf721ac93dd97611b5a686273
2016-09-26 09:58:44 +02:00
Vincent Françoise
55591cce43 Watcher utils cleanup
As some of our utils function are actually available in oslo.utils,
this patchset simply gets rid of our local copy and instead references
the oslo.utils one. This will help us deport the code maintenance of
these to a 3rd party library.

Change-Id: I3760ebab69d35c37bbe78500918d4e46b5bd9119
2016-09-23 11:12:26 +02:00
Jenkins
21b3ac173a Merge "Fixed GMR configuration issue" 2016-09-21 14:37:42 +00:00
Jenkins
35a1ee7670 Merge "Fix typo in docstring from "interprete" to "interpret"" 2016-09-21 14:11:28 +00:00
Vincent Françoise
5112f294c8 Fixed GMR configuration issue
GMR was ignoring the config because the conf wasn't passed when
starting any of the Watcher services. This changeset fixes this issue.

Change-Id: If386c5f0459c4278a2a56c8c3185fcdafce673a0
2016-09-21 15:43:44 +02:00
Jenkins
28970d0512 Merge "Fix a typo in watcher.po" 2016-09-21 13:05:58 +00:00
David TARDIVEL
83d06cef5f Add constraint target to tox.ini and remove 1 dep
This adds a pip install command to tox.ini that is only used when the
tox env is passed with the 'constraints' factor appended onto it.
As such this will not effect developer workflows or current unit tests.

The initial use of this will be in a non-voting job, to verify that the
constrained checks with tox are stable.  DevStack is already running
constrained jobs, as such problems are no expected.

To run a tox with pip using constraints on a developer system a
developer should run the desired tox environment with -constraints.
For example: $(tox -epy27-constraints)
Pip will pull the current version of the upper-constraints.txt file down
from the git.openstack.org, however this method can be overriden to use
a local file setting the environment variable "UPPER_CONSTRAINTS_FILE"
to the local path or a different URL, it is passed directly to pip.

This is currently not enabled in the default tox run, however it is
possible to enable it as a default by adding it to 'envlist' in tox.ini

This also removes requirements.txt from tox.ini deps
This is redundant, per lifeless email:
http://lists.openstack.org/pipermail/openstack-dev/2015-July/069663.html

Change-Id: I79c0ceb46fc980840a8baf5fa4a303bb450bfbec
2016-09-21 10:09:47 +00:00
Jenkins
f2ec9fc99a Merge "'tox -e py27' failed" 2016-09-21 09:20:10 +00:00
OpenStack Proposal Bot
0d6166760a Updated from global requirements
Change-Id: I17b916ccbb31ca6bf72579fc10327e2e91ec3cf2
2016-09-21 07:35:28 +00:00
licanwei
d8ae88dd40 'tox -e py27' failed
cfg.CONF.debug should be set False as default,
If it's True, some unittests can't pass.

Change-Id: Ib098250af3aec48aa9d9152e20c80460f3bd641e
Closes-Bug: #1625560
2016-09-21 15:28:00 +08:00
Ha Van Tu
9eeaa07188 Fix typo in docstring from "interprete" to "interpret"
Change-Id: I931aa71dcf208a33ad8f48037b1296a4339e9b76
2016-09-21 12:16:36 +07:00
zhangyanxian
88b04dba2c Fix a typo in watcher.po
TrivialFix

Change-Id: I71a5338df14c5f5b65bed93fc26dabde447c596c
2016-09-21 03:19:35 +00:00
Jenkins
adf65f9f42 Merge "Deactivate dashboard plugin until fixed" 2016-09-20 16:18:06 +00:00
Vincent Françoise
1fc59cca9f Deactivate dashboard plugin until fixed
I deactivated the watcher-dashboard plugin in the docs since it
doesn't at the moment. Will be reactivated once it gets fixed
(see https://bugs.launchpad.net/devstack/+bug/1540328).

Change-Id: I65de8ca4a08cdb294d0e48eac52927eed13ec66c
2016-09-20 17:23:27 +02:00
Jenkins
1887ae402f Merge "Fix a typo in basic_consolidation.py" 2016-09-20 10:11:56 +00:00
licanwei
7714f48520 remove redundant word
root.description = ("Watcher is an OpenStack project which aims to"
                    "to improve physical resources usage through"
Here more one 'to'

Change-Id: I3dac6cb15af43583a1360b7f2f14d4e2c9c26cf9
2016-09-20 15:54:55 +08:00
licanwei
f741434209 Fix a typo in basic_consolidation.py
LOG.info(_LI("Initializing Sercon Consolidation"))
here 'Sercon' should be 'Server'

Change-Id: I3dc341a49066a7280bebb75471c8b433c7d0c1e7
2016-09-20 14:25:36 +08:00
Jenkins
3a3ce0268b Merge "Update Watcher description" 2016-09-19 13:33:33 +00:00
Jenkins
e647c8e01d Merge "Update reno for stable/newton" 2016-09-19 13:11:54 +00:00
Antoine Cabot
8cf233ab03 Update Watcher description
This change-set update Watcher description
as it is used in the ML to announce each
new release.

Change-Id: I6318107c3e3322a3ef734e90c9e3e0176967ceaf
2016-09-19 15:00:00 +02:00
Vincent Françoise
56a9dd0f08 Test code tidy up
Removed some comments and replace some hardocded values with the
correct variable.

Change-Id: Ica897f2de25c9de04ec2d0f94e7a13e84ee97dbb
2016-09-19 10:50:04 +02:00
Thierry Carrez
f79bed060c Update reno for stable/newton
Change-Id: Ia64ef5a4c1cd135b1bf4bbc8486270a0ff067122
2016-09-16 14:51:10 +00:00
Jenkins
e32df3c5eb Merge "Updated from global requirements" 2016-09-16 13:35:32 +00:00
Vincent Françoise
55537d254e Fixed issue on compute nodes iteration
In this changeset, I fixed the issue with the basic server
consolidation strategy to now loop over all compute nodes
as expected instead of stopping after the first one.

Change-Id: If594f0df41e39dfb0ef8f0fce41822018490c4ec
Closes-bug: #1548874
2016-09-16 14:55:51 +02:00
Vincent Françoise
e621f5ddc6 Refactored Tests to load scenarios from file
In this changeset, I simplified the logic that is used to create
cluster data model scenarios.

Change-Id: Ia6e138d9897190d3207a70485dc62ccc34087686
2016-09-16 14:51:42 +02:00
Jenkins
fc36a7a698 Merge "Remove group_by statement in metric queries" 2016-09-16 08:23:51 +00:00
OpenStack Proposal Bot
b2fe413a53 Updated from global requirements
Change-Id: I1dbcf1796b49de27fa8fb6711e4610fb934ba000
2016-09-15 20:27:53 +00:00
David TARDIVEL
b6ab86c45a Remove group_by statement in metric queries
ceilosca seems to not correctly handle group_by statement.
I remove it, because we don't need it for now.

Change-Id: Ie18551ba5a6bd155f9f4ae9c29c25b379affe019
Closes-Bug: #1623983
2016-09-15 17:16:38 +02:00
233 changed files with 9937 additions and 4970 deletions

View File

@@ -1,6 +0,0 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview
global-exclude *.pyc

View File

@@ -10,10 +10,8 @@ Watcher
OpenStack Watcher provides a flexible and scalable resource optimization
service for multi-tenant OpenStack-based clouds.
Watcher provides a complete optimization loop-including everything from a
metrics receiver, complex event processor and profiler, optimization processor
and an action plan applier. This provides a robust framework to realize a wide
range of cloud optimization goals, including the reduction of data center
Watcher provides a robust framework to realize a wide range of cloud
optimization goals, including the reduction of data center
operating costs, increased system performance via intelligent virtual machine
migration, increased energy efficiency-and more!

View File

@@ -126,6 +126,8 @@ function create_watcher_conf {
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_host $RABBIT_HOST
iniset $WATCHER_CONF oslo_messaging_notifications driver "messaging"
iniset $NOVA_CONF oslo_messaging_notifications topics "notifications,watcher_notifications"
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR

View File

@@ -28,7 +28,7 @@ ENABLED_SERVICES+=,q-svc,q-dhcp,q-meta,q-agt,q-l3,neutron
enable_service n-cauth
# Enable the Watcher Dashboard plugin
enable_plugin watcher-dashboard git://git.openstack.org/openstack/watcher-dashboard
# enable_plugin watcher-dashboard git://git.openstack.org/openstack/watcher-dashboard
# Enable the Watcher plugin
enable_plugin watcher git://git.openstack.org/openstack/watcher

View File

@@ -75,7 +75,7 @@ class WatcherTerm(BaseWatcherDirective):
# Inside your .rst file
.. watcher-term:: import.path.to.your.DocumentedObject
This directive will then import the docstring and then interprete it.
This directive will then import the docstring and then interpret it.
"""
# You need to put an import path as an argument for this directive to work
@@ -85,7 +85,12 @@ class WatcherTerm(BaseWatcherDirective):
cls_path = self.arguments[0]
try:
cls = importlib.import_module(cls_path)
try:
cls = importlib.import_module(cls_path)
except ImportError:
module_name, cls_name = cls_path.rsplit('.', 1)
mod = importlib.import_module(module_name)
cls = getattr(mod, cls_name)
except Exception as exc:
raise self.error(exc)
@@ -97,6 +102,72 @@ class WatcherTerm(BaseWatcherDirective):
return node.children
class WatcherFunc(BaseWatcherDirective):
"""Directive to import a value returned by a func into the Watcher doc
**How to use it**
# inside your .py file
class Bar(object):
def foo(object):
return foo_string
# Inside your .rst file
.. watcher-func:: import.path.to.your.Bar.foo node_classname
node_classname is decumented here:
http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html
This directive will then import the value and then interpret it.
"""
# You need to put an import path as an argument for this directive to work
# required_arguments = 1
# optional_arguments = 1
option_spec = {'format': rst.directives.unchanged}
has_content = True
def run(self):
if not self.content:
error = self.state_machine.reporter.error(
'The "%s" directive is empty; content required.' % self.name,
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
func_path = self.content[0]
try:
cls_path, func_name = func_path.rsplit('.', 1)
module_name, cls_name = cls_path.rsplit('.', 1)
mod = importlib.import_module(module_name)
cls = getattr(mod, cls_name)
except Exception as exc:
raise self.error(exc)
cls_obj = cls()
func = getattr(cls_obj, func_name)
textblock = func()
if not isinstance(textblock, str):
textblock = str(textblock)
self.add_textblock(textblock)
try:
node_class = getattr(nodes,
self.options.get('format', 'paragraph'))
except Exception as exc:
raise self.error(exc)
node = node_class()
node.document = self.state.document
self.state.nested_parse(self.result, 0, node)
return [node]
def setup(app):
app.add_directive('watcher-term', WatcherTerm)
app.add_directive('watcher-func', WatcherFunc)
return {'version': version_info.version_string()}

View File

@@ -0,0 +1,133 @@
# 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.
"""
This provides a sphinx extension able to list the implemented versioned
notifications into the developer documentation.
It is used via a single directive in the .rst file
.. versioned_notifications::
"""
from sphinx.util.compat import Directive
from docutils import nodes
from watcher.notifications import base as notification
from watcher.objects import base
class VersionedNotificationDirective(Directive):
SAMPLE_ROOT = 'doc/notification_samples/'
TOGGLE_SCRIPT = """
<script>
jQuery(document).ready(function(){
jQuery('#%s-div').toggle('show');
jQuery('#%s-hideshow').on('click', function(event) {
jQuery('#%s-div').toggle('show');
});
});
</script>
"""
def run(self):
notifications = self._collect_notifications()
return self._build_markup(notifications)
def _collect_notifications(self):
base.WatcherObjectRegistry.register_notification_objects()
notifications = []
ovos = base.WatcherObjectRegistry.obj_classes()
for name, cls in ovos.items():
cls = cls[0]
if (issubclass(cls, notification.NotificationBase) and
cls != notification.NotificationBase):
payload_name = cls.fields['payload'].objname
payload_cls = ovos[payload_name][0]
for sample in cls.samples:
notifications.append((cls.__name__,
payload_cls.__name__,
sample))
return sorted(notifications)
def _build_markup(self, notifications):
content = []
cols = ['Event type', 'Notification class', 'Payload class', 'Sample']
table = nodes.table()
content.append(table)
group = nodes.tgroup(cols=len(cols))
table.append(group)
head = nodes.thead()
group.append(head)
for _ in cols:
group.append(nodes.colspec(colwidth=1))
body = nodes.tbody()
group.append(body)
# fill the table header
row = nodes.row()
body.append(row)
for col_name in cols:
col = nodes.entry()
row.append(col)
text = nodes.strong(text=col_name)
col.append(text)
# fill the table content, one notification per row
for name, payload, sample_file in notifications:
event_type = sample_file[0: -5].replace('-', '.')
row = nodes.row()
body.append(row)
col = nodes.entry()
row.append(col)
text = nodes.literal(text=event_type)
col.append(text)
col = nodes.entry()
row.append(col)
text = nodes.literal(text=name)
col.append(text)
col = nodes.entry()
row.append(col)
text = nodes.literal(text=payload)
col.append(text)
col = nodes.entry()
row.append(col)
with open(self.SAMPLE_ROOT + sample_file, 'r') as f:
sample_content = f.read()
event_type = sample_file[0: -5]
html_str = self.TOGGLE_SCRIPT % ((event_type, ) * 3)
html_str += ("<input type='button' id='%s-hideshow' "
"value='hide/show sample'>" % event_type)
html_str += ("<div id='%s-div'><pre>%s</pre></div>"
% (event_type, sample_content))
raw = nodes.raw('', html_str, format="html")
col.append(raw)
return content
def setup(app):
app.add_directive('versioned_notifications',
VersionedNotificationDirective)

View File

@@ -0,0 +1,69 @@
{
"priority": "INFO",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "PENDING",
"updated_at": null,
"deleted_at": null,
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditCreatePayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.create",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,69 @@
{
"priority": "INFO",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "DELETED",
"updated_at": null,
"deleted_at": null,
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditDeletePayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.delete",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,70 @@
{
"priority": "INFO",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "ONGOING",
"updated_at": null,
"deleted_at": null,
"fault": null,
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditActionPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.planner.end",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,80 @@
{
"priority": "ERROR",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "ONGOING",
"updated_at": null,
"deleted_at": null,
"fault": {
"watcher_object.data": {
"exception": "WatcherException",
"exception_message": "TEST",
"function_name": "test_send_audit_action_with_error",
"module_name": "watcher.tests.notifications.test_audit_notification"
},
"watcher_object.name": "ExceptionPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditActionPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.planner.error",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,70 @@
{
"priority": "INFO",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "ONGOING",
"updated_at": null,
"deleted_at": null,
"fault": null,
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditActionPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.planner.start",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,70 @@
{
"priority": "INFO",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "ONGOING",
"updated_at": null,
"deleted_at": null,
"fault": null,
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditActionPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.strategy.end",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,80 @@
{
"priority": "ERROR",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "ONGOING",
"updated_at": null,
"deleted_at": null,
"fault": {
"watcher_object.data": {
"exception": "WatcherException",
"exception_message": "TEST",
"function_name": "test_send_audit_action_with_error",
"module_name": "watcher.tests.notifications.test_audit_notification"
},
"watcher_object.name": "ExceptionPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditActionPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.strategy.error",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,70 @@
{
"priority": "INFO",
"payload": {
"watcher_object.data": {
"audit_type": "ONESHOT",
"parameters": {
"para2": "hello",
"para1": 3.2
},
"state": "ONGOING",
"updated_at": null,
"deleted_at": null,
"fault": null,
"goal": {
"watcher_object.data": {
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"name": "dummy",
"updated_at": null,
"deleted_at": null,
"efficacy_specification": [],
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy goal"
},
"watcher_object.name": "GoalPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"interval": null,
"scope": [],
"strategy": {
"watcher_object.data": {
"parameters_spec": {
"properties": {
"para2": {
"type": "string",
"default": "hello",
"description": "string parameter example"
},
"para1": {
"description": "number parameter example",
"maximum": 10.2,
"type": "number",
"default": 3.2,
"minimum": 1.0
}
}
},
"name": "dummy",
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"updated_at": null,
"deleted_at": null,
"created_at": "2016-11-04T16:25:35Z",
"display_name": "Dummy strategy"
},
"watcher_object.name": "StrategyPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"created_at": "2016-11-04T16:29:20Z",
"uuid": "4a97b9dd-2023-43dc-b713-815bdd94d4d6"
},
"watcher_object.name": "AuditActionPayload",
"watcher_object.version": "1.0",
"watcher_object.namespace": "watcher"
},
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:31:36.264673 ",
"event_type": "audit.strategy.start",
"message_id": "cbcf9f2c-7c53-4b4d-91ec-db49cca024b6"
}

View File

@@ -0,0 +1,78 @@
{
"publisher_id": "infra-optim:localhost",
"timestamp": "2016-11-04 16:51:38.722986 ",
"payload": {
"watcher_object.name": "AuditUpdatePayload",
"watcher_object.data": {
"strategy": {
"watcher_object.name": "StrategyPayload",
"watcher_object.data": {
"name": "dummy",
"parameters_spec": {
"properties": {
"para2": {
"default": "hello",
"type": "string",
"description": "string parameter example"
},
"para1": {
"maximum": 10.2,
"default": 3.2,
"minimum": 1.0,
"description": "number parameter example",
"type": "number"
}
}
},
"updated_at": null,
"display_name": "Dummy strategy",
"deleted_at": null,
"uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39",
"created_at": "2016-11-04T16:25:35Z"
},
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"scope": [],
"created_at": "2016-11-04T16:51:21Z",
"uuid": "f1e0d912-afd9-4bf2-91ef-c99cd08cc1ef",
"goal": {
"watcher_object.name": "GoalPayload",
"watcher_object.data": {
"efficacy_specification": [],
"updated_at": null,
"name": "dummy",
"display_name": "Dummy goal",
"deleted_at": null,
"uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a",
"created_at": "2016-11-04T16:25:35Z"
},
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"parameters": {
"para2": "hello",
"para1": 3.2
},
"deleted_at": null,
"state_update": {
"watcher_object.name": "AuditStateUpdatePayload",
"watcher_object.data": {
"state": "ONGOING",
"old_state": "PENDING"
},
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"interval": null,
"updated_at": null,
"state": "ONGOING",
"audit_type": "ONESHOT"
},
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"priority": "INFO",
"event_type": "audit.update",
"message_id": "697fdf55-7252-4b6c-a2c2-5b9e85f6342c"
}

View File

@@ -0,0 +1,16 @@
{
"event_type": "infra-optim.exception",
"payload": {
"watcher_object.data": {
"exception": "NoAvailableStrategyForGoal",
"exception_message": "No strategy could be found to achieve the server_consolidation goal.",
"function_name": "_aggregate_create_in_db",
"module_name": "watcher.objects.aggregate"
},
"watcher_object.name": "ExceptionPayload",
"watcher_object.namespace": "watcher",
"watcher_object.version": "1.0"
},
"priority": "ERROR",
"publisher_id": "watcher-api:fake-mini"
}

View File

@@ -171,12 +171,12 @@ This component is responsible for computing a set of potential optimization
:ref:`Actions <action_definition>` in order to fulfill
the :ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
It first reads the parameters of the :ref:`Audit <audit_definition>` from the
associated :ref:`Audit Template <audit_template_definition>` and knows the
:ref:`Goal <goal_definition>` to achieve.
It first reads the parameters of the :ref:`Audit <audit_definition>` to know
the :ref:`Goal <goal_definition>` to achieve.
It then selects the most appropriate :ref:`Strategy <strategy_definition>`
from the list of available strategies achieving this goal.
Unless specified, it then selects the most appropriate :ref:`strategy
<strategy_definition>` from the list of available strategies achieving this
goal.
The :ref:`Strategy <strategy_definition>` is then dynamically loaded (via
`stevedore <http://docs.openstack.org/developer/stevedore/>`_). The
@@ -290,7 +290,7 @@ the Audit parameters from the
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the
appropriate :ref:`strategy <strategy_definition>` (using entry points)
given both the :ref:`goal <goal_definition>` and the strategy associated to the
parent :ref:`audit template <audit_template_definition>` of the :ref:`Audit
parent :ref:`audit template <audit_template_definition>` of the :ref:`audit
<audit_definition>`. If no strategy is associated to the audit template, the
strategy is dynamically selected by the Decision Engine.
@@ -298,7 +298,7 @@ The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This
data model is needed by the :ref:`Strategy <strategy_definition>` to know the
current state and topology of the audited
:ref:`Openstack cluster <cluster_definition>`.
:ref:`OpenStack cluster <cluster_definition>`.
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` calls
the **execute()** method of the instantiated
@@ -450,7 +450,7 @@ state may be one of the following:
stored in the :ref:`Watcher database <watcher_database_definition>` but is
not returned any more through the Watcher APIs.
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
**PENDING** or **ONGOING** state and was cancelled by the
**RECOMMENDED**, **PENDING** or **ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`

View File

@@ -11,7 +11,21 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
from watcher import version as watcher_version
from watcher import objects
objects.register_all()
# 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('../../'))
sys.path.insert(0, os.path.abspath('../'))
sys.path.insert(0, os.path.abspath('./'))
# -- General configuration ----------------------------------------------------
@@ -26,7 +40,8 @@ extensions = [
'sphinxcontrib.pecanwsme.rest',
'stevedore.sphinxext',
'wsmeext.sphinxext',
'watcher.doc',
'ext.term',
'ext.versioned_notifications',
]
wsme_protocols = ['restjson']
@@ -67,6 +82,8 @@ exclude_patterns = [
# them when scanning for input files.
'man/footer.rst',
'man/general-options.rst',
'strategies/strategy-template.rst',
'image_src/plantuml/README.rst',
]
# If true, '()' will be appended to :func: etc. cross-reference text.

View File

@@ -93,11 +93,11 @@ following command:
.. code:: bash
$ watcher strategy list --goal-uuid <your_goal_uuid>
$ watcher strategy list --goal <your_goal_uuid_or_name>
or::
$ openstack optimize strategy list --goal-uuid <your_goal_uuid>
$ openstack optimize strategy list --goal <your_goal_uuid_or_name>
You can use the following command to check strategy details including which
parameters of which format it supports:

View File

@@ -0,0 +1,13 @@
..
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_notifications:
========================
Notifications in Watcher
========================
.. versioned_notifications::

View File

@@ -22,7 +22,7 @@ cluster data model collectors within Watcher.
Creating a new plugin
=====================
In order to create a new model, you have to:
In order to create a new cluster data model collector, you have to:
- Extend the :py:class:`~.base.BaseClusterDataModelCollector` class.
- Implement its :py:meth:`~.BaseClusterDataModelCollector.execute` abstract
@@ -65,6 +65,49 @@ This implementation is the most basic one. So in order to get a better
understanding on how to implement a more advanced cluster data model collector,
have a look at the :py:class:`~.NovaClusterDataModelCollector` class.
Define a custom model
=====================
As you may have noticed in the above example, we are reusing an existing model
provided by Watcher. However, this model can be easily customized by
implementing a new class that would implement the :py:class:`~.Model` abstract
base class. Here below is simple example on how to proceed in implementing a
custom Model:
.. code-block:: python
# Filepath = <PROJECT_DIR>/thirdparty/dummy.py
# Import path = thirdparty.dummy
from watcher.decision_engine.model import base as modelbase
from watcher.decision_engine.model.collector import base
class MyModel(modelbase.Model):
def to_string(self):
return 'MyModel'
class DummyClusterDataModelCollector(base.BaseClusterDataModelCollector):
def execute(self):
model = MyModel()
# Do something here...
return model
@property
def notification_endpoints(self):
return []
Here below is the abstract ``Model`` class that every single cluster data model
should implement:
.. autoclass:: watcher.decision_engine.model.base.Model
:members:
:special-members: __init__
:noindex:
Define configuration parameters
===============================

View File

@@ -43,7 +43,7 @@ In order to create a new strategy, you have to:
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
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

View File

@@ -96,8 +96,8 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
.. _cluster_data_model_definition:
Cluster Data Model
==================
Cluster Data Model (CDM)
========================
.. watcher-term:: watcher.decision_engine.model.collector.base
@@ -164,7 +164,8 @@ Goal
Host Aggregate
==============
Please, read `the official OpenStack definition of a Host Aggregate <http://docs.openstack.org/developer/nova/aggregates.html>`_.
Please, read `the official OpenStack definition of a Host Aggregate
<http://docs.openstack.org/developer/nova/aggregates.html>`_.
.. _instance_definition:

View File

@@ -4,7 +4,7 @@ actor Administrator
== Create some Audit settings ==
Administrator -> Watcher : create new Audit Template (i.e. Audit settings : goal, scope, deadline,...)
Administrator -> Watcher : create new Audit Template (i.e. Audit settings : goal, scope, ...)
Watcher -> Watcher : save Audit Template in database
Administrator <-- Watcher : Audit Template UUID

View File

@@ -5,11 +5,12 @@
hide methods
hide stereotypes
table(goal) {
table(goals) {
primary_key(id: Integer)
uuid : String[36]
name : String[63]
display_name : String[63]
efficacy_specification : JSONEncodedList, nullable
created_at : DateTime
updated_at : DateTime
@@ -18,12 +19,13 @@ table(goal) {
}
table(strategy) {
table(strategies) {
primary_key(id: Integer)
foreign_key(goal_id : Integer)
uuid : String[36]
name : String[63]
display_name : String[63]
parameters_spec : JSONEncodedDict, nullable
created_at : DateTime
updated_at : DateTime
@@ -32,16 +34,14 @@ table(strategy) {
}
table(audit_template) {
table(audit_templates) {
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
scope : JSONEncodedList
created_at : DateTime
updated_at : DateTime
@@ -50,14 +50,16 @@ table(audit_template) {
}
table(audit) {
table(audits) {
primary_key(id: Integer)
foreign_key("audit_template_id : Integer")
foreign_key("goal_id : Integer")
foreign_key("strategy_id : Integer, nullable")
uuid : String[36]
audit_type : String[20]
state : String[20], nullable
deadline :DateTime, nullable
interval : Integer, nullable
parameters : JSONEncodedDict, nullable
scope : JSONEncodedList, nullable
created_at : DateTime
updated_at : DateTime
@@ -66,9 +68,10 @@ table(audit) {
}
table(action_plan) {
table(action_plans) {
primary_key(id: Integer)
foreign_key("audit_id : Integer, nullable")
foreign_key("strategy_id : Integer")
uuid : String[36]
first_action_id : Integer
state : String[20], nullable
@@ -81,7 +84,7 @@ table(action_plan) {
}
table(action) {
table(actions) {
primary_key(id: Integer)
foreign_key("action_plan_id : Integer")
uuid : String[36]
@@ -97,7 +100,7 @@ table(action) {
}
table(efficacy_indicator) {
table(efficacy_indicators) {
primary_key(id: Integer)
foreign_key("action_plan_id : Integer")
uuid : String[36]
@@ -112,12 +115,39 @@ table(efficacy_indicator) {
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
table(scoring_engines) {
primary_key(id: Integer)
uuid : String[36]
name : String[63]
description : String[255], nullable
metainfo : Text, nullable
created_at : DateTime
updated_at : DateTime
deleted_at : DateTime
deleted : Integer
}
table(service) {
primary_key(id: Integer)
name: String[255]
host: String[255]
last_seen_up: DateTime
created_at : DateTime
updated_at : DateTime
deleted_at : DateTime
deleted : Integer
}
"goals" <.. "strategies" : Foreign Key
"goals" <.. "audit_templates" : Foreign Key
"strategies" <.. "audit_templates" : Foreign Key
"goals" <.. "audits" : Foreign Key
"strategies" <.. "audits" : Foreign Key
"action_plans" <.. "actions" : Foreign Key
"action_plans" <.. "efficacy_indicators" : Foreign Key
"strategies" <.. "action_plans" : Foreign Key
"audits" <.. "action_plans" : Foreign Key
@enduml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -56,6 +56,7 @@ Getting Started
dev/devstack
deploy/configuration
deploy/conf-files
dev/notifications
dev/testing
dev/rally_link
@@ -96,6 +97,7 @@ Introduction
deploy/user-guide
deploy/policy
deploy/gmr
strategies/strategies
Watcher Manual Pages
====================

View File

@@ -0,0 +1,96 @@
==================================
Basic Offline Server Consolidation
==================================
Synopsis
--------
**display name**: ``basic``
**goal**: ``server_consolidation``
.. watcher-term:: watcher.decision_engine.strategy.strategies.basic_consolidation
Requirements
------------
Metrics
*******
The *basic* strategy requires the following metrics:
============================ ============ ======= =======
metric service name plugins comment
============================ ============ ======= =======
``compute.node.cpu.percent`` ceilometer_ none
``cpu_util`` ceilometer_ none
============================ ============ ======= =======
.. _ceilometer: http://docs.openstack.org/admin-guide/telemetry-measurements.html#openstack-compute
Cluster data model
******************
Default Watcher's Compute cluster data model:
.. watcher-term:: watcher.decision_engine.model.collector.nova.NovaClusterDataModelCollector
Actions
*******
Default Watcher's actions:
.. list-table::
:widths: 30 30
:header-rows: 1
* - action
- description
* - ``migration``
- .. watcher-term:: watcher.applier.actions.migration.Migrate
* - ``change_nova_service_state``
- .. watcher-term:: watcher.applier.actions.change_nova_service_state.ChangeNovaServiceState
Planner
*******
Default Watcher's planner:
.. watcher-term:: watcher.decision_engine.planner.default.DefaultPlanner
Configuration
-------------
Strategy parameter is:
====================== ====== ============= ===================================
parameter type default Value description
====================== ====== ============= ===================================
``migration_attempts`` Number 0 Maximum number of combinations to
be tried by the strategy while
searching for potential candidates.
To remove the limit, set it to 0
====================== ====== ============= ===================================
Efficacy Indicator
------------------
.. watcher-func::
:format: literal_block
watcher.decision_engine.goal.efficacy.specs.ServerConsolidation.get_global_efficacy_indicator
How to use it ?
---------------
.. code-block:: shell
$ openstack optimize audittemplate create \
at1 server_consolidation --strategy basic
$ openstack optimize audit create -a at1 -p migration_attempts=4
External Links
--------------
None.

View File

@@ -0,0 +1,8 @@
Strategies
==========
.. toctree::
:glob:
:maxdepth: 1
./*

View File

@@ -0,0 +1,115 @@
=============
Strategy name
=============
Synopsis
--------
**display name**:
**goal**:
Add here a complete description of your strategy
Requirements
------------
Metrics
*******
Write here the list of metrics required by your strategy algorithm (in the form
of a table). If these metrics requires specific Telemetry plugin or other
additional software, please explain here how to deploy them (and add link to
dedicated installation guide).
Example:
======================= ============ ======= =======
metric service name plugins comment
======================= ============ ======= =======
compute.node.* ceilometer_ none one point every 60s
vm.cpu.utilization_perc monasca_ none
power ceilometer_ kwapi_ one point every 60s
======================= ============ ======= =======
.. _ceilometer: http://docs.openstack.org/admin-guide/telemetry-measurements.html#openstack-compute
.. _monasca: https://github.com/openstack/monasca-agent/blob/master/docs/Libvirt.md
.. _kwapi: https://kwapi.readthedocs.io/en/latest/index.html
Cluster data model
******************
Default Watcher's cluster data model.
or
If your strategy implementation requires a new cluster data model, please
describe it in this section, with a link to model plugin's installation guide.
Actions
*******
Default Watcher's actions.
or
If your strategy implementation requires new actions, add the list of Action
plugins here (in the form of a table) with a link to the plugin's installation
procedure.
======== =================
action description
======== =================
action1_ This action1 ...
action2_ This action2 ...
======== =================
.. _action1 : https://github.com/myrepo/watcher/plugins/action1
.. _action2 : https://github.com/myrepo/watcher/plugins/action2
Planner
*******
Default Watcher's planner.
or
If your strategy requires also a new planner to schedule built actions in time,
please describe it in this section, with a link to planner plugin's
installation guide.
Configuration
-------------
If your strategy use configurable parameters, explain here how to tune them.
Efficacy Indicator
------------------
Add here the Efficacy indicator computed by your strategy.
Algorithm
---------
Add here either the description of your algorithm or
link to the existing description.
How to use it ?
---------------
.. code-block:: shell
$ Write the command line to create an audit with your strategy.
External Links
--------------
If you have written papers, blog articles .... about your strategy into Watcher,
or if your strategy is based from external publication(s), please add HTTP
links and references in this section.
- `link1 <http://www.link1.papers.com>`_
- `link2 <http://www.link2.papers.com>`_

View File

@@ -0,0 +1,100 @@
==================================
VM Workload Consolidation Strategy
==================================
Synopsis
--------
**display name**: ``vm_workload_consolidation``
**goal**: ``vm_consolidation``
.. watcher-term:: watcher.decision_engine.strategy.strategies.vm_workload_consolidation
Requirements
------------
Metrics
*******
The *vm_workload_consolidation* strategy requires the following metrics:
============================ ============ ======= =======
metric service name plugins comment
============================ ============ ======= =======
``memory`` ceilometer_ none
``disk.root.size`` ceilometer_ none
============================ ============ ======= =======
The following metrics are not required but increase the accuracy of
the strategy if available:
============================ ============ ======= =======
metric service name plugins comment
============================ ============ ======= =======
``memory.usage`` ceilometer_ none
``cpu_util`` ceilometer_ none
============================ ============ ======= =======
.. _ceilometer: http://docs.openstack.org/admin-guide/telemetry-measurements.html#openstack-compute
Cluster data model
******************
Default Watcher's Compute cluster data model:
.. watcher-term:: watcher.decision_engine.model.collector.nova.NovaClusterDataModelCollector
Actions
*******
Default Watcher's actions:
.. list-table::
:widths: 30 30
:header-rows: 1
* - action
- description
* - ``migration``
- .. watcher-term:: watcher.applier.actions.migration.Migrate
* - ``change_nova_service_state``
- .. watcher-term:: watcher.applier.actions.change_nova_service_state.ChangeNovaServiceState
Planner
*******
Default Watcher's planner:
.. watcher-term:: watcher.decision_engine.planner.default.DefaultPlanner
Efficacy Indicator
------------------
.. watcher-func::
:format: literal_block
watcher.decision_engine.goal.efficacy.specs.ServerConsolidation.get_global_efficacy_indicator
Algorithm
---------
For more information on the VM Workload consolidation strategy please refer to: https://specs.openstack.org/openstack/watcher-specs/specs/mitaka/implemented/zhaw-load-consolidation.html
How to use it ?
---------------
.. code-block:: shell
$ openstack optimize audittemplate create \
at1 vm_consolidation --strategy vm_workload_consolidation
$ openstack optimize audit create -a at1
External Links
--------------
*Spec URL*
https://specs.openstack.org/openstack/watcher-specs/specs/mitaka/implemented/zhaw-load-consolidation.html

View File

@@ -0,0 +1,131 @@
=============================================
Watcher Overload standard deviation algorithm
=============================================
Synopsis
--------
**display name**: ``workload_stabilization``
**goal**: ``workload_balancing``
.. watcher-term:: watcher.decision_engine.strategy.strategies.workload_stabilization
Requirements
------------
Metrics
*******
The *workload_stabilization* strategy requires the following metrics:
============================ ============ ======= =======
metric service name plugins comment
============================ ============ ======= =======
``compute.node.cpu.percent`` ceilometer_ none
``hardware.memory.used`` ceilometer_ SNMP_
``cpu_util`` ceilometer_ none
``memory.resident`` ceilometer_ none
============================ ============ ======= =======
.. _ceilometer: http://docs.openstack.org/admin-guide/telemetry-measurements.html#openstack-compute
.. _SNMP: http://docs.openstack.org/admin-guide/telemetry-measurements.html
Cluster data model
******************
Default Watcher's Compute cluster data model:
.. watcher-term:: watcher.decision_engine.model.collector.nova.NovaClusterDataModelCollector
Actions
*******
Default Watcher's actions:
.. list-table::
:widths: 30 30
:header-rows: 1
* - action
- description
* - ``migration``
- .. watcher-term:: watcher.applier.actions.migration.Migrate
Planner
*******
Default Watcher's planner:
.. watcher-term:: watcher.decision_engine.planner.default.DefaultPlanner
Configuration
-------------
Strategy parameters are:
==================== ====== ===================== =============================
parameter type default Value description
==================== ====== ===================== =============================
``metrics`` array |metrics| Metrics used as rates of
cluster loads.
``thresholds`` object |thresholds| Dict where key is a metric
and value is a trigger value.
``weights`` object |weights| These weights used to
calculate common standard
deviation. Name of weight
contains meter name and
_weight suffix.
``instance_metrics`` object |instance_metrics| Mapping to get hardware
statistics using instance
metrics.
``host_choice`` string retry Method of host's choice.
There are cycle, retry and
fullsearch methods. Cycle
will iterate hosts in cycle.
Retry will get some hosts
random (count defined in
retry_count option).
Fullsearch will return each
host from list.
``retry_count`` number 1 Count of random returned
hosts.
==================== ====== ===================== =============================
.. |metrics| replace:: ["cpu_util", "memory.resident"]
.. |thresholds| replace:: {"cpu_util": 0.2, "memory.resident": 0.2}
.. |weights| replace:: {"cpu_util_weight": 1.0, "memory.resident_weight": 1.0}
.. |instance_metrics| replace:: {"cpu_util": "hardware.cpu.util", "memory.resident": "hardware.memory.used"}
Efficacy Indicator
------------------
.. watcher-func::
:format: literal_block
watcher.decision_engine.goal.efficacy.specs.ServerConsolidation.get_global_efficacy_indicator
Algorithm
---------
You can find description of overload algorithm and role of standard deviation
here: https://specs.openstack.org/openstack/watcher-specs/specs/newton/implemented/sd-strategy.html
How to use it ?
---------------
.. code-block:: shell
$ openstack optimize audittemplate create \
at1 workload_balancing --strategy workload_stabilization
$ openstack optimize audit create -a at1 \
-p thresholds='{"memory.resident": 0.05}' \
-p metrics='["memory.resident"]'
External Links
--------------
- `Watcher Overload standard deviation algorithm spec <https://specs.openstack.org/openstack/watcher-specs/specs/newton/implemented/sd-strategy.html>`_

View File

@@ -37,5 +37,9 @@
"strategy:detail": "rule:default",
"strategy:get": "rule:default",
"strategy:get_all": "rule:default"
"strategy:get_all": "rule:default",
"service:detail": "rule:default",
"service:get": "rule:default",
"service:get_all": "rule:default"
}

View File

@@ -242,3 +242,6 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# -- Options for Internationalization output ------------------------------
locale_dirs = ['locale/']

View File

@@ -6,5 +6,6 @@ Contents:
.. toctree::
:maxdepth: 1
unreleased.rst
unreleased
newton

View File

@@ -0,0 +1,6 @@
===================================
Newton Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/newton

View File

@@ -5,37 +5,38 @@
apscheduler # MIT License
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
jsonpatch>=1.1 # BSD
keystoneauth1>=2.10.0 # Apache-2.0
keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
keystoneauth1>=2.14.0 # Apache-2.0
keystonemiddleware!=4.5.0,>=4.2.0 # Apache-2.0
lxml>=2.3 # BSD
oslo.concurrency>=3.8.0 # Apache-2.0
oslo.cache>=1.5.0 # Apache-2.0
oslo.config>=3.14.0 # Apache-2.0
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
oslo.context>=2.9.0 # Apache-2.0
oslo.db>=4.10.0 # Apache-2.0
oslo.db!=4.13.1,!=4.13.2,>=4.11.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.messaging>=5.2.0 # Apache-2.0
oslo.policy>=1.9.0 # Apache-2.0
oslo.policy>=1.15.0 # Apache-2.0
oslo.reports>=0.6.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.service>=1.10.0 # Apache-2.0
oslo.utils>=3.16.0 # Apache-2.0
oslo.utils>=3.18.0 # Apache-2.0
oslo.versionedobjects>=1.13.0 # Apache-2.0
PasteDeploy>=1.5.0 # MIT
pbr>=1.6 # Apache-2.0
pecan!=1.0.2,!=1.0.3,!=1.0.4,>=1.0.0 # BSD
PrettyTable<0.8,>=0.7 # BSD
pbr>=1.8 # Apache-2.0
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
PrettyTable<0.8,>=0.7.1 # BSD
voluptuous>=0.8.9 # BSD License
python-ceilometerclient>=2.5.0 # Apache-2.0
python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0
python-glanceclient!=2.4.0,>=2.3.0 # Apache-2.0
python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0
python-glanceclient>=2.5.0 # Apache-2.0
python-keystoneclient>=3.6.0 # Apache-2.0
python-neutronclient>=5.1.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>=3.3.0 # Apache-2.0
six>=1.9.0 # MIT
SQLAlchemy<1.1.0,>=1.0.10 # MIT
stevedore>=1.16.0 # Apache-2.0
stevedore>=1.17.1 # Apache-2.0
taskflow>=1.26.0 # Apache-2.0
WebOb>=1.2.3 # MIT
WebOb>=1.6.0 # MIT
WSME>=0.8 # MIT

View File

@@ -1,6 +1,6 @@
[metadata]
name = python-watcher
summary = Watcher takes advantage of CEP and ML algorithms/metaheuristics to improve physical resources usage through better VM placement. Watcher can improve your cloud optimization by reducing energy footprint and increasing profits.
summary = OpenStack Watcher provides a flexible and scalable resource optimization service for multi-tenant OpenStack-based clouds.
description-file =
README.rst
author = OpenStack
@@ -32,7 +32,7 @@ setup-hooks =
[entry_points]
oslo.config.opts =
watcher = watcher.opts:list_opts
watcher = watcher.conf.opts:list_opts
console_scripts =
watcher-api = watcher.cmd.api:main

View File

@@ -2,25 +2,25 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
coverage>=3.6 # Apache-2.0
coverage>=4.0 # Apache-2.0
doc8 # Apache-2.0
freezegun # Apache-2.0
freezegun>=0.3.6 # Apache-2.0
hacking<0.11,>=0.10.2
mock>=2.0 # BSD
oslotest>=1.10.0 # Apache-2.0
os-testr>=0.7.0 # Apache-2.0
os-testr>=0.8.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
# Doc requirements
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
oslosphinx>=4.7.0 # Apache-2.0
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
sphinxcontrib-pecanwsme>=0.8 # Apache-2.0
# releasenotes
reno>=1.8.0 # Apache2
reno>=1.8.0 # Apache-2.0
# bandit
bandit>=1.1.0 # Apache-2.0

16
tox.ini
View File

@@ -1,26 +1,26 @@
[tox]
minversion = 1.6
minversion = 1.8
envlist = py35,py34,py27,pep8
skipsdist = True
[testenv]
usedevelop = True
whitelist_externals = find
install_command = pip install -U {opts} {packages}
install_command =
constraints: pip install -U --force-reinstall -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
deps = -r{toxinidir}/test-requirements.txt
commands =
find . -type f -name "*.pyc" -delete
find . -type d -name "__pycache__" -delete
find . -type f -name "*.py[c|o]" -delete
ostestr --concurrency=6 {posargs}
[testenv:pep8]
commands =
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
flake8
bandit -r watcher -x tests -n5 -ll
bandit -r watcher -x tests -n5 -ll -s B320
[testenv:venv]
setenv = PYTHONHASHSEED=0
@@ -66,4 +66,4 @@ commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasen
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r watcher -x tests -n5 -ll
commands = bandit -r watcher -x tests -n5 -ll -s B320

View File

@@ -35,7 +35,7 @@ CONF.register_opts(AUTH_OPTS)
def install(app, conf, public_routes):
"""Install ACL check on application.
:param app: A WSGI applicatin.
:param app: A WSGI application.
:param conf: Settings. Dict'ified and passed to keystonemiddleware
:param public_routes: The list of the routes which will be allowed to
access without authentication.

View File

@@ -61,7 +61,7 @@ class Root(base.APIBase):
root = Root()
root.name = "OpenStack Watcher API"
root.description = ("Watcher is an OpenStack project which aims to "
"to improve physical resources usage through "
"improve physical resources usage through "
"better VM placement.")
root.versions = [Version.convert('v1')]
root.default_version = Version.convert('v1')

View File

@@ -35,6 +35,7 @@ from watcher.api.controllers.v1 import audit
from watcher.api.controllers.v1 import audit_template
from watcher.api.controllers.v1 import goal
from watcher.api.controllers.v1 import scoring_engine
from watcher.api.controllers.v1 import service
from watcher.api.controllers.v1 import strategy
@@ -105,6 +106,9 @@ class V1(APIBase):
scoring_engines = [link.Link]
"""Links to the Scoring Engines resource"""
services = [link.Link]
"""Links to the services resource"""
links = [link.Link]
"""Links that point to a specific URL for this version and documentation"""
@@ -159,6 +163,14 @@ class V1(APIBase):
'scoring_engines', '',
bookmark=True)
]
v1.services = [link.Link.make_link(
'self', pecan.request.host_url, 'services', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'services', '',
bookmark=True)
]
return v1
@@ -171,6 +183,7 @@ class Controller(rest.RestController):
action_plans = action_plan.ActionPlansController()
goals = goal.GoalsController()
scoring_engines = scoring_engine.ScoringEngineController()
services = service.ServicesController()
strategies = strategy.StrategiesController()
@wsme_pecan.wsexpose(V1)

View File

@@ -379,7 +379,7 @@ class ActionsController(rest.RestController):
action_dict = action.as_dict()
context = pecan.request.context
new_action = objects.Action(context, **action_dict)
new_action.create(context)
new_action.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url('actions', new_action.uuid)

View File

@@ -60,8 +60,6 @@ class AuditPostType(wtypes.Base):
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)
@@ -69,8 +67,7 @@ class AuditPostType(wtypes.Base):
default={})
interval = wsme.wsattr(int, mandatory=False)
host_aggregate = wsme.wsattr(wtypes.IntegerType(minimum=1),
mandatory=False)
scope = wtypes.wsattr(types.jsontype, readonly=True)
def as_audit(self, context):
audit_type_values = [val.value for val in objects.audit.AuditType]
@@ -82,7 +79,7 @@ class AuditPostType(wtypes.Base):
raise exception.AuditIntervalNotAllowed(audit_type=self.audit_type)
if (self.audit_type == objects.audit.AuditType.CONTINUOUS.value and
self.interval in (wtypes.Unset, None)):
self.interval in (wtypes.Unset, None)):
raise exception.AuditIntervalNotSpecified(
audit_type=self.audit_type)
@@ -100,7 +97,7 @@ class AuditPostType(wtypes.Base):
at2a = {
'goal': 'goal_id',
'strategy': 'strategy_id',
'host_aggregate': 'host_aggregate'
'scope': 'scope',
}
to_string_fields = set(['goal', 'strategy'])
for k in at2a:
@@ -114,12 +111,11 @@ class AuditPostType(wtypes.Base):
pass
return Audit(
audit_type=self.audit_type,
deadline=self.deadline,
parameters=self.parameters,
goal_id=self.goal,
host_aggregate=self.host_aggregate,
strategy_id=self.strategy,
interval=self.interval)
interval=self.interval,
scope=self.scope,)
class AuditPatchType(types.JsonPatchType):
@@ -230,9 +226,6 @@ class Audit(base.APIBase):
audit_type = wtypes.text
"""Type of this audit"""
deadline = datetime.datetime
"""deadline of the audit"""
state = wtypes.text
"""This audit state"""
@@ -261,8 +254,8 @@ class Audit(base.APIBase):
interval = wsme.wsattr(int, mandatory=False)
"""Launch audit periodically (in seconds)"""
host_aggregate = wtypes.IntegerType(minimum=1)
"""ID of the Nova host aggregate targeted by the audit template"""
scope = wsme.wsattr(types.jsontype, mandatory=False)
"""Audit Scope"""
def __init__(self, **kwargs):
self.fields = []
@@ -292,10 +285,10 @@ class Audit(base.APIBase):
@staticmethod
def _convert_with_links(audit, url, expand=True):
if not expand:
audit.unset_fields_except(['uuid', 'audit_type', 'deadline',
'state', 'goal_uuid', 'interval',
'strategy_uuid', 'host_aggregate',
'goal_name', 'strategy_name'])
audit.unset_fields_except(['uuid', 'audit_type', 'state',
'goal_uuid', 'interval', 'scope',
'strategy_uuid', 'goal_name',
'strategy_name'])
audit.links = [link.Link.make_link('self', url,
'audits', audit.uuid),
@@ -316,15 +309,15 @@ class Audit(base.APIBase):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
audit_type='ONESHOT',
state='PENDING',
deadline=None,
created_at=datetime.datetime.utcnow(),
deleted_at=None,
updated_at=datetime.datetime.utcnow(),
interval=7200)
interval=7200,
scope=[])
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'
sample.host_aggregate = 1
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -381,7 +374,7 @@ class AuditsController(rest.RestController):
def _get_audits_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None, goal=None,
strategy=None, host_aggregate=None):
strategy=None):
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
@@ -424,19 +417,16 @@ class AuditsController(rest.RestController):
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
wtypes.text, wtypes.text, wtypes.text, int)
def get_all(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', goal=None,
strategy=None, host_aggregate=None):
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc',
goal=None, strategy=None):
"""Retrieve a list of audits.
: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.
id.
:param goal: goal UUID or name to filter by
:param strategy: strategy UUID or name to filter by
:param host_aggregate: Optional host_aggregate
"""
context = pecan.request.context
@@ -445,8 +435,7 @@ class AuditsController(rest.RestController):
return self._get_audits_collection(marker, limit, sort_key,
sort_dir, goal=goal,
strategy=strategy,
host_aggregate=host_aggregate)
strategy=strategy)
@wsme_pecan.wsexpose(AuditCollection, wtypes.text, types.uuid, int,
wtypes.text, wtypes.text)
@@ -530,7 +519,7 @@ class AuditsController(rest.RestController):
audit_dict = audit.as_dict()
new_audit = objects.Audit(context, **audit_dict)
new_audit.create(context)
new_audit.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url('audits', new_audit.uuid)
@@ -555,14 +544,11 @@ class AuditsController(rest.RestController):
raise exception.OperationNotPermitted
context = pecan.request.context
audit_to_update = api_utils.get_resource('Audit',
audit_uuid)
audit_to_update = api_utils.get_resource(
'Audit', audit_uuid, eager=True)
policy.enforce(context, 'audit:update', audit_to_update,
action='audit:update')
audit_to_update = objects.Audit.get_by_uuid(pecan.request.context,
audit_uuid)
try:
audit_dict = audit_to_update.as_dict()
audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch))
@@ -591,7 +577,8 @@ class AuditsController(rest.RestController):
:param audit_uuid: UUID of a audit.
"""
context = pecan.request.context
audit_to_delete = api_utils.get_resource('Audit', audit_uuid)
audit_to_delete = api_utils.get_resource(
'Audit', audit_uuid, eager=True)
policy.enforce(context, 'audit:update', audit_to_delete,
action='audit:update')

View File

@@ -41,11 +41,6 @@ settings related to the level of automation for the
A flag will indicate whether the :ref:`Action Plan <action_plan_definition>`
will be launched automatically or will need a manual confirmation from the
:ref:`Administrator <administrator_definition>`.
Last but not least, an :ref:`Audit Template <audit_template_definition>` may
contain a list of extra parameters related to the
:ref:`Strategy <strategy_definition>` configuration. These parameters can be
provided as a list of key-value pairs.
"""
import datetime
@@ -66,6 +61,7 @@ from watcher.common import context as context_utils
from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils
from watcher.decision_engine.scope import default
from watcher import objects
@@ -78,37 +74,24 @@ class AuditTemplatePostType(wtypes.Base):
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"""
scope = wtypes.wsattr(types.jsontype, mandatory=False, default=[])
"""Audit Scope"""
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,
scope=self.scope,
)
@staticmethod
@@ -123,6 +106,9 @@ class AuditTemplatePostType(wtypes.Base):
else:
raise exception.InvalidGoal(goal=audit_template.goal)
common_utils.Draft4Validator(
default.DefaultScope.DEFAULT_SCHEMA).validate(audit_template.scope)
if audit_template.strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePostType._ctx)
@@ -305,18 +291,9 @@ class AuditTemplate(base.APIBase):
name = wtypes.text
"""Name of this audit template"""
description = wtypes.text
description = wtypes.wsattr(wtypes.text, mandatory=False)
"""Short description of this audit template"""
deadline = datetime.datetime
"""deadline of the audit template"""
host_aggregate = wtypes.IntegerType(minimum=1)
"""ID of the Nova host aggregate targeted by the audit template"""
extra = {wtypes.text: types.jsontype}
"""The metadata of the audit template"""
goal_uuid = wsme.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
"""Goal UUID the audit template refers to"""
@@ -333,15 +310,15 @@ class AuditTemplate(base.APIBase):
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
"""The name of the strategy this audit template refers to"""
version = wtypes.text
"""Internal version of the audit template"""
audits = wsme.wsattr([link.Link], readonly=True)
"""Links to the collection of audits contained in this audit template"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated audit template links"""
scope = wsme.wsattr(types.jsontype, mandatory=False)
"""Audit Scope"""
def __init__(self, **kwargs):
super(AuditTemplate, self).__init__()
self.fields = []
@@ -374,8 +351,8 @@ class AuditTemplate(base.APIBase):
def _convert_with_links(audit_template, url, expand=True):
if not expand:
audit_template.unset_fields_except(
['uuid', 'name', 'host_aggregate', 'goal_uuid', 'goal_name',
'strategy_uuid', 'strategy_name'])
['uuid', 'name', 'goal_uuid', 'goal_name',
'scope', 'strategy_uuid', 'strategy_name'])
# The numeric ID should not be exposed to
# the user, it's internal only.
@@ -402,13 +379,12 @@ class AuditTemplate(base.APIBase):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit Template',
description='Description of my audit template',
host_aggregate=5,
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
extra={'automatic': True},
created_at=datetime.datetime.utcnow(),
deleted_at=None,
updated_at=datetime.datetime.utcnow())
updated_at=datetime.datetime.utcnow(),
scope=[],)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -592,11 +568,11 @@ class AuditTemplatesController(rest.RestController):
audit_template_dict = audit_template.as_dict()
new_audit_template = objects.AuditTemplate(context,
**audit_template_dict)
new_audit_template.create(context)
new_audit_template.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url('audit_templates',
new_audit_template.uuid)
pecan.response.location = link.build_url(
'audit_templates', new_audit_template.uuid)
return AuditTemplate.convert_with_links(new_audit_template)
@wsme.validate(types.uuid, [AuditTemplatePatchType])

View File

@@ -19,7 +19,7 @@ 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
:ref:`global efficacy <efficacy_definition>` of the resulting :ref:`action plan
<action_plan_definition>`.
In Watcher, these efficacy indicators are specified alongside the goal they

View File

@@ -0,0 +1,263 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 Servionica
#
# 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.
"""
Service mechanism provides ability to monitor Watcher services state.
"""
import datetime
import six
from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from watcher._i18n import _LW
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 utils as api_utils
from watcher.common import exception
from watcher.common import policy
from watcher import objects
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class Service(base.APIBase):
"""API representation of a service.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a service.
"""
_status = None
def _get_status(self):
return self._status
def _set_status(self, name):
service = objects.Service.get_by_name(pecan.request.context, name)
last_heartbeat = (service.last_seen_up or service.updated_at
or service.created_at)
if isinstance(last_heartbeat, six.string_types):
# NOTE(russellb) If this service came in over rpc via
# conductor, then the timestamp will be a string and needs to be
# converted back to a datetime.
last_heartbeat = timeutils.parse_strtime(last_heartbeat)
else:
# Objects have proper UTC timezones, but the timeutils comparison
# below does not (and will fail)
last_heartbeat = last_heartbeat.replace(tzinfo=None)
elapsed = timeutils.delta_seconds(last_heartbeat, timeutils.utcnow())
is_up = abs(elapsed) <= CONF.service_down_time
if not is_up:
LOG.warning(_LW('Seems service %(name)s on host %(host)s is down. '
'Last heartbeat was %(lhb)s.'
'Elapsed time is %(el)s'),
{'name': service.name,
'host': service.host,
'lhb': str(last_heartbeat), 'el': str(elapsed)})
self._status = objects.service.ServiceStatus.FAILED
else:
self._status = objects.service.ServiceStatus.ACTIVE
id = wsme.wsattr(int, readonly=True)
"""ID for this service."""
name = wtypes.text
"""Name of the service."""
host = wtypes.text
"""Host where service is placed on."""
last_seen_up = wsme.wsattr(datetime.datetime, readonly=True)
"""Time when Watcher service sent latest heartbeat."""
status = wsme.wsproperty(wtypes.text, _get_status, _set_status,
mandatory=True)
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link."""
def __init__(self, **kwargs):
super(Service, self).__init__()
fields = list(objects.Service.fields.keys()) + ['status']
self.fields = []
for field in fields:
self.fields.append(field)
setattr(self, field, kwargs.get(
field if field != 'status' else 'name', wtypes.Unset))
@staticmethod
def _convert_with_links(service, url, expand=True):
if not expand:
service.unset_fields_except(
['id', 'name', 'host', 'status'])
service.links = [
link.Link.make_link('self', url, 'services', str(service.id)),
link.Link.make_link('bookmark', url, 'services', str(service.id),
bookmark=True)]
return service
@classmethod
def convert_with_links(cls, service, expand=True):
service = Service(**service.as_dict())
return cls._convert_with_links(
service, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(id=1,
name='watcher-applier',
host='Controller',
last_seen_up=datetime.datetime(2016, 1, 1))
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
class ServiceCollection(collection.Collection):
"""API representation of a collection of services."""
services = [Service]
"""A list containing services objects"""
def __init__(self, **kwargs):
super(ServiceCollection, self).__init__()
self._type = 'services'
@staticmethod
def convert_with_links(services, limit, url=None, expand=False,
**kwargs):
service_collection = ServiceCollection()
service_collection.services = [
Service.convert_with_links(g, expand) for g in services]
if 'sort_key' in kwargs:
reverse = False
if kwargs['sort_key'] == 'service':
if 'sort_dir' in kwargs:
reverse = True if kwargs['sort_dir'] == 'desc' else False
service_collection.services = sorted(
service_collection.services,
key=lambda service: service.id,
reverse=reverse)
service_collection.next = service_collection.get_next(
limit, url=url, marker_field='id', **kwargs)
return service_collection
@classmethod
def sample(cls):
sample = cls()
sample.services = [Service.sample(expand=False)]
return sample
class ServicesController(rest.RestController):
"""REST controller for Services."""
def __init__(self):
super(ServicesController, self).__init__()
from_services = False
"""A flag to indicate if the requests to this controller are coming
from the top-level resource Services."""
_custom_actions = {
'detail': ['GET'],
}
def _get_services_collection(self, marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
sort_db_key = (sort_key if sort_key in objects.Service.fields.keys()
else None)
marker_obj = None
if marker:
marker_obj = objects.Service.get(
pecan.request.context, marker)
services = objects.Service.list(
pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
return ServiceCollection.convert_with_links(
services, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
@wsme_pecan.wsexpose(ServiceCollection, int, int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of services.
: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, 'service:get_all',
action='service:get_all')
return self._get_services_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(ServiceCollection, int, int, wtypes.text, wtypes.text)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of services with detail.
: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, 'service:detail',
action='service:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "services":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['services', 'detail'])
return self._get_services_collection(
marker, limit, sort_key, sort_dir, expand, resource_url)
@wsme_pecan.wsexpose(Service, wtypes.text)
def get_one(self, service):
"""Retrieve information about the given service.
:param service: ID or name of the service.
"""
if self.from_services:
raise exception.OperationNotPermitted
context = pecan.request.context
rpc_service = api_utils.get_resource('Service', service)
policy.enforce(context, 'service:get', rpc_service,
action='service:get')
return Service.convert_with_links(rpc_service)

View File

@@ -110,7 +110,7 @@ class Strategy(base.APIBase):
"""The name of the goal this audit refers to"""
parameters_spec = {wtypes.text: types.jsontype}
""" Parameters spec dict"""
"""Parameters spec dict"""
def __init__(self, **kwargs):
super(Strategy, self).__init__()

View File

@@ -15,11 +15,13 @@
import jsonpatch
from oslo_config import cfg
from oslo_utils import reflection
from oslo_utils import uuidutils
import pecan
import wsme
from watcher._i18n import _
from watcher.common import utils
from watcher import objects
CONF = cfg.CONF
@@ -80,17 +82,27 @@ def as_filters_dict(**filters):
return filters_dict
def get_resource(resource, resource_ident):
"""Get the resource from the uuid or logical name.
def get_resource(resource, resource_id, eager=False):
"""Get the resource from the uuid, id or logical name.
:param resource: the resource type.
:param resource_ident: the UUID or logical name of the resource.
:param resource_id: the UUID, ID 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)
_get = None
if utils.is_int_like(resource_id):
resource_id = int(resource_id)
_get = resource.get
elif uuidutils.is_uuid_like(resource_id):
_get = resource.get_by_uuid
else:
_get = resource.get_by_name
return resource.get_by_name(pecan.request.context, resource_ident)
method_signature = reflection.get_signature(_get)
if 'eager' in method_signature.parameters:
return _get(pecan.request.context, resource_id, eager=eager)
return _get(pecan.request.context, resource_id)

View File

@@ -114,6 +114,6 @@ class NoExceptionTracebackHook(hooks.PecanHook):
faultstring = faultstring.split(traceback_marker, 1)[0]
# Remove trailing newlines and spaces if any.
json_body['faultstring'] = faultstring.rstrip()
# Replace the whole json. Cannot change original one beacause it's
# Replace the whole json. Cannot change original one because it's
# generated on the fly.
state.response.json = json_body

View File

@@ -33,7 +33,7 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
for public routes in the API.
"""
def __init__(self, app, conf, public_api_routes=[]):
def __init__(self, app, conf, public_api_routes=()):
route_pattern_tpl = '%s(\.json|\.xml)?$'
try:

View File

@@ -20,9 +20,7 @@ from oslo_log import log
from watcher.applier.action_plan import base
from watcher.applier import default
from watcher.applier.messaging import event_types
from watcher.common.messaging.events import event
from watcher.objects import action_plan as ap_objects
from watcher import objects
LOG = log.getLogger(__name__)
@@ -34,32 +32,20 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
self.service = service
self.action_plan_uuid = action_plan_uuid
def notify(self, uuid, event_type, state):
action_plan = ap_objects.ActionPlan.get_by_uuid(self.ctx, uuid)
def update_action_plan(self, uuid, state):
action_plan = objects.ActionPlan.get_by_uuid(self.ctx, uuid)
action_plan.state = state
action_plan.save()
ev = event.Event()
ev.type = event_type
ev.data = {}
payload = {'action_plan__uuid': uuid,
'action_plan_state': state}
self.service.publish_status_event(ev.type.name, payload)
def execute(self):
try:
# update state
self.notify(self.action_plan_uuid,
event_types.EventTypes.LAUNCH_ACTION_PLAN,
ap_objects.State.ONGOING)
self.update_action_plan(self.action_plan_uuid,
objects.action_plan.State.ONGOING)
applier = default.DefaultApplier(self.ctx, self.service)
applier.execute(self.action_plan_uuid)
state = ap_objects.State.SUCCEEDED
state = objects.action_plan.State.SUCCEEDED
except Exception as e:
LOG.exception(e)
state = ap_objects.State.FAILED
state = objects.action_plan.State.FAILED
finally:
# update state
self.notify(self.action_plan_uuid,
event_types.EventTypes.LAUNCH_ACTION_PLAN,
state)
self.update_action_plan(self.action_plan_uuid, state)

View File

@@ -20,6 +20,7 @@
from oslo_config import cfg
from watcher.applier.messaging import trigger
from watcher.common import service_manager
CONF = cfg.CONF
@@ -36,13 +37,6 @@ APPLIER_MANAGER_OPTS = [
help='The topic name used for'
'control events, this topic '
'used for rpc call '),
cfg.StrOpt('status_topic',
default='watcher.applier.status',
help='The topic name used for '
'status events, this topic '
'is used so as to notify'
'the others components '
'of the system'),
cfg.StrOpt('publisher_id',
default='watcher.applier.api',
help='The identifier used by watcher '
@@ -60,17 +54,32 @@ CONF.register_group(opt_group)
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
class ApplierManager(object):
class ApplierManager(service_manager.ServiceManager):
API_VERSION = '1.0'
@property
def service_name(self):
return 'watcher-applier'
conductor_endpoints = [trigger.TriggerActionPlan]
status_endpoints = []
notification_endpoints = []
notification_topics = []
@property
def api_version(self):
return '1.0'
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
@property
def publisher_id(self):
return CONF.watcher_applier.publisher_id
@property
def conductor_topic(self):
return CONF.watcher_applier.conductor_topic
@property
def notification_topics(self):
return []
@property
def conductor_endpoints(self):
return [trigger.TriggerActionPlan]
@property
def notification_endpoints(self):
return []

View File

@@ -15,12 +15,13 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from oslo_config import cfg
from watcher.applier import manager
from watcher.common import exception
from watcher.common import service
from watcher.common import service_manager
from watcher.common import utils
@@ -39,21 +40,35 @@ class ApplierAPI(service.Service):
raise exception.InvalidUuidOrName(name=action_plan_uuid)
return self.conductor_client.call(
context.to_dict(), 'launch_action_plan',
action_plan_uuid=action_plan_uuid)
context, 'launch_action_plan', action_plan_uuid=action_plan_uuid)
class ApplierAPIManager(object):
class ApplierAPIManager(service_manager.ServiceManager):
API_VERSION = '1.0'
@property
def service_name(self):
return None
conductor_endpoints = []
status_endpoints = []
notification_endpoints = []
notification_topics = []
@property
def api_version(self):
return '1.0'
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
@property
def publisher_id(self):
return CONF.watcher_applier.publisher_id
@property
def conductor_topic(self):
return CONF.watcher_applier.conductor_topic
@property
def notification_topics(self):
return []
@property
def conductor_endpoints(self):
return []
@property
def notification_endpoints(self):
return []

View File

@@ -21,10 +21,8 @@ import abc
import six
from watcher.applier.actions import factory
from watcher.applier.messaging import event_types
from watcher.common import clients
from watcher.common.loader import loadable
from watcher.common.messaging.events import event
from watcher import objects
@@ -77,12 +75,7 @@ class BaseWorkFlowEngine(loadable.Loadable):
db_action = objects.Action.get_by_uuid(self.context, action.uuid)
db_action.state = state
db_action.save()
ev = event.Event()
ev.type = event_types.EventTypes.LAUNCH_ACTION
ev.data = {}
payload = {'action_uuid': action.uuid,
'action_state': state}
self.applier_manager.publish_status_event(ev.type.name, payload)
# NOTE(v-francoise): Implement notifications for action
@abc.abstractmethod
def execute(self, actions):

View File

@@ -23,7 +23,7 @@ from taskflow import task
from watcher._i18n import _LE, _LW, _LC
from watcher.applier.workflow_engine import base
from watcher.common import exception
from watcher.objects import action as obj_action
from watcher import objects
LOG = log.getLogger(__name__)
@@ -107,14 +107,12 @@ class TaskFlowActionContainer(task.Task):
def pre_execute(self):
try:
self.engine.notify(self._db_action,
obj_action.State.ONGOING)
self.engine.notify(self._db_action, objects.action.State.ONGOING)
LOG.debug("Pre-condition action: %s", self.name)
self.action.pre_condition()
except Exception as e:
LOG.exception(e)
self.engine.notify(self._db_action,
obj_action.State.FAILED)
self.engine.notify(self._db_action, objects.action.State.FAILED)
raise
def execute(self, *args, **kwargs):
@@ -122,15 +120,13 @@ class TaskFlowActionContainer(task.Task):
LOG.debug("Running action: %s", self.name)
self.action.execute()
self.engine.notify(self._db_action,
obj_action.State.SUCCEEDED)
self.engine.notify(self._db_action, objects.action.State.SUCCEEDED)
except Exception as e:
LOG.exception(e)
LOG.error(_LE('The workflow engine has failed '
'to execute the action: %s'), self.name)
self.engine.notify(self._db_action,
obj_action.State.FAILED)
self.engine.notify(self._db_action, objects.action.State.FAILED)
raise
def post_execute(self):
@@ -139,8 +135,7 @@ class TaskFlowActionContainer(task.Task):
self.action.post_condition()
except Exception as e:
LOG.exception(e)
self.engine.notify(self._db_action,
obj_action.State.FAILED)
self.engine.notify(self._db_action, objects.action.State.FAILED)
raise
def revert(self, *args, **kwargs):

View File

@@ -24,19 +24,19 @@ from oslo_log import log as logging
from watcher._i18n import _LI
from watcher.common import service
from watcher import conf
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF = conf.CONF
def main():
service.prepare_service(sys.argv)
service.prepare_service(sys.argv, CONF)
host, port = cfg.CONF.api.host, cfg.CONF.api.port
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)
server = service.WSGIService('watcher-api', CONF.api.enable_ssl_api)
if host == '127.0.0.1':
LOG.info(_LI('serving on 127.0.0.1:%(port)s, '

View File

@@ -20,19 +20,19 @@
import os
import sys
from oslo_config import cfg
from oslo_log import log as logging
from watcher._i18n import _LI
from watcher.applier import manager
from watcher.common import service as watcher_service
from watcher import conf
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF = conf.CONF
def main():
watcher_service.prepare_service(sys.argv)
watcher_service.prepare_service(sys.argv, CONF)
LOG.info(_LI('Starting Watcher Applier service in PID %s'), os.getpid())

View File

@@ -24,10 +24,11 @@ import sys
from oslo_config import cfg
from watcher.common import service
from watcher import conf
from watcher.db import migration
from watcher.db import purge
CONF = cfg.CONF
CONF = conf.CONF
class DBCommand(object):
@@ -152,5 +153,5 @@ def main():
if not set(sys.argv).intersection(valid_commands):
sys.argv.append('upgrade')
service.prepare_service(sys.argv)
service.prepare_service(sys.argv, CONF)
CONF.command.func()

View File

@@ -20,22 +20,22 @@
import os
import sys
from oslo_config import cfg
from oslo_log import log as logging
from watcher._i18n import _LI
from watcher.common import service as watcher_service
from watcher import conf
from watcher.decision_engine import gmr
from watcher.decision_engine import manager
from watcher.decision_engine import scheduling
from watcher.decision_engine import sync
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF = conf.CONF
def main():
watcher_service.prepare_service(sys.argv)
watcher_service.prepare_service(sys.argv, CONF)
gmr.register_gmr_plugins()
LOG.info(_LI('Starting Watcher Decision Engine service in PID %s'),

View File

@@ -24,15 +24,17 @@ from oslo_log import log as logging
from watcher._i18n import _LI
from watcher.common import service as service
from watcher import conf
from watcher.decision_engine import sync
LOG = logging.getLogger(__name__)
CONF = conf.CONF
def main():
LOG.info(_LI('Watcher sync started.'))
service.prepare_service(sys.argv)
service.prepare_service(sys.argv, CONF)
syncer = sync.Syncer()
syncer.sync()

View File

@@ -144,17 +144,17 @@ class CeilometerHelper(object):
:return:
"""
end_time = datetime.datetime.utcnow()
start_time = (datetime.datetime.utcnow() -
datetime.timedelta(seconds=int(period)))
query = self.build_query(
resource_id=resource_id, start_time=start_time)
resource_id=resource_id, start_time=start_time, end_time=end_time)
statistic = self.query_retry(f=self.ceilometer.statistics.list,
meter_name=meter_name,
q=query,
period=period,
aggregates=[
{'func': aggregate}],
groupby=['resource_id'])
{'func': aggregate}])
item_value = None
if statistic:

View File

@@ -34,14 +34,14 @@ from watcher._i18n import _, _LE
LOG = logging.getLogger(__name__)
exc_log_opts = [
EXC_LOG_OPTS = [
cfg.BoolOpt('fatal_exception_format_errors',
default=False,
help='Make exception message format errors fatal.'),
]
CONF = cfg.CONF
CONF.register_opts(exc_log_opts)
CONF.register_opts(EXC_LOG_OPTS)
def wrap_keystone_exception(func):
@@ -119,6 +119,10 @@ class WatcherException(Exception):
return six.text_type(self)
class UnsupportedError(WatcherException):
msg_fmt = _("Not supported")
class NotAuthorized(WatcherException):
msg_fmt = _("Not authorized")
code = 403
@@ -168,6 +172,14 @@ class InvalidStrategy(Invalid):
msg_fmt = _("Strategy %(strategy)s is invalid")
class InvalidAudit(Invalid):
msg_fmt = _("Audit %(audit)s is invalid")
class EagerlyLoadedAuditRequired(InvalidAudit):
msg_fmt = _("Audit %(audit)s was not eagerly loaded")
class InvalidUUID(Invalid):
msg_fmt = _("Expected a uuid but received %(uuid)s")
@@ -362,6 +374,19 @@ class NoSuchMetricForHost(WatcherException):
msg_fmt = _("No %(metric)s metric for %(host)s found.")
class ServiceAlreadyExists(Conflict):
msg_fmt = _("A service with name %(name)s is already working on %(host)s.")
class ServiceNotFound(ResourceNotFound):
msg_fmt = _("The service %(service)s cannot be found.")
class WildcardCharacterIsUsed(WatcherException):
msg_fmt = _("You shouldn't use any other IDs of %(resource)s if you use "
"wildcard character.")
# Model
class InstanceNotFound(WatcherException):
@@ -386,3 +411,8 @@ class NotSoftDeletedStateError(WatcherException):
class NegativeLimitError(WatcherException):
msg_fmt = _("Limit should be positive")
class NotificationPayloadError(WatcherException):
_msg_fmt = _("Payload not populated when trying to send notification "
"\"%(class_name)s\"")

View File

@@ -1,54 +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.
class Event(object):
"""Generic event to use with EventDispatcher"""
def __init__(self, event_type=None, data=None, request_id=None):
"""Default constructor
:param event_type: the type of the event
:param data: a dictionary which contains data
:param request_id: a string which represent the uuid of the request
"""
self._type = event_type
self._data = data
self._request_id = request_id
@property
def type(self):
return self._type
@type.setter
def type(self, type):
self._type = type
@property
def data(self):
return self._data
@data.setter
def data(self, data):
self._data = data
@property
def request_id(self):
return self._request_id
@request_id.setter
def request_id(self, id):
self._request_id = id

View File

@@ -1,78 +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.decision_engine.messaging import events as messaging_events
LOG = log.getLogger(__name__)
class EventDispatcher(object):
"""Generic event dispatcher which listen and dispatch events"""
def __init__(self):
self._events = dict()
def __del__(self):
self._events = None
def has_listener(self, event_type, listener):
"""Return true if listener is register to event_type """
# Check for event type and for the listener
if event_type in self._events.keys():
return listener in self._events[event_type]
else:
return False
def dispatch_event(self, event):
LOG.debug("dispatch evt : %s" % str(event.type))
"""
Dispatch an instance of Event class
"""
if messaging_events.Events.ALL in self._events.keys():
listeners = self._events[messaging_events.Events.ALL]
for listener in listeners:
listener(event)
# Dispatch the event to all the associated listeners
if event.type in self._events.keys():
listeners = self._events[event.type]
for listener in listeners:
listener(event)
def add_event_listener(self, event_type, listener):
"""Add an event listener for an event type"""
# Add listener to the event type
if not self.has_listener(event_type, listener):
listeners = self._events.get(event_type, [])
listeners.append(listener)
self._events[event_type] = listeners
def remove_event_listener(self, event_type, listener):
"""Remove event listener. """
# Remove the listener from the event type
if self.has_listener(event_type, listener):
listeners = self._events[event_type]
if len(listeners) == 1:
# Only this listener remains so remove the key
del self._events[event_type]
else:
# Update listeners chain
listeners.remove(listener)
self._events[event_type] = listeners

View File

@@ -1,120 +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.
import socket
import threading
import eventlet
from oslo_config import cfg
from oslo_log import log
import oslo_messaging as om
from watcher.common import rpc
from watcher._i18n import _LE, _LW
# NOTE:
# Ubuntu 14.04 forces librabbitmq when kombu is used
# Unfortunately it forces a version that has a crash
# bug. Calling eventlet.monkey_patch() tells kombu
# to use libamqp instead.
eventlet.monkey_patch()
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class MessagingHandler(threading.Thread):
def __init__(self, publisher_id, topic_name, endpoints, version,
serializer=None):
super(MessagingHandler, self).__init__()
self.publisher_id = publisher_id
self.topic_name = topic_name
self.__endpoints = []
self.__serializer = serializer
self.__version = version
self.__server = None
self.__notifier = None
self.__transport = None
self.add_endpoints(endpoints)
def add_endpoints(self, endpoints):
self.__endpoints.extend(endpoints)
def remove_endpoint(self, endpoint):
if endpoint in self.__endpoints:
self.__endpoints.remove(endpoint)
@property
def endpoints(self):
return self.__endpoints
@property
def transport(self):
return self.__transport
def build_notifier(self):
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
return om.Notifier(
self.__transport,
publisher_id=self.publisher_id,
topic=self.topic_name,
serializer=serializer
)
def build_server(self, target):
return om.get_rpc_server(self.__transport, target,
self.__endpoints,
executor='eventlet',
serializer=self.__serializer)
def _configure(self):
try:
self.__transport = om.get_transport(CONF)
self.__notifier = self.build_notifier()
if len(self.__endpoints):
target = om.Target(
topic=self.topic_name,
# For compatibility, we can override it with 'host' opt
server=CONF.host or socket.getfqdn(),
version=self.__version,
)
self.__server = self.build_server(target)
else:
LOG.warning(
_LW("No endpoint defined; can only publish events"))
except Exception as e:
LOG.exception(e)
LOG.error(_LE("Messaging configuration error"))
def run(self):
LOG.debug("configure MessagingHandler for %s" % self.topic_name)
self._configure()
if len(self.__endpoints) > 0:
LOG.debug("Starting up server")
self.__server.start()
def stop(self):
LOG.debug('Stopped server')
self.__server.stop()
def publish_event(self, event_type, payload, request_id=None):
self.__notifier.info(
{'version_api': self.__version,
'request_id': request_id},
{'event_id': event_type}, payload
)

View File

@@ -1,47 +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.
import eventlet
import oslo_messaging as messaging
from watcher.common.messaging.utils import observable
eventlet.monkey_patch()
class NotificationHandler(observable.Observable):
def __init__(self, publisher_id):
super(NotificationHandler, self).__init__()
self.publisher_id = publisher_id
def info(self, ctx, publisher_id, event_type, payload, metadata):
if publisher_id == self.publisher_id:
self.set_changed()
self.notify(ctx, publisher_id, event_type, metadata, payload)
return messaging.NotificationResult.HANDLED
def warn(self, ctx, publisher_id, event_type, payload, metadata):
if publisher_id == self.publisher_id:
self.set_changed()
self.notify(ctx, publisher_id, event_type, metadata, payload)
return messaging.NotificationResult.HANDLED
def error(self, ctx, publisher_id, event_type, payload, metadata):
if publisher_id == self.publisher_id:
self.set_changed()
self.notify(ctx, publisher_id, event_type, metadata, payload)
return messaging.NotificationResult.HANDLED

View File

@@ -63,16 +63,17 @@ class NovaHelper(object):
LOG.exception(exc)
raise exception.ComputeNodeNotFound(name=node_hostname)
def get_aggregate_list(self):
return self.nova.aggregates.list()
def get_aggregate_detail(self, aggregate_id):
return self.nova.aggregates.get(aggregate_id)
def get_availability_zone_list(self):
return self.nova.availability_zones.list(detailed=True)
def find_instance(self, instance_id):
search_opts = {'all_tenants': True}
instances = self.nova.servers.list(detailed=True,
search_opts=search_opts)
instance = None
for _instance in instances:
if _instance.id == instance_id:
instance = _instance
break
return instance
return self.nova.servers.get(instance_id)
def wait_for_volume_status(self, volume, status, timeout=60,
poll_interval=1):
@@ -663,7 +664,7 @@ class NovaHelper(object):
cache[fid] = flavor
attr_defaults = [('name', 'unknown-id-%s' % fid),
('vcpus', 0), ('ram', 0), ('disk', 0),
('ephemeral', 0)]
('ephemeral', 0), ('extra_specs', {})]
for attr, default in attr_defaults:
if not flavor:
instance.flavor[attr] = default

View File

@@ -72,8 +72,12 @@ def init(conf):
aliases=TRANSPORT_ALIASES)
serializer = RequestContextSerializer(JsonPayloadSerializer())
NOTIFIER = messaging.Notifier(NOTIFICATION_TRANSPORT,
serializer=serializer)
if not conf.notification_level:
NOTIFIER = messaging.Notifier(
NOTIFICATION_TRANSPORT, serializer=serializer, driver='noop')
else:
NOTIFIER = messaging.Notifier(NOTIFICATION_TRANSPORT,
serializer=serializer)
def initialized():
@@ -121,7 +125,7 @@ class RequestContextSerializer(messaging.Serializer):
return self._base.deserialize_entity(context, entity)
def serialize_context(self, context):
return context
return context.to_dict()
def deserialize_context(self, context):
return watcher_context.RequestContext.from_dict(context)
@@ -146,8 +150,6 @@ def get_server(target, endpoints, serializer=None):
serializer=serializer)
def get_notifier(service=None, host=None, publisher_id=None):
def get_notifier(publisher_id):
assert NOTIFIER is not None
if not publisher_id:
publisher_id = "%s.%s" % (service, host or CONF.host)
return NOTIFIER.prepare(publisher_id=publisher_id)

View File

@@ -14,9 +14,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
import datetime
import socket
import eventlet
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import _options
@@ -27,30 +28,36 @@ from oslo_reports import opts as gmr_opts
from oslo_service import service
from oslo_service import wsgi
from watcher._i18n import _, _LI
from watcher._i18n import _
from watcher.api import app
from watcher.common import config
from watcher.common.messaging.events import event_dispatcher as dispatcher
from watcher.common.messaging import messaging_handler
from watcher.common import context
from watcher.common import rpc
from watcher.common import scheduling
from watcher.conf import plugins as plugins_conf
from watcher import objects
from watcher.objects import base
from watcher import opts
from watcher.objects import fields as wfields
from watcher import version
service_opts = [
cfg.IntOpt('periodic_interval',
default=60,
help=_('Seconds between running periodic tasks.')),
cfg.StrOpt('host',
default=socket.getfqdn(),
help=_('Name of this node. This can be an opaque identifier. '
'It is not necessarily a hostname, FQDN, or IP address. '
'However, the node name must be valid within '
'an AMQP key, and if using ZeroMQ, a valid '
'hostname, FQDN, or IP address.')),
]
# NOTE:
# Ubuntu 14.04 forces librabbitmq when kombu is used
# Unfortunately it forces a version that has a crash
# bug. Calling eventlet.monkey_patch() tells kombu
# to use libamqp instead.
eventlet.monkey_patch()
NOTIFICATION_OPTS = [
cfg.StrOpt('notification_level',
choices=[''] + list(wfields.NotificationPriority.ALL),
default=wfields.NotificationPriority.INFO,
help=_('Specifies the minimum level for which to send '
'notifications. If not set, no notifications will '
'be sent. The default is for this option to be at the '
'`INFO` level.'))
]
cfg.CONF.register_opts(NOTIFICATION_OPTS)
cfg.CONF.register_opts(service_opts)
CONF = cfg.CONF
LOG = log.getLogger(__name__)
@@ -68,21 +75,21 @@ Singleton = service.Singleton
class WSGIService(service.ServiceBase):
"""Provides ability to launch Watcher API from wsgi app."""
def __init__(self, name, use_ssl=False):
def __init__(self, service_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 service_name: The service name of the WSGI server.
:param use_ssl: Wraps the socket in an SSL context if True.
"""
self.name = name
self.service_name = service_name
self.app = app.VersionSelectorApplication()
self.workers = (CONF.api.workers or
processutils.get_worker_count())
self.server = wsgi.Server(CONF, name, self.app,
self.server = wsgi.Server(CONF, self.service_name, self.app,
host=CONF.api.host,
port=CONF.api.port,
use_ssl=use_ssl,
logger_name=name)
logger_name=self.service_name)
def start(self):
"""Start serving this service using loaded configuration"""
@@ -101,7 +108,53 @@ class WSGIService(service.ServiceBase):
self.server.reset()
class Service(service.ServiceBase, dispatcher.EventDispatcher):
class ServiceHeartbeat(scheduling.BackgroundSchedulerService):
def __init__(self, gconfig=None, service_name=None, **kwargs):
gconfig = None or {}
super(ServiceHeartbeat, self).__init__(gconfig, **kwargs)
self.service_name = service_name
self.context = context.make_context()
def send_beat(self):
host = CONF.host
watcher_list = objects.Service.list(
self.context, filters={'name': self.service_name,
'host': host})
if watcher_list:
watcher_service = watcher_list[0]
watcher_service.last_seen_up = datetime.datetime.utcnow()
watcher_service.save()
else:
watcher_service = objects.Service(self.context)
watcher_service.name = self.service_name
watcher_service.host = host
watcher_service.create()
def add_heartbeat_job(self):
self.add_job(self.send_beat, 'interval', seconds=60,
next_run_time=datetime.datetime.now())
def start(self):
"""Start service."""
self.add_heartbeat_job()
super(ServiceHeartbeat, self).start()
def stop(self):
"""Stop service."""
self.shutdown()
def wait(self):
"""Wait for service to complete."""
def reset(self):
"""Reset service.
Called in case service running in daemon mode receives SIGHUP.
"""
class Service(service.ServiceBase):
API_VERSION = '1.0'
@@ -110,18 +163,14 @@ class Service(service.ServiceBase, dispatcher.EventDispatcher):
self.manager = manager_class()
self.publisher_id = self.manager.publisher_id
self.api_version = self.manager.API_VERSION
self.api_version = self.manager.api_version
self.conductor_topic = self.manager.conductor_topic
self.status_topic = self.manager.status_topic
self.notification_topics = self.manager.notification_topics
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.notification_endpoints = self.manager.notification_endpoints
self.serializer = rpc.RequestContextSerializer(
@@ -130,22 +179,23 @@ class Service(service.ServiceBase, dispatcher.EventDispatcher):
self._transport = None
self._notification_transport = None
self._conductor_client = None
self._status_client = None
self.conductor_topic_handler = None
self.status_topic_handler = None
self.notification_handler = None
self.heartbeat = None
if self.conductor_topic and self.conductor_endpoints:
self.conductor_topic_handler = self.build_topic_handler(
self.conductor_topic, self.conductor_endpoints)
if self.status_topic and self.status_endpoints:
self.status_topic_handler = self.build_topic_handler(
self.status_topic, self.status_endpoints)
if self.notification_topics and self.notification_endpoints:
self.notification_handler = self.build_notification_handler(
self.notification_topics, self.notification_endpoints
)
self.service_name = self.manager.service_name
if self.service_name:
self.heartbeat = ServiceHeartbeat(
service_name=self.manager.service_name)
@property
def transport(self):
@@ -174,25 +224,17 @@ class Service(service.ServiceBase, dispatcher.EventDispatcher):
def conductor_client(self, c):
self.conductor_client = c
@property
def status_client(self):
if self._status_client is None:
target = om.Target(
topic=self.status_topic,
version=self.API_VERSION,
)
self._status_client = om.RPCClient(
self.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)
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
target = om.Target(
topic=topic_name,
# For compatibility, we can override it with 'host' opt
server=CONF.host or socket.gethostname(),
version=self.api_version,
)
return om.get_rpc_server(
self.transport, target, endpoints,
executor='eventlet', serializer=serializer)
def build_notification_handler(self, topic_names, endpoints=()):
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
@@ -207,20 +249,20 @@ class Service(service.ServiceBase, dispatcher.EventDispatcher):
CONF.transport_url, CONF.rpc_backend)
if self.conductor_topic_handler:
self.conductor_topic_handler.start()
if self.status_topic_handler:
self.status_topic_handler.start()
if self.notification_handler:
self.notification_handler.start()
if self.heartbeat:
self.heartbeat.start()
def stop(self):
LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
if self.conductor_topic_handler:
self.conductor_topic_handler.stop()
if self.status_topic_handler:
self.status_topic_handler.stop()
if self.notification_handler:
self.notification_handler.stop()
if self.heartbeat:
self.heartbeat.stop()
def reset(self):
"""Reset a service in case it received a SIGHUP."""
@@ -228,34 +270,11 @@ class Service(service.ServiceBase, dispatcher.EventDispatcher):
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_event(self, event, payload, request_id=None):
if self.status_topic_handler:
return self.status_topic_handler.publish_event(
event, payload, request_id)
else:
LOG.info(
_LI("No status notifier declared: notification '%s' not sent"),
event)
def get_version(self):
return self.api_version
def check_api_version(self, context):
def check_api_version(self, ctx):
api_manager_version = self.conductor_client.call(
context.to_dict(), 'check_api_version',
api_version=self.api_version)
ctx, '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_event(evt, payload)
def launch(conf, service_, workers=1, restart_method='reload'):
return service.launch(conf, service_, workers, restart_method)
@@ -269,7 +288,9 @@ def prepare_service(argv=(), conf=cfg.CONF):
cfg.set_defaults(_options.log_opts,
default_log_levels=_DEFAULT_LOG_LEVELS)
log.setup(conf, 'python-watcher')
conf.log_opt_values(LOG, logging.DEBUG)
conf.log_opt_values(LOG, log.DEBUG)
objects.register_all()
gmr.TextGuruMeditation.register_section(_('Plugins'), opts.show_plugins)
gmr.TextGuruMeditation.setup_autorun(version)
gmr.TextGuruMeditation.register_section(
_('Plugins'), plugins_conf.show_plugins)
gmr.TextGuruMeditation.setup_autorun(version, conf=conf)

View File

@@ -0,0 +1,50 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2016 Servionica
##
# 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
@six.add_metaclass(abc.ABCMeta)
class ServiceManager(object):
@abc.abstractproperty
def service_name(self):
raise NotImplementedError()
@abc.abstractproperty
def api_version(self):
raise NotImplementedError()
@abc.abstractproperty
def publisher_id(self):
raise NotImplementedError()
@abc.abstractproperty
def conductor_topic(self):
raise NotImplementedError()
@abc.abstractproperty
def notification_topics(self):
raise NotImplementedError()
@abc.abstractproperty
def conductor_endpoints(self):
raise NotImplementedError()
@abc.abstractproperty
def notification_endpoints(self):
raise NotImplementedError()

View File

@@ -16,17 +16,19 @@
"""Utilities and helper functions."""
import re
from jsonschema import validators
from oslo_config import cfg
from oslo_log import log as logging
from watcher.common import exception
import re
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six
import uuid
from watcher._i18n import _LW
from watcher.common import exception
UTILS_OPTS = [
cfg.StrOpt('rootwrap_config',
@@ -66,6 +68,12 @@ class Struct(dict):
raise AttributeError(name)
generate_uuid = uuidutils.generate_uuid
is_uuid_like = uuidutils.is_uuid_like
is_int_like = strutils.is_int_like
strtime = timeutils.strtime
def safe_rstrip(value, chars=None):
"""Removes trailing characters from a string if that does not make it empty
@@ -83,31 +91,6 @@ def safe_rstrip(value, chars=None):
return value.rstrip(chars) or value
def generate_uuid():
return str(uuid.uuid4())
def is_int_like(val):
"""Check if a value looks like an int."""
try:
return str(int(val)) == str(val)
except Exception:
return False
def is_uuid_like(val):
"""Returns validation of a value as a UUID.
For our purposes, a UUID is a canonical form string:
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
"""
try:
return str(uuid.UUID(val)) == val
except (TypeError, ValueError, AttributeError):
return False
def is_hostname_safe(hostname):
"""Determine if the supplied hostname is RFC compliant.
@@ -133,10 +116,6 @@ def get_cls_import_path(cls):
return module + '.' + cls.__name__
def strtime(at):
return at.strftime("%Y-%m-%dT%H:%M:%S.%f")
# Default value feedback extension as jsonschema doesn't support it
def extend_with_default(validator_class):
validate_properties = validator_class.VALIDATORS["properties"]
@@ -173,3 +152,5 @@ def extend_with_strict_schema(validator_class):
StrictDefaultValidatingDraft4Validator = extend_with_default(
extend_with_strict_schema(validators.Draft4Validator))
Draft4Validator = validators.Draft4Validator

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
# Copyright (c) 2016 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,12 +15,11 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import enum
from oslo_config import cfg
from watcher.conf import service
class Events(enum.Enum):
ALL = '*',
ACTION_PLAN = "action_plan"
TRIGGER_AUDIT = "trigger_audit"
CONF = cfg.CONF
service.register_opts(CONF)

56
watcher/conf/_opts.py Normal file
View File

@@ -0,0 +1,56 @@
# -*- encoding: utf-8 -*-
# Copyright 2014
# The Cloudscaling Group, Inc.
#
# 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 keystoneauth1 import loading as ka_loading
from watcher.api import acl as api_acl
from watcher.api import app as api_app
from watcher.applier import manager as applier_manager
from watcher.common import clients
from watcher.common import exception
from watcher.common import paths
from watcher.db.sqlalchemy import models
from watcher.decision_engine.audit import continuous
from watcher.decision_engine import manager as decision_engine_manager
from watcher.decision_engine.planner import manager as planner_manager
def list_opts():
"""Legacy aggregation of all the watcher config options"""
return [
('DEFAULT',
(api_app.API_SERVICE_OPTS +
api_acl.AUTH_OPTS +
exception.EXC_LOG_OPTS +
paths.PATH_OPTS)),
('api', api_app.API_SERVICE_OPTS),
('database', models.SQL_OPTS),
('watcher_decision_engine',
(decision_engine_manager.WATCHER_DECISION_ENGINE_OPTS +
continuous.WATCHER_CONTINUOUS_OPTS)),
('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
('watcher_planner', planner_manager.WATCHER_PLANNER_OPTS),
('nova_client', clients.NOVA_CLIENT_OPTS),
('glance_client', clients.GLANCE_CLIENT_OPTS),
('cinder_client', clients.CINDER_CLIENT_OPTS),
('ceilometer_client', clients.CEILOMETER_CLIENT_OPTS),
('neutron_client', clients.NEUTRON_CLIENT_OPTS),
('watcher_clients_auth',
(ka_loading.get_auth_common_conf_options() +
ka_loading.get_auth_plugin_conf_options('password') +
ka_loading.get_session_conf_options()))
]

95
watcher/conf/opts.py Normal file
View File

@@ -0,0 +1,95 @@
# 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.
"""
This is the single point of entry to generate the sample configuration
file for Watcher. It collects all the necessary info from the other modules
in this package. It is assumed that:
* every other module in this package has a 'list_opts' function which
return a dict where
* the keys are strings which are the group names
* the value of each key is a list of config options for that group
* the watcher.conf package doesn't have further packages with config options
* this module is only used in the context of sample file generation
"""
import collections
import importlib
import os
import pkgutil
LIST_OPTS_FUNC_NAME = "list_opts"
def _tupleize(dct):
"""Take the dict of options and convert to the 2-tuple format."""
return [(key, val) for key, val in dct.items()]
def list_opts():
"""Grouped list of all the Watcher-specific configuration options
:return: A list of ``(group, [opt_1, opt_2])`` tuple pairs, where ``group``
is either a group name as a string or an OptGroup object.
"""
opts = collections.defaultdict(list)
module_names = _list_module_names()
imported_modules = _import_modules(module_names)
_append_config_options(imported_modules, opts)
return _tupleize(opts)
def _list_module_names():
module_names = []
package_path = os.path.dirname(os.path.abspath(__file__))
for __, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
if modname == "opts" or ispkg:
continue
else:
module_names.append(modname)
return module_names
def _import_modules(module_names):
imported_modules = []
for modname in module_names:
mod = importlib.import_module("watcher.conf." + modname)
if not hasattr(mod, LIST_OPTS_FUNC_NAME):
msg = "The module 'watcher.conf.%s' should have a '%s' "\
"function which returns the config options." % \
(modname, LIST_OPTS_FUNC_NAME)
raise Exception(msg)
else:
imported_modules.append(mod)
return imported_modules
def _process_old_opts(configs):
"""Convert old-style 2-tuple configs to dicts."""
if isinstance(configs, tuple):
configs = [configs]
return {label: options for label, options in configs}
def _append_config_options(imported_modules, config_options):
for mod in imported_modules:
configs = mod.list_opts()
# TODO(markus_z): Remove this compatibility shim once all list_opts()
# functions have been updated to return dicts.
if not isinstance(configs, dict):
configs = _process_old_opts(configs)
for key, val in configs.items():
config_options[key].extend(val)

View File

@@ -1,6 +1,7 @@
# -*- encoding: utf-8 -*-
# Copyright 2014
# The Cloudscaling Group, Inc.
# Copyright (c) 2016 b<>com
#
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,18 +16,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from keystoneauth1 import loading as ka_loading
import prettytable as ptable
import watcher.api.app
from watcher.applier.loading import default as applier_loader
from watcher.applier import manager as applier_manager
from watcher.common import clients
from watcher.common import utils
from watcher.decision_engine.loading import default as decision_engine_loader
from watcher.decision_engine import manager as decision_engine_manger
from watcher.decision_engine.planner import manager as planner_manager
PLUGIN_LOADERS = (
applier_loader.DefaultActionLoader,
@@ -40,29 +34,7 @@ PLUGIN_LOADERS = (
def list_opts():
watcher_opts = [
('api', watcher.api.app.API_SERVICE_OPTS),
('watcher_decision_engine',
decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS),
('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
('watcher_planner', planner_manager.WATCHER_PLANNER_OPTS),
('nova_client', clients.NOVA_CLIENT_OPTS),
('glance_client', clients.GLANCE_CLIENT_OPTS),
('cinder_client', clients.CINDER_CLIENT_OPTS),
('ceilometer_client', clients.CEILOMETER_CLIENT_OPTS),
('neutron_client', clients.NEUTRON_CLIENT_OPTS),
('watcher_clients_auth',
(ka_loading.get_auth_common_conf_options() +
ka_loading.get_auth_plugin_conf_options('password') +
ka_loading.get_session_conf_options()))
]
watcher_opts += list_plugin_opts()
return watcher_opts
def list_plugin_opts():
"""Load config options for all Watcher plugins"""
plugins_opts = []
for plugin_loader_cls in PLUGIN_LOADERS:
plugin_loader = plugin_loader_cls()

49
watcher/conf/service.py Normal file
View File

@@ -0,0 +1,49 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
#
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import socket
from oslo_config import cfg
from watcher._i18n import _
SERVICE_OPTS = [
cfg.IntOpt('periodic_interval',
default=60,
help=_('Seconds between running periodic tasks.')),
cfg.StrOpt('host',
default=socket.gethostname(),
help=_('Name of this node. This can be an opaque identifier. '
'It is not necessarily a hostname, FQDN, or IP address. '
'However, the node name must be valid within '
'an AMQP key, and if using ZeroMQ, a valid '
'hostname, FQDN, or IP address.')),
cfg.IntOpt('service_down_time',
default=90,
help=_('Maximum time since last check-in for up service.'))
]
def register_opts(conf):
conf.register_opts(SERVICE_OPTS)
def list_opts():
return [
('DEFAULT', SERVICE_OPTS),
]

View File

@@ -36,7 +36,7 @@ class BaseConnection(object):
@abc.abstractmethod
def get_goal_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None, eager=False):
"""Get specific columns for matching goals.
Return a list of the specified columns for all goals that
@@ -50,6 +50,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -72,31 +73,34 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def get_goal_by_id(self, context, goal_id):
def get_goal_by_id(self, context, goal_id, eager=False):
"""Return a goal given its ID.
:param context: The security context
:param goal_id: The ID of a goal
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_goal_by_uuid(self, context, goal_uuid):
def get_goal_by_uuid(self, context, goal_uuid, eager=False):
"""Return a goal given its UUID.
:param context: The security context
:param goal_uuid: The UUID of a goal
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_goal_by_name(self, context, goal_name):
def get_goal_by_name(self, context, goal_name, eager=False):
"""Return a goal given its name.
:param context: The security context
:param goal_name: The name of a goal
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A goal
:raises: :py:class:`~.GoalNotFound`
"""
@@ -129,9 +133,17 @@ class BaseConnection(object):
:raises: :py:class:`~.Invalid`
"""
def soft_delete_goal(self, goal_id):
"""Soft delete a goal.
:param goal_id: The id or uuid of a goal.
:raises: :py:class:`~.GoalNotFound`
"""
@abc.abstractmethod
def get_strategy_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None,
eager=True):
"""Get specific columns for matching strategies.
Return a list of the specified columns for all strategies that
@@ -146,6 +158,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -170,31 +183,34 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def get_strategy_by_id(self, context, strategy_id):
def get_strategy_by_id(self, context, strategy_id, eager=False):
"""Return a strategy given its ID.
:param context: The security context
:param strategy_id: The ID of a strategy
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_strategy_by_uuid(self, context, strategy_uuid):
def get_strategy_by_uuid(self, context, strategy_uuid, eager=False):
"""Return a strategy given its UUID.
:param context: The security context
:param strategy_uuid: The UUID of a strategy
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_strategy_by_name(self, context, strategy_name):
def get_strategy_by_name(self, context, strategy_name, eager=False):
"""Return a strategy given its name.
:param context: The security context
:param strategy_name: The name of a strategy
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A strategy
:raises: :py:class:`~.StrategyNotFound`
"""
@@ -217,10 +233,17 @@ class BaseConnection(object):
:raises: :py:class:`~.Invalid`
"""
def soft_delete_strategy(self, strategy_id):
"""Soft delete a strategy.
:param strategy_id: The id or uuid of a strategy.
:raises: :py:class:`~.StrategyNotFound`
"""
@abc.abstractmethod
def get_audit_template_list(self, context, filters=None,
limit=None, marker=None, sort_key=None,
sort_dir=None):
sort_dir=None, eager=False):
"""Get specific columns for matching audit templates.
Return a list of the specified columns for all audit templates that
@@ -234,6 +257,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -250,46 +274,50 @@ class BaseConnection(object):
'uuid': utils.generate_uuid(),
'name': 'example',
'description': 'free text description'
'host_aggregate': 'nova aggregate name or id'
'goal': 'DUMMY'
'extra': {'automatic': True}
}
:returns: An audit template.
:raises: :py:class:`~.AuditTemplateAlreadyExists`
"""
@abc.abstractmethod
def get_audit_template_by_id(self, context, audit_template_id):
def get_audit_template_by_id(self, context, audit_template_id,
eager=False):
"""Return an audit template.
:param context: The security context
:param audit_template_id: The id of an audit template.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An audit template.
:raises: :py:class:`~.AuditTemplateNotFound`
"""
@abc.abstractmethod
def get_audit_template_by_uuid(self, context, audit_template_uuid):
def get_audit_template_by_uuid(self, context, audit_template_uuid,
eager=False):
"""Return an audit template.
:param context: The security context
:param audit_template_uuid: The uuid of an audit template.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An audit template.
: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,
eager=False):
"""Return an audit template.
:param context: The security context
:param audit_template_name: The name of an audit template.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An audit template.
:raises: :py:class:`~.AuditTemplateNotFound`
"""
@abc.abstractmethod
def destroy_audit_template(self, audit_template_id):
"""Destroy an audit_template.
"""Destroy an audit template.
:param audit_template_id: The id or uuid of an audit template.
:raises: :py:class:`~.AuditTemplateNotFound`
@@ -307,7 +335,7 @@ class BaseConnection(object):
@abc.abstractmethod
def soft_delete_audit_template(self, audit_template_id):
"""Soft delete an audit_template.
"""Soft delete an audit template.
:param audit_template_id: The id or uuid of an audit template.
:raises: :py:class:`~.AuditTemplateNotFound`
@@ -315,7 +343,7 @@ class BaseConnection(object):
@abc.abstractmethod
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, eager=False):
"""Get specific columns for matching audits.
Return a list of the specified columns for all audits that match the
@@ -329,6 +357,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -345,28 +374,29 @@ class BaseConnection(object):
{
'uuid': utils.generate_uuid(),
'type': 'ONESHOT',
'deadline': None
}
:returns: An audit.
:raises: :py:class:`~.AuditAlreadyExists`
"""
@abc.abstractmethod
def get_audit_by_id(self, context, audit_id):
def get_audit_by_id(self, context, audit_id, eager=False):
"""Return an audit.
:param context: The security context
:param audit_id: The id of an audit.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An audit.
:raises: :py:class:`~.AuditNotFound`
"""
@abc.abstractmethod
def get_audit_by_uuid(self, context, audit_uuid):
def get_audit_by_uuid(self, context, audit_uuid, eager=False):
"""Return an audit.
:param context: The security context
:param audit_uuid: The uuid of an audit.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An audit.
:raises: :py:class:`~.AuditNotFound`
"""
@@ -393,13 +423,13 @@ class BaseConnection(object):
"""Soft delete an audit and all associated action plans.
:param audit_id: The id or uuid of an audit.
:returns: An audit.
:raises: :py:class:`~.AuditNotFound`
"""
@abc.abstractmethod
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,
eager=False):
"""Get specific columns for matching actions.
Return a list of the specified columns for all actions that match the
@@ -413,6 +443,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -437,21 +468,23 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def get_action_by_id(self, context, action_id):
def get_action_by_id(self, context, action_id, eager=False):
"""Return a action.
:param context: The security context
:param action_id: The id of a action.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A action.
:raises: :py:class:`~.ActionNotFound`
"""
@abc.abstractmethod
def get_action_by_uuid(self, context, action_uuid):
def get_action_by_uuid(self, context, action_uuid, eager=False):
"""Return a action.
:param context: The security context
:param action_uuid: The uuid of a action.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A action.
:raises: :py:class:`~.ActionNotFound`
"""
@@ -476,10 +509,17 @@ class BaseConnection(object):
:raises: :py:class:`~.Invalid`
"""
def soft_delete_action(self, action_id):
"""Soft delete an action.
:param action_id: The id or uuid of an action.
:raises: :py:class:`~.ActionNotFound`
"""
@abc.abstractmethod
def get_action_plan_list(
self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None, eager=False):
"""Get specific columns for matching action plans.
Return a list of the specified columns for all action plans that
@@ -493,6 +533,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -507,21 +548,23 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def get_action_plan_by_id(self, context, action_plan_id):
def get_action_plan_by_id(self, context, action_plan_id, eager=False):
"""Return an action plan.
:param context: The security context
:param action_plan_id: The id of an action plan.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An action plan.
:raises: :py:class:`~.ActionPlanNotFound`
"""
@abc.abstractmethod
def get_action_plan_by_uuid(self, context, action_plan__uuid):
def get_action_plan_by_uuid(self, context, action_plan__uuid, eager=False):
"""Return a action plan.
:param context: The security context
:param action_plan__uuid: The uuid of an action plan.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An action plan.
:raises: :py:class:`~.ActionPlanNotFound`
"""
@@ -546,9 +589,17 @@ class BaseConnection(object):
:raises: :py:class:`~.Invalid`
"""
def soft_delete_action_plan(self, action_plan_id):
"""Soft delete an action plan.
:param action_plan_id: The id or uuid of an action plan.
:raises: :py:class:`~.ActionPlanNotFound`
"""
@abc.abstractmethod
def get_efficacy_indicator_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None,
eager=False):
"""Get specific columns for matching efficacy indicators.
Return a list of the specified columns for all efficacy indicators that
@@ -565,6 +616,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -589,31 +641,37 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id):
def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id,
eager=False):
"""Return an efficacy indicator given its ID.
:param context: The security context
:param efficacy_indicator_id: The ID of an efficacy indicator
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An efficacy indicator
:raises: :py:class:`~.EfficacyIndicatorNotFound`
"""
@abc.abstractmethod
def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid):
def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid,
eager=False):
"""Return an efficacy indicator given its UUID.
:param context: The security context
:param efficacy_indicator_uuid: The UUID of an efficacy indicator
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An efficacy indicator
:raises: :py:class:`~.EfficacyIndicatorNotFound`
"""
@abc.abstractmethod
def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name):
def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name,
eager=False):
"""Return an efficacy indicator given its name.
:param context: The security context
:param efficacy_indicator_name: The name of an efficacy indicator
:param eager: If True, also loads One-to-X data (Default: False)
:returns: An efficacy indicator
:raises: :py:class:`~.EfficacyIndicatorNotFound`
"""
@@ -627,7 +685,7 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def update_efficacy_indicator(self, efficacy_indicator_uuid, values):
def update_efficacy_indicator(self, efficacy_indicator_id, values):
"""Update properties of an efficacy indicator.
:param efficacy_indicator_uuid: The UUID of an efficacy indicator
@@ -639,7 +697,7 @@ class BaseConnection(object):
@abc.abstractmethod
def get_scoring_engine_list(
self, context, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None, eager=False):
"""Get specific columns for matching scoring engines.
Return a list of the specified columns for all scoring engines that
@@ -655,6 +713,7 @@ class BaseConnection(object):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@@ -669,31 +728,37 @@ class BaseConnection(object):
"""
@abc.abstractmethod
def get_scoring_engine_by_id(self, context, scoring_engine_id):
def get_scoring_engine_by_id(self, context, scoring_engine_id,
eager=False):
"""Return a scoring engine by its id.
:param context: The security context
:param scoring_engine_id: The id of a scoring engine.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A scoring engine.
:raises: :py:class:`~.ScoringEngineNotFound`
"""
@abc.abstractmethod
def get_scoring_engine_by_uuid(self, context, scoring_engine_uuid):
def get_scoring_engine_by_uuid(self, context, scoring_engine_uuid,
eager=False):
"""Return a scoring engine by its uuid.
:param context: The security context
:param scoring_engine_uuid: The uuid of a scoring engine.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A scoring engine.
:raises: :py:class:`~.ScoringEngineNotFound`
"""
@abc.abstractmethod
def get_scoring_engine_by_name(self, context, scoring_engine_name):
def get_scoring_engine_by_name(self, context, scoring_engine_name,
eager=False):
"""Return a scoring engine by its name.
:param context: The security context
:param scoring_engine_name: The name of a scoring engine.
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A scoring engine.
:raises: :py:class:`~.ScoringEngineNotFound`
"""
@@ -715,3 +780,92 @@ class BaseConnection(object):
:raises: :py:class:`~.ScoringEngineNotFound`
:raises: :py:class:`~.Invalid`
"""
@abc.abstractmethod
def get_service_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None, eager=False):
"""Get specific columns for matching services.
Return a list of the specified columns for all services that
match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of services 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)
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_service(self, values):
"""Create a new service.
:param values: A dict containing items used to identify
and track the service. For example:
::
{
'id': 1,
'name': 'watcher-api',
'status': 'ACTIVE',
'host': 'controller'
}
:returns: A service
:raises: :py:class:`~.ServiceAlreadyExists`
"""
@abc.abstractmethod
def get_service_by_id(self, context, service_id, eager=False):
"""Return a service given its ID.
:param context: The security context
:param service_id: The ID of a service
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A service
:raises: :py:class:`~.ServiceNotFound`
"""
@abc.abstractmethod
def get_service_by_name(self, context, service_name, eager=False):
"""Return a service given its name.
:param context: The security context
:param service_name: The name of a service
:param eager: If True, also loads One-to-X data (Default: False)
:returns: A service
:raises: :py:class:`~.ServiceNotFound`
"""
@abc.abstractmethod
def destroy_service(self, service_id):
"""Destroy a service.
:param service_id: The ID of a service
:raises: :py:class:`~.ServiceNotFound`
"""
@abc.abstractmethod
def update_service(self, service_id, values):
"""Update properties of a service.
:param service_id: The ID of a service
:returns: A service
:raises: :py:class:`~.ServiceyNotFound`
:raises: :py:class:`~.Invalid`
"""
@abc.abstractmethod
def soft_delete_service(self, service_id):
"""Soft delete a service.
:param service_id: The id of a service.
:returns: A service.
:raises: :py:class:`~.ServiceNotFound`
"""

View File

@@ -24,17 +24,17 @@ from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import session as db_session
from oslo_db.sqlalchemy import utils as db_utils
from oslo_utils import timeutils
from sqlalchemy.inspection import inspect
from sqlalchemy.orm import exc
from sqlalchemy.orm import joinedload
from watcher._i18n import _
from watcher.common import exception
from watcher.common import utils
from watcher.db import api
from watcher.db.sqlalchemy import models
from watcher.objects import action as action_objects
from watcher.objects import action_plan as ap_objects
from watcher.objects import audit as audit_objects
from watcher.objects import utils as objutils
from watcher import objects
CONF = cfg.CONF
@@ -68,7 +68,6 @@ def model_query(model, *args, **kwargs):
:param session: if present, the session to use
"""
session = kwargs.get('session') or get_session()
query = session.query(model, *args)
return query
@@ -133,8 +132,9 @@ class Connection(api.BaseConnection):
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)
if field.type.python_type is datetime.datetime and value:
if not isinstance(value, datetime.datetime):
value = timeutils.parse_isotime(value)
return query.filter(self.valid_operators[operator_](field, value))
@@ -233,8 +233,32 @@ class Connection(api.BaseConnection):
return query
def _get(self, context, model, fieldname, value):
@staticmethod
def _get_relationships(model):
return inspect(model).relationships
@staticmethod
def _set_eager_options(model, query):
relationships = inspect(model).relationships
for relationship in relationships:
if not relationship.uselist:
# We have a One-to-X relationship
query = query.options(joinedload(relationship.key))
return query
def _create(self, model, values):
obj = model()
cleaned_values = {k: v for k, v in values.items()
if k not in self._get_relationships(model)}
obj.update(cleaned_values)
obj.save()
return obj
def _get(self, context, model, fieldname, value, eager):
query = model_query(model)
if eager:
query = self._set_eager_options(model, query)
query = query.filter(getattr(model, fieldname) == value)
if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None))
@@ -246,7 +270,8 @@ class Connection(api.BaseConnection):
return obj
def _update(self, model, id_, values):
@staticmethod
def _update(model, id_, values):
session = get_session()
with session.begin():
query = model_query(model, session=session)
@@ -259,7 +284,8 @@ class Connection(api.BaseConnection):
ref.update(values)
return ref
def _soft_delete(self, model, id_):
@staticmethod
def _soft_delete(model, id_):
session = get_session()
with session.begin():
query = model_query(model, session=session)
@@ -271,7 +297,8 @@ class Connection(api.BaseConnection):
query.soft_delete()
def _destroy(self, model, id_):
@staticmethod
def _destroy(model, id_):
session = get_session()
with session.begin():
query = model_query(model, session=session)
@@ -309,8 +336,7 @@ class Connection(api.BaseConnection):
if filters is None:
filters = {}
plain_fields = ['uuid', 'name', 'host_aggregate',
'goal_id', 'strategy_id']
plain_fields = ['uuid', 'name', 'goal_id', 'strategy_id']
join_fieldmap = JoinMap(
goal_uuid=NaturalJoinFilter(
join_fieldname="uuid", join_model=models.Goal),
@@ -399,10 +425,11 @@ class Connection(api.BaseConnection):
# ### GOALS ### #
def get_goal_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
def get_goal_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None, eager=False):
query = model_query(models.Goal)
if eager:
query = self._set_eager_options(models.Goal, query)
query = self._add_goals_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
@@ -414,30 +441,30 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
goal = models.Goal()
goal.update(values)
try:
goal.save()
goal = self._create(models.Goal, values)
except db_exc.DBDuplicateEntry:
raise exception.GoalAlreadyExists(uuid=values['uuid'])
return goal
def _get_goal(self, context, fieldname, value):
def _get_goal(self, context, fieldname, value, eager):
try:
return self._get(context, model=models.Goal,
fieldname=fieldname, value=value)
fieldname=fieldname, value=value, eager=eager)
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_id(self, context, goal_id, eager=False):
return self._get_goal(
context, fieldname="id", value=goal_id, eager=eager)
def get_goal_by_uuid(self, context, goal_uuid):
return self._get_goal(context, fieldname="uuid", value=goal_uuid)
def get_goal_by_uuid(self, context, goal_uuid, eager=False):
return self._get_goal(
context, fieldname="uuid", value=goal_uuid, eager=eager)
def get_goal_by_name(self, context, goal_name):
return self._get_goal(context, fieldname="name", value=goal_name)
def get_goal_by_name(self, context, goal_name, eager=False):
return self._get_goal(
context, fieldname="name", value=goal_name, eager=eager)
def destroy_goal(self, goal_id):
try:
@@ -464,9 +491,11 @@ class Connection(api.BaseConnection):
# ### STRATEGIES ### #
def get_strategy_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None,
eager=True):
query = model_query(models.Strategy)
if eager:
query = self._set_eager_options(models.Strategy, query)
query = self._add_strategies_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
@@ -478,32 +507,30 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
strategy = models.Strategy()
strategy.update(values)
try:
strategy.save()
strategy = self._create(models.Strategy, values)
except db_exc.DBDuplicateEntry:
raise exception.StrategyAlreadyExists(uuid=values['uuid'])
return strategy
def _get_strategy(self, context, fieldname, value):
def _get_strategy(self, context, fieldname, value, eager):
try:
return self._get(context, model=models.Strategy,
fieldname=fieldname, value=value)
fieldname=fieldname, value=value, eager=eager)
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):
def get_strategy_by_id(self, context, strategy_id, eager=False):
return self._get_strategy(
context, fieldname="uuid", value=strategy_uuid)
context, fieldname="id", value=strategy_id, eager=eager)
def get_strategy_by_name(self, context, strategy_name):
def get_strategy_by_uuid(self, context, strategy_uuid, eager=False):
return self._get_strategy(
context, fieldname="name", value=strategy_name)
context, fieldname="uuid", value=strategy_uuid, eager=eager)
def get_strategy_by_name(self, context, strategy_name, eager=False):
return self._get_strategy(
context, fieldname="name", value=strategy_name, eager=eager)
def destroy_strategy(self, strategy_id):
try:
@@ -530,9 +557,12 @@ class Connection(api.BaseConnection):
# ### AUDIT TEMPLATES ### #
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,
eager=False):
query = model_query(models.AuditTemplate)
if eager:
query = self._set_eager_options(models.AuditTemplate, query)
query = self._add_audit_templates_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
@@ -552,34 +582,34 @@ class Connection(api.BaseConnection):
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
audit_template = models.AuditTemplate()
audit_template.update(values)
try:
audit_template.save()
audit_template = self._create(models.AuditTemplate, values)
except db_exc.DBDuplicateEntry:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
return audit_template
def _get_audit_template(self, context, fieldname, value):
def _get_audit_template(self, context, fieldname, value, eager):
try:
return self._get(context, model=models.AuditTemplate,
fieldname=fieldname, value=value)
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.AuditTemplateNotFound(audit_template=value)
def get_audit_template_by_id(self, context, audit_template_id):
def get_audit_template_by_id(self, context, audit_template_id,
eager=False):
return self._get_audit_template(
context, fieldname="id", value=audit_template_id)
context, fieldname="id", value=audit_template_id, eager=eager)
def get_audit_template_by_uuid(self, context, audit_template_uuid):
def get_audit_template_by_uuid(self, context, audit_template_uuid,
eager=False):
return self._get_audit_template(
context, fieldname="uuid", value=audit_template_uuid)
context, fieldname="uuid", value=audit_template_uuid, eager=eager)
def get_audit_template_by_name(self, context, audit_template_name):
def get_audit_template_by_name(self, context, audit_template_name,
eager=False):
return self._get_audit_template(
context, fieldname="name", value=audit_template_name)
context, fieldname="name", value=audit_template_name, eager=eager)
def destroy_audit_template(self, audit_template_id):
try:
@@ -610,12 +640,14 @@ class Connection(api.BaseConnection):
# ### AUDITS ### #
def get_audit_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, eager=False):
query = model_query(models.Audit)
if eager:
query = self._set_eager_options(models.Audit, query)
query = self._add_audits_filters(query, filters)
if not context.show_deleted:
query = query.filter(
~(models.Audit.state == audit_objects.State.DELETED))
~(models.Audit.state == objects.audit.State.DELETED))
return _paginate_query(models.Audit, limit, marker,
sort_key, sort_dir, query)
@@ -626,41 +658,28 @@ class Connection(api.BaseConnection):
values['uuid'] = utils.generate_uuid()
if values.get('state') is None:
values['state'] = audit_objects.State.PENDING
audit = models.Audit()
audit.update(values)
values['state'] = objects.audit.State.PENDING
try:
audit.save()
audit = self._create(models.Audit, values)
except db_exc.DBDuplicateEntry:
raise exception.AuditAlreadyExists(uuid=values['uuid'])
return audit
def get_audit_by_id(self, context, audit_id):
query = model_query(models.Audit)
query = query.filter_by(id=audit_id)
def _get_audit(self, context, fieldname, value, eager):
try:
audit = query.one()
if not context.show_deleted:
if audit.state == audit_objects.State.DELETED:
raise exception.AuditNotFound(audit=audit_id)
return audit
except exc.NoResultFound:
raise exception.AuditNotFound(audit=audit_id)
return self._get(context, model=models.Audit,
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.AuditNotFound(audit=value)
def get_audit_by_uuid(self, context, audit_uuid):
query = model_query(models.Audit)
query = query.filter_by(uuid=audit_uuid)
def get_audit_by_id(self, context, audit_id, eager=False):
return self._get_audit(
context, fieldname="id", value=audit_id, eager=eager)
try:
audit = query.one()
if not context.show_deleted:
if audit.state == audit_objects.State.DELETED:
raise exception.AuditNotFound(audit=audit_uuid)
return audit
except exc.NoResultFound:
raise exception.AuditNotFound(audit=audit_uuid)
def get_audit_by_uuid(self, context, audit_uuid, eager=False):
return self._get_audit(
context, fieldname="uuid", value=audit_uuid, eager=eager)
def destroy_audit(self, audit_id):
def is_audit_referenced(session, audit_id):
@@ -705,12 +724,14 @@ class Connection(api.BaseConnection):
# ### ACTIONS ### #
def get_action_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, eager=False):
query = model_query(models.Action)
if eager:
query = self._set_eager_options(models.Action, query)
query = self._add_actions_filters(query, filters)
if not context.show_deleted:
query = query.filter(
~(models.Action.state == action_objects.State.DELETED))
~(models.Action.state == objects.action.State.DELETED))
return _paginate_query(models.Action, limit, marker,
sort_key, sort_dir, query)
@@ -719,39 +740,26 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
action = models.Action()
action.update(values)
try:
action.save()
action = self._create(models.Action, values)
except db_exc.DBDuplicateEntry:
raise exception.ActionAlreadyExists(uuid=values['uuid'])
return action
def get_action_by_id(self, context, action_id):
query = model_query(models.Action)
query = query.filter_by(id=action_id)
def _get_action(self, context, fieldname, value, eager):
try:
action = query.one()
if not context.show_deleted:
if action.state == action_objects.State.DELETED:
raise exception.ActionNotFound(
action=action_id)
return action
except exc.NoResultFound:
raise exception.ActionNotFound(action=action_id)
return self._get(context, model=models.Action,
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.ActionNotFound(action=value)
def get_action_by_uuid(self, context, action_uuid):
query = model_query(models.Action)
query = query.filter_by(uuid=action_uuid)
try:
action = query.one()
if not context.show_deleted:
if action.state == action_objects.State.DELETED:
raise exception.ActionNotFound(
action=action_uuid)
return action
except exc.NoResultFound:
raise exception.ActionNotFound(action=action_uuid)
def get_action_by_id(self, context, action_id, eager=False):
return self._get_action(
context, fieldname="id", value=action_id, eager=eager)
def get_action_by_uuid(self, context, action_uuid, eager=False):
return self._get_action(
context, fieldname="uuid", value=action_uuid, eager=eager)
def destroy_action(self, action_id):
session = get_session()
@@ -766,12 +774,12 @@ class Connection(api.BaseConnection):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing "
"Action."))
message=_("Cannot overwrite UUID for an existing Action."))
return self._do_update_action(action_id, values)
def _do_update_action(self, action_id, values):
@staticmethod
def _do_update_action(action_id, values):
session = get_session()
with session.begin():
query = model_query(models.Action, session=session)
@@ -800,13 +808,16 @@ class Connection(api.BaseConnection):
# ### ACTION PLANS ### #
def get_action_plan_list(
self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None, eager=False):
query = model_query(models.ActionPlan)
if eager:
query = self._set_eager_options(models.ActionPlan, query)
query = self._add_action_plans_filters(query, filters)
if not context.show_deleted:
query = query.filter(
~(models.ActionPlan.state == ap_objects.State.DELETED))
~(models.ActionPlan.state ==
objects.action_plan.State.DELETED))
return _paginate_query(models.ActionPlan, limit, marker,
sort_key, sort_dir, query)
@@ -816,41 +827,26 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
action_plan = models.ActionPlan()
action_plan.update(values)
try:
action_plan.save()
action_plan = self._create(models.ActionPlan, values)
except db_exc.DBDuplicateEntry:
raise exception.ActionPlanAlreadyExists(uuid=values['uuid'])
return action_plan
def get_action_plan_by_id(self, context, action_plan_id):
query = model_query(models.ActionPlan)
query = query.filter_by(id=action_plan_id)
def _get_action_plan(self, context, fieldname, value, eager):
try:
action_plan = query.one()
if not context.show_deleted:
if action_plan.state == ap_objects.State.DELETED:
raise exception.ActionPlanNotFound(
action_plan=action_plan_id)
return action_plan
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
return self._get(context, model=models.ActionPlan,
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.ActionPlanNotFound(action_plan=value)
def get_action_plan_by_uuid(self, context, action_plan__uuid):
query = model_query(models.ActionPlan)
query = query.filter_by(uuid=action_plan__uuid)
def get_action_plan_by_id(self, context, action_plan_id, eager=False):
return self._get_action_plan(
context, fieldname="id", value=action_plan_id, eager=eager)
try:
action_plan = query.one()
if not context.show_deleted:
if action_plan.state == ap_objects.State.DELETED:
raise exception.ActionPlanNotFound(
action_plan=action_plan__uuid)
return action_plan
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan__uuid)
def get_action_plan_by_uuid(self, context, action_plan_uuid, eager=False):
return self._get_action_plan(
context, fieldname="uuid", value=action_plan_uuid, eager=eager)
def destroy_action_plan(self, action_plan_id):
def is_action_plan_referenced(session, action_plan_id):
@@ -884,7 +880,8 @@ class Connection(api.BaseConnection):
return self._do_update_action_plan(action_plan_id, values)
def _do_update_action_plan(self, action_plan_id, values):
@staticmethod
def _do_update_action_plan(action_plan_id, values):
session = get_session()
with session.begin():
query = model_query(models.ActionPlan, session=session)
@@ -913,9 +910,12 @@ class Connection(api.BaseConnection):
# ### EFFICACY INDICATORS ### #
def get_efficacy_indicator_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
marker=None, sort_key=None, sort_dir=None,
eager=False):
query = model_query(models.EfficacyIndicator)
if eager:
query = self._set_eager_options(models.EfficacyIndicator, query)
query = self._add_efficacy_indicators_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
@@ -927,33 +927,36 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
efficacy_indicator = models.EfficacyIndicator()
efficacy_indicator.update(values)
try:
efficacy_indicator.save()
efficacy_indicator = self._create(models.EfficacyIndicator, values)
except db_exc.DBDuplicateEntry:
raise exception.EfficacyIndicatorAlreadyExists(uuid=values['uuid'])
return efficacy_indicator
def _get_efficacy_indicator(self, context, fieldname, value):
def _get_efficacy_indicator(self, context, fieldname, value, eager):
try:
return self._get(context, model=models.EfficacyIndicator,
fieldname=fieldname, value=value)
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.EfficacyIndicatorNotFound(efficacy_indicator=value)
def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id):
def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id,
eager=False):
return self._get_efficacy_indicator(
context, fieldname="id", value=efficacy_indicator_id)
context, fieldname="id",
value=efficacy_indicator_id, eager=eager)
def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid):
def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid,
eager=False):
return self._get_efficacy_indicator(
context, fieldname="uuid", value=efficacy_indicator_uuid)
context, fieldname="uuid",
value=efficacy_indicator_uuid, eager=eager)
def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name):
def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name,
eager=False):
return self._get_efficacy_indicator(
context, fieldname="name", value=efficacy_indicator_name)
context, fieldname="name",
value=efficacy_indicator_name, eager=eager)
def update_efficacy_indicator(self, efficacy_indicator_id, values):
if 'uuid' in values:
@@ -996,9 +999,11 @@ class Connection(api.BaseConnection):
plain_fields=plain_fields)
def get_scoring_engine_list(
self, context, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
self, context, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None, eager=False):
query = model_query(models.ScoringEngine)
if eager:
query = self._set_eager_options(models.ScoringEngine, query)
query = self._add_scoring_engine_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
@@ -1011,33 +1016,33 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
scoring_engine = models.ScoringEngine()
scoring_engine.update(values)
try:
scoring_engine.save()
scoring_engine = self._create(models.ScoringEngine, values)
except db_exc.DBDuplicateEntry:
raise exception.ScoringEngineAlreadyExists(uuid=values['uuid'])
return scoring_engine
def _get_scoring_engine(self, context, fieldname, value):
def _get_scoring_engine(self, context, fieldname, value, eager):
try:
return self._get(context, model=models.ScoringEngine,
fieldname=fieldname, value=value)
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.ScoringEngineNotFound(scoring_engine=value)
def get_scoring_engine_by_id(self, context, scoring_engine_id):
def get_scoring_engine_by_id(self, context, scoring_engine_id,
eager=False):
return self._get_scoring_engine(
context, fieldname="id", value=scoring_engine_id)
context, fieldname="id", value=scoring_engine_id, eager=eager)
def get_scoring_engine_by_uuid(self, context, scoring_engine_uuid):
def get_scoring_engine_by_uuid(self, context, scoring_engine_uuid,
eager=False):
return self._get_scoring_engine(
context, fieldname="uuid", value=scoring_engine_uuid)
context, fieldname="uuid", value=scoring_engine_uuid, eager=eager)
def get_scoring_engine_by_name(self, context, scoring_engine_name):
def get_scoring_engine_by_name(self, context, scoring_engine_name,
eager=False):
return self._get_scoring_engine(
context, fieldname="name", value=scoring_engine_name)
context, fieldname="name", value=scoring_engine_name, eager=eager)
def destroy_scoring_engine(self, scoring_engine_id):
try:
@@ -1047,9 +1052,9 @@ class Connection(api.BaseConnection):
scoring_engine=scoring_engine_id)
def update_scoring_engine(self, scoring_engine_id, values):
if 'id' in values:
if 'uuid' in values:
raise exception.Invalid(
message=_("Cannot overwrite ID for an existing "
message=_("Cannot overwrite UUID for an existing "
"Scoring Engine."))
try:
@@ -1065,3 +1070,67 @@ class Connection(api.BaseConnection):
except exception.ResourceNotFound:
raise exception.ScoringEngineNotFound(
scoring_engine=scoring_engine_id)
# ### SERVICES ### #
def _add_services_filters(self, query, filters):
if not filters:
filters = {}
plain_fields = ['id', 'name', 'host']
return self._add_filters(
query=query, model=models.Service, filters=filters,
plain_fields=plain_fields)
def get_service_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None, eager=False):
query = model_query(models.Service)
if eager:
query = self._set_eager_options(models.Service, query)
query = self._add_services_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
return _paginate_query(models.Service, limit, marker,
sort_key, sort_dir, query)
def create_service(self, values):
try:
service = self._create(models.Service, values)
except db_exc.DBDuplicateEntry:
raise exception.ServiceAlreadyExists(name=values['name'],
host=values['host'])
return service
def _get_service(self, context, fieldname, value, eager):
try:
return self._get(context, model=models.Service,
fieldname=fieldname, value=value, eager=eager)
except exception.ResourceNotFound:
raise exception.ServiceNotFound(service=value)
def get_service_by_id(self, context, service_id, eager=False):
return self._get_service(
context, fieldname="id", value=service_id, eager=eager)
def get_service_by_name(self, context, service_name, eager=False):
return self._get_service(
context, fieldname="name", value=service_name, eager=eager)
def destroy_service(self, service_id):
try:
return self._destroy(models.Service, service_id)
except exception.ResourceNotFound:
raise exception.ServiceNotFound(service=service_id)
def update_service(self, service_id, values):
try:
return self._update(models.Service, service_id, values)
except exception.ResourceNotFound:
raise exception.ServiceNotFound(service=service_id)
def soft_delete_service(self, service_id):
try:
self._soft_delete(models.Service, service_id)
except exception.ResourceNotFound:
raise exception.ServiceNotFound(service=service_id)

View File

@@ -27,14 +27,15 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import Numeric
from sqlalchemy import schema
from sqlalchemy import orm
from sqlalchemy import String
from sqlalchemy import Text
from sqlalchemy.types import TypeDecorator, TEXT
from sqlalchemy import UniqueConstraint
from watcher.common import paths
sql_opts = [
SQL_OPTS = [
cfg.StrOpt('mysql_engine',
default='InnoDB',
help='MySQL engine to use.')
@@ -43,7 +44,7 @@ sql_opts = [
_DEFAULT_SQL_CONNECTION = 'sqlite:///{0}'.format(
paths.state_path_def('watcher.sqlite'))
cfg.CONF.register_opts(sql_opts, 'database')
cfg.CONF.register_opts(SQL_OPTS, 'database')
db_options.set_defaults(cfg.CONF, _DEFAULT_SQL_CONNECTION, 'watcher.sqlite')
@@ -57,6 +58,7 @@ def table_args():
class JsonEncodedType(TypeDecorator):
"""Abstract base type serialized as json-encoded string in db."""
type = None
impl = TEXT
@@ -81,11 +83,13 @@ class JsonEncodedType(TypeDecorator):
class JSONEncodedDict(JsonEncodedType):
"""Represents dict serialized as json-encoded string in db."""
type = dict
class JSONEncodedList(JsonEncodedType):
"""Represents list serialized as json-encoded string in db."""
type = list
@@ -111,35 +115,39 @@ class WatcherBase(models.SoftDeleteMixin,
Base = declarative_base(cls=WatcherBase)
class Goal(Base):
"""Represents a goal."""
__tablename__ = 'goals'
__table_args__ = (
UniqueConstraint('uuid', name='uniq_goals0uuid'),
UniqueConstraint('name', 'deleted', name='uniq_goals0name'),
table_args(),
)
id = Column(Integer, primary_key=True, autoincrement=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 Strategy(Base):
"""Represents a strategy."""
__tablename__ = 'strategies'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_strategies0uuid'),
UniqueConstraint('uuid', name='uniq_strategies0uuid'),
UniqueConstraint('name', 'deleted', name='uniq_strategies0name'),
table_args()
)
id = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True, autoincrement=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)
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
class AuditTemplate(Base):
@@ -147,18 +155,20 @@ class AuditTemplate(Base):
__tablename__ = 'audit_templates'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
UniqueConstraint('uuid', name='uniq_audit_templates0uuid'),
UniqueConstraint('name', 'deleted', name='uniq_audit_templates0name'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(63), nullable=True)
description = Column(String(255), nullable=True)
host_aggregate = Column(Integer, nullable=True)
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
extra = Column(JSONEncodedDict)
version = Column(String(15), nullable=True)
scope = Column(JSONEncodedList)
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
class Audit(Base):
@@ -166,19 +176,41 @@ class Audit(Base):
__tablename__ = 'audits'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_audits0uuid'),
UniqueConstraint('uuid', name='uniq_audits0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36))
audit_type = Column(String(20))
state = Column(String(20), nullable=True)
deadline = Column(DateTime, nullable=True)
parameters = Column(JSONEncodedDict, nullable=True)
interval = Column(Integer, nullable=True)
host_aggregate = Column(Integer, nullable=True)
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
scope = Column(JSONEncodedList, nullable=True)
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
class ActionPlan(Base):
"""Represents an action plan."""
__tablename__ = 'action_plans'
__table_args__ = (
UniqueConstraint('uuid', name='uniq_action_plans0uuid'),
table_args()
)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36))
first_action_id = Column(Integer)
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
state = Column(String(20), nullable=True)
global_efficacy = Column(JSONEncodedDict, nullable=True)
audit = orm.relationship(Audit, foreign_keys=audit_id, lazy=None)
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
class Action(Base):
@@ -186,10 +218,10 @@ class Action(Base):
__tablename__ = 'actions'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_actions0uuid'),
UniqueConstraint('uuid', name='uniq_actions0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36), nullable=False)
action_plan_id = Column(Integer, ForeignKey('action_plans.id'),
nullable=False)
@@ -199,22 +231,8 @@ class Action(Base):
state = Column(String(20), nullable=True)
next = Column(String(36), nullable=True)
class ActionPlan(Base):
"""Represents an action plan."""
__tablename__ = 'action_plans'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_action_plans0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
first_action_id = Column(Integer)
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
state = Column(String(20), nullable=True)
global_efficacy = Column(JSONEncodedDict, nullable=True)
action_plan = orm.relationship(
ActionPlan, foreign_keys=action_plan_id, lazy=None)
class EfficacyIndicator(Base):
@@ -222,10 +240,10 @@ class EfficacyIndicator(Base):
__tablename__ = 'efficacy_indicators'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_efficacy_indicators0uuid'),
UniqueConstraint('uuid', name='uniq_efficacy_indicators0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36))
name = Column(String(63))
description = Column(String(255), nullable=True)
@@ -234,16 +252,20 @@ class EfficacyIndicator(Base):
action_plan_id = Column(Integer, ForeignKey('action_plans.id'),
nullable=False)
action_plan = orm.relationship(
ActionPlan, foreign_keys=action_plan_id, lazy=None)
class ScoringEngine(Base):
"""Represents a scoring engine."""
__tablename__ = 'scoring_engines'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_scoring_engines0uuid'),
UniqueConstraint('uuid', name='uniq_scoring_engines0uuid'),
UniqueConstraint('name', 'deleted', name='uniq_scoring_engines0name'),
table_args()
)
id = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36), nullable=False)
name = Column(String(63), nullable=False)
description = Column(String(255), nullable=True)
@@ -251,3 +273,18 @@ class ScoringEngine(Base):
# The format might vary between different models (e.g. be JSON, XML or
# even some custom format), the blob type should cover all scenarios.
metainfo = Column(Text, nullable=True)
class Service(Base):
"""Represents a service entity"""
__tablename__ = 'services'
__table_args__ = (
UniqueConstraint('host', 'name', 'deleted',
name="uniq_services0host0name0deleted"),
table_args()
)
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
host = Column(String(255), nullable=False)
last_seen_up = Column(DateTime, nullable=True)

View File

@@ -22,17 +22,18 @@ 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
from watcher import notifications
from watcher import objects
from watcher.objects import fields
LOG = log.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class BaseAuditHandler(object):
@abc.abstractmethod
def execute(self, audit_uuid, request_context):
raise NotImplementedError()
@@ -72,32 +73,40 @@ class AuditHandler(BaseAuditHandler):
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.publish_status_event(event.type.name, payload)
def do_schedule(self, request_context, audit, solution):
try:
notifications.audit.send_action_notification(
request_context, audit,
action=fields.NotificationAction.PLANNER,
phase=fields.NotificationPhase.START)
self.planner.schedule(request_context, audit.id, solution)
notifications.audit.send_action_notification(
request_context, audit,
action=fields.NotificationAction.PLANNER,
phase=fields.NotificationPhase.END)
except Exception:
notifications.audit.send_action_notification(
request_context, audit,
action=fields.NotificationAction.PLANNER,
priority=fields.NotificationPriority.ERROR,
phase=fields.NotificationPhase.ERROR)
raise
def update_audit_state(self, request_context, audit, state):
@staticmethod
def update_audit_state(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)
self.update_audit_state(audit, objects.audit.State.ONGOING)
def post_execute(self, audit, solution, request_context):
self.planner.schedule(request_context, audit.id, solution)
self.do_schedule(request_context, audit, solution)
# change state of the audit to SUCCEEDED
self.update_audit_state(request_context, audit,
audit_objects.State.SUCCEEDED)
self.update_audit_state(audit, objects.audit.State.SUCCEEDED)
def execute(self, audit, request_context):
try:
@@ -106,5 +115,4 @@ class AuditHandler(BaseAuditHandler):
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)
self.update_audit_state(audit, objects.audit.State.FAILED)

View File

@@ -25,8 +25,7 @@ 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
from watcher import objects
CONF = cfg.CONF
@@ -56,11 +55,11 @@ class ContinuousAuditHandler(base.AuditHandler):
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):
audit = objects.Audit.get_by_uuid(
self.context_show_deleted, audit.uuid)
if audit.state in (objects.audit.State.CANCELLED,
objects.audit.State.DELETED,
objects.audit.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
@@ -77,14 +76,13 @@ class ContinuousAuditHandler(base.AuditHandler):
solution = self.strategy_context.execute_strategy(
audit, request_context)
if audit.audit_type == audit_objects.AuditType.CONTINUOUS.value:
if audit.audit_type == objects.audit.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)
'state': objects.action_plan.State.RECOMMENDED}
action_plans = objects.ActionPlan.list(
request_context, filters=a_plan_filters)
for plan in action_plans:
plan.state = action_objects.State.CANCELLED
plan.state = objects.action_plan.State.CANCELLED
plan.save()
return solution
@@ -93,18 +91,18 @@ class ContinuousAuditHandler(base.AuditHandler):
self.execute(audit, request_context)
def post_execute(self, audit, solution, request_context):
self.planner.schedule(request_context, audit.id, solution)
self.do_schedule(request_context, audit, 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)
'audit_type': objects.audit.AuditType.CONTINUOUS.value,
'state__in': (objects.audit.State.PENDING,
objects.audit.State.ONGOING,
objects.audit.State.SUCCEEDED)
}
audits = audit_objects.Audit.list(audit_context,
filters=audit_filters)
audits = objects.Audit.list(
audit_context, filters=audit_filters, eager=True)
scheduler_job_args = [job.args for job in self.scheduler.get_jobs()
if job.name == 'execute_audit']
for audit in audits:

View File

@@ -59,7 +59,7 @@ in any appropriate storage system (InfluxDB, OpenTSDB, MongoDB,...).
import abc
import six
""" Work in progress Helper to query metrics """
"""Work in progress Helper to query metrics"""
@six.add_metaclass(abc.ABCMeta)

View File

@@ -37,9 +37,9 @@ class ServerConsolidation(base.EfficacySpecification):
indicators.InstanceMigrationsCount(),
]
def get_global_efficacy_indicator(self, indicators_map):
def get_global_efficacy_indicator(self, indicators_map=None):
value = 0
if indicators_map.instance_migrations_count > 0:
if indicators_map and indicators_map.instance_migrations_count > 0:
value = (float(indicators_map.released_compute_nodes_count) /
float(indicators_map.instance_migrations_count)) * 100

View File

@@ -38,6 +38,7 @@ See :doc:`../architecture` for more details on this component.
from oslo_config import cfg
from watcher.common import service_manager
from watcher.decision_engine.messaging import audit_endpoint
from watcher.decision_engine.model.collector import manager
@@ -50,13 +51,6 @@ WATCHER_DECISION_ENGINE_OPTS = [
help='The topic name used for '
'control events, this topic '
'used for RPC calls'),
cfg.StrOpt('status_topic',
default='watcher.decision.status',
help='The topic name used for '
'status events; this topic '
'is used so as to notify'
'the others components '
'of the system'),
cfg.ListOpt('notification_topics',
default=['versioned_notifications', 'watcher_notifications'],
help='The topic names from which notification events '
@@ -78,23 +72,36 @@ CONF.register_group(decision_engine_opt_group)
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
class DecisionEngineManager(object):
class DecisionEngineManager(service_manager.ServiceManager):
API_VERSION = '1.0'
@property
def service_name(self):
return 'watcher-decision-engine'
def __init__(self):
self.api_version = self.API_VERSION
@property
def api_version(self):
return '1.0'
self.publisher_id = CONF.watcher_decision_engine.publisher_id
self.conductor_topic = CONF.watcher_decision_engine.conductor_topic
self.status_topic = CONF.watcher_decision_engine.status_topic
self.notification_topics = (
CONF.watcher_decision_engine.notification_topics)
@property
def publisher_id(self):
return CONF.watcher_decision_engine.publisher_id
self.conductor_endpoints = [audit_endpoint.AuditEndpoint]
@property
def conductor_topic(self):
return CONF.watcher_decision_engine.conductor_topic
self.status_endpoints = []
@property
def notification_topics(self):
return CONF.watcher_decision_engine.notification_topics
self.collector_manager = manager.CollectorManager()
self.notification_endpoints = (
self.collector_manager.get_notification_endpoints())
@property
def conductor_endpoints(self):
return [audit_endpoint.AuditEndpoint]
@property
def notification_endpoints(self):
return self.collector_manager.get_notification_endpoints()
@property
def collector_manager(self):
return manager.CollectorManager()

View File

@@ -23,7 +23,7 @@ from oslo_log import log
from watcher.decision_engine.audit import continuous as continuous_handler
from watcher.decision_engine.audit import oneshot as oneshot_handler
from watcher.objects import audit as audit_objects
from watcher import objects
CONF = cfg.CONF
LOG = log.getLogger(__name__)
@@ -49,7 +49,7 @@ class AuditEndpoint(object):
return self._messaging
def do_trigger_audit(self, context, audit_uuid):
audit = audit_objects.Audit.get_by_uuid(context, audit_uuid)
audit = objects.Audit.get_by_uuid(context, audit_uuid, eager=True)
self._oneshot_handler.execute(audit, context)
def trigger_audit(self, context, audit_uuid):

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
# Copyright (c) 2016 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
# Authors: Vincent FRANCOISE <Vincent.FRANCOISE@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,11 +15,22 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import enum
"""
This component is in charge of executing the
:ref:`Action Plan <action_plan_definition>` built by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
See: :doc:`../architecture` for more details on this component.
"""
import abc
import six
class EventTypes(enum.Enum):
LAUNCH_ACTION_PLAN = "launch_action_plan"
LAUNCH_ACTION = "launch_action"
@six.add_metaclass(abc.ABCMeta)
class Model(object):
@abc.abstractmethod
def to_string(self):
raise NotImplementedError()

View File

@@ -19,8 +19,8 @@
#
"""
A :ref:`Cluster Data Model <cluster_data_model_definition>` is a logical
representation of the current state and topology of the :ref:`Cluster
A :ref:`Cluster Data Model <cluster_data_model_definition>` (or CDM) is a
logical representation of the current state and topology of the :ref:`Cluster
<cluster_definition>` :ref:`Managed resources <managed_resource_definition>`.
It is represented as a set of :ref:`Managed resources
@@ -31,9 +31,8 @@ to know the current relationships between the different :ref:`resources
during an :ref:`Audit <audit_definition>` and enables the :ref:`Strategy
<strategy_definition>` to request information such as:
- What compute nodes are in a given :ref:`Availability Zone
<availability_zone_definition>` or a given :ref:`Host Aggregate
<host_aggregates_definition>`?
- What compute nodes are in a given :ref:`Audit Scope
<audit_scope_definition>`?
- What :ref:`Instances <instance_definition>` are hosted on a given compute
node?
- What is the current load of a compute node?
@@ -59,8 +58,11 @@ to know:
In the Watcher project, we aim at providing a some generic and basic
:ref:`Cluster Data Model <cluster_data_model_definition>` for each :ref:`Goal
<goal_definition>`, usable in the associated :ref:`Strategies
<strategy_definition>` through a plugin-based mechanism that are directly
accessible from the strategies classes in order to:
<strategy_definition>` through a plugin-based mechanism which are called
cluster data model collectors (or CDMCs). These CDMCs are responsible for
loading and keeping up-to-date their associated CDM by listening to events and
also periodically rebuilding themselves from the ground up. They are also
directly accessible from the strategies classes. These CDMs are used to:
- simplify the development of a new :ref:`Strategy <strategy_definition>` for a
given :ref:`Goal <goal_definition>` when there already are some existing

View File

@@ -28,16 +28,10 @@ LOG = log.getLogger(__name__)
class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"""nova
"""Nova cluster data model collector
*Description*
This Nova cluster data model collector creates an in-memory representation
of the resources exposed by the compute service.
*Spec URL*
<None>
The Nova cluster data model collector creates an in-memory
representation of the resources exposed by the compute service.
"""
def __init__(self, config, osc=None):

View File

@@ -14,8 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import threading
from oslo_concurrency import lockutils
from oslo_log import log
from watcher._i18n import _LW
@@ -28,7 +27,6 @@ class Mapping(object):
self.model = model
self.compute_node_mapping = {}
self.instance_mapping = {}
self.lock = threading.Lock()
def map(self, node, instance):
"""Select the node where the instance is launched
@@ -36,9 +34,7 @@ class Mapping(object):
:param node: the node
:param instance: the virtual machine or instance
"""
try:
self.lock.acquire()
with lockutils.lock(__name__):
# init first
if node.uuid not in self.compute_node_mapping.keys():
self.compute_node_mapping[node.uuid] = set()
@@ -49,9 +45,6 @@ class Mapping(object):
# map instance => node
self.instance_mapping[instance.uuid] = node.uuid
finally:
self.lock.release()
def unmap(self, node, instance):
"""Remove the instance from the node
@@ -65,8 +58,7 @@ class Mapping(object):
:rtype : object
"""
try:
self.lock.acquire()
with lockutils.lock(__name__):
if str(node_uuid) in self.compute_node_mapping:
self.compute_node_mapping[str(node_uuid)].remove(
str(instance_uuid))
@@ -77,8 +69,6 @@ class Mapping(object):
_LW("Trying to delete the instance %(instance)s but it "
"was not found on node %(node)s") %
{'instance': instance_uuid, 'node': node_uuid})
finally:
self.lock.release()
def get_mapping(self):
return self.compute_node_mapping

View File

@@ -22,11 +22,13 @@ import six
from watcher._i18n import _
from watcher.common import exception
from watcher.common import utils
from watcher.decision_engine.model import base
from watcher.decision_engine.model import element
from watcher.decision_engine.model import mapping
class ModelRoot(object):
class ModelRoot(base.Model):
def __init__(self, stale=False):
self._nodes = utils.Struct()
self._instances = utils.Struct()
@@ -223,3 +225,53 @@ class ModelRoot(object):
root.append(self._build_instance_element(instance))
return etree.tostring(root, pretty_print=True).decode('utf-8')
@classmethod
def from_xml(cls, data):
model = cls()
root = etree.fromstring(data)
mem = element.Resource(element.ResourceType.memory)
num_cores = element.Resource(element.ResourceType.cpu_cores)
disk = element.Resource(element.ResourceType.disk)
disk_capacity = element.Resource(element.ResourceType.disk_capacity)
model.create_resource(mem)
model.create_resource(num_cores)
model.create_resource(disk)
model.create_resource(disk_capacity)
for cn in root.findall('.//ComputeNode'):
node = element.ComputeNode(cn.get('id'))
node.uuid = cn.get('uuid')
node.hostname = cn.get('hostname')
# set capacity
mem.set_capacity(node, int(cn.get(str(mem.name))))
disk.set_capacity(node, int(cn.get(str(disk.name))))
disk_capacity.set_capacity(
node, int(cn.get(str(disk_capacity.name))))
num_cores.set_capacity(node, int(cn.get(str(num_cores.name))))
node.state = cn.get('state')
node.status = cn.get('status')
model.add_node(node)
for inst in root.findall('.//Instance'):
instance = element.Instance()
instance.uuid = inst.get('uuid')
instance.state = inst.get('state')
mem.set_capacity(instance, int(inst.get(str(mem.name))))
disk.set_capacity(instance, int(inst.get(str(disk.name))))
disk_capacity.set_capacity(
instance, int(inst.get(str(disk_capacity.name))))
num_cores.set_capacity(
instance, int(inst.get(str(num_cores.name))))
parent = inst.getparent()
if parent.tag == 'ComputeNode':
node = model.get_node_by_uuid(parent.get('uuid'))
model.map_instance(instance, node)
else:
model.add_instance(instance)
return model

View File

@@ -19,8 +19,6 @@
import abc
import six
from watcher.common import rpc
@six.add_metaclass(abc.ABCMeta)
class NotificationEndpoint(object):
@@ -38,10 +36,3 @@ class NotificationEndpoint(object):
@property
def cluster_data_model(self):
return self.collector.cluster_data_model
@property
def notifier(self):
if self._notifier is None:
self._notifier = rpc.get_notifier('decision-engine')
return self._notifier

View File

@@ -128,7 +128,7 @@ class DefaultPlanner(base.BasePlanner):
}
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
new_action_plan.create(context)
new_action_plan.create()
return new_action_plan
@@ -145,7 +145,7 @@ class DefaultPlanner(base.BasePlanner):
}
new_efficacy_indicator = objects.EfficacyIndicator(
context, **efficacy_indicator_dict)
new_efficacy_indicator.create(context)
new_efficacy_indicator.create()
efficacy_indicators.append(new_efficacy_indicator)
return efficacy_indicators
@@ -156,7 +156,7 @@ class DefaultPlanner(base.BasePlanner):
_action.get("action_type"))
new_action = objects.Action(context, **_action)
new_action.create(context)
new_action.create()
new_action.save()
if parent_action:

View File

@@ -20,8 +20,8 @@
from oslo_config import cfg
from watcher.common import exception
from watcher.common.messaging import notification_handler
from watcher.common import service
from watcher.common import service_manager
from watcher.common import utils
from watcher.decision_engine import manager
@@ -43,20 +43,35 @@ class DecisionEngineAPI(service.Service):
raise exception.InvalidUuidOrName(name=audit_uuid)
return self.conductor_client.call(
context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid)
context, 'trigger_audit', audit_uuid=audit_uuid)
class DecisionEngineAPIManager(object):
class DecisionEngineAPIManager(service_manager.ServiceManager):
API_VERSION = '1.0'
@property
def service_name(self):
return None
conductor_endpoints = []
status_endpoints = [notification_handler.NotificationHandler]
notification_endpoints = []
notification_topics = []
@property
def api_version(self):
return '1.0'
def __init__(self):
self.publisher_id = CONF.watcher_decision_engine.publisher_id
self.conductor_topic = CONF.watcher_decision_engine.conductor_topic
self.status_topic = CONF.watcher_decision_engine.status_topic
self.api_version = self.API_VERSION
@property
def publisher_id(self):
return CONF.watcher_decision_engine.publisher_id
@property
def conductor_topic(self):
return CONF.watcher_decision_engine.conductor_topic
@property
def notification_topics(self):
return []
@property
def conductor_endpoints(self):
return []
@property
def notification_endpoints(self):
return []

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