Compare commits

...

89 Commits

Author SHA1 Message Date
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
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
Jenkins
ad40c61ea9 Merge "Update docs for password auth configuration options" 2016-02-05 15:58:31 +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
Jenkins
1d74f7e3bc Merge "Remove references to SERVERS_CONSOLIDATION" 2016-02-05 14:28:34 +00:00
Jenkins
b7641a9311 Merge "Added Tempest scenario for BASIC_CONSOLIDATION" 2016-02-04 08:18:30 +00:00
David TARDIVEL
376d669af6 Update docs for password auth configuration options
Watcher uses now auth_type 'password' plugin for authentication.
Configuration related to credentials used to validate and apply
for a token has been updated.

Change-Id: If71bb908741130cb01d5d1525a12cf9a68b58a58
Closes-Bug: #1541296
2016-02-03 19:24:00 +01:00
Jenkins
25d27f0288 Merge "Create OpenStackClients convenience class" 2016-02-03 10:21:32 +00:00
Jenkins
3f4686ce79 Merge "Use install instead of mkdir for DevStack dirs" 2016-02-03 06:43:17 +00:00
Taylor Peoples
86c1a9d77f Remove references to SERVERS_CONSOLIDATION
Change I6c43eba941022a88851a199b56a6c20f017b9e71 seemed to have remove
most references to the SERVERS_CONSOLIDATION goal.  Since this goal does
not currently exist in the actual code and all usages of it are for
samples or for tests, it is replaced with the DUMMY goal to avoid
confusion.

Change-Id: I4d2240d3b22c42ebf4e6120e2cd7677ec49d8e98
Closes-Bug: #1538388
2016-02-03 07:22:44 +01:00
Taylor Peoples
9a6811ae6b Create OpenStackClients convenience class
The OpenStackClients class provides a convenient way to create and
cache client instances.  The idea behind this code comes from Magnum
[0].

The OpenStackClients class will act as the manager of other project's
clients, providing an easy way to fetch instances of said clients. This
will allow the clients to be cached.

An instance of OpenStackClients is created for every call that comes
into the decision engine and the applier, using the request context to
pass needed (domain id) parameters to get a Keystone session.  This
instance should be shared as much as possible to avoid additional
unneccessary connections to the other services.

This class will also allow for the version of each client to be
configurable via the watcher.conf file.

The method by which a Keystone session is also changed to use the
keystoneauth1.loading library.  In order to avoid DuplicateOptErrors
with the keystone_authtoken group used for the keystonemiddleware in the
API code, a new conf group named "watcher_clients_auth" is created.  A
typical configuration using a password authentication scheme will look
like:
  [watcher_clients_auth]
  auth_type = password
  auth_url = http://<server-ip>:<port>
  username = <username>
  password = <password>
  project_domain_id = default
  user_domain_id = default

[0]: https://github.com/openstack/magnum/blob/master/magnum/common/clients.py

DocImpact
Change-Id: Iab9d0b304099686da2e9e2b19e8b1de4332ff378
Implements: blueprint external-api-versioning
Closes-Bug: #1530790
Closes-Bug: #1539670
Closes-Bug: #1522774
2016-02-03 02:27:26 +01:00
Jenkins
e520f5f452 Merge "Removed unused parameter in dt_deserializer()" 2016-02-03 00:13:38 +00:00
Jenkins
6a25bd983c Merge "Remove InvalidParameterValue exception" 2016-02-02 23:29:55 +00:00
Jenkins
c175ef2170 Merge "Define self.client in MessagingCore" 2016-02-02 16:08:12 +00:00
Jenkins
28733a5f30 Merge "Remove unused parameter in Actions API controller" 2016-02-02 14:45:47 +00:00
Vincent Françoise
7f8fec1bca Added Tempest scenario for BASIC_CONSOLIDATION
As of now we only have a single scenario which creates and
successfully executes the DUMMY goal.

This patchset adds a new scenario which creates and executes the
BASIC_CONSOLIDATION goal mapped to the 'basic' (sercon) strategy.

The documentation has also been updated to take into account the
multinode configuration.

Change-Id: Ie246aed288ade56a8fe9c0d9b08365d72e60ada1
Closes-Bug: #1538606
2016-02-02 13:35:17 +00:00
Edwin Zhai
278b1819d6 Use install instead of mkdir for DevStack dirs
The current code will not work if WATCHER_CONF_DIR or
WATCHER_AUTH_CACHE_DIR already exist but are owned by a different user
such as root. Use install instead of mkdir to handle this scenario.

Change-Id: Ie582a4b393e898e007d73f31de490c4b77e40be3
Closes-Bug: #1539422
2016-02-02 09:53:55 +00:00
Gábor Antal
978bb11d4a Removed unused parameter in dt_deserializer()
In the file watcher/objects/utils.py, on line 120,
there is an unused parameter:
  def dt_deserializer(instance, val):

I removed that parameter, and modified the test.

Change-Id: Ibc7ab703d37d7f9248a84e41508820453c8954b7
Closes-Bug: #1540521
2016-02-01 19:29:04 +01:00
Steve Wilkerson
3027b28942 Remove unused parameter in Actions API controller
Removed the action_uuid parameter in get_all() and
detail()

Change-Id: If99a4a50bb72383bd96ad284d35946911cb68d1d
Closes-Bug: #1538171
2016-01-29 12:41:10 -06:00
Darren Shaw
2f0c1c12cf Define self.client in MessagingCore
Currently self.client is referenced within MessagingCore,
but no definition is made in its constructor. Additionally
self.client is defined in children classes of MessagingCore.
This patchset defines self.client in the constructor of
MessagingCore and removes the redefinition in its children.

-self.client lazily loaded

Co-Authored-By: v-francoise <Vincent.FRANCOISE@b-com.com>
Change-Id: I14525a175bf1ebde3d2636024ad2f2219c79d6e1
Closes-Bug: #1521636
2016-01-27 16:24:45 +01:00
Taylor Peoples
e122c61840 Remove InvalidParameterValue exception
The InvalidParameterValue exception does not define a meaningful
msg_fmt.  It is currently _("%(err)s"), which is the equivalent of
nothing and does not help with translation.

Replace InvalidParameterValue with Invalid exceptions.

Change-Id: If8b064e446cbc97e380127f360f262be9e8877a1
Closes-Bug: #1538398
2016-01-27 16:13:52 +01:00
Vincent Françoise
8f6eac819f Tempest API tests on /actions
Following the blueprint tempest-basic-set-up which implemented a first
batch of tests, this one adds a new set of API tests on actions.

I also added extra check on actions within the dummy strategy
scenario.

Change-Id: Ib9bf093d0ed457ecba32e8251c019d2cf5c98128
Closes-Bug: #1538074
2016-01-27 10:02:59 +01:00
Vincent Françoise
de307e536e GET on an action_plan provides first_action_uuid
Whenever trying to get the first action related to a given action
plan, we were getting back a 'null' value from the API even though
we knew there were actions to be linked to it in the DB.
So I fixed this issue and added a related unit test.

Change-Id: I1fa755f24fbf37ecd6ce2cc2396658fca8743a1c
Closes-Bug: #1538130
2016-01-27 09:38:15 +01:00
Vincent Françoise
7406a1e713 Fixed ActionPlanNotFound typo in msg_fmt
The msg_fmt of ActionPlanNotFound was missing an "_" which caused
errors upon trying to format it, so I fixed it.

Change-Id: I515c2097a563f809e319d2e57480fd340b878cef
Closes-Bug: #1538065
2016-01-26 11:32:09 +01:00
Vincent Françoise
982410dd3e Fixed tempest test bug
has_audit_succeeded was not implemented so I added it back.

Change-Id: Ic567ff56ea6d513c32fbe7ad08cca96b5dfb15e8
Closes-Bug: #1537144
2016-01-26 09:17:39 +01:00
130 changed files with 4673 additions and 2721 deletions

View File

@@ -80,10 +80,7 @@ function cleanup_watcher {
# configure_watcher() - Set config files, create data dirs, etc
function configure_watcher {
# Put config files in ``/etc/watcher`` for everyone to find
if [[ ! -d $WATCHER_CONF_DIR ]]; then
sudo mkdir -p $WATCHER_CONF_DIR
sudo chown $STACK_USER $WATCHER_CONF_DIR
fi
sudo install -d -o $STACK_USER $WATCHER_CONF_DIR
install_default_policy watcher
@@ -99,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
@@ -128,14 +123,8 @@ function create_watcher_conf {
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_host $RABBIT_HOST
iniset $WATCHER_CONF keystone_authtoken admin_user watcher
iniset $WATCHER_CONF keystone_authtoken admin_password $SERVICE_PASSWORD
iniset $WATCHER_CONF keystone_authtoken admin_tenant_name $SERVICE_TENANT_NAME
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR
iniset $WATCHER_CONF keystone_authtoken auth_uri $KEYSTONE_SERVICE_URI/v3
iniset $WATCHER_CONF keystone_authtoken auth_version v3
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR "watcher_clients_auth"
if is_fedora || is_suse; then
# watcher defaults to /usr/local/bin, but fedora and suse pip like to
@@ -178,9 +167,8 @@ function create_watcher_conf {
# create_watcher_cache_dir() - Part of the init_watcher() process
function create_watcher_cache_dir {
# Create cache dir
sudo mkdir -p $WATCHER_AUTH_CACHE_DIR
sudo chown $STACK_USER $WATCHER_AUTH_CACHE_DIR
rm -f $WATCHER_AUTH_CACHE_DIR/*
sudo install -d -o $STACK_USER $WATCHER_AUTH_CACHE_DIR
rm -rf $WATCHER_AUTH_CACHE_DIR/*
}
# init_watcher() - Initialize databases, etc.

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:

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/
@@ -166,11 +169,17 @@ The configuration file is organized into the following sections:
* ``[api]`` - API server configuration
* ``[database]`` - SQL driver configuration
* ``[keystone_authtoken]`` - Keystone Authentication plugin configuration
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients
* ``[watcher_applier]`` - Watcher Applier module configuration
* ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration
* ``[watcher_goals]`` - Goals mapping configuration
* ``[watcher_strategies]`` - Strategy configuration
* ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration
* ``[ceilometer_client]`` - Ceilometer client configuration
* ``[cinder_client]`` - Cinder client configuration
* ``[glance_client]`` - Glance client configuration
* ``[nova_client]`` - Nova client configuration
* ``[neutron_client]`` - Neutron client configuration
The Watcher configuration file is expected to be named
``watcher.conf``. When starting Watcher, you can specify a different
@@ -237,39 +246,105 @@ so that the watcher service is configured for your needs.
#rabbit_port = 5672
#. Configure the Watcher Service to use these credentials with the Identity
Service. Replace IDENTITY_IP with the IP of the Identity server, and
replace WATCHER_PASSWORD with the password you chose for the ``watcher``
user in the Identity Service::
#. Watcher API shall validate the token provided by every incoming request,
via keystonemiddleware, which requires the Watcher service to be configured
with the right credentials for the Identity service.
[keystone_authtoken]
In the configuration section here below:
# Complete public Identity API endpoint (string value)
#auth_uri=<None>
auth_uri=http://IDENTITY_IP:5000/v3
* replace IDENTITY_IP with the IP of the Identity server
* replace WATCHER_PASSWORD with the password you chose for the ``watcher``
user
* replace KEYSTONE_SERVICE_PROJECT_NAME with the name of project created
for OpenStack services (e.g. ``service``) ::
# Complete admin Identity API endpoint. This should specify the
# unversioned root endpoint e.g. https://localhost:35357/ (string
# value)
#identity_uri = <None>
identity_uri = http://IDENTITY_IP:5000
[keystone_authtoken]
# Keystone account username (string value)
#admin_user=<None>
admin_user=watcher
# Authentication type to load (unknown value)
# Deprecated group/name - [DEFAULT]/auth_plugin
#auth_type = <None>
auth_type = password
# Keystone account password (string value)
#admin_password=<None>
admin_password=WATCHER_DBPASSWORD
# Authentication URL (unknown value)
#auth_url = <None>
auth_url = http://IDENTITY_IP:35357
# Keystone service account tenant name to validate user tokens
# (string value)
#admin_tenant_name=admin
admin_tenant_name=KEYSTONE_SERVICE_PROJECT_NAME
# Username (unknown value)
# Deprecated group/name - [DEFAULT]/username
#username = <None>
username=watcher
# Directory used to cache files related to PKI tokens (string
# value)
#signing_dir=<None>
# User's password (unknown value)
#password = <None>
password = WATCHER_PASSWORD
# Domain ID containing project (unknown value)
#project_domain_id = <None>
project_domain_id = default
# User's domain id (unknown value)
#user_domain_id = <None>
user_domain_id = default
# Project name to scope to (unknown value)
# Deprecated group/name - [DEFAULT]/tenant-name
#project_name = <None>
project_name = KEYSTONE_SERVICE_PROJECT_NAME
#. Watcher's decision engine and applier interact with other OpenStack
projects through those projects' clients. In order to instantiate these
clients, Watcher needs to request a new session from the Identity service
using the right credentials.
In the configuration section here below:
* replace IDENTITY_IP with the IP of the Identity server
* replace WATCHER_PASSWORD with the password you chose for the ``watcher``
user
* replace KEYSTONE_SERVICE_PROJECT_NAME with the name of project created
for OpenStack services (e.g. ``service``) ::
[watcher_clients_auth]
# Authentication type to load (unknown value)
# Deprecated group/name - [DEFAULT]/auth_plugin
#auth_type = <None>
auth_type = password
# Authentication URL (unknown value)
#auth_url = <None>
auth_url = http://IDENTITY_IP:35357
# Username (unknown value)
# Deprecated group/name - [DEFAULT]/username
#username = <None>
username=watcher
# User's password (unknown value)
#password = <None>
password = WATCHER_PASSWORD
# Domain ID containing project (unknown value)
#project_domain_id = <None>
project_domain_id = default
# User's domain id (unknown value)
#user_domain_id = <None>
user_domain_id = default
# Project name to scope to (unknown value)
# Deprecated group/name - [DEFAULT]/tenant-name
#project_name = <None>
project_name = KEYSTONE_SERVICE_PROJECT_NAME
#. Configure the clients to use a specific version if desired. For example, to
configure Watcher to use a Nova client with version 2.1, use::
[nova_client]
# Version of Nova API to use in novaclient. (string value)
#api_version = 2
api_version = 2.1
#. Create the Watcher Service database tables::

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 ?
------------------------------------
@@ -60,7 +64,7 @@ This goal should be declared in the Watcher service configuration file
.. code:: bash
$ watcher audit-template-create my_first_audit SERVERS_CONSOLIDATION
$ watcher audit-template-create my_first_audit DUMMY
If you get "*You must provide a username via either --os-username or via
env[OS_USERNAME]*" you may have to verify your credentials.

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,159 @@
..
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
from watcher.applier.actions import base
class DummyAction(baseBaseAction):
@property
def schema(self):
return 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.
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

@@ -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,6 +55,7 @@ Getting Started
dev/environment
dev/devstack
deploy/configuration
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

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,29 +2,32 @@
# 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
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.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
voluptuous>=0.8.6 # BSD License
python-ceilometerclient>=2.2.1 # Apache-2.0
python-cinderclient>=1.3.1 # Apache-2.0
python-glanceclient>=1.2.0 # Apache-2.0
python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0
python-neutronclient>=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

@@ -63,7 +63,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,19 @@
# 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
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

11
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,7 +35,7 @@ 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
@@ -53,11 +53,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
@@ -67,4 +62,4 @@ import_exceptions = watcher._i18n
[doc8]
extension=.rst
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed
ignore-path=doc/source/image_src,doc/source/man
ignore-path=doc/source/image_src,doc/source/man,doc/source/api

View File

@@ -129,13 +129,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 +254,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:
@@ -288,10 +285,10 @@ class ActionsController(rest.RestController):
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(ActionCollection, types.uuid, types.uuid,
int, wtypes.text, wtypes.text, types.uuid,
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
wtypes.text, wtypes.text, types.uuid,
types.uuid)
def get_all(self, action_uuid=None, marker=None, limit=None,
def get_all(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None):
"""Retrieve a list of actions.
@@ -312,16 +309,14 @@ class ActionsController(rest.RestController):
marker, limit, sort_key, sort_dir,
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
@wsme_pecan.wsexpose(ActionCollection, types.uuid,
types.uuid, int, wtypes.text, wtypes.text,
types.uuid, types.uuid)
def detail(self, action_uuid=None, marker=None, limit=None,
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
wtypes.text, wtypes.text, types.uuid,
types.uuid)
def detail(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None):
"""Retrieve a list of actions with detail.
:param action_uuid: UUID of a action, to get only actions for that
action.
: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.

View File

@@ -181,7 +181,6 @@ class ActionPlan(base.APIBase):
self.fields = []
fields = list(objects.ActionPlan.fields)
fields.append('audit_uuid')
for field in fields:
# Skip fields we do not expose.
if not hasattr(self, field):
@@ -189,14 +188,19 @@ class ActionPlan(base.APIBase):
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
self.fields.append('audit_id')
self.fields.append('audit_uuid')
self.fields.append('first_action_uuid')
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
setattr(self, 'first_action_uuid',
kwargs.get('first_action_id', wtypes.Unset))
@staticmethod
def _convert_with_links(action_plan, url, expand=True):
if not expand:
action_plan.unset_fields_except(['uuid', 'state', 'updated_at',
'audit_uuid'])
action_plan.unset_fields_except(
['uuid', 'state', 'updated_at',
'audit_uuid', 'first_action_uuid'])
action_plan.links = [link.Link.make_link(
'self', url,
@@ -279,7 +283,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:
@@ -402,12 +406,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),
]
@@ -423,7 +427,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
@@ -439,7 +443,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

@@ -65,7 +65,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 +263,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 +369,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

@@ -149,7 +149,7 @@ class AuditTemplate(base.APIBase):
name='My Audit Template',
description='Description of my audit template',
host_aggregate=5,
goal='SERVERS_CONSOLIDATION',
goal='DUMMY',
extra={'automatic': True},
created_at=datetime.datetime.utcnow(),
deleted_at=None,
@@ -197,12 +197,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 +213,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 +225,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 +259,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 +271,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

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

@@ -43,8 +43,8 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
ev.data = {}
payload = {'action_plan__uuid': uuid,
'action_plan_state': state}
self.applier_manager.topic_status.publish_event(ev.type.name,
payload)
self.applier_manager.status_topic_handler.publish_event(
ev.type.name, payload)
def execute(self):
try:
@@ -52,7 +52,7 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
self.notify(self.action_plan_uuid,
event_types.EventTypes.LAUNCH_ACTION_PLAN,
ap_objects.State.ONGOING)
applier = default.DefaultApplier(self.applier_manager, self.ctx)
applier = default.DefaultApplier(self.ctx, self.applier_manager)
result = applier.execute(self.action_plan_uuid)
except Exception as e:
LOG.exception(e)

View File

@@ -22,12 +22,27 @@ import abc
import six
from watcher.common import clients
@six.add_metaclass(abc.ABCMeta)
class BaseAction(object):
def __init__(self):
# 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
def osc(self):
if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@property
def input_parameters(self):
@@ -38,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,54 +16,84 @@
# 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
from watcher.common import exception
from watcher.common import keystone as kclient
from watcher.common import nova as nclient
from watcher.common import nova_helper
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"))
keystone = kclient.KeystoneClient()
wrapper = nclient.NovaClient(keystone.get_credentials(),
session=keystone.get_session())
nova = nova_helper.NovaHelper(osc=self.osc)
if state is True:
return wrapper.enable_service_nova_compute(self.host)
return nova.enable_service_nova_compute(self.host)
else:
return wrapper.disable_service_nova_compute(self.host)
return nova.disable_service_nova_compute(self.host)
def precondition(self):
pass

View File

@@ -28,9 +28,15 @@ class ActionFactory(object):
def __init__(self):
self.action_loader = default.DefaultActionLoader()
def make_action(self, object_action):
def make_action(self, object_action, osc=None):
LOG.debug("Creating instance of %s", object_action.action_type)
loaded_action = self.action_loader.load(name=object_action.action_type)
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,45 +18,128 @@
#
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 keystone as kclient
from watcher.common import nova as nclient
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):
keystone = kclient.KeystoneClient()
wrapper = nclient.NovaClient(keystone.get_credentials(),
session=keystone.get_session())
LOG.debug("Migrate instance %s to %s ", self.instance_uuid,
nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
destination)
instance = wrapper.find_instance(self.instance_uuid)
instance = nova.find_instance(self.instance_uuid)
if instance:
if self.migration_type == 'live':
return wrapper.live_migrate_instance(
instance_id=self.instance_uuid, dest_hostname=destination)
return self._live_migrate_instance(nova, destination)
else:
raise exception.InvalidParameterValue(err=self.migration_type)
raise exception.Invalid(
message=(_('Migration of type %(migration_type)s is not '
'supported.') %
{'migration_type': self.migration_type}))
else:
raise exception.InstanceNotFound(name=self.instance_uuid)

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

@@ -28,7 +28,7 @@ CONF = cfg.CONF
class DefaultApplier(base.BaseApplier):
def __init__(self, applier_manager, context):
def __init__(self, context, applier_manager):
super(DefaultApplier, self).__init__()
self._applier_manager = applier_manager
self._loader = default.DefaultWorkFlowEngineLoader()
@@ -48,9 +48,10 @@ class DefaultApplier(base.BaseApplier):
if self._engine is None:
selected_workflow_engine = CONF.watcher_applier.workflow_engine
LOG.debug("Loading workflow engine %s ", selected_workflow_engine)
self._engine = self._loader.load(name=selected_workflow_engine)
self._engine.context = self.context
self._engine.applier_manager = self.applier_manager
self._engine = self._loader.load(
name=selected_workflow_engine,
context=self.context,
applier_manager=self.applier_manager)
return self._engine
def execute(self, action_plan_uuid):

View File

@@ -34,12 +34,12 @@ APPLIER_MANAGER_OPTS = [
min=1,
required=True,
help='Number of workers for applier, default value is 1.'),
cfg.StrOpt('topic_control',
cfg.StrOpt('conductor_topic',
default='watcher.applier.control',
help='The topic name used for'
'control events, this topic '
'used for rpc call '),
cfg.StrOpt('topic_status',
cfg.StrOpt('status_topic',
default='watcher.applier.status',
help='The topic name used for '
'status events, this topic '
@@ -67,12 +67,13 @@ class ApplierManager(messaging_core.MessagingCore):
def __init__(self):
super(ApplierManager, self).__init__(
CONF.watcher_applier.publisher_id,
CONF.watcher_applier.topic_control,
CONF.watcher_applier.topic_status,
CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.status_topic,
api_version=self.API_VERSION,
)
self.topic_control.add_endpoint(trigger.TriggerActionPlan(self))
self.conductor_topic_handler.add_endpoint(
trigger.TriggerActionPlan(self))
def join(self):
self.topic_control.join()
self.topic_status.join()
self.conductor_topic_handler.join()
self.status_topic_handler.join()

View File

@@ -39,17 +39,17 @@ class ApplierAPI(messaging_core.MessagingCore):
def __init__(self):
super(ApplierAPI, self).__init__(
CONF.watcher_applier.publisher_id,
CONF.watcher_applier.topic_control,
CONF.watcher_applier.topic_status,
CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.status_topic,
api_version=self.API_VERSION,
)
self.handler = notification.NotificationHandler(self.publisher_id)
self.handler.register_observer(self)
self.topic_status.add_endpoint(self.handler)
self.status_topic_handler.add_endpoint(self.handler)
transport = om.get_transport(CONF)
target = om.Target(
topic=CONF.watcher_applier.topic_control,
topic=CONF.watcher_applier.conductor_topic,
version=self.API_VERSION,
)
@@ -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,33 +22,33 @@ import six
from watcher.applier.actions import factory
from watcher.applier.messaging import event_types
from watcher.common import clients
from watcher.common.messaging.events import event
from watcher import objects
@six.add_metaclass(abc.ABCMeta)
class BaseWorkFlowEngine(object):
def __init__(self):
self._applier_manager = None
self._context = None
def __init__(self, context=None, applier_manager=None):
self._context = context
self._applier_manager = applier_manager
self._action_factory = factory.ActionFactory()
self._osc = None
@property
def context(self):
return self._context
@context.setter
def context(self, c):
self._context = c
@property
def osc(self):
if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@property
def applier_manager(self):
return self._applier_manager
@applier_manager.setter
def applier_manager(self, a):
self._applier_manager = a
@property
def action_factory(self):
return self._action_factory
@@ -62,8 +62,8 @@ class BaseWorkFlowEngine(object):
ev.data = {}
payload = {'action_uuid': action.uuid,
'action_state': state}
self.applier_manager.topic_status.publish_event(ev.type.name,
payload)
self.applier_manager.status_topic_handler.publish_event(
ev.type.name, payload)
@abc.abstractmethod
def execute(self, actions):

View File

@@ -28,6 +28,12 @@ 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
#
@@ -89,7 +95,9 @@ class TaskFlowActionContainer(task.Task):
@property
def action(self):
if self.loaded_action is None:
action = self.engine.action_factory.make_action(self._db_action)
action = self.engine.action_factory.make_action(
self._db_action,
osc=self._engine.osc)
self.loaded_action = action
return self.loaded_action

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

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

@@ -17,30 +17,16 @@
# limitations under the License.
#
from ceilometerclient import client
from ceilometerclient.exc import HTTPUnauthorized
from watcher.common import keystone
from watcher.common import clients
class CeilometerClient(object):
def __init__(self, api_version='2'):
self._cmclient = None
self._api_version = api_version
@property
def cmclient(self):
"""Initialization of Ceilometer client."""
if not self._cmclient:
ksclient = keystone.KeystoneClient()
creds = ksclient.get_credentials()
endpoint = ksclient.get_endpoint(
service_type='metering',
endpoint_type='publicURL')
self._cmclient = client.get_client(self._api_version,
ceilometer_url=endpoint,
**creds)
return self._cmclient
class CeilometerHelper(object):
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
self.osc = osc if osc else clients.OpenStackClients()
self.ceilometer = self.osc.ceilometer()
def build_query(self, user_id=None, tenant_id=None, resource_id=None,
user_ids=None, tenant_ids=None, resource_ids=None):
@@ -83,20 +69,21 @@ class CeilometerClient(object):
try:
return f(*args, **kargs)
except HTTPUnauthorized:
self.reset_client()
self.osc.reset_clients()
self.ceilometer = self.osc.ceilometer()
return f(*args, **kargs)
except Exception:
raise
def query_sample(self, meter_name, query, limit=1):
return self.query_retry(f=self.cmclient.samples.list,
return self.query_retry(f=self.ceilometer.samples.list,
meter_name=meter_name,
limit=limit,
q=query)
def statistic_list(self, meter_name, query=None, period=None):
"""List of statistics."""
statistics = self.cmclient.statistics.list(
statistics = self.ceilometer.statistics.list(
meter_name=meter_name,
q=query,
period=period)
@@ -104,7 +91,8 @@ class CeilometerClient(object):
def meter_list(self, query=None):
"""List the user's meters."""
meters = self.query_retry(f=self.cmclient.meters.list, query=query)
meters = self.query_retry(f=self.ceilometer.meters.list,
query=query)
return meters
def statistic_aggregation(self,
@@ -125,7 +113,7 @@ class CeilometerClient(object):
"""
query = self.build_query(resource_id=resource_id)
statistic = self.query_retry(f=self.cmclient.statistics.list,
statistic = self.query_retry(f=self.ceilometer.statistics.list,
meter_name=meter_name,
q=query,
period=period,
@@ -140,7 +128,8 @@ class CeilometerClient(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(
@@ -156,6 +145,3 @@ class CeilometerClient(object):
return samples[-1]._info['counter_volume']
else:
return False
def reset_client(self):
self._cmclient = None

158
watcher/common/clients.py Normal file
View File

@@ -0,0 +1,158 @@
# 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 ceilometerclient import client as ceclient
from cinderclient import client as ciclient
from glanceclient import client as glclient
from keystoneauth1 import loading as ka_loading
from keystoneclient import client as keyclient
from neutronclient.neutron import client as netclient
from novaclient import client as nvclient
from oslo_config import cfg
from watcher._i18n import _
from watcher.common import exception
NOVA_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Nova API to use in novaclient.'))]
GLANCE_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Glance API to use in glanceclient.'))]
CINDER_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Cinder API to use in cinderclient.'))]
CEILOMETER_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Ceilometer API to use in '
'ceilometerclient.'))]
NEUTRON_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2.0',
help=_('Version of Neutron API to use in neutronclient.'))]
cfg.CONF.register_opts(NOVA_CLIENT_OPTS, group='nova_client')
cfg.CONF.register_opts(GLANCE_CLIENT_OPTS, group='glance_client')
cfg.CONF.register_opts(CINDER_CLIENT_OPTS, group='cinder_client')
cfg.CONF.register_opts(CEILOMETER_CLIENT_OPTS, group='ceilometer_client')
cfg.CONF.register_opts(NEUTRON_CLIENT_OPTS, group='neutron_client')
_CLIENTS_AUTH_GROUP = 'watcher_clients_auth'
ka_loading.register_auth_conf_options(cfg.CONF, _CLIENTS_AUTH_GROUP)
ka_loading.register_session_conf_options(cfg.CONF, _CLIENTS_AUTH_GROUP)
class OpenStackClients(object):
"""Convenience class to create and cache client instances."""
def __init__(self):
self.reset_clients()
def reset_clients(self):
self._session = None
self._keystone = None
self._nova = None
self._glance = None
self._cinder = None
self._ceilometer = None
self._neutron = None
def _get_keystone_session(self):
auth = ka_loading.load_auth_from_conf_options(cfg.CONF,
_CLIENTS_AUTH_GROUP)
sess = ka_loading.load_session_from_conf_options(cfg.CONF,
_CLIENTS_AUTH_GROUP,
auth=auth)
return sess
@property
def auth_url(self):
return self.keystone().auth_url
@property
def session(self):
if not self._session:
self._session = self._get_keystone_session()
return self._session
def _get_client_option(self, client, option):
return getattr(getattr(cfg.CONF, '%s_client' % client), option)
@exception.wrap_keystone_exception
def keystone(self):
if not self._keystone:
self._keystone = keyclient.Client(session=self.session)
return self._keystone
@exception.wrap_keystone_exception
def nova(self):
if self._nova:
return self._nova
novaclient_version = self._get_client_option('nova', 'api_version')
self._nova = nvclient.Client(novaclient_version,
session=self.session)
return self._nova
@exception.wrap_keystone_exception
def glance(self):
if self._glance:
return self._glance
glanceclient_version = self._get_client_option('glance', 'api_version')
self._glance = glclient.Client(glanceclient_version,
session=self.session)
return self._glance
@exception.wrap_keystone_exception
def cinder(self):
if self._cinder:
return self._cinder
cinderclient_version = self._get_client_option('cinder', 'api_version')
self._cinder = ciclient.Client(cinderclient_version,
session=self.session)
return self._cinder
@exception.wrap_keystone_exception
def ceilometer(self):
if self._ceilometer:
return self._ceilometer
ceilometerclient_version = self._get_client_option('ceilometer',
'api_version')
self._ceilometer = ceclient.get_client(ceilometerclient_version,
session=self.session)
return self._ceilometer
@exception.wrap_keystone_exception
def neutron(self):
if self._neutron:
return self._neutron
neutronclient_version = self._get_client_option('neutron',
'api_version')
self._neutron = netclient.Client(neutronclient_version,
session=self.session)
self._neutron.format = 'json'
return self._neutron

View File

@@ -22,6 +22,10 @@ SHOULD include dedicated exception logging.
"""
import functools
import sys
from keystoneclient import exceptions as keystone_exceptions
from oslo_config import cfg
from oslo_log import log as logging
import six
@@ -40,6 +44,23 @@ CONF = cfg.CONF
CONF.register_opts(exc_log_opts)
def wrap_keystone_exception(func):
"""Wrap keystone exceptions and throw Watcher specific exceptions."""
@functools.wraps(func)
def wrapped(*args, **kw):
try:
return func(*args, **kw)
except keystone_exceptions.AuthorizationFailure:
raise AuthorizationFailure(
client=func.__name__, reason=sys.exc_info()[1])
except keystone_exceptions.ClientException:
raise AuthorizationFailure(
client=func.__name__,
reason=(_('Unexpected keystone client error occurred: %s')
% sys.exc_info()[1]))
return wrapped
class WatcherException(Exception):
"""Base Watcher Exception
@@ -133,12 +154,6 @@ class InvalidGoal(Invalid):
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file")
# Cannot be templated as the error syntax varies.
# msg needs to be constructed when raised.
class InvalidParameterValue(Invalid):
msg_fmt = _("%(err)s")
class InvalidUUID(Invalid):
msg_fmt = _("Expected a uuid but received %(uuid)s")
@@ -179,7 +194,7 @@ class AuditReferenced(Invalid):
class ActionPlanNotFound(ResourceNotFound):
msg_fmt = _("ActionPlan %(action plan)s could not be found")
msg_fmt = _("ActionPlan %(action_plan)s could not be found")
class ActionPlanAlreadyExists(Conflict):
@@ -232,6 +247,10 @@ class NoDataFound(WatcherException):
msg_fmt = _('No rows were returned')
class AuthorizationFailure(WatcherException):
msg_fmt = _('%(client)s connection failed. Reason: %(reason)s')
class KeystoneFailure(WatcherException):
msg_fmt = _("'Keystone API endpoint is missing''")
@@ -260,3 +279,7 @@ 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")

View File

@@ -1,131 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from keystoneclient.auth.identity import generic
from keystoneclient import session as keystone_session
from oslo_config import cfg
from oslo_log import log
from six.moves.urllib.parse import urljoin
from six.moves.urllib.parse import urlparse
from watcher._i18n import _
from watcher.common import exception
LOG = log.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('admin_password', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('auth_version', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
CONF.import_opt('insecure', 'keystonemiddleware.auth_token',
group='keystone_authtoken')
class KeystoneClient(object):
def __init__(self):
self._ks_client = None
self._session = None
self._auth = None
self._token = None
def get_endpoint(self, **kwargs):
kc = self.get_ksclient()
if not kc.has_service_catalog():
raise exception.KeystoneFailure(
_('No Keystone service catalog loaded')
)
attr = None
filter_value = None
if kwargs.get('region_name'):
attr = 'region'
filter_value = kwargs.get('region_name')
return kc.service_catalog.url_for(
service_type=kwargs.get('service_type') or 'metering',
attr=attr,
filter_value=filter_value,
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
def _is_apiv3(self, auth_url, auth_version):
return auth_version == 'v3.0' or '/v3' in urlparse(auth_url).path
def get_keystone_url(self, auth_url, auth_version):
"""Gives an http/https url to contact keystone."""
api_v3 = self._is_apiv3(auth_url, auth_version)
api_version = 'v3' if api_v3 else 'v2.0'
# NOTE(lucasagomes): Get rid of the trailing '/' otherwise urljoin()
# fails to override the version in the URL
return urljoin(auth_url.rstrip('/'), api_version)
def get_ksclient(self, creds=None):
"""Get an endpoint and auth token from Keystone."""
auth_version = CONF.keystone_authtoken.auth_version
auth_url = CONF.keystone_authtoken.auth_uri
api_v3 = self._is_apiv3(auth_url, auth_version)
if creds is None:
ks_args = self._get_credentials(api_v3)
else:
ks_args = creds
if api_v3:
from keystoneclient.v3 import client
else:
from keystoneclient.v2_0 import client
# generic
# ksclient = client.Client(version=api_version,
# session=session,**ks_args)
return client.Client(**ks_args)
def _get_credentials(self, api_v3):
if api_v3:
creds = \
{'auth_url': CONF.keystone_authtoken.auth_uri,
'username': CONF.keystone_authtoken.admin_user,
'password': CONF.keystone_authtoken.admin_password,
'project_name': CONF.keystone_authtoken.admin_tenant_name,
'user_domain_name': "default",
'project_domain_name': "default"}
else:
creds = \
{'auth_url': CONF.keystone_authtoken.auth_uri,
'username': CONF.keystone_authtoken.admin_user,
'password': CONF.keystone_authtoken.admin_password,
'tenant_name': CONF.keystone_authtoken.admin_tenant_name}
LOG.debug(creds)
return creds
def get_credentials(self):
api_v3 = self._is_apiv3(CONF.keystone_authtoken.auth_uri,
CONF.keystone_authtoken.auth_version)
return self._get_credentials(api_v3)
def get_session(self):
creds = self.get_credentials()
self._auth = generic.Password(**creds)
session = keystone_session.Session(auth=self._auth)
return session

View File

@@ -31,7 +31,7 @@ class DefaultLoader(BaseLoader):
super(DefaultLoader, self).__init__()
self.namespace = namespace
def load(self, name):
def load(self, name, **kwargs):
try:
LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
driver_manager = DriverManager(namespace=self.namespace,
@@ -41,7 +41,7 @@ class DefaultLoader(BaseLoader):
LOG.exception(exc)
raise exception.LoadingError(name=name)
return loaded()
return loaded(**kwargs)
def list_available(self):
extension_manager = ExtensionManager(namespace=self.namespace)

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

@@ -16,58 +16,100 @@
from oslo_config import cfg
from oslo_log import log
from watcher.common.messaging.events.event_dispatcher import \
EventDispatcher
from watcher.common.messaging.messaging_handler import \
MessagingHandler
from watcher.common.rpc import RequestContextSerializer
import oslo_messaging as om
from watcher.objects.base import WatcherObjectSerializer
from watcher.common.messaging.events import event_dispatcher as dispatcher
from watcher.common.messaging import messaging_handler
from watcher.common import rpc
from watcher.objects import base
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class MessagingCore(EventDispatcher):
class MessagingCore(dispatcher.EventDispatcher):
API_VERSION = '1.0'
def __init__(self, publisher_id, topic_control, topic_status,
def __init__(self, publisher_id, conductor_topic, status_topic,
api_version=API_VERSION):
super(MessagingCore, self).__init__()
self.serializer = RequestContextSerializer(WatcherObjectSerializer())
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self.publisher_id = publisher_id
self.api_version = api_version
self.topic_control = self.build_topic(topic_control)
self.topic_status = self.build_topic(topic_status)
def build_topic(self, topic_name):
return MessagingHandler(self.publisher_id, topic_name, self,
self.api_version, self.serializer)
self.conductor_topic = conductor_topic
self.status_topic = status_topic
self.conductor_topic_handler = self.build_topic_handler(
conductor_topic)
self.status_topic_handler = self.build_topic_handler(status_topic)
self._conductor_client = None
self._status_client = None
@property
def conductor_client(self):
if self._conductor_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.conductor_topic,
version=self.API_VERSION,
)
self._conductor_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._conductor_client
@conductor_client.setter
def conductor_client(self, c):
self.conductor_client = c
@property
def status_client(self):
if self._status_client is None:
transport = om.get_transport(CONF)
target = om.Target(
topic=self.status_topic,
version=self.API_VERSION,
)
self._status_client = om.RPCClient(
transport, target, serializer=self.serializer)
return self._status_client
@status_client.setter
def status_client(self, c):
self.status_client = c
def build_topic_handler(self, topic_name):
return messaging_handler.MessagingHandler(
self.publisher_id, topic_name, self,
self.api_version, self.serializer)
def connect(self):
LOG.debug("Connecting to '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.topic_control.start()
self.topic_status.start()
self.conductor_topic_handler.start()
self.status_topic_handler.start()
def disconnect(self):
LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend)
self.topic_control.stop()
self.topic_status.stop()
self.conductor_topic_handler.stop()
self.status_topic_handler.stop()
def publish_control(self, event, payload):
return self.topic_control.publish_event(event, payload)
return self.conductor_topic_handler.publish_event(event, payload)
def publish_status(self, event, payload, request_id=None):
return self.topic_status.publish_event(event, payload, request_id)
return self.status_topic_handler.publish_event(
event, payload, request_id)
def get_version(self):
return self.api_version
def check_api_version(self, context):
api_manager_version = self.client.call(
api_manager_version = self.conductor_client.call(
context.to_dict(), 'check_api_version',
api_version=self.api_version)
return api_manager_version

View File

@@ -38,11 +38,11 @@ CONF = cfg.CONF
class MessagingHandler(threading.Thread):
def __init__(self, publisher_id, topic_watcher, endpoint, version,
def __init__(self, publisher_id, topic_name, endpoint, version,
serializer=None):
super(MessagingHandler, self).__init__()
self.publisher_id = publisher_id
self.topic_watcher = topic_watcher
self.topic_name = topic_name
self.__endpoints = []
self.__serializer = serializer
self.__version = version
@@ -72,7 +72,7 @@ class MessagingHandler(threading.Thread):
return om.Notifier(
self.__transport,
publisher_id=self.publisher_id,
topic=self.topic_watcher,
topic=self.topic_name,
serializer=serializer
)
@@ -87,7 +87,7 @@ class MessagingHandler(threading.Thread):
self.__notifier = self.build_notifier()
if len(self.__endpoints):
target = om.Target(
topic=self.topic_watcher,
topic=self.topic_name,
# For compatibility, we can override it with 'host' opt
server=CONF.host or socket.getfqdn(),
version=self.__version,
@@ -101,7 +101,7 @@ class MessagingHandler(threading.Thread):
LOG.error(_LE("Messaging configuration error"))
def run(self):
LOG.debug("configure MessagingHandler for %s" % self.topic_watcher)
LOG.debug("configure MessagingHandler for %s" % self.topic_name)
self._configure()
if len(self.__endpoints) > 0:
LOG.debug("Starting up server")

View File

@@ -18,17 +18,16 @@ import eventlet
from oslo_log import log
import oslo_messaging as messaging
from watcher.common.messaging.utils.observable import \
Observable
from watcher.common.messaging.utils import observable
eventlet.monkey_patch()
LOG = log.getLogger(__name__)
class NotificationHandler(Observable):
class NotificationHandler(observable.Observable):
def __init__(self, publisher_id):
Observable.__init__(self)
super(NotificationHandler, self).__init__()
self.publisher_id = publisher_id
def info(self, ctx, publisher_id, event_type, payload, metadata):

View File

@@ -14,19 +14,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_log import log
from watcher.common.messaging.utils.synchronization import \
Synchronization
LOG = log.getLogger(__name__)
from watcher.common.messaging.utils import synchronization
class Observable(Synchronization):
class Observable(synchronization.Synchronization):
def __init__(self):
super(Observable, self).__init__()
self.__observers = []
self.changed = 0
Synchronization.__init__(self)
def set_changed(self):
self.changed = 1

View File

@@ -23,29 +23,22 @@ import time
from oslo_log import log
import cinderclient.exceptions as ciexceptions
import cinderclient.v2.client as ciclient
import glanceclient.v2.client as glclient
import neutronclient.neutron.client as netclient
import novaclient.client as nvclient
import novaclient.exceptions as nvexceptions
from watcher.common import keystone
from watcher.common import clients
LOG = log.getLogger(__name__)
class NovaClient(object):
NOVA_CLIENT_API_VERSION = "2"
class NovaHelper(object):
def __init__(self, creds, session):
self.user = creds['username']
self.session = session
self.neutron = None
self.cinder = None
self.nova = nvclient.Client(self.NOVA_CLIENT_API_VERSION,
session=session)
self.keystone = keystone.KeystoneClient().get_ksclient(creds)
self.glance = None
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
self.osc = osc if osc else clients.OpenStackClients()
self.neutron = self.osc.neutron()
self.cinder = self.osc.cinder()
self.nova = self.osc.nova()
self.glance = self.osc.glance()
def get_hypervisors_list(self):
return self.nova.hypervisors.list()
@@ -180,9 +173,6 @@ class NovaClient(object):
volume_id = attached_volume['id']
try:
if self.cinder is None:
self.cinder = ciclient.Client('2',
session=self.session)
volume = self.cinder.volumes.get(volume_id)
attachments_list = getattr(volume, "attachments")
@@ -275,58 +265,6 @@ class NovaClient(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
@@ -446,13 +384,6 @@ class NovaClient(object):
:param metadata: a dictionary containing the list of
key-value pairs to associate to the image as metadata.
"""
if self.glance is None:
glance_endpoint = self.keystone. \
service_catalog.url_for(service_type='image',
endpoint_type='publicURL')
self.glance = glclient.Client(glance_endpoint,
token=self.keystone.auth_token)
LOG.debug(
"Trying to create an image from instance %s ..." % instance_id)
@@ -676,10 +607,6 @@ class NovaClient(object):
def get_network_id_from_name(self, net_name="private"):
"""This method returns the unique id of the provided network name"""
if self.neutron is None:
self.neutron = netclient.Client('2.0', session=self.session)
self.neutron.format = 'json'
networks = self.neutron.list_networks(name=net_name)
# LOG.debug(networks)

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

@@ -71,7 +71,7 @@ class BaseConnection(object):
'name': 'example',
'description': 'free text description'
'host_aggregate': 'nova aggregate name or id'
'goal': 'SERVER_CONSOLiDATION'
'goal': 'DUMMY'
'extra': {'automatic': True}
}
:returns: An audit template.
@@ -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
@@ -122,7 +122,7 @@ class BaseConnection(object):
:param audit_template_id: The id or uuid of an audit template.
:returns: An audit template.
:raises: AuditTemplateNotFound
:raises: InvalidParameterValue
:raises: Invalid
"""
@abc.abstractmethod
@@ -209,7 +209,7 @@ class BaseConnection(object):
:param audit_id: The id or uuid of an audit.
:returns: An audit.
:raises: AuditNotFound
:raises: InvalidParameterValue
:raises: Invalid
"""
def soft_delete_audit(self, audit_id):
@@ -299,6 +299,7 @@ class BaseConnection(object):
:returns: A action.
:raises: ActionNotFound
:raises: ActionReferenced
:raises: Invalid
"""
@abc.abstractmethod
@@ -371,4 +372,5 @@ class BaseConnection(object):
:returns: An action plan.
:raises: ActionPlanNotFound
:raises: ActionPlanReferenced
:raises: Invalid
"""

View File

@@ -274,8 +274,9 @@ class Connection(api.BaseConnection):
def update_audit_template(self, audit_template_id, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing AuditTemplate.")
raise exception.InvalidParameterValue(err=msg)
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing "
"Audit Template."))
return self._do_update_audit_template(audit_template_id, values)
@@ -383,8 +384,9 @@ class Connection(api.BaseConnection):
def update_audit(self, audit_id, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Audit.")
raise exception.InvalidParameterValue(err=msg)
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing "
"Audit."))
return self._do_update_audit(audit_id, values)
@@ -474,8 +476,9 @@ class Connection(api.BaseConnection):
def update_action(self, action_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Action.")
raise exception.InvalidParameterValue(err=msg)
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing "
"Action."))
return self._do_update_action(action_id, values)
@@ -583,8 +586,9 @@ class Connection(api.BaseConnection):
def update_action_plan(self, action_plan_id, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Audit.")
raise exception.InvalidParameterValue(err=msg)
raise exception.Invalid(
message=_("Cannot overwrite UUID for an existing "
"Action Plan."))
return self._do_update_action_plan(action_plan_id, values)

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

@@ -54,8 +54,8 @@ class DefaultAuditHandler(base.BaseAuditHandler):
event.data = {}
payload = {'audit_uuid': audit_uuid,
'audit_status': status}
self.messaging.topic_status.publish_event(event.type.name,
payload)
self.messaging.status_topic_handler.publish_event(
event.type.name, payload)
def update_audit_state(self, request_context, audit_uuid, state):
LOG.debug("Update audit state: %s", state)

View File

@@ -40,20 +40,20 @@ See :doc:`../architecture` for more details on this component.
from oslo_config import cfg
from oslo_log import log
from watcher.common.messaging.messaging_core import MessagingCore
from watcher.decision_engine.messaging.audit_endpoint import AuditEndpoint
from watcher.common.messaging import messaging_core
from watcher.decision_engine.messaging import audit_endpoint
LOG = log.getLogger(__name__)
CONF = cfg.CONF
WATCHER_DECISION_ENGINE_OPTS = [
cfg.StrOpt('topic_control',
cfg.StrOpt('conductor_topic',
default='watcher.decision.control',
help='The topic name used for'
'control events, this topic '
'used for rpc call '),
cfg.StrOpt('topic_status',
cfg.StrOpt('status_topic',
default='watcher.decision.status',
help='The topic name used for '
'status events, this topic '
@@ -78,18 +78,18 @@ CONF.register_group(decision_engine_opt_group)
CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
class DecisionEngineManager(MessagingCore):
class DecisionEngineManager(messaging_core.MessagingCore):
def __init__(self):
super(DecisionEngineManager, self).__init__(
CONF.watcher_decision_engine.publisher_id,
CONF.watcher_decision_engine.topic_control,
CONF.watcher_decision_engine.topic_status,
CONF.watcher_decision_engine.conductor_topic,
CONF.watcher_decision_engine.status_topic,
api_version=self.API_VERSION)
endpoint = AuditEndpoint(self,
max_workers=CONF.watcher_decision_engine.
max_workers)
self.topic_control.add_endpoint(endpoint)
endpoint = audit_endpoint.AuditEndpoint(
self,
max_workers=CONF.watcher_decision_engine.max_workers)
self.conductor_topic_handler.add_endpoint(endpoint)
def join(self):
self.topic_control.join()
self.topic_status.join()
self.conductor_topic_handler.join()
self.status_topic_handler.join()

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

@@ -50,11 +50,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

@@ -19,10 +19,10 @@
from oslo_config import cfg
from oslo_log import log
import oslo_messaging as om
from watcher.common import exception
from watcher.common.messaging.messaging_core import MessagingCore
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,27 +35,22 @@ 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__(
CONF.watcher_decision_engine.publisher_id,
CONF.watcher_decision_engine.topic_control,
CONF.watcher_decision_engine.topic_status,
CONF.watcher_decision_engine.conductor_topic,
CONF.watcher_decision_engine.status_topic,
api_version=self.API_VERSION,
)
transport = om.get_transport(CONF)
target = om.Target(
topic=CONF.watcher_decision_engine.topic_control,
version=self.API_VERSION,
)
self.client = om.RPCClient(transport, target,
serializer=self.serializer)
self.handler = notification_handler.NotificationHandler(
self.publisher_id)
self.status_topic_handler.add_endpoint(self.handler)
def trigger_audit(self, context, audit_uuid=None):
if not utils.is_uuid_like(audit_uuid):
raise exception.InvalidUuidOrName(name=audit_uuid)
return self.client.call(
return self.conductor_client.call(
context.to_dict(), 'trigger_audit', audit_uuid=audit_uuid)

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

@@ -15,22 +15,22 @@
# limitations under the License.
from oslo_log import log
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.common import clients
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):
@@ -47,15 +47,17 @@ class DefaultStrategyContext(BaseStrategyContext):
audit_template = objects.\
AuditTemplate.get_by_id(request_context, audit.audit_template_id)
osc = clients.OpenStackClients()
# todo(jed) retrieve in audit_template parameters (threshold,...)
# todo(jed) create ActionPlan
collector_manager = self.collector.get_cluster_model_collector()
collector_manager = self.collector.get_cluster_model_collector(osc=osc)
# todo(jed) remove call to get_latest_cluster_data_model
cluster_data_model = collector_manager.get_latest_cluster_data_model()
selected_strategy = self.strategy_selector. \
define_from_goal(audit_template.goal)
selected_strategy = self.strategy_selector.define_from_goal(
audit_template.goal, osc=osc)
# todo(jed) add parameters and remove cluster_data_model
return selected_strategy.execute(cluster_data_model)

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

@@ -48,11 +48,12 @@ class DefaultStrategySelector(base.BaseSelector):
super(DefaultStrategySelector, self).__init__()
self.strategy_loader = default.DefaultStrategyLoader()
def define_from_goal(self, goal_name):
def define_from_goal(self, goal_name, osc=None):
""":param osc: an OpenStackClients instance"""
strategy_to_load = None
try:
strategy_to_load = CONF.watcher_goals.goals[goal_name]
return self.strategy_loader.load(strategy_to_load)
return self.strategy_loader.load(strategy_to_load, osc=osc)
except KeyError as exc:
LOG.exception(exc)
raise exception.WatcherException(

View File

@@ -33,12 +33,13 @@ provided as well.
"""
import abc
from oslo_log import log
import six
from watcher.decision_engine.solution.default import DefaultSolution
from watcher.decision_engine.strategy.common.level import StrategyLevel
from watcher.common import clients
from watcher.decision_engine.solution import default
from watcher.decision_engine.strategy.common import level
LOG = log.getLogger(__name__)
@@ -51,25 +52,33 @@ class BaseStrategy(object):
Solution for a given Goal.
"""
def __init__(self, name=None, description=None):
def __init__(self, name=None, description=None, osc=None):
""":param osc: an OpenStackClients instance"""
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`
"""
@property
def osc(self):
if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@property
def solution(self):
return self._solution

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"
@@ -41,35 +74,17 @@ class BasicConsolidation(BaseStrategy):
MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
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 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)
super(BasicConsolidation, self).__init__(name, description, osc)
# set default value for the number of released nodes
self.number_of_released_nodes = 0
@@ -102,12 +117,13 @@ class BasicConsolidation(BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory()
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
@@ -116,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
@@ -139,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,
@@ -170,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
@@ -212,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))
@@ -256,25 +277,25 @@ class BasicConsolidation(BaseStrategy):
:return:
"""
resource_id = "%s_%s" % (hypervisor.uuid, hypervisor.hostname)
cpu_avg_vm = self.ceilometer. \
vm_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 vm_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
vm_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 * (vm_avg_cpu_util / 100)
return self.calculate_weight(model, hypervisor, total_cores_used,
0,
@@ -292,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. \
@@ -318,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):
@@ -342,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
@@ -360,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)))
@@ -388,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()
@@ -444,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:
@@ -471,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,33 +18,52 @@
#
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"
NOP = "nop"
SLEEP = "sleep"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
super(DummyStrategy, self).__init__(name, description)
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
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"
@@ -40,35 +80,15 @@ class OutletTempControl(BaseStrategy):
MIGRATION = "migrate"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
"""[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.
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
osc=None):
"""Outlet temperature control using live migration
:param name: the name of the strategy
:param description: a description of the strategy
:param osc: an OpenStackClients object
"""
super(OutletTempControl, self).__init__(name, description)
super(OutletTempControl, self).__init__(name, description, osc)
# the migration plan will be triggered when the outlet temperature
# reaches threshold
# TODO(zhenzanz): Threshold should be configurable for each audit
@@ -79,32 +99,33 @@ class OutletTempControl(BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory()
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()
@@ -112,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(
@@ -134,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
@@ -156,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)
@@ -237,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

@@ -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-19 17:54+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,25 +24,29 @@ msgstr ""
msgid "Invalid state: %(state)s"
msgstr "État invalide : %(state)s"
#: watcher/api/controllers/v1/action_plan.py:418
#: 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)"
#: watcher/api/controllers/v1/audit.py:359
msgid "The audit template UUID or name specified is invalid"
msgstr "Le nom ou UUID de l'audit template est invalide"
#: watcher/api/controllers/v1/types.py:148
#, python-format
msgid "%s is not JSON serializable"
msgstr ""
msgstr "%s n'est pas sérialisable en JSON"
#: watcher/api/controllers/v1/types.py:184
#, python-format
msgid "Wrong type. Expected '%(type)s', got '%(value)s'"
msgstr ""
msgstr "Type incorrect. '%(type)s' attendu, '%(value)s' obtenu"
#: watcher/api/controllers/v1/types.py:223
#, python-format
msgid "'%s' is an internal attribute and can not be updated"
msgstr ""
msgstr "'%s' wat un attribut interne et ne peut pas être modifié"
#: watcher/api/controllers/v1/types.py:227
#, python-format
@@ -60,7 +64,7 @@ msgstr "Limit doit être positif"
#: watcher/api/controllers/v1/utils.py:47
#, python-format
msgid "Invalid sort direction: %s. Acceptable values are 'asc' or 'desc'"
msgstr ""
msgstr "Ordre de tri invalide : %s. Les valeurs acceptées sont 'asc' or 'desc'"
#: watcher/api/controllers/v1/utils.py:57
#, python-format
@@ -69,33 +73,42 @@ msgstr ""
#: watcher/api/middleware/auth_token.py:45
msgid "Cannot compile public API routes"
msgstr ""
msgstr "Ne peut pas compiler les chemins d'API publique"
#: watcher/api/middleware/parsable_error.py:52
#, python-format
msgid "ErrorDocumentMiddleware received an invalid status %s"
msgstr ""
#: watcher/api/middleware/parsable_error.py:80
#: watcher/api/middleware/parsable_error.py:79
#, python-format
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:69
#: 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:77
#: 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:83
#: 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é"
@@ -115,191 +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:56
#: 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:77
#: watcher/common/exception.py:92
msgid "Exception in string format operation"
msgstr ""
#: watcher/common/exception.py:107
#: watcher/common/exception.py:122
msgid "Not authorized"
msgstr ""
#: watcher/common/exception.py:112
#: watcher/common/exception.py:127
msgid "Operation not permitted"
msgstr ""
#: watcher/common/exception.py:116
#: watcher/common/exception.py:131
msgid "Unacceptable parameters"
msgstr ""
#: watcher/common/exception.py:121
#: watcher/common/exception.py:136
#, python-format
msgid "The %(name)s %(id)s could not be found"
msgstr ""
#: watcher/common/exception.py:125
#: watcher/common/exception.py:140
#, fuzzy
msgid "Conflict"
msgstr "Conflit."
msgstr "Conflit"
#: watcher/common/exception.py:130
#: watcher/common/exception.py:145
#, python-format
msgid "The %(name)s resource %(id)s could not be found"
msgstr ""
msgstr "La ressource %(name)s / %(id)s est introuvable"
#: watcher/common/exception.py:135
#: watcher/common/exception.py:150
#, python-format
msgid "Expected an uuid or int but received %(identity)s"
msgstr ""
#: watcher/common/exception.py:139
#: watcher/common/exception.py:154
#, python-format
msgid "Goal %(goal)s is not defined in Watcher configuration file"
msgstr ""
#: watcher/common/exception.py:145
#, python-format
msgid "%(err)s"
msgstr ""
#: watcher/common/exception.py:149
#: watcher/common/exception.py:158
#, python-format
msgid "Expected a uuid but received %(uuid)s"
msgstr ""
#: watcher/common/exception.py:153
#: watcher/common/exception.py:162
#, python-format
msgid "Expected a logical name but received %(name)s"
msgstr ""
#: watcher/common/exception.py:157
#: watcher/common/exception.py:166
#, python-format
msgid "Expected a logical name or uuid but received %(name)s"
msgstr ""
#: watcher/common/exception.py:161
#: watcher/common/exception.py:170
#, python-format
msgid "AuditTemplate %(audit_template)s could not be found"
msgstr ""
#: watcher/common/exception.py:165
#: 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:170
#: watcher/common/exception.py:179
#, python-format
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
msgstr ""
#: watcher/common/exception.py:175
#: watcher/common/exception.py:184
#, python-format
msgid "Audit %(audit)s could not be found"
msgstr ""
#: watcher/common/exception.py:179
#: watcher/common/exception.py:188
#, python-format
msgid "An audit with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:183
#: watcher/common/exception.py:192
#, python-format
msgid "Audit %(audit)s is referenced by one or multiple action plans"
msgstr ""
#: watcher/common/exception.py:188
msgid "ActionPlan %(action plan)s could not be found"
msgstr ""
#: watcher/common/exception.py:192
#: watcher/common/exception.py:197
#, python-format
msgid "An action plan with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:196
#, python-format
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
msgid "ActionPlan %(action_plan)s could not be found"
msgstr ""
#: watcher/common/exception.py:201
#, python-format
msgid "Action %(action)s could not be found"
msgid "An action plan with UUID %(uuid)s already exists"
msgstr ""
#: 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:210
#, python-format
msgid "Action %(action)s could not be found"
msgstr ""
#: watcher/common/exception.py:214
#, python-format
msgid "An action with UUID %(uuid)s already exists"
msgstr ""
#: watcher/common/exception.py:209
#: 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:214
#: watcher/common/exception.py:223
msgid "Filtering actions on both audit and action-plan is prohibited"
msgstr ""
#: watcher/common/exception.py:223
#: watcher/common/exception.py:232
#, python-format
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
msgstr ""
#: watcher/common/exception.py:233
msgid "Description must be an instance of str"
#: watcher/common/exception.py:239
msgid "Illegal argument"
msgstr ""
#: watcher/common/exception.py:243
msgid "An exception occurred without a description"
msgstr ""
#: watcher/common/exception.py:251
msgid "Description cannot be empty"
msgstr ""
#: watcher/common/exception.py:260
msgid "No such metric"
msgstr ""
#: watcher/common/exception.py:269
#: watcher/common/exception.py:247
msgid "No rows were returned"
msgstr ""
#: watcher/common/exception.py:277
#: 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:281
#: watcher/common/exception.py:259
msgid "The list of hypervisor(s) in the cluster is empty"
msgstr ""
#: watcher/common/exception.py:285
#: watcher/common/exception.py:263
msgid "The metrics resource collector is not defined"
msgstr ""
#: watcher/common/exception.py:289
#: watcher/common/exception.py:267
msgid "the cluster state is not defined"
msgstr ""
#: watcher/common/exception.py:295
#: 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:299
#: watcher/common/exception.py:277
msgid "The hypervisor is not found"
msgstr ""
#: watcher/common/exception.py:303
#: 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
@@ -332,7 +364,7 @@ msgstr ""
#: watcher/common/utils.py:53
#, python-format
msgid ""
"Failed to remove trailing character. Returning original object. Supplied "
"Failed to remove trailing character. Returning original object.Supplied "
"object is not a string: %s,"
msgstr ""
@@ -350,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() "
@@ -373,44 +409,44 @@ msgstr ""
msgid "'obj' argument type is not valid"
msgstr ""
#: watcher/decision_engine/planner/default.py:75
#: 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:349
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:426
msgid "Initializing Sercon Consolidation"
msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:406
#: 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 ""
@@ -573,3 +609,32 @@ msgstr ""
#~ msgid "The WorkFlow Engine has failedto execute the action %s"
#~ msgstr ""
#~ msgid "ActionPlan %(action plan)s could not be found"
#~ msgstr ""
#~ msgid "Description must be an instance of str"
#~ msgstr ""
#~ msgid "An exception occurred without a description"
#~ msgstr ""
#~ msgid "Description cannot be empty"
#~ msgstr ""
#~ 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,9 +7,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: python-watcher 0.22.1.dev49\n"
"Project-Id-Version: python-watcher 0.23.3.dev2\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-01-22 10:43+0100\n"
"POT-Creation-Date: 2016-02-09 09:07+0100\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"
@@ -23,7 +23,7 @@ msgstr ""
msgid "Invalid state: %(state)s"
msgstr ""
#: watcher/api/controllers/v1/action_plan.py:416
#: watcher/api/controllers/v1/action_plan.py:422
#, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr ""
@@ -84,21 +84,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 ""
#: 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 ""
#: watcher/applier/workflow_engine/default.py:144
#: watcher/applier/workflow_engine/default.py:146
#, python-format
msgid "Revert action %s"
msgstr ""
#: watcher/applier/workflow_engine/default.py:150
#: watcher/applier/workflow_engine/default.py:152
msgid "Oops! We need disaster recover plan"
msgstr ""
@@ -118,182 +127,209 @@ 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:139
#, python-format
msgid "%(err)s"
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
msgid "ActionPlan %(action plan)s could not be found"
#: 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 ""
#: 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
#, python-format
msgid "Error loading plugin '%(name)s'"
msgstr ""
#: watcher/common/keystone.py:59
msgid "No Keystone service catalog loaded"
#: watcher/common/exception.py:285
#, python-format
msgid "The identifier '%(name)s' is a reserved word"
msgstr ""
#: watcher/common/service.py:83
@@ -344,18 +380,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() "
@@ -367,44 +407,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 ""

View File

@@ -20,17 +20,19 @@
from oslo_config import cfg
from oslo_log import log
from watcher.common.ceilometer import CeilometerClient
from watcher.common import ceilometer_helper
from watcher.metrics_engine.cluster_history.api import BaseClusterHistory
from watcher.metrics_engine.cluster_history import api
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class CeilometerClusterHistory(BaseClusterHistory):
def __init__(self):
self.ceilometer = CeilometerClient()
class CeilometerClusterHistory(api.BaseClusterHistory):
def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
super(CeilometerClusterHistory, self).__init__()
self.ceilometer = ceilometer_helper.CeilometerHelper(osc=osc)
def statistic_list(self, meter_name, query=None, period=None):
return self.ceilometer.statistic_list(meter_name, query, period)

View File

@@ -20,18 +20,16 @@
from oslo_config import cfg
from oslo_log import log
from watcher.common.keystone import KeystoneClient
from watcher.common.nova import NovaClient
from watcher.metrics_engine.cluster_model_collector.nova import \
NovaClusterModelCollector
from watcher.common import nova_helper
from watcher.metrics_engine.cluster_model_collector import nova as cnova
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class CollectorManager(object):
def get_cluster_model_collector(self):
keystone = KeystoneClient()
wrapper = NovaClient(keystone.get_credentials(),
session=keystone.get_session())
return NovaClusterModelCollector(wrapper=wrapper)
def get_cluster_model_collector(self, osc=None):
""":param osc: an OpenStackClients instance"""
nova = nova_helper.NovaHelper(osc=osc)
return cnova.NovaClusterModelCollector(nova)

View File

@@ -17,32 +17,29 @@
# 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 api
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class NovaClusterModelCollector(BaseClusterModelCollector):
class NovaClusterModelCollector(api.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)
cluster.create_resource(mem)
cluster.create_resource(num_cores)
cluster.create_resource(disk)
@@ -52,7 +49,7 @@ class NovaClusterModelCollector(BaseClusterModelCollector):
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
@@ -65,7 +62,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

@@ -164,7 +164,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 +174,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 +184,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,

View File

@@ -117,7 +117,7 @@ def dt_serializer(name):
return serializer
def dt_deserializer(instance, val):
def dt_deserializer(val):
"""A deserializer method for datetime attributes."""
if val is None:
return None

View File

@@ -15,8 +15,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from keystoneauth1 import loading as ka_loading
import watcher.api.app
from watcher.applier import manager as applier_manager
from watcher.common import clients
from watcher.decision_engine import manager as decision_engine_manger
from watcher.decision_engine.planner import manager as planner_manager
from watcher.decision_engine.strategy.selection import default \
@@ -29,7 +32,15 @@ def list_opts():
('watcher_goals', strategy_selector.WATCHER_GOALS_OPTS),
('watcher_decision_engine',
decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS),
('watcher_applier',
applier_manager.APPLIER_MANAGER_OPTS),
('watcher_planner', planner_manager.WATCHER_PLANNER_OPTS)
('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
('watcher_planner', planner_manager.WATCHER_PLANNER_OPTS),
('nova_client', clients.NOVA_CLIENT_OPTS),
('glance_client', clients.GLANCE_CLIENT_OPTS),
('cinder_client', clients.CINDER_CLIENT_OPTS),
('ceilometer_client', clients.CEILOMETER_CLIENT_OPTS),
('neutron_client', clients.NEUTRON_CLIENT_OPTS),
('watcher_clients_auth',
(ka_loading.get_auth_common_conf_options() +
ka_loading.get_auth_plugin_conf_options('password') +
ka_loading.get_session_conf_options()))
]

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

@@ -285,33 +285,41 @@ 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['uuid'] for act in response['actions']
if act['action_plan_uuid'] == action_plan2.uuid]),
set([act.as_dict()['uuid'] for act in ap2_action_list]))
def test_many_with_next_uuid(self):
action_list = []

View File

@@ -35,6 +35,7 @@ class TestActionPlanObject(base.TestCase):
act_plan_dict = api_utils.action_plan_post_data()
del act_plan_dict['state']
del act_plan_dict['audit_id']
del act_plan_dict['first_action_id']
act_plan = api_action_plan.ActionPlan(**act_plan_dict)
self.assertEqual(wtypes.Unset, act_plan.state)
@@ -69,12 +70,20 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json('/action_plans')
self.assertEqual([], response['action_plans'])
def test_get_one(self):
def test_get_one_ok(self):
action_plan = obj_utils.create_action_plan_without_audit(self.context)
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
self.assertEqual(action_plan.uuid, response['uuid'])
self._assert_action_plans_fields(response)
def test_get_one_with_first_action(self):
action_plan = obj_utils.create_test_action_plan(self.context)
action = obj_utils.create_test_action(self.context, id=1)
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
self.assertEqual(action_plan.uuid, response['uuid'])
self.assertEqual(action.uuid, response['first_action_uuid'])
self._assert_action_plans_fields(response)
def test_get_one_soft_deleted(self):
action_plan = obj_utils.create_action_plan_without_audit(self.context)
action_plan.soft_delete()
@@ -298,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)
@@ -306,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(len(acts_response['actions']), 0)
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)
@@ -353,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)
@@ -403,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'])
@@ -420,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},
]
@@ -446,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
@@ -460,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)
@@ -484,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)
@@ -512,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):

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

@@ -15,45 +15,45 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from mock import call
from mock import MagicMock
from watcher.applier.action_plan.default import DefaultActionPlanHandler
from watcher.applier.messaging.event_types import EventTypes
import mock
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, 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 = MagicMock()
command = DefaultActionPlanHandler(self.context, messaging,
self.action_plan.uuid)
messaging = mock.MagicMock()
command = default.DefaultActionPlanHandler(self.context, messaging,
self.action_plan.uuid)
command.execute()
call_on_going = 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 = 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})
calls = [call_on_going, call_succeeded]
messaging.topic_status.publish_event.assert_has_calls(calls)
self.assertEqual(2, messaging.topic_status.publish_event.call_count)
messaging.status_topic_handler.publish_event.assert_has_calls(calls)
self.assertEqual(
2, messaging.status_topic_handler.publish_event.call_count)

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(exc.kwargs["name"], self.INSTANCE_UUID)
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)

View File

@@ -32,6 +32,12 @@ from watcher.tests.db import base
@six.add_metaclass(abc.ABCMeta)
class FakeAction(abase.BaseAction):
def schema(self):
pass
def postcondition(self):
pass
def precondition(self):
pass
@@ -53,21 +59,20 @@ class FakeAction(abase.BaseAction):
class TestDefaultWorkFlowEngine(base.DbTestCase):
def setUp(self):
super(TestDefaultWorkFlowEngine, self).setUp()
self.engine = tflow.DefaultWorkFlowEngine()
self.engine.context = self.context
self.engine.applier_manager = mock.MagicMock()
self.engine = tflow.DefaultWorkFlowEngine(
context=self.context,
applier_manager=mock.MagicMock())
def test_execute(self):
actions = mock.MagicMock()
result = self.engine.execute(actions)
self.assertEqual(result, True)
def create_action(self, action_type, applies_to, parameters, next):
def create_action(self, action_type, parameters, next):
action = {
'uuid': utils.generate_uuid(),
'action_plan_id': 0,
'action_type': action_type,
'applies_to': applies_to,
'input_parameters': parameters,
'state': objects.action.State.PENDING,
'alarm': None,
@@ -92,15 +97,15 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
self.assertEqual(result, True)
def test_execute_with_one_action(self):
actions = [self.create_action("nop", "", {'message': 'test'}, None)]
actions = [self.create_action("nop", {'message': 'test'}, None)]
result = self.engine.execute(actions)
self.assertEqual(result, True)
self.check_actions_state(actions, objects.action.State.SUCCEEDED)
def test_execute_with_two_actions(self):
actions = []
next = self.create_action("sleep", "", {'duration': '0'}, None)
first = self.create_action("nop", "", {'message': 'test'}, next.id)
next = self.create_action("sleep", {'duration': 0.0}, None)
first = self.create_action("nop", {'message': 'test'}, next.id)
actions.append(first)
actions.append(next)
@@ -111,9 +116,9 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
def test_execute_with_three_actions(self):
actions = []
next2 = self.create_action("nop", "vm1", {'message': 'next'}, None)
next = self.create_action("sleep", "vm1", {'duration': '0'}, next2.id)
first = self.create_action("nop", "vm1", {'message': 'hello'}, next.id)
next2 = self.create_action("nop", {'message': 'next'}, None)
next = self.create_action("sleep", {'duration': 0.0}, next2.id)
first = self.create_action("nop", {'message': 'hello'}, next.id)
self.check_action_state(first, objects.action.State.PENDING)
self.check_action_state(next, objects.action.State.PENDING)
self.check_action_state(next2, objects.action.State.PENDING)
@@ -128,12 +133,9 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
def test_execute_with_exception(self):
actions = []
next2 = self.create_action("no_exist",
"vm1", {'message': 'next'}, None)
next = self.create_action("sleep", "vm1",
{'duration': '0'}, next2.id)
first = self.create_action("nop", "vm1",
{'message': 'hello'}, next.id)
next2 = self.create_action("no_exist", {'message': 'next'}, None)
next = self.create_action("sleep", {'duration': 0.0}, next2.id)
first = self.create_action("nop", {'message': 'hello'}, next.id)
self.check_action_state(first, objects.action.State.PENDING)
self.check_action_state(next, objects.action.State.PENDING)
@@ -158,7 +160,7 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
plugin=FakeAction,
obj=None),
namespace=FakeAction.namespace())
actions = [self.create_action("dontcare", "vm1", {}, None)]
actions = [self.create_action("dontcare", {}, None)]
result = self.engine.execute(actions)
self.assertEqual(result, False)
self.check_action_state(actions[0], objects.action.State.FAILED)

View File

@@ -15,59 +15,79 @@
# limitations under the License.
from mock import patch
import mock
from watcher.common.messaging.messaging_core import MessagingCore
from watcher.common.messaging.messaging_handler import MessagingHandler
from watcher.common.rpc import RequestContextSerializer
from watcher.tests.base import TestCase
from watcher.common.messaging import messaging_core
from watcher.common.messaging import messaging_handler
from watcher.common import rpc
from watcher.tests import base
class TestMessagingCore(TestCase):
class TestMessagingCore(base.TestCase):
def setUp(self):
super(TestMessagingCore, self).setUp()
def test_build_topic(self):
@mock.patch.object(messaging_handler, "MessagingHandler")
def test_connect(self, m_handler):
messaging = messaging_core.MessagingCore("", "", "")
messaging.connect()
self.assertEqual(m_handler.call_count, 2)
@mock.patch.object(messaging_handler, "MessagingHandler")
def test_disconnect(self, m_handler):
messaging = messaging_core.MessagingCore("", "", "")
messaging.disconnect()
self.assertEqual(m_handler.call_count, 2)
def test_build_topic_handler(self):
topic_name = "MyTopic"
messaging = MessagingCore("", "", "")
messaging_handler = messaging.build_topic(topic_name)
self.assertIsNotNone(messaging_handler)
messaging = messaging_core.MessagingCore("", "", "")
handler = messaging.build_topic_handler(topic_name)
self.assertIsNotNone(handler)
def test_init_messaging_core(self):
messaging = MessagingCore("", "", "")
messaging = messaging_core.MessagingCore("", "", "")
self.assertIsInstance(messaging.serializer,
RequestContextSerializer)
self.assertIsInstance(messaging.topic_control, MessagingHandler)
self.assertIsInstance(messaging.topic_status, MessagingHandler)
rpc.RequestContextSerializer)
self.assertIsInstance(
messaging.conductor_topic_handler,
messaging_handler.MessagingHandler)
self.assertIsInstance(
messaging.status_topic_handler,
messaging_handler.MessagingHandler)
@patch.object(MessagingCore, 'publish_control')
def test_publish_control(self, mock_call):
@mock.patch.object(messaging_handler, "MessagingHandler")
def test_publish_control(self, m_handler_cls):
m_handler = mock.Mock()
m_handler_cls.return_value = m_handler
payload = {
"name": "value",
}
event = "MyEvent"
messaging = MessagingCore("", "", "")
messaging = messaging_core.MessagingCore("", "", "")
messaging.publish_control(event, payload)
mock_call.assert_called_once_with(event, payload)
m_handler.publish_event.assert_called_once_with(event, payload)
@patch.object(MessagingCore, 'publish_status')
def test_publish_status(self, mock_call):
@mock.patch.object(messaging_handler, "MessagingHandler")
def test_publish_status(self, m_handler_cls):
m_handler = mock.Mock()
m_handler_cls.return_value = m_handler
payload = {
"name": "value",
}
event = "MyEvent"
messaging = MessagingCore("", "", "")
messaging = messaging_core.MessagingCore("", "", "")
messaging.publish_status(event, payload)
mock_call.assert_called_once_with(event, payload)
m_handler.publish_event.assert_called_once_with(event, payload, None)
@patch.object(MessagingCore, 'publish_status')
@mock.patch.object(messaging_core.MessagingCore, 'publish_status')
def test_response(self, mock_call):
event = "My event"
context = {'request_id': 12}
message = "My Message"
messaging = MessagingCore("", "", "")
messaging = messaging_core.MessagingCore("", "", "")
messaging.response(event, context, message)
expected_payload = {
@@ -76,13 +96,15 @@ class TestMessagingCore(TestCase):
}
mock_call.assert_called_once_with(event, expected_payload)
def test_messaging_build_topic(self):
messaging = MessagingCore("pub_id", "test_topic", "does not matter")
topic = messaging.build_topic("test_topic")
def test_messaging_build_topic_handler(self):
messaging = messaging_core.MessagingCore(
"pub_id", "test_topic", "does not matter")
topic = messaging.build_topic_handler("test_topic")
self.assertIsInstance(topic, MessagingHandler)
self.assertIsInstance(topic, messaging_handler.MessagingHandler)
self.assertEqual(messaging.publisher_id, "pub_id")
self.assertEqual(topic.publisher_id, "pub_id")
self.assertEqual(messaging.topic_control.topic_watcher, "test_topic")
self.assertEqual(topic.topic_watcher, "test_topic")
self.assertEqual(
messaging.conductor_topic_handler.topic_name, "test_topic")
self.assertEqual(topic.topic_name, "test_topic")

View File

@@ -14,17 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from mock import Mock
from mock import patch
import mock
from oslo_config import cfg
import oslo_messaging as messaging
from watcher.common.messaging.messaging_handler import MessagingHandler
from watcher.tests.base import TestCase
from watcher.common.messaging import messaging_handler
from watcher.tests import base
CONF = cfg.CONF
class TestMessagingHandler(TestCase):
class TestMessagingHandler(base.TestCase):
PUBLISHER_ID = 'TEST_API'
TOPIC_WATCHER = 'TEST_TOPIC_WATCHER'
@@ -35,20 +34,20 @@ class TestMessagingHandler(TestCase):
super(TestMessagingHandler, self).setUp()
CONF.set_default('host', 'fake-fqdn')
@patch.object(messaging, "get_rpc_server")
@patch.object(messaging, "Target")
@mock.patch.object(messaging, "get_rpc_server")
@mock.patch.object(messaging, "Target")
def test_setup_messaging_handler(self, m_target_cls, m_get_rpc_server):
m_target = Mock()
m_target = mock.Mock()
m_target_cls.return_value = m_target
messaging_handler = MessagingHandler(
handler = messaging_handler.MessagingHandler(
publisher_id=self.PUBLISHER_ID,
topic_watcher=self.TOPIC_WATCHER,
topic_name=self.TOPIC_WATCHER,
endpoint=self.ENDPOINT,
version=self.VERSION,
serializer=None,
)
messaging_handler.run()
handler.run()
m_target_cls.assert_called_once_with(
server="fake-fqdn",
@@ -56,23 +55,23 @@ class TestMessagingHandler(TestCase):
version="1.0",
)
m_get_rpc_server.assert_called_once_with(
messaging_handler.transport,
handler.transport,
m_target,
[self.ENDPOINT],
serializer=None,
)
def test_messaging_handler_remove_endpoint(self):
messaging_handler = MessagingHandler(
handler = messaging_handler.MessagingHandler(
publisher_id=self.PUBLISHER_ID,
topic_watcher=self.TOPIC_WATCHER,
topic_name=self.TOPIC_WATCHER,
endpoint=self.ENDPOINT,
version=self.VERSION,
serializer=None,
)
self.assertEqual(messaging_handler.endpoints, [self.ENDPOINT])
self.assertEqual(handler.endpoints, [self.ENDPOINT])
messaging_handler.remove_endpoint(self.ENDPOINT)
handler.remove_endpoint(self.ENDPOINT)
self.assertEqual(messaging_handler.endpoints, [])
self.assertEqual(handler.endpoints, [])

View File

@@ -1,99 +0,0 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import
from __future__ import unicode_literals
from mock import MagicMock
from mock import mock
from oslo_config import cfg
from watcher.common.ceilometer import CeilometerClient
from watcher.tests.base import BaseTestCase
CONF = cfg.CONF
class TestCeilometer(BaseTestCase):
def setUp(self):
super(TestCeilometer, self).setUp()
self.cm = CeilometerClient()
def test_build_query(self):
expected = [{'field': 'user_id', 'op': 'eq', 'value': u'user_id'},
{'field': 'project_id', 'op': 'eq', 'value': u'tenant_id'},
{'field': 'resource_id', 'op': 'eq',
'value': u'resource_id'}]
query = self.cm.build_query(user_id="user_id",
tenant_id="tenant_id",
resource_id="resource_id",
user_ids=["user_ids"],
tenant_ids=["tenant_ids"],
resource_ids=["resource_ids"])
self.assertEqual(query, expected)
@mock.patch('keystoneclient.v2_0.client.Client', autospec=True)
@mock.patch('ceilometerclient.v2.client.Client', autospec=True)
def test_get_ceilometer_v2(self, mock_keystone, mock_ceilometer):
cfg.CONF.set_override(
'auth_uri', "http://127.0.0.1:9898/v2", group="keystone_authtoken",
enforce_type=True
)
c = CeilometerClient(api_version='2')
from ceilometerclient.v2 import Client
self.assertIsInstance(c.cmclient, Client)
@mock.patch.object(CeilometerClient, "cmclient")
def test_statistic_aggregation(self, mock_keystone):
statistic = MagicMock()
expected_result = 100
statistic[-1]._info = {'aggregate': {'avg': expected_result}}
mock_keystone.statistics.list.return_value = statistic
val = self.cm.statistic_aggregation(
resource_id="VM_ID",
meter_name="cpu_util",
period="7300"
)
self.assertEqual(val, expected_result)
@mock.patch.object(CeilometerClient, "cmclient")
def test_get_last_sample(self, mock_keystone):
statistic = MagicMock()
expected_result = 100
statistic[-1]._info = {'counter_volume': expected_result}
mock_keystone.samples.list.return_value = statistic
val = self.cm.get_last_sample_value(
resource_id="id",
meter_name="compute.node.percent"
)
self.assertEqual(val, expected_result)
@mock.patch.object(CeilometerClient, "cmclient")
def test_get_last_sample_none(self, mock_keystone):
expected = []
mock_keystone.samples.list.return_value = expected
val = self.cm.get_last_sample_values(
resource_id="id",
meter_name="compute.node.percent"
)
self.assertEqual(val, expected)
@mock.patch.object(CeilometerClient, "cmclient")
def test_statistic_list(self, mock_keystone):
expected_value = []
mock_keystone.statistics.list.return_value = expected_value
val = self.cm.statistic_list(meter_name="cpu_util")
self.assertEqual(val, expected_value)

View File

@@ -0,0 +1,98 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import
from __future__ import unicode_literals
import mock
from oslo_config import cfg
from watcher.common import ceilometer_helper
from watcher.common import clients
from watcher.tests import base
CONF = cfg.CONF
@mock.patch.object(clients.OpenStackClients, 'ceilometer')
class TestCeilometerHelper(base.BaseTestCase):
def test_build_query(self, mock_ceilometer):
mock_ceilometer.return_value = mock.MagicMock()
cm = ceilometer_helper.CeilometerHelper()
expected = [{'field': 'user_id', 'op': 'eq', 'value': u'user_id'},
{'field': 'project_id', 'op': 'eq', 'value': u'tenant_id'},
{'field': 'resource_id', 'op': 'eq',
'value': u'resource_id'}]
query = cm.build_query(user_id="user_id",
tenant_id="tenant_id",
resource_id="resource_id",
user_ids=["user_ids"],
tenant_ids=["tenant_ids"],
resource_ids=["resource_ids"])
self.assertEqual(query, expected)
def test_statistic_aggregation(self, mock_ceilometer):
cm = ceilometer_helper.CeilometerHelper()
ceilometer = mock.MagicMock()
statistic = mock.MagicMock()
expected_result = 100
statistic[-1]._info = {'aggregate': {'avg': expected_result}}
ceilometer.statistics.list.return_value = statistic
mock_ceilometer.return_value = ceilometer
cm = ceilometer_helper.CeilometerHelper()
val = cm.statistic_aggregation(
resource_id="VM_ID",
meter_name="cpu_util",
period="7300"
)
self.assertEqual(val, expected_result)
def test_get_last_sample(self, mock_ceilometer):
ceilometer = mock.MagicMock()
statistic = mock.MagicMock()
expected_result = 100
statistic[-1]._info = {'counter_volume': expected_result}
ceilometer.samples.list.return_value = statistic
mock_ceilometer.return_value = ceilometer
cm = ceilometer_helper.CeilometerHelper()
val = cm.get_last_sample_value(
resource_id="id",
meter_name="compute.node.percent"
)
self.assertEqual(val, expected_result)
def test_get_last_sample_none(self, mock_ceilometer):
ceilometer = mock.MagicMock()
expected = []
ceilometer.samples.list.return_value = expected
mock_ceilometer.return_value = ceilometer
cm = ceilometer_helper.CeilometerHelper()
val = cm.get_last_sample_values(
resource_id="id",
meter_name="compute.node.percent"
)
self.assertEqual(val, expected)
def test_statistic_list(self, mock_ceilometer):
ceilometer = mock.MagicMock()
expected_value = []
ceilometer.statistics.list.return_value = expected_value
mock_ceilometer.return_value = ceilometer
cm = ceilometer_helper.CeilometerHelper()
val = cm.statistic_list(meter_name="cpu_util")
self.assertEqual(val, expected_value)

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