Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad40c61ea9 | ||
|
|
1d74f7e3bc | ||
|
|
b7641a9311 | ||
|
|
376d669af6 | ||
|
|
25d27f0288 | ||
|
|
3f4686ce79 | ||
|
|
86c1a9d77f | ||
|
|
9a6811ae6b | ||
|
|
e520f5f452 | ||
|
|
6a25bd983c | ||
|
|
c175ef2170 | ||
|
|
28733a5f30 | ||
|
|
7f8fec1bca | ||
|
|
278b1819d6 | ||
|
|
978bb11d4a | ||
|
|
3027b28942 | ||
|
|
2f0c1c12cf | ||
|
|
e122c61840 | ||
|
|
8f6eac819f | ||
|
|
de307e536e | ||
|
|
7406a1e713 | ||
|
|
982410dd3e |
@@ -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.
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
2
tox.ini
2
tox.ini
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
158
watcher/common/clients.py
Normal 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
|
||||
@@ -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''")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
@@ -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)
|
||||
98
watcher/tests/common/test_ceilometer_helper.py
Normal file
98
watcher/tests/common/test_ceilometer_helper.py
Normal 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)
|
||||
241
watcher/tests/common/test_clients.py
Normal file
241
watcher/tests/common/test_clients.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
144
watcher/tests/common/test_nova_helper.py
Normal file
144
watcher/tests/common/test_nova_helper.py
Normal 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)
|
||||
@@ -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'})
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
102
watcher_tempest_plugin/tests/api/admin/test_action.py
Normal file
102
watcher_tempest_plugin/tests/api/admin/test_action.py
Normal 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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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']])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user