Compare commits

...

22 Commits

Author SHA1 Message Date
Jenkins
ad40c61ea9 Merge "Update docs for password auth configuration options" 2016-02-05 15:58:31 +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
75 changed files with 2111 additions and 1354 deletions

View File

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

View File

@@ -166,11 +166,17 @@ The configuration file is organized into the following sections:
* ``[api]`` - API server configuration * ``[api]`` - API server configuration
* ``[database]`` - SQL driver configuration * ``[database]`` - SQL driver configuration
* ``[keystone_authtoken]`` - Keystone Authentication plugin configuration * ``[keystone_authtoken]`` - Keystone Authentication plugin configuration
* ``[watcher_clients_auth]`` - Keystone auth configuration for clients
* ``[watcher_applier]`` - Watcher Applier module configuration * ``[watcher_applier]`` - Watcher Applier module configuration
* ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration * ``[watcher_decision_engine]`` - Watcher Decision Engine module configuration
* ``[watcher_goals]`` - Goals mapping configuration * ``[watcher_goals]`` - Goals mapping configuration
* ``[watcher_strategies]`` - Strategy configuration * ``[watcher_strategies]`` - Strategy configuration
* ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration * ``[oslo_messaging_rabbit]`` - Oslo Messaging RabbitMQ driver configuration
* ``[ceilometer_client]`` - Ceilometer client configuration
* ``[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 The Watcher configuration file is expected to be named
``watcher.conf``. When starting Watcher, you can specify a different ``watcher.conf``. When starting Watcher, you can specify a different
@@ -237,39 +243,105 @@ so that the watcher service is configured for your needs.
#rabbit_port = 5672 #rabbit_port = 5672
#. Configure the Watcher Service to use these credentials with the Identity #. Watcher API shall validate the token provided by every incoming request,
Service. Replace IDENTITY_IP with the IP of the Identity server, and via keystonemiddleware, which requires the Watcher service to be configured
replace WATCHER_PASSWORD with the password you chose for the ``watcher`` with the right credentials for the Identity service.
user in the Identity Service::
[keystone_authtoken] In the configuration section here below:
# Complete public Identity API endpoint (string value) * replace IDENTITY_IP with the IP of the Identity server
#auth_uri=<None> * replace WATCHER_PASSWORD with the password you chose for the ``watcher``
auth_uri=http://IDENTITY_IP:5000/v3 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 [keystone_authtoken]
# unversioned root endpoint e.g. https://localhost:35357/ (string
# value)
#identity_uri = <None>
identity_uri = http://IDENTITY_IP:5000
# Keystone account username (string value) # Authentication type to load (unknown value)
#admin_user=<None> # Deprecated group/name - [DEFAULT]/auth_plugin
admin_user=watcher #auth_type = <None>
auth_type = password
# Keystone account password (string value) # Authentication URL (unknown value)
#admin_password=<None> #auth_url = <None>
admin_password=WATCHER_DBPASSWORD auth_url = http://IDENTITY_IP:35357
# Keystone service account tenant name to validate user tokens # Username (unknown value)
# (string value) # Deprecated group/name - [DEFAULT]/username
#admin_tenant_name=admin #username = <None>
admin_tenant_name=KEYSTONE_SERVICE_PROJECT_NAME username=watcher
# Directory used to cache files related to PKI tokens (string # User's password (unknown value)
# value) #password = <None>
#signing_dir=<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:: #. Create the Watcher Service database tables::

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
enum34;python_version=='2.7' or python_version=='2.6' enum34;python_version=='2.7' or python_version=='2.6'
jsonpatch>=1.1 jsonpatch>=1.1
keystoneauth1>=2.1.0
keystonemiddleware>=2.0.0,!=2.4.0 keystonemiddleware>=2.0.0,!=2.4.0
oslo.config>=2.3.0 # Apache-2.0 oslo.config>=2.3.0 # Apache-2.0
oslo.db>=2.4.1 # Apache-2.0 oslo.db>=2.4.1 # Apache-2.0

View File

@@ -67,4 +67,4 @@ import_exceptions = watcher._i18n
[doc8] [doc8]
extension=.rst extension=.rst
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed # todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed
ignore-path=doc/source/image_src,doc/source/man ignore-path=doc/source/image_src,doc/source/man,doc/source/api

View File

@@ -288,10 +288,10 @@ class ActionsController(rest.RestController):
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) sort_dir=sort_dir)
@wsme_pecan.wsexpose(ActionCollection, types.uuid, types.uuid, @wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
int, wtypes.text, wtypes.text, types.uuid, wtypes.text, wtypes.text, types.uuid,
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, sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None): audit_uuid=None):
"""Retrieve a list of actions. """Retrieve a list of actions.
@@ -312,16 +312,14 @@ class ActionsController(rest.RestController):
marker, limit, sort_key, sort_dir, marker, limit, sort_key, sort_dir,
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid) action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
@wsme_pecan.wsexpose(ActionCollection, types.uuid, @wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
types.uuid, int, wtypes.text, wtypes.text, wtypes.text, wtypes.text, types.uuid,
types.uuid, types.uuid) types.uuid)
def detail(self, action_uuid=None, marker=None, limit=None, def detail(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None, sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None): audit_uuid=None):
"""Retrieve a list of actions with detail. """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 marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result. :param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.

View File

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

View File

@@ -149,7 +149,7 @@ class AuditTemplate(base.APIBase):
name='My Audit Template', name='My Audit Template',
description='Description of my audit template', description='Description of my audit template',
host_aggregate=5, host_aggregate=5,
goal='SERVERS_CONSOLIDATION', goal='DUMMY',
extra={'automatic': True}, extra={'automatic': True},
created_at=datetime.datetime.utcnow(), created_at=datetime.datetime.utcnow(),
deleted_at=None, deleted_at=None,

View File

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

View File

@@ -22,12 +22,22 @@ import abc
import six import six
from watcher.common import clients
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseAction(object): class BaseAction(object):
def __init__(self): def __init__(self, osc=None):
""":param osc: an OpenStackClients instance"""
self._input_parameters = {} self._input_parameters = {}
self._applies_to = "" self._applies_to = ""
self._osc = osc
@property
def osc(self):
if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@property @property
def input_parameters(self): def input_parameters(self):

View File

@@ -21,8 +21,7 @@
from watcher._i18n import _ from watcher._i18n import _
from watcher.applier.actions import base from watcher.applier.actions import base
from watcher.common import exception from watcher.common import exception
from watcher.common import keystone as kclient from watcher.common import nova_helper
from watcher.common import nova as nclient
from watcher.decision_engine.model import hypervisor_state as hstate from watcher.decision_engine.model import hypervisor_state as hstate
@@ -57,13 +56,11 @@ class ChangeNovaServiceState(base.BaseAction):
raise exception.IllegalArgumentException( raise exception.IllegalArgumentException(
message=_("The target state is not defined")) message=_("The target state is not defined"))
keystone = kclient.KeystoneClient() nova = nova_helper.NovaHelper(osc=self.osc)
wrapper = nclient.NovaClient(keystone.get_credentials(),
session=keystone.get_session())
if state is True: if state is True:
return wrapper.enable_service_nova_compute(self.host) return nova.enable_service_nova_compute(self.host)
else: else:
return wrapper.disable_service_nova_compute(self.host) return nova.disable_service_nova_compute(self.host)
def precondition(self): def precondition(self):
pass pass

View File

@@ -28,9 +28,10 @@ class ActionFactory(object):
def __init__(self): def __init__(self):
self.action_loader = default.DefaultActionLoader() 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) 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.input_parameters = object_action.input_parameters
loaded_action.applies_to = object_action.applies_to loaded_action.applies_to = object_action.applies_to
return loaded_action return loaded_action

View File

@@ -21,8 +21,7 @@ from oslo_log import log
from watcher.applier.actions import base from watcher.applier.actions import base
from watcher.common import exception from watcher.common import exception
from watcher.common import keystone as kclient from watcher.common import nova_helper
from watcher.common import nova as nclient
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -45,18 +44,19 @@ class Migrate(base.BaseAction):
return self.input_parameters.get('src_hypervisor') return self.input_parameters.get('src_hypervisor')
def migrate(self, destination): def migrate(self, destination):
keystone = kclient.KeystoneClient() nova = nova_helper.NovaHelper(osc=self.osc)
wrapper = nclient.NovaClient(keystone.get_credentials(), LOG.debug("Migrate instance %s to %s", self.instance_uuid,
session=keystone.get_session())
LOG.debug("Migrate instance %s to %s ", self.instance_uuid,
destination) destination)
instance = wrapper.find_instance(self.instance_uuid) instance = nova.find_instance(self.instance_uuid)
if instance: if instance:
if self.migration_type == 'live': if self.migration_type == 'live':
return wrapper.live_migrate_instance( return nova.live_migrate_instance(
instance_id=self.instance_uuid, dest_hostname=destination) instance_id=self.instance_uuid, dest_hostname=destination)
else: 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: else:
raise exception.InstanceNotFound(name=self.instance_uuid) raise exception.InstanceNotFound(name=self.instance_uuid)

View File

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

View File

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

View File

@@ -39,17 +39,17 @@ class ApplierAPI(messaging_core.MessagingCore):
def __init__(self): def __init__(self):
super(ApplierAPI, self).__init__( super(ApplierAPI, self).__init__(
CONF.watcher_applier.publisher_id, CONF.watcher_applier.publisher_id,
CONF.watcher_applier.topic_control, CONF.watcher_applier.conductor_topic,
CONF.watcher_applier.topic_status, CONF.watcher_applier.status_topic,
api_version=self.API_VERSION, api_version=self.API_VERSION,
) )
self.handler = notification.NotificationHandler(self.publisher_id) self.handler = notification.NotificationHandler(self.publisher_id)
self.handler.register_observer(self) 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) transport = om.get_transport(CONF)
target = om.Target( target = om.Target(
topic=CONF.watcher_applier.topic_control, topic=CONF.watcher_applier.conductor_topic,
version=self.API_VERSION, version=self.API_VERSION,
) )

View File

@@ -22,33 +22,33 @@ import six
from watcher.applier.actions import factory from watcher.applier.actions import factory
from watcher.applier.messaging import event_types from watcher.applier.messaging import event_types
from watcher.common import clients
from watcher.common.messaging.events import event from watcher.common.messaging.events import event
from watcher import objects from watcher import objects
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseWorkFlowEngine(object): class BaseWorkFlowEngine(object):
def __init__(self): def __init__(self, context=None, applier_manager=None):
self._applier_manager = None self._context = context
self._context = None self._applier_manager = applier_manager
self._action_factory = factory.ActionFactory() self._action_factory = factory.ActionFactory()
self._osc = None
@property @property
def context(self): def context(self):
return self._context return self._context
@context.setter @property
def context(self, c): def osc(self):
self._context = c if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@property @property
def applier_manager(self): def applier_manager(self):
return self._applier_manager return self._applier_manager
@applier_manager.setter
def applier_manager(self, a):
self._applier_manager = a
@property @property
def action_factory(self): def action_factory(self):
return self._action_factory return self._action_factory
@@ -62,8 +62,8 @@ class BaseWorkFlowEngine(object):
ev.data = {} ev.data = {}
payload = {'action_uuid': action.uuid, payload = {'action_uuid': action.uuid,
'action_state': state} 'action_state': state}
self.applier_manager.topic_status.publish_event(ev.type.name, self.applier_manager.status_topic_handler.publish_event(
payload) ev.type.name, payload)
@abc.abstractmethod @abc.abstractmethod
def execute(self, actions): def execute(self, actions):

View File

@@ -89,7 +89,9 @@ class TaskFlowActionContainer(task.Task):
@property @property
def action(self): def action(self):
if self.loaded_action is None: 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 self.loaded_action = action
return self.loaded_action return self.loaded_action

View File

@@ -17,30 +17,16 @@
# limitations under the License. # limitations under the License.
# #
from ceilometerclient import client
from ceilometerclient.exc import HTTPUnauthorized from ceilometerclient.exc import HTTPUnauthorized
from watcher.common import keystone from watcher.common import clients
class CeilometerClient(object): class CeilometerHelper(object):
def __init__(self, api_version='2'): def __init__(self, osc=None):
self._cmclient = None """:param osc: an OpenStackClients instance"""
self._api_version = api_version self.osc = osc if osc else clients.OpenStackClients()
self.ceilometer = self.osc.ceilometer()
@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
def build_query(self, user_id=None, tenant_id=None, resource_id=None, def build_query(self, user_id=None, tenant_id=None, resource_id=None,
user_ids=None, tenant_ids=None, resource_ids=None): user_ids=None, tenant_ids=None, resource_ids=None):
@@ -83,20 +69,21 @@ class CeilometerClient(object):
try: try:
return f(*args, **kargs) return f(*args, **kargs)
except HTTPUnauthorized: except HTTPUnauthorized:
self.reset_client() self.osc.reset_clients()
self.ceilometer = self.osc.ceilometer()
return f(*args, **kargs) return f(*args, **kargs)
except Exception: except Exception:
raise raise
def query_sample(self, meter_name, query, limit=1): 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, meter_name=meter_name,
limit=limit, limit=limit,
q=query) q=query)
def statistic_list(self, meter_name, query=None, period=None): def statistic_list(self, meter_name, query=None, period=None):
"""List of statistics.""" """List of statistics."""
statistics = self.cmclient.statistics.list( statistics = self.ceilometer.statistics.list(
meter_name=meter_name, meter_name=meter_name,
q=query, q=query,
period=period) period=period)
@@ -104,7 +91,8 @@ class CeilometerClient(object):
def meter_list(self, query=None): def meter_list(self, query=None):
"""List the user's meters.""" """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 return meters
def statistic_aggregation(self, def statistic_aggregation(self,
@@ -125,7 +113,7 @@ class CeilometerClient(object):
""" """
query = self.build_query(resource_id=resource_id) 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, meter_name=meter_name,
q=query, q=query,
period=period, period=period,
@@ -156,6 +144,3 @@ class CeilometerClient(object):
return samples[-1]._info['counter_volume'] return samples[-1]._info['counter_volume']
else: else:
return False 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',
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.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_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
import six import six
@@ -40,6 +44,23 @@ CONF = cfg.CONF
CONF.register_opts(exc_log_opts) 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): class WatcherException(Exception):
"""Base Watcher Exception """Base Watcher Exception
@@ -133,12 +154,6 @@ class InvalidGoal(Invalid):
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file") 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): class InvalidUUID(Invalid):
msg_fmt = _("Expected a uuid but received %(uuid)s") msg_fmt = _("Expected a uuid but received %(uuid)s")
@@ -179,7 +194,7 @@ class AuditReferenced(Invalid):
class ActionPlanNotFound(ResourceNotFound): 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): class ActionPlanAlreadyExists(Conflict):
@@ -232,6 +247,10 @@ class NoDataFound(WatcherException):
msg_fmt = _('No rows were returned') msg_fmt = _('No rows were returned')
class AuthorizationFailure(WatcherException):
msg_fmt = _('%(client)s connection failed. Reason: %(reason)s')
class KeystoneFailure(WatcherException): class KeystoneFailure(WatcherException):
msg_fmt = _("'Keystone API endpoint is missing''") msg_fmt = _("'Keystone API endpoint is missing''")

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

View File

@@ -16,58 +16,100 @@
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from watcher.common.messaging.events.event_dispatcher import \ import oslo_messaging as om
EventDispatcher
from watcher.common.messaging.messaging_handler import \
MessagingHandler
from watcher.common.rpc import RequestContextSerializer
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__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
class MessagingCore(EventDispatcher): class MessagingCore(dispatcher.EventDispatcher):
API_VERSION = '1.0' 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): api_version=API_VERSION):
super(MessagingCore, self).__init__() super(MessagingCore, self).__init__()
self.serializer = RequestContextSerializer(WatcherObjectSerializer()) self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self.publisher_id = publisher_id self.publisher_id = publisher_id
self.api_version = api_version 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): self.conductor_topic = conductor_topic
return MessagingHandler(self.publisher_id, topic_name, self, self.status_topic = status_topic
self.api_version, self.serializer) 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): def connect(self):
LOG.debug("Connecting to '%s' (%s)", LOG.debug("Connecting to '%s' (%s)",
CONF.transport_url, CONF.rpc_backend) CONF.transport_url, CONF.rpc_backend)
self.topic_control.start() self.conductor_topic_handler.start()
self.topic_status.start() self.status_topic_handler.start()
def disconnect(self): def disconnect(self):
LOG.debug("Disconnecting from '%s' (%s)", LOG.debug("Disconnecting from '%s' (%s)",
CONF.transport_url, CONF.rpc_backend) CONF.transport_url, CONF.rpc_backend)
self.topic_control.stop() self.conductor_topic_handler.stop()
self.topic_status.stop() self.status_topic_handler.stop()
def publish_control(self, event, payload): 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): 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): def get_version(self):
return self.api_version return self.api_version
def check_api_version(self, context): 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', context.to_dict(), 'check_api_version',
api_version=self.api_version) api_version=self.api_version)
return api_manager_version return api_manager_version

View File

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

View File

@@ -18,17 +18,16 @@ import eventlet
from oslo_log import log from oslo_log import log
import oslo_messaging as messaging import oslo_messaging as messaging
from watcher.common.messaging.utils.observable import \ from watcher.common.messaging.utils import observable
Observable
eventlet.monkey_patch() eventlet.monkey_patch()
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class NotificationHandler(Observable): class NotificationHandler(observable.Observable):
def __init__(self, publisher_id): def __init__(self, publisher_id):
Observable.__init__(self) super(NotificationHandler, self).__init__()
self.publisher_id = publisher_id self.publisher_id = publisher_id
def info(self, ctx, publisher_id, event_type, payload, metadata): 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from oslo_log import log from watcher.common.messaging.utils import synchronization
from watcher.common.messaging.utils.synchronization import \
Synchronization
LOG = log.getLogger(__name__)
class Observable(Synchronization): class Observable(synchronization.Synchronization):
def __init__(self): def __init__(self):
super(Observable, self).__init__()
self.__observers = [] self.__observers = []
self.changed = 0 self.changed = 0
Synchronization.__init__(self)
def set_changed(self): def set_changed(self):
self.changed = 1 self.changed = 1

View File

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

View File

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

View File

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

View File

@@ -54,8 +54,8 @@ class DefaultAuditHandler(base.BaseAuditHandler):
event.data = {} event.data = {}
payload = {'audit_uuid': audit_uuid, payload = {'audit_uuid': audit_uuid,
'audit_status': status} 'audit_status': status}
self.messaging.topic_status.publish_event(event.type.name, self.messaging.status_topic_handler.publish_event(
payload) event.type.name, payload)
def update_audit_state(self, request_context, audit_uuid, state): def update_audit_state(self, request_context, audit_uuid, state):
LOG.debug("Update audit state: %s", 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_config import cfg
from oslo_log import log from oslo_log import log
from watcher.common.messaging.messaging_core import MessagingCore from watcher.common.messaging import messaging_core
from watcher.decision_engine.messaging.audit_endpoint import AuditEndpoint from watcher.decision_engine.messaging import audit_endpoint
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
WATCHER_DECISION_ENGINE_OPTS = [ WATCHER_DECISION_ENGINE_OPTS = [
cfg.StrOpt('topic_control', cfg.StrOpt('conductor_topic',
default='watcher.decision.control', default='watcher.decision.control',
help='The topic name used for' help='The topic name used for'
'control events, this topic ' 'control events, this topic '
'used for rpc call '), 'used for rpc call '),
cfg.StrOpt('topic_status', cfg.StrOpt('status_topic',
default='watcher.decision.status', default='watcher.decision.status',
help='The topic name used for ' help='The topic name used for '
'status events, this topic ' '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) CONF.register_opts(WATCHER_DECISION_ENGINE_OPTS, decision_engine_opt_group)
class DecisionEngineManager(MessagingCore): class DecisionEngineManager(messaging_core.MessagingCore):
def __init__(self): def __init__(self):
super(DecisionEngineManager, self).__init__( super(DecisionEngineManager, self).__init__(
CONF.watcher_decision_engine.publisher_id, CONF.watcher_decision_engine.publisher_id,
CONF.watcher_decision_engine.topic_control, CONF.watcher_decision_engine.conductor_topic,
CONF.watcher_decision_engine.topic_status, CONF.watcher_decision_engine.status_topic,
api_version=self.API_VERSION) api_version=self.API_VERSION)
endpoint = AuditEndpoint(self, endpoint = audit_endpoint.AuditEndpoint(
max_workers=CONF.watcher_decision_engine. self,
max_workers) max_workers=CONF.watcher_decision_engine.max_workers)
self.topic_control.add_endpoint(endpoint) self.conductor_topic_handler.add_endpoint(endpoint)
def join(self): def join(self):
self.topic_control.join() self.conductor_topic_handler.join()
self.topic_status.join() self.status_topic_handler.join()

View File

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

View File

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

View File

@@ -48,11 +48,12 @@ class DefaultStrategySelector(base.BaseSelector):
super(DefaultStrategySelector, self).__init__() super(DefaultStrategySelector, self).__init__()
self.strategy_loader = default.DefaultStrategyLoader() 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 strategy_to_load = None
try: try:
strategy_to_load = CONF.watcher_goals.goals[goal_name] 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: except KeyError as exc:
LOG.exception(exc) LOG.exception(exc)
raise exception.WatcherException( raise exception.WatcherException(

View File

@@ -33,10 +33,11 @@ provided as well.
""" """
import abc import abc
from oslo_log import log from oslo_log import log
import six import six
from watcher.common import clients
from watcher.decision_engine.solution.default import DefaultSolution from watcher.decision_engine.solution.default import DefaultSolution
from watcher.decision_engine.strategy.common.level import StrategyLevel from watcher.decision_engine.strategy.common.level import StrategyLevel
@@ -51,7 +52,8 @@ class BaseStrategy(object):
Solution for a given Goal. 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._name = name
self.description = description self.description = description
# default strategy level # default strategy level
@@ -59,6 +61,7 @@ class BaseStrategy(object):
self._cluster_state_collector = None self._cluster_state_collector = None
# the solution given by the strategy # the solution given by the strategy
self._solution = DefaultSolution() self._solution = DefaultSolution()
self._osc = osc
@abc.abstractmethod @abc.abstractmethod
def execute(self, model): def execute(self, model):
@@ -70,6 +73,12 @@ class BaseStrategy(object):
:rtype: :class:`watcher.decision_engine.solution.base.BaseSolution` :rtype: :class:`watcher.decision_engine.solution.base.BaseSolution`
""" """
@property
def osc(self):
if not self._osc:
self._osc = clients.OpenStackClients()
return self._osc
@property @property
def solution(self): def solution(self):
return self._solution return self._solution

View File

@@ -41,7 +41,8 @@ class BasicConsolidation(BaseStrategy):
MIGRATION = "migrate" MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state" 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 """Basic offline Consolidation using live migration
The basic consolidation algorithm has several limitations. The basic consolidation algorithm has several limitations.
@@ -68,8 +69,9 @@ class BasicConsolidation(BaseStrategy):
:param name: the name of the strategy :param name: the name of the strategy
:param description: a description of the strategy :param description: a description of the strategy
:param osc: an OpenStackClients object
""" """
super(BasicConsolidation, self).__init__(name, description) super(BasicConsolidation, self).__init__(name, description, osc)
# set default value for the number of released nodes # set default value for the number of released nodes
self.number_of_released_nodes = 0 self.number_of_released_nodes = 0
@@ -102,7 +104,7 @@ class BasicConsolidation(BaseStrategy):
@property @property
def ceilometer(self): def ceilometer(self):
if self._ceilometer is None: if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory() self._ceilometer = CeilometerClusterHistory(osc=self.osc)
return self._ceilometer return self._ceilometer
@ceilometer.setter @ceilometer.setter

View File

@@ -30,8 +30,9 @@ class DummyStrategy(BaseStrategy):
NOP = "nop" NOP = "nop"
SLEEP = "sleep" SLEEP = "sleep"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION): def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
super(DummyStrategy, self).__init__(name, description) osc=None):
super(DummyStrategy, self).__init__(name, description, osc)
def execute(self, model): def execute(self, model):
parameters = {'message': 'hello World'} parameters = {'message': 'hello World'}

View File

@@ -40,7 +40,8 @@ class OutletTempControl(BaseStrategy):
MIGRATION = "migrate" MIGRATION = "migrate"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION): def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
osc=None):
"""[PoC]Outlet temperature control using live migration """[PoC]Outlet temperature control using live migration
It is a migration strategy based on the Outlet Temperature of physical It is a migration strategy based on the Outlet Temperature of physical
@@ -67,8 +68,9 @@ class OutletTempControl(BaseStrategy):
:param name: the name of the strategy :param name: the name of the strategy
:param description: a description 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 # the migration plan will be triggered when the outlet temperature
# reaches threshold # reaches threshold
# TODO(zhenzanz): Threshold should be configurable for each audit # TODO(zhenzanz): Threshold should be configurable for each audit
@@ -79,7 +81,7 @@ class OutletTempControl(BaseStrategy):
@property @property
def ceilometer(self): def ceilometer(self):
if self._ceilometer is None: if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory() self._ceilometer = CeilometerClusterHistory(osc=self.osc)
return self._ceilometer return self._ceilometer
@ceilometer.setter @ceilometer.setter

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: python-watcher 0.21.1.dev32\n" "Project-Id-Version: python-watcher 0.21.1.dev32\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-01-19 17:54+0100\n" "POT-Creation-Date: 2016-01-26 11:26+0100\n"
"PO-Revision-Date: 2015-12-11 15:42+0100\n" "PO-Revision-Date: 2015-12-11 15:42+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n" "Language: fr\n"
@@ -24,25 +24,29 @@ msgstr ""
msgid "Invalid state: %(state)s" msgid "Invalid state: %(state)s"
msgstr "État invalide : %(state)s" msgstr "État invalide : %(state)s"
#: watcher/api/controllers/v1/action_plan.py:418 #: watcher/api/controllers/v1/action_plan.py:420
#, python-format #, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)" msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr "Transition d'état non autorisée : (%(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 #: watcher/api/controllers/v1/types.py:148
#, python-format #, python-format
msgid "%s is not JSON serializable" msgid "%s is not JSON serializable"
msgstr "" msgstr "%s n'est pas sérialisable en JSON"
#: watcher/api/controllers/v1/types.py:184 #: watcher/api/controllers/v1/types.py:184
#, python-format #, python-format
msgid "Wrong type. Expected '%(type)s', got '%(value)s'" 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 #: watcher/api/controllers/v1/types.py:223
#, python-format #, python-format
msgid "'%s' is an internal attribute and can not be updated" 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 #: watcher/api/controllers/v1/types.py:227
#, python-format #, python-format
@@ -60,7 +64,7 @@ msgstr "Limit doit être positif"
#: watcher/api/controllers/v1/utils.py:47 #: watcher/api/controllers/v1/utils.py:47
#, python-format #, python-format
msgid "Invalid sort direction: %s. Acceptable values are 'asc' or 'desc'" 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 #: watcher/api/controllers/v1/utils.py:57
#, python-format #, python-format
@@ -69,14 +73,14 @@ msgstr ""
#: watcher/api/middleware/auth_token.py:45 #: watcher/api/middleware/auth_token.py:45
msgid "Cannot compile public API routes" msgid "Cannot compile public API routes"
msgstr "" msgstr "Ne peut pas compiler les chemins d'API publique"
#: watcher/api/middleware/parsable_error.py:52 #: watcher/api/middleware/parsable_error.py:52
#, python-format #, python-format
msgid "ErrorDocumentMiddleware received an invalid status %s" msgid "ErrorDocumentMiddleware received an invalid status %s"
msgstr "" msgstr ""
#: watcher/api/middleware/parsable_error.py:80 #: watcher/api/middleware/parsable_error.py:79
#, python-format #, python-format
msgid "Error parsing HTTP response: %s" msgid "Error parsing HTTP response: %s"
msgstr "" msgstr ""
@@ -85,17 +89,17 @@ msgstr ""
msgid "The target state is not defined" msgid "The target state is not defined"
msgstr "" msgstr ""
#: watcher/applier/workflow_engine/default.py:69 #: watcher/applier/workflow_engine/default.py:126
#, python-format #, python-format
msgid "The WorkFlow Engine has failed to execute the action %s" 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" 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:144
#, python-format #, python-format
msgid "Revert action %s" msgid "Revert action %s"
msgstr "Annulation de l'action %s" msgstr "Annulation de l'action %s"
#: watcher/applier/workflow_engine/default.py:83 #: watcher/applier/workflow_engine/default.py:150
msgid "Oops! We need disaster recover plan" msgid "Oops! We need disaster recover plan"
msgstr "Oops! Nous avons besoin d'un plan de reprise d'activité" msgstr "Oops! Nous avons besoin d'un plan de reprise d'activité"
@@ -115,185 +119,178 @@ 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" msgid "serving on http://%(host)s:%(port)s"
msgstr "Sert sur http://%(host)s:%(port)s" msgstr "Sert sur http://%(host)s:%(port)s"
#: watcher/common/exception.py:56 #: watcher/common/exception.py:51
msgid "An unknown exception occurred" msgid "An unknown exception occurred"
msgstr "" msgstr ""
#: watcher/common/exception.py:77 #: watcher/common/exception.py:71
msgid "Exception in string format operation" msgid "Exception in string format operation"
msgstr "" msgstr ""
#: watcher/common/exception.py:107 #: watcher/common/exception.py:101
msgid "Not authorized" msgid "Not authorized"
msgstr "" msgstr ""
#: watcher/common/exception.py:112 #: watcher/common/exception.py:106
msgid "Operation not permitted" msgid "Operation not permitted"
msgstr "" msgstr ""
#: watcher/common/exception.py:116 #: watcher/common/exception.py:110
msgid "Unacceptable parameters" msgid "Unacceptable parameters"
msgstr "" msgstr ""
#: watcher/common/exception.py:121 #: watcher/common/exception.py:115
#, python-format #, python-format
msgid "The %(name)s %(id)s could not be found" msgid "The %(name)s %(id)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:125 #: watcher/common/exception.py:119
#, fuzzy #, fuzzy
msgid "Conflict" msgid "Conflict"
msgstr "Conflit." msgstr "Conflit"
#: watcher/common/exception.py:130 #: watcher/common/exception.py:124
#, python-format #, python-format
msgid "The %(name)s resource %(id)s could not be found" 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:129
#, python-format #, python-format
msgid "Expected an uuid or int but received %(identity)s" msgid "Expected an uuid or int but received %(identity)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:139 #: watcher/common/exception.py:133
#, python-format #, python-format
msgid "Goal %(goal)s is not defined in Watcher configuration file" msgid "Goal %(goal)s is not defined in Watcher configuration file"
msgstr "" msgstr ""
#: watcher/common/exception.py:145 #: watcher/common/exception.py:139
#, python-format #, python-format
msgid "%(err)s" msgid "%(err)s"
msgstr "" msgstr "%(err)s"
#: watcher/common/exception.py:149 #: watcher/common/exception.py:143
#, python-format #, python-format
msgid "Expected a uuid but received %(uuid)s" msgid "Expected a uuid but received %(uuid)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:153 #: watcher/common/exception.py:147
#, python-format #, python-format
msgid "Expected a logical name but received %(name)s" msgid "Expected a logical name but received %(name)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:157 #: watcher/common/exception.py:151
#, python-format #, python-format
msgid "Expected a logical name or uuid but received %(name)s" msgid "Expected a logical name or uuid but received %(name)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:161 #: watcher/common/exception.py:155
#, python-format #, python-format
msgid "AuditTemplate %(audit_template)s could not be found" msgid "AuditTemplate %(audit_template)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:165 #: watcher/common/exception.py:159
#, python-format #, python-format
msgid "An audit_template with UUID %(uuid)s or name %(name)s already exists" msgid "An audit_template with UUID %(uuid)s or name %(name)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:170 #: watcher/common/exception.py:164
#, python-format #, python-format
msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit" msgid "AuditTemplate %(audit_template)s is referenced by one or multiple audit"
msgstr "" msgstr ""
#: watcher/common/exception.py:175 #: watcher/common/exception.py:169
#, python-format #, python-format
msgid "Audit %(audit)s could not be found" msgid "Audit %(audit)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:179 #: watcher/common/exception.py:173
#, python-format #, python-format
msgid "An audit with UUID %(uuid)s already exists" msgid "An audit with UUID %(uuid)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:183 #: watcher/common/exception.py:177
#, python-format #, python-format
msgid "Audit %(audit)s is referenced by one or multiple action plans" msgid "Audit %(audit)s is referenced by one or multiple action plans"
msgstr "" msgstr ""
#: watcher/common/exception.py:188 #: watcher/common/exception.py:182
msgid "ActionPlan %(action plan)s could not be found" #, python-format
msgid "ActionPlan %(action_plan)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:192 #: watcher/common/exception.py:186
#, python-format #, python-format
msgid "An action plan with UUID %(uuid)s already exists" msgid "An action plan with UUID %(uuid)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:196 #: watcher/common/exception.py:190
#, python-format #, python-format
msgid "Action Plan %(action_plan)s is referenced by one or multiple actions" msgid "Action Plan %(action_plan)s is referenced by one or multiple actions"
msgstr "" msgstr ""
#: watcher/common/exception.py:201 #: watcher/common/exception.py:195
#, python-format #, python-format
msgid "Action %(action)s could not be found" msgid "Action %(action)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:205 #: watcher/common/exception.py:199
#, python-format #, python-format
msgid "An action with UUID %(uuid)s already exists" msgid "An action with UUID %(uuid)s already exists"
msgstr "" msgstr ""
#: watcher/common/exception.py:209 #: watcher/common/exception.py:203
#, python-format #, python-format
msgid "Action plan %(action_plan)s is referenced by one or multiple goals" msgid "Action plan %(action_plan)s is referenced by one or multiple goals"
msgstr "" msgstr ""
#: watcher/common/exception.py:214 #: watcher/common/exception.py:208
msgid "Filtering actions on both audit and action-plan is prohibited" msgid "Filtering actions on both audit and action-plan is prohibited"
msgstr "" msgstr ""
#: watcher/common/exception.py:223 #: watcher/common/exception.py:217
#, python-format #, python-format
msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s" msgid "Couldn't apply patch '%(patch)s'. Reason: %(reason)s"
msgstr "" msgstr ""
#: watcher/common/exception.py:233 #: watcher/common/exception.py:224
msgid "Description must be an instance of str" msgid "Illegal argument"
msgstr "" msgstr ""
#: watcher/common/exception.py:243 #: watcher/common/exception.py:228
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" msgid "No such metric"
msgstr "" msgstr ""
#: watcher/common/exception.py:269 #: watcher/common/exception.py:232
msgid "No rows were returned" msgid "No rows were returned"
msgstr "" msgstr ""
#: watcher/common/exception.py:277 #: watcher/common/exception.py:236
msgid "'Keystone API endpoint is missing''" msgid "'Keystone API endpoint is missing''"
msgstr "" msgstr ""
#: watcher/common/exception.py:281 #: watcher/common/exception.py:240
msgid "The list of hypervisor(s) in the cluster is empty" msgid "The list of hypervisor(s) in the cluster is empty"
msgstr "" msgstr ""
#: watcher/common/exception.py:285 #: watcher/common/exception.py:244
msgid "The metrics resource collector is not defined" msgid "The metrics resource collector is not defined"
msgstr "" msgstr ""
#: watcher/common/exception.py:289 #: watcher/common/exception.py:248
msgid "the cluster state is not defined" msgid "the cluster state is not defined"
msgstr "" msgstr ""
#: watcher/common/exception.py:295 #: watcher/common/exception.py:254
#, python-format #, python-format
msgid "The instance '%(name)s' is not found" msgid "The instance '%(name)s' is not found"
msgstr "L'instance '%(name)s' n'a pas été trouvée" msgstr "L'instance '%(name)s' n'a pas été trouvée"
#: watcher/common/exception.py:299 #: watcher/common/exception.py:258
msgid "The hypervisor is not found" msgid "The hypervisor is not found"
msgstr "" msgstr ""
#: watcher/common/exception.py:303 #: watcher/common/exception.py:262
#, fuzzy, python-format #, fuzzy, python-format
msgid "Error loading plugin '%(name)s'" msgid "Error loading plugin '%(name)s'"
msgstr "Erreur lors du chargement du module '%(name)s'" msgstr "Erreur lors du chargement du module '%(name)s'"
@@ -332,7 +329,7 @@ msgstr ""
#: watcher/common/utils.py:53 #: watcher/common/utils.py:53
#, python-format #, python-format
msgid "" 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," "object is not a string: %s,"
msgstr "" msgstr ""
@@ -373,7 +370,7 @@ msgstr ""
msgid "'obj' argument type is not valid" msgid "'obj' argument type is not valid"
msgstr "" msgstr ""
#: watcher/decision_engine/planner/default.py:75 #: watcher/decision_engine/planner/default.py:76
msgid "The action plan is empty" msgid "The action plan is empty"
msgstr "" msgstr ""
@@ -388,11 +385,11 @@ msgstr ""
msgid "No values returned by %(resource_id)s for %(metric_name)s" msgid "No values returned by %(resource_id)s for %(metric_name)s"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:349 #: watcher/decision_engine/strategy/strategies/basic_consolidation.py:424
msgid "Initializing Sercon Consolidation" msgid "Initializing Sercon Consolidation"
msgstr "" msgstr ""
#: watcher/decision_engine/strategy/strategies/basic_consolidation.py:406 #: watcher/decision_engine/strategy/strategies/basic_consolidation.py:468
msgid "The workloads of the compute nodes of the cluster is zero" msgid "The workloads of the compute nodes of the cluster is zero"
msgstr "" msgstr ""
@@ -573,3 +570,21 @@ msgstr ""
#~ msgid "The WorkFlow Engine has failedto execute the action %s" #~ msgid "The WorkFlow Engine has failedto execute the action %s"
#~ msgstr "" #~ 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 ""
#~ "Failed to remove trailing character. "
#~ "Returning original object. Supplied object "
#~ "is not a string: %s,"
#~ msgstr ""

View File

@@ -7,9 +7,9 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: python-watcher 0.22.1.dev49\n" "Project-Id-Version: python-watcher 0.23.2.dev1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-01-22 10:43+0100\n" "POT-Creation-Date: 2016-01-26 11:26+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -23,7 +23,7 @@ msgstr ""
msgid "Invalid state: %(state)s" msgid "Invalid state: %(state)s"
msgstr "" msgstr ""
#: watcher/api/controllers/v1/action_plan.py:416 #: watcher/api/controllers/v1/action_plan.py:420
#, python-format #, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)" msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr "" msgstr ""
@@ -88,6 +88,10 @@ msgstr ""
msgid "The target state is not defined" msgid "The target state is not defined"
msgstr "" msgstr ""
#: watcher/applier/actions/migration.py:60
msgid "Migration of type %(migration_type)s is not supported."
msgstr ""
#: watcher/applier/workflow_engine/default.py:126 #: watcher/applier/workflow_engine/default.py:126
#, python-format #, python-format
msgid "The WorkFlow Engine has failed to execute the action %s" msgid "The WorkFlow Engine has failed to execute the action %s"
@@ -162,11 +166,6 @@ msgstr ""
msgid "Goal %(goal)s is not defined in Watcher configuration file" msgid "Goal %(goal)s is not defined in Watcher configuration file"
msgstr "" msgstr ""
#: watcher/common/exception.py:139
#, python-format
msgid "%(err)s"
msgstr ""
#: watcher/common/exception.py:143 #: watcher/common/exception.py:143
#, python-format #, python-format
msgid "Expected a uuid but received %(uuid)s" msgid "Expected a uuid but received %(uuid)s"
@@ -213,7 +212,8 @@ msgid "Audit %(audit)s is referenced by one or multiple action plans"
msgstr "" msgstr ""
#: watcher/common/exception.py:182 #: watcher/common/exception.py:182
msgid "ActionPlan %(action plan)s could not be found" #, python-format
msgid "ActionPlan %(action_plan)s could not be found"
msgstr "" msgstr ""
#: watcher/common/exception.py:186 #: watcher/common/exception.py:186

View File

@@ -20,7 +20,7 @@
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log 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.api import BaseClusterHistory
@@ -29,8 +29,10 @@ LOG = log.getLogger(__name__)
class CeilometerClusterHistory(BaseClusterHistory): class CeilometerClusterHistory(BaseClusterHistory):
def __init__(self): def __init__(self, osc=None):
self.ceilometer = CeilometerClient() """: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): def statistic_list(self, meter_name, query=None, period=None):
return self.ceilometer.statistic_list(meter_name, query, period) return self.ceilometer.statistic_list(meter_name, query, period)

View File

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

View File

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

View File

@@ -15,8 +15,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from keystoneauth1 import loading as ka_loading
import watcher.api.app import watcher.api.app
from watcher.applier import manager as applier_manager 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 import manager as decision_engine_manger
from watcher.decision_engine.planner import manager as planner_manager from watcher.decision_engine.planner import manager as planner_manager
from watcher.decision_engine.strategy.selection import default \ from watcher.decision_engine.strategy.selection import default \
@@ -29,7 +32,15 @@ def list_opts():
('watcher_goals', strategy_selector.WATCHER_GOALS_OPTS), ('watcher_goals', strategy_selector.WATCHER_GOALS_OPTS),
('watcher_decision_engine', ('watcher_decision_engine',
decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS), decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS),
('watcher_applier', ('watcher_applier', applier_manager.APPLIER_MANAGER_OPTS),
applier_manager.APPLIER_MANAGER_OPTS), ('watcher_planner', planner_manager.WATCHER_PLANNER_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

@@ -35,6 +35,7 @@ class TestActionPlanObject(base.TestCase):
act_plan_dict = api_utils.action_plan_post_data() act_plan_dict = api_utils.action_plan_post_data()
del act_plan_dict['state'] del act_plan_dict['state']
del act_plan_dict['audit_id'] del act_plan_dict['audit_id']
del act_plan_dict['first_action_id']
act_plan = api_action_plan.ActionPlan(**act_plan_dict) act_plan = api_action_plan.ActionPlan(**act_plan_dict)
self.assertEqual(wtypes.Unset, act_plan.state) self.assertEqual(wtypes.Unset, act_plan.state)
@@ -69,12 +70,20 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json('/action_plans') response = self.get_json('/action_plans')
self.assertEqual([], response['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) action_plan = obj_utils.create_action_plan_without_audit(self.context)
response = self.get_json('/action_plans/%s' % action_plan['uuid']) response = self.get_json('/action_plans/%s' % action_plan['uuid'])
self.assertEqual(action_plan.uuid, response['uuid']) self.assertEqual(action_plan.uuid, response['uuid'])
self._assert_action_plans_fields(response) 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): def test_get_one_soft_deleted(self):
action_plan = obj_utils.create_action_plan_without_audit(self.context) action_plan = obj_utils.create_action_plan_without_audit(self.context)
action_plan.soft_delete() action_plan.soft_delete()

View File

@@ -15,9 +15,8 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
#
from mock import call import mock
from mock import MagicMock
from watcher.applier.action_plan.default import DefaultActionPlanHandler from watcher.applier.action_plan.default import DefaultActionPlanHandler
from watcher.applier.messaging.event_types import EventTypes from watcher.applier.messaging.event_types import EventTypes
@@ -34,7 +33,7 @@ class TestDefaultActionPlanHandler(DbTestCase):
self.context) self.context)
def test_launch_action_plan(self): def test_launch_action_plan(self):
command = DefaultActionPlanHandler(self.context, MagicMock(), command = DefaultActionPlanHandler(self.context, mock.MagicMock(),
self.action_plan.uuid) self.action_plan.uuid)
command.execute() command.execute()
action_plan = ActionPlan.get_by_uuid(self.context, action_plan = ActionPlan.get_by_uuid(self.context,
@@ -42,18 +41,19 @@ class TestDefaultActionPlanHandler(DbTestCase):
self.assertEqual(ap_objects.State.SUCCEEDED, action_plan.state) self.assertEqual(ap_objects.State.SUCCEEDED, action_plan.state)
def test_trigger_audit_send_notification(self): def test_trigger_audit_send_notification(self):
messaging = MagicMock() messaging = mock.MagicMock()
command = DefaultActionPlanHandler(self.context, messaging, command = DefaultActionPlanHandler(self.context, messaging,
self.action_plan.uuid) self.action_plan.uuid)
command.execute() command.execute()
call_on_going = call(EventTypes.LAUNCH_ACTION_PLAN.name, { call_on_going = mock.call(EventTypes.LAUNCH_ACTION_PLAN.name, {
'action_plan_state': ap_objects.State.ONGOING, 'action_plan_state': ap_objects.State.ONGOING,
'action_plan__uuid': self.action_plan.uuid}) 'action_plan__uuid': self.action_plan.uuid})
call_succeeded = call(EventTypes.LAUNCH_ACTION_PLAN.name, { call_succeeded = mock.call(EventTypes.LAUNCH_ACTION_PLAN.name, {
'action_plan_state': ap_objects.State.SUCCEEDED, 'action_plan_state': ap_objects.State.SUCCEEDED,
'action_plan__uuid': self.action_plan.uuid}) 'action_plan__uuid': self.action_plan.uuid})
calls = [call_on_going, call_succeeded] calls = [call_on_going, call_succeeded]
messaging.topic_status.publish_event.assert_has_calls(calls) messaging.status_topic_handler.publish_event.assert_has_calls(calls)
self.assertEqual(2, messaging.topic_status.publish_event.call_count) self.assertEqual(
2, messaging.status_topic_handler.publish_event.call_count)

View File

@@ -53,9 +53,9 @@ class FakeAction(abase.BaseAction):
class TestDefaultWorkFlowEngine(base.DbTestCase): class TestDefaultWorkFlowEngine(base.DbTestCase):
def setUp(self): def setUp(self):
super(TestDefaultWorkFlowEngine, self).setUp() super(TestDefaultWorkFlowEngine, self).setUp()
self.engine = tflow.DefaultWorkFlowEngine() self.engine = tflow.DefaultWorkFlowEngine(
self.engine.context = self.context context=self.context,
self.engine.applier_manager = mock.MagicMock() applier_manager=mock.MagicMock())
def test_execute(self): def test_execute(self):
actions = mock.MagicMock() actions = mock.MagicMock()

View File

@@ -15,59 +15,79 @@
# limitations under the License. # limitations under the License.
from mock import patch import mock
from watcher.common.messaging.messaging_core import MessagingCore from watcher.common.messaging import messaging_core
from watcher.common.messaging.messaging_handler import MessagingHandler from watcher.common.messaging import messaging_handler
from watcher.common.rpc import RequestContextSerializer from watcher.common import rpc
from watcher.tests.base import TestCase from watcher.tests import base
class TestMessagingCore(TestCase): class TestMessagingCore(base.TestCase):
def setUp(self): def setUp(self):
super(TestMessagingCore, self).setUp() 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" topic_name = "MyTopic"
messaging = MessagingCore("", "", "") messaging = messaging_core.MessagingCore("", "", "")
messaging_handler = messaging.build_topic(topic_name) handler = messaging.build_topic_handler(topic_name)
self.assertIsNotNone(messaging_handler) self.assertIsNotNone(handler)
def test_init_messaging_core(self): def test_init_messaging_core(self):
messaging = MessagingCore("", "", "") messaging = messaging_core.MessagingCore("", "", "")
self.assertIsInstance(messaging.serializer, self.assertIsInstance(messaging.serializer,
RequestContextSerializer) rpc.RequestContextSerializer)
self.assertIsInstance(messaging.topic_control, MessagingHandler) self.assertIsInstance(
self.assertIsInstance(messaging.topic_status, MessagingHandler) messaging.conductor_topic_handler,
messaging_handler.MessagingHandler)
self.assertIsInstance(
messaging.status_topic_handler,
messaging_handler.MessagingHandler)
@patch.object(MessagingCore, 'publish_control') @mock.patch.object(messaging_handler, "MessagingHandler")
def test_publish_control(self, mock_call): def test_publish_control(self, m_handler_cls):
m_handler = mock.Mock()
m_handler_cls.return_value = m_handler
payload = { payload = {
"name": "value", "name": "value",
} }
event = "MyEvent" event = "MyEvent"
messaging = MessagingCore("", "", "") messaging = messaging_core.MessagingCore("", "", "")
messaging.publish_control(event, payload) 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') @mock.patch.object(messaging_handler, "MessagingHandler")
def test_publish_status(self, mock_call): def test_publish_status(self, m_handler_cls):
m_handler = mock.Mock()
m_handler_cls.return_value = m_handler
payload = { payload = {
"name": "value", "name": "value",
} }
event = "MyEvent" event = "MyEvent"
messaging = MessagingCore("", "", "") messaging = messaging_core.MessagingCore("", "", "")
messaging.publish_status(event, payload) 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): def test_response(self, mock_call):
event = "My event" event = "My event"
context = {'request_id': 12} context = {'request_id': 12}
message = "My Message" message = "My Message"
messaging = MessagingCore("", "", "") messaging = messaging_core.MessagingCore("", "", "")
messaging.response(event, context, message) messaging.response(event, context, message)
expected_payload = { expected_payload = {
@@ -76,13 +96,15 @@ class TestMessagingCore(TestCase):
} }
mock_call.assert_called_once_with(event, expected_payload) mock_call.assert_called_once_with(event, expected_payload)
def test_messaging_build_topic(self): def test_messaging_build_topic_handler(self):
messaging = MessagingCore("pub_id", "test_topic", "does not matter") messaging = messaging_core.MessagingCore(
topic = messaging.build_topic("test_topic") "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(messaging.publisher_id, "pub_id")
self.assertEqual(topic.publisher_id, "pub_id") self.assertEqual(topic.publisher_id, "pub_id")
self.assertEqual(messaging.topic_control.topic_watcher, "test_topic") self.assertEqual(
self.assertEqual(topic.topic_watcher, "test_topic") 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from mock import Mock import mock
from mock import patch
from oslo_config import cfg from oslo_config import cfg
import oslo_messaging as messaging import oslo_messaging as messaging
from watcher.common.messaging.messaging_handler import MessagingHandler from watcher.common.messaging import messaging_handler
from watcher.tests.base import TestCase from watcher.tests import base
CONF = cfg.CONF CONF = cfg.CONF
class TestMessagingHandler(TestCase): class TestMessagingHandler(base.TestCase):
PUBLISHER_ID = 'TEST_API' PUBLISHER_ID = 'TEST_API'
TOPIC_WATCHER = 'TEST_TOPIC_WATCHER' TOPIC_WATCHER = 'TEST_TOPIC_WATCHER'
@@ -35,20 +34,20 @@ class TestMessagingHandler(TestCase):
super(TestMessagingHandler, self).setUp() super(TestMessagingHandler, self).setUp()
CONF.set_default('host', 'fake-fqdn') CONF.set_default('host', 'fake-fqdn')
@patch.object(messaging, "get_rpc_server") @mock.patch.object(messaging, "get_rpc_server")
@patch.object(messaging, "Target") @mock.patch.object(messaging, "Target")
def test_setup_messaging_handler(self, m_target_cls, m_get_rpc_server): 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 m_target_cls.return_value = m_target
messaging_handler = MessagingHandler( handler = messaging_handler.MessagingHandler(
publisher_id=self.PUBLISHER_ID, publisher_id=self.PUBLISHER_ID,
topic_watcher=self.TOPIC_WATCHER, topic_name=self.TOPIC_WATCHER,
endpoint=self.ENDPOINT, endpoint=self.ENDPOINT,
version=self.VERSION, version=self.VERSION,
serializer=None, serializer=None,
) )
messaging_handler.run() handler.run()
m_target_cls.assert_called_once_with( m_target_cls.assert_called_once_with(
server="fake-fqdn", server="fake-fqdn",
@@ -56,23 +55,23 @@ class TestMessagingHandler(TestCase):
version="1.0", version="1.0",
) )
m_get_rpc_server.assert_called_once_with( m_get_rpc_server.assert_called_once_with(
messaging_handler.transport, handler.transport,
m_target, m_target,
[self.ENDPOINT], [self.ENDPOINT],
serializer=None, serializer=None,
) )
def test_messaging_handler_remove_endpoint(self): def test_messaging_handler_remove_endpoint(self):
messaging_handler = MessagingHandler( handler = messaging_handler.MessagingHandler(
publisher_id=self.PUBLISHER_ID, publisher_id=self.PUBLISHER_ID,
topic_watcher=self.TOPIC_WATCHER, topic_name=self.TOPIC_WATCHER,
endpoint=self.ENDPOINT, endpoint=self.ENDPOINT,
version=self.VERSION, version=self.VERSION,
serializer=None, 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)

View File

@@ -0,0 +1,241 @@
# 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
import ceilometerclient.v2.client as ceclient_v2
from cinderclient import client as ciclient
from cinderclient.v1 import client as ciclient_v1
from glanceclient import client as glclient
from keystoneauth1 import loading as ka_loading
import mock
from neutronclient.neutron import client as netclient
from neutronclient.v2_0 import client as netclient_v2
from novaclient import client as nvclient
from oslo_config import cfg
from watcher.common import clients
from watcher.tests import base
class TestClients(base.BaseTestCase):
def setUp(self):
super(TestClients, self).setUp()
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='nova_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='glance_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='cinder_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='ceilometer_client')
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
group='neutron_client')
def test_get_keystone_session(self):
_AUTH_CONF_GROUP = 'watcher_clients_auth'
ka_loading.register_auth_conf_options(cfg.CONF, _AUTH_CONF_GROUP)
ka_loading.register_session_conf_options(cfg.CONF, _AUTH_CONF_GROUP)
cfg.CONF.set_override('auth_type', 'password',
group=_AUTH_CONF_GROUP)
# If we don't clean up the _AUTH_CONF_GROUP conf options, then other
# tests that run after this one will fail, complaining about required
# options that _AUTH_CONF_GROUP wants.
def cleanup_conf_from_loading():
# oslo_config doesn't seem to allow unregistering groups through a
# single method, so we do this instead
cfg.CONF.reset()
del cfg.CONF._groups[_AUTH_CONF_GROUP]
self.addCleanup(cleanup_conf_from_loading)
osc = clients.OpenStackClients()
expected = {'username': 'foousername',
'password': 'foopassword',
'auth_url': 'http://server.ip:35357',
'user_domain_id': 'foouserdomainid',
'project_domain_id': 'fooprojdomainid'}
def reset_register_opts_mock(conf_obj, original_method):
conf_obj.register_opts = original_method
original_register_opts = cfg.CONF.register_opts
self.addCleanup(reset_register_opts_mock,
cfg.CONF,
original_register_opts)
# Because some of the conf options for auth plugins are not registered
# until right before they are loaded, and because the method that does
# the actual loading of the conf option values is an anonymous method
# (see _getter method of load_from_conf_options in
# keystoneauth1.loading.conf.py), we need to manually monkey patch
# the register opts method so that we can override the conf values to
# our custom values.
def mock_register_opts(*args, **kwargs):
ret = original_register_opts(*args, **kwargs)
if 'group' in kwargs and kwargs['group'] == _AUTH_CONF_GROUP:
for key, value in expected.items():
cfg.CONF.set_override(key, value, group=_AUTH_CONF_GROUP)
return ret
cfg.CONF.register_opts = mock_register_opts
sess = osc.session
self.assertEqual(expected['auth_url'], sess.auth.auth_url)
self.assertEqual(expected['username'], sess.auth._username)
self.assertEqual(expected['password'], sess.auth._password)
self.assertEqual(expected['user_domain_id'], sess.auth._user_domain_id)
self.assertEqual(expected['project_domain_id'],
sess.auth._project_domain_id)
@mock.patch.object(nvclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_nova(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._nova = None
osc.nova()
mock_call.assert_called_once_with(cfg.CONF.nova_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_nova_diff_vers(self, mock_session):
cfg.CONF.set_override('api_version', '2.3',
group='nova_client')
osc = clients.OpenStackClients()
osc._nova = None
osc.nova()
self.assertEqual('2.3', osc.nova().api_version.get_string())
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_nova_cached(self, mock_session):
osc = clients.OpenStackClients()
osc._nova = None
nova = osc.nova()
nova_cached = osc.nova()
self.assertEqual(nova, nova_cached)
@mock.patch.object(glclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
mock_call.assert_called_once_with(cfg.CONF.glance_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance_diff_vers(self, mock_session):
cfg.CONF.set_override('api_version', '1',
group='glance_client')
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
self.assertEqual(1.0, osc.glance().version)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance_cached(self, mock_session):
osc = clients.OpenStackClients()
osc._glance = None
glance = osc.glance()
glance_cached = osc.glance()
self.assertEqual(glance, glance_cached)
@mock.patch.object(ciclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_cinder(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._cinder = None
osc.cinder()
mock_call.assert_called_once_with(cfg.CONF.cinder_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_cinder_diff_vers(self, mock_session):
cfg.CONF.set_override('api_version', '1',
group='cinder_client')
osc = clients.OpenStackClients()
osc._cinder = None
osc.cinder()
self.assertEqual(ciclient_v1.Client, type(osc.cinder()))
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_cinder_cached(self, mock_session):
osc = clients.OpenStackClients()
osc._cinder = None
cinder = osc.cinder()
cinder_cached = osc.cinder()
self.assertEqual(cinder, cinder_cached)
@mock.patch.object(ceclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_ceilometer(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._ceilometer = None
osc.ceilometer()
mock_call.assert_called_once_with(
cfg.CONF.ceilometer_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_alarm_client')
def test_clients_ceilometer_diff_vers(self, mock_get_alarm_client,
mock_session):
'''ceilometerclient currently only has one version (v2)'''
cfg.CONF.set_override('api_version', '2',
group='ceilometer_client')
osc = clients.OpenStackClients()
osc._ceilometer = None
osc.ceilometer()
self.assertEqual(ceclient_v2.Client,
type(osc.ceilometer()))
@mock.patch.object(clients.OpenStackClients, 'session')
@mock.patch.object(ceclient_v2.Client, '_get_alarm_client')
def test_clients_ceilometer_cached(self, mock_get_alarm_client,
mock_session):
osc = clients.OpenStackClients()
osc._ceilometer = None
ceilometer = osc.ceilometer()
ceilometer_cached = osc.ceilometer()
self.assertEqual(ceilometer, ceilometer_cached)
@mock.patch.object(netclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_neutron(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._neutron = None
osc.neutron()
mock_call.assert_called_once_with(cfg.CONF.neutron_client.api_version,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_neutron_diff_vers(self, mock_session):
'''neutronclient currently only has one version (v2)'''
cfg.CONF.set_override('api_version', '2',
group='neutron_client')
osc = clients.OpenStackClients()
osc._neutron = None
osc.neutron()
self.assertEqual(netclient_v2.Client,
type(osc.neutron()))
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_neutron_cached(self, mock_session):
osc = clients.OpenStackClients()
osc._neutron = None
neutron = osc.neutron()
neutron_cached = osc.neutron()
self.assertEqual(neutron, neutron_cached)

View File

@@ -1,68 +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 keystoneclient.auth.identity import Password
from keystoneclient.session import Session
from mock import mock
from oslo_config import cfg
from watcher.common.keystone import KeystoneClient
from watcher.tests.base import BaseTestCase
CONF = cfg.CONF
class TestKeystone(BaseTestCase):
def setUp(self):
super(TestKeystone, self).setUp()
self.ckeystone = KeystoneClient()
@mock.patch('keystoneclient.v2_0.client.Client', autospec=True)
def test_get_endpoint_v2(self, keystone):
expected_endpoint = "http://ip:port/v2"
cfg.CONF.set_override(
'auth_uri', expected_endpoint, group="keystone_authtoken",
enforce_type=True
)
ks = mock.Mock()
ks.service_catalog.url_for.return_value = expected_endpoint
keystone.return_value = ks
ep = self.ckeystone.get_endpoint(service_type='metering',
endpoint_type='publicURL',
region_name='RegionOne')
self.assertEqual(ep, expected_endpoint)
@mock.patch('watcher.common.keystone.KeystoneClient._is_apiv3')
def test_get_session(self, mock_apiv3):
mock_apiv3.return_value = True
k = KeystoneClient()
session = k.get_session()
self.assertIsInstance(session.auth, Password)
self.assertIsInstance(session, Session)
@mock.patch('watcher.common.keystone.KeystoneClient._is_apiv3')
def test_get_credentials(self, mock_apiv3):
mock_apiv3.return_value = True
expected_creds = {'auth_url': None,
'password': None,
'project_domain_name': 'default',
'project_name': 'admin',
'user_domain_name': 'default',
'username': None}
creds = self.ckeystone.get_credentials()
self.assertEqual(creds, expected_creds)

View File

@@ -1,153 +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.
#
import time
import glanceclient.v2.client as glclient
import mock
import novaclient.client as nvclient
from watcher.common import keystone
from watcher.common.nova import NovaClient
from watcher.common import utils
from watcher.tests import base
class TestNovaClient(base.TestCase):
def setUp(self):
super(TestNovaClient, self).setUp()
self.instance_uuid = "fb5311b7-37f3-457e-9cde-6494a3c59bfe"
self.source_hypervisor = "ldev-indeedsrv005"
self.destination_hypervisor = "ldev-indeedsrv006"
self.creds = mock.MagicMock()
self.session = mock.MagicMock()
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
def test_stop_instance(self):
nova_client = NovaClient(creds=self.creds, session=self.session)
instance_id = utils.generate_uuid()
server = mock.MagicMock()
server.id = instance_id
setattr(server, 'OS-EXT-STS:vm_state', 'stopped')
nova_client.nova.servers = mock.MagicMock()
nova_client.nova.servers.find.return_value = server
nova_client.nova.servers.list.return_value = [server]
result = nova_client.stop_instance(instance_id)
self.assertEqual(result, True)
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
def test_set_host_offline(self):
nova_client = NovaClient(creds=self.creds, session=self.session)
host = mock.MagicMock()
nova_client.nova.hosts = mock.MagicMock()
nova_client.nova.hosts.get.return_value = host
result = nova_client.set_host_offline("rennes")
self.assertEqual(result, True)
@mock.patch.object(time, 'sleep', mock.Mock())
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
def test_live_migrate_instance(self):
nova_client = NovaClient(creds=self.creds, session=self.session)
server = mock.MagicMock()
server.id = self.instance_uuid
nova_client.nova.servers = mock.MagicMock()
nova_client.nova.servers.list.return_value = [server]
instance = nova_client.live_migrate_instance(
self.instance_uuid, self.destination_hypervisor
)
self.assertIsNotNone(instance)
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
def test_watcher_non_live_migrate_instance_not_found(self):
nova_client = NovaClient(creds=self.creds, session=self.session)
nova_client.nova.servers.list.return_value = []
nova_client.nova.servers.find.return_value = None
is_success = nova_client.watcher_non_live_migrate_instance(
self.instance_uuid,
self.destination_hypervisor)
self.assertEqual(is_success, False)
@mock.patch.object(time, 'sleep', mock.Mock())
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
def test_watcher_non_live_migrate_instance_volume(self):
nova_client = NovaClient(creds=self.creds, session=self.session)
instance = mock.MagicMock(id=self.instance_uuid)
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_hypervisor)
nova_client.nova.servers.list.return_value = [instance]
nova_client.nova.servers.find.return_value = instance
instance = nova_client.watcher_non_live_migrate_instance(
self.instance_uuid,
self.destination_hypervisor)
self.assertIsNotNone(instance)
@mock.patch.object(time, 'sleep', mock.Mock())
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
def test_watcher_non_live_migrate_keep_image(self):
nova_client = NovaClient(creds=self.creds, session=self.session)
instance = mock.MagicMock(id=self.instance_uuid)
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_hypervisor)
addresses = mock.MagicMock()
type = mock.MagicMock()
networks = []
networks.append(("lan", type))
addresses.items.return_value = networks
attached_volumes = mock.MagicMock()
setattr(instance, 'addresses', addresses)
setattr(instance, "os-extended-volumes:volumes_attached",
attached_volumes)
nova_client.nova.servers.list.return_value = [instance]
nova_client.nova.servers.find.return_value = instance
instance = nova_client.watcher_non_live_migrate_instance(
self.instance_uuid,
self.destination_hypervisor, keep_original_image_name=False)
self.assertIsNotNone(instance)
@mock.patch.object(time, 'sleep', mock.Mock())
@mock.patch.object(keystone, 'KeystoneClient', mock.Mock())
@mock.patch.object(nvclient, "Client", mock.Mock())
@mock.patch.object(glclient, "Client")
def test_create_image_from_instance(self, m_glance_cls):
nova_client = NovaClient(creds=self.creds, session=self.session)
instance = mock.MagicMock()
image = mock.MagicMock()
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_hypervisor)
nova_client.nova.servers.list.return_value = [instance]
nova_client.nova.servers.find.return_value = instance
image_uuid = 'fake-image-uuid'
nova_client.nova.servers.create_image.return_value = image
m_glance = mock.MagicMock()
m_glance_cls.return_value = m_glance
m_glance.images = {image_uuid: image}
instance = nova_client.create_image_from_instance(
self.instance_uuid, "Cirros"
)
self.assertIsNone(instance)

View File

@@ -0,0 +1,144 @@
# -*- 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.
#
import time
import mock
from watcher.common import clients
from watcher.common import nova_helper
from watcher.common import utils
from watcher.tests import base
@mock.patch.object(clients.OpenStackClients, 'nova')
@mock.patch.object(clients.OpenStackClients, 'neutron')
@mock.patch.object(clients.OpenStackClients, 'cinder')
@mock.patch.object(clients.OpenStackClients, 'glance')
class TestNovaHelper(base.TestCase):
def setUp(self):
super(TestNovaHelper, self).setUp()
self.instance_uuid = "fb5311b7-37f3-457e-9cde-6494a3c59bfe"
self.source_hypervisor = "ldev-indeedsrv005"
self.destination_hypervisor = "ldev-indeedsrv006"
def test_stop_instance(self, mock_glance, mock_cinder, mock_neutron,
mock_nova):
nova_util = nova_helper.NovaHelper()
instance_id = utils.generate_uuid()
server = mock.MagicMock()
server.id = instance_id
setattr(server, 'OS-EXT-STS:vm_state', 'stopped')
nova_util.nova.servers = mock.MagicMock()
nova_util.nova.servers.find.return_value = server
nova_util.nova.servers.list.return_value = [server]
result = nova_util.stop_instance(instance_id)
self.assertEqual(result, True)
def test_set_host_offline(self, mock_glance, mock_cinder, mock_neutron,
mock_nova):
nova_util = nova_helper.NovaHelper()
host = mock.MagicMock()
nova_util.nova.hosts = mock.MagicMock()
nova_util.nova.hosts.get.return_value = host
result = nova_util.set_host_offline("rennes")
self.assertEqual(result, True)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_live_migrate_instance(self, mock_glance, mock_cinder,
mock_neutron, mock_nova):
nova_util = nova_helper.NovaHelper()
server = mock.MagicMock()
server.id = self.instance_uuid
nova_util.nova.servers = mock.MagicMock()
nova_util.nova.servers.list.return_value = [server]
instance = nova_util.live_migrate_instance(
self.instance_uuid, self.destination_hypervisor
)
self.assertIsNotNone(instance)
def test_watcher_non_live_migrate_instance_not_found(
self, mock_glance, mock_cinder, mock_neutron, mock_nova):
nova_util = nova_helper.NovaHelper()
nova_util.nova.servers.list.return_value = []
nova_util.nova.servers.find.return_value = None
is_success = nova_util.watcher_non_live_migrate_instance(
self.instance_uuid,
self.destination_hypervisor)
self.assertEqual(is_success, False)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_watcher_non_live_migrate_instance_volume(
self, mock_glance, mock_cinder, mock_neutron, mock_nova):
nova_util = nova_helper.NovaHelper()
instance = mock.MagicMock(id=self.instance_uuid)
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_hypervisor)
nova_util.nova.servers.list.return_value = [instance]
nova_util.nova.servers.find.return_value = instance
instance = nova_util.watcher_non_live_migrate_instance(
self.instance_uuid,
self.destination_hypervisor)
self.assertIsNotNone(instance)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_watcher_non_live_migrate_keep_image(
self, mock_glance, mock_cinder, mock_neutron, mock_nova):
nova_util = nova_helper.NovaHelper()
instance = mock.MagicMock(id=self.instance_uuid)
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_hypervisor)
addresses = mock.MagicMock()
type = mock.MagicMock()
networks = []
networks.append(("lan", type))
addresses.items.return_value = networks
attached_volumes = mock.MagicMock()
setattr(instance, 'addresses', addresses)
setattr(instance, "os-extended-volumes:volumes_attached",
attached_volumes)
nova_util.nova.servers.list.return_value = [instance]
nova_util.nova.servers.find.return_value = instance
instance = nova_util.watcher_non_live_migrate_instance(
self.instance_uuid,
self.destination_hypervisor, keep_original_image_name=False)
self.assertIsNotNone(instance)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_create_image_from_instance(self, mock_glance, mock_cinder,
mock_neutron, mock_nova):
nova_util = nova_helper.NovaHelper()
instance = mock.MagicMock()
image = mock.MagicMock()
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_hypervisor)
nova_util.nova.servers.list.return_value = [instance]
nova_util.nova.servers.find.return_value = instance
image_uuid = 'fake-image-uuid'
nova_util.nova.servers.create_image.return_value = image
glance_client = mock.MagicMock()
mock_glance.return_value = glance_client
glance_client.images = {image_uuid: image}
instance = nova_util.create_image_from_instance(
self.instance_uuid, "Cirros"
)
self.assertIsNone(instance)

View File

@@ -126,7 +126,7 @@ class DbActionTestCase(base.DbTestCase):
def test_update_action_uuid(self): def test_update_action_uuid(self):
action = self._create_test_action() action = self._create_test_action()
self.assertRaises(exception.InvalidParameterValue, self.assertRaises(exception.Invalid,
self.dbapi.update_action, action['id'], self.dbapi.update_action, action['id'],
{'uuid': 'hello'}) {'uuid': 'hello'})

View File

@@ -108,7 +108,7 @@ class DbActionPlanTestCase(base.DbTestCase):
def test_update_action_plan_uuid(self): def test_update_action_plan_uuid(self):
action_plan = self._create_test_action_plan() action_plan = self._create_test_action_plan()
self.assertRaises(exception.InvalidParameterValue, self.assertRaises(exception.Invalid,
self.dbapi.update_action_plan, action_plan['id'], self.dbapi.update_action_plan, action_plan['id'],
{'uuid': 'hello'}) {'uuid': 'hello'})

View File

@@ -92,7 +92,7 @@ class DbAuditTestCase(base.DbTestCase):
name='My Audit Template 1', name='My Audit Template 1',
description='Description of my audit template 1', description='Description of my audit template 1',
host_aggregate=5, host_aggregate=5,
goal='SERVERS_CONSOLIDATION', goal='DUMMY',
extra={'automatic': True}) extra={'automatic': True})
) )
@@ -118,7 +118,7 @@ class DbAuditTestCase(base.DbTestCase):
name='My Audit Template 1', name='My Audit Template 1',
description='Description of my audit template 1', description='Description of my audit template 1',
host_aggregate=5, host_aggregate=5,
goal='SERVERS_CONSOLIDATION', goal='DUMMY',
extra={'automatic': True}) extra={'automatic': True})
) )
@@ -147,7 +147,7 @@ class DbAuditTestCase(base.DbTestCase):
def test_update_audit_uuid(self): def test_update_audit_uuid(self):
audit = self._create_test_audit() audit = self._create_test_audit()
self.assertRaises(exception.InvalidParameterValue, self.assertRaises(exception.Invalid,
self.dbapi.update_audit, audit['id'], self.dbapi.update_audit, audit['id'],
{'uuid': 'hello'}) {'uuid': 'hello'})

View File

@@ -47,7 +47,7 @@ class DbAuditTemplateTestCase(base.DbTestCase):
name='My Audit Template 1', name='My Audit Template 1',
description='Description of my audit template 1', description='Description of my audit template 1',
host_aggregate=5, host_aggregate=5,
goal='SERVERS_CONSOLIDATION', goal='DUMMY',
extra={'automatic': True}) extra={'automatic': True})
audit_template2 = self._create_test_audit_template( audit_template2 = self._create_test_audit_template(
id=2, id=2,
@@ -55,7 +55,7 @@ class DbAuditTemplateTestCase(base.DbTestCase):
name='My Audit Template 2', name='My Audit Template 2',
description='Description of my audit template 2', description='Description of my audit template 2',
host_aggregate=3, host_aggregate=3,
goal='SERVERS_CONSOLIDATION', goal='DUMMY',
extra={'automatic': True}) extra={'automatic': True})
res = self.dbapi.get_audit_template_list(self.context, res = self.dbapi.get_audit_template_list(self.context,
@@ -68,7 +68,7 @@ class DbAuditTemplateTestCase(base.DbTestCase):
res = self.dbapi.get_audit_template_list( res = self.dbapi.get_audit_template_list(
self.context, self.context,
filters={'goal': 'SERVERS_CONSOLIDATION'}) filters={'goal': 'DUMMY'})
self.assertEqual([audit_template1['id'], audit_template2['id']], self.assertEqual([audit_template1['id'], audit_template2['id']],
[r.id for r in res]) [r.id for r in res])
@@ -106,7 +106,7 @@ class DbAuditTemplateTestCase(base.DbTestCase):
def test_update_audit_template_uuid(self): def test_update_audit_template_uuid(self):
audit_template = self._create_test_audit_template() audit_template = self._create_test_audit_template()
self.assertRaises(exception.InvalidParameterValue, self.assertRaises(exception.Invalid,
self.dbapi.update_audit_template, self.dbapi.update_audit_template,
audit_template['id'], audit_template['id'],
{'uuid': 'hello'}) {'uuid': 'hello'})

View File

@@ -63,5 +63,6 @@ class TestDefaultAuditHandler(base.DbTestCase):
'audit_uuid': self.audit.uuid}) 'audit_uuid': self.audit.uuid})
calls = [call_on_going, call_succeeded] calls = [call_on_going, call_succeeded]
messaging.topic_status.publish_event.assert_has_calls(calls) messaging.status_topic_handler.publish_event.assert_has_calls(calls)
self.assertEqual(2, messaging.topic_status.publish_event.call_count) self.assertEqual(
2, messaging.status_topic_handler.publish_event.call_count)

View File

@@ -36,8 +36,8 @@ class TestStrategySelector(TestCase):
enforce_type=True) enforce_type=True)
expected_goal = 'DUMMY' expected_goal = 'DUMMY'
expected_strategy = CONF.watcher_goals.goals[expected_goal] expected_strategy = CONF.watcher_goals.goals[expected_goal]
self.strategy_selector.define_from_goal(expected_goal) self.strategy_selector.define_from_goal(expected_goal, osc=None)
mock_call.assert_called_once_with(expected_strategy) mock_call.assert_called_once_with(expected_strategy, osc=None)
@patch.object(DefaultStrategyLoader, 'load') @patch.object(DefaultStrategyLoader, 'load')
def test_define_from_goal_with_incorrect_mapping(self, mock_call): def test_define_from_goal_with_incorrect_mapping(self, mock_call):

View File

@@ -112,19 +112,19 @@ class TestAuditTemplateObject(base.DbTestCase):
as mock_update_audit_template: as mock_update_audit_template:
audit_template = objects.AuditTemplate.get_by_uuid( audit_template = objects.AuditTemplate.get_by_uuid(
self.context, uuid) self.context, uuid)
audit_template.goal = 'SERVERS_CONSOLIDATION' audit_template.goal = 'DUMMY'
audit_template.save() audit_template.save()
mock_get_audit_template.assert_called_once_with( mock_get_audit_template.assert_called_once_with(
self.context, uuid) self.context, uuid)
mock_update_audit_template.assert_called_once_with( mock_update_audit_template.assert_called_once_with(
uuid, {'goal': 'SERVERS_CONSOLIDATION'}) uuid, {'goal': 'DUMMY'})
self.assertEqual(self.context, audit_template._context) self.assertEqual(self.context, audit_template._context)
def test_refresh(self): def test_refresh(self):
uuid = self.fake_audit_template['uuid'] uuid = self.fake_audit_template['uuid']
returns = [dict(self.fake_audit_template, returns = [dict(self.fake_audit_template,
goal="SERVERS_CONSOLIDATION"), goal="DUMMY"),
dict(self.fake_audit_template, goal="BALANCE_LOAD")] dict(self.fake_audit_template, goal="BALANCE_LOAD")]
expected = [mock.call(self.context, uuid), expected = [mock.call(self.context, uuid),
mock.call(self.context, uuid)] mock.call(self.context, uuid)]
@@ -132,7 +132,7 @@ class TestAuditTemplateObject(base.DbTestCase):
side_effect=returns, side_effect=returns,
autospec=True) as mock_get_audit_template: autospec=True) as mock_get_audit_template:
audit_template = objects.AuditTemplate.get(self.context, uuid) audit_template = objects.AuditTemplate.get(self.context, uuid)
self.assertEqual("SERVERS_CONSOLIDATION", audit_template.goal) self.assertEqual("DUMMY", audit_template.goal)
audit_template.refresh() audit_template.refresh()
self.assertEqual("BALANCE_LOAD", audit_template.goal) self.assertEqual("BALANCE_LOAD", audit_template.goal)
self.assertEqual(expected, mock_get_audit_template.call_args_list) self.assertEqual(expected, mock_get_audit_template.call_args_list)

View File

@@ -79,7 +79,7 @@ class MyObj2(object):
pass pass
class TestSubclassedObject(MyObj): class DummySubclassedObject(MyObj):
fields = {'new_field': str} fields = {'new_field': str}
@@ -172,10 +172,9 @@ class TestUtils(test_base.TestCase):
def test_dt_deserializer(self): def test_dt_deserializer(self):
dt = timeutils.parse_isotime('1955-11-05T00:00:00Z') dt = timeutils.parse_isotime('1955-11-05T00:00:00Z')
self.assertEqual(utils.dt_deserializer(None, timeutils.isotime(dt)), self.assertEqual(utils.dt_deserializer(timeutils.isotime(dt)), dt)
dt) self.assertIsNone(utils.dt_deserializer(None))
self.assertIsNone(utils.dt_deserializer(None, None)) self.assertRaises(ValueError, utils.dt_deserializer, 'foo')
self.assertRaises(ValueError, utils.dt_deserializer, None, 'foo')
def test_obj_to_primitive_list(self): def test_obj_to_primitive_list(self):
class MyList(base.ObjectListBase, base.WatcherObject): class MyList(base.ObjectListBase, base.WatcherObject):
@@ -438,13 +437,13 @@ class _TestObject(object):
base_fields = base.WatcherObject.fields.keys() base_fields = base.WatcherObject.fields.keys()
myobj_fields = ['foo', 'bar', 'missing'] + base_fields myobj_fields = ['foo', 'bar', 'missing'] + base_fields
myobj3_fields = ['new_field'] myobj3_fields = ['new_field']
self.assertTrue(issubclass(TestSubclassedObject, MyObj)) self.assertTrue(issubclass(DummySubclassedObject, MyObj))
self.assertEqual(len(myobj_fields), len(MyObj.fields)) self.assertEqual(len(myobj_fields), len(MyObj.fields))
self.assertEqual(set(myobj_fields), set(MyObj.fields.keys())) self.assertEqual(set(myobj_fields), set(MyObj.fields.keys()))
self.assertEqual(len(myobj_fields) + len(myobj3_fields), self.assertEqual(len(myobj_fields) + len(myobj3_fields),
len(TestSubclassedObject.fields)) len(DummySubclassedObject.fields))
self.assertEqual(set(myobj_fields) | set(myobj3_fields), self.assertEqual(set(myobj_fields) | set(myobj3_fields),
set(TestSubclassedObject.fields.keys())) set(DummySubclassedObject.fields.keys()))
def test_get_changes(self): def test_get_changes(self):
obj = MyObj(self.context) obj = MyObj(self.context)

View File

@@ -55,7 +55,8 @@ Otherwise, if you are not using a virtualenv::
$ cd <TEMPEST_DIR> $ cd <TEMPEST_DIR>
$ tempest init --config-dir ./etc watcher-cloud $ tempest init --config-dir ./etc watcher-cloud
By default the configuration file is empty so before starting, you need to issue the following commands:: By default the configuration file is empty so before starting, you need to
issue the following commands::
$ cd <TEMPEST_DIR>/watcher-cloud/etc $ cd <TEMPEST_DIR>/watcher-cloud/etc
$ cp tempest.conf.sample tempest.conf $ cp tempest.conf.sample tempest.conf
@@ -67,26 +68,50 @@ Shown below is a minimal configuration you need to set within your
For Keystone V3:: For Keystone V3::
[identity]
uri_v3 = http://<KEYSTONE_PUBLIC_ENDPOINT_IP>:<KEYSTONE_PORT>/v3 uri_v3 = http://<KEYSTONE_PUBLIC_ENDPOINT_IP>:<KEYSTONE_PORT>/v3
admin_tenant_name = <ADMIN_TENANT_NAME> auth_version = v3
[auth]
admin_username = <ADMIN_USERNAME> admin_username = <ADMIN_USERNAME>
admin_password = <ADMIN_PASSWORD> admin_password = <ADMIN_PASSWORD>
admin_tenant_name = <ADMIN_TENANT_NAME>
admin_domain_name = <ADMIN_DOMAIN_NAME> admin_domain_name = <ADMIN_DOMAIN_NAME>
[identity-feature-enabled]
api_v2 = false api_v2 = false
api_v3 = true api_v3 = true
auth_version = v3
For Keystone V2:: For Keystone V2::
[identity]
uri = http://<KEYSTONE_PUBLIC_ENDPOINT_IP>:<KEYSTONE_PORT>/v2.0 uri = http://<KEYSTONE_PUBLIC_ENDPOINT_IP>:<KEYSTONE_PORT>/v2.0
auth_version = v2
[auth]
admin_tenant_name = <ADMIN_TENANT_NAME> admin_tenant_name = <ADMIN_TENANT_NAME>
admin_username = <ADMIN_USERNAME> admin_username = <ADMIN_USERNAME>
admin_password = <ADMIN_PASSWORD> admin_password = <ADMIN_PASSWORD>
auth_version = v2
In both cases:: In both cases::
[network]
public_network_id = <PUBLIC_NETWORK_ID> public_network_id = <PUBLIC_NETWORK_ID>
You now have the minimum configuration for running Watcher Tempest tests on a
single node.
Since deploying Watcher with only a single compute node is not very useful, a
few more configuration have to be set in your ``tempest.conf`` file in order to
enable the execution of multi-node scenarios::
[compute]
# To indicate Tempest test that yout have provided enough compute nodes
min_compute_nodes = 2
# Image UUID you can get using the "glance image-list" command
image_ref = <IMAGE_UUID>
For more information, please refer to: For more information, please refer to:

View File

@@ -54,25 +54,6 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
""" """
return self._show_request('audit_templates', audit_template_uuid) return self._show_request('audit_templates', audit_template_uuid)
@base.handle_errors
def filter_audit_template_by_host_aggregate(self, host_aggregate):
"""Gets an audit template associated with given host agregate ID.
:param host_aggregate: Unique identifier of the host aggregate
:return: Serialized audit template as a dictionary.
"""
return self._list_request('/audit_templates',
host_aggregate=host_aggregate)
@base.handle_errors
def filter_audit_template_by_goal(self, goal):
"""Gets an audit template associated with given goal.
:param goal: goal identifier
:return: Serialized audit template as a dictionary.
"""
return self._list_request('/audit_templates', goal=goal)
@base.handle_errors @base.handle_errors
def create_audit_template(self, **kwargs): def create_audit_template(self, **kwargs):
"""Creates an audit template with the specified parameters. """Creates an audit template with the specified parameters.
@@ -139,12 +120,6 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
"""Lists details of all existing audit templates.""" """Lists details of all existing audit templates."""
return self._list_request('/audits/detail', **kwargs) return self._list_request('/audits/detail', **kwargs)
@base.handle_errors
def list_audit_by_audit_template(self, audit_template_uuid):
"""Lists all audits associated with an audit template."""
return self._list_request(
'/audits', audit_template=audit_template_uuid)
@base.handle_errors @base.handle_errors
def show_audit(self, audit_uuid): def show_audit(self, audit_uuid):
"""Gets a specific audit template. """Gets a specific audit template.
@@ -254,3 +229,24 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
:return: Serialized goal as a dictionary :return: Serialized goal as a dictionary
""" """
return self._show_request('/goals', goal) return self._show_request('/goals', goal)
# ### ACTIONS ### #
@base.handle_errors
def list_actions(self, **kwargs):
"""List all existing actions"""
return self._list_request('/actions', **kwargs)
@base.handle_errors
def list_actions_detail(self, **kwargs):
"""Lists details of all existing actions"""
return self._list_request('/actions/detail', **kwargs)
@base.handle_errors
def show_action(self, action_uuid):
"""Gets a specific action
:param action_uuid: Unique identifier of the action
:return: Serialized action as a dictionary
"""
return self._show_request('/actions', action_uuid)

View File

@@ -22,11 +22,6 @@ from tempest_lib import exceptions as lib_exc
from watcher_tempest_plugin import infra_optim_clients as clients from watcher_tempest_plugin import infra_optim_clients as clients
# Resources must be deleted in a specific order, this list
# defines the resource types to clean up, and the correct order.
RESOURCE_TYPES = ['audit_template', 'audit', 'action_plan']
# RESOURCE_TYPES = ['action', 'action_plan', 'audit', 'audit_template']
def creates(resource): def creates(resource):
"""Decorator that adds resources to the appropriate cleanup list.""" """Decorator that adds resources to the appropriate cleanup list."""
@@ -47,6 +42,8 @@ def creates(resource):
class BaseInfraOptimTest(test.BaseTestCase): class BaseInfraOptimTest(test.BaseTestCase):
"""Base class for Infrastructure Optimization API tests.""" """Base class for Infrastructure Optimization API tests."""
RESOURCE_TYPES = ['audit_template', 'audit']
@classmethod @classmethod
def setup_credentials(cls): def setup_credentials(cls):
super(BaseInfraOptimTest, cls).setup_credentials() super(BaseInfraOptimTest, cls).setup_credentials()
@@ -62,19 +59,18 @@ class BaseInfraOptimTest(test.BaseTestCase):
super(BaseInfraOptimTest, cls).resource_setup() super(BaseInfraOptimTest, cls).resource_setup()
cls.created_objects = {} cls.created_objects = {}
for resource in RESOURCE_TYPES: for resource in cls.RESOURCE_TYPES:
cls.created_objects[resource] = set() cls.created_objects[resource] = set()
@classmethod @classmethod
def resource_cleanup(cls): def resource_cleanup(cls):
"""Ensure that all created objects get destroyed.""" """Ensure that all created objects get destroyed."""
try: try:
for resource in RESOURCE_TYPES: for resource in cls.RESOURCE_TYPES:
uuids = cls.created_objects[resource] obj_uuids = cls.created_objects[resource]
delete_method = getattr(cls.client, 'delete_%s' % resource) delete_method = getattr(cls.client, 'delete_%s' % resource)
for u in uuids: for obj_uuid in obj_uuids:
delete_method(u, ignore_errors=lib_exc.NotFound) delete_method(obj_uuid, ignore_errors=lib_exc.NotFound)
finally: finally:
super(BaseInfraOptimTest, cls).resource_cleanup() super(BaseInfraOptimTest, cls).resource_cleanup()
@@ -178,21 +174,6 @@ class BaseInfraOptimTest(test.BaseTestCase):
# ### ACTION PLANS ### # # ### ACTION PLANS ### #
@classmethod
@creates('action_plan')
def start_action_plan(cls, audit_uuid, type='ONESHOT',
state='PENDING', deadline=None):
"""Wrapper utility for creating a test action plan
:param audit_uuid: Audit Template UUID this action plan will use
:param type: Audit type (either ONESHOT or CONTINUOUS)
:return: A tuple with The HTTP response and its body
"""
resp, body = cls.client.create_action_plan(
audit_uuid=audit_uuid, type=type,
state=state, deadline=deadline)
return resp, body
@classmethod @classmethod
def delete_action_plan(cls, action_plan_uuid): def delete_action_plan(cls, action_plan_uuid):
"""Deletes an action plan having the specified UUID """Deletes an action plan having the specified UUID

View File

@@ -0,0 +1,102 @@
# -*- 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 collections
import functools
from tempest import test
from watcher_tempest_plugin.tests.api.admin import base
class TestShowListAction(base.BaseInfraOptimTest):
"""Tests for actions"""
@classmethod
def resource_setup(cls):
super(TestShowListAction, cls).resource_setup()
_, cls.audit_template = cls.create_audit_template()
_, cls.audit = cls.create_audit(cls.audit_template['uuid'])
assert test.call_until_true(
func=functools.partial(cls.has_audit_succeeded, cls.audit['uuid']),
duration=30,
sleep_for=.5
)
_, action_plans = cls.client.list_action_plan_by_audit(
cls.audit['uuid'])
cls.action_plan = action_plans['action_plans'][0]
@test.attr(type='smoke')
def test_show_one_action(self):
_, action = self.client.show_action(
self.action_plan["first_action_uuid"])
self.assertEqual(action['uuid'], self.action_plan["first_action_uuid"])
self.assertEqual(action['action_type'], "nop")
self.assertEqual(action['state'], "PENDING")
@test.attr(type='smoke')
def test_show_action_with_links(self):
_, action = self.client.show_action(
self.action_plan["first_action_uuid"])
self.assertIn('links', action.keys())
self.assertEqual(2, len(action['links']))
self.assertIn(action['uuid'], action['links'][0]['href'])
@test.attr(type="smoke")
def test_list_actions(self):
_, body = self.client.list_actions()
# Verify self links.
for action in body['actions']:
self.validate_self_link('actions', action['uuid'],
action['links'][0]['href'])
@test.attr(type="smoke")
def test_list_actions_by_action_plan(self):
_, body = self.client.list_actions(
action_plan_uuid=self.action_plan["uuid"])
for item in body['actions']:
self.assertEqual(self.action_plan["uuid"],
item['action_plan_uuid'])
action_counter = collections.Counter(
act['action_type'] for act in body['actions'])
# A dummy strategy generates 2 "nop" actions and 1 "sleep" action
self.assertEqual(len(body['actions']), 3)
self.assertEqual(action_counter.get("nop"), 2)
self.assertEqual(action_counter.get("sleep"), 1)
@test.attr(type="smoke")
def test_list_actions_by_audit(self):
_, body = self.client.list_actions(audit_uuid=self.audit["uuid"])
for item in body['actions']:
self.assertEqual(self.action_plan["uuid"],
item['action_plan_uuid'])
action_counter = collections.Counter(
act['action_type'] for act in body['actions'])
# A dummy strategy generates 2 "nop" actions and 1 "sleep" action
self.assertEqual(len(body['actions']), 3)
self.assertEqual(action_counter.get("nop"), 2)
self.assertEqual(action_counter.get("sleep"), 1)

View File

@@ -34,7 +34,7 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=10, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
_, action_plans = self.client.list_action_plan_by_audit(audit['uuid']) _, action_plans = self.client.list_action_plan_by_audit(audit['uuid'])
@@ -52,7 +52,7 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=10, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
_, action_plans = self.client.list_action_plan_by_audit(audit['uuid']) _, action_plans = self.client.list_action_plan_by_audit(audit['uuid'])
@@ -72,7 +72,7 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']), func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=10, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
_, action_plans = self.client.list_action_plan_by_audit(audit['uuid']) _, action_plans = self.client.list_action_plan_by_audit(audit['uuid'])
@@ -89,7 +89,7 @@ class TestCreateDeleteExecuteActionPlan(base.BaseInfraOptimTest):
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial( func=functools.partial(
self.has_action_plan_finished, action_plan['uuid']), self.has_action_plan_finished, action_plan['uuid']),
duration=10, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
_, finished_ap = self.client.show_action_plan(action_plan['uuid']) _, finished_ap = self.client.show_action_plan(action_plan['uuid'])
@@ -109,7 +109,7 @@ class TestShowListActionPlan(base.BaseInfraOptimTest):
assert test.call_until_true( assert test.call_until_true(
func=functools.partial(cls.has_audit_succeeded, cls.audit['uuid']), func=functools.partial(cls.has_audit_succeeded, cls.audit['uuid']),
duration=10, duration=30,
sleep_for=.5 sleep_for=.5
) )
_, action_plans = cls.client.list_action_plan_by_audit( _, action_plans = cls.client.list_action_plan_by_audit(

View File

@@ -179,9 +179,8 @@ class TestShowListAudit(base.BaseInfraOptimTest):
self.assertEqual(len(body['audits']), 3) self.assertEqual(len(body['audits']), 3)
self.assertIn(next_marker, body['next']) self.assertIn(next_marker, body['next'])
# @decorators.skip_because(bug="1533220")
@test.attr(type='smoke') @test.attr(type='smoke')
def test_list_audits_related_to_given_audit_template(self): def test_list_audits_related_to_given_audit_template(self):
_, body = self.client.list_audit_by_audit_template( _, body = self.client.list_audits(
self.audit_template['uuid']) audit_template=self.audit_template['uuid'])
self.assertIn(self.audit['uuid'], [n['uuid'] for n in body['audits']]) self.assertIn(self.audit['uuid'], [n['uuid'] for n in body['audits']])

View File

@@ -88,8 +88,8 @@ class TestAuditTemplate(base.BaseInfraOptimTest):
@decorators.skip_because(bug="1510189") @decorators.skip_because(bug="1510189")
@test.attr(type='smoke') @test.attr(type='smoke')
def test_filter_audit_template_by_goal(self): def test_filter_audit_template_by_goal(self):
_, audit_template = self.client.\ _, audit_template = self.client.list_audit_templates(
filter_audit_template_by_goal(self.audit_template['goal']) goal=self.audit_template['goal'])
self.assert_expected(self.audit_template, self.assert_expected(self.audit_template,
audit_template['audit_templates'][0]) audit_template['audit_templates'][0])
@@ -97,9 +97,8 @@ class TestAuditTemplate(base.BaseInfraOptimTest):
@decorators.skip_because(bug="1510189") @decorators.skip_because(bug="1510189")
@test.attr(type='smoke') @test.attr(type='smoke')
def test_filter_audit_template_by_host_aggregate(self): def test_filter_audit_template_by_host_aggregate(self):
_, audit_template = self.client.\ _, audit_template = self.client.list_audit_templates(
filter_audit_template_by_host_aggregate( host_aggregate=self.audit_template['host_aggregate'])
self.audit_template['host_aggregate'])
self.assert_expected(self.audit_template, self.assert_expected(self.audit_template,
audit_template['audit_templates'][0]) audit_template['audit_templates'][0])

View File

@@ -142,6 +142,10 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
resp, _ = self.client.delete_audit(audit_uuid) resp, _ = self.client.delete_audit(audit_uuid)
return resp return resp
def has_audit_succeeded(self, audit_uuid):
_, audit = self.client.show_audit(audit_uuid)
return audit.get('state') == 'SUCCEEDED'
# ### ACTION PLANS ### # # ### ACTION PLANS ### #
def delete_action_plan(self, action_plan_uuid): def delete_action_plan(self, action_plan_uuid):
@@ -152,3 +156,7 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
""" """
resp, _ = self.client.delete_action_plan(action_plan_uuid) resp, _ = self.client.delete_action_plan(action_plan_uuid)
return resp return resp
def has_action_plan_finished(self, action_plan_uuid):
_, action_plan = self.client.show_action_plan(action_plan_uuid)
return action_plan.get('state') in ('FAILED', 'SUCCEEDED', 'CANCELLED')

View File

@@ -0,0 +1,145 @@
# -*- 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 functools
from tempest import config
from tempest import test
from watcher_tempest_plugin.tests.scenario import base
CONF = config.CONF
class TestExecuteBasicStrategy(base.BaseInfraOptimScenarioTest):
"""Tests for action plans"""
BASIC_GOAL = "BASIC_CONSOLIDATION"
@classmethod
def skip_checks(cls):
super(TestExecuteBasicStrategy, cls).skip_checks()
@classmethod
def resource_setup(cls):
super(TestExecuteBasicStrategy, cls).resource_setup()
if CONF.compute.min_compute_nodes < 2:
raise cls.skipException(
"Less than 2 compute nodes, skipping multinode tests.")
if not CONF.compute_feature_enabled.live_migration:
raise cls.skipException("Live migration is not enabled")
cls.initial_compute_nodes_setup = cls.get_compute_nodes_setup()
enabled_compute_nodes = [cn for cn in cls.initial_compute_nodes_setup
if cn.get('status') == 'enabled']
if len(enabled_compute_nodes) < 2:
raise cls.skipException(
"Less than 2 compute nodes are enabled, "
"skipping multinode tests.")
@classmethod
def get_compute_nodes_setup(cls):
services_client = cls.mgr.services_client
available_services = services_client.list_services()['services']
return [srv for srv in available_services
if srv.get('binary') == 'nova-compute']
@classmethod
def rollback_compute_nodes_status(cls):
current_compute_nodes_setup = cls.get_compute_nodes_setup()
for cn_setup in current_compute_nodes_setup:
cn_hostname = cn_setup.get('host')
matching_cns = [
cns for cns in cls.initial_compute_nodes_setup
if cns.get('host') == cn_hostname
]
initial_cn_setup = matching_cns[0] # Should return a single result
if cn_setup.get('status') != initial_cn_setup.get('status'):
if initial_cn_setup.get('status') == 'enabled':
rollback_func = cls.mgr.services_client.enable_service
else:
rollback_func = cls.mgr.services_client.disable_service
rollback_func(binary='nova-compute', host=cn_hostname)
def _create_one_instance_per_host(self):
"""Create 1 instance per compute node
This goes up to the min_compute_nodes threshold so that things don't
get crazy if you have 1000 compute nodes but set min to 3.
"""
host_client = self.mgr.hosts_client
all_hosts = host_client.list_hosts()['hosts']
compute_nodes = [x for x in all_hosts if x['service'] == 'compute']
for _ in compute_nodes[:CONF.compute.min_compute_nodes]:
# by getting to active state here, this means this has
# landed on the host in question.
self.create_server(image_id=CONF.compute.image_ref,
wait_until='ACTIVE',
clients=self.mgr)
@test.services('compute', 'network', 'telemetry', 'image')
def test_execute_basic_action_plan(self):
"""Execute an action plan based on the BASIC strategy
- create an audit template with the basic strategy
- run the audit to create an action plan
- get the action plan
- run the action plan
- get results and make sure it succeeded
"""
self.addCleanup(self.rollback_compute_nodes_status)
self._create_one_instance_per_host()
_, audit_template = self.create_audit_template(goal=self.BASIC_GOAL)
_, audit = self.create_audit(audit_template['uuid'])
self.assertTrue(test.call_until_true(
func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=300,
sleep_for=2
))
_, action_plans = self.client.list_action_plan_by_audit(audit['uuid'])
action_plan = action_plans['action_plans'][0]
_, action_plan = self.client.show_action_plan(action_plan['uuid'])
# Execute the action by changing its state to TRIGGERED
_, updated_ap = self.client.update_action_plan(
action_plan['uuid'],
patch=[{'path': '/state', 'op': 'replace', 'value': 'TRIGGERED'}]
)
self.assertTrue(test.call_until_true(
func=functools.partial(
self.has_action_plan_finished, action_plan['uuid']),
duration=300,
sleep_for=2
))
_, finished_ap = self.client.show_action_plan(action_plan['uuid'])
_, action_list = self.client.list_actions(
action_plan_uuid=finished_ap["uuid"])
self.assertIn(updated_ap['state'], ('TRIGGERED', 'ONGOING'))
self.assertEqual(finished_ap['state'], 'SUCCEEDED')
for action in action_list['actions']:
self.assertEqual(action.get('state'), 'SUCCEEDED')

View File

@@ -17,6 +17,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import collections
import functools import functools
from tempest import test from tempest import test
@@ -55,15 +56,23 @@ class TestExecuteDummyStrategy(base.BaseInfraOptimScenarioTest):
patch=[{'path': '/state', 'op': 'replace', 'value': 'TRIGGERED'}] patch=[{'path': '/state', 'op': 'replace', 'value': 'TRIGGERED'}]
) )
def has_finished(action_plan_uuid):
return self.has_action_plan_finished(action_plan_uuid)
self.assertTrue(test.call_until_true( self.assertTrue(test.call_until_true(
func=functools.partial(has_finished, action_plan['uuid']), func=functools.partial(
self.has_action_plan_finished, action_plan['uuid']),
duration=30, duration=30,
sleep_for=.5 sleep_for=.5
)) ))
_, finished_ap = self.client.show_action_plan(action_plan['uuid']) _, finished_ap = self.client.show_action_plan(action_plan['uuid'])
_, action_list = self.client.list_actions(
action_plan_uuid=finished_ap["uuid"])
action_counter = collections.Counter(
act['action_type'] for act in action_list['actions'])
self.assertIn(updated_ap['state'], ('TRIGGERED', 'ONGOING')) self.assertIn(updated_ap['state'], ('TRIGGERED', 'ONGOING'))
self.assertEqual(finished_ap['state'], 'SUCCEEDED') self.assertEqual(finished_ap['state'], 'SUCCEEDED')
# A dummy strategy generates 2 "nop" actions and 1 "sleep" action
self.assertEqual(len(action_list['actions']), 3)
self.assertEqual(action_counter.get("nop"), 2)
self.assertEqual(action_counter.get("sleep"), 1)