Compare commits

..

102 Commits

Author SHA1 Message Date
Jenkins
46d5094add Merge "Added information on plugin mechanism to glossary" 2016-04-05 07:52:55 +00:00
Jenkins
783c7c0177 Merge "Disabled PATCH, POST and DELETE for /actions" 2016-04-05 07:30:23 +00:00
Jenkins
6d0717199c Merge "Invalid states for Action Plan in the glossary" 2016-04-05 07:27:12 +00:00
cima
8b77e78f3d Added missing support for resource states in unicode format in VM workload consolidation strategy
Unicode type resource state is now handled in the same fashion as resource state specified by general string.

Change-Id: I35ffa09015283b51c935515436735aecbe83a9d6
Closes-Bug: #1565764
2016-04-04 15:17:35 +02:00
Vincent Françoise
22c9c4df87 Disabled PATCH, POST and DELETE for /actions
I removed the POST, PATCH and DELETE verbs from the actions
controller as they should only be modified internally.

Change-Id: Ia72484249240f829423056f66c5c0f9632d02106
Closes-Bug: #1533281
2016-03-30 10:10:28 +02:00
Jenkins
99ff6d3348 Merge "Integrated consolidation strategy with watcher" 2016-03-29 15:36:28 +00:00
Tin Lam
c67f83cce0 Added information on plugin mechanism to glossary
Added extra information regarding the plugin mechanism for:
action, strategy, and Watcher planner.

Change-Id: I9a7523282e229b83c16b06e3806ff795a0699c78
Closes-Bug: #1558470
2016-03-24 18:42:17 -05:00
Larry Rensing
397bb3497e Invalid states for Action Plan in the glossary
The list of possible states for Action Plan objects was outdated, and
was updated to match the state machine diagram.  A reference to the
state machines for Audits and Action Plans were added to the glossary,
and the descriptions of each state were moved to the sections containing
the state machines within the Architecture page.

Change-Id: I27043ad864c02fff50fb31868b27dc4b4897dbd4
Closes-Bug: #1558464
2016-03-24 15:14:42 +00:00
Bruno Grazioli
4c924fc505 Integrated consolidation strategy with watcher
This patch adds a new load consolidation strategy based on a heuristic
algorithm which focuses on measured CPU utilization and tries to
minimize hosts which have too much or too little load.
A new goal "vm_workload_consolidation" was added which executes
the strategy "VM_WORKLOAD_CONSOLIDATION".
This work depends on the implemetation of the bug:
https://bugs.launchpad.net/watcher/+bug/1553124

Change-Id: Ide05bddb5c85a3df05b94658ee5bd98f32e554b0
Implements: blueprint basic-cloud-consolidation-integration
2016-03-24 12:00:01 +01:00
jaugustine
4c5ecc808d Added oslo.context to requirements.txt
Added missing dependency oslo.context to requirements.txt

Change-Id: I88c42fd2381bad55ff499e096a93dcc2cc1d44e5
Closes-Bug: #1560976
2016-03-23 10:45:23 -05:00
Jenkins
64b5a7c3e4 Merge "Updated action-plugin doc to refer to Voluptuous" 2016-03-22 15:17:37 +00:00
Jenkins
40bb92f749 Merge "Remove true/false return from action.execute()" 2016-03-22 01:13:43 +00:00
Jenkins
92bd06cf94 Merge "Remove the watcher sample configuration file" 2016-03-21 13:10:28 +00:00
David TARDIVEL
c9e0dfd3f5 Remove the watcher sample configuration file
Watcher sample configuration file groups parameters from
various projects and the watcher project ones. This makes it
tricky to review updates on configuration parameters.

It is inconvenient for developer if the add/remove/change some
configuration options cause they need to take care about the
config.sample file.

The sample configuration file should be available into HTML doc.

This patchset:
. removes the file /etc/watcher/watcher.conf.sample
. adds an admin script tool to be able to built it, by using tox
. includes a new section 'Watcher sample configuration files' into
  the doc source files
. uses sphinx extension oslo_config.sphinxgenconfig

Change-Id: If2180de3614663f9cbc5396961a8d2175e28e315
Closes-Bug: #1541734
2016-03-21 11:47:29 +01:00
Vincent Françoise
446fe1307a Updated action-plugin doc to refer to Voluptuous
In this patchset, I added a small subsection which highlights the fact
that actions are using Voluptuous Schemas to validate their input
parameters.

Change-Id: I96a6060cf167468e4a3f7c8d8cd78330a20572e3
Closes-Bug: #1545643
2016-03-21 11:33:31 +01:00
Larry Rensing
2836f460e3 Rename variable vm_avg_cpu_util
The variable vm_avg_cpu_util was renamed to host_avg_cpu_util for
clarity, as it was really referring to the host average cpu util.

Change-Id: I7aaef9eb2c8421d01715c86afa36ab67f2fd5f30
Closes-Bug: #1559113
2016-03-18 10:45:08 -05:00
Jenkins
cb9bb7301b Merge "renamed "efficiency" with "efficacy" Closes-Bug:#1558468" 2016-03-18 11:09:12 +00:00
Jenkins
cb644fcef9 Merge "Renamed api.py to base.py in metrics engine" 2016-03-18 10:59:59 +00:00
sai
0a7c87eebf renamed "efficiency" with "efficacy"
Closes-Bug:#1558468

Change-Id: Iaf5f113b0aeb02904e76e7d1e729a93df3554275
2016-03-18 00:06:04 -05:00
Tin Lam
d7f4f42772 Remove true/false return from action.execute()
In watcher/applier/workflow_engine/default.py, we are checking the
return value of action.execute(). As the "TODO" above indicates it
(line 118), we should get rid of this and only flag an action as
failed if an exception was raised during its execute(). We will
need to update the related unit tests.

Change-Id: Ia8ff7abd9994c3504e733ccd1d629cafe9d4b839
Closes-Bug: #1548383
2016-03-16 18:18:55 -05:00
OpenStack Proposal Bot
bdc0eb196a Updated from global requirements
Change-Id: I568d88a71c47e16daa2f34b7dec97137eb7519b8
2016-03-16 13:34:18 +00:00
Jenkins
59427eb0d9 Merge "Refactored check for invalid goal" 2016-03-16 00:25:39 +00:00
Jenkins
b6801b192a Merge "Updated from global requirements" 2016-03-15 21:10:26 +00:00
Jenkins
0a6c2c16a4 Merge "Added Disk Capacity in cluster-data-model" 2016-03-15 16:28:53 +00:00
Vincent Françoise
9a44941c66 Documentation on purge command
This patchset add a new entry for the purge into the Watcher
documentation.

Change-Id: Ifb74b379bccd59ff736bf186bdaaf74de77098f1
Implements: blueprint db-purge-engine
2016-03-14 15:49:45 +01:00
Vincent Françoise
a6508a0013 Added purge script for soft deleted objects
This patchset implements the purge script as specified in its
related blueprint:

- The '--age-in-days' option allows to specify the number of
  days before expiry
- The '--max-number' option allows us to specify a limit on the number
  of objects to delete
- The '--audit-template' option allows you to only delete objects
  related to the specified audit template UUID or name
- The '--dry-run' option to go through the purge procedure without
  actually deleting anything
- The '--exclude-orphans' option which allows you to exclude from the
  purge any object that does not have a parent (i.e. and audit without
  a related audit template)

A prompt has been added to also propose to narrow down the number of
deletions to be below the specified limit.

Change-Id: I3ce83ab95277c109df67a6b5b920a878f6e59d3f
Implements: blueprint db-purge-engine
2016-03-14 15:49:45 +01:00
Vincent Françoise
c3db66ca09 Added Mixin-related filters on DB queries
As a pre-requisite for being able to query the database for objects
that are expired, I need a way to express date comparison on the
'deleted_at' field which is common for every Watcher object. As they
are coming from mixins, I decided to implement these filters with a
syntax borrowed from the Django ORM where the field is suffixed by the
comparison operator you want to apply:

- The '__lt' suffix stands for 'less than'
- The '__lte' suffix stands for 'less than or equal to'
- The '__gt' suffix stands for 'greater than'
- The '__gte' suffix stands for 'greater than or equal to'
- The '__eq' suffix stands for 'equal to'

I also added a 'uuid' filter to later on be able to filter by uuid.

Partially Implements: blueprint db-purge-engine

Change-Id: I763f330c1b8ea8395990d2276b71e87f5b3f3ddc
2016-03-14 15:46:58 +01:00
Jenkins
5d0fe553c4 Merge "Fixed wrongly used assertEqual method" 2016-03-14 14:43:34 +00:00
OpenStack Proposal Bot
8b8239c3d8 Updated from global requirements
Change-Id: Ib9bec0311e8ad6680487631e6707bafe4a259be6
2016-03-09 16:53:45 +00:00
Larry Rensing
920bd502ec Refactored check for invalid goal
When creating a new audit template, the verification of its goal
existence was previously done in watcher/objects/audit_template.py.
This check was moved to api/controllers/v1/audit_template.py, rather
than in the DAO class.

Change-Id: I6efb0657f64c46a56914a946ec78013b9e47331b
Closes-Bug: #1536191
2016-03-09 10:48:16 -06:00
Gábor Antal
c68d33f341 Renamed api.py to base.py in metrics engine
In watcher/metrics_engine/cluster_history/api.py,
we can find the BaseClusterHistory abstract base class.

To follow the same naming convention observed throughout the rest
of the project, I renamed watcher/metrics_engine/cluster_history/api.py
to watcher/metrics_engine/cluster_history/base.py

Change-Id: If18f8db7f0982e47c1998a469c54952670c262f5
Closes-Bug: #1548398
2016-03-09 12:02:05 +01:00
Vincent Françoise
8e8fdbd809 Re-generated the watcher.pot
There are translations that are missing from watcher.pot.
This patchset includes them.

Change-Id: Ia418066b5653b4c81885d3eb150613ba357f9b7b
Related-Bug: #1510189
2016-03-09 11:20:16 +01:00
Bruno Grazioli
681536c8c7 Added Disk Capacity in cluster-data-model
Fetched information of total disk capacity from nova and added a new
resource 'disk_capacity' to NovaClusterModelCollector cluster. Also a
new resource type 'disk_capacity' was added to ResourceType.
https://bugs.launchpad.net/watcher/+bug/1553124

Change-Id: I85750f25c6d2693432da8e5e3a3d0861320f4787
Closes-Bug: #1553124
2016-03-09 08:33:31 +00:00
Tin Lam
083b170083 Removing unicode from README.rst
Removing unicode from README.rst to unblock
the python34 gate failure.

Change-Id: I60da31e22b6a09540c9d6fca659a1b21d931a0b7
Closes-Bug: #1554347
2016-03-08 17:07:53 -06:00
Vincent Françoise
de7b0129a1 Doc on how to set up a thirdparty project
This documentation is a pre-requisite to all plugin documentation
as it guides you through the creation of a project from scratch
instead of simply forcusing on the implementation of the plugin
itself.

Change-Id: Id2e09b3667390ee6c4be42454c41f9d266fdfac2
Related-Bug: #1534639
Related-Bug: #1533739
Related-Bug: #1533740
2016-03-04 12:07:41 +01:00
Jenkins
323fd01a85 Merge "Updated Strategy plugin doc" 2016-03-04 08:27:36 +00:00
Jenkins
c0133e6585 Merge "Remove tests omission from coverage target in tox.ini" 2016-03-04 08:25:30 +00:00
Gábor Antal
63fffeacd8 Remove tests omission from coverage target in tox.ini
In coverage target in tox.ini, there is the following argument:
  --omit="watcher/tests/*"

However, in .coveragerc, the tests are also omitted,
according to line 4 in .coveragerc:
  omit = watcher/tests/*

So the watcher/tests/* directory is omitted twice. As it can be
seen in other modules (e.g.: swift, nova) omitting only in
.coveragerc should be enough.

Change-Id: I72951196a346fb73a90c998138fc91dd171432cd
Closes-Bug: #1552617
2016-03-04 07:36:26 +00:00
Jenkins
bc791f0e75 Merge "add Goal into RESTful Web API (v1) documentation" 2016-03-03 16:45:20 +00:00
David TARDIVEL
6ed417e6a7 add Goal into RESTful Web API (v1) documentation
Change-Id: I94ba5debf6f0ff9562dc95cbc5c646f33881d9e2
Closes-Bug: #1546630
2016-03-03 16:02:56 +00:00
Vincent Françoise
4afefa3dfb Updated Strategy plugin doc
As we modified the way a strategy gets implemented in
blueprint watcher-add-actions-via-conf, this patchset updates the
documentation regarding the implementation of a strategy plugin.

Change-Id: I517455bc34623feff704956ce30ed545a0e1014b
Closes-Bug: #1533740
2016-03-03 16:18:33 +01:00
Jenkins
9fadfbe40a Merge "Doc on how to implement a custom Watcher action" 2016-03-03 15:12:21 +00:00
Jenkins
f278874a93 Merge "Add Watcher dashboard to the list of projects" 2016-03-03 14:34:20 +00:00
Jenkins
1c5b247300 Merge "Doc on how to implement a custom Watcher planner" 2016-03-03 14:19:58 +00:00
Jenkins
547bf5f87e Merge "Improve DevStack documentation for beginners" 2016-03-03 14:19:21 +00:00
Vincent Françoise
96c0ac0ca8 Doc on how to implement a custom Watcher planner
This documentation describes step-by-step the process for implementing
a new planner in Watcher.

Change-Id: I8addba53de69be93730924a58107687020c19c74
Closes-Bug: #1533739
2016-03-03 14:09:41 +00:00
Antoine Cabot
a80fd2a51e Add Watcher dashboard to the list of projects
We provide a list of Watcher related projects
on the generated doc. This commit add links to
the Watcher dashboard project in various locations.

Change-Id: I1993cd5a11d3fcc8ca2c40b1779700359adab4ea
2016-03-03 13:47:24 +00:00
Vincent Françoise
58ea85c852 Doc on how to implement a custom Watcher action
This documentation describes step-by-step the process for implementing
a new action in Watcher.

Change-Id: I978b81cdf9ac6dcf43eb3ecbb79ab64ae4fd6f72
Closes-Bug: #1534639
2016-03-02 14:46:37 +01:00
Jenkins
78f122f241 Merge "RST directive to discover and generate drivers doc" 2016-03-01 23:09:18 +00:00
Jenkins
43eb997edb Merge "Updated Watcher doc to mention Tempest tests" 2016-03-01 22:58:24 +00:00
Gábor Antal
c440cdd69f Fixed wrongly used assertEqual method
In several places, assertEqual is used the following way:
  assertEqual(observed, expected)
However, the correct way to use assertEqual is:
  assertEqual(expected, observed)

Change-Id: I5a7442f4adf98bf7bc73cef1d17d20da39d9a7f8
Closes-Bug: #1551861
2016-03-01 18:20:37 +01:00
Taylor Peoples
b5bccba169 Improve DevStack documentation for beginners
The DevStack documentation should provide basic steps for setting up
Watcher with DevStack assuming the reader has no previous knowledge of
DevStack.

Change-Id: I830b1d9accb0e65bba73944697cba9c53ac3263e
Closes-Bug: #1538291
2016-03-01 17:25:24 +01:00
Jenkins
e058437ae0 Merge "Replace "Triggered" state by "Pending" state" 2016-03-01 15:48:33 +00:00
Jenkins
1acacaa812 Merge "Added support for live migration on non-shared storage" 2016-03-01 09:31:50 +00:00
Daniel Pawlik
5bb1b6cbf0 Added support for live migration on non-shared storage
Watcher applier should be able to live migrate instances on any storage
type. To do this watcher will catch error 400 returned from nova if we
try to live migrate instance which is not on shared storage and live
migrate instance using block_migrate.

Added unit tests, changed action in watcher applier.

Closes-bug: #1549307

Change-Id: I97e583c9b4a0bb9daa1d39e6d652d6474a5aaeb1
2016-03-01 08:35:14 +00:00
Vincent Françoise
de058d7ed1 Updated Watcher doc to mention Tempest tests
The Watcher Tempest tests are only mentioned inside a README.rst.
They are now part of the main documentation.

Change-Id: Ieca85dc7f7307b45e4b99af4a4600a8c2d2b59d7
Closes-Bug: #1536993
2016-02-29 17:42:21 +01:00
Vincent Françoise
98a65efb16 RST directive to discover and generate drivers doc
This patchset introduces a new custom directive called 'drivers-doc'
which loads all available drivers under a given namespace and import
their respective docstring into the .rst document.

This patchset also contains some modification/addition to the
docstring of these drivers to make the final document complete.

Change-Id: Ib3df59fa45cea9d11d20fb73a5f0f1d564135bca
Closes-Bug: #1536218
Closes-Bug: #1536735
2016-02-29 16:34:44 +01:00
Tin Lam
338539ec53 Rename 'TRIGGERED' state as 'PENDING'
"TRIGGERED" is not the correct word to use to describe the action
state, we should use "PENDING" instead.

Change-Id: If24979cdb916523861324f7bcc024e2f1fc28b05
Closes-Bug: #1548377
2016-02-26 23:07:59 -06:00
Jenkins
02f0f8e70a Merge "Add missing requirements" 2016-02-26 17:06:55 +00:00
Jenkins
7f1bd20a09 Merge "Add start directory for oslo_debug_helper" 2016-02-26 17:04:03 +00:00
Jenkins
d3d2a5ef8c Merge "Updated from global requirements" 2016-02-26 17:03:57 +00:00
Jenkins
3a6ae820c0 Merge "Fixed type in get_audit_template_by_name method" 2016-02-26 17:03:51 +00:00
Jenkins
5a8860419e Merge "Cleanup in tests/__init__.py" 2016-02-26 16:59:08 +00:00
Steve Wilkerson
4aa1c7558b Fixed type in get_audit_template_by_name method
Removed an extra underscore in the
get_audit_template_by__name method name in
watcher/db/api.py

Change-Id: I2687858ff4510c626c4dd2e2e9a5701405b5da55
Closes-Bug: #1548765
2016-02-26 08:25:42 -06:00
OpenStack Proposal Bot
a8dab52376 Updated from global requirements
Change-Id: Id68a055e6514ea30e28ef6adcfb22bb2ff3eeafb
2016-02-26 01:54:52 +00:00
Gábor Antal
5615d0523d Cleanup in tests/__init__.py
In watcher/tests/__init__.py has a totally unused,
misleading class so I removed it, as it is never used.

Change-Id: Ib878252453489eb3e1b1ff06f4d6b5e2b0726be5
Closes-Bug: #1549920
2016-02-25 19:10:54 +01:00
Jenkins
10823ce133 Merge "Cleanup in test_objects.py" 2016-02-25 16:06:02 +00:00
Jenkins
18c098c4c1 Merge "Update nova service state" 2016-02-25 08:30:40 +00:00
Jenkins
db649d86b6 Merge "Useless return statement in validate_sort_dir" 2016-02-24 16:49:01 +00:00
Tin Lam
6e380b685b Update nova service state
The primitive ChangeNovaServiceState allows us to change the state of
the nova-compute by calling nova api. The state of a nova-compute can
be ENABLED or DISABLED, however in the current implementation we use
OFFLINE and ONLINE.

Update the code to use ENABLED or DISABLED.

Change-Id: If3d9726bc5ae980b66c7fd4c5b7986f89d8bc690
Closes-Bug: #1523891
2016-02-23 00:46:30 -06:00
Antoine Cabot
8dfff0e8e6 Replace "Triggered" state by "Pending" state
This commit only applies to documentation, state name
must be updated in code accordingly.

Change-Id: I027fd55d968c12992d800de3657543be417c71b0
Related-Bug: #1548377
2016-02-22 17:33:41 +01:00
Chaozhe.Chen
fbc7da755a Add start directory for oslo_debug_helper
When I used `tox -e debug <test-name>` to test a case, the case did not
run properly and there is an error like this:

ImportError: Start directory is not importable: './python-watcher/tests'

If we use '-t wartcher/tests' to point out the test path, the case will
run properly under debug.
So we'd better add this start directory for oslo_debug_helper.

Change-Id: I04d9937f72a95f8f045129af08df0cd0d0870d39
2016-02-23 00:01:18 +08:00
Chaozhe.Chen
b947c30910 Add missing requirements
WebOb is needed in our code[1].

[1]https://github.com/openstack/watcher/blob/master/watcher/api/
middleware/parsable_error.py#L70

Change-Id: I6dd3940057368ff70656dd40c75ec59284f378bf
2016-02-22 21:14:25 +08:00
OpenStack Proposal Bot
9af96114af Updated from global requirements
Change-Id: I28d515eef452dd715a28e3fb4c177fc93f307c79
2016-02-20 22:02:12 +00:00
Vincent Françoise
1ddf69a68f Re-enable related Tempest test
Following the previous 2 patchset
https://review.openstack.org/#/c/279517/ and
https://review.openstack.org/#/c/279555/, this patchset re-enables
the related Tempest test which filters goals while removing the one
that was filtering by host aggregate.

Change-Id: I384e62320de34761e29d5cbac37ddc8ae253a70c
Closes-Bug: #1510189
2016-02-19 14:32:42 +00:00
Jenkins
379ac791a8 Merge "Pass parameter to the query in get_last_sample_values" 2016-02-19 10:24:36 +00:00
Jenkins
81ea37de41 Merge "Remove unused function and argument" 2016-02-19 01:01:10 +00:00
Gábor Antal
1c963fdc96 Useless return statement in validate_sort_dir
In watcher/api/controllers/v1/utils.py, in
the validate_sort_dir method has a return statement,
however the return value is exactly the parameter's value.

This is misleading, so I removed it.

Change-Id: I18c5c7853a5afedac88431347712a4348c9fd5dd
Closes-Bug: #1546917
2016-02-18 17:56:16 +01:00
Gábor Antal
f32995228b Pass parameter to the query in get_last_sample_values
In watcher/common/ceilometer_help.py:129,
there is an unused parameter, called limit:
  def get_last_sample_values(self, resource_id, meter_name, limit=1):

In the next line, there is a method call which can take this
currently unused 'limit' parameter. Probably, passing this 'limit'
parameter to the query was originally intended, however it wasn't
added to the query call.

Closes-Bug: #1541415
Change-Id: I025070c6004243d6b8a6ea7a1d83081480c4148b
2016-02-18 13:55:02 +01:00
Jenkins
0ec3d68994 Merge "Added goal filter in Watcher API" 2016-02-18 10:26:38 +00:00
Jenkins
3503e11506 Merge "Improve variable names in strategy implementations" 2016-02-18 10:22:50 +00:00
Jenkins
c7f0ef37d0 Merge "Added unit tests on actions" 2016-02-18 08:42:46 +00:00
Béla Vancsics
d93b1ffe9f Remove unused function and argument
I removed the unused function and (function)argument in code

Change-Id: Ib7afa5d868c3c7769f53e45c270850e4c3370f86
2016-02-18 09:17:43 +01:00
Vincent Françoise
4e71a0c655 Added goal filter in Watcher API
Although it was proposed via python-watcherclient, the feature was
not implemented on the Watcher API.
As the notion of host aggregate is currently unused in Watcher,
decision was made to only implement the filtering of goal within
the Watcher API whilst removing the host_aggregate filter from the
Watcher client.
Thus, this patchset adds this missing functionality by adding the
'goal' parameter to the API.

Change-Id: I54d248f7e470249c6412650ddf50a3e3631d2a09
Related-Bug: #1510189
2016-02-17 18:22:51 +01:00
Steve Wilkerson
37dd713ed5 Improve variable names in strategy implementations
Renamed many of the variables and method parameters
in the strategy implementations to make the names
more meaningful.  Also changed the abstract method
signature in base.py to reflect these changes.

Closes-Bug: #1541615

Change-Id: Ibeba6c6ef6d5b70482930f387b05d5d650812355
2016-02-16 09:15:14 -06:00
Vincent Françoise
55aeb783e3 Added unit tests on actions
As we had a low test coverage on actions, I added some more tests
with this patchset. This actually revealed a small bug (typo) in
"change_nova_service_state" which has been fixed in here.

Note that Tempest test also cover these action via the
basic_consolidation strategy.

Change-Id: I2d7116a6fdefee82ca254512a9cf50fc61e3c80e
Closes-Bug: #1523513
2016-02-16 11:42:37 +01:00
Jenkins
fe3f6e73be Merge "Remove KEYSTONE_CATALOG_BACKEND from DevStack plugin" 2016-02-16 08:24:54 +00:00
Jean-Emile DARTOIS
5baff7dc3e Clean imports in code
In some part in the code we import objects.
In the Openstack style guidelines they recommand
to import only modules.
We need to fix that.

Change-Id: I4bfee2b94d101940d615f78f9bebb83310ed90ba
Partial-Bug:1543101
2016-02-15 18:04:24 +01:00
Jean-Emile DARTOIS
e3198d25a5 Add Voluptuous to validate the action parameters
We want a simplest way to validate the input parameters of an
Action through a schema.

APIImpact
DocImpact
Partially implements: blueprint watcher-add-actions-via-conf

Change-Id: I139775f467fe7778c7354b0cfacf796fc27ffcb2
2016-02-12 17:47:52 +01:00
Jenkins
33ee575936 Merge "Better cleanup for Tempest tests" 2016-02-12 16:04:26 +00:00
Darren Shaw
259f2562e6 Remove KEYSTONE_CATALOG_BACKEND from DevStack plugin
Via Sean Dague in the mailing list:
In Newton, the KEYSTONE_CATALOG_BACKEND variable will be removed.
This commit removes the if check and variable from watcher

Change-Id: I73c5dd25feff9ba9824c267a8817a49e4ad3a06a
Closes-Bug: #1544433
2016-02-12 09:51:51 -06:00
Jenkins
1629247413 Merge "Update the default version of Neutron API" 2016-02-12 15:41:34 +00:00
Jenkins
58d84aca6d Merge "Ceilometer client instantiation fixup" 2016-02-12 10:00:33 +00:00
Gábor Antal
236879490d Cleanup in test_objects.py
In watcher/tests/objects/test_objects.py, there is a class
called "_TestObject(object)" which is probably an older test class.

This test class runs never (as the name starts with "_"
and inherits from object), and has some really old test,
like test_orphaned_object method, which is testing an exception
that doesn't exist at the current codebase.

Change-Id: I7559a004e8c136a206fc1cf7ac330c7d4157f94f
Closes-Bug: #1544685
2016-02-11 19:29:16 +01:00
Vincent Françoise
79850cc89c Better cleanup for Tempest tests
When running our Tempest tests, we are now cleaning up all the
objects we have created within the WatcherDB via a soft_delete.

Partially Implements: blueprint deletion-of-actions-plan

Change-Id: Ibdcfd2be37094377d09ad77d5c20298ee2baa4d0
2016-02-11 18:36:16 +01:00
Vincent Françoise
b958214db8 Ceilometer client instantiation fixup
A problem was found during manual integration tests which were failing
because we couldn't instantiate the ceilometer client when trying to
execute an action plan using the 'basic_consolidation' strategy.

This patchset fixes the problem with an update of the related tests

Change-Id: I2b1f1dcc16fd8dfbf508c4d5661c1fce194254e4
Closes-Bug: #1544652
2016-02-11 18:29:58 +01:00
Jenkins
a0b5f5aa1d Merge "Delete linked actions when deleting an action plan" 2016-02-11 09:39:33 +00:00
David TARDIVEL
a4a009a2c6 Update the default version of Neutron API
Default value of Neutron API should be 2.0 in neutronclient.
Closes-Bug: #1544134

Change-Id: I241c067c33c992da53f30e974ffca3edb466811d
2016-02-10 17:04:34 +01:00
Taylor Peoples
0ba8a35ade Sync with openstack/requirements master branch
In order to accept the requirements contract defined in
openstack/requirements, we need to sync with the master branch of that
project's global-requirements.txt and test-requirements.txt files.

Most of the changes are just version changes and nothing major.  The
only major change is:

1) The twine dependency is removed, and therefore the pypi tox
environment was also removed.

Change-Id: Idbe9e73ddc5a34ac49aa6f6eff0779d46a75f583
Closes-Bug: #1533282
2016-02-09 16:31:07 +00:00
Vincent Françoise
8bcc1b2097 Delete linked actions when deleting an action plan
When a user deletes an action plan, we now delete all related actions.

Partially Implements: blueprint deletion-of-actions-plan

Change-Id: I5d519c38458741be78591cbec04dbd410a6dc14b
2016-02-08 17:04:37 +01:00
Jenkins
b440f5c69a Merge "Add IRC information into contributing page" 2016-02-05 17:19:48 +00:00
David TARDIVEL
858bbbf126 Add IRC information into contributing page
Add Watcher IRC channels : #openstack-watcher and
 #openstack-meeeting-4

Change-Id: I4c128544a32548065fddde54b28f645fc63ebfd5
Closes-Bug: #1536200
2016-02-05 15:53:21 +00:00
150 changed files with 6148 additions and 2776 deletions

2
.gitignore vendored
View File

@@ -44,6 +44,8 @@ output/*/index.html
# Sphinx
doc/build
doc/source/api
doc/source/samples
doc/source/watcher.conf.sample
# pbr generates these
AUTHORS

View File

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

View File

@@ -96,15 +96,13 @@ function configure_watcher {
function create_watcher_accounts {
create_service_user "watcher" "admin"
if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then
local watcher_service=$(get_or_create_service "watcher" \
"infra-optim" "Watcher Infrastructure Optimization Service")
get_or_create_endpoint $watcher_service \
"$REGION_NAME" \
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT"
fi
local watcher_service=$(get_or_create_service "watcher" \
"infra-optim" "Watcher Infrastructure Optimization Service")
get_or_create_endpoint $watcher_service \
"$REGION_NAME" \
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT"
}
# create_watcher_conf() - Create a new watcher.conf file

View File

@@ -127,7 +127,19 @@ Watcher CLI
The watcher command-line interface (CLI) can be used to interact with the
Watcher system in order to control it or to know its current status.
Please, read `the detailed documentation about Watcher CLI <https://factory.b-com.com/www/watcher/doc/python-watcherclient/>`_
Please, read `the detailed documentation about Watcher CLI
<https://factory.b-com.com/www/watcher/doc/python-watcherclient/>`_.
.. _archi_watcher_dashboard_definition:
Watcher Dashboard
-----------------
The Watcher Dashboard can be used to interact with the Watcher system through
Horizon in order to control it or to know its current status.
Please, read `the detailed documentation about Watcher Dashboard
<https://factory.b-com.com/www/watcher/doc/watcher-dashboard/>`_.
.. _archi_watcher_database_definition:
@@ -349,6 +361,28 @@ State Machine diagrams
Audit State Machine
-------------------
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
be one of the following:
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
submitted (either manually by the
:ref:`Administrator <administrator_definition>` or automatically via some
event handling mechanism) and is in the queue for being processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
successfully and at least one solution was found
- **FAILED** : an error occured while executing the
:ref:`Audit <audit_definition>`
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
:ref:`Watcher database <watcher_database_definition>` but is not returned
any more through the Watcher APIs.
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
**ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
The following diagram shows the different possible states of an
:ref:`Audit <audit_definition>` and what event makes the state change to a new
value:
@@ -361,6 +395,31 @@ value:
Action Plan State Machine
-------------------------
An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
state may be one of the following:
- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
for a validation from the :ref:`Administrator <administrator_definition>`
- **PENDING** : a request for an :ref:`Action Plan <action_plan_definition>`
has been submitted (due to an
:ref:`Administrator <administrator_definition>` executing an
:ref:`Audit <audit_definition>`) and is in the queue for
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
executed successfully (i.e. all :ref:`Actions <action_definition>` that it
contains have been executed successfully)
- **FAILED** : an error occured while executing the
:ref:`Action Plan <action_plan_definition>`
- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
stored in the :ref:`Watcher database <watcher_database_definition>` but is
not returned any more through the Watcher APIs.
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
**PENDING** or **ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
The following diagram shows the different possible states of an
:ref:`Action Plan <action_plan_definition>` and what event makes the state
change to a new value:

View File

@@ -18,6 +18,7 @@ from watcher import version as watcher_version
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'oslo_config.sphinxconfiggen',
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinxcontrib.httpdomain',
@@ -28,7 +29,8 @@ extensions = [
]
wsme_protocols = ['restjson']
config_generator_config_file = '../../etc/watcher/watcher-config-generator.conf'
sample_config_basename = 'watcher'
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.

View File

@@ -0,0 +1 @@
../../etc/watcher/watcher-config-generator.conf

View File

@@ -0,0 +1,14 @@
.. _watcher_sample_configuration_files:
==================================
Watcher sample configuration files
==================================
watcher.conf
~~~~~~~~~~~~
The ``watcher.conf`` file contains most of the options to configure the
Watcher services.
.. literalinclude:: ../watcher.conf.sample
:language: ini

View File

@@ -34,6 +34,8 @@ The Watcher service includes the following components:
- ``watcher-applier``: applies the action plan.
- `python-watcherclient`_: A command-line interface (CLI) for interacting with
the Watcher service.
- `watcher-dashboard`_: An Horizon plugin for interacting with the Watcher
service.
Additionally, the Bare Metal service has certain external dependencies, which
are very similar to other OpenStack services:
@@ -52,6 +54,7 @@ additional functionality:
.. _`ceilometer`: https://github.com/openstack/ceilometer
.. _`nova`: https://github.com/openstack/nova
.. _`python-watcherclient`: https://github.com/openstack/python-watcherclient
.. _`watcher-dashboard`: https://github.com/openstack/watcher-dashboard
.. _`watcher metering`: https://github.com/b-com/watcher-metering
.. _`RabbitMQ`: https://www.rabbitmq.com/
@@ -160,6 +163,16 @@ Configure the Watcher service
The Watcher service is configured via its configuration file. This file
is typically located at ``/etc/watcher/watcher.conf``.
You can easily generate and update a sample configuration file
named :ref:`watcher.conf.sample <watcher_sample_configuration_files>` by using
these following commands::
$ git clone git://git.openstack.org/openstack/watcher
$ cd watcher/
$ tox -econfig
$ vi etc/watcher/watcher.conf.sample
The configuration file is organized into the following sections:
* ``[DEFAULT]`` - General configuration

View File

@@ -38,7 +38,11 @@ If you need help on a specific command, you can use:
$ watcher help COMMAND
If you want to deploy Watcher in Horizon, please refer to the `Watcher Horizon
plugin installation guide`_.
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
.. _`Watcher Horizon plugin installation guide`: https://factory.b-com.com/www/watcher/doc/watcher-dashboard/deploy/installation.html
Seeing what the Watcher CLI can do ?
------------------------------------

View File

@@ -60,3 +60,12 @@ Code Hosting
Code Review
https://review.openstack.org/#/q/status:open+project:openstack/watcher,n,z
IRC Channel
``#openstack-watcher`` (changelog_)
Weekly Meetings
on Wednesdays at 14:00 UTC in the ``#openstack-meeting-4`` IRC
channel (`meetings logs`_)
.. _changelog: http://eavesdrop.openstack.org/irclogs/%23openstack-watcher/
.. _meetings logs: http://eavesdrop.openstack.org/meetings/watcher/

View File

@@ -9,35 +9,97 @@ Set up a development environment via DevStack
=============================================
Watcher is currently able to optimize compute resources - specifically Nova
compute hosts - via operations such as live migrations. In order for you to
compute hosts - via operations such as live migrations. In order for you to
fully be able to exercise what Watcher can do, it is necessary to have a
multinode environment to use. If you have no experience with DevStack, you
should check out the `DevStack documentation`_ and be comfortable with the
basics of DevStack before attempting to get a multinode DevStack setup with
the Watcher plugin.
multinode environment to use.
You can set up the Watcher services quickly and easily using a Watcher
DevStack plugin. See `PluginModelDocs`_ for information on DevStack's plugin
model.
.. _DevStack documentation: http://docs.openstack.org/developer/devstack/
.. _PluginModelDocs: http://docs.openstack.org/developer/devstack/plugins.html
It is recommended that you build off of the provided example local.conf files
(`local.conf.controller`_, `local.conf.compute`_). You'll likely want to
configure something to obtain metrics, such as Ceilometer. Ceilometer is used
in the example local.conf files.
To configure the Watcher services with DevStack, add the following to the
DevStack plugin. See `PluginModelDocs`_ for information on DevStack's plugin
model. To enable the Watcher plugin with DevStack, add the following to the
`[[local|localrc]]` section of your controller's `local.conf` to enable the
Watcher plugin::
enable_plugin watcher git://git.openstack.org/openstack/watcher
Then run devstack normally::
For more detailed instructions, see `Detailed DevStack Instructions`_. Check
out the `DevStack documentation`_ for more information regarding DevStack.
cd /opt/stack/devstack
./stack.sh
.. _PluginModelDocs: http://docs.openstack.org/developer/devstack/plugins.html
.. _DevStack documentation: http://docs.openstack.org/developer/devstack/
Detailed DevStack Instructions
==============================
#. Obtain N (where N >= 1) servers (virtual machines preferred for DevStack).
One of these servers will be the controller node while the others will be
compute nodes. N is preferably >= 3 so that you have at least 2 compute
nodes, but in order to stand up the Watcher services only 1 server is
needed (i.e., no computes are needed if you want to just experiment with
the Watcher services). These servers can be VMs running on your local
machine via VirtualBox if you prefer. DevStack currently recommends that
you use Ubuntu 14.04 LTS. The servers should also have connections to the
same network such that they are all able to communicate with one another.
#. For each server, clone the DevStack repository and create the stack user::
sudo apt-get update
sudo apt-get install git
git clone https://git.openstack.org/openstack-dev/devstack
sudo ./devstack/tools/create-stack-user.sh
Now you have a stack user that is used to run the DevStack processes. You
may want to give your stack user a password to allow SSH via a password::
sudo passwd stack
#. Switch to the stack user and clone the DevStack repo again::
sudo su stack
cd ~
git clone https://git.openstack.org/openstack-dev/devstack
#. For each compute node, copy the provided `local.conf.compute`_ example file
to the compute node's system at ~/devstack/local.conf. Make sure the
HOST_IP and SERVICE_HOST values are changed appropriately - i.e., HOST_IP
is set to the IP address of the compute node and SERVICE_HOST is set to the
IP address of the controller node.
If you need specific metrics collected (or want to use something other
than Ceilometer), be sure to configure it. For example, in the
`local.conf.compute`_ example file, the appropriate ceilometer plugins and
services are enabled and disabled. If you were using something other than
Ceilometer, then you would likely want to configure it likewise. The
example file also sets the compute monitors nova configuration option to
use the CPU virt driver. If you needed other metrics, it may be necessary
to configure similar configuration options for the projects providing those
metrics.
#. For the controller node, copy the provided `local.conf.controller`_ example
file to the controller node's system at ~/devstack/local.conf. Make sure
the HOST_IP value is changed appropriately - i.e., HOST_IP is set to the IP
address of the controller node.
Note: if you want to use another Watcher git repository (such as a local
one), then change the enable plugin line::
enable_plugin watcher <your_local_git_repo> [optional_branch]
If you do this, then the Watcher DevStack plugin will try to pull the
python-watcherclient repo from <your_local_git_repo>/../, so either make
sure that is also available or specify WATCHERCLIENT_REPO in the local.conf
file.
Note: if you want to use a specific branch, specify WATCHER_BRANCH in the
local.conf file. By default it will use the master branch.
#. Start stacking from the controller node::
./devstack/stack.sh
#. Start stacking on each of the compute nodes using the same command.
#. Configure the environment for live migration via NFS. See the
`Multi-Node DevStack Environment`_ section for more details.
.. _local.conf.controller: https://github.com/openstack/watcher/tree/master/devstack/local.conf.controller
.. _local.conf.compute: https://github.com/openstack/watcher/tree/master/devstack/local.conf.compute
@@ -104,7 +166,6 @@ Restart the libvirt service::
sudo service libvirt-bin restart
Setting up SSH keys between compute nodes to enable live migration
------------------------------------------------------------------
@@ -113,8 +174,8 @@ each compute node:
1. The SOURCE root user's public RSA key (likely in /root/.ssh/id_rsa.pub)
needs to be in the DESTINATION stack user's authorized_keys file
(~stack/.ssh/authorized_keys). This can be accomplished by manually
copying the contents from the file on the SOURCE to the DESTINATION. If
(~stack/.ssh/authorized_keys). This can be accomplished by manually
copying the contents from the file on the SOURCE to the DESTINATION. If
you have a password configured for the stack user, then you can use the
following command to accomplish the same thing::
@@ -122,7 +183,7 @@ each compute node:
2. The DESTINATION host's public ECDSA key (/etc/ssh/ssh_host_ecdsa_key.pub)
needs to be in the SOURCE root user's known_hosts file
(/root/.ssh/known_hosts). This can be accomplished by running the
(/root/.ssh/known_hosts). This can be accomplished by running the
following on the SOURCE machine (hostname must be used)::
ssh-keyscan -H DEST_HOSTNAME | sudo tee -a /root/.ssh/known_hosts
@@ -131,3 +192,13 @@ In essence, this means that every compute node's root user's public RSA key
must exist in every other compute node's stack user's authorized_keys file and
every compute node's public ECDSA key needs to be in every other compute
node's root user's known_hosts file.
Environment final checkup
-------------------------
If you are willing to make sure everything is in order in your DevStack
environment, you can run the Watcher Tempest tests which will validate its API
but also that you can perform the typical Watcher workflows. To do so, have a
look at the :ref:`Tempest tests <tempest_tests>` section which will explain to
you how to run them.

View File

@@ -4,6 +4,8 @@
https://creativecommons.org/licenses/by/3.0/
.. _watcher_developement_environment:
=========================================
Set up a development environment manually
=========================================
@@ -143,34 +145,13 @@ You should then be able to `import watcher` using Python without issue:
If you can import watcher without a traceback, you should be ready to develop.
Run Watcher unit tests
======================
Run Watcher tests
=================
All unit tests should be run using tox. To run the unit tests under py27 and
also run the pep8 tests:
Watcher provides both :ref:`unit tests <unit_tests>` and
:ref:`functional/tempest tests <tempest_tests>`. Please refer to :doc:`testing`
to understand how to run them.
.. code-block:: bash
$ workon watcher
(watcher) $ pip install tox
(watcher) $ cd watcher
(watcher) $ tox -epep8 -epy27
You may pass options to the test programs using positional arguments. To run a
specific unit test, this passes the -r option and desired test (regex string)
to os-testr:
.. code-block:: bash
$ workon watcher
(watcher) $ tox -epy27 -- tests.api
When you're done, deactivate the virtualenv:
.. code-block:: bash
$ deactivate
Build the Watcher documentation
===============================
@@ -269,6 +250,11 @@ interface.
.. _`python-watcherclient`: https://github.com/openstack/python-watcherclient
There is also an Horizon plugin for Watcher `watcher-dashboard`_ which
allows to interact with Watcher through a web-based interface.
.. _`watcher-dashboard`: https://github.com/openstack/watcher-dashboard
Exercising the Watcher Services locally
=======================================

View File

@@ -0,0 +1,171 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
==================
Build a new action
==================
Watcher Applier has an external :ref:`action <action_definition>` plugin
interface which gives anyone the ability to integrate an external
:ref:`action <action_definition>` in order to extend the initial set of actions
Watcher provides.
This section gives some guidelines on how to implement and integrate custom
actions with Watcher.
Creating a new plugin
=====================
First of all you have to extend the base :py:class:`BaseAction` class which
defines a set of abstract methods and/or properties that you will have to
implement:
- The :py:attr:`~.BaseAction.schema` is an abstract property that you have to
implement. This is the first function to be called by the
:ref:`applier <watcher_applier_definition>` before any further processing
and its role is to validate the input parameters that were provided to it.
- The :py:meth:`~.BaseAction.precondition` is called before the execution of
an action. This method is a hook that can be used to perform some
initializations or to make some more advanced validation on its input
parameters. If you wish to block the execution based on this factor, you
simply have to ``raise`` an exception.
- The :py:meth:`~.BaseAction.postcondition` is called after the execution of
an action. As this function is called regardless of whether an action
succeeded or not, this can prove itself useful to perform cleanup
operations.
- The :py:meth:`~.BaseAction.execute` is the main component of an action.
This is where you should implement the logic of your action.
- The :py:meth:`~.BaseAction.revert` allows you to roll back the targeted
resource to its original state following a faulty execution. Indeed, this
method is called by the workflow engine whenever an action raises an
exception.
Here is an example showing how you can write a plugin called ``DummyAction``:
.. code-block:: python
# Filepath = <PROJECT_DIR>/thirdparty/dummy.py
# Import path = thirdparty.dummy
import voluptuous
from watcher.applier.actions import base
class DummyAction(baseBaseAction):
@property
def schema(self):
return voluptuous.Schema({})
def execute(self):
# Does nothing
pass # Only returning False is considered as a failure
def revert(self):
# Does nothing
pass
def precondition(self):
# No pre-checks are done here
pass
def postcondition(self):
# Nothing done here
pass
This implementation is the most basic one. So if you want to have more advanced
examples, have a look at the implementation of the actions already provided
by Watcher like.
To get a better understanding on how to implement a more advanced action,
have a look at the :py:class:`~watcher.applier.actions.migration.Migrate`
class.
Input validation
----------------
As you can see in the previous example, we are using `Voluptuous`_ to validate
the input parameters of an action. So if you want to learn more about how to
work with `Voluptuous`_, you can have a look at their `documentation`_ here:
.. _Voluptuous: https://github.com/alecthomas/voluptuous
.. _documentation: https://github.com/alecthomas/voluptuous/blob/master/README.md
Abstract Plugin Class
=====================
Here below is the abstract ``BaseAction`` class that every single action
should implement:
.. autoclass:: watcher.applier.actions.base.BaseAction
:members:
:noindex:
.. py:attribute:: schema
Defines a Schema that the input parameters shall comply to
:returns: A schema declaring the input parameters this action should be
provided along with their respective constraints
(e.g. type, value range, ...)
:rtype: :py:class:`voluptuous.Schema` instance
Register a new entry point
==========================
In order for the Watcher Applier to load your new action, the
action must be registered as a named entry point under the
``watcher_actions`` entry point of your ``setup.py`` file. If you are using
pbr_, this entry point should be placed in your ``setup.cfg`` file.
The name you give to your entry point has to be unique.
Here below is how you would proceed to register ``DummyAction`` using pbr_:
.. code-block:: ini
[entry_points]
watcher_actions =
dummy = thirdparty.dummy:DummyAction
.. _pbr: http://docs.openstack.org/developer/pbr/
Using action plugins
====================
The Watcher Applier service will automatically discover any installed plugins
when it is restarted. If a Python package containing a custom plugin is
installed within the same environment as Watcher, Watcher will automatically
make that plugin available for use.
At this point, you can use your new action plugin in your :ref:`strategy plugin
<implement_strategy_plugin>` if you reference it via the use of the
:py:meth:`~.Solution.add_action` method:
.. code-block:: python
# [...]
self.solution.add_action(
action_type="dummy", # Name of the entry point we registered earlier
applies_to="",
input_parameters={})
By doing so, your action will be saved within the Watcher Database, ready to be
processed by the planner for creating an action plan which can then be executed
by the Watcher Applier via its workflow engine.
Scheduling of an action plugin
==============================
Watcher provides a basic built-in :ref:`planner <watcher_planner_definition>`
which is only able to process the Watcher built-in actions. Therefore, you will
either have to use an existing third-party planner or :ref:`implement another
planner <implement_planner_plugin>` that will be able to take into account your
new action plugin.

View File

@@ -0,0 +1,90 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
.. _plugin-base_setup:
=======================================
Create a third-party plugin for Watcher
=======================================
Watcher provides a plugin architecture which allows anyone to extend the
existing functionalities by implementing third-party plugins. This process can
be cumbersome so this documentation is there to help you get going as quickly
as possible.
Pre-requisites
==============
We assume that you have set up a working Watcher development environment. So if
this not already the case, you can check out our documentation which explains
how to set up a :ref:`development environment
<watcher_developement_environment>`.
.. _development environment:
Third party project scaffolding
===============================
First off, we need to create the project structure. To do so, we can use
`cookiecutter`_ and the `OpenStack cookiecutter`_ project scaffolder to
generate the skeleton of our project::
$ virtualenv thirdparty
$ source thirdparty/bin/activate
$ pip install cookiecutter
$ cookiecutter https://github.com/openstack-dev/cookiecutter
The last command will ask you for many information, and If you set
``module_name`` and ``repo_name`` as ``thirdparty``, you should end up with a
structure that looks like this::
$ cd thirdparty
$ tree .
.
├── babel.cfg
├── CONTRIBUTING.rst
├── doc
│   └── source
│   ├── conf.py
│   ├── contributing.rst
│   ├── index.rst
│   ├── installation.rst
│   ├── readme.rst
│   └── usage.rst
├── HACKING.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── requirements.txt
├── setup.cfg
├── setup.py
├── test-requirements.txt
├── thirdparty
│   ├── __init__.py
│   └── tests
│   ├── base.py
│   ├── __init__.py
│   └── test_thirdparty.py
└── tox.ini
.. _cookiecutter: https://github.com/audreyr/cookiecutter
.. _OpenStack cookiecutter: https://github.com/openstack-dev/cookiecutter
Implementing a plugin for Watcher
=================================
Now that the project skeleton has been created, you can start the
implementation of your plugin. As of now, you can implement the following
plugins for Watcher:
- A :ref:`strategy plugin <implement_strategy_plugin>`
- A :ref:`planner plugin <implement_planner_plugin>`
- An :ref:`action plugin <implement_strategy_plugin>`
- A :ref:`workflow engine plugin <implement_workflow_engine_plugin>`
If you want to learn more on how to implement them, you can refer to their
dedicated documentation.

View File

@@ -0,0 +1,127 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
.. _implement_planner_plugin:
===================
Build a new planner
===================
Watcher :ref:`Decision Engine <watcher_decision_engine_definition>` has an
external :ref:`planner <planner_definition>` plugin interface which gives
anyone the ability to integrate an external :ref:`planner <planner_definition>`
in order to extend the initial set of planners Watcher provides.
This section gives some guidelines on how to implement and integrate custom
planners with Watcher.
.. _Decision Engine: watcher_decision_engine_definition
Creating a new plugin
=====================
First of all you have to extend the base :py:class:`~.BasePlanner` class which
defines an abstract method that you will have to implement. The
:py:meth:`~.BasePlanner.schedule` is the method being called by the Decision
Engine to schedule a given solution (:py:class:`~.BaseSolution`) into an
:ref:`action plan <action_plan_definition>` by ordering/sequencing an unordered
set of actions contained in the proposed solution (for more details, see
:ref:`definition of a solution <solution_definition>`).
Here is an example showing how you can write a planner plugin called
``DummyPlanner``:
.. code-block:: python
# Filepath = third-party/third_party/dummy.py
# Import path = third_party.dummy
import uuid
from watcher.decision_engine.planner import base
class DummyPlanner(base.BasePlanner):
def _create_action_plan(self, context, audit_id):
action_plan_dict = {
'uuid': uuid.uuid4(),
'audit_id': audit_id,
'first_action_id': None,
'state': objects.action_plan.State.RECOMMENDED
}
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
new_action_plan.create(context)
new_action_plan.save()
return new_action_plan
def schedule(self, context, audit_id, solution):
# Empty action plan
action_plan = self._create_action_plan(context, audit_id)
# todo: You need to create the workflow of actions here
# and attach it to the action plan
return action_plan
This implementation is the most basic one. So if you want to have more advanced
examples, have a look at the implementation of planners already provided by
Watcher like :py:class:`~.DefaultPlanner`. A list with all available planner
plugins can be found :ref:`here <watcher_planners>`.
Abstract Plugin Class
=====================
Here below is the abstract ``BasePlanner`` class that every single planner
should implement:
.. autoclass:: watcher.decision_engine.planner.base.BasePlanner
:members:
:noindex:
Register a new entry point
==========================
In order for the Watcher Decision Engine to load your new planner, the
latter must be registered as a new entry point under the
``watcher_planners`` entry point namespace of your ``setup.py`` file. If you
are using pbr_, this entry point should be placed in your ``setup.cfg`` file.
The name you give to your entry point has to be unique.
Here below is how you would proceed to register ``DummyPlanner`` using pbr_:
.. code-block:: ini
[entry_points]
watcher_planners =
dummy = third_party.dummy:DummyPlanner
.. _pbr: http://docs.openstack.org/developer/pbr/
Using planner plugins
=====================
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` service
will automatically discover any installed plugins when it is started. This
means that if Watcher is already running when you install your plugin, you will
have to restart the related Watcher services. If a Python package containing a
custom plugin is installed within the same environment as Watcher, Watcher will
automatically make that plugin available for use.
At this point, Watcher will use your new planner if you referenced it in the
``planner`` option under the ``[watcher_planner]`` section of your
``watcher.conf`` configuration file when you started it. For example, if you
want to use the ``dummy`` planner you just installed, you would have to
select it as followed:
.. code-block:: ini
[watcher_planner]
planner = dummy
As you may have noticed, only a single planner implementation can be activated
at a time, so make sure it is generic enough to support all your strategies
and actions.

View File

@@ -4,17 +4,19 @@
https://creativecommons.org/licenses/by/3.0/
.. _implement_strategy_plugin:
=================================
Build a new optimization strategy
=================================
Watcher Decision Engine has an external :ref:`strategy <strategy_definition>`
plugin interface which gives anyone the ability to integrate an external
:ref:`strategy <strategy_definition>` in order to make use of placement
algorithms.
strategy in order to make use of placement algorithms.
This section gives some guidelines on how to implement and integrate custom
Stategies with Watcher.
strategies with Watcher.
Pre-requisites
==============
@@ -29,15 +31,14 @@ Creating a new plugin
First of all you have to:
- Extend the base ``BaseStrategy`` class
- Implement its ``execute`` method
- Extend :py:class:`~.BaseStrategy`
- Implement its :py:meth:`~.BaseStrategy.execute` method
Here is an example showing how you can write a plugin called ``DummyStrategy``:
.. code-block:: python
# Filepath = third-party/third_party/dummy.py
# Import path = third_party.dummy
import uuid
class DummyStrategy(BaseStrategy):
@@ -48,14 +49,25 @@ Here is an example showing how you can write a plugin called ``DummyStrategy``:
super(DummyStrategy, self).__init__(name, description)
def execute(self, model):
self.solution.add_change_request(
Migrate(vm=my_vm, src_hypervisor=src, dest_hypervisor=dest)
)
migration_type = 'live'
src_hypervisor = 'compute-host-1'
dst_hypervisor = 'compute-host-2'
instance_id = uuid.uuid4()
parameters = {'migration_type': migration_type,
'src_hypervisor': src_hypervisor,
'dst_hypervisor': dst_hypervisor}
self.solution.add_action(action_type="migration",
resource_id=instance_id,
input_parameters=parameters)
# Do some more stuff here ...
return self.solution
As you can see in the above example, the ``execute()`` method returns a
solution as required.
As you can see in the above example, the :py:meth:`~.BaseStrategy.execute`
method returns a :py:class:`~.BaseSolution` instance as required. This solution
is what wraps the abstract set of actions the strategy recommends to you. This
solution is then processed by a :ref:`planner <planner_definition>` to produce
an action plan which shall contain the sequenced flow of actions to be
executed by the :ref:`Watcher Applier <watcher_applier_definition>`.
Please note that your strategy class will be instantiated without any
parameter. Therefore, you should make sure not to make any of them required in
@@ -65,13 +77,10 @@ your ``__init__`` method.
Abstract Plugin Class
=====================
Here below is the abstract ``BaseStrategy`` class that every single strategy
should implement:
Here below is the abstract :py:class:`~.BaseStrategy` class that every single
strategy should implement:
.. automodule:: watcher.decision_engine.strategy.strategies.base
:noindex:
.. autoclass:: BaseStrategy
.. autoclass:: watcher.decision_engine.strategy.strategies.base.BaseStrategy
:members:
:noindex:
@@ -92,11 +101,11 @@ Here below is how you would proceed to register ``DummyStrategy`` using pbr_:
[entry_points]
watcher_strategies =
dummy = third_party.dummy:DummyStrategy
dummy = thirdparty.dummy:DummyStrategy
To get a better understanding on how to implement a more advanced strategy,
have a look at the :py:class:`BasicConsolidation` class.
have a look at the :py:class:`~.BasicConsolidation` class.
.. _pbr: http://docs.openstack.org/developer/pbr/
@@ -104,12 +113,12 @@ Using strategy plugins
======================
The Watcher Decision Engine service will automatically discover any installed
plugins when it is run. If a Python package containing a custom plugin is
plugins when it is restarted. If a Python package containing a custom plugin is
installed within the same environment as Watcher, Watcher will automatically
make that plugin available for use.
At this point, the way Watcher will use your new strategy if you reference it
in the ``goals`` under the ``[watcher_goals]`` section of your ``watcher.conf``
At this point, Watcher will use your new strategy if you reference it in the
``goals`` under the ``[watcher_goals]`` section of your ``watcher.conf``
configuration file. For example, if you want to use a ``dummy`` strategy you
just installed, you would have to associate it to a goal like this:
@@ -143,13 +152,13 @@ pluggable backend.
Finally, if your strategy requires new metrics not covered by Ceilometer, you
can add them through a Ceilometer `plugin`_.
.. _`Helper`: https://github.com/openstack/watcher/blob/master/watcher/metrics_engine/cluster_history/ceilometer.py#L31
.. _`Ceilometer developer guide`: http://docs.openstack.org/developer/ceilometer/architecture.html#storing-the-data
.. _`here`: http://docs.openstack.org/developer/ceilometer/install/dbreco.html#choosing-a-database-backend
.. _`plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
Read usage metrics using the Python binding
-------------------------------------------
@@ -157,39 +166,43 @@ You can find the information about the Ceilometer Python binding on the
OpenStack `ceilometer client python API documentation
<http://docs.openstack.org/developer/python-ceilometerclient/api.html>`_
The first step is to authenticate against the Ceilometer service
(assuming that you already imported the Ceilometer client for Python)
with this call:
To facilitate the process, Watcher provides the ``osc`` attribute to every
strategy which includes clients to major OpenStack services, including
Ceilometer. So to access it within your strategy, you can do the following:
.. code-block:: py
cclient = ceilometerclient.client.get_client(VERSION, os_username=USERNAME,
os_password=PASSWORD, os_tenant_name=PROJECT_NAME, os_auth_url=AUTH_URL)
# Within your strategy "execute()"
cclient = self.osc.ceilometer
# TODO: Do something here
Using that you can now query the values for that specific metric:
.. code-block:: py
value_cpu = cclient.samples.list(meter_name='cpu_util', limit=10, q=query)
query = None # e.g. [{'field': 'foo', 'op': 'le', 'value': 34},]
value_cpu = cclient.samples.list(
meter_name='cpu_util',
limit=10, q=query)
Read usage metrics using the Watcher Cluster History Helper
-----------------------------------------------------------
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
.. automodule:: watcher.metrics_engine.cluster_history.api
:noindex:
.. autoclass:: BaseClusterHistory
.. autoclass:: watcher.metrics_engine.cluster_history.api.BaseClusterHistory
:members:
:noindex:
The following snippet code shows how to create a Cluster History class:
The following code snippet shows how to create a Cluster History class:
.. code-block:: py
query_history = CeilometerClusterHistory()
from watcher.metrics_engine.cluster_history import ceilometer as ceil
query_history = ceil.CeilometerClusterHistory()
Using that you can now query the values for that specific metric:
@@ -200,4 +213,3 @@ Using that you can now query the values for that specific metric:
period="7200",
aggregate='avg'
)

View File

@@ -0,0 +1,38 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
=================
Available Plugins
=================
.. _watcher_strategies:
Strategies
==========
.. drivers-doc:: watcher_strategies
.. _watcher_actions:
Actions
=======
.. drivers-doc:: watcher_actions
.. _watcher_workflow_engines:
Workflow Engines
================
.. drivers-doc:: watcher_workflow_engines
.. _watcher_planners:
Planners
========
.. drivers-doc:: watcher_planners

View File

@@ -0,0 +1,50 @@
..
Except where otherwise noted, this document is licensed under Creative
Commons Attribution 3.0 License. You can view the license at:
https://creativecommons.org/licenses/by/3.0/
=======
Testing
=======
.. _unit_tests:
Unit tests
==========
All unit tests should be run using `tox`_. To run the same unit tests that are
executing onto `Gerrit`_ which includes ``py34``, ``py27`` and ``pep8``, you
can issue the following command::
$ workon watcher
(watcher) $ pip install tox
(watcher) $ cd watcher
(watcher) $ tox
If you want to only run one of the aforementioned, you can then issue one of
the following::
$ workon watcher
(watcher) $ tox -e py34
(watcher) $ tox -e py27
(watcher) $ tox -e pep8
.. _tox: https://tox.readthedocs.org/
.. _Gerrit: http://review.openstack.org/
You may pass options to the test programs using positional arguments. To run a
specific unit test, you can pass extra options to `os-testr`_ after putting
the ``--`` separator. So using the ``-r`` option followed by a regex string,
you can run the desired test::
$ workon watcher
(watcher) $ tox -e py27 -- -r watcher.tests.api
.. _os-testr: http://docs.openstack.org/developer/os-testr/
When you're done, deactivate the virtualenv::
$ deactivate
.. include:: ../../../watcher_tempest_plugin/README.rst

View File

@@ -213,27 +213,27 @@ Here are some examples of
It can be any of the `the official list of available resource types defined in OpenStack for HEAT <http://docs.openstack.org/developer/heat/template_guide/openstack.html>`_.
.. _efficiency_definition:
.. _efficacy_definition:
Optimization Efficiency
=======================
Optimization Efficacy
=====================
The :ref:`Optimization Efficiency <efficiency_definition>` is the objective
The :ref:`Optimization Efficacy <efficacy_definition>` is the objective
measure of how much of the :ref:`Goal <goal_definition>` has been achieved in
respect with constraints and :ref:`SLAs <sla_definition>` defined by the
:ref:`Customer <customer_definition>`.
The way efficiency is evaluated will depend on the
The way efficacy is evaluated will depend on the
:ref:`Goal <goal_definition>` to achieve.
Of course, the efficiency will be relevant only as long as the
Of course, the efficacy will be relevant only as long as the
:ref:`Action Plan <action_plan_definition>` is relevant
(i.e., the current state of the :ref:`Cluster <cluster_definition>`
has not changed in a way that a new :ref:`Audit <audit_definition>` would need
to be launched).
For example, if the :ref:`Goal <goal_definition>` is to lower the energy
consumption, the :ref:`Efficiency <efficiency_definition>` will be computed
consumption, the :ref:`Efficacy <efficacy_definition>` will be computed
using several indicators (KPIs):
- the percentage of energy gain (which must be the highest possible)
@@ -244,7 +244,7 @@ using several indicators (KPIs):
All those indicators (KPIs) are computed within a given timeframe, which is the
time taken to execute the whole :ref:`Action Plan <action_plan_definition>`.
The efficiency also enables the :ref:`Administrator <administrator_definition>`
The efficacy also enables the :ref:`Administrator <administrator_definition>`
to objectively compare different :ref:`Strategies <strategy_definition>` for
the same goal and same workload of the :ref:`Cluster <cluster_definition>`.

View File

@@ -1,15 +1,15 @@
@startuml
[*] --> RECOMMENDED: The Watcher Planner\ncreates the Action Plan
RECOMMENDED --> TRIGGERED: Administrator launches\nthe Action Plan
TRIGGERED --> ONGOING: The Watcher Applier receives the request\nto launch the Action Plan
RECOMMENDED --> PENDING: Adminisrator launches\nthe Action Plan
PENDING --> ONGOING: The Watcher Applier receives the request\nto launch the Action Plan
ONGOING --> FAILED: Something failed while executing\nthe Action Plan in the Watcher Applier
ONGOING --> SUCCEEDED: The Watcher Applier executed\nthe Action Plan successfully
FAILED --> DELETED : Administrator removes\nAction Plan
SUCCEEDED --> DELETED : Administrator removes\nAction Plan
ONGOING --> CANCELLED : Administrator cancels\nAction Plan
RECOMMENDED --> CANCELLED : Administrator cancels\nAction Plan
TRIGGERED --> CANCELLED : Administrator cancels\nAction Plan
PENDING --> CANCELLED : Administrator cancels\nAction Plan
CANCELLED --> DELETED
DELETED --> [*]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -22,6 +22,7 @@ Watcher project consists of several source code repositories:
* `watcher`_ - is the main repository. It contains code for Watcher API server,
Watcher Decision Engine and Watcher Applier.
* `python-watcherclient`_ - Client library and CLI client for Watcher.
* `watcher-dashboard`_ - Watcher Horizon plugin.
The documentation provided here is continually kept up-to-date based
on the latest code, and may not represent the state of the project at any
@@ -29,6 +30,7 @@ specific prior release.
.. _watcher: https://git.openstack.org/cgit/openstack/watcher/
.. _python-watcherclient: https://git.openstack.org/cgit/openstack/python-watcherclient/
.. _watcher-dashboard: https://git.openstack.org/cgit/openstack/watcher-dashboard/
Developer Guide
===============
@@ -53,7 +55,8 @@ Getting Started
dev/environment
dev/devstack
deploy/configuration
deploy/conf-files
dev/testing
API References
--------------
@@ -69,7 +72,11 @@ Plugins
.. toctree::
:maxdepth: 1
dev/strategy-plugin
dev/plugin/base-setup
dev/plugin/strategy-plugin
dev/plugin/action-plugin
dev/plugin/planner-plugin
dev/plugins
Admin Guide

View File

@@ -52,7 +52,7 @@ run the following::
Show the program's version number and exit.
.. option:: upgrade, downgrade, stamp, revision, version, create_schema
.. option:: upgrade, downgrade, stamp, revision, version, create_schema, purge
The :ref:`command <db-manage_cmds>` to run.
@@ -219,3 +219,42 @@ version
Show help for version and exit.
This command will output the current database version.
purge
-----
.. program:: purge
.. option:: -h, --help
Show help for purge and exit.
.. option:: -d, --age-in-days
The number of days (starting from today) before which we consider soft
deleted objects as expired and should hence be erased. By default, all
objects soft deleted are considered expired. This can be useful as removing
a significant amount of objects may cause a performance issues.
.. option:: -n, --max-number
The maximum number of database objects we expect to be deleted. If exceeded,
this will prevent any deletion.
.. option:: -t, --audit-template
Either the UUID or name of the soft deleted audit template to purge. This
will also include any related objects with it.
.. option:: -e, --exclude-orphans
This is a flag to indicate when we want to exclude orphan objects from
deletion.
.. option:: --dry-run
This is a flag to indicate when we want to perform a dry run. This will show
the objects that would be deleted instead of actually deleting them.
This command will purge the current database by removing both its soft deleted
and orphan objects.

View File

@@ -8,6 +8,19 @@
RESTful Web API (v1)
====================
Goals
=====
.. rest-controller:: watcher.api.controllers.v1.goal:GoalsController
:webprefix: /v1/goal
.. autotype:: watcher.api.controllers.v1.goal.GoalCollection
:members:
.. autotype:: watcher.api.controllers.v1.goal.Goal
:members:
Audit Templates
===============

View File

@@ -0,0 +1,4 @@
To generate the sample watcher.conf file, run the following
command from the top level of the watcher directory:
tox -econfig

View File

@@ -0,0 +1,9 @@
[DEFAULT]
output_file = etc/watcher/watcher.conf.sample
wrap_width = 79
namespace = watcher
namespace = keystonemiddleware.auth_token
namespace = oslo.log
namespace = oslo.db
namespace = oslo.messaging

View File

@@ -1,908 +0,0 @@
[DEFAULT]
#
# From oslo.log
#
# Enables or disables fatal status of deprecations. (boolean value)
#fatal_deprecations = false
# DEPRECATED. A logging.Formatter log message format string which may
# use any of the available logging.LogRecord attributes. This option
# is deprecated. Please use logging_context_format_string and
# logging_default_format_string instead. This option is ignored if
# log_config_append is set. (string value)
#log_format = <None>
# Defines the format string for %%(asctime)s in log records. Default:
# %(default)s . This option is ignored if log_config_append is set.
# (string value)
#log_date_format = %Y-%m-%d %H:%M:%S
# Enables or disables publication of error events. (boolean value)
#publish_errors = false
# (Optional) Name of log file to send logging output to. If no default
# is set, logging will go to stderr as defined by use_stderr. This
# option is ignored if log_config_append is set. (string value)
# Deprecated group/name - [DEFAULT]/logfile
#log_file = <None>
# (Optional) The base directory used for relative log_file paths.
# This option is ignored if log_config_append is set. (string value)
# Deprecated group/name - [DEFAULT]/logdir
#log_dir = <None>
# Uses logging handler designed to watch file system. When log file is
# moved or removed this handler will open a new log file with
# specified path instantaneously. It makes sense only if log_file
# option is specified and Linux platform is used. This option is
# ignored if log_config_append is set. (boolean value)
#watch_log_file = false
# Use syslog for logging. Existing syslog format is DEPRECATED and
# will be changed later to honor RFC5424. This option is ignored if
# log_config_append is set. (boolean value)
#use_syslog = false
# Enables or disables syslog rfc5424 format for logging. If enabled,
# prefixes the MSG part of the syslog message with APP-NAME (RFC5424).
# The format without the APP-NAME is deprecated in Kilo, and will be
# removed in Mitaka, along with this option. This option is ignored if
# log_config_append is set. (boolean value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
#use_syslog_rfc_format = true
# Syslog facility to receive log lines. This option is ignored if
# log_config_append is set. (string value)
#syslog_log_facility = LOG_USER
# Log output to standard error. This option is ignored if
# log_config_append is set. (boolean value)
#use_stderr = true
# Format string to use for log messages with context. (string value)
#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
# Format string to use for log messages when context is undefined.
# (string value)
#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
# Additional data to append to log message when logging level for the
# message is DEBUG. (string value)
#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d
# Prefix each line of exception output with this format. (string
# value)
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
# Defines the format string for %(user_identity)s that is used in
# logging_context_format_string. (string value)
#logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s
# If set to false, the logging level will be set to WARNING instead of
# the default INFO level. (boolean value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
#verbose = true
# List of package logging levels in logger=LEVEL pairs. This option is
# ignored if log_config_append is set. (list value)
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN
# If set to true, the logging level will be set to DEBUG instead of
# the default INFO level. (boolean value)
#debug = false
# The format for an instance that is passed with the log message.
# (string value)
#instance_format = "[instance: %(uuid)s] "
# The name of a logging configuration file. This file is appended to
# any existing logging configuration files. For details about logging
# configuration files, see the Python logging module documentation.
# Note that when logging configuration files are used all logging
# configuration is defined in the configuration file and other logging
# configuration options are ignored (for example, log_format). (string
# value)
# Deprecated group/name - [DEFAULT]/log_config
#log_config_append = <None>
# The format for an instance UUID that is passed with the log message.
# (string value)
#instance_uuid_format = "[instance: %(uuid)s] "
#
# From oslo.messaging
#
# The default number of seconds that poll should wait. Poll raises
# timeout exception when timeout expired. (integer value)
#rpc_poll_timeout = 1
# Name of this node. Must be a valid hostname, FQDN, or IP address.
# Must match "host" option, if running Nova. (string value)
#rpc_zmq_host = localhost
# Configures zmq-messaging to use proxy with non PUB/SUB patterns.
# (boolean value)
#direct_over_proxy = true
# AMQP topic used for OpenStack notifications. (list value)
# Deprecated group/name - [rpc_notifier2]/topics
# Deprecated group/name - [DEFAULT]/notification_topics
#topics = notifications
# Use PUB/SUB pattern for fanout methods. PUB/SUB always uses proxy.
# (boolean value)
#use_pub_sub = true
# Minimal port number for random ports range. (port value)
# Minimum value: 0
# Maximum value: 65535
#rpc_zmq_min_port = 49152
# Seconds to wait for a response from a call. (integer value)
#rpc_response_timeout = 60
# Maximal port number for random ports range. (integer value)
# Minimum value: 1
# Maximum value: 65536
#rpc_zmq_max_port = 65536
# A URL representing the messaging driver to use and its full
# configuration. If not set, we fall back to the rpc_backend option
# and driver specific configuration. (string value)
#transport_url = <None>
# Use this port to connect to redis host. (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 6379
# Number of retries to find free port number before fail with
# ZMQBindError. (integer value)
#rpc_zmq_bind_port_retries = 100
# Size of RPC connection pool. (integer value)
# Deprecated group/name - [DEFAULT]/rpc_conn_pool_size
#rpc_conn_pool_size = 30
# The messaging driver to use, defaults to rabbit. Other drivers
# include amqp and zmq. (string value)
#rpc_backend = rabbit
# Host to locate redis. (string value)
#host = 127.0.0.1
# The default exchange under which topics are scoped. May be
# overridden by an exchange name specified in the transport_url
# option. (string value)
#control_exchange = openstack
# MatchMaker driver. (string value)
#rpc_zmq_matchmaker = redis
# Type of concurrency used. Either "native" or "eventlet" (string
# value)
#rpc_zmq_concurrency = eventlet
# Password for Redis server (optional). (string value)
#password =
# Number of ZeroMQ contexts, defaults to 1. (integer value)
#rpc_zmq_contexts = 1
# Maximum number of ingress messages to locally buffer per topic.
# Default is unlimited. (integer value)
#rpc_zmq_topic_backlog = <None>
# Size of executor thread pool. (integer value)
# Deprecated group/name - [DEFAULT]/rpc_thread_pool_size
#executor_thread_pool_size = 64
# Directory for holding IPC sockets. (string value)
#rpc_zmq_ipc_dir = /var/run/openstack
# The Drivers(s) to handle sending notifications. Possible values are
# messaging, messagingv2, routing, log, test, noop (multi valued)
# Deprecated group/name - [DEFAULT]/notification_driver
#driver =
# ZeroMQ bind address. Should be a wildcard (*), an ethernet
# interface, or IP. The "host" option should point or resolve to this
# address. (string value)
#rpc_zmq_bind_address = *
# Seconds to wait before a cast expires (TTL). Only supported by
# impl_zmq. (integer value)
#rpc_cast_timeout = 30
# A URL representing the messaging driver to use for notifications. If
# not set, we fall back to the same configuration used for RPC.
# (string value)
# Deprecated group/name - [DEFAULT]/notification_transport_url
#transport_url = <None>
[api]
#
# From watcher
#
# The listen IP for the watcher API server (string value)
#host = 0.0.0.0
# The port for the watcher API server (integer value)
#port = 9322
# The maximum number of items returned in a single response from a
# collection resource. (integer value)
#max_limit = 1000
[ceilometer_client]
#
# From watcher
#
# Version of Ceilometer API to use in ceilometerclient. (string value)
#api_version = 2
[cinder_client]
#
# From watcher
#
# Version of Cinder API to use in cinderclient. (string value)
#api_version = 2
[database]
#
# From oslo.db
#
# If set, use this value for max_overflow with SQLAlchemy. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_max_overflow
# Deprecated group/name - [DATABASE]/sqlalchemy_max_overflow
#max_overflow = <None>
# Maximum number of SQL connections to keep open in a pool. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_max_pool_size
# Deprecated group/name - [DATABASE]/sql_max_pool_size
#max_pool_size = <None>
# Interval between retries of opening a SQL connection. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_retry_interval
# Deprecated group/name - [DATABASE]/reconnect_interval
#retry_interval = 10
# Enable the experimental use of database reconnect on connection
# lost. (boolean value)
#use_db_reconnect = false
# Minimum number of SQL connections to keep open in a pool. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_min_pool_size
# Deprecated group/name - [DATABASE]/sql_min_pool_size
#min_pool_size = 1
# If True, SQLite uses synchronous mode. (boolean value)
# Deprecated group/name - [DEFAULT]/sqlite_synchronous
#sqlite_synchronous = true
# Maximum number of database connection retries during startup. Set to
# -1 to specify an infinite retry count. (integer value)
# Deprecated group/name - [DEFAULT]/sql_max_retries
# Deprecated group/name - [DATABASE]/sql_max_retries
#max_retries = 10
# Verbosity of SQL debugging information: 0=None, 100=Everything.
# (integer value)
# Deprecated group/name - [DEFAULT]/sql_connection_debug
#connection_debug = 0
# The SQL mode to be used for MySQL sessions. This option, including
# the default, overrides any server-set SQL mode. To use whatever SQL
# mode is set by the server configuration, set this to no value.
# Example: mysql_sql_mode= (string value)
#mysql_sql_mode = TRADITIONAL
# Maximum retries in case of connection error or deadlock error before
# error is raised. Set to -1 to specify an infinite retry count.
# (integer value)
#db_max_retries = 20
# The back end to use for the database. (string value)
# Deprecated group/name - [DEFAULT]/db_backend
#backend = sqlalchemy
# Seconds between retries of a database transaction. (integer value)
#db_retry_interval = 1
# Timeout before idle SQL connections are reaped. (integer value)
# Deprecated group/name - [DEFAULT]/sql_idle_timeout
# Deprecated group/name - [DATABASE]/sql_idle_timeout
# Deprecated group/name - [sql]/idle_timeout
#idle_timeout = 3600
# Add Python stack traces to SQL as comment strings. (boolean value)
# Deprecated group/name - [DEFAULT]/sql_connection_trace
#connection_trace = false
# The file name to use with SQLite. (string value)
# Deprecated group/name - [DEFAULT]/sqlite_db
#sqlite_db = oslo.sqlite
# The SQLAlchemy connection string to use to connect to the database.
# (string value)
# Deprecated group/name - [DEFAULT]/sql_connection
# Deprecated group/name - [DATABASE]/sql_connection
# Deprecated group/name - [sql]/connection
#connection = <None>
# If True, increases the interval between retries of a database
# operation up to db_max_retry_interval. (boolean value)
#db_inc_retry_interval = true
# If db_inc_retry_interval is set, the maximum seconds between retries
# of a database operation. (integer value)
#db_max_retry_interval = 10
# If set, use this value for pool_timeout with SQLAlchemy. (integer
# value)
# Deprecated group/name - [DATABASE]/sqlalchemy_pool_timeout
#pool_timeout = <None>
# The SQLAlchemy connection string to use to connect to the slave
# database. (string value)
#slave_connection = <None>
[glance_client]
#
# From watcher
#
# Version of Glance API to use in glanceclient. (string value)
#api_version = 2
[keystone_authtoken]
#
# From keystonemiddleware.auth_token
#
# The region in which the identity server can be found. (string value)
#region_name = <None>
# Directory used to cache files related to PKI tokens. (string value)
#signing_dir = <None>
# Used to control the use and type of token binding. Can be set to:
# "disabled" to not check token binding. "permissive" (default) to
# validate binding information if the bind type is of a form known to
# the server and ignore it if not. "strict" like "permissive" but if
# the bind type is unknown the token will be rejected. "required" any
# form of token binding is needed to be allowed. Finally the name of a
# binding method that must be present in tokens. (string value)
#enforce_token_bind = permissive
# Optionally specify a list of memcached server(s) to use for caching.
# If left undefined, tokens will instead be cached in-process. (list
# value)
# Deprecated group/name - [DEFAULT]/memcache_servers
#memcached_servers = <None>
# Env key for the swift cache. (string value)
#cache = <None>
# If true, the revocation list will be checked for cached tokens. This
# requires that PKI tokens are configured on the identity server.
# (boolean value)
#check_revocations_for_cached = false
# Hash algorithms to use for hashing PKI tokens. This may be a single
# algorithm or multiple. The algorithms are those supported by Python
# standard hashlib.new(). The hashes will be tried in the order given,
# so put the preferred one first for performance. The result of the
# first hash will be stored in the cache. This will typically be set
# to multiple values only while migrating from a less secure algorithm
# to a more secure one. Once all the old tokens are expired this
# option should be set to a single value for better performance. (list
# value)
#hash_algorithms = md5
# In order to prevent excessive effort spent validating tokens, the
# middleware caches previously-seen tokens for a configurable duration
# (in seconds). Set to -1 to disable caching completely. (integer
# value)
#token_cache_time = 300
# Determines the frequency at which the list of revoked tokens is
# retrieved from the Identity service (in seconds). A high number of
# revocation events combined with a low cache duration may
# significantly reduce performance. (integer value)
#revocation_cache_time = 10
# Authentication type to load (unknown value)
# Deprecated group/name - [DEFAULT]/auth_plugin
#auth_type = <None>
# (Optional) If defined, indicate whether token data should be
# authenticated or authenticated and encrypted. If MAC, token data is
# authenticated (with HMAC) in the cache. If ENCRYPT, token data is
# encrypted and authenticated in the cache. If the value is not one of
# these options or empty, auth_token will raise an exception on
# initialization. (string value)
# Allowed values: None, MAC, ENCRYPT
#memcache_security_strategy = None
# (Optional) Indicate whether to set the X-Service-Catalog header. If
# False, middleware will not ask for service catalog on token
# validation and will not set the X-Service-Catalog header. (boolean
# value)
#include_service_catalog = true
# (Optional, mandatory if memcache_security_strategy is defined) This
# string is used for key derivation. (string value)
#memcache_secret_key = <None>
# Config Section from which to load plugin specific options (unknown
# value)
#auth_section = <None>
# (Optional) Number of seconds memcached server is considered dead
# before it is tried again. (integer value)
#memcache_pool_dead_retry = 300
# (Optional) Maximum total number of open connections to every
# memcached server. (integer value)
#memcache_pool_maxsize = 10
# How many times are we trying to reconnect when communicating with
# Identity API Server. (integer value)
#http_request_max_retries = 3
# Request timeout value for communicating with Identity API server.
# (integer value)
#http_connect_timeout = <None>
# (Optional) Socket timeout in seconds for communicating with a
# memcached server. (integer value)
#memcache_pool_socket_timeout = 3
# Required if identity server requires client certificate (string
# value)
#certfile = <None>
# (Optional) Number of seconds a connection to memcached is held
# unused in the pool before it is closed. (integer value)
#memcache_pool_unused_timeout = 60
# Required if identity server requires client certificate (string
# value)
#keyfile = <None>
# Do not handle authorization requests within the middleware, but
# delegate the authorization decision to downstream WSGI components.
# (boolean value)
#delay_auth_decision = false
# (Optional) Number of seconds that an operation will wait to get a
# memcached client connection from the pool. (integer value)
#memcache_pool_conn_get_timeout = 10
# Complete public Identity API endpoint. (string value)
#auth_uri = <None>
# A PEM encoded Certificate Authority to use when verifying HTTPs
# connections. Defaults to system CAs. (string value)
#cafile = <None>
# API version of the admin Identity API endpoint. (string value)
#auth_version = <None>
# (Optional) Use the advanced (eventlet safe) memcached client pool.
# The advanced pool will only work under python 2.x. (boolean value)
#memcache_use_advanced_pool = false
# Verify HTTPS connections. (boolean value)
#insecure = false
[matchmaker_redis]
#
# From oslo.messaging
#
# Host to locate redis. (string value)
#host = 127.0.0.1
# Use this port to connect to redis host. (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 6379
# Password for Redis server (optional). (string value)
#password =
[neutron_client]
#
# From watcher
#
# Version of Neutron API to use in neutronclient. (string value)
#api_version = 2
[nova_client]
#
# From watcher
#
# Version of Nova API to use in novaclient. (string value)
#api_version = 2
[oslo_messaging_amqp]
#
# From oslo.messaging
#
# User name for message broker authentication (string value)
# Deprecated group/name - [amqp1]/username
#username =
# Private key PEM file used to sign cert_file certificate (string
# value)
# Deprecated group/name - [amqp1]/ssl_key_file
#ssl_key_file =
# address prefix used when broadcasting to all servers (string value)
# Deprecated group/name - [amqp1]/broadcast_prefix
#broadcast_prefix = broadcast
# Debug: dump AMQP frames to stdout (boolean value)
# Deprecated group/name - [amqp1]/trace
#trace = false
# Identifying certificate PEM file to present to clients (string
# value)
# Deprecated group/name - [amqp1]/ssl_cert_file
#ssl_cert_file =
# Space separated list of acceptable SASL mechanisms (string value)
# Deprecated group/name - [amqp1]/sasl_mechanisms
#sasl_mechanisms =
# address prefix when sending to any server in group (string value)
# Deprecated group/name - [amqp1]/group_request_prefix
#group_request_prefix = unicast
# Password for decrypting ssl_key_file (if encrypted) (string value)
# Deprecated group/name - [amqp1]/ssl_key_password
#ssl_key_password = <None>
# Name of configuration file (without .conf suffix) (string value)
# Deprecated group/name - [amqp1]/sasl_config_name
#sasl_config_name =
# Password for message broker authentication (string value)
# Deprecated group/name - [amqp1]/password
#password =
# address prefix used when sending to a specific server (string value)
# Deprecated group/name - [amqp1]/server_request_prefix
#server_request_prefix = exclusive
# Name for the AMQP container (string value)
# Deprecated group/name - [amqp1]/container_name
#container_name = <None>
# CA certificate PEM file to verify server certificate (string value)
# Deprecated group/name - [amqp1]/ssl_ca_file
#ssl_ca_file =
# Path to directory that contains the SASL configuration (string
# value)
# Deprecated group/name - [amqp1]/sasl_config_dir
#sasl_config_dir =
# Timeout for inactive connections (in seconds) (integer value)
# Deprecated group/name - [amqp1]/idle_timeout
#idle_timeout = 0
# Accept clients using either SSL or plain TCP (boolean value)
# Deprecated group/name - [amqp1]/allow_insecure_clients
#allow_insecure_clients = false
[oslo_messaging_rabbit]
#
# From oslo.messaging
#
# SSL cert file (valid only if SSL enabled). (string value)
# Deprecated group/name - [DEFAULT]/kombu_ssl_certfile
#kombu_ssl_certfile =
# Use HA queues in RabbitMQ (x-ha-policy: all). If you change this
# option, you must wipe the RabbitMQ database. (boolean value)
# Deprecated group/name - [DEFAULT]/rabbit_ha_queues
#rabbit_ha_queues = false
# Deprecated, use rpc_backend=kombu+memory or rpc_backend=fake
# (boolean value)
# Deprecated group/name - [DEFAULT]/fake_rabbit
#fake_rabbit = false
# Number of seconds after which the Rabbit broker is considered down
# if heartbeat's keep-alive fails (0 disable the heartbeat).
# EXPERIMENTAL (integer value)
#heartbeat_timeout_threshold = 60
# How long to wait before reconnecting in response to an AMQP consumer
# cancel notification. (floating point value)
# Deprecated group/name - [DEFAULT]/kombu_reconnect_delay
#kombu_reconnect_delay = 1.0
# How long to wait a missing client beforce abandoning to send it its
# replies. This value should not be longer than rpc_response_timeout.
# (integer value)
# Deprecated group/name - [DEFAULT]/kombu_reconnect_timeout
#kombu_missing_consumer_retry_timeout = 60
# How often times during the heartbeat_timeout_threshold we check the
# heartbeat. (integer value)
#heartbeat_rate = 2
# The RabbitMQ broker address where a single node is used. (string
# value)
# Deprecated group/name - [DEFAULT]/rabbit_host
#rabbit_host = localhost
# The RabbitMQ broker port where a single node is used. (port value)
# Minimum value: 0
# Maximum value: 65535
# Deprecated group/name - [DEFAULT]/rabbit_port
#rabbit_port = 5672
# Use durable queues in AMQP. (boolean value)
# Deprecated group/name - [DEFAULT]/amqp_durable_queues
# Deprecated group/name - [DEFAULT]/rabbit_durable_queues
#amqp_durable_queues = false
# Determines how the next RabbitMQ node is chosen in case the one we
# are currently connected to becomes unavailable. Takes effect only if
# more than one RabbitMQ node is provided in config. (string value)
# Allowed values: round-robin, shuffle
#kombu_failover_strategy = round-robin
# RabbitMQ HA cluster host:port pairs. (list value)
# Deprecated group/name - [DEFAULT]/rabbit_hosts
#rabbit_hosts = $rabbit_host:$rabbit_port
# Connect over SSL for RabbitMQ. (boolean value)
# Deprecated group/name - [DEFAULT]/rabbit_use_ssl
#rabbit_use_ssl = false
# SSL certification authority file (valid only if SSL enabled).
# (string value)
# Deprecated group/name - [DEFAULT]/kombu_ssl_ca_certs
#kombu_ssl_ca_certs =
# The RabbitMQ userid. (string value)
# Deprecated group/name - [DEFAULT]/rabbit_userid
#rabbit_userid = guest
# The RabbitMQ password. (string value)
# Deprecated group/name - [DEFAULT]/rabbit_password
#rabbit_password = guest
# The RabbitMQ login method. (string value)
# Deprecated group/name - [DEFAULT]/rabbit_login_method
#rabbit_login_method = AMQPLAIN
# The RabbitMQ virtual host. (string value)
# Deprecated group/name - [DEFAULT]/rabbit_virtual_host
#rabbit_virtual_host = /
# Auto-delete queues in AMQP. (boolean value)
# Deprecated group/name - [DEFAULT]/amqp_auto_delete
#amqp_auto_delete = false
# How frequently to retry connecting with RabbitMQ. (integer value)
#rabbit_retry_interval = 1
# SSL version to use (valid only if SSL enabled). Valid values are
# TLSv1 and SSLv23. SSLv2, SSLv3, TLSv1_1, and TLSv1_2 may be
# available on some distributions. (string value)
# Deprecated group/name - [DEFAULT]/kombu_ssl_version
#kombu_ssl_version =
# How long to backoff for between retries when connecting to RabbitMQ.
# (integer value)
# Deprecated group/name - [DEFAULT]/rabbit_retry_backoff
#rabbit_retry_backoff = 2
# SSL key file (valid only if SSL enabled). (string value)
# Deprecated group/name - [DEFAULT]/kombu_ssl_keyfile
#kombu_ssl_keyfile =
# Maximum number of RabbitMQ connection retries. Default is 0
# (infinite retry count). (integer value)
# Deprecated group/name - [DEFAULT]/rabbit_max_retries
#rabbit_max_retries = 0
[watcher_applier]
#
# From watcher
#
# The topic name used forcontrol events, this topic used for rpc call
# (string value)
#topic_control = watcher.applier.control
# Number of workers for applier, default value is 1. (integer value)
# Minimum value: 1
#workers = 1
# The identifier used by watcher module on the message broker (string
# value)
#publisher_id = watcher.applier.api
# Select the engine to use to execute the workflow (string value)
#workflow_engine = taskflow
# The topic name used for status events, this topic is used so as to
# notifythe others components of the system (string value)
#topic_status = watcher.applier.status
[watcher_clients_auth]
#
# From watcher
#
# Authentication URL (unknown value)
#auth_url = <None>
# Domain ID to scope to (unknown value)
#domain_id = <None>
# Domain name to scope to (unknown value)
#domain_name = <None>
# Domain ID containing project (unknown value)
#project_domain_id = <None>
# Project ID to scope to (unknown value)
# Deprecated group/name - [DEFAULT]/tenant-id
#project_id = <None>
# Project name to scope to (unknown value)
# Deprecated group/name - [DEFAULT]/tenant-name
#project_name = <None>
# PEM encoded client certificate cert file (string value)
#certfile = <None>
# PEM encoded client certificate key file (string value)
#keyfile = <None>
# Domain name containing project (unknown value)
#project_domain_name = <None>
# Trust ID (unknown value)
#trust_id = <None>
# Optional domain ID to use with v3 and v2 parameters. It will be used
# for both the user and project domain in v3 and ignored in v2
# authentication. (unknown value)
#default_domain_id = <None>
# Timeout value for http requests (integer value)
#timeout = <None>
# Optional domain name to use with v3 API and v2 parameters. It will
# be used for both the user and project domain in v3 and ignored in v2
# authentication. (unknown value)
#default_domain_name = <None>
# User id (unknown value)
#user_id = <None>
# Username (unknown value)
# Deprecated group/name - [DEFAULT]/username
#username = <None>
# Verify HTTPS connections. (boolean value)
#insecure = false
# User's domain name (unknown value)
#user_domain_name = <None>
# Authentication type to load (unknown value)
# Deprecated group/name - [DEFAULT]/auth_plugin
#auth_type = <None>
# Config Section from which to load plugin specific options (unknown
# value)
#auth_section = <None>
# User's password (unknown value)
#password = <None>
# User's domain id (unknown value)
#user_domain_id = <None>
# PEM encoded Certificate Authority to use when verifying HTTPs
# connections. (string value)
#cafile = <None>
[watcher_decision_engine]
#
# From watcher
#
# The topic name used for status events, this topic is used so as to
# notifythe others components of the system (string value)
#topic_status = watcher.decision.status
# The topic name used forcontrol events, this topic used for rpc call
# (string value)
#topic_control = watcher.decision.control
# The identifier used by watcher module on the message broker (string
# value)
#publisher_id = watcher.decision.api
# The maximum number of threads that can be used to execute strategies
# (integer value)
#max_workers = 2
[watcher_goals]
#
# From watcher
#
# Goals used for the optimization. Maps each goal to an associated
# strategy (for example: BASIC_CONSOLIDATION:basic,
# MY_GOAL:my_strategy_1) (dict value)
#goals = DUMMY:dummy
[watcher_planner]
#
# From watcher
#
# The selected planner used to schedule the actions (string value)
#planner = default

View File

@@ -2,30 +2,34 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
enum34;python_version=='2.7' or python_version=='2.6'
jsonpatch>=1.1
keystoneauth1>=2.1.0
keystonemiddleware>=2.0.0,!=2.4.0
oslo.config>=2.3.0 # Apache-2.0
oslo.db>=2.4.1 # Apache-2.0
oslo.i18n>=1.5.0 # Apache-2.0
oslo.log>=1.8.0 # Apache-2.0
oslo.messaging>=1.16.0,!=1.17.0,!=1.17.1,!=2.6.0,!=2.6.1 # Apache-2.0
oslo.policy>=0.5.0 # Apache-2.0
oslo.service>=0.7.0 # Apache-2.0
oslo.utils>=2.0.0,!=2.6.0 # Apache-2.0
PasteDeploy>=1.5.0
pbr>=1.6
pecan>=1.0.0
python-ceilometerclient>=1.5.0
python-cinderclient>=1.3.1
python-glanceclient>=0.18.0
python-keystoneclient>=1.6.0,!=1.8.0
python-neutronclient>=2.6.0
python-novaclient>=2.28.1,!=2.33.0
python-openstackclient>=1.5.0
six>=1.9.0
SQLAlchemy>=0.9.9,<1.1.0
stevedore>=1.5.0 # Apache-2.0
taskflow>=1.25.0 # Apache-2.0
WSME>=0.7
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
jsonpatch>=1.1 # BSD
keystoneauth1>=2.1.0 # Apache-2.0
keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0
oslo.config>=3.7.0 # Apache-2.0
oslo.context>=0.2.0 # Apache-2.0
oslo.db>=4.1.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.messaging>=4.0.0 # Apache-2.0
oslo.policy>=0.5.0 # Apache-2.0
oslo.service>=1.0.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0
PasteDeploy>=1.5.0 # MIT
pbr>=1.6 # Apache-2.0
pecan>=1.0.0 # BSD
PrettyTable<0.8,>=0.7 # BSD
voluptuous>=0.8.6 # BSD License
python-ceilometerclient>=2.2.1 # Apache-2.0
python-cinderclient>=1.3.1 # Apache-2.0
python-glanceclient>=2.0.0 # Apache-2.0
python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0
python-neutronclient!=4.1.0,>=2.6.0 # Apache-2.0
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
python-openstackclient>=2.1.0 # Apache-2.0
six>=1.9.0 # MIT
SQLAlchemy<1.1.0,>=1.0.10 # MIT
stevedore>=1.5.0 # Apache-2.0
taskflow>=1.26.0 # Apache-2.0
WebOb>=1.2.3 # MIT
WSME>=0.8 # MIT

View File

@@ -49,6 +49,7 @@ watcher_strategies =
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
watcher_actions =
migrate = watcher.applier.actions.migration:Migrate
@@ -63,7 +64,8 @@ watcher_planners =
default = watcher.decision_engine.planner.default:DefaultPlanner
[pbr]
autodoc_index_modules = True
warnerrors = true
autodoc_index_modules = true
autodoc_exclude_modules =
watcher.db.sqlalchemy.alembic.env
watcher.db.sqlalchemy.alembic.versions.*

3
setup.py Executable file → Normal file
View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,5 +25,5 @@ except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
setup_requires=['pbr>=1.8'],
pbr=True)

View File

@@ -2,23 +2,20 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
coverage>=3.6
discover
coverage>=3.6 # Apache-2.0
discover # BSD
doc8 # Apache-2.0
hacking>=0.10.2,<0.11
mock>=1.2
oslotest>=1.10.0 # Apache-2.0
os-testr>=0.1.0
python-subunit>=0.0.18
testrepository>=0.0.18
testscenarios>=0.4
testtools>=1.4.0
freezegun # Apache-2.0
hacking<0.11,>=0.10.2
mock>=1.2 # BSD
oslotest>=1.10.0 # Apache-2.0
os-testr>=0.4.1 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
# Doc requirements
oslosphinx>=2.5.0 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
sphinxcontrib-pecanwsme>=0.8
# For PyPI distribution
twine
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
sphinxcontrib-pecanwsme>=0.8 # Apache-2.0

16
tox.ini
View File

@@ -26,7 +26,7 @@ setenv = PYTHONHASHSEED=0
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --omit="watcher/tests/*" --testr-args='{posargs}'
commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:docs]
setenv = PYTHONHASHSEED=0
@@ -35,17 +35,12 @@ commands =
python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
commands = oslo_debug_helper -t watcher/tests {posargs}
[testenv:config]
sitepackages = False
commands =
oslo-config-generator --namespace watcher \
--namespace keystonemiddleware.auth_token \
--namespace oslo.log \
--namespace oslo.db \
--namespace oslo.messaging \
--output-file etc/watcher/watcher.conf.sample
oslo-config-generator --config-file etc/watcher/watcher-config-generator.conf
[flake8]
show-source=True
@@ -53,11 +48,6 @@ ignore=
builtins= _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
[testenv:pypi]
commands =
python setup.py sdist bdist_wheel
twine upload --config-file .pypirc {posargs} dist/*
[testenv:wheel]
commands = python setup.py bdist_wheel

View File

@@ -49,6 +49,10 @@ be one of the following:
- **CANCELLED** : the :ref:`Action <action_definition>` was in **PENDING** or
**ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
:ref:`Some default implementations are provided <watcher_planners>`, but it is
possible to :ref:`develop new implementations <implement_action_plugin>` which
are dynamically loaded by Watcher at launch time.
"""
import datetime
@@ -129,13 +133,10 @@ class Action(base.APIBase):
alarm = types.uuid
"""An alarm UUID related to this action"""
applies_to = wtypes.text
"""Applies to"""
action_type = wtypes.text
"""Action type"""
input_parameters = wtypes.DictType(wtypes.text, wtypes.text)
input_parameters = types.jsontype
"""One or more key/value pairs """
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
@@ -257,7 +258,7 @@ class ActionsController(rest.RestController):
resource_url=None,
action_plan_uuid=None, audit_uuid=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
@@ -362,6 +363,10 @@ class ActionsController(rest.RestController):
:param action: a action within the request body.
"""
# FIXME: blueprint edit-action-plan-flow
raise exception.OperationNotPermitted(
_("Cannot create an action directly"))
if self.from_actions:
raise exception.OperationNotPermitted
@@ -382,6 +387,10 @@ class ActionsController(rest.RestController):
:param action_uuid: UUID of a action.
:param patch: a json PATCH document to apply to this action.
"""
# FIXME: blueprint edit-action-plan-flow
raise exception.OperationNotPermitted(
_("Cannot modify an action directly"))
if self.from_actions:
raise exception.OperationNotPermitted
@@ -414,6 +423,9 @@ class ActionsController(rest.RestController):
:param action_uuid: UUID of a action.
"""
# FIXME: blueprint edit-action-plan-flow
raise exception.OperationNotPermitted(
_("Cannot delete an action directly"))
action_to_delete = objects.Action.get_by_uuid(
pecan.request.context,

View File

@@ -49,24 +49,9 @@ standard workflow model description formats such as
`Business Process Model and Notation 2.0 (BPMN 2.0) <http://www.omg.org/spec/BPMN/2.0/>`_
or `Unified Modeling Language (UML) <http://www.uml.org/>`_.
An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
state may be one of the following:
- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
for a validation from the :ref:`Administrator <administrator_definition>`
- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
executed successfully (i.e. all :ref:`Actions <action_definition>` that it
contains have been executed successfully)
- **FAILED** : an error occured while executing the
:ref:`Action Plan <action_plan_definition>`
- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
stored in the :ref:`Watcher database <watcher_database_definition>` but is
not returned any more through the Watcher APIs.
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
**PENDING** or **ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
To see the life-cycle and description of
:ref:`Action Plan <action_plan_definition>` states, visit :ref:`the Action Plan state
machine <action_plan_state_machine>`.
""" # noqa
import datetime
@@ -283,7 +268,7 @@ class ActionPlansController(rest.RestController):
resource_url=None, audit_uuid=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
@@ -406,12 +391,12 @@ class ActionPlansController(rest.RestController):
# transitions that are allowed via PATCH
allowed_patch_transitions = [
(ap_objects.State.RECOMMENDED,
ap_objects.State.TRIGGERED),
ap_objects.State.PENDING),
(ap_objects.State.RECOMMENDED,
ap_objects.State.CANCELLED),
(ap_objects.State.ONGOING,
ap_objects.State.CANCELLED),
(ap_objects.State.TRIGGERED,
(ap_objects.State.PENDING,
ap_objects.State.CANCELLED),
]
@@ -427,7 +412,7 @@ class ActionPlansController(rest.RestController):
initial_state=action_plan_to_update.state,
new_state=action_plan.state))
if action_plan.state == ap_objects.State.TRIGGERED:
if action_plan.state == ap_objects.State.PENDING:
launch_action_plan = True
# Update only the fields that have changed
@@ -443,7 +428,7 @@ class ActionPlansController(rest.RestController):
action_plan_to_update[field] = patch_val
if (field == 'state'
and patch_val == objects.action_plan.State.TRIGGERED):
and patch_val == objects.action_plan.State.PENDING):
launch_action_plan = True
action_plan_to_update.save()

View File

@@ -25,28 +25,8 @@ on a given :ref:`Cluster <cluster_definition>`.
For each :ref:`Audit <audit_definition>`, the Watcher system generates an
:ref:`Action Plan <action_plan_definition>`.
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
be one of the following:
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
submitted (either manually by the
:ref:`Administrator <administrator_definition>` or automatically via some
event handling mechanism) and is in the queue for being processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
successfully (note that it may not necessarily produce a
:ref:`Solution <solution_definition>`).
- **FAILED** : an error occured while executing the
:ref:`Audit <audit_definition>`
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
:ref:`Watcher database <watcher_database_definition>` but is not returned
any more through the Watcher APIs.
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
**ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
To see the life-cycle and description of an :ref:`Audit <audit_definition>`
states, visit :ref:`the Audit State machine <audit_state_machine>`.
"""
import datetime
@@ -65,7 +45,7 @@ from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import utils
from watcher.decision_engine.rpcapi import DecisionEngineAPI
from watcher.decision_engine import rpcapi
from watcher import objects
@@ -263,7 +243,7 @@ class AuditsController(rest.RestController):
sort_key, sort_dir, expand=False,
resource_url=None, audit_template=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
@@ -369,7 +349,7 @@ class AuditsController(rest.RestController):
# trigger decision-engine to run the audit
dc_client = DecisionEngineAPI()
dc_client = rpcapi.DecisionEngineAPI()
dc_client.trigger_audit(context, new_audit.uuid)
return Audit.convert_with_links(new_audit)

View File

@@ -50,6 +50,7 @@ provided as a list of key-value pairs.
import datetime
from oslo_config import cfg
import pecan
from pecan import rest
import wsme
@@ -67,11 +68,25 @@ from watcher import objects
class AuditTemplatePatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return []
@staticmethod
def validate(patch):
if patch.path == "/goal":
AuditTemplatePatchType._validate_goal(patch)
return types.JsonPatchType.validate(patch)
@staticmethod
def _validate_goal(patch):
serialized_patch = {'path': patch.path, 'op': patch.op}
if patch.value is not wsme.Unset:
serialized_patch['value'] = patch.value
new_goal = patch.value
if new_goal and new_goal not in cfg.CONF.watcher_goals.goals.keys():
raise exception.InvalidGoal(goal=new_goal)
class AuditTemplate(base.APIBase):
"""API representation of a audit template.
@@ -156,6 +171,12 @@ class AuditTemplate(base.APIBase):
updated_at=datetime.datetime.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@staticmethod
def validate(audit_template):
if audit_template.goal not in cfg.CONF.watcher_goals.goals.keys():
raise exception.InvalidGoal(audit_template.goal)
return audit_template
class AuditTemplateCollection(collection.Collection):
"""API representation of a collection of audit templates."""
@@ -197,12 +218,13 @@ class AuditTemplatesController(rest.RestController):
'detail': ['GET'],
}
def _get_audit_templates_collection(self, marker, limit,
def _get_audit_templates_collection(self, filters, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
api_utils.validate_search_filters(
filters, objects.audit_template.AuditTemplate.fields.keys())
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
@@ -212,6 +234,7 @@ class AuditTemplatesController(rest.RestController):
audit_templates = objects.AuditTemplate.list(
pecan.request.context,
filters,
limit,
marker_obj, sort_key=sort_key,
sort_dir=sort_dir)
@@ -223,26 +246,30 @@ class AuditTemplatesController(rest.RestController):
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(AuditTemplateCollection, types.uuid, int,
wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None,
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of audit templates.
:param goal: goal name to filter by (case sensitive)
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
return self._get_audit_templates_collection(marker, limit, sort_key,
sort_dir)
filters = api_utils.as_filters_dict(goal=goal)
@wsme_pecan.wsexpose(AuditTemplateCollection, types.uuid, int,
return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, types.uuid, int,
wtypes.text, wtypes.text)
def detail(self, marker=None, limit=None,
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of audit templates with detail.
:param goal: goal name to filter by (case sensitive)
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
@@ -253,9 +280,11 @@ class AuditTemplatesController(rest.RestController):
if parent != "audit_templates":
raise exception.HTTPNotFound
filters = api_utils.as_filters_dict(goal=goal)
expand = True
resource_url = '/'.join(['audit_templates', 'detail'])
return self._get_audit_templates_collection(marker, limit,
return self._get_audit_templates_collection(filters, marker, limit,
sort_key, sort_dir, expand,
resource_url)
@@ -263,7 +292,7 @@ class AuditTemplatesController(rest.RestController):
def get_one(self, audit_template):
"""Retrieve information about the given audit template.
:param audit template_uuid: UUID or name of an audit template.
:param audit audit_template: UUID or name of an audit template.
"""
if self.from_audit_templates:
raise exception.OperationNotPermitted
@@ -279,12 +308,14 @@ class AuditTemplatesController(rest.RestController):
return AuditTemplate.convert_with_links(rpc_audit_template)
@wsme.validate(types.uuid, AuditTemplate)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplate, status_code=201)
def post(self, audit_template):
"""Create a new audit template.
:param audit template: a audit template within the request body.
"""
if self.from_audit_templates:
raise exception.OperationNotPermitted

View File

@@ -159,7 +159,7 @@ class GoalsController(rest.RestController):
resource_url=None, goal_name=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
api_utils.validate_sort_dir(sort_dir)
goals = []

View File

@@ -47,7 +47,15 @@ def validate_sort_dir(sort_dir):
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
"Acceptable values are "
"'asc' or 'desc'") % sort_dir)
return sort_dir
def validate_search_filters(filters, allowed_fields):
# Very leightweight validation for now
# todo: improve this (e.g. https://www.parse.com/docs/rest/guide/#queries)
for filter_name in filters.keys():
if filter_name not in allowed_fields:
raise wsme.exc.ClientSideError(
_("Invalid filter: %s") % filter_name)
def apply_jsonpatch(doc, patch):
@@ -58,3 +66,12 @@ def apply_jsonpatch(doc, patch):
' the resource is not allowed')
raise wsme.exc.ClientSideError(msg % p['path'])
return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch))
def as_filters_dict(**filters):
filters_dict = {}
for filter_name, filter_value in filters.items():
if filter_value:
filters_dict[filter_name] = filter_value
return filters_dict

View File

@@ -53,16 +53,15 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
event_types.EventTypes.LAUNCH_ACTION_PLAN,
ap_objects.State.ONGOING)
applier = default.DefaultApplier(self.ctx, self.applier_manager)
result = applier.execute(self.action_plan_uuid)
applier.execute(self.action_plan_uuid)
state = ap_objects.State.SUCCEEDED
except Exception as e:
LOG.exception(e)
result = False
state = ap_objects.State.FAILED
finally:
if result is True:
status = ap_objects.State.SUCCEEDED
else:
status = ap_objects.State.FAILED
# update state
self.notify(self.action_plan_uuid,
event_types.EventTypes.LAUNCH_ACTION_PLAN,
status)
state)

View File

@@ -27,10 +27,15 @@ from watcher.common import clients
@six.add_metaclass(abc.ABCMeta)
class BaseAction(object):
# NOTE(jed) by convention we decided
# that the attribute "resource_id" is the unique id of
# the resource to which the Action applies to allow us to use it in the
# watcher dashboard and will be nested in input_parameters
RESOURCE_ID = 'resource_id'
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
self._input_parameters = {}
self._applies_to = ""
self._osc = osc
@property
@@ -48,25 +53,64 @@ class BaseAction(object):
self._input_parameters = p
@property
def applies_to(self):
return self._applies_to
@applies_to.setter
def applies_to(self, a):
self._applies_to = a
def resource_id(self):
return self.input_parameters[self.RESOURCE_ID]
@abc.abstractmethod
def execute(self):
"""Executes the main logic of the action
This method can be used to perform an action on a given set of input
parameters to accomplish some type of operation. This operation may
return a boolean value as a result of its execution. If False, this
will be considered as an error and will then trigger the reverting of
the actions.
:returns: A flag indicating whether or not the action succeeded
:rtype: bool
"""
raise NotImplementedError()
@abc.abstractmethod
def revert(self):
"""Revert this action
This method should rollback the resource to its initial state in the
event of a faulty execution. This happens when the action raised an
exception during its :py:meth:`~.BaseAction.execute`.
"""
raise NotImplementedError()
@abc.abstractmethod
def precondition(self):
"""Hook: called before the execution of an action
This method can be used to perform some initializations or to make
some more advanced validation on its input parameters. So if you wish
to block its execution based on this factor, `raise` the related
exception.
"""
raise NotImplementedError()
@abc.abstractmethod
def postcondition(self):
"""Hook: called after the execution of an action
This function is called regardless of whether an action succeded or
not. So you can use it to perform cleanup operations.
"""
raise NotImplementedError()
@abc.abstractproperty
def schema(self):
"""Defines a Schema that the input parameters shall comply to
:returns: A schema declaring the input parameters this action should be
provided along with their respective constraints
:rtype: :py:class:`voluptuous.Schema` instance
"""
raise NotImplementedError()
def validate_parameters(self):
self.schema(self.input_parameters)
return True

View File

@@ -16,7 +16,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import six
import voluptuous
from watcher._i18n import _
from watcher.applier.actions import base
@@ -26,32 +27,64 @@ from watcher.decision_engine.model import hypervisor_state as hstate
class ChangeNovaServiceState(base.BaseAction):
"""Disables or enables the nova-compute service, deployed on a host
By using this action, you will be able to update the state of a
nova-compute service. A disabled nova-compute service can not be selected
by the nova scheduler for future deployment of server.
The action schema is::
schema = Schema({
'resource_id': str,
'state': str,
})
The `resource_id` references a nova-compute service name (list of available
nova-compute services is returned by this command: ``nova service-list
--binary nova-compute``).
The `state` value should either be `ONLINE` or `OFFLINE`.
"""
STATE = 'state'
@property
def schema(self):
return voluptuous.Schema({
voluptuous.Required(self.RESOURCE_ID):
voluptuous.All(
voluptuous.Any(*six.string_types),
voluptuous.Length(min=1)),
voluptuous.Required(self.STATE):
voluptuous.Any(*[state.value
for state in list(hstate.HypervisorState)]),
})
@property
def host(self):
return self.applies_to
return self.resource_id
@property
def state(self):
return self.input_parameters.get('state')
return self.input_parameters.get(self.STATE)
def execute(self):
target_state = None
if self.state == hstate.HypervisorState.OFFLINE.value:
if self.state == hstate.HypervisorState.DISABLED.value:
target_state = False
elif self.status == hstate.HypervisorState.ONLINE.value:
elif self.state == hstate.HypervisorState.ENABLED.value:
target_state = True
return self.nova_manage_service(target_state)
return self._nova_manage_service(target_state)
def revert(self):
target_state = None
if self.state == hstate.HypervisorState.OFFLINE.value:
if self.state == hstate.HypervisorState.DISABLED.value:
target_state = True
elif self.state == hstate.HypervisorState.ONLINE.value:
elif self.state == hstate.HypervisorState.ENABLED.value:
target_state = False
return self.nova_manage_service(target_state)
return self._nova_manage_service(target_state)
def nova_manage_service(self, state):
def _nova_manage_service(self, state):
if state is None:
raise exception.IllegalArgumentException(
message=_("The target state is not defined"))

View File

@@ -33,5 +33,10 @@ class ActionFactory(object):
loaded_action = self.action_loader.load(name=object_action.action_type,
osc=osc)
loaded_action.input_parameters = object_action.input_parameters
loaded_action.applies_to = object_action.applies_to
LOG.debug("Checking the input parameters")
# NOTE(jed) if we change the schema of an action and we try to reload
# an older version of the Action, the validation can fail.
# We need to add the versioning of an Action or a migration tool.
# We can also create an new Action which extends the previous one.
loaded_action.validate_parameters()
return loaded_action

View File

@@ -18,30 +18,114 @@
#
from oslo_log import log
import six
import voluptuous
from watcher._i18n import _, _LC
from watcher.applier.actions import base
from watcher.common import exception
from watcher.common import nova_helper
from watcher.common import utils
LOG = log.getLogger(__name__)
class Migrate(base.BaseAction):
"""Live-Migrates a server to a destination nova-compute host
This action will allow you to migrate a server to another compute
destination host. As of now, only live migration can be performed using
this action.
.. If either host uses shared storage, you can use ``live``
.. as ``migration_type``. If both source and destination hosts provide
.. local disks, you can set the block_migration parameter to True (not
.. supported for yet).
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live" only
'dst_hypervisor': str,
'src_hypervisor': str,
})
The `resource_id` is the UUID of the server to migrate. Only live migration
is supported.
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
source and the destination compute hostname (list of available compute
hosts is returned by this command: ``nova service-list --binary
nova-compute``).
"""
# input parameters constants
MIGRATION_TYPE = 'migration_type'
LIVE_MIGRATION = 'live'
DST_HYPERVISOR = 'dst_hypervisor'
SRC_HYPERVISOR = 'src_hypervisor'
def check_resource_id(self, value):
if (value is not None and
len(value) > 0 and not
utils.is_uuid_like(value)):
raise voluptuous.Invalid(_("The parameter"
" resource_id is invalid."))
@property
def schema(self):
return voluptuous.Schema({
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
voluptuous.Required(self.MIGRATION_TYPE,
default=self.LIVE_MIGRATION):
voluptuous.Any(*[self.LIVE_MIGRATION]),
voluptuous.Required(self.DST_HYPERVISOR):
voluptuous.All(voluptuous.Any(*six.string_types),
voluptuous.Length(min=1)),
voluptuous.Required(self.SRC_HYPERVISOR):
voluptuous.All(voluptuous.Any(*six.string_types),
voluptuous.Length(min=1)),
})
@property
def instance_uuid(self):
return self.applies_to
return self.resource_id
@property
def migration_type(self):
return self.input_parameters.get('migration_type')
return self.input_parameters.get(self.MIGRATION_TYPE)
@property
def dst_hypervisor(self):
return self.input_parameters.get('dst_hypervisor')
return self.input_parameters.get(self.DST_HYPERVISOR)
@property
def src_hypervisor(self):
return self.input_parameters.get('src_hypervisor')
return self.input_parameters.get(self.SRC_HYPERVISOR)
def _live_migrate_instance(self, nova, destination):
result = None
try:
result = nova.live_migrate_instance(instance_id=self.instance_uuid,
dest_hostname=destination)
except nova_helper.nvexceptions.ClientException as e:
if e.code == 400:
LOG.debug("Live migration of instance %s failed. "
"Trying to live migrate using block migration."
% self.instance_uuid)
result = nova.live_migrate_instance(
instance_id=self.instance_uuid,
dest_hostname=destination,
block_migration=True)
else:
LOG.debug("Nova client exception occured while live migrating "
"instance %s.Exception: %s" %
(self.instance_uuid, e))
except Exception:
LOG.critical(_LC("Unexpected error occured. Migration failed for"
"instance %s. Leaving instance on previous "
"host."), self.instance_uuid)
return result
def migrate(self, destination):
nova = nova_helper.NovaHelper(osc=self.osc)
@@ -50,8 +134,7 @@ class Migrate(base.BaseAction):
instance = nova.find_instance(self.instance_uuid)
if instance:
if self.migration_type == 'live':
return nova.live_migrate_instance(
instance_id=self.instance_uuid, dest_hostname=destination)
return self._live_migrate_instance(nova, destination)
else:
raise exception.Invalid(
message=(_('Migration of type %(migration_type)s is not '

View File

@@ -18,6 +18,8 @@
#
from oslo_log import log
import six
import voluptuous
from watcher.applier.actions import base
@@ -26,10 +28,29 @@ LOG = log.getLogger(__name__)
class Nop(base.BaseAction):
"""logs a message
The action schema is::
schema = Schema({
'message': str,
})
The `message` is the actual message that will be logged.
"""
MESSAGE = 'message'
@property
def schema(self):
return voluptuous.Schema({
voluptuous.Required(self.MESSAGE): voluptuous.Any(
voluptuous.Any(*six.string_types), None)
})
@property
def message(self):
return self.input_parameters.get('message')
return self.input_parameters.get(self.MESSAGE)
def execute(self):
LOG.debug("executing action NOP message:%s ", self.message)

View File

@@ -18,19 +18,39 @@
#
import time
from oslo_log import log
import voluptuous
from watcher.applier.actions import base
LOG = log.getLogger(__name__)
class Sleep(base.BaseAction):
"""Makes the executor of the action plan wait for a given duration
The action schema is::
schema = Schema({
'duration': float,
})
The `duration` is expressed in seconds.
"""
DURATION = 'duration'
@property
def schema(self):
return voluptuous.Schema({
voluptuous.Required(self.DURATION, default=1):
voluptuous.All(float, voluptuous.Range(min=0))
})
@property
def duration(self):
return int(self.input_parameters.get('duration'))
return int(self.input_parameters.get(self.DURATION))
def execute(self):
LOG.debug("Starting action Sleep duration:%s ", self.duration)

View File

@@ -63,10 +63,3 @@ class ApplierAPI(messaging_core.MessagingCore):
return self.client.call(
context.to_dict(), 'launch_action_plan',
action_plan_uuid=action_plan_uuid)
def event_receive(self, event):
try:
pass
except Exception as e:
LOG.exception(e)
raise

View File

@@ -22,12 +22,19 @@ from taskflow import task
from watcher._i18n import _LE, _LW, _LC
from watcher.applier.workflow_engine import base
from watcher.common import exception
from watcher.objects import action as obj_action
LOG = log.getLogger(__name__)
class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
"""Taskflow as a workflow engine for Watcher
Full documentation on taskflow at
http://docs.openstack.org/developer/taskflow/
"""
def decider(self, history):
# FIXME(jed) not possible with the current Watcher Planner
#
@@ -71,10 +78,9 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
e = engines.load(flow)
e.run()
return True
except Exception as e:
LOG.exception(e)
return False
raise exception.WorkflowExecutionException(error=e)
class TaskFlowActionContainer(task.Task):
@@ -115,14 +121,9 @@ class TaskFlowActionContainer(task.Task):
try:
LOG.debug("Running action %s", self.name)
# todo(jed) remove return (true or false) raise an Exception
result = self.action.execute()
if result is not True:
self.engine.notify(self._db_action,
obj_action.State.FAILED)
else:
self.engine.notify(self._db_action,
obj_action.State.SUCCEEDED)
self.action.execute()
self.engine.notify(self._db_action,
obj_action.State.SUCCEEDED)
except Exception as e:
LOG.exception(e)
LOG.error(_LE('The WorkFlow Engine has failed '

View File

@@ -25,7 +25,7 @@ from oslo_config import cfg
from oslo_log import log as logging
from watcher import _i18n
from watcher.applier.manager import ApplierManager
from watcher.applier import manager
from watcher.common import service
LOG = logging.getLogger(__name__)
@@ -40,6 +40,6 @@ def main():
LOG.debug("Configuration:")
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
server = ApplierManager()
server = manager.ApplierManager()
server.connect()
server.join()

View File

@@ -25,7 +25,7 @@ from oslo_config import cfg
from watcher.common import service
from watcher.db import migration
from watcher.db import purge
CONF = cfg.CONF
@@ -56,6 +56,12 @@ class DBCommand(object):
def create_schema():
migration.create_schema()
@staticmethod
def purge():
purge.purge(CONF.command.age_in_days, CONF.command.max_number,
CONF.command.audit_template, CONF.command.exclude_orphans,
CONF.command.dry_run)
def add_command_parsers(subparsers):
parser = subparsers.add_parser(
@@ -96,6 +102,33 @@ def add_command_parsers(subparsers):
help="Create the database schema.")
parser.set_defaults(func=DBCommand.create_schema)
parser = subparsers.add_parser(
'purge',
help="Purge the database.")
parser.add_argument('-d', '--age-in-days',
help="Number of days since deletion (from today) "
"to exclude from the purge. If None, everything "
"will be purged.",
type=int, default=None, nargs='?')
parser.add_argument('-n', '--max-number',
help="Max number of objects expected to be deleted. "
"Prevents the deletion if exceeded. No limit if "
"set to None.",
type=int, default=None, nargs='?')
parser.add_argument('-t', '--audit-template',
help="UUID or name of the audit template to purge.",
type=str, default=None, nargs='?')
parser.add_argument('-e', '--exclude-orphans', action='store_true',
help="Flag to indicate whether or not you want to "
"exclude orphans from deletion (default: False).",
default=False)
parser.add_argument('--dry-run', action='store_true',
help="Flag to indicate whether or not you want to "
"perform a dry run (no deletion).",
default=False)
parser.set_defaults(func=DBCommand.purge)
command_opt = cfg.SubCommandOpt('command',
title='Command',
@@ -114,6 +147,7 @@ def main():
valid_commands = set([
'upgrade', 'downgrade', 'revision',
'version', 'stamp', 'create_schema',
'purge',
])
if not set(sys.argv).intersection(valid_commands):
sys.argv.append('upgrade')

View File

@@ -26,7 +26,7 @@ from oslo_log import log as logging
from watcher import _i18n
from watcher.common import service
from watcher.decision_engine.manager import DecisionEngineManager
from watcher.decision_engine import manager
LOG = logging.getLogger(__name__)
@@ -41,6 +41,6 @@ def main():
LOG.debug("Configuration:")
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
server = DecisionEngineManager()
server = manager.DecisionEngineManager()
server.connect()
server.join()

View File

@@ -128,7 +128,8 @@ class CeilometerHelper(object):
def get_last_sample_values(self, resource_id, meter_name, limit=1):
samples = self.query_sample(meter_name=meter_name,
query=self.build_query(resource_id))
query=self.build_query(resource_id),
limit=limit)
values = []
for index, sample in enumerate(samples):
values.append(

View File

@@ -46,7 +46,7 @@ CEILOMETER_CLIENT_OPTS = [
NEUTRON_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
default='2.0',
help=_('Version of Neutron API to use in neutronclient.'))]
cfg.CONF.register_opts(NOVA_CLIENT_OPTS, group='nova_client')
@@ -141,8 +141,8 @@ class OpenStackClients(object):
ceilometerclient_version = self._get_client_option('ceilometer',
'api_version')
self._ceilometer = ceclient.Client(ceilometerclient_version,
session=self.session)
self._ceilometer = ceclient.get_client(ceilometerclient_version,
session=self.session)
return self._ceilometer
@exception.wrap_keystone_exception

View File

@@ -22,6 +22,8 @@ from watcher import version
def parse_args(argv, default_config_files=None):
default_config_files = (default_config_files or
cfg.find_config_files(project='watcher'))
rpc.set_defaults(control_exchange='watcher')
cfg.CONF(argv[1:],
project='python-watcher',

View File

@@ -234,6 +234,9 @@ class PatchError(Invalid):
# decision engine
class WorkflowExecutionException(WatcherException):
msg_fmt = _('Workflow execution error: %(error)s')
class IllegalArgumentException(WatcherException):
msg_fmt = _('Illegal argument')
@@ -279,3 +282,15 @@ class HypervisorNotFound(WatcherException):
class LoadingError(WatcherException):
msg_fmt = _("Error loading plugin '%(name)s'")
class ReservedWord(WatcherException):
msg_fmt = _("The identifier '%(name)s' is a reserved word")
class NotSoftDeletedStateError(WatcherException):
msg_fmt = _("The %(name)s resource %(id)s is not soft deleted")
class NegativeLimitError(WatcherException):
msg_fmt = _("Limit should be positive")

View File

@@ -16,7 +16,7 @@
from oslo_log import log
from watcher.decision_engine.messaging.events import Events
from watcher.decision_engine.messaging import events as messaging_events
LOG = log.getLogger(__name__)
@@ -43,8 +43,8 @@ class EventDispatcher(object):
"""
Dispatch an instance of Event class
"""
if Events.ALL in self._events.keys():
listeners = self._events[Events.ALL]
if messaging_events.Events.ALL in self._events.keys():
listeners = self._events[messaging_events.Events.ALL]
for listener in listeners:
listener(event)

View File

@@ -265,58 +265,6 @@ class NovaHelper(object):
return True
def built_in_non_live_migrate_instance(self, instance_id, hypervisor_id):
"""This method does a live migration of a given instance
This method uses the Nova built-in non-live migrate()
action to migrate a given instance.
It returns True if the migration was successful, False otherwise.
:param instance_id: the unique id of the instance to migrate.
"""
LOG.debug(
"Trying a Nova built-in non-live "
"migrate of instance %s ..." % instance_id)
# Looking for the instance to migrate
instance = self.find_instance(instance_id)
if not instance:
LOG.debug("Instance not found: %s" % instance_id)
return False
else:
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
LOG.debug(
"Instance %s found on host '%s'." % (instance_id, host_name))
instance.migrate()
# Poll at 5 second intervals, until the status is as expected
if self.wait_for_instance_status(instance,
('VERIFY_RESIZE', 'ERROR'),
5, 10):
instance = self.nova.servers.get(instance.id)
if instance.status == 'VERIFY_RESIZE':
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
LOG.debug(
"Instance %s has been successfully "
"migrated to host '%s'." % (
instance_id, host_name))
# We need to confirm that the resize() operation
# has succeeded in order to
# get back instance state to 'ACTIVE'
instance.confirm_resize()
return True
elif instance.status == 'ERROR':
LOG.debug("Instance %s migration failed" % instance_id)
return False
def live_migrate_instance(self, instance_id, dest_hostname,
block_migration=False, retry=120):
"""This method does a live migration of a given instance

View File

@@ -102,7 +102,7 @@ class RPCService(service.Service):
'%(host)s.'),
{'service': self.topic, 'host': self.host})
def _handle_signal(self, signo, frame):
def _handle_signal(self):
LOG.info(_LI('Got signal SIGUSR1. Not deregistering on next shutdown '
'of service %(service)s on host %(host)s.'),
{'service': self.topic, 'host': self.host})

View File

@@ -98,7 +98,7 @@ class BaseConnection(object):
:raises: AuditTemplateNotFound
"""
def get_audit_template_by__name(self, context, audit_template_name):
def get_audit_template_by_name(self, context, audit_template_name):
"""Return an audit template.
:param context: The security context

410
watcher/db/purge.py Normal file
View File

@@ -0,0 +1,410 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import print_function
import collections
import datetime
import itertools
import sys
from oslo_log import log
from oslo_utils import strutils
import prettytable as ptable
from six.moves import input
from watcher._i18n import _, _LI
from watcher.common import context
from watcher.common import exception
from watcher.common import utils
from watcher import objects
LOG = log.getLogger(__name__)
class WatcherObjectsMap(object):
"""Wrapper to deal with watcher objects per type
This wrapper object contains a list of watcher objects per type.
Its main use is to simplify the merge of watcher objects by avoiding
duplicates, but also for representing the relationships between these
objects.
"""
# This is for generating the .pot translations
keymap = collections.OrderedDict([
("audit_templates", _("Audit Templates")),
("audits", _("Audits")),
("action_plans", _("Action Plans")),
("actions", _("Actions")),
])
def __init__(self):
for attr_name in self.__class__.keys():
setattr(self, attr_name, [])
def values(self):
return (getattr(self, key) for key in self.__class__.keys())
@classmethod
def keys(cls):
return cls.keymap.keys()
def __iter__(self):
return itertools.chain(*self.values())
def __add__(self, other):
new_map = self.__class__()
# Merge the 2 items dicts into a new object (and avoid dupes)
for attr_name, initials, others in zip(self.keys(), self.values(),
other.values()):
# Creates a copy
merged = initials[:]
initials_ids = [item.id for item in initials]
non_dupes = [item for item in others
if item.id not in initials_ids]
merged += non_dupes
setattr(new_map, attr_name, merged)
return new_map
def __str__(self):
out = ""
for key, vals in zip(self.keys(), self.values()):
ids = [val.id for val in vals]
out += "%(key)s: %(val)s" % (dict(key=key, val=ids))
out += "\n"
return out
def __len__(self):
return sum(len(getattr(self, key)) for key in self.keys())
def get_count_table(self):
headers = list(self.keymap.values())
headers.append(_("Total")) # We also add a total count
counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)]
table = ptable.PrettyTable(field_names=headers)
table.add_row(counters)
return table.get_string()
class PurgeCommand(object):
"""Purges the DB by removing soft deleted entries
The workflow for this purge is the following:
# Find soft deleted objects which are expired
# Find orphan objects
# Find their related objects whether they are expired or not
# Merge them together
# If it does not exceed the limit, destroy them all
"""
ctx = context.make_context(show_deleted=True)
def __init__(self, age_in_days=None, max_number=None,
uuid=None, exclude_orphans=False, dry_run=None):
self.age_in_days = age_in_days
self.max_number = max_number
self.uuid = uuid
self.exclude_orphans = exclude_orphans
self.dry_run = dry_run
self._delete_up_to_max = None
self._objects_map = WatcherObjectsMap()
def get_expiry_date(self):
if not self.age_in_days:
return None
today = datetime.datetime.today()
expiry_date = today - datetime.timedelta(days=self.age_in_days)
return expiry_date
@classmethod
def get_audit_template_uuid(cls, uuid_or_name):
if uuid_or_name is None:
return
query_func = None
if not utils.is_uuid_like(uuid_or_name):
query_func = objects.audit_template.AuditTemplate.get_by_name
else:
query_func = objects.audit_template.AuditTemplate.get_by_uuid
try:
audit_template = query_func(cls.ctx, uuid_or_name)
except Exception as exc:
LOG.exception(exc)
raise exception.AuditTemplateNotFound(audit_template=uuid_or_name)
if not audit_template.deleted_at:
raise exception.NotSoftDeletedStateError(
name=_('Audit Template'), id=uuid_or_name)
return audit_template.uuid
def _find_audit_templates(self, filters=None):
return objects.audit_template.AuditTemplate.list(
self.ctx, filters=filters)
def _find_audits(self, filters=None):
return objects.audit.Audit.list(self.ctx, filters=filters)
def _find_action_plans(self, filters=None):
return objects.action_plan.ActionPlan.list(self.ctx, filters=filters)
def _find_actions(self, filters=None):
return objects.action.Action.list(self.ctx, filters=filters)
def _find_orphans(self):
orphans = WatcherObjectsMap()
filters = dict(deleted=False)
audit_templates = objects.audit_template.AuditTemplate.list(
self.ctx, filters=filters)
audits = objects.audit.Audit.list(self.ctx, filters=filters)
action_plans = objects.action_plan.ActionPlan.list(
self.ctx, filters=filters)
actions = objects.action.Action.list(self.ctx, filters=filters)
audit_template_ids = set(at.id for at in audit_templates)
orphans.audits = [
audit for audit in audits
if audit.audit_template_id not in audit_template_ids]
# Objects with orphan parents are themselves orphans
audit_ids = [audit.id for audit in (a for a in audits
if a not in orphans.audits)]
orphans.action_plans = [
ap for ap in action_plans
if ap.audit_id not in audit_ids]
# Objects with orphan parents are themselves orphans
action_plan_ids = [ap.id for ap in (a for a in action_plans
if a not in orphans.action_plans)]
orphans.actions = [
action for action in actions
if action.action_plan_id not in action_plan_ids]
LOG.debug("Orphans found:\n%s", orphans)
LOG.info(_LI("Orphans found:\n%s"), orphans.get_count_table())
return orphans
def _find_soft_deleted_objects(self):
to_be_deleted = WatcherObjectsMap()
expiry_date = self.get_expiry_date()
filters = dict(deleted=True)
if self.uuid:
filters["uuid"] = self.uuid
if expiry_date:
filters.update(dict(deleted_at__lt=expiry_date))
to_be_deleted.audit_templates.extend(
self._find_audit_templates(filters))
to_be_deleted.audits.extend(self._find_audits(filters))
to_be_deleted.action_plans.extend(self._find_action_plans(filters))
to_be_deleted.actions.extend(self._find_actions(filters))
soft_deleted_objs = self._find_related_objects(
to_be_deleted, base_filters=dict(deleted=True))
LOG.debug("Soft deleted objects:\n%s", soft_deleted_objs)
return soft_deleted_objs
def _find_related_objects(self, objects_map, base_filters=None):
base_filters = base_filters or {}
for audit_template in objects_map.audit_templates:
filters = {}
filters.update(base_filters)
filters.update(dict(audit_template_id=audit_template.id))
related_objs = WatcherObjectsMap()
related_objs.audits = self._find_audits(filters)
objects_map += related_objs
for audit in objects_map.audits:
filters = {}
filters.update(base_filters)
filters.update(dict(audit_id=audit.id))
related_objs = WatcherObjectsMap()
related_objs.action_plans = self._find_action_plans(filters)
objects_map += related_objs
for action_plan in objects_map.action_plans:
filters = {}
filters.update(base_filters)
filters.update(dict(action_plan_id=action_plan.id))
related_objs = WatcherObjectsMap()
related_objs.actions = self._find_actions(filters)
objects_map += related_objs
return objects_map
def confirmation_prompt(self):
print(self._objects_map.get_count_table())
raw_val = input(
_("There are %(count)d objects set for deletion. "
"Continue? [y/N]") % dict(count=len(self._objects_map)))
return strutils.bool_from_string(raw_val)
def delete_up_to_max_prompt(self, objects_map):
print(objects_map.get_count_table())
print(_("The number of objects (%(num)s) to delete from the database "
"exceeds the maximum number of objects (%(max_number)s) "
"specified.") % dict(max_number=self.max_number,
num=len(objects_map)))
raw_val = input(
_("Do you want to delete objects up to the specified maximum "
"number? [y/N]"))
self._delete_up_to_max = strutils.bool_from_string(raw_val)
return self._delete_up_to_max
def _aggregate_objects(self):
"""Objects aggregated on a 'per audit template' basis"""
# todo: aggregate orphans as well
aggregate = []
for audit_template in self._objects_map.audit_templates:
related_objs = WatcherObjectsMap()
related_objs.audit_templates = [audit_template]
related_objs.audits = [
audit for audit in self._objects_map.audits
if audit.audit_template_id == audit_template.id
]
audit_ids = [audit.id for audit in related_objs.audits]
related_objs.action_plans = [
action_plan for action_plan in self._objects_map.action_plans
if action_plan.audit_id in audit_ids
]
action_plan_ids = [
action_plan.id for action_plan in related_objs.action_plans
]
related_objs.actions = [
action for action in self._objects_map.actions
if action.action_plan_id in action_plan_ids
]
aggregate.append(related_objs)
return aggregate
def _get_objects_up_to_limit(self):
aggregated_objects = self._aggregate_objects()
to_be_deleted_subset = WatcherObjectsMap()
for aggregate in aggregated_objects:
if len(aggregate) + len(to_be_deleted_subset) <= self.max_number:
to_be_deleted_subset += aggregate
else:
break
LOG.debug(to_be_deleted_subset)
return to_be_deleted_subset
def find_objects_to_delete(self):
"""Finds all the objects to be purged
:returns: A mapping with all the Watcher objects to purged
:rtype: :py:class:`~.WatcherObjectsMap` instance
"""
to_be_deleted = self._find_soft_deleted_objects()
if not self.exclude_orphans:
to_be_deleted += self._find_orphans()
LOG.debug("Objects to be deleted:\n%s", to_be_deleted)
return to_be_deleted
def do_delete(self):
LOG.info(_LI("Deleting..."))
# Reversed to avoid errors with foreign keys
for entry in reversed(list(self._objects_map)):
entry.destroy()
def execute(self):
LOG.info(_LI("Starting purge command"))
self._objects_map = self.find_objects_to_delete()
if (self.max_number is not None and
len(self._objects_map) > self.max_number):
if self.delete_up_to_max_prompt(self._objects_map):
self._objects_map = self._get_objects_up_to_limit()
else:
return
_orphans_note = (_(" (orphans excluded)") if self.exclude_orphans
else _(" (may include orphans)"))
if not self.dry_run and self.confirmation_prompt():
self.do_delete()
print(_("Purge results summary%s:") % _orphans_note)
LOG.info(_LI("Purge results summary%s:"), _orphans_note)
else:
LOG.debug(self._objects_map)
print(_("Here below is a table containing the objects "
"that can be purged%s:") % _orphans_note)
LOG.info("\n%s", self._objects_map.get_count_table())
print(self._objects_map.get_count_table())
LOG.info(_LI("Purge process completed"))
def purge(age_in_days, max_number, audit_template, exclude_orphans, dry_run):
"""Removes soft deleted objects from the database
:param age_in_days: Number of days since deletion (from today)
to exclude from the purge. If None, everything will be purged.
:type age_in_days: int
:param max_number: Max number of objects expected to be deleted.
Prevents the deletion if exceeded. No limit if set to None.
:type max_number: int
:param audit_template: UUID or name of the audit template to purge.
:type audit_template: str
:param exclude_orphans: Flag to indicate whether or not you want to
exclude orphans from deletion (default: False).
:type exclude_orphans: bool
:param dry_run: Flag to indicate whether or not you want to perform
a dry run (no deletion).
:type dry_run: bool
"""
try:
if max_number and max_number < 0:
raise exception.NegativeLimitError
LOG.info("[options] age_in_days = %s", age_in_days)
LOG.info("[options] max_number = %s", max_number)
LOG.info("[options] audit_template = %s", audit_template)
LOG.info("[options] exclude_orphans = %s", exclude_orphans)
LOG.info("[options] dry_run = %s", dry_run)
uuid = PurgeCommand.get_audit_template_uuid(audit_template)
cmd = PurgeCommand(age_in_days, max_number, uuid,
exclude_orphans, dry_run)
cmd.execute()
except Exception as exc:
LOG.exception(exc)
print(exc)
sys.exit(1)

View File

@@ -29,7 +29,10 @@ from watcher.common import exception
from watcher.common import utils
from watcher.db import api
from watcher.db.sqlalchemy import models
from watcher.objects import action as action_objects
from watcher.objects import action_plan as ap_objects
from watcher.objects import audit as audit_objects
from watcher.objects import utils as objutils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
@@ -105,12 +108,88 @@ class Connection(api.BaseConnection):
"""SqlAlchemy connection."""
def __init__(self):
pass
super(Connection, self).__init__()
def __add_soft_delete_mixin_filters(self, query, filters, model):
if 'deleted' in filters:
if bool(filters['deleted']):
query = query.filter(model.deleted != 0)
else:
query = query.filter(model.deleted == 0)
if 'deleted_at__eq' in filters:
query = query.filter(
model.deleted_at == objutils.datetime_or_str_or_none(
filters['deleted_at__eq']))
if 'deleted_at__gt' in filters:
query = query.filter(
model.deleted_at > objutils.datetime_or_str_or_none(
filters['deleted_at__gt']))
if 'deleted_at__gte' in filters:
query = query.filter(
model.deleted_at >= objutils.datetime_or_str_or_none(
filters['deleted_at__gte']))
if 'deleted_at__lt' in filters:
query = query.filter(
model.deleted_at < objutils.datetime_or_str_or_none(
filters['deleted_at__lt']))
if 'deleted_at__lte' in filters:
query = query.filter(
model.deleted_at <= objutils.datetime_or_str_or_none(
filters['deleted_at__lte']))
return query
def __add_timestamp_mixin_filters(self, query, filters, model):
if 'created_at__eq' in filters:
query = query.filter(
model.created_at == objutils.datetime_or_str_or_none(
filters['created_at__eq']))
if 'created_at__gt' in filters:
query = query.filter(
model.created_at > objutils.datetime_or_str_or_none(
filters['created_at__gt']))
if 'created_at__gte' in filters:
query = query.filter(
model.created_at >= objutils.datetime_or_str_or_none(
filters['created_at__gte']))
if 'created_at__lt' in filters:
query = query.filter(
model.created_at < objutils.datetime_or_str_or_none(
filters['created_at__lt']))
if 'created_at__lte' in filters:
query = query.filter(
model.created_at <= objutils.datetime_or_str_or_none(
filters['created_at__lte']))
if 'updated_at__eq' in filters:
query = query.filter(
model.updated_at == objutils.datetime_or_str_or_none(
filters['updated_at__eq']))
if 'updated_at__gt' in filters:
query = query.filter(
model.updated_at > objutils.datetime_or_str_or_none(
filters['updated_at__gt']))
if 'updated_at__gte' in filters:
query = query.filter(
model.updated_at >= objutils.datetime_or_str_or_none(
filters['updated_at__gte']))
if 'updated_at__lt' in filters:
query = query.filter(
model.updated_at < objutils.datetime_or_str_or_none(
filters['updated_at__lt']))
if 'updated_at__lte' in filters:
query = query.filter(
model.updated_at <= objutils.datetime_or_str_or_none(
filters['updated_at__lte']))
return query
def _add_audit_templates_filters(self, query, filters):
if filters is None:
filters = []
if 'uuid' in filters:
query = query.filter_by(uuid=filters['uuid'])
if 'name' in filters:
query = query.filter_by(name=filters['name'])
if 'host_aggregate' in filters:
@@ -118,12 +197,19 @@ class Connection(api.BaseConnection):
if 'goal' in filters:
query = query.filter_by(goal=filters['goal'])
query = self.__add_soft_delete_mixin_filters(
query, filters, models.AuditTemplate)
query = self.__add_timestamp_mixin_filters(
query, filters, models.AuditTemplate)
return query
def _add_audits_filters(self, query, filters):
if filters is None:
filters = []
if 'uuid' in filters:
query = query.filter_by(uuid=filters['uuid'])
if 'type' in filters:
query = query.filter_by(type=filters['type'])
if 'state' in filters:
@@ -144,12 +230,20 @@ class Connection(api.BaseConnection):
query = query.filter(
models.AuditTemplate.name ==
filters['audit_template_name'])
query = self.__add_soft_delete_mixin_filters(
query, filters, models.Audit)
query = self.__add_timestamp_mixin_filters(
query, filters, models.Audit)
return query
def _add_action_plans_filters(self, query, filters):
if filters is None:
filters = []
if 'uuid' in filters:
query = query.filter_by(uuid=filters['uuid'])
if 'state' in filters:
query = query.filter_by(state=filters['state'])
if 'audit_id' in filters:
@@ -158,12 +252,20 @@ class Connection(api.BaseConnection):
query = query.join(models.Audit,
models.ActionPlan.audit_id == models.Audit.id)
query = query.filter(models.Audit.uuid == filters['audit_uuid'])
query = self.__add_soft_delete_mixin_filters(
query, filters, models.ActionPlan)
query = self.__add_timestamp_mixin_filters(
query, filters, models.ActionPlan)
return query
def _add_actions_filters(self, query, filters):
if filters is None:
filters = []
if 'uuid' in filters:
query = query.filter_by(uuid=filters['uuid'])
if 'action_plan_id' in filters:
query = query.filter_by(action_plan_id=filters['action_plan_id'])
if 'action_plan_uuid' in filters:
@@ -184,6 +286,11 @@ class Connection(api.BaseConnection):
if 'alarm' in filters:
query = query.filter_by(alarm=filters['alarm'])
query = self.__add_soft_delete_mixin_filters(
query, filters, models.Action)
query = self.__add_timestamp_mixin_filters(
query, filters, models.Action)
return query
def get_audit_template_list(self, context, filters=None, limit=None,
@@ -193,7 +300,6 @@ class Connection(api.BaseConnection):
query = self._add_audit_templates_filters(query, filters)
if not context.show_deleted:
query = query.filter_by(deleted_at=None)
return _paginate_query(models.AuditTemplate, limit, marker,
sort_key, sort_dir, query)
@@ -312,7 +418,8 @@ class Connection(api.BaseConnection):
query = model_query(models.Audit)
query = self._add_audits_filters(query, filters)
if not context.show_deleted:
query = query.filter(~(models.Audit.state == 'DELETED'))
query = query.filter(
~(models.Audit.state == audit_objects.State.DELETED))
return _paginate_query(models.Audit, limit, marker,
sort_key, sort_dir, query)
@@ -340,7 +447,7 @@ class Connection(api.BaseConnection):
try:
audit = query.one()
if not context.show_deleted:
if audit.state == 'DELETED':
if audit.state == audit_objects.State.DELETED:
raise exception.AuditNotFound(audit=audit_id)
return audit
except exc.NoResultFound:
@@ -353,7 +460,7 @@ class Connection(api.BaseConnection):
try:
audit = query.one()
if not context.show_deleted:
if audit.state == 'DELETED':
if audit.state == audit_objects.State.DELETED:
raise exception.AuditNotFound(audit=audit_uuid)
return audit
except exc.NoResultFound:
@@ -421,7 +528,8 @@ class Connection(api.BaseConnection):
query = model_query(models.Action)
query = self._add_actions_filters(query, filters)
if not context.show_deleted:
query = query.filter(~(models.Action.state == 'DELETED'))
query = query.filter(
~(models.Action.state == action_objects.State.DELETED))
return _paginate_query(models.Action, limit, marker,
sort_key, sort_dir, query)
@@ -444,7 +552,7 @@ class Connection(api.BaseConnection):
try:
action = query.one()
if not context.show_deleted:
if action.state == 'DELETED':
if action.state == action_objects.State.DELETED:
raise exception.ActionNotFound(
action=action_id)
return action
@@ -457,7 +565,7 @@ class Connection(api.BaseConnection):
try:
action = query.one()
if not context.show_deleted:
if action.state == 'DELETED':
if action.state == action_objects.State.DELETED:
raise exception.ActionNotFound(
action=action_uuid)
return action
@@ -514,7 +622,8 @@ class Connection(api.BaseConnection):
query = model_query(models.ActionPlan)
query = self._add_action_plans_filters(query, filters)
if not context.show_deleted:
query = query.filter(~(models.ActionPlan.state == 'DELETED'))
query = query.filter(
~(models.ActionPlan.state == ap_objects.State.DELETED))
return _paginate_query(models.ActionPlan, limit, marker,
sort_key, sort_dir, query)
@@ -539,7 +648,7 @@ class Connection(api.BaseConnection):
try:
action_plan = query.one()
if not context.show_deleted:
if action_plan.state == 'DELETED':
if action_plan.state == ap_objects.State.DELETED:
raise exception.ActionPlanNotFound(
action_plan=action_plan_id)
return action_plan
@@ -553,7 +662,7 @@ class Connection(api.BaseConnection):
try:
action_plan = query.one()
if not context.show_deleted:
if action_plan.state == 'DELETED':
if action_plan.state == ap_objects.State.DELETED:
raise exception.ActionPlanNotFound(
action_plan=action_plan__uuid)
return action_plan

View File

@@ -32,7 +32,7 @@ def _alembic_config():
return config
def version(config=None, engine=None):
def version(engine=None):
"""Current database version.
:returns: Database version

View File

@@ -160,7 +160,6 @@ class Action(Base):
nullable=False)
# only for the first version
action_type = Column(String(255), nullable=False)
applies_to = Column(String(255), nullable=True)
input_parameters = Column(JSONEncodedDict, nullable=True)
state = Column(String(20), nullable=True)
# todo(jed) remove parameter alarm

View File

@@ -16,11 +16,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from concurrent.futures import ThreadPoolExecutor
from concurrent import futures
from oslo_log import log
from watcher.decision_engine.audit.default import DefaultAuditHandler
from watcher.decision_engine.audit import default
LOG = log.getLogger(__name__)
@@ -28,7 +28,7 @@ LOG = log.getLogger(__name__)
class AuditEndpoint(object):
def __init__(self, messaging, max_workers):
self._messaging = messaging
self._executor = ThreadPoolExecutor(max_workers=max_workers)
self._executor = futures.ThreadPoolExecutor(max_workers=max_workers)
@property
def executor(self):
@@ -39,7 +39,7 @@ class AuditEndpoint(object):
return self._messaging
def do_trigger_audit(self, context, audit_uuid):
audit = DefaultAuditHandler(self.messaging)
audit = default.DefaultAuditHandler(self.messaging)
audit.execute(audit_uuid, context)
def trigger_audit(self, context, audit_uuid):

View File

@@ -21,6 +21,7 @@ class ResourceType(Enum):
cpu_cores = 'num_cores'
memory = 'memory'
disk = 'disk'
disk_capacity = 'disk_capacity'
class Resource(object):

View File

@@ -37,6 +37,10 @@ congestion which may decrease the :ref:`SLA <sla_definition>` for
It is also important to schedule :ref:`Actions <action_definition>` in order to
avoid security issues such as denial of service on core OpenStack services.
:ref:`Some default implementations are provided <watcher_planners>`, but it is
possible to :ref:`develop new implementations <implement_planner_plugin>`
which are dynamically loaded by Watcher at launch time.
See :doc:`../architecture` for more details on this component.
"""
@@ -50,11 +54,13 @@ class BasePlanner(object):
def schedule(self, context, audit_uuid, solution):
"""The planner receives a solution to schedule
:param solution: the solution given by the strategy to
:param solution: A solution provided by a strategy for scheduling
:type solution: :py:class:`~.BaseSolution` subclass instance
:param audit_uuid: the audit uuid
:return: ActionPlan ordered sequence of change requests
such that all security, dependency, and performance
requirements are met.
:type audit_uuid: str
:return: Action plan with an ordered sequence of actions such that all
security, dependency, and performance requirements are met.
:rtype: :py:class:`watcher.objects.action_plan.ActionPlan` instance
"""
# example: directed acyclic graph
raise NotImplementedError()

View File

@@ -28,6 +28,13 @@ LOG = log.getLogger(__name__)
class DefaultPlanner(base.BasePlanner):
"""Default planner implementation
This implementation comes with basic rules with a fixed set of action types
that are weighted. An action having a lower weight will be scheduled before
the other ones.
"""
priorities = {
'nop': 0,
'sleep': 1,
@@ -38,14 +45,12 @@ class DefaultPlanner(base.BasePlanner):
def create_action(self,
action_plan_id,
action_type,
applies_to,
input_parameters=None):
uuid = utils.generate_uuid()
action = {
'uuid': uuid,
'action_plan_id': int(action_plan_id),
'action_type': action_type,
'applies_to': applies_to,
'input_parameters': input_parameters,
'state': objects.action.State.PENDING,
'alarm': None,
@@ -63,8 +68,6 @@ class DefaultPlanner(base.BasePlanner):
json_action = self.create_action(action_plan_id=action_plan.id,
action_type=action.get(
'action_type'),
applies_to=action.get(
'applies_to'),
input_parameters=action.get(
'input_parameters'))
to_schedule.append((self.priorities[action.get('action_type')],

View File

@@ -21,8 +21,8 @@ from oslo_config import cfg
from oslo_log import log
from watcher.common import exception
from watcher.common.messaging.messaging_core import MessagingCore
from watcher.common.messaging.notification_handler import NotificationHandler
from watcher.common.messaging import messaging_core
from watcher.common.messaging import notification_handler
from watcher.common import utils
from watcher.decision_engine.manager import decision_engine_opt_group
from watcher.decision_engine.manager import WATCHER_DECISION_ENGINE_OPTS
@@ -35,7 +35,7 @@ CONF.register_group(decision_engine_opt_group)
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
class DecisionEngineAPI(MessagingCore):
class DecisionEngineAPI(messaging_core.MessagingCore):
def __init__(self):
super(DecisionEngineAPI, self).__init__(
@@ -44,7 +44,8 @@ class DecisionEngineAPI(MessagingCore):
CONF.watcher_decision_engine.status_topic,
api_version=self.API_VERSION,
)
self.handler = NotificationHandler(self.publisher_id)
self.handler = notification_handler.NotificationHandler(
self.publisher_id)
self.status_topic_handler.add_endpoint(self.handler)
def trigger_audit(self, context, audit_uuid=None):

View File

@@ -87,13 +87,13 @@ class BaseSolution(object):
@abc.abstractmethod
def add_action(self,
action_type,
applies_to,
resource_id,
input_parameters=None):
"""Add a new Action in the Action Plan
:param action_type: the unique id of an action type defined in
entry point 'watcher_actions'
:param applies_to: the unique id of the resource to which the
:param resource_id: the unique id of the resource to which the
`Action` applies.
:param input_parameters: An array of input parameters provided as
key-value pairs of strings. Each key-pair contains names and

View File

@@ -18,12 +18,14 @@
#
from oslo_log import log
from watcher.decision_engine.solution.base import BaseSolution
from watcher.applier.actions import base as baction
from watcher.common import exception
from watcher.decision_engine.solution import base
LOG = log.getLogger(__name__)
class DefaultSolution(BaseSolution):
class DefaultSolution(base.BaseSolution):
def __init__(self):
"""Stores a set of actions generated by a strategy
@@ -34,12 +36,17 @@ class DefaultSolution(BaseSolution):
self._actions = []
def add_action(self, action_type,
applies_to,
input_parameters=None):
# todo(jed) add https://pypi.python.org/pypi/schema
input_parameters=None,
resource_id=None):
if input_parameters is not None:
if baction.BaseAction.RESOURCE_ID in input_parameters.keys():
raise exception.ReservedWord(name=baction.BaseAction.
RESOURCE_ID)
if resource_id is not None:
input_parameters[baction.BaseAction.RESOURCE_ID] = resource_id
action = {
'action_type': action_type,
'applies_to': applies_to,
'input_parameters': input_parameters
}
self._actions.append(action)

View File

@@ -17,10 +17,10 @@
# limitations under the License.
#
from enum import Enum
import enum
class StrategyLevel(Enum):
class StrategyLevel(enum.Enum):
conservative = "conservative"
balanced = "balanced"
growth = "growth"

View File

@@ -16,22 +16,21 @@
from oslo_log import log
from watcher.common import clients
from watcher.decision_engine.strategy.context.base import BaseStrategyContext
from watcher.decision_engine.strategy.selection.default import \
DefaultStrategySelector
from watcher.metrics_engine.cluster_model_collector.manager import \
CollectorManager
from watcher.decision_engine.strategy.context import base
from watcher.decision_engine.strategy.selection import default
from watcher.metrics_engine.cluster_model_collector import manager
from watcher import objects
LOG = log.getLogger(__name__)
class DefaultStrategyContext(BaseStrategyContext):
class DefaultStrategyContext(base.BaseStrategyContext):
def __init__(self):
super(DefaultStrategyContext, self).__init__()
LOG.debug("Initializing Strategy Context")
self._strategy_selector = DefaultStrategySelector()
self._collector_manager = CollectorManager()
self._strategy_selector = default.DefaultStrategySelector()
self._collector_manager = manager.CollectorManager()
@property
def collector(self):

View File

@@ -21,12 +21,12 @@ from __future__ import unicode_literals
from oslo_log import log
from watcher.common.loader.default import DefaultLoader
from watcher.common.loader import default
LOG = log.getLogger(__name__)
class DefaultStrategyLoader(DefaultLoader):
class DefaultStrategyLoader(default.DefaultLoader):
def __init__(self):
super(DefaultStrategyLoader, self).__init__(
namespace='watcher_strategies')

View File

@@ -18,10 +18,14 @@
from watcher.decision_engine.strategy.strategies import basic_consolidation
from watcher.decision_engine.strategy.strategies import dummy_strategy
from watcher.decision_engine.strategy.strategies import outlet_temp_control
from watcher.decision_engine.strategy.strategies \
import vm_workload_consolidation
BasicConsolidation = basic_consolidation.BasicConsolidation
OutletTempControl = outlet_temp_control.OutletTempControl
DummyStrategy = dummy_strategy.DummyStrategy
VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation
__all__ = (BasicConsolidation, OutletTempControl, DummyStrategy)
__all__ = (BasicConsolidation, OutletTempControl, DummyStrategy,
VMWorkloadConsolidation)

View File

@@ -30,6 +30,10 @@ to find an optimal :ref:`Solution <solution_definition>`.
When a new :ref:`Goal <goal_definition>` is added to the Watcher configuration,
at least one default associated :ref:`Strategy <strategy_definition>` should be
provided as well.
:ref:`Some default implementations are provided <watcher_strategies>`, but it
is possible to :ref:`develop new implementations <implement_strategy_plugin>`
which are dynamically loaded by Watcher at launch time.
"""
import abc
@@ -38,8 +42,8 @@ from oslo_log import log
import six
from watcher.common import clients
from watcher.decision_engine.solution.default import DefaultSolution
from watcher.decision_engine.strategy.common.level import StrategyLevel
from watcher.decision_engine.solution import default
from watcher.decision_engine.strategy.common import level
LOG = log.getLogger(__name__)
@@ -57,17 +61,17 @@ class BaseStrategy(object):
self._name = name
self.description = description
# default strategy level
self._strategy_level = StrategyLevel.conservative
self._strategy_level = level.StrategyLevel.conservative
self._cluster_state_collector = None
# the solution given by the strategy
self._solution = DefaultSolution()
self._solution = default.DefaultSolution()
self._osc = osc
@abc.abstractmethod
def execute(self, model):
def execute(self, original_model):
"""Execute a strategy
:param model: The name of the strategy to execute (loaded dynamically)
:param original_model: The model the strategy is executed on
:type model: str
:return: A computed solution (via a placement algorithm)
:rtype: :class:`watcher.decision_engine.solution.base.BaseSolution`

View File

@@ -16,22 +16,55 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
*Good server consolidation strategy*
Consolidation of VMs is essential to achieve energy optimization in cloud
environments such as OpenStack. As VMs are spinned up and/or moved over time,
it becomes necessary to migrate VMs among servers to lower the costs. However,
migration of VMs introduces runtime overheads and consumes extra energy, thus
a good server consolidation strategy should carefully plan for migration in
order to both minimize energy consumption and comply to the various SLAs.
"""
from oslo_log import log
from watcher._i18n import _LE, _LI, _LW
from watcher.common import exception
from watcher.decision_engine.model.hypervisor_state import HypervisorState
from watcher.decision_engine.model.resource import ResourceType
from watcher.decision_engine.model.vm_state import VMState
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
from watcher.metrics_engine.cluster_history.ceilometer import \
CeilometerClusterHistory
from watcher.decision_engine.model import hypervisor_state as hyper_state
from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm_state
from watcher.decision_engine.strategy.strategies import base
from watcher.metrics_engine.cluster_history import ceilometer as \
ceilometer_cluster_history
LOG = log.getLogger(__name__)
class BasicConsolidation(BaseStrategy):
class BasicConsolidation(base.BaseStrategy):
"""Basic offline consolidation using live migration
*Description*
This is server consolidation algorithm which not only minimizes the overall
number of used servers, but also minimizes the number of migrations.
*Requirements*
* You must have at least 2 physical compute nodes to run this strategy.
*Limitations*
- It has been developed only for tests.
- It assumes that the virtual machine and the compute node are on the same
private network.
- It assume that live migrations are possible
*Spec URL*
<None>
"""
DEFAULT_NAME = "basic"
DEFAULT_DESCRIPTION = "Basic offline consolidation"
@@ -45,31 +78,11 @@ class BasicConsolidation(BaseStrategy):
osc=None):
"""Basic offline Consolidation using live migration
The basic consolidation algorithm has several limitations.
It has been developed only for tests.
eg: The BasicConsolidation assumes that the virtual mahine and
the compute node are on the same private network.
Good Strategy :
The workloads of the VMs are changing over the time
and often tend to migrate from one physical machine to another.
Hence, the traditional and offline heuristics such as bin packing
are not applicable for the placement VM in cloud computing.
So, the decision Engine optimizer provides placement strategy considering
not only the performance effects but also the workload characteristics of
VMs and others metrics like the power consumption and
the tenants constraints (SLAs).
The watcher optimizer uses an online VM placement technique
based on machine learning and meta-heuristics that must handle :
- multi-objectives
- Contradictory objectives
- Adapt to changes dynamically
- Fast convergence
:param name: the name of the strategy
:param description: a description of the strategy
:param osc: an OpenStackClients object
:param name: The name of the strategy (Default: "basic")
:param description: The description of the strategy
(Default: "Basic offline consolidation")
:param osc: An :py:class:`~watcher.common.clients.OpenStackClients`
instance
"""
super(BasicConsolidation, self).__init__(name, description, osc)
@@ -104,12 +117,13 @@ class BasicConsolidation(BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory(osc=self.osc)
self._ceilometer = (ceilometer_cluster_history.
CeilometerClusterHistory(osc=self.osc))
return self._ceilometer
@ceilometer.setter
def ceilometer(self, c):
self._ceilometer = c
def ceilometer(self, ceilometer):
self._ceilometer = ceilometer
def compute_attempts(self, size_cluster):
"""Upper bound of the number of migration
@@ -118,13 +132,13 @@ class BasicConsolidation(BaseStrategy):
"""
self.migration_attempts = size_cluster * self.bound_migration
def check_migration(self, model,
def check_migration(self, cluster_data_model,
src_hypervisor,
dest_hypervisor,
vm_to_mig):
"""check if the migration is possible
:param model: the current state of the cluster
:param cluster_data_model: the current state of the cluster
:param src_hypervisor: the current node of the virtual machine
:param dest_hypervisor: the destination of the virtual machine
:param vm_to_mig: the virtual machine
@@ -141,28 +155,32 @@ class BasicConsolidation(BaseStrategy):
total_cores = 0
total_disk = 0
total_mem = 0
cap_cores = model.get_resource_from_id(ResourceType.cpu_cores)
cap_disk = model.get_resource_from_id(ResourceType.disk)
cap_mem = model.get_resource_from_id(ResourceType.memory)
cpu_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.cpu_cores)
disk_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.disk)
memory_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.memory)
for vm_id in model.get_mapping().get_node_vms(dest_hypervisor):
vm = model.get_vm_from_id(vm_id)
total_cores += cap_cores.get_capacity(vm)
total_disk += cap_disk.get_capacity(vm)
total_mem += cap_mem.get_capacity(vm)
for vm_id in cluster_data_model. \
get_mapping().get_node_vms(dest_hypervisor):
vm = cluster_data_model.get_vm_from_id(vm_id)
total_cores += cpu_capacity.get_capacity(vm)
total_disk += disk_capacity.get_capacity(vm)
total_mem += memory_capacity.get_capacity(vm)
# capacity requested by hypervisor
total_cores += cap_cores.get_capacity(vm_to_mig)
total_disk += cap_disk.get_capacity(vm_to_mig)
total_mem += cap_mem.get_capacity(vm_to_mig)
total_cores += cpu_capacity.get_capacity(vm_to_mig)
total_disk += disk_capacity.get_capacity(vm_to_mig)
total_mem += memory_capacity.get_capacity(vm_to_mig)
return self.check_threshold(model,
return self.check_threshold(cluster_data_model,
dest_hypervisor,
total_cores,
total_disk,
total_mem)
def check_threshold(self, model,
def check_threshold(self, cluster_data_model,
dest_hypervisor,
total_cores,
total_disk,
@@ -172,23 +190,23 @@ class BasicConsolidation(BaseStrategy):
check the threshold value defined by the ratio of
aggregated CPU capacity of VMs on one node to CPU capacity
of this node must not exceed the threshold value.
:param dest_hypervisor:
:param cluster_data_model: the current state of the cluster
:param dest_hypervisor: the destination of the virtual machine
:param total_cores
:param total_disk
:param total_mem
:return: True if the threshold is not exceed
"""
cap_cores = model.get_resource_from_id(ResourceType.cpu_cores)
cap_disk = model.get_resource_from_id(ResourceType.disk)
cap_mem = model.get_resource_from_id(ResourceType.memory)
# available
cores_available = cap_cores.get_capacity(dest_hypervisor)
disk_available = cap_disk.get_capacity(dest_hypervisor)
mem_available = cap_mem.get_capacity(dest_hypervisor)
cpu_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(dest_hypervisor)
disk_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.disk).get_capacity(dest_hypervisor)
memory_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.memory).get_capacity(dest_hypervisor)
if cores_available >= total_cores * self.threshold_cores \
and disk_available >= total_disk * self.threshold_disk \
and mem_available >= total_mem * self.threshold_mem:
if (cpu_capacity >= total_cores * self.threshold_cores and
disk_capacity >= total_disk * self.threshold_disk and
memory_capacity >= total_mem * self.threshold_mem):
return True
else:
return False
@@ -214,25 +232,26 @@ class BasicConsolidation(BaseStrategy):
def get_number_of_migrations(self):
return self.number_of_migrations
def calculate_weight(self, model, element, total_cores_used,
total_disk_used, total_memory_used):
def calculate_weight(self, cluster_data_model, element,
total_cores_used, total_disk_used,
total_memory_used):
"""Calculate weight of every resource
:param model:
:param cluster_data_model:
:param element:
:param total_cores_used:
:param total_disk_used:
:param total_memory_used:
:return:
"""
cpu_capacity = model.get_resource_from_id(
ResourceType.cpu_cores).get_capacity(element)
cpu_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(element)
disk_capacity = model.get_resource_from_id(
ResourceType.disk).get_capacity(element)
disk_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.disk).get_capacity(element)
memory_capacity = model.get_resource_from_id(
ResourceType.memory).get_capacity(element)
memory_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.memory).get_capacity(element)
score_cores = (1 - (float(cpu_capacity) - float(total_cores_used)) /
float(cpu_capacity))
@@ -258,25 +277,25 @@ class BasicConsolidation(BaseStrategy):
:return:
"""
resource_id = "%s_%s" % (hypervisor.uuid, hypervisor.hostname)
cpu_avg_vm = self.ceilometer. \
host_avg_cpu_util = self.ceilometer. \
statistic_aggregation(resource_id=resource_id,
meter_name=self.HOST_CPU_USAGE_METRIC_NAME,
period="7200",
aggregate='avg'
)
if cpu_avg_vm is None:
if host_avg_cpu_util is None:
LOG.error(
_LE("No values returned by %(resource_id)s "
"for %(metric_name)s"),
resource_id=resource_id,
metric_name=self.HOST_CPU_USAGE_METRIC_NAME,
)
cpu_avg_vm = 100
host_avg_cpu_util = 100
cpu_capacity = model.get_resource_from_id(
ResourceType.cpu_cores).get_capacity(hypervisor)
resource.ResourceType.cpu_cores).get_capacity(hypervisor)
total_cores_used = cpu_capacity * (cpu_avg_vm / 100)
total_cores_used = cpu_capacity * (host_avg_cpu_util / 100)
return self.calculate_weight(model, hypervisor, total_cores_used,
0,
@@ -294,14 +313,14 @@ class BasicConsolidation(BaseStrategy):
else:
return 0
def calculate_score_vm(self, vm, model):
def calculate_score_vm(self, vm, cluster_data_model):
"""Calculate Score of virtual machine
:param vm: the virtual machine
:param model: the model
:param cluster_data_model: the cluster model
:return: score
"""
if model is None:
if cluster_data_model is None:
raise exception.ClusterStateNotDefined()
vm_cpu_utilization = self.ceilometer. \
@@ -320,23 +339,22 @@ class BasicConsolidation(BaseStrategy):
)
vm_cpu_utilization = 100
cpu_capacity = model.get_resource_from_id(
ResourceType.cpu_cores).get_capacity(vm)
cpu_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(vm)
total_cores_used = cpu_capacity * (vm_cpu_utilization / 100.0)
return self.calculate_weight(model, vm, total_cores_used,
0,
0)
return self.calculate_weight(cluster_data_model, vm,
total_cores_used, 0, 0)
def add_change_service_state(self, applies_to, state):
def add_change_service_state(self, resource_id, state):
parameters = {'state': state}
self.solution.add_action(action_type=self.CHANGE_NOVA_SERVICE_STATE,
applies_to=applies_to,
resource_id=resource_id,
input_parameters=parameters)
def add_migration(self,
applies_to,
resource_id,
migration_type,
src_hypervisor,
dst_hypervisor):
@@ -344,17 +362,19 @@ class BasicConsolidation(BaseStrategy):
'src_hypervisor': src_hypervisor,
'dst_hypervisor': dst_hypervisor}
self.solution.add_action(action_type=self.MIGRATION,
applies_to=applies_to,
resource_id=resource_id,
input_parameters=parameters)
def score_of_nodes(self, current_model, score):
def score_of_nodes(self, cluster_data_model, score):
"""Calculate score of nodes based on load by VMs"""
for hypervisor_id in current_model.get_all_hypervisors():
hypervisor = current_model.get_hypervisor_from_id(hypervisor_id)
count = current_model.get_mapping(). \
for hypervisor_id in cluster_data_model.get_all_hypervisors():
hypervisor = cluster_data_model. \
get_hypervisor_from_id(hypervisor_id)
count = cluster_data_model.get_mapping(). \
get_node_vms_from_id(hypervisor_id)
if len(count) > 0:
result = self.calculate_score_node(hypervisor, current_model)
result = self.calculate_score_node(hypervisor,
cluster_data_model)
else:
''' the hypervisor has not VMs '''
result = 0
@@ -362,16 +382,16 @@ class BasicConsolidation(BaseStrategy):
score.append((hypervisor_id, result))
return score
def node_and_vm_score(self, s, score, current_model):
def node_and_vm_score(self, sorted_score, score, current_model):
"""Get List of VMs from Node"""
node_to_release = s[len(score) - 1][0]
node_to_release = sorted_score[len(score) - 1][0]
vms_to_mig = current_model.get_mapping().get_node_vms_from_id(
node_to_release)
vm_score = []
for vm_id in vms_to_mig:
vm = current_model.get_vm_from_id(vm_id)
if vm.state == VMState.ACTIVE.value:
if vm.state == vm_state.VMState.ACTIVE.value:
vm_score.append(
(vm_id, self.calculate_score_vm(vm, current_model)))
@@ -390,51 +410,53 @@ class BasicConsolidation(BaseStrategy):
mig_src_hypervisor)) == 0:
self.add_change_service_state(mig_src_hypervisor.
uuid,
HypervisorState.
OFFLINE.value)
hyper_state.HypervisorState.
DISABLED.value)
self.number_of_released_nodes += 1
def calculate_m(self, v, current_model, node_to_release, s):
m = 0
for vm in v:
for j in range(0, len(s)):
def calculate_num_migrations(self, sorted_vms, current_model,
node_to_release, sorted_score):
number_migrations = 0
for vm in sorted_vms:
for j in range(0, len(sorted_score)):
mig_vm = current_model.get_vm_from_id(vm[0])
mig_src_hypervisor = current_model.get_hypervisor_from_id(
node_to_release)
mig_dst_hypervisor = current_model.get_hypervisor_from_id(
s[j][0])
sorted_score[j][0])
result = self.check_migration(current_model,
mig_src_hypervisor,
mig_dst_hypervisor, mig_vm)
if result is True:
if result:
self.create_migration_vm(
current_model, mig_vm,
mig_src_hypervisor, mig_dst_hypervisor)
m += 1
number_migrations += 1
break
return m
return number_migrations
def unsuccessful_migration_actualization(self, m, unsuccessful_migration):
if m > 0:
self.number_of_migrations += m
def unsuccessful_migration_actualization(self, number_migrations,
unsuccessful_migration):
if number_migrations > 0:
self.number_of_migrations += number_migrations
return 0
else:
return unsuccessful_migration + 1
def execute(self, orign_model):
def execute(self, original_model):
LOG.info(_LI("Initializing Sercon Consolidation"))
if orign_model is None:
if original_model is None:
raise exception.ClusterStateNotDefined()
# todo(jed) clone model
current_model = orign_model
current_model = original_model
self.efficacy = 100
unsuccessful_migration = 0
first = True
first_migration = True
size_cluster = len(current_model.get_all_hypervisors())
if size_cluster == 0:
raise exception.ClusterEmpty()
@@ -446,24 +468,24 @@ class BasicConsolidation(BaseStrategy):
count = current_model.get_mapping(). \
get_node_vms_from_id(hypervisor_id)
if len(count) == 0:
if hypervisor.state == HypervisorState.ONLINE:
if hypervisor.state == hyper_state.HypervisorState.ENABLED:
self.add_change_service_state(hypervisor_id,
HypervisorState.
OFFLINE.value)
hyper_state.HypervisorState.
DISABLED.value)
while self.get_allowed_migration_attempts() >= unsuccessful_migration:
if first is not True:
if not first_migration:
self.efficacy = self.calculate_migration_efficacy()
if self.efficacy < float(self.target_efficacy):
break
first = False
first_migration = False
score = []
score = self.score_of_nodes(current_model, score)
''' sort compute nodes by Score decreasing '''''
s = sorted(score, reverse=True, key=lambda x: (x[1]))
LOG.debug("Hypervisor(s) BFD {0}".format(s))
sorted_score = sorted(score, reverse=True, key=lambda x: (x[1]))
LOG.debug("Hypervisor(s) BFD {0}".format(sorted_score))
''' get Node to be released '''
if len(score) == 0:
@@ -473,17 +495,18 @@ class BasicConsolidation(BaseStrategy):
break
node_to_release, vm_score = self.node_and_vm_score(
s, score, current_model)
sorted_score, score, current_model)
''' sort VMs by Score '''
v = sorted(vm_score, reverse=True, key=lambda x: (x[1]))
sorted_vms = sorted(vm_score, reverse=True, key=lambda x: (x[1]))
# BFD: Best Fit Decrease
LOG.debug("VM(s) BFD {0}".format(v))
LOG.debug("VM(s) BFD {0}".format(sorted_vms))
m = self.calculate_m(v, current_model, node_to_release, s)
migrations = self.calculate_num_migrations(
sorted_vms, current_model, node_to_release, sorted_score)
unsuccessful_migration = self.unsuccessful_migration_actualization(
m, unsuccessful_migration)
migrations, unsuccessful_migration)
infos = {
"number_of_migrations": self.number_of_migrations,
"number_of_nodes_released": self.number_of_released_nodes,

View File

@@ -18,12 +18,32 @@
#
from oslo_log import log
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
from watcher.decision_engine.strategy.strategies import base
LOG = log.getLogger(__name__)
class DummyStrategy(BaseStrategy):
class DummyStrategy(base.BaseStrategy):
"""Dummy strategy used for integration testing via Tempest
*Description*
This strategy does not provide any useful optimization. Indeed, its only
purpose is to be used by Tempest tests.
*Requirements*
<None>
*Limitations*
Do not use in production.
*Spec URL*
<None>
"""
DEFAULT_NAME = "dummy"
DEFAULT_DESCRIPTION = "Dummy Strategy"
@@ -34,18 +54,16 @@ class DummyStrategy(BaseStrategy):
osc=None):
super(DummyStrategy, self).__init__(name, description, osc)
def execute(self, model):
def execute(self, original_model):
LOG.debug("Executing Dummy strategy")
parameters = {'message': 'hello World'}
self.solution.add_action(action_type=self.NOP,
applies_to="",
input_parameters=parameters)
parameters = {'message': 'Welcome'}
self.solution.add_action(action_type=self.NOP,
applies_to="",
input_parameters=parameters)
self.solution.add_action(action_type=self.SLEEP,
applies_to="",
input_parameters={'duration': '5'})
input_parameters={'duration': 5.0})
return self.solution

View File

@@ -16,20 +16,60 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
*Good Thermal Strategy*:
Towards to software defined infrastructure, the power and thermal
intelligences is being adopted to optimize workload, which can help
improve efficiency, reduce power, as well as to improve datacenter PUE
and lower down operation cost in data center.
Outlet (Exhaust Air) Temperature is one of the important thermal
telemetries to measure thermal/workload status of server.
"""
from oslo_log import log
from watcher._i18n import _LE
from watcher.common import exception as wexc
from watcher.decision_engine.model.resource import ResourceType
from watcher.decision_engine.model.vm_state import VMState
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
from watcher.metrics_engine.cluster_history.ceilometer import \
CeilometerClusterHistory
from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm_state
from watcher.decision_engine.strategy.strategies import base
from watcher.metrics_engine.cluster_history import ceilometer as ceil
LOG = log.getLogger(__name__)
class OutletTempControl(BaseStrategy):
class OutletTempControl(base.BaseStrategy):
"""[PoC] Outlet temperature control using live migration
*Description*
It is a migration strategy based on the outlet temperature of compute
hosts. It generates solutions to move a workload whenever a server's
outlet temperature is higher than the specified threshold.
*Requirements*
* Hardware: All computer hosts should support IPMI and PTAS technology
* Software: Ceilometer component ceilometer-agent-ipmi running
in each compute host, and Ceilometer API can report such telemetry
``hardware.ipmi.node.outlet_temperature`` successfully.
* You must have at least 2 physical compute hosts to run this strategy.
*Limitations*
- This is a proof of concept that is not meant to be used in production
- We cannot forecast how many servers should be migrated. This is the
reason why we only plan a single virtual machine migration at a time.
So it's better to use this algorithm with `CONTINUOUS` audits.
- It assume that live migrations are possible
*Spec URL*
https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/approved/outlet-temperature-based-strategy.rst
""" # noqa
DEFAULT_NAME = "outlet_temp_control"
DEFAULT_DESCRIPTION = "outlet temperature based migration strategy"
@@ -42,29 +82,7 @@ class OutletTempControl(BaseStrategy):
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
osc=None):
"""[PoC]Outlet temperature control using live migration
It is a migration strategy based on the Outlet Temperature of physical
servers. It generates solutions to move a workload whenever a servers
outlet temperature is higher than the specified threshold. As of now,
we cannot forecast how many instances should be migrated. This is the
reason why we simply plan a single virtual machine migration.
So it's better to use this algorithm with CONTINUOUS audits.
Requirements:
* Hardware: computer node should support IPMI and PTAS technology
* Software: Ceilometer component ceilometer-agent-ipmi running
in each compute node, and Ceilometer API can report such telemetry
"hardware.ipmi.node.outlet_temperature" successfully.
* You must have at least 2 physical compute nodes to run this strategy.
Good Strategy:
Towards to software defined infrastructure, the power and thermal
intelligences is being adopted to optimize workload, which can help
improve efficiency, reduce power, as well as to improve datacenter PUE
and lower down operation cost in data center.
Outlet(Exhaust Air) Temperature is one of the important thermal
telemetries to measure thermal/workload status of server.
"""Outlet temperature control using live migration
:param name: the name of the strategy
:param description: a description of the strategy
@@ -81,32 +99,33 @@ class OutletTempControl(BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory(osc=self.osc)
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
return self._ceilometer
@ceilometer.setter
def ceilometer(self, c):
self._ceilometer = c
def calc_used_res(self, model, hypervisor, cap_cores, cap_mem, cap_disk):
def calc_used_res(self, cluster_data_model, hypervisor, cpu_capacity,
memory_capacity, disk_capacity):
'''calculate the used vcpus, memory and disk based on VM flavors'''
vms = model.get_mapping().get_node_vms(hypervisor)
vms = cluster_data_model.get_mapping().get_node_vms(hypervisor)
vcpus_used = 0
memory_mb_used = 0
disk_gb_used = 0
if len(vms) > 0:
for vm_id in vms:
vm = model.get_vm_from_id(vm_id)
vcpus_used += cap_cores.get_capacity(vm)
memory_mb_used += cap_mem.get_capacity(vm)
disk_gb_used += cap_disk.get_capacity(vm)
vm = cluster_data_model.get_vm_from_id(vm_id)
vcpus_used += cpu_capacity.get_capacity(vm)
memory_mb_used += memory_capacity.get_capacity(vm)
disk_gb_used += disk_capacity.get_capacity(vm)
return vcpus_used, memory_mb_used, disk_gb_used
def group_hosts_by_outlet_temp(self, model):
'''Group hosts based on outlet temp meters'''
def group_hosts_by_outlet_temp(self, cluster_data_model):
"""Group hosts based on outlet temp meters"""
hypervisors = model.get_all_hypervisors()
hypervisors = cluster_data_model.get_all_hypervisors()
size_cluster = len(hypervisors)
if size_cluster == 0:
raise wexc.ClusterEmpty()
@@ -114,7 +133,8 @@ class OutletTempControl(BaseStrategy):
hosts_need_release = []
hosts_target = []
for hypervisor_id in hypervisors:
hypervisor = model.get_hypervisor_from_id(hypervisor_id)
hypervisor = cluster_data_model.get_hypervisor_from_id(
hypervisor_id)
resource_id = hypervisor.uuid
outlet_temp = self.ceilometer.statistic_aggregation(
@@ -136,18 +156,19 @@ class OutletTempControl(BaseStrategy):
hosts_target.append(hvmap)
return hosts_need_release, hosts_target
def choose_vm_to_migrate(self, model, hosts):
'''pick up an active vm instance to migrate from provided hosts'''
def choose_vm_to_migrate(self, cluster_data_model, hosts):
"""pick up an active vm instance to migrate from provided hosts"""
for hvmap in hosts:
mig_src_hypervisor = hvmap['hv']
vms_of_src = model.get_mapping().get_node_vms(mig_src_hypervisor)
vms_of_src = cluster_data_model.get_mapping().get_node_vms(
mig_src_hypervisor)
if len(vms_of_src) > 0:
for vm_id in vms_of_src:
try:
# select the first active VM to migrate
vm = model.get_vm_from_id(vm_id)
if vm.state != VMState.ACTIVE.value:
vm = cluster_data_model.get_vm_from_id(vm_id)
if vm.state != vm_state.VMState.ACTIVE.value:
LOG.info(_LE("VM not active, skipped: %s"),
vm.uuid)
continue
@@ -158,44 +179,45 @@ class OutletTempControl(BaseStrategy):
return None
def filter_dest_servers(self, model, hosts, vm_to_migrate):
'''Only return hosts with sufficient available resources'''
def filter_dest_servers(self, cluster_data_model, hosts, vm_to_migrate):
"""Only return hosts with sufficient available resources"""
cap_cores = model.get_resource_from_id(ResourceType.cpu_cores)
cap_disk = model.get_resource_from_id(ResourceType.disk)
cap_mem = model.get_resource_from_id(ResourceType.memory)
cpu_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.cpu_cores)
disk_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.disk)
memory_capacity = cluster_data_model.get_resource_from_id(
resource.ResourceType.memory)
required_cores = cap_cores.get_capacity(vm_to_migrate)
required_disk = cap_disk.get_capacity(vm_to_migrate)
required_mem = cap_mem.get_capacity(vm_to_migrate)
required_cores = cpu_capacity.get_capacity(vm_to_migrate)
required_disk = disk_capacity.get_capacity(vm_to_migrate)
required_memory = memory_capacity.get_capacity(vm_to_migrate)
# filter hypervisors without enough resource
dest_servers = []
for hvmap in hosts:
host = hvmap['hv']
# available
cores_used, mem_used, disk_used = self.calc_used_res(model,
host,
cap_cores,
cap_mem,
cap_disk)
cores_available = cap_cores.get_capacity(host) - cores_used
disk_available = cap_disk.get_capacity(host) - mem_used
mem_available = cap_mem.get_capacity(host) - disk_used
cores_used, mem_used, disk_used = self.calc_used_res(
cluster_data_model, host, cpu_capacity, memory_capacity,
disk_capacity)
cores_available = cpu_capacity.get_capacity(host) - cores_used
disk_available = disk_capacity.get_capacity(host) - mem_used
mem_available = memory_capacity.get_capacity(host) - disk_used
if cores_available >= required_cores \
and disk_available >= required_disk \
and mem_available >= required_mem:
and mem_available >= required_memory:
dest_servers.append(hvmap)
return dest_servers
def execute(self, orign_model):
def execute(self, original_model):
LOG.debug("Initializing Outlet temperature strategy")
if orign_model is None:
if original_model is None:
raise wexc.ClusterStateNotDefined()
current_model = orign_model
current_model = original_model
hosts_need_release, hosts_target = self.group_hosts_by_outlet_temp(
current_model)
@@ -239,10 +261,10 @@ class OutletTempControl(BaseStrategy):
mig_src_hypervisor,
mig_dst_hypervisor):
parameters = {'migration_type': 'live',
'src_hypervisor': mig_src_hypervisor,
'dst_hypervisor': mig_dst_hypervisor}
'src_hypervisor': mig_src_hypervisor.uuid,
'dst_hypervisor': mig_dst_hypervisor.uuid}
self.solution.add_action(action_type=self.MIGRATION,
applies_to=vm_src,
resource_id=vm_src.uuid,
input_parameters=parameters)
self.solution.model = current_model

View File

@@ -0,0 +1,523 @@
# -*- encoding: utf-8 -*-
#
# Authors: Vojtech CIMA <cima@zhaw.ch>
# Bruno GRAZIOLI <gaea@zhaw.ch>
# Sean MURPHY <murp@zhaw.ch>
#
# 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 copy import deepcopy
from oslo_log import log
import six
from watcher._i18n import _LE, _LI
from watcher.common import exception
from watcher.decision_engine.model import hypervisor_state as hyper_state
from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm_state
from watcher.decision_engine.strategy.strategies import base
from watcher.metrics_engine.cluster_history import ceilometer \
as ceilometer_cluster_history
LOG = log.getLogger(__name__)
class VMWorkloadConsolidation(base.BaseStrategy):
"""VM Workload Consolidation Strategy.
A load consolidation strategy based on heuristic first-fit
algorithm which focuses on measured CPU utilization and tries to
minimize hosts which have too much or too little load respecting
resource capacity constraints.
This strategy produces a solution resulting in more efficient
utilization of cluster resources using following four phases:
* Offload phase - handling over-utilized resources
* Consolidation phase - handling under-utilized resources
* Solution optimization - reducing number of migrations
* Deactivation of unused hypervisors
A capacity coefficients (cc) might be used to adjust optimization
thresholds. Different resources may require different coefficient
values as well as setting up different coefficient values in both
phases may lead to to more efficient consolidation in the end.
If the cc equals 1 the full resource capacity may be used, cc
values lower than 1 will lead to resource under utilization and
values higher than 1 will lead to resource overbooking.
e.g. If targeted utilization is 80% of hypervisor capacity,
the coefficient in the consolidation phase will be 0.8, but
may any lower value in the offloading phase. The lower it gets
the cluster will appear more released (distributed) for the
following consolidation phase.
As this strategy laverages VM live migration to move the load
from one hypervisor to another, this feature needs to be set up
correctly on all hypervisors within the cluster.
This strategy assumes it is possible to live migrate any VM from
an active hypervisor to any other active hypervisor.
"""
DEFAULT_NAME = 'vm_workload_consolidation'
DEFAULT_DESCRIPTION = 'VM Workload Consolidation Strategy'
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
osc=None):
super(VMWorkloadConsolidation, self).__init__(name, description, osc)
self._ceilometer = None
self.number_of_migrations = 0
self.number_of_released_hypervisors = 0
self.ceilometer_vm_data_cache = dict()
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = (ceilometer_cluster_history.
CeilometerClusterHistory(osc=self.osc))
return self._ceilometer
@ceilometer.setter
def ceilometer(self, ceilometer):
self._ceilometer = ceilometer
def get_state_str(self, state):
"""Get resource state in string format.
:param state: resource state of unknown type
"""
if isinstance(state, six.string_types):
return state
elif (type(state) == hyper_state.HypervisorState or
type(state) == vm_state.VMState):
return state.value
else:
LOG.error(_LE('Unexpexted resource state type, '
'state=%(state)s, state_type=%(st)s.'),
state=state,
st=type(state))
raise exception.WatcherException
def add_action_activate_hypervisor(self, hypervisor):
"""Add an action for hypervisor activation into the solution.
:param hypervisor: hypervisor object
:return: None
"""
params = {'state': hyper_state.HypervisorState.ONLINE.value}
self.solution.add_action(
action_type='change_nova_service_state',
resource_id=hypervisor.uuid,
input_parameters=params)
self.number_of_released_hypervisors -= 1
def add_action_deactivate_hypervisor(self, hypervisor):
"""Add an action for hypervisor deactivation into the solution.
:param hypervisor: hypervisor object
:return: None
"""
params = {'state': hyper_state.HypervisorState.OFFLINE.value}
self.solution.add_action(
action_type='change_nova_service_state',
resource_id=hypervisor.uuid,
input_parameters=params)
self.number_of_released_hypervisors += 1
def add_migration(self, vm_uuid, src_hypervisor,
dst_hypervisor, model):
"""Add an action for VM migration into the solution.
:param vm_uuid: vm uuid
:param src_hypervisor: hypervisor object
:param dst_hypervisor: hypervisor object
:param model: model_root object
:return: None
"""
vm = model.get_vm_from_id(vm_uuid)
vm_state_str = self.get_state_str(vm.state)
if vm_state_str != vm_state.VMState.ACTIVE.value:
'''
Watcher curently only supports live VM migration and block live
VM migration which both requires migrated VM to be active.
When supported, the cold migration may be used as a fallback
migration mechanism to move non active VMs.
'''
LOG.error(_LE('Cannot live migrate: vm_uuid=%(vm_uuid)s, '
'state=%(vm_state)s.'),
vm_uuid=vm_uuid,
vm_state=vm_state_str)
raise exception.WatcherException
migration_type = 'live'
dst_hyper_state_str = self.get_state_str(dst_hypervisor.state)
if dst_hyper_state_str == hyper_state.HypervisorState.OFFLINE.value:
self.add_action_activate_hypervisor(dst_hypervisor)
model.get_mapping().unmap(src_hypervisor, vm)
model.get_mapping().map(dst_hypervisor, vm)
params = {'migration_type': migration_type,
'src_hypervisor': src_hypervisor.uuid,
'dst_hypervisor': dst_hypervisor.uuid}
self.solution.add_action(action_type='migrate',
resource_id=vm.uuid,
input_parameters=params)
self.number_of_migrations += 1
def deactivate_unused_hypervisors(self, model):
"""Generate actions for deactivation of unused hypervisors.
:param model: model_root object
:return: None
"""
for hypervisor in model.get_all_hypervisors().values():
if len(model.get_mapping().get_node_vms(hypervisor)) == 0:
self.add_action_deactivate_hypervisor(hypervisor)
def get_prediction_model(self, model):
"""Return a deepcopy of a model representing current cluster state.
:param model: model_root object
:return: model_root object
"""
return deepcopy(model)
def get_vm_utilization(self, vm_uuid, model, period=3600, aggr='avg'):
"""Collect cpu, ram and disk utilization statistics of a VM.
:param vm_uuid: vm object
:param model: model_root object
:param period: seconds
:param aggr: string
:return: dict(cpu(number of vcpus used), ram(MB used), disk(B used))
"""
if vm_uuid in self.ceilometer_vm_data_cache.keys():
return self.ceilometer_vm_data_cache.get(vm_uuid)
cpu_util_metric = 'cpu_util'
ram_util_metric = 'memory.usage'
ram_alloc_metric = 'memory'
disk_alloc_metric = 'disk.root.size'
vm_cpu_util = self.ceilometer.statistic_aggregation(
resource_id=vm_uuid, meter_name=cpu_util_metric,
period=period, aggregate=aggr)
vm_cpu_cores = model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(
model.get_vm_from_id(vm_uuid))
if vm_cpu_util:
total_cpu_utilization = vm_cpu_cores * (vm_cpu_util / 100.0)
else:
total_cpu_utilization = vm_cpu_cores
vm_ram_util = self.ceilometer.statistic_aggregation(
resource_id=vm_uuid, meter_name=ram_util_metric,
period=period, aggregate=aggr)
if not vm_ram_util:
vm_ram_util = self.ceilometer.statistic_aggregation(
resource_id=vm_uuid, meter_name=ram_alloc_metric,
period=period, aggregate=aggr)
vm_disk_util = self.ceilometer.statistic_aggregation(
resource_id=vm_uuid, meter_name=disk_alloc_metric,
period=period, aggregate=aggr)
if not vm_ram_util or not vm_disk_util:
LOG.error(
_LE('No values returned by %(resource_id)s '
'for memory.usage or disk.root.size'),
resource_id=vm_uuid
)
raise exception.NoDataFound
self.ceilometer_vm_data_cache[vm_uuid] = dict(
cpu=total_cpu_utilization, ram=vm_ram_util, disk=vm_disk_util)
return self.ceilometer_vm_data_cache.get(vm_uuid)
def get_hypervisor_utilization(self, hypervisor, model, period=3600,
aggr='avg'):
"""Collect cpu, ram and disk utilization statistics of a hypervisor.
:param hypervisor: hypervisor object
:param model: model_root object
:param period: seconds
:param aggr: string
:return: dict(cpu(number of cores used), ram(MB used), disk(B used))
"""
hypervisor_vms = model.get_mapping().get_node_vms_from_id(
hypervisor.uuid)
hypervisor_ram_util = 0
hypervisor_disk_util = 0
hypervisor_cpu_util = 0
for vm_uuid in hypervisor_vms:
vm_util = self.get_vm_utilization(vm_uuid, model, period, aggr)
hypervisor_cpu_util += vm_util['cpu']
hypervisor_ram_util += vm_util['ram']
hypervisor_disk_util += vm_util['disk']
return dict(cpu=hypervisor_cpu_util, ram=hypervisor_ram_util,
disk=hypervisor_disk_util)
def get_hypervisor_capacity(self, hypervisor, model):
"""Collect cpu, ram and disk capacity of a hypervisor.
:param hypervisor: hypervisor object
:param model: model_root object
:return: dict(cpu(cores), ram(MB), disk(B))
"""
hypervisor_cpu_capacity = model.get_resource_from_id(
resource.ResourceType.cpu_cores).get_capacity(hypervisor)
hypervisor_disk_capacity = model.get_resource_from_id(
resource.ResourceType.disk_capacity).get_capacity(hypervisor)
hypervisor_ram_capacity = model.get_resource_from_id(
resource.ResourceType.memory).get_capacity(hypervisor)
return dict(cpu=hypervisor_cpu_capacity, ram=hypervisor_ram_capacity,
disk=hypervisor_disk_capacity)
def get_relative_hypervisor_utilization(self, hypervisor, model):
"""Return relative hypervisor utilization (rhu).
:param hypervisor: hypervisor object
:param model: model_root object
:return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>}
"""
rhu = {}
util = self.get_hypervisor_utilization(hypervisor, model)
cap = self.get_hypervisor_capacity(hypervisor, model)
for k in util.keys():
rhu[k] = float(util[k]) / float(cap[k])
return rhu
def get_relative_cluster_utilization(self, model):
"""Calculate relative cluster utilization (rcu).
RCU is an average of relative utilizations (rhu) of active hypervisors.
:param model: model_root object
:return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>}
"""
hypervisors = model.get_all_hypervisors().values()
rcu = {}
counters = {}
for hypervisor in hypervisors:
hyper_state_str = self.get_state_str(hypervisor.state)
if hyper_state_str == hyper_state.HypervisorState.ONLINE.value:
rhu = self.get_relative_hypervisor_utilization(
hypervisor, model)
for k in rhu.keys():
if k not in rcu:
rcu[k] = 0
if k not in counters:
counters[k] = 0
rcu[k] += rhu[k]
counters[k] += 1
for k in rcu.keys():
rcu[k] /= counters[k]
return rcu
def is_overloaded(self, hypervisor, model, cc):
"""Indicate whether a hypervisor is overloaded.
This considers provided resource capacity coefficients (cc).
:param hypervisor: hypervisor object
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
:return: [True, False]
"""
hypervisor_capacity = self.get_hypervisor_capacity(hypervisor, model)
hypervisor_utilization = self.get_hypervisor_utilization(
hypervisor, model)
metrics = ['cpu']
for m in metrics:
if hypervisor_utilization[m] > hypervisor_capacity[m] * cc[m]:
return True
return False
def vm_fits(self, vm_uuid, hypervisor, model, cc):
"""Indicate whether is a hypervisor able to accomodate a VM.
This considers provided resource capacity coefficients (cc).
:param vm_uuid: string
:param hypervisor: hypervisor object
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
:return: [True, False]
"""
hypervisor_capacity = self.get_hypervisor_capacity(hypervisor, model)
hypervisor_utilization = self.get_hypervisor_utilization(
hypervisor, model)
vm_utilization = self.get_vm_utilization(vm_uuid, model)
metrics = ['cpu', 'ram', 'disk']
for m in metrics:
if (vm_utilization[m] + hypervisor_utilization[m] >
hypervisor_capacity[m] * cc[m]):
return False
return True
def optimize_solution(self, model):
"""Optimize solution.
This is done by eliminating unnecessary or circular set of migrations
which can be replaced by a more efficient solution.
e.g.:
* A->B, B->C => replace migrations A->B, B->C with
a single migration A->C as both solution result in
VM running on hypervisor C which can be achieved with
one migration instead of two.
* A->B, B->A => remove A->B and B->A as they do not result
in a new VM placement.
:param model: model_root object
"""
migrate_actions = (
a for a in self.solution.actions if a[
'action_type'] == 'migrate')
vm_to_be_migrated = (a['input_parameters']['resource_id']
for a in migrate_actions)
vm_uuids = list(set(vm_to_be_migrated))
for vm_uuid in vm_uuids:
actions = list(
a for a in self.solution.actions if a[
'input_parameters'][
'resource_id'] == vm_uuid)
if len(actions) > 1:
src = actions[0]['input_parameters']['src_hypervisor']
dst = actions[-1]['input_parameters']['dst_hypervisor']
for a in actions:
self.solution.actions.remove(a)
self.number_of_migrations -= 1
if src != dst:
self.add_migration(vm_uuid, src, dst, model)
def offload_phase(self, model, cc):
"""Perform offloading phase.
This considers provided resource capacity coefficients.
Offload phase performing first-fit based bin packing to offload
overloaded hypervisors. This is done in a fashion of moving
the least CPU utilized VM first as live migration these
generaly causes less troubles. This phase results in a cluster
with no overloaded hypervisors.
* This phase is be able to activate turned off hypervisors (if needed
and any available) in the case of the resource capacity provided by
active hypervisors is not able to accomodate all the load.
As the offload phase is later followed by the consolidation phase,
the hypervisor activation in this phase doesn't necessarily results
in more activated hypervisors in the final solution.
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
"""
sorted_hypervisors = sorted(
model.get_all_hypervisors().values(),
key=lambda x: self.get_hypervisor_utilization(x, model)['cpu'])
for hypervisor in reversed(sorted_hypervisors):
if self.is_overloaded(hypervisor, model, cc):
for vm in sorted(model.get_mapping().get_node_vms(hypervisor),
key=lambda x: self.get_vm_utilization(
x, model)['cpu']):
for dst_hypervisor in reversed(sorted_hypervisors):
if self.vm_fits(vm, dst_hypervisor, model, cc):
self.add_migration(vm, hypervisor,
dst_hypervisor, model)
break
if not self.is_overloaded(hypervisor, model, cc):
break
def consolidation_phase(self, model, cc):
"""Perform consolidation phase.
This considers provided resource capacity coefficients.
Consolidation phase performing first-fit based bin packing.
First, hypervisors with the lowest cpu utilization are consolidated
by moving their load to hypervisors with the highest cpu utilization
which can accomodate the load. In this phase the most cpu utilizied
VMs are prioritizied as their load is more difficult to accomodate
in the system than less cpu utilizied VMs which can be later used
to fill smaller CPU capacity gaps.
:param model: model_root object
:param cc: dictionary containing resource capacity coefficients
"""
sorted_hypervisors = sorted(
model.get_all_hypervisors().values(),
key=lambda x: self.get_hypervisor_utilization(x, model)['cpu'])
asc = 0
for hypervisor in sorted_hypervisors:
vms = sorted(model.get_mapping().get_node_vms(hypervisor),
key=lambda x: self.get_vm_utilization(x,
model)['cpu'])
for vm in reversed(vms):
dsc = len(sorted_hypervisors) - 1
for dst_hypervisor in reversed(sorted_hypervisors):
if asc >= dsc:
break
if self.vm_fits(vm, dst_hypervisor, model, cc):
self.add_migration(vm, hypervisor,
dst_hypervisor, model)
break
dsc -= 1
asc += 1
def execute(self, original_model):
"""Execute strategy.
This strategy produces a solution resulting in more
efficient utilization of cluster resources using following
four phases:
* Offload phase - handling over-utilized resources
* Consolidation phase - handling under-utilized resources
* Solution optimization - reducing number of migrations
* Deactivation of unused hypervisors
:param original_model: root_model object
"""
LOG.info(_LI('Executing Smart Strategy'))
model = self.get_prediction_model(original_model)
rcu = self.get_relative_cluster_utilization(model)
self.ceilometer_vm_data_cache = dict()
cc = {'cpu': 1.0, 'ram': 1.0, 'disk': 1.0}
# Offloading phase
self.offload_phase(model, cc)
# Consolidation phase
self.consolidation_phase(model, cc)
# Optimize solution
self.optimize_solution(model)
# Deactivate unused hypervisors
self.deactivate_unused_hypervisors(model)
rcu_after = self.get_relative_cluster_utilization(model)
info = {
'number_of_migrations': self.number_of_migrations,
'number_of_released_hypervisors':
self.number_of_released_hypervisors,
'relative_cluster_utilization_before': str(rcu),
'relative_cluster_utilization_after': str(rcu_after)
}
LOG.debug(info)
self.solution.model = model
self.solution.efficacy = rcu_after['cpu']
return self.solution

View File

@@ -17,21 +17,56 @@
from __future__ import unicode_literals
import importlib
import inspect
from docutils import nodes
from docutils.parsers import rst
from docutils import statemachine as sm
from docutils import statemachine
from stevedore import extension
from watcher.version import version_info
import textwrap
class BaseWatcherDirective(rst.Directive):
def __init__(self, name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
super(BaseWatcherDirective, self).__init__(
name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine)
self.result = statemachine.ViewList()
def run(self):
raise NotImplementedError('Must override run() is subclass.')
def add_line(self, line, *lineno):
"""Append one line of generated reST to the output."""
self.result.append(line, rst.directives.unchanged, *lineno)
def add_textblock(self, textblock):
for line in textblock.splitlines():
self.add_line(line)
def add_object_docstring(self, obj):
obj_raw_docstring = obj.__doc__ or ""
# Maybe it's within the __init__
if not obj_raw_docstring and hasattr(obj, "__init__"):
if obj.__init__.__doc__:
obj_raw_docstring = obj.__init__.__doc__
if not obj_raw_docstring:
# Raise a warning to make the tests fail wit doc8
raise self.error("No docstring available for this plugin!")
obj_docstring = inspect.cleandoc(obj_raw_docstring)
self.add_textblock(obj_docstring)
class WatcherTerm(rst.Directive):
class WatcherTerm(BaseWatcherDirective):
"""Directive to import an RST formatted docstring into the Watcher glossary
How to use it
-------------
**How to use it**
# inside your .py file
class DocumentedObject(object):
@@ -47,17 +82,7 @@ class WatcherTerm(rst.Directive):
# You need to put an import path as an argument for this directive to work
required_arguments = 1
def add_textblock(self, textblock, *lineno):
for line in textblock.splitlines():
self.add_line(line)
def add_line(self, line, *lineno):
"""Append one line of generated reST to the output."""
self.result.append(line, rst.directives.unchanged, *lineno)
def run(self):
self.result = sm.ViewList()
cls_path = self.arguments[0]
try:
@@ -65,20 +90,82 @@ class WatcherTerm(rst.Directive):
except Exception as exc:
raise self.error(exc)
self.add_class_docstring(cls)
self.add_object_docstring(cls)
node = nodes.paragraph()
node.document = self.state.document
self.state.nested_parse(self.result, 0, node)
return node.children
def add_class_docstring(self, cls):
# Added 4 spaces to align the first line with the rest of the text
# to be able to dedent it correctly
cls_docstring = textwrap.dedent("%s%s" % (" " * 4, cls.__doc__))
self.add_textblock(cls_docstring)
class DriversDoc(BaseWatcherDirective):
"""Directive to import an RST formatted docstring into the Watcher doc
This directive imports the RST formatted docstring of every driver declared
within an entry point namespace provided as argument
**How to use it**
# inside your .py file
class DocumentedClassReferencedInEntrypoint(object):
'''My *.rst* docstring'''
def foo(self):
'''Foo docstring'''
# Inside your .rst file
.. drivers-doc:: entrypoint_namespace
:append_methods_doc: foo
This directive will then import the docstring and then interprete it.
Note that no section/sub-section can be imported via this directive as it
is a Sphinx restriction.
"""
# You need to put an import path as an argument for this directive to work
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
has_content = False
option_spec = dict(
# CSV formatted list of method names whose return values will be zipped
# together in the given order
append_methods_doc=lambda opts: [
opt.strip() for opt in opts.split(",") if opt.strip()],
# By default, we always start by adding the driver object docstring
exclude_driver_docstring=rst.directives.flag,
)
def run(self):
ext_manager = extension.ExtensionManager(namespace=self.arguments[0])
extensions = ext_manager.extensions
# Aggregates drivers based on their module name (i.e import path)
classes = [(ext.name, ext.plugin) for ext in extensions]
for name, cls in classes:
self.add_line(".. rubric:: %s" % name)
self.add_line("")
if "exclude_driver_docstring" not in self.options:
self.add_object_docstring(cls)
self.add_line("")
for method_name in self.options.get("append_methods_doc", []):
if hasattr(cls, method_name):
method = getattr(cls, method_name)
method_result = inspect.cleandoc(method)
self.add_textblock(method_result())
self.add_line("")
node = nodes.paragraph()
node.document = self.state.document
self.state.nested_parse(self.result, 0, node)
return node.children
def setup(app):
app.add_directive('drivers-doc', DriversDoc)
app.add_directive('watcher-term', WatcherTerm)
return {'version': version_info.version_string()}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: python-watcher 0.21.1.dev32\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-01-26 11:26+0100\n"
"POT-Creation-Date: 2016-02-09 09:07+0100\n"
"PO-Revision-Date: 2015-12-11 15:42+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
@@ -24,7 +24,7 @@ msgstr ""
msgid "Invalid state: %(state)s"
msgstr "État invalide : %(state)s"
#: watcher/api/controllers/v1/action_plan.py:420
#: watcher/api/controllers/v1/action_plan.py:422
#, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr "Transition d'état non autorisée : (%(initial_state)s -> %(new_state)s)"
@@ -85,21 +85,30 @@ msgstr ""
msgid "Error parsing HTTP response: %s"
msgstr ""
#: watcher/applier/actions/change_nova_service_state.py:58
#: watcher/applier/actions/change_nova_service_state.py:69
msgid "The target state is not defined"
msgstr ""
#: watcher/applier/workflow_engine/default.py:126
#: watcher/applier/actions/migration.py:43
msgid "The parameter resource_id is invalid."
msgstr "Le paramètre resource_id est invalide"
#: watcher/applier/actions/migration.py:86
#, python-format
msgid "Migration of type %(migration_type)s is not supported."
msgstr ""
#: watcher/applier/workflow_engine/default.py:128
#, python-format
msgid "The WorkFlow Engine has failed to execute the action %s"
msgstr "Le moteur de workflow a echoué lors de l'éxécution de l'action %s"
#: watcher/applier/workflow_engine/default.py:144
#: watcher/applier/workflow_engine/default.py:146
#, python-format
msgid "Revert action %s"
msgstr "Annulation de l'action %s"
#: watcher/applier/workflow_engine/default.py:150
#: watcher/applier/workflow_engine/default.py:152
msgid "Oops! We need disaster recover plan"
msgstr "Oops! Nous avons besoin d'un plan de reprise d'activité"
@@ -119,184 +128,210 @@ msgstr "Sert sur 0.0.0.0:%(port)s, accessible à http://127.0.0.1:%(port)s"
msgid "serving on http://%(host)s:%(port)s"
msgstr "Sert sur http://%(host)s:%(port)s"
#: watcher/common/exception.py:51
#: watcher/common/clients.py:29
msgid "Version of Nova API to use in novaclient."
msgstr ""
#: watcher/common/clients.py:34
msgid "Version of Glance API to use in glanceclient."
msgstr ""
#: watcher/common/clients.py:39
msgid "Version of Cinder API to use in cinderclient."
msgstr ""
#: watcher/common/clients.py:44
msgid "Version of Ceilometer API to use in ceilometerclient."
msgstr ""
#: watcher/common/clients.py:50
msgid "Version of Neutron API to use in neutronclient."
msgstr ""
#: watcher/common/exception.py:59
#, python-format
msgid "Unexpected keystone client error occurred: %s"
msgstr ""
#: watcher/common/exception.py:72
msgid "An unknown exception occurred"
msgstr ""
#: watcher/common/exception.py:71
#: watcher/common/exception.py:92
msgid "Exception in string format operation"
msgstr ""
#: watcher/common/exception.py:101
#: watcher/common/exception.py:122
msgid "Not authorized"
msgstr ""
#: watcher/common/exception.py:106
#: watcher/common/exception.py:127
msgid "Operation not permitted"
msgstr ""
#: watcher/common/exception.py:110
#: watcher/common/exception.py:131
msgid "Unacceptable parameters"
msgstr ""
#: watcher/common/exception.py:115
#: watcher/common/exception.py:136
#, python-format
msgid "The %(name)s %(id)s could not be found"
msgstr ""
#: watcher/common/exception.py:119
#: watcher/common/exception.py:140
#, fuzzy
msgid "Conflict"
msgstr "Conflit"
#: watcher/common/exception.py:124
#: watcher/common/exception.py:145
#, python-format
msgid "The %(name)s resource %(id)s could not be found"
msgstr "La ressource %(name)s / %(id)s est introuvable"
#: watcher/common/exception.py:129
#: watcher/common/exception.py:150
#, python-format
msgid "Expected an uuid or int but received %(identity)s"
msgstr ""
#: watcher/common/exception.py:133
#: watcher/common/exception.py:154
#, python-format
msgid "Goal %(goal)s is not defined in Watcher configuration file"
msgstr ""
#: watcher/common/exception.py:139
#, python-format
msgid "%(err)s"
msgstr "%(err)s"
#: watcher/common/exception.py:143
#: watcher/common/exception.py:158
#, python-format
msgid "Expected a uuid but received %(uuid)s"
msgstr ""
#: watcher/common/exception.py:147
#: watcher/common/exception.py:162
#, python-format
msgid "Expected a logical name but received %(name)s"
msgstr ""
#: watcher/common/exception.py:151
#: watcher/common/exception.py:166
#, python-format
msgid "Expected a logical name or uuid but received %(name)s"
msgstr ""
#: watcher/common/exception.py:155
#: watcher/common/exception.py:170
#, python-format
msgid "AuditTemplate %(audit_template)s could not be found"
msgstr ""
#: watcher/common/exception.py:159
#: watcher/common/exception.py:174
#, python-format
msgid "An audit_template with UUID %(uuid)s or name %(name)s already exists"
msgstr ""
#: watcher/common/exception.py:164
#: watcher/common/exception.py:179
#, python-format
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
msgstr ""
#: watcher/common/exception.py:169
#: watcher/common/exception.py:184
#, python-format
msgid "Audit %(audit)s could not be found"
msgstr ""
#: watcher/common/exception.py:173
#: watcher/common/exception.py:188
#, python-format
msgid "An audit with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:177
#: watcher/common/exception.py:192
#, python-format
msgid "Audit %(audit)s is referenced by one or multiple action plans"
msgstr ""
#: watcher/common/exception.py:182
#: watcher/common/exception.py:197
#, python-format
msgid "ActionPlan %(action_plan)s could not be found"
msgstr ""
#: watcher/common/exception.py:186
#: watcher/common/exception.py:201
#, python-format
msgid "An action plan with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:190
#: watcher/common/exception.py:205
#, python-format
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
msgstr ""
#: watcher/common/exception.py:195
#: watcher/common/exception.py:210
#, python-format
msgid "Action %(action)s could not be found"
msgstr ""
#: watcher/common/exception.py:199
#: watcher/common/exception.py:214
#, python-format
msgid "An action with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:203
#: watcher/common/exception.py:218
#, python-format
msgid "Action plan %(action_plan)s is referenced by one or multiple goals"
msgstr ""
#: watcher/common/exception.py:208
#: watcher/common/exception.py:223
msgid "Filtering actions on both audit and action-plan is prohibited"
msgstr ""
#: watcher/common/exception.py:217
#: watcher/common/exception.py:232
#, python-format
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
msgstr ""
#: watcher/common/exception.py:224
#: watcher/common/exception.py:239
msgid "Illegal argument"
msgstr ""
#: watcher/common/exception.py:228
#: watcher/common/exception.py:243
msgid "No such metric"
msgstr ""
#: watcher/common/exception.py:232
#: watcher/common/exception.py:247
msgid "No rows were returned"
msgstr ""
#: watcher/common/exception.py:236
#: watcher/common/exception.py:251
#, python-format
msgid "%(client)s connection failed. Reason: %(reason)s"
msgstr ""
#: watcher/common/exception.py:255
msgid "'Keystone API endpoint is missing''"
msgstr ""
#: watcher/common/exception.py:240
#: watcher/common/exception.py:259
msgid "The list of hypervisor(s) in the cluster is empty"
msgstr ""
#: watcher/common/exception.py:244
#: watcher/common/exception.py:263
msgid "The metrics resource collector is not defined"
msgstr ""
#: watcher/common/exception.py:248
#: watcher/common/exception.py:267
msgid "the cluster state is not defined"
msgstr ""
#: watcher/common/exception.py:254
#: watcher/common/exception.py:273
#, python-format
msgid "The instance '%(name)s' is not found"
msgstr "L'instance '%(name)s' n'a pas été trouvée"
#: watcher/common/exception.py:258
#: watcher/common/exception.py:277
msgid "The hypervisor is not found"
msgstr ""
#: watcher/common/exception.py:262
#: watcher/common/exception.py:281
#, fuzzy, python-format
msgid "Error loading plugin '%(name)s'"
msgstr "Erreur lors du chargement du module '%(name)s'"
#: watcher/common/keystone.py:59
msgid "No Keystone service catalog loaded"
#: watcher/common/exception.py:285
#, fuzzy, python-format
msgid "The identifier '%(name)s' is a reserved word"
msgstr ""
#: watcher/common/service.py:83
@@ -347,18 +382,22 @@ msgid ""
"template uuid instead"
msgstr ""
#: watcher/db/sqlalchemy/api.py:277
msgid "Cannot overwrite UUID for an existing AuditTemplate."
#: watcher/db/sqlalchemy/api.py:278
msgid "Cannot overwrite UUID for an existing Audit Template."
msgstr ""
#: watcher/db/sqlalchemy/api.py:386 watcher/db/sqlalchemy/api.py:586
#: watcher/db/sqlalchemy/api.py:388
msgid "Cannot overwrite UUID for an existing Audit."
msgstr ""
#: watcher/db/sqlalchemy/api.py:477
#: watcher/db/sqlalchemy/api.py:480
msgid "Cannot overwrite UUID for an existing Action."
msgstr ""
#: watcher/db/sqlalchemy/api.py:590
msgid "Cannot overwrite UUID for an existing Action Plan."
msgstr ""
#: watcher/db/sqlalchemy/migration.py:73
msgid ""
"Watcher database schema is already under version control; use upgrade() "
@@ -370,44 +409,44 @@ msgstr ""
msgid "'obj' argument type is not valid"
msgstr ""
#: watcher/decision_engine/planner/default.py:76
#: watcher/decision_engine/planner/default.py:72
msgid "The action plan is empty"
msgstr ""
#: watcher/decision_engine/strategy/selection/default.py:59
#: watcher/decision_engine/strategy/selection/default.py:60
#, python-format
msgid "Incorrect mapping: could not find associated strategy for '%s'"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:267
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:314
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:269
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:316
#, python-format
msgid "No values returned by %(resource_id)s for %(metric_name)s"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:424
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:426
msgid "Initializing Sercon Consolidation"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:468
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:470
msgid "The workloads of the compute nodes of the cluster is zero"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:125
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:127
#, python-format
msgid "%s: no outlet temp data"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:149
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:151
#, python-format
msgid "VM not active, skipped: %s"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:206
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:208
msgid "No hosts under outlet temp threshold found"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:229
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:231
msgid "No proper target host could be found"
msgstr ""
@@ -582,9 +621,20 @@ msgstr ""
#~ msgid "Description cannot be empty"
#~ msgstr ""
#~ msgid ""
#~ "Failed to remove trailing character. "
#~ "Returning original object. Supplied object "
#~ "is not a string: %s,"
#~ msgid "The hypervisor state is invalid."
#~ msgstr "L'état de l'hyperviseur est invalide"
#~ msgid "%(err)s"
#~ msgstr "%(err)s"
#~ msgid "No Keystone service catalog loaded"
#~ msgstr ""
#~ msgid "Cannot overwrite UUID for an existing AuditTemplate."
#~ msgstr ""
#~ msgid ""
#~ "This identifier is reserved word and "
#~ "cannot be used as variables '%(name)s'"
#~ msgstr ""

View File

@@ -7,23 +7,35 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: python-watcher 0.23.2.dev1\n"
"Project-Id-Version: python-watcher 0.25.1.dev3\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-01-26 11:26+0100\n"
"POT-Creation-Date: 2016-03-30 10:10+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.1.1\n"
"Generated-By: Babel 2.2.0\n"
#: watcher/api/controllers/v1/action.py:364
msgid "Cannot create an action directly"
msgstr ""
#: watcher/api/controllers/v1/action.py:388
msgid "Cannot modify an action directly"
msgstr ""
#: watcher/api/controllers/v1/action.py:424
msgid "Cannot delete an action directly"
msgstr ""
#: watcher/api/controllers/v1/action_plan.py:102
#, python-format
msgid "Invalid state: %(state)s"
msgstr ""
#: watcher/api/controllers/v1/action_plan.py:420
#: watcher/api/controllers/v1/action_plan.py:422
#, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr ""
@@ -65,7 +77,12 @@ msgstr ""
msgid "Invalid sort direction: %s. Acceptable values are 'asc' or 'desc'"
msgstr ""
#: watcher/api/controllers/v1/utils.py:57
#: watcher/api/controllers/v1/utils.py:58
#, python-format
msgid "Invalid filter: %s"
msgstr ""
#: watcher/api/controllers/v1/utils.py:65
#, python-format
msgid "Adding a new attribute (%s) to the root of the resource is not allowed"
msgstr ""
@@ -84,25 +101,37 @@ msgstr ""
msgid "Error parsing HTTP response: %s"
msgstr ""
#: watcher/applier/actions/change_nova_service_state.py:58
#: watcher/applier/actions/change_nova_service_state.py:90
msgid "The target state is not defined"
msgstr ""
#: watcher/applier/actions/migration.py:60
#: watcher/applier/actions/migration.py:71
msgid "The parameter resource_id is invalid."
msgstr ""
#: watcher/applier/actions/migration.py:124
#, python-format
msgid ""
"Unexpected error occured. Migration failed forinstance %s. Leaving "
"instance on previous host."
msgstr ""
#: watcher/applier/actions/migration.py:140
#, python-format
msgid "Migration of type %(migration_type)s is not supported."
msgstr ""
#: watcher/applier/workflow_engine/default.py:126
#: watcher/applier/workflow_engine/default.py:129
#, python-format
msgid "The WorkFlow Engine has failed to execute the action %s"
msgstr ""
#: watcher/applier/workflow_engine/default.py:144
#: watcher/applier/workflow_engine/default.py:147
#, python-format
msgid "Revert action %s"
msgstr ""
#: watcher/applier/workflow_engine/default.py:150
#: watcher/applier/workflow_engine/default.py:153
msgid "Oops! We need disaster recover plan"
msgstr ""
@@ -122,178 +151,223 @@ msgstr ""
msgid "serving on http://%(host)s:%(port)s"
msgstr ""
#: watcher/common/exception.py:51
#: watcher/common/clients.py:29
msgid "Version of Nova API to use in novaclient."
msgstr ""
#: watcher/common/clients.py:34
msgid "Version of Glance API to use in glanceclient."
msgstr ""
#: watcher/common/clients.py:39
msgid "Version of Cinder API to use in cinderclient."
msgstr ""
#: watcher/common/clients.py:44
msgid "Version of Ceilometer API to use in ceilometerclient."
msgstr ""
#: watcher/common/clients.py:50
msgid "Version of Neutron API to use in neutronclient."
msgstr ""
#: watcher/common/exception.py:59
#, python-format
msgid "Unexpected keystone client error occurred: %s"
msgstr ""
#: watcher/common/exception.py:72
msgid "An unknown exception occurred"
msgstr ""
#: watcher/common/exception.py:71
#: watcher/common/exception.py:92
msgid "Exception in string format operation"
msgstr ""
#: watcher/common/exception.py:101
#: watcher/common/exception.py:122
msgid "Not authorized"
msgstr ""
#: watcher/common/exception.py:106
#: watcher/common/exception.py:127
msgid "Operation not permitted"
msgstr ""
#: watcher/common/exception.py:110
#: watcher/common/exception.py:131
msgid "Unacceptable parameters"
msgstr ""
#: watcher/common/exception.py:115
#: watcher/common/exception.py:136
#, python-format
msgid "The %(name)s %(id)s could not be found"
msgstr ""
#: watcher/common/exception.py:119
#: watcher/common/exception.py:140
msgid "Conflict"
msgstr ""
#: watcher/common/exception.py:124
#: watcher/common/exception.py:145
#, python-format
msgid "The %(name)s resource %(id)s could not be found"
msgstr ""
#: watcher/common/exception.py:129
#: watcher/common/exception.py:150
#, python-format
msgid "Expected an uuid or int but received %(identity)s"
msgstr ""
#: watcher/common/exception.py:133
#: watcher/common/exception.py:154
#, python-format
msgid "Goal %(goal)s is not defined in Watcher configuration file"
msgstr ""
#: watcher/common/exception.py:143
#: watcher/common/exception.py:158
#, python-format
msgid "Expected a uuid but received %(uuid)s"
msgstr ""
#: watcher/common/exception.py:147
#: watcher/common/exception.py:162
#, python-format
msgid "Expected a logical name but received %(name)s"
msgstr ""
#: watcher/common/exception.py:151
#: watcher/common/exception.py:166
#, python-format
msgid "Expected a logical name or uuid but received %(name)s"
msgstr ""
#: watcher/common/exception.py:155
#: watcher/common/exception.py:170
#, python-format
msgid "AuditTemplate %(audit_template)s could not be found"
msgstr ""
#: watcher/common/exception.py:159
#: watcher/common/exception.py:174
#, python-format
msgid "An audit_template with UUID %(uuid)s or name %(name)s already exists"
msgstr ""
#: watcher/common/exception.py:164
#: watcher/common/exception.py:179
#, python-format
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
msgstr ""
#: watcher/common/exception.py:169
#: watcher/common/exception.py:184
#, python-format
msgid "Audit %(audit)s could not be found"
msgstr ""
#: watcher/common/exception.py:173
#: watcher/common/exception.py:188
#, python-format
msgid "An audit with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:177
#: watcher/common/exception.py:192
#, python-format
msgid "Audit %(audit)s is referenced by one or multiple action plans"
msgstr ""
#: watcher/common/exception.py:182
#: watcher/common/exception.py:197
#, python-format
msgid "ActionPlan %(action_plan)s could not be found"
msgstr ""
#: watcher/common/exception.py:186
#: watcher/common/exception.py:201
#, python-format
msgid "An action plan with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:190
#: watcher/common/exception.py:205
#, python-format
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
msgstr ""
#: watcher/common/exception.py:195
#: watcher/common/exception.py:210
#, python-format
msgid "Action %(action)s could not be found"
msgstr ""
#: watcher/common/exception.py:199
#: watcher/common/exception.py:214
#, python-format
msgid "An action with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:203
#: watcher/common/exception.py:218
#, python-format
msgid "Action plan %(action_plan)s is referenced by one or multiple goals"
msgstr ""
#: watcher/common/exception.py:208
#: watcher/common/exception.py:223
msgid "Filtering actions on both audit and action-plan is prohibited"
msgstr ""
#: watcher/common/exception.py:217
#: watcher/common/exception.py:232
#, python-format
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
msgstr ""
#: watcher/common/exception.py:224
#: watcher/common/exception.py:238
#, python-format
msgid "Workflow execution error: %(error)s"
msgstr ""
#: watcher/common/exception.py:242
msgid "Illegal argument"
msgstr ""
#: watcher/common/exception.py:228
#: watcher/common/exception.py:246
msgid "No such metric"
msgstr ""
#: watcher/common/exception.py:232
#: watcher/common/exception.py:250
msgid "No rows were returned"
msgstr ""
#: watcher/common/exception.py:236
msgid "'Keystone API endpoint is missing''"
msgstr ""
#: watcher/common/exception.py:240
msgid "The list of hypervisor(s) in the cluster is empty"
msgstr ""
#: watcher/common/exception.py:244
msgid "The metrics resource collector is not defined"
msgstr ""
#: watcher/common/exception.py:248
msgid "the cluster state is not defined"
msgstr ""
#: watcher/common/exception.py:254
#, python-format
msgid "The instance '%(name)s' is not found"
msgid "%(client)s connection failed. Reason: %(reason)s"
msgstr ""
#: watcher/common/exception.py:258
msgid "The hypervisor is not found"
msgid "'Keystone API endpoint is missing''"
msgstr ""
#: watcher/common/exception.py:262
msgid "The list of hypervisor(s) in the cluster is empty"
msgstr ""
#: watcher/common/exception.py:266
msgid "The metrics resource collector is not defined"
msgstr ""
#: watcher/common/exception.py:270
msgid "the cluster state is not defined"
msgstr ""
#: watcher/common/exception.py:276
#, python-format
msgid "The instance '%(name)s' is not found"
msgstr ""
#: watcher/common/exception.py:280
msgid "The hypervisor is not found"
msgstr ""
#: watcher/common/exception.py:284
#, python-format
msgid "Error loading plugin '%(name)s'"
msgstr ""
#: watcher/common/keystone.py:59
msgid "No Keystone service catalog loaded"
#: watcher/common/exception.py:288
#, python-format
msgid "The identifier '%(name)s' is a reserved word"
msgstr ""
#: watcher/common/exception.py:292
#, python-format
msgid "The %(name)s resource %(id)s is not soft deleted"
msgstr ""
#: watcher/common/exception.py:296
msgid "Limit should be positive"
msgstr ""
#: watcher/common/service.py:83
@@ -338,24 +412,105 @@ msgstr ""
msgid "Messaging configuration error"
msgstr ""
#: watcher/db/sqlalchemy/api.py:256
#: watcher/db/purge.py:50
msgid "Audit Templates"
msgstr ""
#: watcher/db/purge.py:51
msgid "Audits"
msgstr ""
#: watcher/db/purge.py:52
msgid "Action Plans"
msgstr ""
#: watcher/db/purge.py:53
msgid "Actions"
msgstr ""
#: watcher/db/purge.py:100
msgid "Total"
msgstr ""
#: watcher/db/purge.py:158
msgid "Audit Template"
msgstr ""
#: watcher/db/purge.py:206
#, python-format
msgid ""
"Orphans found:\n"
"%s"
msgstr ""
#: watcher/db/purge.py:265
#, python-format
msgid "There are %(count)d objects set for deletion. Continue? [y/N]"
msgstr ""
#: watcher/db/purge.py:272
#, python-format
msgid ""
"The number of objects (%(num)s) to delete from the database exceeds the "
"maximum number of objects (%(max_number)s) specified."
msgstr ""
#: watcher/db/purge.py:277
msgid "Do you want to delete objects up to the specified maximum number? [y/N]"
msgstr ""
#: watcher/db/purge.py:340
msgid "Deleting..."
msgstr ""
#: watcher/db/purge.py:346
msgid "Starting purge command"
msgstr ""
#: watcher/db/purge.py:356
msgid " (orphans excluded)"
msgstr ""
#: watcher/db/purge.py:357
msgid " (may include orphans)"
msgstr ""
#: watcher/db/purge.py:360 watcher/db/purge.py:361
#, python-format
msgid "Purge results summary%s:"
msgstr ""
#: watcher/db/purge.py:364
#, python-format
msgid "Here below is a table containing the objects that can be purged%s:"
msgstr ""
#: watcher/db/purge.py:369
msgid "Purge process completed"
msgstr ""
#: watcher/db/sqlalchemy/api.py:362
msgid ""
"Multiple audit templates exist with the same name. Please use the audit "
"template uuid instead"
msgstr ""
#: watcher/db/sqlalchemy/api.py:277
msgid "Cannot overwrite UUID for an existing AuditTemplate."
#: watcher/db/sqlalchemy/api.py:384
msgid "Cannot overwrite UUID for an existing Audit Template."
msgstr ""
#: watcher/db/sqlalchemy/api.py:386 watcher/db/sqlalchemy/api.py:586
#: watcher/db/sqlalchemy/api.py:495
msgid "Cannot overwrite UUID for an existing Audit."
msgstr ""
#: watcher/db/sqlalchemy/api.py:477
#: watcher/db/sqlalchemy/api.py:588
msgid "Cannot overwrite UUID for an existing Action."
msgstr ""
#: watcher/db/sqlalchemy/api.py:699
msgid "Cannot overwrite UUID for an existing Action Plan."
msgstr ""
#: watcher/db/sqlalchemy/migration.py:73
msgid ""
"Watcher database schema is already under version control; use upgrade() "
@@ -367,47 +522,66 @@ msgstr ""
msgid "'obj' argument type is not valid"
msgstr ""
#: watcher/decision_engine/planner/default.py:76
#: watcher/decision_engine/planner/default.py:79
msgid "The action plan is empty"
msgstr ""
#: watcher/decision_engine/strategy/selection/default.py:59
#: watcher/decision_engine/strategy/selection/default.py:60
#, python-format
msgid "Incorrect mapping: could not find associated strategy for '%s'"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:267
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:314
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:288
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:335
#, python-format
msgid "No values returned by %(resource_id)s for %(metric_name)s"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:424
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:448
msgid "Initializing Sercon Consolidation"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:468
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:492
msgid "The workloads of the compute nodes of the cluster is zero"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:125
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:147
#, python-format
msgid "%s: no outlet temp data"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:149
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:172
#, python-format
msgid "VM not active, skipped: %s"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:206
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:230
msgid "No hosts under outlet temp threshold found"
msgstr ""
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:229
#: watcher/decision_engine/strategy/strategies/outlet_temp_control.py:253
msgid "No proper target host could be found"
msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:104
#, python-format
msgid "Unexpexted resource state type, state=%(state)s, state_type=%(st)s."
msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:156
#, python-format
msgid "Cannot live migrate: vm_uuid=%(vm_uuid)s, state=%(vm_state)s."
msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:240
#, python-format
msgid "No values returned by %(resource_id)s for memory.usage or disk.root.size"
msgstr ""
#: watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py:489
msgid "Executing Smart Strategy"
msgstr ""
#: watcher/objects/base.py:70
#, python-format
msgid "Error setting %(attr)s"

View File

@@ -22,13 +22,13 @@ from oslo_config import cfg
from oslo_log import log
from watcher.common import ceilometer_helper
from watcher.metrics_engine.cluster_history.api import BaseClusterHistory
from watcher.metrics_engine.cluster_history import base
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class CeilometerClusterHistory(BaseClusterHistory):
class CeilometerClusterHistory(base.BaseClusterHistory):
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
super(CeilometerClusterHistory, self).__init__()

View File

@@ -21,8 +21,8 @@ from oslo_config import cfg
from oslo_log import log
from watcher.common import nova_helper
from watcher.metrics_engine.cluster_model_collector.nova import \
NovaClusterModelCollector
from watcher.metrics_engine.cluster_model_collector import nova as cnova
LOG = log.getLogger(__name__)
CONF = cfg.CONF
@@ -32,4 +32,4 @@ class CollectorManager(object):
def get_cluster_model_collector(self, osc=None):
""":param osc: an OpenStackClients instance"""
nova = nova_helper.NovaHelper(osc=osc)
return NovaClusterModelCollector(nova)
return cnova.NovaClusterModelCollector(nova)

View File

@@ -17,47 +17,47 @@
# limitations under the License.
#
from oslo_config import cfg
from oslo_log import log
from watcher.decision_engine.model.hypervisor import Hypervisor
from watcher.decision_engine.model.model_root import ModelRoot
from watcher.decision_engine.model.resource import Resource
from watcher.decision_engine.model.resource import ResourceType
from watcher.decision_engine.model.vm import VM
from watcher.metrics_engine.cluster_model_collector.api import \
BaseClusterModelCollector
from watcher.decision_engine.model import hypervisor as obj_hypervisor
from watcher.decision_engine.model import model_root
from watcher.decision_engine.model import resource
from watcher.decision_engine.model import vm as obj_vm
from watcher.metrics_engine.cluster_model_collector import base
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class NovaClusterModelCollector(BaseClusterModelCollector):
class NovaClusterModelCollector(base.BaseClusterModelCollector):
def __init__(self, wrapper):
super(NovaClusterModelCollector, self).__init__()
self.wrapper = wrapper
def get_latest_cluster_data_model(self):
LOG.debug("Getting latest cluster data model")
cluster = ModelRoot()
mem = Resource(ResourceType.memory)
num_cores = Resource(ResourceType.cpu_cores)
disk = Resource(ResourceType.disk)
cluster = model_root.ModelRoot()
mem = resource.Resource(resource.ResourceType.memory)
num_cores = resource.Resource(resource.ResourceType.cpu_cores)
disk = resource.Resource(resource.ResourceType.disk)
disk_capacity = resource.Resource(resource.ResourceType.disk_capacity)
cluster.create_resource(mem)
cluster.create_resource(num_cores)
cluster.create_resource(disk)
cluster.create_resource(disk_capacity)
flavor_cache = {}
hypervisors = self.wrapper.get_hypervisors_list()
for h in hypervisors:
service = self.wrapper.nova.services.find(id=h.service['id'])
# create hypervisor in cluster_model_collector
hypervisor = Hypervisor()
hypervisor = obj_hypervisor.Hypervisor()
hypervisor.uuid = service.host
hypervisor.hostname = h.hypervisor_hostname
# set capacity
mem.set_capacity(hypervisor, h.memory_mb)
disk.set_capacity(hypervisor, h.free_disk_gb)
disk_capacity.set_capacity(hypervisor, h.local_gb)
num_cores.set_capacity(hypervisor, h.vcpus)
hypervisor.state = h.state
hypervisor.status = h.status
@@ -65,7 +65,7 @@ class NovaClusterModelCollector(BaseClusterModelCollector):
vms = self.wrapper.get_vms_by_hypervisor(str(service.host))
for v in vms:
# create VM in cluster_model_collector
vm = VM()
vm = obj_vm.VM()
vm.uuid = v.id
# nova/nova/compute/vm_states.py
vm.state = getattr(v, 'OS-EXT-STS:vm_state')

View File

@@ -42,7 +42,6 @@ class Action(base.WatcherObject):
'uuid': obj_utils.str_or_none,
'action_plan_id': obj_utils.int_or_none,
'action_type': obj_utils.str_or_none,
'applies_to': obj_utils.str_or_none,
'input_parameters': obj_utils.dict_or_none,
'state': obj_utils.str_or_none,
# todo(jed) remove parameter alarm

View File

@@ -71,13 +71,14 @@ state may be one of the following:
from watcher.common import exception
from watcher.common import utils
from watcher.db import api as dbapi
from watcher.objects import action as action_objects
from watcher.objects import base
from watcher.objects import utils as obj_utils
class State(object):
RECOMMENDED = 'RECOMMENDED'
TRIGGERED = 'TRIGGERED'
PENDING = 'PENDING'
ONGOING = 'ONGOING'
FAILED = 'FAILED'
SUCCEEDED = 'SUCCEEDED'
@@ -251,6 +252,14 @@ class ActionPlan(base.WatcherObject):
A context should be set when instantiating the
object, e.g.: Audit(context)
"""
related_actions = action_objects.Action.list(
context=self._context,
filters={"action_plan_uuid": self.uuid})
# Cascade soft_delete of related actions
for related_action in related_actions:
related_action.soft_delete()
self.dbapi.soft_delete_action_plan(self.uuid)
self.state = "DELETED"
self.state = State.DELETED
self.save()

View File

@@ -47,7 +47,6 @@ contain a list of extra parameters related to the
provided as a list of key-value pairs.
"""
from oslo_config import cfg
from watcher.common import exception
from watcher.common import utils
from watcher.db import api as dbapi
@@ -164,7 +163,7 @@ class AuditTemplate(base.WatcherObject):
return audit_template
@classmethod
def list(cls, context, limit=None, marker=None,
def list(cls, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of :class:`AuditTemplate` objects.
@@ -174,6 +173,7 @@ class AuditTemplate(base.WatcherObject):
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: AuditTemplate(context)
:param filters: dict mapping the filter key to a value.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
@@ -183,6 +183,7 @@ class AuditTemplate(base.WatcherObject):
db_audit_templates = cls.dbapi.get_audit_template_list(
context,
filters=filters,
limit=limit,
marker=marker,
sort_key=sort_key,
@@ -202,9 +203,6 @@ class AuditTemplate(base.WatcherObject):
"""
values = self.obj_get_changes()
goal = values['goal']
if goal not in cfg.CONF.watcher_goals.goals.keys():
raise exception.InvalidGoal(goal=goal)
db_audit_template = self.dbapi.create_audit_template(values)
self._from_db_object(self, db_audit_template)

View File

@@ -1,43 +0,0 @@
# 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 os
import unittest
from oslo_config import cfg
import pecan
from pecan import testing
cfg.CONF.import_opt('enable_authentication', 'watcher.api.acl')
__all__ = ['FunctionalTest']
class FunctionalTest(unittest.TestCase):
"""Functional tests
Used for functional tests where you need to test your
literal application and its integration with the framework.
"""
def setUp(self):
cfg.CONF.set_override("enable_authentication", False,
enforce_type=True)
self.app = testing.load_test_app(os.path.join(
os.path.dirname(__file__),
'config.py'
))
def tearDown(self):
pecan.set_config({}, overwrite=True)

View File

@@ -44,7 +44,7 @@ class TestApiUtilsValidScenarios(base.TestCase):
cfg.CONF.set_override("max_limit", self.max_limit, group="api",
enforce_type=True)
actual_limit = v1_utils.validate_limit(self.limit)
self.assertEqual(actual_limit, self.expected)
self.assertEqual(self.expected, actual_limit)
class TestApiUtilsInvalidScenarios(base.TestCase):

View File

@@ -14,13 +14,11 @@ import datetime
import mock
from oslo_config import cfg
from oslo_utils import timeutils
from wsme import types as wtypes
from watcher.api.controllers.v1 import action as api_action
from watcher.common import utils
from watcher.db import api as db_api
from watcher import objects
from watcher.tests.api import base as api_base
from watcher.tests.api import utils as api_utils
from watcher.tests import base
@@ -285,33 +283,40 @@ class TestListAction(api_base.FunctionalTest):
id=3,
uuid=utils.generate_uuid(),
audit_id=1)
action_list = []
ap1_action_list = []
ap2_action_list = []
for id_ in range(0, 2):
action = obj_utils.create_test_action(
self.context, id=id_,
action_plan_id=2,
action_plan_id=action_plan1.id,
uuid=utils.generate_uuid())
action_list.append(action.uuid)
ap1_action_list.append(action)
for id_ in range(2, 4):
action = obj_utils.create_test_action(
self.context, id=id_,
action_plan_id=3,
action_plan_id=action_plan2.id,
uuid=utils.generate_uuid())
action_list.append(action.uuid)
ap2_action_list.append(action)
self.delete('/action_plans/%s' % action_plan1.uuid)
response = self.get_json('/actions')
self.assertEqual(len(action_list), len(response['actions']))
for id_ in range(0, 2):
action = response['actions'][id_]
self.assertEqual(None, action['action_plan_uuid'])
# We deleted the actions from the 1st action plan so we've got 2 left
self.assertEqual(len(ap2_action_list), len(response['actions']))
for id_ in range(2, 4):
action = response['actions'][id_]
self.assertEqual(action_plan2.uuid, action['action_plan_uuid'])
# We deleted them so that's normal
self.assertEqual([],
[act for act in response['actions']
if act['action_plan_uuid'] == action_plan1.uuid])
# Here are the 2 actions left
self.assertEqual(
set([act.as_dict()['uuid'] for act in ap2_action_list]),
set([act['uuid'] for act in response['actions']
if act['action_plan_uuid'] == action_plan2.uuid]))
def test_many_with_next_uuid(self):
action_list = []
@@ -435,7 +440,7 @@ class TestPatch(api_base.FunctionalTest):
return action
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok(self, mock_utcnow):
def test_patch_not_allowed(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
@@ -446,104 +451,12 @@ class TestPatch(api_base.FunctionalTest):
response = self.patch_json(
'/actions/%s' % self.action.uuid,
[{'path': '/state', 'value': new_state,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/actions/%s' % self.action.uuid)
self.assertEqual(new_state, response['state'])
return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at)
def test_replace_non_existent_action(self):
response = self.patch_json('/actions/%s' % utils.generate_uuid(),
[{'path': '/state', 'value': 'SUBMITTED',
'op': 'replace'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_add_ok(self):
new_state = 'SUCCEEDED'
response = self.patch_json(
'/actions/%s' % self.action.uuid,
[{'path': '/state', 'value': new_state, 'op': 'add'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_int)
response = self.get_json('/actions/%s' % self.action.uuid)
self.assertEqual(new_state, response['state'])
def test_add_non_existent_property(self):
response = self.patch_json(
'/actions/%s' % self.action.uuid,
[{'path': '/foo', 'value': 'bar', 'op': 'add'}],
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
self.assertEqual(403, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_ok(self):
response = self.get_json('/actions/%s' % self.action.uuid)
self.assertIsNotNone(response['state'])
response = self.patch_json('/actions/%s' % self.action.uuid,
[{'path': '/state', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/actions/%s' % self.action.uuid)
self.assertIsNone(response['state'])
def test_remove_uuid(self):
response = self.patch_json('/actions/%s' % self.action.uuid,
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_remove_non_existent_property(self):
response = self.patch_json(
'/actions/%s' % self.action.uuid,
[{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
# class TestDelete(api_base.FunctionalTest):
# def setUp(self):
# super(TestDelete, self).setUp()
# self.action = obj_utils.create_test_action(self.context, next=None)
# p = mock.patch.object(db_api.Connection, 'destroy_action')
# self.mock_action_delete = p.start()
# self.mock_action_delete.side_effect =
# self._simulate_rpc_action_delete
# self.addCleanup(p.stop)
# def _simulate_rpc_action_delete(self, action_uuid):
# action = objects.Action.get_by_uuid(self.context, action_uuid)
# action.destroy()
# def test_delete_action(self):
# self.delete('/actions/%s' % self.action.uuid)
# response = self.get_json('/actions/%s' % self.action.uuid,
# expect_errors=True)
# self.assertEqual(404, response.status_int)
# self.assertEqual('application/json', response.content_type)
# self.assertTrue(response.json['error_message'])
# def test_delete_action_not_found(self):
# uuid = utils.generate_uuid()
# response = self.delete('/actions/%s' % uuid, expect_errors=True)
# self.assertEqual(404, response.status_int)
# self.assertEqual('application/json', response.content_type)
# self.assertTrue(response.json['error_message'])
class TestDelete(api_base.FunctionalTest):
@@ -561,26 +474,11 @@ class TestDelete(api_base.FunctionalTest):
return action
@mock.patch('oslo_utils.timeutils.utcnow')
def test_delete_action(self, mock_utcnow):
def test_delete_action_not_allowed(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
self.delete('/actions/%s' % self.action.uuid)
response = self.get_json('/actions/%s' % self.action.uuid,
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
self.context.show_deleted = True
action = objects.Action.get_by_uuid(self.context, self.action.uuid)
return_deleted_at = timeutils.strtime(action['deleted_at'])
self.assertEqual(timeutils.strtime(test_time), return_deleted_at)
self.assertEqual(action['state'], 'DELETED')
def test_delete_action_not_found(self):
uuid = utils.generate_uuid()
response = self.delete('/actions/%s' % uuid, expect_errors=True)
self.assertEqual(404, response.status_int)
response = self.delete('/actions/%s' % self.action.uuid,
expect_errors=True)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])

View File

@@ -307,7 +307,7 @@ class TestDelete(api_base.FunctionalTest):
action_plan = objects.ActionPlan.get_by_uuid(self.context, audit_uuid)
action_plan.destroy()
def test_delete_action_plan(self):
def test_delete_action_plan_without_action(self):
self.delete('/action_plans/%s' % self.action_plan.uuid)
response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
expect_errors=True)
@@ -315,6 +315,30 @@ class TestDelete(api_base.FunctionalTest):
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_delete_action_plan_with_action(self):
action = obj_utils.create_test_action(
self.context, id=self.action_plan.first_action_id)
self.delete('/action_plans/%s' % self.action_plan.uuid)
ap_response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
expect_errors=True)
acts_response = self.get_json(
'/actions/?action_plan_uuid=%s' % self.action_plan.uuid)
act_response = self.get_json(
'/actions/%s' % action.uuid,
expect_errors=True)
# The action plan does not exist anymore
self.assertEqual(404, ap_response.status_int)
self.assertEqual('application/json', ap_response.content_type)
self.assertTrue(ap_response.json['error_message'])
# Nor does the action
self.assertEqual(0, len(acts_response['actions']))
self.assertEqual(404, act_response.status_int)
self.assertEqual('application/json', act_response.content_type)
self.assertTrue(act_response.json['error_message'])
def test_delete_action_plan_not_found(self):
uuid = utils.generate_uuid()
response = self.delete('/action_plans/%s' % uuid, expect_errors=True)
@@ -362,7 +386,7 @@ class TestPatch(api_base.FunctionalTest):
response = self.patch_json(
'/action_plans/%s' % utils.generate_uuid(),
[{'path': '/state',
'value': objects.action_plan.State.TRIGGERED,
'value': objects.action_plan.State.PENDING,
'op': 'replace'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
@@ -412,8 +436,8 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['error_message'])
@mock.patch.object(aapi.ApplierAPI, 'launch_action_plan')
def test_replace_state_triggered_ok(self, applier_mock):
new_state = objects.action_plan.State.TRIGGERED
def test_replace_state_pending_ok(self, applier_mock):
new_state = objects.action_plan.State.PENDING
response = self.get_json(
'/action_plans/%s' % self.action_plan.uuid)
self.assertNotEqual(new_state, response['state'])
@@ -429,12 +453,12 @@ class TestPatch(api_base.FunctionalTest):
ALLOWED_TRANSITIONS = [
{"original_state": objects.action_plan.State.RECOMMENDED,
"new_state": objects.action_plan.State.TRIGGERED},
"new_state": objects.action_plan.State.PENDING},
{"original_state": objects.action_plan.State.RECOMMENDED,
"new_state": objects.action_plan.State.CANCELLED},
{"original_state": objects.action_plan.State.ONGOING,
"new_state": objects.action_plan.State.CANCELLED},
{"original_state": objects.action_plan.State.TRIGGERED,
{"original_state": objects.action_plan.State.PENDING,
"new_state": objects.action_plan.State.CANCELLED},
]
@@ -455,7 +479,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest):
for original_state, new_state
in list(itertools.product(STATES, STATES))
# from DELETED to ...
# NOTE: Any state transition from DELETED (To RECOMMENDED, TRIGGERED,
# NOTE: Any state transition from DELETED (To RECOMMENDED, PENDING,
# ONGOING, CANCELLED, SUCCEEDED and FAILED) will cause a 404 Not Found
# because we cannot retrieve them with a GET (soft_deleted state).
# This is the reason why they are not listed here but they have a
@@ -469,7 +493,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest):
@mock.patch.object(
db_api.BaseConnection, 'update_action_plan',
mock.Mock(side_effect=lambda ap: ap.save() or ap))
def test_replace_state_triggered_denied(self):
def test_replace_state_pending_denied(self):
action_plan = obj_utils.create_action_plan_without_audit(
self.context, state=self.original_state)
@@ -493,7 +517,7 @@ class TestPatchStateDeletedNotFound(api_base.FunctionalTest):
@mock.patch.object(
db_api.BaseConnection, 'update_action_plan',
mock.Mock(side_effect=lambda ap: ap.save() or ap))
def test_replace_state_triggered_not_found(self):
def test_replace_state_pending_not_found(self):
action_plan = obj_utils.create_action_plan_without_audit(
self.context, state=objects.action_plan.State.DELETED)
@@ -521,7 +545,7 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
db_api.BaseConnection, 'update_action_plan',
mock.Mock(side_effect=lambda ap: ap.save() or ap))
@mock.patch.object(aapi.ApplierAPI, 'launch_action_plan', mock.Mock())
def test_replace_state_triggered_ok(self):
def test_replace_state_pending_ok(self):
action_plan = obj_utils.create_action_plan_without_audit(
self.context, state=self.original_state)

View File

@@ -207,6 +207,25 @@ class TestListAuditTemplate(api_base.FunctionalTest):
next_marker = response['audit_templates'][-1]['uuid']
self.assertIn(next_marker, response['next'])
def test_filter_by_goal(self):
cfg.CONF.set_override('goals', {"DUMMY": "DUMMY", "BASIC": "BASIC"},
group='watcher_goals', enforce_type=True)
for id_ in range(2):
obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_),
goal="DUMMY")
for id_ in range(2, 5):
obj_utils.create_test_audit_template(
self.context, id=id_, uuid=utils.generate_uuid(),
name='My Audit Template {0}'.format(id_),
goal="BASIC")
response = self.get_json('/audit_templates?goal=BASIC')
self.assertEqual(3, len(response['audit_templates']))
class TestPatch(api_base.FunctionalTest):
@@ -218,6 +237,8 @@ class TestPatch(api_base.FunctionalTest):
self.mock_audit_template_update = p.start()
self.mock_audit_template_update.side_effect = \
self._simulate_rpc_audit_template_update
cfg.CONF.set_override('goals', {"DUMMY": "DUMMY", "BASIC": "BASIC"},
group='watcher_goals', enforce_type=True)
self.addCleanup(p.stop)
def _simulate_rpc_audit_template_update(self, audit_template):
@@ -229,7 +250,7 @@ class TestPatch(api_base.FunctionalTest):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
new_goal = 'BALANCE_LOAD'
new_goal = "BASIC"
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
self.assertNotEqual(new_goal, response['goal'])
@@ -253,7 +274,7 @@ class TestPatch(api_base.FunctionalTest):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
new_goal = 'BALANCE_LOAD'
new_goal = 'BASIC'
response = self.get_json(urlparse.quote(
'/audit_templates/%s' % self.audit_template.name))
self.assertNotEqual(new_goal, response['goal'])
@@ -275,15 +296,29 @@ class TestPatch(api_base.FunctionalTest):
def test_replace_non_existent_audit_template(self):
response = self.patch_json(
'/audit_templates/%s' % utils.generate_uuid(),
[{'path': '/goal', 'value': 'BALANCE_LOAD',
[{'path': '/goal', 'value': 'DUMMY',
'op': 'replace'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_replace_invalid_goal(self):
with mock.patch.object(
self.dbapi,
'update_audit_template',
wraps=self.dbapi.update_audit_template
) as cn_mock:
response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'value': 'INVALID_GOAL',
'op': 'replace'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_add_ok(self):
new_goal = 'BALANCE_LOAD'
new_goal = 'DUMMY'
response = self.patch_json(
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'value': new_goal, 'op': 'add'}])

View File

@@ -564,7 +564,7 @@ class TestDelete(api_base.FunctionalTest):
return_deleted_at = timeutils.strtime(audit['deleted_at'])
self.assertEqual(timeutils.strtime(test_time), return_deleted_at)
self.assertEqual(audit['state'], 'DELETED')
self.assertEqual('DELETED', audit['state'])
def test_delete_audit_not_found(self):
uuid = utils.generate_uuid()

View File

@@ -15,11 +15,11 @@
import wsme
from oslo_config import cfg
from watcher.api.controllers.v1 import utils
from watcher.tests import base
from oslo_config import cfg
CONF = cfg.CONF
@@ -40,10 +40,31 @@ class TestApiUtils(base.TestCase):
self.assertRaises(wsme.exc.ClientSideError, utils.validate_limit, 0)
def test_validate_sort_dir(self):
sort_dir = utils.validate_sort_dir('asc')
self.assertEqual('asc', sort_dir)
# if sort_dir is valid, nothing should happen
try:
utils.validate_sort_dir('asc')
except Exception as exc:
self.fail(exc)
# invalid sort_dir parameter
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_sort_dir,
'fake-sort')
def test_validate_search_filters(self):
allowed_fields = ["allowed", "authorized"]
test_filters = {"allowed": 1, "authorized": 2}
try:
utils.validate_search_filters(test_filters, allowed_fields)
except Exception as exc:
self.fail(exc)
def test_validate_search_filters_with_invalid_key(self):
allowed_fields = ["allowed", "authorized"]
test_filters = {"allowed": 1, "unauthorized": 2}
self.assertRaises(
wsme.exc.ClientSideError, utils.validate_search_filters,
test_filters, allowed_fields)

View File

@@ -18,38 +18,38 @@
import mock
from watcher.applier.action_plan.default import DefaultActionPlanHandler
from watcher.applier.messaging.event_types import EventTypes
from watcher.applier.action_plan import default
from watcher.applier.messaging import event_types as ev
from watcher.objects import action_plan as ap_objects
from watcher.objects import ActionPlan
from watcher.tests.db.base import DbTestCase
from watcher.tests.db import base
from watcher.tests.objects import utils as obj_utils
class TestDefaultActionPlanHandler(DbTestCase):
class TestDefaultActionPlanHandler(base.DbTestCase):
def setUp(self):
super(TestDefaultActionPlanHandler, self).setUp()
self.action_plan = obj_utils.create_test_action_plan(
self.context)
def test_launch_action_plan(self):
command = DefaultActionPlanHandler(self.context, mock.MagicMock(),
self.action_plan.uuid)
command = default.DefaultActionPlanHandler(self.context,
mock.MagicMock(),
self.action_plan.uuid)
command.execute()
action_plan = ActionPlan.get_by_uuid(self.context,
self.action_plan.uuid)
action_plan = ap_objects.ActionPlan.get_by_uuid(self.context,
self.action_plan.uuid)
self.assertEqual(ap_objects.State.SUCCEEDED, action_plan.state)
def test_trigger_audit_send_notification(self):
messaging = mock.MagicMock()
command = DefaultActionPlanHandler(self.context, messaging,
self.action_plan.uuid)
command = default.DefaultActionPlanHandler(self.context, messaging,
self.action_plan.uuid)
command.execute()
call_on_going = mock.call(EventTypes.LAUNCH_ACTION_PLAN.name, {
call_on_going = mock.call(ev.EventTypes.LAUNCH_ACTION_PLAN.name, {
'action_plan_state': ap_objects.State.ONGOING,
'action_plan__uuid': self.action_plan.uuid})
call_succeeded = mock.call(EventTypes.LAUNCH_ACTION_PLAN.name, {
call_succeeded = mock.call(ev.EventTypes.LAUNCH_ACTION_PLAN.name, {
'action_plan_state': ap_objects.State.SUCCEEDED,
'action_plan__uuid': self.action_plan.uuid})

View File

@@ -0,0 +1,146 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import unicode_literals
import mock
import voluptuous
from watcher.applier.actions import base as baction
from watcher.applier.actions import change_nova_service_state
from watcher.common import clients
from watcher.common import nova_helper
from watcher.decision_engine.model import hypervisor_state as hstate
from watcher.tests import base
class TestChangeNovaServiceState(base.TestCase):
def setUp(self):
super(TestChangeNovaServiceState, self).setUp()
self.m_osc_cls = mock.Mock()
self.m_helper_cls = mock.Mock()
self.m_helper = mock.Mock(spec=nova_helper.NovaHelper)
self.m_helper_cls.return_value = self.m_helper
self.m_osc = mock.Mock(spec=clients.OpenStackClients)
self.m_osc_cls.return_value = self.m_osc
m_openstack_clients = mock.patch.object(
clients, "OpenStackClients", self.m_osc_cls)
m_nova_helper = mock.patch.object(
nova_helper, "NovaHelper", self.m_helper_cls)
m_openstack_clients.start()
m_nova_helper.start()
self.addCleanup(m_openstack_clients.stop)
self.addCleanup(m_nova_helper.stop)
self.input_parameters = {
baction.BaseAction.RESOURCE_ID: "compute-1",
"state": hstate.HypervisorState.ENABLED.value,
}
self.action = change_nova_service_state.ChangeNovaServiceState()
self.action.input_parameters = self.input_parameters
def test_parameters_down(self):
self.action.input_parameters = {
baction.BaseAction.RESOURCE_ID: "compute-1",
self.action.STATE: hstate.HypervisorState.DISABLED.value}
self.assertEqual(True, self.action.validate_parameters())
def test_parameters_up(self):
self.action.input_parameters = {
baction.BaseAction.RESOURCE_ID: "compute-1",
self.action.STATE: hstate.HypervisorState.ENABLED.value}
self.assertEqual(True, self.action.validate_parameters())
def test_parameters_exception_wrong_state(self):
self.action.input_parameters = {
baction.BaseAction.RESOURCE_ID: "compute-1",
self.action.STATE: 'error'}
exc = self.assertRaises(
voluptuous.Invalid, self.action.validate_parameters)
self.assertEqual(
[(['state'], voluptuous.ScalarInvalid)],
[([str(p) for p in e.path], type(e)) for e in exc.errors])
def test_parameters_resource_id_empty(self):
self.action.input_parameters = {
self.action.STATE: hstate.HypervisorState.ENABLED.value,
}
exc = self.assertRaises(
voluptuous.Invalid, self.action.validate_parameters)
self.assertEqual(
[(['resource_id'], voluptuous.RequiredFieldInvalid)],
[([str(p) for p in e.path], type(e)) for e in exc.errors])
def test_parameters_applies_add_extra(self):
self.action.input_parameters = {"extra": "failed"}
exc = self.assertRaises(
voluptuous.Invalid, self.action.validate_parameters)
self.assertEqual(
sorted([(['resource_id'], voluptuous.RequiredFieldInvalid),
(['state'], voluptuous.RequiredFieldInvalid),
(['extra'], voluptuous.Invalid)],
key=lambda x: str(x[0])),
sorted([([str(p) for p in e.path], type(e)) for e in exc.errors],
key=lambda x: str(x[0])))
def test_change_service_state_precondition(self):
try:
self.action.precondition()
except Exception as exc:
self.fail(exc)
def test_change_service_state_postcondition(self):
try:
self.action.postcondition()
except Exception as exc:
self.fail(exc)
def test_execute_change_service_state_with_enable_target(self):
self.action.execute()
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
self.m_helper.enable_service_nova_compute.assert_called_once_with(
"compute-1")
def test_execute_change_service_state_with_disable_target(self):
self.action.input_parameters["state"] = (
hstate.HypervisorState.DISABLED.value)
self.action.execute()
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
self.m_helper.disable_service_nova_compute.assert_called_once_with(
"compute-1")
def test_revert_change_service_state_with_enable_target(self):
self.action.revert()
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
self.m_helper.disable_service_nova_compute.assert_called_once_with(
"compute-1")
def test_revert_change_service_state_with_disable_target(self):
self.action.input_parameters["state"] = (
hstate.HypervisorState.DISABLED.value)
self.action.revert()
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
self.m_helper.enable_service_nova_compute.assert_called_once_with(
"compute-1")

View File

@@ -0,0 +1,206 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import unicode_literals
import mock
import voluptuous
from watcher.applier.actions import base as baction
from watcher.applier.actions import migration
from watcher.common import clients
from watcher.common import exception
from watcher.common import nova_helper
from watcher.tests import base
class TestMigration(base.TestCase):
INSTANCE_UUID = "45a37aeb-95ab-4ddb-a305-7d9f62c2f5ba"
def setUp(self):
super(TestMigration, self).setUp()
self.m_osc_cls = mock.Mock()
self.m_helper_cls = mock.Mock()
self.m_helper = mock.Mock(spec=nova_helper.NovaHelper)
self.m_helper_cls.return_value = self.m_helper
self.m_osc = mock.Mock(spec=clients.OpenStackClients)
self.m_osc_cls.return_value = self.m_osc
m_openstack_clients = mock.patch.object(
clients, "OpenStackClients", self.m_osc_cls)
m_nova_helper = mock.patch.object(
nova_helper, "NovaHelper", self.m_helper_cls)
m_openstack_clients.start()
m_nova_helper.start()
self.addCleanup(m_openstack_clients.stop)
self.addCleanup(m_nova_helper.stop)
self.input_parameters = {
"migration_type": "live",
"src_hypervisor": "hypervisor1-hostname",
"dst_hypervisor": "hypervisor2-hostname",
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
}
self.action = migration.Migrate()
self.action.input_parameters = self.input_parameters
def test_parameters(self):
params = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
self.action.MIGRATION_TYPE: 'live',
self.action.DST_HYPERVISOR: 'compute-2',
self.action.SRC_HYPERVISOR: 'compute-3'}
self.action.input_parameters = params
self.assertEqual(True, self.action.validate_parameters())
def test_parameters_exception_empty_fields(self):
parameters = {baction.BaseAction.RESOURCE_ID: None,
'migration_type': None,
'src_hypervisor': None,
'dst_hypervisor': None}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual(
sorted([(['migration_type'], voluptuous.ScalarInvalid),
(['src_hypervisor'], voluptuous.TypeInvalid),
(['dst_hypervisor'], voluptuous.TypeInvalid)]),
sorted([(e.path, type(e)) for e in exc.errors]))
def test_parameters_exception_migration_type(self):
parameters = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
'migration_type': 'cold',
'src_hypervisor': 'compute-2',
'dst_hypervisor': 'compute-3'}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.Invalid, self.action.validate_parameters)
self.assertEqual(
[(['migration_type'], voluptuous.ScalarInvalid)],
[(e.path, type(e)) for e in exc.errors])
def test_parameters_exception_src_hypervisor(self):
parameters = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
'migration_type': 'live',
'src_hypervisor': None,
'dst_hypervisor': 'compute-3'}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual(
[(['src_hypervisor'], voluptuous.TypeInvalid)],
[(e.path, type(e)) for e in exc.errors])
def test_parameters_exception_dst_hypervisor(self):
parameters = {baction.BaseAction.RESOURCE_ID:
self.INSTANCE_UUID,
'migration_type': 'live',
'src_hypervisor': 'compute-1',
'dst_hypervisor': None}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual(
[(['dst_hypervisor'], voluptuous.TypeInvalid)],
[(e.path, type(e)) for e in exc.errors])
def test_parameters_exception_resource_id(self):
parameters = {baction.BaseAction.RESOURCE_ID: "EFEF",
'migration_type': 'live',
'src_hypervisor': 'compute-2',
'dst_hypervisor': 'compute-3'}
self.action.input_parameters = parameters
exc = self.assertRaises(
voluptuous.MultipleInvalid, self.action.validate_parameters)
self.assertEqual(
[(['resource_id'], voluptuous.Invalid)],
[(e.path, type(e)) for e in exc.errors])
def test_migration_precondition(self):
try:
self.action.precondition()
except Exception as exc:
self.fail(exc)
def test_migration_postcondition(self):
try:
self.action.postcondition()
except Exception as exc:
self.fail(exc)
def test_execute_live_migration_invalid_instance(self):
self.m_helper.find_instance.return_value = None
exc = self.assertRaises(
exception.InstanceNotFound, self.action.execute)
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
def test_execute_live_migration(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
try:
self.action.execute()
except Exception as exc:
self.fail(exc)
self.m_helper.live_migrate_instance.assert_called_once_with(
instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname")
def test_revert_live_migration(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
self.action.revert()
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
self.m_helper.live_migrate_instance.assert_called_once_with(
instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor1-hostname"
)
def test_live_migrate_non_shared_storage_instance(self):
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
self.m_helper.live_migrate_instance.side_effect = [
nova_helper.nvexceptions.ClientException(400, "BadRequest"), True]
try:
self.action.execute()
except Exception as exc:
self.fail(exc)
self.m_helper.live_migrate_instance.assert_has_calls([
mock.call(instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname"),
mock.call(instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname",
block_migration=True)
])
expected = [mock.call.first(instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname"),
mock.call.second(instance_id=self.INSTANCE_UUID,
dest_hostname="hypervisor2-hostname",
block_migration=True)
]
self.m_helper.live_migrate_instance.mock_calls == expected
self.assertEqual(2, self.m_helper.live_migrate_instance.call_count)

View File

@@ -0,0 +1,42 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2016 b<>com
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import voluptuous
from watcher.applier.actions import sleep
from watcher.tests import base
class TestSleep(base.TestCase):
def setUp(self):
super(TestSleep, self).setUp()
self.s = sleep.Sleep()
def test_parameters_duration(self):
self.s.input_parameters = {self.s.DURATION: 1.0}
self.assertEqual(True, self.s.validate_parameters())
def test_parameters_duration_empty(self):
self.s.input_parameters = {self.s.DURATION: None}
self.assertRaises(voluptuous.Invalid, self.s.validate_parameters)
def test_parameters_wrong_parameter(self):
self.s.input_parameters = {self.s.DURATION: "ef"}
self.assertRaises(voluptuous.Invalid, self.s.validate_parameters)
def test_parameters_add_field(self):
self.s.input_parameters = {self.s.DURATION: 1.0, "not_required": "nop"}
self.assertRaises(voluptuous.Invalid, self.s.validate_parameters)

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