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

View File

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

View File

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

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'
jsonpatch>=1.1
keystoneauth1>=2.1.0
keystonemiddleware>=2.0.0,!=2.4.0
oslo.config>=2.3.0 # Apache-2.0
oslo.db>=2.4.1 # Apache-2.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,9 +28,10 @@ class ActionFactory(object):
def __init__(self):
self.action_loader = default.DefaultActionLoader()
def make_action(self, object_action):
def make_action(self, object_action, osc=None):
LOG.debug("Creating instance of %s", object_action.action_type)
loaded_action = self.action_loader.load(name=object_action.action_type)
loaded_action = self.action_loader.load(name=object_action.action_type,
osc=osc)
loaded_action.input_parameters = object_action.input_parameters
loaded_action.applies_to = object_action.applies_to
return loaded_action

View File

@@ -21,8 +21,7 @@ from oslo_log import log
from watcher.applier.actions import base
from watcher.common import exception
from watcher.common import keystone as kclient
from watcher.common import nova as nclient
from watcher.common import nova_helper
LOG = log.getLogger(__name__)
@@ -45,18 +44,19 @@ class Migrate(base.BaseAction):
return self.input_parameters.get('src_hypervisor')
def migrate(self, destination):
keystone = kclient.KeystoneClient()
wrapper = nclient.NovaClient(keystone.get_credentials(),
session=keystone.get_session())
LOG.debug("Migrate instance %s to %s ", self.instance_uuid,
nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
destination)
instance = wrapper.find_instance(self.instance_uuid)
instance = nova.find_instance(self.instance_uuid)
if instance:
if self.migration_type == 'live':
return wrapper.live_migrate_instance(
return nova.live_migrate_instance(
instance_id=self.instance_uuid, dest_hostname=destination)
else:
raise exception.InvalidParameterValue(err=self.migration_type)
raise exception.Invalid(
message=(_('Migration of type %(migration_type)s is not '
'supported.') %
{'migration_type': self.migration_type}))
else:
raise exception.InstanceNotFound(name=self.instance_uuid)

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,7 +89,9 @@ class TaskFlowActionContainer(task.Task):
@property
def action(self):
if self.loaded_action is None:
action = self.engine.action_factory.make_action(self._db_action)
action = self.engine.action_factory.make_action(
self._db_action,
osc=self._engine.osc)
self.loaded_action = action
return self.loaded_action

View File

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

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

@@ -0,0 +1,158 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from ceilometerclient import client as ceclient
from cinderclient import client as ciclient
from glanceclient import client as glclient
from keystoneauth1 import loading as ka_loading
from keystoneclient import client as keyclient
from neutronclient.neutron import client as netclient
from novaclient import client as nvclient
from oslo_config import cfg
from watcher._i18n import _
from watcher.common import exception
NOVA_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Nova API to use in novaclient.'))]
GLANCE_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Glance API to use in glanceclient.'))]
CINDER_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Cinder API to use in cinderclient.'))]
CEILOMETER_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help=_('Version of Ceilometer API to use in '
'ceilometerclient.'))]
NEUTRON_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
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_log import log as logging
import six
@@ -40,6 +44,23 @@ CONF = cfg.CONF
CONF.register_opts(exc_log_opts)
def wrap_keystone_exception(func):
"""Wrap keystone exceptions and throw Watcher specific exceptions."""
@functools.wraps(func)
def wrapped(*args, **kw):
try:
return func(*args, **kw)
except keystone_exceptions.AuthorizationFailure:
raise AuthorizationFailure(
client=func.__name__, reason=sys.exc_info()[1])
except keystone_exceptions.ClientException:
raise AuthorizationFailure(
client=func.__name__,
reason=(_('Unexpected keystone client error occurred: %s')
% sys.exc_info()[1]))
return wrapped
class WatcherException(Exception):
"""Base Watcher Exception
@@ -133,12 +154,6 @@ class InvalidGoal(Invalid):
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file")
# Cannot be templated as the error syntax varies.
# msg needs to be constructed when raised.
class InvalidParameterValue(Invalid):
msg_fmt = _("%(err)s")
class InvalidUUID(Invalid):
msg_fmt = _("Expected a uuid but received %(uuid)s")
@@ -179,7 +194,7 @@ class AuditReferenced(Invalid):
class ActionPlanNotFound(ResourceNotFound):
msg_fmt = _("ActionPlan %(action plan)s could not be found")
msg_fmt = _("ActionPlan %(action_plan)s could not be found")
class ActionPlanAlreadyExists(Conflict):
@@ -232,6 +247,10 @@ class NoDataFound(WatcherException):
msg_fmt = _('No rows were returned')
class AuthorizationFailure(WatcherException):
msg_fmt = _('%(client)s connection failed. Reason: %(reason)s')
class KeystoneFailure(WatcherException):
msg_fmt = _("'Keystone API endpoint is missing''")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,8 @@ class BasicConsolidation(BaseStrategy):
MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION,
osc=None):
"""Basic offline Consolidation using live migration
The basic consolidation algorithm has several limitations.
@@ -68,8 +69,9 @@ class BasicConsolidation(BaseStrategy):
:param name: the name 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
self.number_of_released_nodes = 0
@@ -102,7 +104,7 @@ class BasicConsolidation(BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory()
self._ceilometer = CeilometerClusterHistory(osc=self.osc)
return self._ceilometer
@ceilometer.setter

View File

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

View File

@@ -40,7 +40,8 @@ class OutletTempControl(BaseStrategy):
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
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 description: a description of the strategy
:param osc: an OpenStackClients object
"""
super(OutletTempControl, self).__init__(name, description)
super(OutletTempControl, self).__init__(name, description, osc)
# the migration plan will be triggered when the outlet temperature
# reaches threshold
# TODO(zhenzanz): Threshold should be configurable for each audit
@@ -79,7 +81,7 @@ class OutletTempControl(BaseStrategy):
@property
def ceilometer(self):
if self._ceilometer is None:
self._ceilometer = CeilometerClusterHistory()
self._ceilometer = CeilometerClusterHistory(osc=self.osc)
return self._ceilometer
@ceilometer.setter

View File

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

View File

@@ -7,9 +7,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: python-watcher 0.22.1.dev49\n"
"Project-Id-Version: python-watcher 0.23.2.dev1\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -23,7 +23,7 @@ msgstr ""
msgid "Invalid state: %(state)s"
msgstr ""
#: watcher/api/controllers/v1/action_plan.py:416
#: watcher/api/controllers/v1/action_plan.py:420
#, python-format
msgid "State transition not allowed: (%(initial_state)s -> %(new_state)s)"
msgstr ""
@@ -88,6 +88,10 @@ msgstr ""
msgid "The target state is not defined"
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
#, python-format
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"
msgstr ""
#: watcher/common/exception.py:139
#, python-format
msgid "%(err)s"
msgstr ""
#: watcher/common/exception.py:143
#, python-format
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 ""
#: 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 ""
#: watcher/common/exception.py:186

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ class TestActionPlanObject(base.TestCase):
act_plan_dict = api_utils.action_plan_post_data()
del act_plan_dict['state']
del act_plan_dict['audit_id']
del act_plan_dict['first_action_id']
act_plan = api_action_plan.ActionPlan(**act_plan_dict)
self.assertEqual(wtypes.Unset, act_plan.state)
@@ -69,12 +70,20 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json('/action_plans')
self.assertEqual([], response['action_plans'])
def test_get_one(self):
def test_get_one_ok(self):
action_plan = obj_utils.create_action_plan_without_audit(self.context)
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
self.assertEqual(action_plan.uuid, response['uuid'])
self._assert_action_plans_fields(response)
def test_get_one_with_first_action(self):
action_plan = obj_utils.create_test_action_plan(self.context)
action = obj_utils.create_test_action(self.context, id=1)
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
self.assertEqual(action_plan.uuid, response['uuid'])
self.assertEqual(action.uuid, response['first_action_uuid'])
self._assert_action_plans_fields(response)
def test_get_one_soft_deleted(self):
action_plan = obj_utils.create_action_plan_without_audit(self.context)
action_plan.soft_delete()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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):
action = self._create_test_action()
self.assertRaises(exception.InvalidParameterValue,
self.assertRaises(exception.Invalid,
self.dbapi.update_action, action['id'],
{'uuid': 'hello'})

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,8 +36,8 @@ class TestStrategySelector(TestCase):
enforce_type=True)
expected_goal = 'DUMMY'
expected_strategy = CONF.watcher_goals.goals[expected_goal]
self.strategy_selector.define_from_goal(expected_goal)
mock_call.assert_called_once_with(expected_strategy)
self.strategy_selector.define_from_goal(expected_goal, osc=None)
mock_call.assert_called_once_with(expected_strategy, osc=None)
@patch.object(DefaultStrategyLoader, 'load')
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:
audit_template = objects.AuditTemplate.get_by_uuid(
self.context, uuid)
audit_template.goal = 'SERVERS_CONSOLIDATION'
audit_template.goal = 'DUMMY'
audit_template.save()
mock_get_audit_template.assert_called_once_with(
self.context, uuid)
mock_update_audit_template.assert_called_once_with(
uuid, {'goal': 'SERVERS_CONSOLIDATION'})
uuid, {'goal': 'DUMMY'})
self.assertEqual(self.context, audit_template._context)
def test_refresh(self):
uuid = self.fake_audit_template['uuid']
returns = [dict(self.fake_audit_template,
goal="SERVERS_CONSOLIDATION"),
goal="DUMMY"),
dict(self.fake_audit_template, goal="BALANCE_LOAD")]
expected = [mock.call(self.context, uuid),
mock.call(self.context, uuid)]
@@ -132,7 +132,7 @@ class TestAuditTemplateObject(base.DbTestCase):
side_effect=returns,
autospec=True) as mock_get_audit_template:
audit_template = objects.AuditTemplate.get(self.context, uuid)
self.assertEqual("SERVERS_CONSOLIDATION", audit_template.goal)
self.assertEqual("DUMMY", audit_template.goal)
audit_template.refresh()
self.assertEqual("BALANCE_LOAD", audit_template.goal)
self.assertEqual(expected, mock_get_audit_template.call_args_list)

View File

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

View File

@@ -55,7 +55,8 @@ Otherwise, if you are not using a virtualenv::
$ cd <TEMPEST_DIR>
$ 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
$ 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::
[identity]
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_password = <ADMIN_PASSWORD>
admin_tenant_name = <ADMIN_TENANT_NAME>
admin_domain_name = <ADMIN_DOMAIN_NAME>
[identity-feature-enabled]
api_v2 = false
api_v3 = true
auth_version = v3
For Keystone V2::
[identity]
uri = http://<KEYSTONE_PUBLIC_ENDPOINT_IP>:<KEYSTONE_PORT>/v2.0
auth_version = v2
[auth]
admin_tenant_name = <ADMIN_TENANT_NAME>
admin_username = <ADMIN_USERNAME>
admin_password = <ADMIN_PASSWORD>
auth_version = v2
In both cases::
[network]
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:

View File

@@ -54,25 +54,6 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
"""
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
def create_audit_template(self, **kwargs):
"""Creates an audit template with the specified parameters.
@@ -139,12 +120,6 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
"""Lists details of all existing audit templates."""
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
def show_audit(self, audit_uuid):
"""Gets a specific audit template.
@@ -254,3 +229,24 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient):
:return: Serialized goal as a dictionary
"""
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
# 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):
"""Decorator that adds resources to the appropriate cleanup list."""
@@ -47,6 +42,8 @@ def creates(resource):
class BaseInfraOptimTest(test.BaseTestCase):
"""Base class for Infrastructure Optimization API tests."""
RESOURCE_TYPES = ['audit_template', 'audit']
@classmethod
def setup_credentials(cls):
super(BaseInfraOptimTest, cls).setup_credentials()
@@ -62,19 +59,18 @@ class BaseInfraOptimTest(test.BaseTestCase):
super(BaseInfraOptimTest, cls).resource_setup()
cls.created_objects = {}
for resource in RESOURCE_TYPES:
for resource in cls.RESOURCE_TYPES:
cls.created_objects[resource] = set()
@classmethod
def resource_cleanup(cls):
"""Ensure that all created objects get destroyed."""
try:
for resource in RESOURCE_TYPES:
uuids = cls.created_objects[resource]
for resource in cls.RESOURCE_TYPES:
obj_uuids = cls.created_objects[resource]
delete_method = getattr(cls.client, 'delete_%s' % resource)
for u in uuids:
delete_method(u, ignore_errors=lib_exc.NotFound)
for obj_uuid in obj_uuids:
delete_method(obj_uuid, ignore_errors=lib_exc.NotFound)
finally:
super(BaseInfraOptimTest, cls).resource_cleanup()
@@ -178,21 +174,6 @@ class BaseInfraOptimTest(test.BaseTestCase):
# ### 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
def delete_action_plan(cls, action_plan_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(
func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=10,
duration=30,
sleep_for=.5
))
_, 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(
func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=10,
duration=30,
sleep_for=.5
))
_, 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(
func=functools.partial(self.has_audit_succeeded, audit['uuid']),
duration=10,
duration=30,
sleep_for=.5
))
_, 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(
func=functools.partial(
self.has_action_plan_finished, action_plan['uuid']),
duration=10,
duration=30,
sleep_for=.5
))
_, finished_ap = self.client.show_action_plan(action_plan['uuid'])
@@ -109,7 +109,7 @@ class TestShowListActionPlan(base.BaseInfraOptimTest):
assert test.call_until_true(
func=functools.partial(cls.has_audit_succeeded, cls.audit['uuid']),
duration=10,
duration=30,
sleep_for=.5
)
_, 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.assertIn(next_marker, body['next'])
# @decorators.skip_because(bug="1533220")
@test.attr(type='smoke')
def test_list_audits_related_to_given_audit_template(self):
_, body = self.client.list_audit_by_audit_template(
self.audit_template['uuid'])
_, body = self.client.list_audits(
audit_template=self.audit_template['uuid'])
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")
@test.attr(type='smoke')
def test_filter_audit_template_by_goal(self):
_, audit_template = self.client.\
filter_audit_template_by_goal(self.audit_template['goal'])
_, audit_template = self.client.list_audit_templates(
goal=self.audit_template['goal'])
self.assert_expected(self.audit_template,
audit_template['audit_templates'][0])
@@ -97,9 +97,8 @@ class TestAuditTemplate(base.BaseInfraOptimTest):
@decorators.skip_because(bug="1510189")
@test.attr(type='smoke')
def test_filter_audit_template_by_host_aggregate(self):
_, audit_template = self.client.\
filter_audit_template_by_host_aggregate(
self.audit_template['host_aggregate'])
_, audit_template = self.client.list_audit_templates(
host_aggregate=self.audit_template['host_aggregate'])
self.assert_expected(self.audit_template,
audit_template['audit_templates'][0])

View File

@@ -142,6 +142,10 @@ class BaseInfraOptimScenarioTest(manager.ScenarioTest):
resp, _ = self.client.delete_audit(audit_uuid)
return resp
def has_audit_succeeded(self, audit_uuid):
_, audit = self.client.show_audit(audit_uuid)
return audit.get('state') == 'SUCCEEDED'
# ### ACTION PLANS ### #
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)
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
import collections
import functools
from tempest import test
@@ -55,15 +56,23 @@ class TestExecuteDummyStrategy(base.BaseInfraOptimScenarioTest):
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(
func=functools.partial(has_finished, action_plan['uuid']),
func=functools.partial(
self.has_action_plan_finished, action_plan['uuid']),
duration=30,
sleep_for=.5
))
_, 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.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)