Merge "New default planner"
This commit is contained in:
commit
926dbc8392
@ -64,6 +64,7 @@ watcher_scoring_engine_containers =
|
|||||||
watcher_strategies =
|
watcher_strategies =
|
||||||
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
|
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
|
||||||
dummy_with_scorer = watcher.decision_engine.strategy.strategies.dummy_with_scorer:DummyWithScorer
|
dummy_with_scorer = watcher.decision_engine.strategy.strategies.dummy_with_scorer:DummyWithScorer
|
||||||
|
dummy_with_resize = watcher.decision_engine.strategy.strategies.dummy_with_resize:DummyWithResize
|
||||||
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
||||||
outlet_temperature = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
outlet_temperature = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
||||||
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
|
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
|
||||||
@ -76,12 +77,14 @@ watcher_actions =
|
|||||||
nop = watcher.applier.actions.nop:Nop
|
nop = watcher.applier.actions.nop:Nop
|
||||||
sleep = watcher.applier.actions.sleep:Sleep
|
sleep = watcher.applier.actions.sleep:Sleep
|
||||||
change_nova_service_state = watcher.applier.actions.change_nova_service_state:ChangeNovaServiceState
|
change_nova_service_state = watcher.applier.actions.change_nova_service_state:ChangeNovaServiceState
|
||||||
|
resize = watcher.applier.actions.resize:Resize
|
||||||
|
|
||||||
watcher_workflow_engines =
|
watcher_workflow_engines =
|
||||||
taskflow = watcher.applier.workflow_engine.default:DefaultWorkFlowEngine
|
taskflow = watcher.applier.workflow_engine.default:DefaultWorkFlowEngine
|
||||||
|
|
||||||
watcher_planners =
|
watcher_planners =
|
||||||
default = watcher.decision_engine.planner.default:DefaultPlanner
|
weight = watcher.decision_engine.planner.weight:WeightPlanner
|
||||||
|
workload_stabilization = watcher.decision_engine.planner.workload_stabilization:WorkloadStabilizationPlanner
|
||||||
|
|
||||||
watcher_cluster_data_model_collectors =
|
watcher_cluster_data_model_collectors =
|
||||||
compute = watcher.decision_engine.model.collector.nova:NovaClusterDataModelCollector
|
compute = watcher.decision_engine.model.collector.nova:NovaClusterDataModelCollector
|
||||||
|
@ -88,7 +88,6 @@ class Action(base.APIBase):
|
|||||||
between the internal object model and the API representation of a action.
|
between the internal object model and the API representation of a action.
|
||||||
"""
|
"""
|
||||||
_action_plan_uuid = None
|
_action_plan_uuid = None
|
||||||
_next_uuid = None
|
|
||||||
|
|
||||||
def _get_action_plan_uuid(self):
|
def _get_action_plan_uuid(self):
|
||||||
return self._action_plan_uuid
|
return self._action_plan_uuid
|
||||||
@ -105,22 +104,6 @@ class Action(base.APIBase):
|
|||||||
except exception.ActionPlanNotFound:
|
except exception.ActionPlanNotFound:
|
||||||
self._action_plan_uuid = None
|
self._action_plan_uuid = None
|
||||||
|
|
||||||
def _get_next_uuid(self):
|
|
||||||
return self._next_uuid
|
|
||||||
|
|
||||||
def _set_next_uuid(self, value):
|
|
||||||
if value == wtypes.Unset:
|
|
||||||
self._next_uuid = wtypes.Unset
|
|
||||||
elif value and self._next_uuid != value:
|
|
||||||
try:
|
|
||||||
action_next = objects.Action.get(
|
|
||||||
pecan.request.context, value)
|
|
||||||
self._next_uuid = action_next.uuid
|
|
||||||
self.next = action_next.id
|
|
||||||
except exception.ActionNotFound:
|
|
||||||
self.action_next_uuid = None
|
|
||||||
# raise e
|
|
||||||
|
|
||||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||||
"""Unique UUID for this action"""
|
"""Unique UUID for this action"""
|
||||||
|
|
||||||
@ -138,10 +121,8 @@ class Action(base.APIBase):
|
|||||||
input_parameters = types.jsontype
|
input_parameters = types.jsontype
|
||||||
"""One or more key/value pairs """
|
"""One or more key/value pairs """
|
||||||
|
|
||||||
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
parents = wtypes.wsattr(types.jsontype, readonly=True)
|
||||||
_set_next_uuid,
|
"""UUIDs of parent actions"""
|
||||||
mandatory=True)
|
|
||||||
"""This next action UUID"""
|
|
||||||
|
|
||||||
links = wsme.wsattr([link.Link], readonly=True)
|
links = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""A list containing a self link and associated action links"""
|
"""A list containing a self link and associated action links"""
|
||||||
@ -152,7 +133,6 @@ class Action(base.APIBase):
|
|||||||
self.fields = []
|
self.fields = []
|
||||||
fields = list(objects.Action.fields)
|
fields = list(objects.Action.fields)
|
||||||
fields.append('action_plan_uuid')
|
fields.append('action_plan_uuid')
|
||||||
fields.append('next_uuid')
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
# Skip fields we do not expose.
|
# Skip fields we do not expose.
|
||||||
if not hasattr(self, field):
|
if not hasattr(self, field):
|
||||||
@ -163,15 +143,13 @@ class Action(base.APIBase):
|
|||||||
self.fields.append('action_plan_id')
|
self.fields.append('action_plan_id')
|
||||||
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
|
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
|
||||||
wtypes.Unset))
|
wtypes.Unset))
|
||||||
setattr(self, 'next_uuid', kwargs.get('next',
|
|
||||||
wtypes.Unset))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(action, url, expand=True):
|
def _convert_with_links(action, url, expand=True):
|
||||||
if not expand:
|
if not expand:
|
||||||
action.unset_fields_except(['uuid', 'state', 'next', 'next_uuid',
|
action.unset_fields_except(['uuid', 'state', 'action_plan_uuid',
|
||||||
'action_plan_uuid', 'action_plan_id',
|
'action_plan_id', 'action_type',
|
||||||
'action_type'])
|
'parents'])
|
||||||
|
|
||||||
action.links = [link.Link.make_link('self', url,
|
action.links = [link.Link.make_link('self', url,
|
||||||
'actions', action.uuid),
|
'actions', action.uuid),
|
||||||
@ -193,9 +171,9 @@ class Action(base.APIBase):
|
|||||||
state='PENDING',
|
state='PENDING',
|
||||||
created_at=datetime.datetime.utcnow(),
|
created_at=datetime.datetime.utcnow(),
|
||||||
deleted_at=None,
|
deleted_at=None,
|
||||||
updated_at=datetime.datetime.utcnow())
|
updated_at=datetime.datetime.utcnow(),
|
||||||
|
parents=[])
|
||||||
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||||
sample._next_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
|
||||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||||
|
|
||||||
|
|
||||||
@ -216,17 +194,6 @@ class ActionCollection(collection.Collection):
|
|||||||
collection.actions = [Action.convert_with_links(p, expand)
|
collection.actions = [Action.convert_with_links(p, expand)
|
||||||
for p in actions]
|
for p in actions]
|
||||||
|
|
||||||
if 'sort_key' in kwargs:
|
|
||||||
reverse = False
|
|
||||||
if kwargs['sort_key'] == 'next_uuid':
|
|
||||||
if 'sort_dir' in kwargs:
|
|
||||||
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
|
||||||
collection.actions = sorted(
|
|
||||||
collection.actions,
|
|
||||||
key=lambda action: action.next_uuid or '',
|
|
||||||
reverse=reverse)
|
|
||||||
|
|
||||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -268,9 +235,6 @@ class ActionsController(rest.RestController):
|
|||||||
if audit_uuid:
|
if audit_uuid:
|
||||||
filters['audit_uuid'] = audit_uuid
|
filters['audit_uuid'] = audit_uuid
|
||||||
|
|
||||||
if sort_key == 'next_uuid':
|
|
||||||
sort_db_key = None
|
|
||||||
else:
|
|
||||||
sort_db_key = sort_key
|
sort_db_key = sort_key
|
||||||
|
|
||||||
actions = objects.Action.list(pecan.request.context,
|
actions = objects.Action.list(pecan.request.context,
|
||||||
|
@ -106,7 +106,7 @@ class ActionPlanPatchType(types.JsonPatchType):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mandatory_attrs():
|
def mandatory_attrs():
|
||||||
return ["audit_id", "state", "first_action_id"]
|
return ["audit_id", "state"]
|
||||||
|
|
||||||
|
|
||||||
class ActionPlan(base.APIBase):
|
class ActionPlan(base.APIBase):
|
||||||
@ -120,7 +120,6 @@ class ActionPlan(base.APIBase):
|
|||||||
_audit_uuid = None
|
_audit_uuid = None
|
||||||
_strategy_uuid = None
|
_strategy_uuid = None
|
||||||
_strategy_name = None
|
_strategy_name = None
|
||||||
_first_action_uuid = None
|
|
||||||
_efficacy_indicators = None
|
_efficacy_indicators = None
|
||||||
|
|
||||||
def _get_audit_uuid(self):
|
def _get_audit_uuid(self):
|
||||||
@ -137,21 +136,6 @@ class ActionPlan(base.APIBase):
|
|||||||
except exception.AuditNotFound:
|
except exception.AuditNotFound:
|
||||||
self._audit_uuid = None
|
self._audit_uuid = None
|
||||||
|
|
||||||
def _get_first_action_uuid(self):
|
|
||||||
return self._first_action_uuid
|
|
||||||
|
|
||||||
def _set_first_action_uuid(self, value):
|
|
||||||
if value == wtypes.Unset:
|
|
||||||
self._first_action_uuid = wtypes.Unset
|
|
||||||
elif value and self._first_action_uuid != value:
|
|
||||||
try:
|
|
||||||
first_action = objects.Action.get(pecan.request.context,
|
|
||||||
value)
|
|
||||||
self._first_action_uuid = first_action.uuid
|
|
||||||
self.first_action_id = first_action.id
|
|
||||||
except exception.ActionNotFound:
|
|
||||||
self._first_action_uuid = None
|
|
||||||
|
|
||||||
def _get_efficacy_indicators(self):
|
def _get_efficacy_indicators(self):
|
||||||
if self._efficacy_indicators is None:
|
if self._efficacy_indicators is None:
|
||||||
self._set_efficacy_indicators(wtypes.Unset)
|
self._set_efficacy_indicators(wtypes.Unset)
|
||||||
@ -220,11 +204,6 @@ class ActionPlan(base.APIBase):
|
|||||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||||
"""Unique UUID for this action plan"""
|
"""Unique UUID for this action plan"""
|
||||||
|
|
||||||
first_action_uuid = wsme.wsproperty(
|
|
||||||
types.uuid, _get_first_action_uuid, _set_first_action_uuid,
|
|
||||||
mandatory=True)
|
|
||||||
"""The UUID of the first action this action plans links to"""
|
|
||||||
|
|
||||||
audit_uuid = wsme.wsproperty(types.uuid, _get_audit_uuid, _set_audit_uuid,
|
audit_uuid = wsme.wsproperty(types.uuid, _get_audit_uuid, _set_audit_uuid,
|
||||||
mandatory=True)
|
mandatory=True)
|
||||||
"""The UUID of the audit this port belongs to"""
|
"""The UUID of the audit this port belongs to"""
|
||||||
@ -263,7 +242,6 @@ class ActionPlan(base.APIBase):
|
|||||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||||
|
|
||||||
self.fields.append('audit_uuid')
|
self.fields.append('audit_uuid')
|
||||||
self.fields.append('first_action_uuid')
|
|
||||||
self.fields.append('efficacy_indicators')
|
self.fields.append('efficacy_indicators')
|
||||||
|
|
||||||
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
|
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
|
||||||
@ -271,16 +249,13 @@ class ActionPlan(base.APIBase):
|
|||||||
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
|
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
|
||||||
fields.append('strategy_name')
|
fields.append('strategy_name')
|
||||||
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
|
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
|
||||||
setattr(self, 'first_action_uuid',
|
|
||||||
kwargs.get('first_action_id', wtypes.Unset))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(action_plan, url, expand=True):
|
def _convert_with_links(action_plan, url, expand=True):
|
||||||
if not expand:
|
if not expand:
|
||||||
action_plan.unset_fields_except(
|
action_plan.unset_fields_except(
|
||||||
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
|
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
|
||||||
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name',
|
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name'])
|
||||||
'first_action_uuid'])
|
|
||||||
|
|
||||||
action_plan.links = [
|
action_plan.links = [
|
||||||
link.Link.make_link(
|
link.Link.make_link(
|
||||||
@ -305,7 +280,6 @@ class ActionPlan(base.APIBase):
|
|||||||
created_at=datetime.datetime.utcnow(),
|
created_at=datetime.datetime.utcnow(),
|
||||||
deleted_at=None,
|
deleted_at=None,
|
||||||
updated_at=datetime.datetime.utcnow())
|
updated_at=datetime.datetime.utcnow())
|
||||||
sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720'
|
|
||||||
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
|
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
|
||||||
sample._efficacy_indicators = [{'description': 'Test indicator',
|
sample._efficacy_indicators = [{'description': 'Test indicator',
|
||||||
'name': 'test_indicator',
|
'name': 'test_indicator',
|
||||||
|
106
watcher/applier/actions/resize.py
Normal file
106
watcher/applier/actions/resize.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2017 Servionica
|
||||||
|
#
|
||||||
|
# Authors: Alexander Chadin <a.chadin@servionica.ru>
|
||||||
|
#
|
||||||
|
# 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 oslo_log import log
|
||||||
|
import six
|
||||||
|
import voluptuous
|
||||||
|
|
||||||
|
from watcher._i18n import _, _LC
|
||||||
|
from watcher.applier.actions import base
|
||||||
|
from watcher.common import nova_helper
|
||||||
|
from watcher.common import utils
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Resize(base.BaseAction):
|
||||||
|
"""Resizes a server with specified flavor.
|
||||||
|
|
||||||
|
This action will allow you to resize a server to another flavor.
|
||||||
|
|
||||||
|
The action schema is::
|
||||||
|
|
||||||
|
schema = Schema({
|
||||||
|
'resource_id': str, # should be a UUID
|
||||||
|
'flavor': str, # should be either ID or Name of Flavor
|
||||||
|
})
|
||||||
|
|
||||||
|
The `resource_id` is the UUID of the server to resize.
|
||||||
|
The `flavor` is the ID or Name of Flavor (Nova accepts either ID or Name
|
||||||
|
of Flavor to resize() function).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# input parameters constants
|
||||||
|
FLAVOR = 'flavor'
|
||||||
|
|
||||||
|
def check_resource_id(self, value):
|
||||||
|
if (value is not None and
|
||||||
|
len(value) > 0 and not
|
||||||
|
utils.is_uuid_like(value)):
|
||||||
|
raise voluptuous.Invalid(_("The parameter "
|
||||||
|
"resource_id is invalid."))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schema(self):
|
||||||
|
return voluptuous.Schema({
|
||||||
|
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
||||||
|
voluptuous.Required(self.FLAVOR):
|
||||||
|
voluptuous.All(voluptuous.Any(*six.string_types),
|
||||||
|
voluptuous.Length(min=1)),
|
||||||
|
})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_uuid(self):
|
||||||
|
return self.resource_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flavor(self):
|
||||||
|
return self.input_parameters.get(self.FLAVOR)
|
||||||
|
|
||||||
|
def resize(self):
|
||||||
|
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||||
|
LOG.debug("Resize instance %s to %s flavor", self.instance_uuid,
|
||||||
|
self.flavor)
|
||||||
|
instance = nova.find_instance(self.instance_uuid)
|
||||||
|
result = None
|
||||||
|
if instance:
|
||||||
|
try:
|
||||||
|
result = nova.resize_instance(
|
||||||
|
instance_id=self.instance_uuid, flavor=self.flavor)
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
LOG.critical(
|
||||||
|
_LC("Unexpected error occurred. Resizing failed for "
|
||||||
|
"instance %s."), self.instance_uuid)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
return self.resize()
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
return self.migrate(destination=self.source_node)
|
||||||
|
|
||||||
|
def pre_condition(self):
|
||||||
|
# TODO(jed): check if the instance exists / check if the instance is on
|
||||||
|
# the source_node
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post_condition(self):
|
||||||
|
# TODO(jed): check extra parameters (network response, etc.)
|
||||||
|
pass
|
@ -282,6 +282,10 @@ class ActionFilterCombinationProhibited(Invalid):
|
|||||||
"prohibited")
|
"prohibited")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedActionType(UnsupportedError):
|
||||||
|
msg_fmt = _("Provided %(action_type) is not supported yet")
|
||||||
|
|
||||||
|
|
||||||
class EfficacyIndicatorNotFound(ResourceNotFound):
|
class EfficacyIndicatorNotFound(ResourceNotFound):
|
||||||
msg_fmt = _("Efficacy indicator %(efficacy_indicator)s could not be found")
|
msg_fmt = _("Efficacy indicator %(efficacy_indicator)s could not be found")
|
||||||
|
|
||||||
|
@ -305,6 +305,70 @@ class NovaHelper(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def resize_instance(self, instance_id, flavor, retry=120):
|
||||||
|
"""This method resizes given instance with specified flavor.
|
||||||
|
|
||||||
|
This method uses the Nova built-in resize()
|
||||||
|
action to do a resize of a given instance.
|
||||||
|
|
||||||
|
It returns True if the resize was successful,
|
||||||
|
False otherwise.
|
||||||
|
|
||||||
|
:param instance_id: the unique id of the instance to resize.
|
||||||
|
:param flavor: the name or ID of the flavor to resize to.
|
||||||
|
"""
|
||||||
|
LOG.debug("Trying a resize of instance %s to flavor '%s'" % (
|
||||||
|
instance_id, flavor))
|
||||||
|
|
||||||
|
# Looking for the instance to resize
|
||||||
|
instance = self.find_instance(instance_id)
|
||||||
|
|
||||||
|
flavor_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
flavor_id = self.nova.flavors.get(flavor)
|
||||||
|
except nvexceptions.NotFound:
|
||||||
|
flavor_id = [f.id for f in self.nova.flavors.list() if
|
||||||
|
f.name == flavor][0]
|
||||||
|
except nvexceptions.ClientException as e:
|
||||||
|
LOG.debug("Nova client exception occurred while resizing "
|
||||||
|
"instance %s. Exception: %s", instance_id, e)
|
||||||
|
|
||||||
|
if not flavor_id:
|
||||||
|
LOG.debug("Flavor not found: %s" % flavor)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not instance:
|
||||||
|
LOG.debug("Instance not found: %s" % instance_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
instance_status = getattr(instance, 'OS-EXT-STS:vm_state')
|
||||||
|
LOG.debug(
|
||||||
|
"Instance %s is in '%s' status." % (instance_id,
|
||||||
|
instance_status))
|
||||||
|
|
||||||
|
instance.resize(flavor=flavor_id)
|
||||||
|
while getattr(instance,
|
||||||
|
'OS-EXT-STS:vm_state') != 'resized' \
|
||||||
|
and retry:
|
||||||
|
instance = self.nova.servers.get(instance.id)
|
||||||
|
LOG.debug(
|
||||||
|
'Waiting the resize of {0} to {1}'.format(
|
||||||
|
instance, flavor_id))
|
||||||
|
time.sleep(1)
|
||||||
|
retry -= 1
|
||||||
|
|
||||||
|
instance_status = getattr(instance, 'status')
|
||||||
|
if instance_status != 'VERIFY_RESIZE':
|
||||||
|
return False
|
||||||
|
|
||||||
|
instance.confirm_resize()
|
||||||
|
|
||||||
|
LOG.debug("Resizing succeeded : instance %s is now on flavor "
|
||||||
|
"'%s'.", instance_id, flavor_id)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def live_migrate_instance(self, instance_id, dest_hostname,
|
def live_migrate_instance(self, instance_id, dest_hostname,
|
||||||
block_migration=False, retry=120):
|
block_migration=False, retry=120):
|
||||||
"""This method does a live migration of a given instance
|
"""This method does a live migration of a given instance
|
||||||
@ -645,6 +709,16 @@ class NovaHelper(object):
|
|||||||
|
|
||||||
return network_id
|
return network_id
|
||||||
|
|
||||||
|
def get_instance_by_uuid(self, instance_uuid):
|
||||||
|
return [instance for instance in
|
||||||
|
self.nova.servers.list(search_opts={"all_tenants": True,
|
||||||
|
"uuid": instance_uuid})]
|
||||||
|
|
||||||
|
def get_instance_by_name(self, instance_name):
|
||||||
|
return [instance for instance in
|
||||||
|
self.nova.servers.list(search_opts={"all_tenants": True,
|
||||||
|
"name": instance_name})]
|
||||||
|
|
||||||
def get_instances_by_node(self, host):
|
def get_instances_by_node(self, host):
|
||||||
return [instance for instance in
|
return [instance for instance in
|
||||||
self.nova.servers.list(search_opts={"all_tenants": True})
|
self.nova.servers.list(search_opts={"all_tenants": True})
|
||||||
|
@ -22,7 +22,7 @@ watcher_planner = cfg.OptGroup(name='watcher_planner',
|
|||||||
title='Defines the parameters of '
|
title='Defines the parameters of '
|
||||||
'the planner')
|
'the planner')
|
||||||
|
|
||||||
default_planner = 'default'
|
default_planner = 'weight'
|
||||||
|
|
||||||
WATCHER_PLANNER_OPTS = {
|
WATCHER_PLANNER_OPTS = {
|
||||||
cfg.StrOpt('planner',
|
cfg.StrOpt('planner',
|
||||||
|
@ -746,6 +746,9 @@ class Connection(api.BaseConnection):
|
|||||||
if not values.get('uuid'):
|
if not values.get('uuid'):
|
||||||
values['uuid'] = utils.generate_uuid()
|
values['uuid'] = utils.generate_uuid()
|
||||||
|
|
||||||
|
if values.get('state') is None:
|
||||||
|
values['state'] = objects.action.State.PENDING
|
||||||
|
|
||||||
try:
|
try:
|
||||||
action = self._create(models.Action, values)
|
action = self._create(models.Action, values)
|
||||||
except db_exc.DBDuplicateEntry:
|
except db_exc.DBDuplicateEntry:
|
||||||
|
@ -193,7 +193,6 @@ class ActionPlan(Base):
|
|||||||
)
|
)
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
uuid = Column(String(36))
|
uuid = Column(String(36))
|
||||||
first_action_id = Column(Integer)
|
|
||||||
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False)
|
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False)
|
||||||
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
|
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
|
||||||
state = Column(String(20), nullable=True)
|
state = Column(String(20), nullable=True)
|
||||||
@ -219,7 +218,7 @@ class Action(Base):
|
|||||||
action_type = Column(String(255), nullable=False)
|
action_type = Column(String(255), nullable=False)
|
||||||
input_parameters = Column(JSONEncodedDict, nullable=True)
|
input_parameters = Column(JSONEncodedDict, nullable=True)
|
||||||
state = Column(String(20), nullable=True)
|
state = Column(String(20), nullable=True)
|
||||||
next = Column(String(36), nullable=True)
|
parents = Column(JSONEncodedList, nullable=True)
|
||||||
|
|
||||||
action_plan = orm.relationship(
|
action_plan = orm.relationship(
|
||||||
ActionPlan, foreign_keys=action_plan_id, lazy=None)
|
ActionPlan, foreign_keys=action_plan_id, lazy=None)
|
||||||
|
@ -1,169 +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 oslo_config import cfg
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from watcher._i18n import _LW
|
|
||||||
from watcher.common import utils
|
|
||||||
from watcher.decision_engine.planner import base
|
|
||||||
from watcher import objects
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultPlanner(base.BasePlanner):
|
|
||||||
"""Default planner implementation
|
|
||||||
|
|
||||||
This implementation comes with basic rules with a set of action types that
|
|
||||||
are weighted. An action having a lower weight will be scheduled before the
|
|
||||||
other ones. The set of action types can be specified by 'weights' in the
|
|
||||||
``watcher.conf``. You need to associate a different weight to all available
|
|
||||||
actions into the configuration file, otherwise you will get an error when
|
|
||||||
the new action will be referenced in the solution produced by a strategy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
weights_dict = {
|
|
||||||
'nop': 0,
|
|
||||||
'sleep': 1,
|
|
||||||
'change_nova_service_state': 2,
|
|
||||||
'migrate': 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_config_opts(cls):
|
|
||||||
return [
|
|
||||||
cfg.DictOpt(
|
|
||||||
'weights',
|
|
||||||
help="These weights are used to schedule the actions",
|
|
||||||
default=cls.weights_dict),
|
|
||||||
]
|
|
||||||
|
|
||||||
def create_action(self,
|
|
||||||
action_plan_id,
|
|
||||||
action_type,
|
|
||||||
input_parameters=None):
|
|
||||||
uuid = utils.generate_uuid()
|
|
||||||
action = {
|
|
||||||
'uuid': uuid,
|
|
||||||
'action_plan_id': int(action_plan_id),
|
|
||||||
'action_type': action_type,
|
|
||||||
'input_parameters': input_parameters,
|
|
||||||
'state': objects.action.State.PENDING,
|
|
||||||
'next': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
return action
|
|
||||||
|
|
||||||
def schedule(self, context, audit_id, solution):
|
|
||||||
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
|
|
||||||
priorities = self.config.weights
|
|
||||||
action_plan = self._create_action_plan(context, audit_id, solution)
|
|
||||||
|
|
||||||
actions = list(solution.actions)
|
|
||||||
to_schedule = []
|
|
||||||
for action in actions:
|
|
||||||
json_action = self.create_action(
|
|
||||||
action_plan_id=action_plan.id,
|
|
||||||
action_type=action.get('action_type'),
|
|
||||||
input_parameters=action.get('input_parameters'))
|
|
||||||
to_schedule.append((priorities[action.get('action_type')],
|
|
||||||
json_action))
|
|
||||||
|
|
||||||
self._create_efficacy_indicators(
|
|
||||||
context, action_plan.id, solution.efficacy_indicators)
|
|
||||||
|
|
||||||
# scheduling
|
|
||||||
scheduled = sorted(to_schedule, key=lambda x: (x[0]))
|
|
||||||
if len(scheduled) == 0:
|
|
||||||
LOG.warning(_LW("The action plan is empty"))
|
|
||||||
action_plan.first_action_id = None
|
|
||||||
action_plan.state = objects.action_plan.State.SUCCEEDED
|
|
||||||
action_plan.save()
|
|
||||||
else:
|
|
||||||
# create the first action
|
|
||||||
parent_action = self._create_action(context,
|
|
||||||
scheduled[0][1],
|
|
||||||
None)
|
|
||||||
# remove first
|
|
||||||
scheduled.pop(0)
|
|
||||||
|
|
||||||
action_plan.first_action_id = parent_action.id
|
|
||||||
action_plan.save()
|
|
||||||
|
|
||||||
for s_action in scheduled:
|
|
||||||
current_action = self._create_action(context, s_action[1],
|
|
||||||
parent_action)
|
|
||||||
parent_action = current_action
|
|
||||||
|
|
||||||
return action_plan
|
|
||||||
|
|
||||||
def _create_action_plan(self, context, audit_id, solution):
|
|
||||||
strategy = objects.Strategy.get_by_name(
|
|
||||||
context, solution.strategy.name)
|
|
||||||
|
|
||||||
action_plan_dict = {
|
|
||||||
'uuid': utils.generate_uuid(),
|
|
||||||
'audit_id': audit_id,
|
|
||||||
'strategy_id': strategy.id,
|
|
||||||
'first_action_id': None,
|
|
||||||
'state': objects.action_plan.State.RECOMMENDED,
|
|
||||||
'global_efficacy': solution.global_efficacy,
|
|
||||||
}
|
|
||||||
|
|
||||||
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
|
||||||
new_action_plan.create()
|
|
||||||
|
|
||||||
return new_action_plan
|
|
||||||
|
|
||||||
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
|
||||||
efficacy_indicators = []
|
|
||||||
for indicator in indicators:
|
|
||||||
efficacy_indicator_dict = {
|
|
||||||
'uuid': utils.generate_uuid(),
|
|
||||||
'name': indicator.name,
|
|
||||||
'description': indicator.description,
|
|
||||||
'unit': indicator.unit,
|
|
||||||
'value': indicator.value,
|
|
||||||
'action_plan_id': action_plan_id,
|
|
||||||
}
|
|
||||||
new_efficacy_indicator = objects.EfficacyIndicator(
|
|
||||||
context, **efficacy_indicator_dict)
|
|
||||||
new_efficacy_indicator.create()
|
|
||||||
|
|
||||||
efficacy_indicators.append(new_efficacy_indicator)
|
|
||||||
return efficacy_indicators
|
|
||||||
|
|
||||||
def _create_action(self, context, _action, parent_action):
|
|
||||||
try:
|
|
||||||
LOG.debug("Creating the %s in the Watcher database",
|
|
||||||
_action.get("action_type"))
|
|
||||||
|
|
||||||
new_action = objects.Action(context, **_action)
|
|
||||||
new_action.create()
|
|
||||||
new_action.save()
|
|
||||||
|
|
||||||
if parent_action:
|
|
||||||
parent_action.next = new_action.id
|
|
||||||
parent_action.save()
|
|
||||||
|
|
||||||
return new_action
|
|
||||||
except Exception as exc:
|
|
||||||
LOG.exception(exc)
|
|
||||||
raise
|
|
233
watcher/decision_engine/planner/weight.py
Normal file
233
watcher/decision_engine/planner/weight.py
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Authors: Vincent Francoise <Vincent.FRANCOISE@b-com.com>
|
||||||
|
# Alexander Chadin <a.chadin@servionica.ru>
|
||||||
|
# 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 collections
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_config import types
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _LW
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.decision_engine.planner import base
|
||||||
|
from watcher import objects
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WeightPlanner(base.BasePlanner):
|
||||||
|
"""Weight planner implementation
|
||||||
|
|
||||||
|
This implementation builds actions with parents in accordance with weights.
|
||||||
|
Set of actions having a lower weight will be scheduled before
|
||||||
|
the other ones. There are two config options to configure:
|
||||||
|
action_weights and parallelization.
|
||||||
|
|
||||||
|
*Limitations*
|
||||||
|
|
||||||
|
- This planner requires to have action_weights and parallelization configs
|
||||||
|
tuned well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
super(WeightPlanner, self).__init__(config)
|
||||||
|
|
||||||
|
action_weights = {
|
||||||
|
'turn_host_to_acpi_s3_state': 10,
|
||||||
|
'resize': 20,
|
||||||
|
'migrate': 30,
|
||||||
|
'sleep': 40,
|
||||||
|
'change_nova_service_state': 50,
|
||||||
|
'nop': 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
parallelization = {
|
||||||
|
'turn_host_to_acpi_s3_state': 2,
|
||||||
|
'resize': 2,
|
||||||
|
'migrate': 2,
|
||||||
|
'sleep': 1,
|
||||||
|
'change_nova_service_state': 1,
|
||||||
|
'nop': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_opts(cls):
|
||||||
|
return [
|
||||||
|
cfg.Opt(
|
||||||
|
'weights',
|
||||||
|
type=types.Dict(value_type=types.Integer()),
|
||||||
|
help="These weights are used to schedule the actions. "
|
||||||
|
"Action Plan will be build in accordance with sets of "
|
||||||
|
"actions ordered by descending weights."
|
||||||
|
"Two action types cannot have the same weight. ",
|
||||||
|
default=cls.action_weights),
|
||||||
|
cfg.Opt(
|
||||||
|
'parallelization',
|
||||||
|
type=types.Dict(value_type=types.Integer()),
|
||||||
|
help="Number of actions to be run in parallel on a per "
|
||||||
|
"action type basis.",
|
||||||
|
default=cls.parallelization),
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_action(action_plan_id, action_type,
|
||||||
|
input_parameters=None, parents=()):
|
||||||
|
return {
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'action_plan_id': int(action_plan_id),
|
||||||
|
'action_type': action_type,
|
||||||
|
'input_parameters': input_parameters,
|
||||||
|
'state': objects.action.State.PENDING,
|
||||||
|
'parents': parents or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def chunkify(lst, n):
|
||||||
|
"""Yield successive n-sized chunks from lst."""
|
||||||
|
if n < 1:
|
||||||
|
# Just to make sure the number is valid
|
||||||
|
n = 1
|
||||||
|
|
||||||
|
# Split a flat list in a list of chunks of size n.
|
||||||
|
# e.g. chunkify([0, 1, 2, 3, 4], 2) -> [[0, 1], [2, 3], [4]]
|
||||||
|
for i in range(0, len(lst), n):
|
||||||
|
yield lst[i:i + n]
|
||||||
|
|
||||||
|
def compute_action_graph(self, sorted_weighted_actions):
|
||||||
|
reverse_weights = {v: k for k, v in self.config.weights.items()}
|
||||||
|
# leaf_groups contains a list of list of nodes called groups
|
||||||
|
# each group is a set of nodes from which a future node will
|
||||||
|
# branch off (parent nodes).
|
||||||
|
|
||||||
|
# START --> migrate-1 --> migrate-3
|
||||||
|
# \ \--> resize-1 --> FINISH
|
||||||
|
# \--> migrate-2 -------------/
|
||||||
|
# In the above case migrate-1 will the only memeber of the leaf
|
||||||
|
# group that migrate-3 will use as parent group, whereas
|
||||||
|
# resize-1 will have both migrate-2 and migrate-3 in its
|
||||||
|
# parent/leaf group
|
||||||
|
leaf_groups = []
|
||||||
|
action_graph = nx.DiGraph()
|
||||||
|
# We iterate through each action type category (sorted by weight) to
|
||||||
|
# insert them in a Directed Acyclic Graph
|
||||||
|
for idx, (weight, actions) in enumerate(sorted_weighted_actions):
|
||||||
|
action_chunks = self.chunkify(
|
||||||
|
actions, self.config.parallelization[reverse_weights[weight]])
|
||||||
|
|
||||||
|
# We split the actions into chunks/layers that will have to be
|
||||||
|
# spread across all the available branches of the graph
|
||||||
|
for chunk_idx, actions_chunk in enumerate(action_chunks):
|
||||||
|
for action in actions_chunk:
|
||||||
|
action_graph.add_node(action)
|
||||||
|
|
||||||
|
# all other actions
|
||||||
|
parent_nodes = []
|
||||||
|
if not idx and not chunk_idx:
|
||||||
|
parent_nodes = []
|
||||||
|
elif leaf_groups:
|
||||||
|
parent_nodes = leaf_groups
|
||||||
|
|
||||||
|
for parent_node in parent_nodes:
|
||||||
|
action_graph.add_edge(parent_node, action)
|
||||||
|
action.parents.append(parent_node.uuid)
|
||||||
|
|
||||||
|
if leaf_groups:
|
||||||
|
leaf_groups = []
|
||||||
|
leaf_groups.extend([a for a in actions_chunk])
|
||||||
|
|
||||||
|
return action_graph
|
||||||
|
|
||||||
|
def schedule(self, context, audit_id, solution):
|
||||||
|
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
|
||||||
|
action_plan = self.create_action_plan(context, audit_id, solution)
|
||||||
|
|
||||||
|
sorted_weighted_actions = self.get_sorted_actions_by_weight(
|
||||||
|
context, action_plan, solution)
|
||||||
|
action_graph = self.compute_action_graph(sorted_weighted_actions)
|
||||||
|
|
||||||
|
self._create_efficacy_indicators(
|
||||||
|
context, action_plan.id, solution.efficacy_indicators)
|
||||||
|
|
||||||
|
if len(action_graph.nodes()) == 0:
|
||||||
|
LOG.warning(_LW("The action plan is empty"))
|
||||||
|
action_plan.state = objects.action_plan.State.SUCCEEDED
|
||||||
|
action_plan.save()
|
||||||
|
|
||||||
|
self.create_scheduled_actions(action_plan, action_graph)
|
||||||
|
return action_plan
|
||||||
|
|
||||||
|
def get_sorted_actions_by_weight(self, context, action_plan, solution):
|
||||||
|
# We need to make them immutable to add them to the graph
|
||||||
|
action_objects = list([
|
||||||
|
objects.Action(
|
||||||
|
context, uuid=utils.generate_uuid(), parents=[],
|
||||||
|
action_plan_id=action_plan.id, **a)
|
||||||
|
for a in solution.actions])
|
||||||
|
# This is a dict of list with each being a weight and the list being
|
||||||
|
# all the actions associated to this weight
|
||||||
|
weighted_actions = collections.defaultdict(list)
|
||||||
|
for action in action_objects:
|
||||||
|
action_weight = self.config.weights[action.action_type]
|
||||||
|
weighted_actions[action_weight].append(action)
|
||||||
|
|
||||||
|
return reversed(sorted(weighted_actions.items(), key=lambda x: x[0]))
|
||||||
|
|
||||||
|
def create_scheduled_actions(self, action_plan, graph):
|
||||||
|
for action in graph.nodes():
|
||||||
|
LOG.debug("Creating the %s in the Watcher database",
|
||||||
|
action.action_type)
|
||||||
|
try:
|
||||||
|
action.create()
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_action_plan(self, context, audit_id, solution):
|
||||||
|
strategy = objects.Strategy.get_by_name(
|
||||||
|
context, solution.strategy.name)
|
||||||
|
|
||||||
|
action_plan_dict = {
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'audit_id': audit_id,
|
||||||
|
'strategy_id': strategy.id,
|
||||||
|
'state': objects.action_plan.State.RECOMMENDED,
|
||||||
|
'global_efficacy': solution.global_efficacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||||
|
new_action_plan.create()
|
||||||
|
|
||||||
|
return new_action_plan
|
||||||
|
|
||||||
|
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
||||||
|
efficacy_indicators = []
|
||||||
|
for indicator in indicators:
|
||||||
|
efficacy_indicator_dict = {
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'name': indicator.name,
|
||||||
|
'description': indicator.description,
|
||||||
|
'unit': indicator.unit,
|
||||||
|
'value': indicator.value,
|
||||||
|
'action_plan_id': action_plan_id,
|
||||||
|
}
|
||||||
|
new_efficacy_indicator = objects.EfficacyIndicator(
|
||||||
|
context, **efficacy_indicator_dict)
|
||||||
|
new_efficacy_indicator.create()
|
||||||
|
|
||||||
|
efficacy_indicators.append(new_efficacy_indicator)
|
||||||
|
return efficacy_indicators
|
301
watcher/decision_engine/planner/workload_stabilization.py
Normal file
301
watcher/decision_engine/planner/workload_stabilization.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# 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 abc
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_config import types
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _LW
|
||||||
|
from watcher.common import clients
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.common import nova_helper
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.decision_engine.planner import base
|
||||||
|
from watcher import objects
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkloadStabilizationPlanner(base.BasePlanner):
|
||||||
|
"""Workload Stabilization planner implementation
|
||||||
|
|
||||||
|
This implementation comes with basic rules with a set of action types that
|
||||||
|
are weighted. An action having a lower weight will be scheduled before the
|
||||||
|
other ones. The set of action types can be specified by 'weights' in the
|
||||||
|
``watcher.conf``. You need to associate a different weight to all available
|
||||||
|
actions into the configuration file, otherwise you will get an error when
|
||||||
|
the new action will be referenced in the solution produced by a strategy.
|
||||||
|
|
||||||
|
*Limitations*
|
||||||
|
|
||||||
|
- This is a proof of concept that is not meant to be used in production
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
super(WorkloadStabilizationPlanner, self).__init__(config)
|
||||||
|
self._osc = clients.OpenStackClients()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def osc(self):
|
||||||
|
return self._osc
|
||||||
|
|
||||||
|
weights_dict = {
|
||||||
|
'turn_host_to_acpi_s3_state': 0,
|
||||||
|
'resize': 1,
|
||||||
|
'migrate': 2,
|
||||||
|
'sleep': 3,
|
||||||
|
'change_nova_service_state': 4,
|
||||||
|
'nop': 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_opts(cls):
|
||||||
|
return [
|
||||||
|
cfg.Opt(
|
||||||
|
'weights',
|
||||||
|
type=types.Dict(value_type=types.Integer()),
|
||||||
|
help="These weights are used to schedule the actions",
|
||||||
|
default=cls.weights_dict),
|
||||||
|
]
|
||||||
|
|
||||||
|
def create_action(self,
|
||||||
|
action_plan_id,
|
||||||
|
action_type,
|
||||||
|
input_parameters=None):
|
||||||
|
uuid = utils.generate_uuid()
|
||||||
|
action = {
|
||||||
|
'uuid': uuid,
|
||||||
|
'action_plan_id': int(action_plan_id),
|
||||||
|
'action_type': action_type,
|
||||||
|
'input_parameters': input_parameters,
|
||||||
|
'state': objects.action.State.PENDING,
|
||||||
|
'parents': None
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
|
||||||
|
def load_child_class(self, child_name):
|
||||||
|
for c in BaseActionValidator.__subclasses__():
|
||||||
|
if child_name == c.action_name:
|
||||||
|
return c()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def schedule(self, context, audit_id, solution):
|
||||||
|
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
|
||||||
|
weights = self.config.weights
|
||||||
|
action_plan = self._create_action_plan(context, audit_id, solution)
|
||||||
|
|
||||||
|
actions = list(solution.actions)
|
||||||
|
to_schedule = []
|
||||||
|
for action in actions:
|
||||||
|
json_action = self.create_action(
|
||||||
|
action_plan_id=action_plan.id,
|
||||||
|
action_type=action.get('action_type'),
|
||||||
|
input_parameters=action.get('input_parameters'))
|
||||||
|
to_schedule.append((weights[action.get('action_type')],
|
||||||
|
json_action))
|
||||||
|
|
||||||
|
self._create_efficacy_indicators(
|
||||||
|
context, action_plan.id, solution.efficacy_indicators)
|
||||||
|
|
||||||
|
# scheduling
|
||||||
|
scheduled = sorted(to_schedule, key=lambda weight: (weight[0]),
|
||||||
|
reverse=True)
|
||||||
|
if len(scheduled) == 0:
|
||||||
|
LOG.warning(_LW("The action plan is empty"))
|
||||||
|
action_plan.state = objects.action_plan.State.SUCCEEDED
|
||||||
|
action_plan.save()
|
||||||
|
else:
|
||||||
|
resource_action_map = {}
|
||||||
|
scheduled_actions = [x[1] for x in scheduled]
|
||||||
|
for action in scheduled_actions:
|
||||||
|
a_type = action['action_type']
|
||||||
|
if a_type != 'turn_host_to_acpi_s3_state':
|
||||||
|
plugin_action = self.load_child_class(
|
||||||
|
action.get("action_type"))
|
||||||
|
if not plugin_action:
|
||||||
|
raise exception.UnsupportedActionType(
|
||||||
|
action_type=action.get("action_type"))
|
||||||
|
db_action = self._create_action(context, action)
|
||||||
|
parents = plugin_action.validate_parents(
|
||||||
|
resource_action_map, action)
|
||||||
|
if parents:
|
||||||
|
db_action.parents = parents
|
||||||
|
db_action.save()
|
||||||
|
# if we have an action that will make host unreachable, we need
|
||||||
|
# to complete all actions (resize and migration type)
|
||||||
|
# related to the host.
|
||||||
|
# Note(alexchadin): turn_host_to_acpi_s3_state doesn't
|
||||||
|
# actually exist. Placed code shows relations between
|
||||||
|
# action types.
|
||||||
|
# TODO(alexchadin): add turn_host_to_acpi_s3_state action type.
|
||||||
|
else:
|
||||||
|
host_to_acpi_s3 = action['input_parameters']['resource_id']
|
||||||
|
host_actions = resource_action_map.get(host_to_acpi_s3)
|
||||||
|
action_parents = []
|
||||||
|
if host_actions:
|
||||||
|
resize_actions = [x[0] for x in host_actions
|
||||||
|
if x[1] == 'resize']
|
||||||
|
migrate_actions = [x[0] for x in host_actions
|
||||||
|
if x[1] == 'migrate']
|
||||||
|
resize_migration_parents = [
|
||||||
|
x.parents for x in
|
||||||
|
[objects.Action.get_by_uuid(context, resize_action)
|
||||||
|
for resize_action in resize_actions]]
|
||||||
|
# resize_migration_parents should be one level list
|
||||||
|
resize_migration_parents = [
|
||||||
|
parent for sublist in resize_migration_parents
|
||||||
|
for parent in sublist]
|
||||||
|
action_parents.extend([uuid for uuid in
|
||||||
|
resize_actions])
|
||||||
|
action_parents.extend([uuid for uuid in
|
||||||
|
migrate_actions if uuid not in
|
||||||
|
resize_migration_parents])
|
||||||
|
db_action = self._create_action(context, action)
|
||||||
|
db_action.parents = action_parents
|
||||||
|
db_action.save()
|
||||||
|
|
||||||
|
return action_plan
|
||||||
|
|
||||||
|
def _create_action_plan(self, context, audit_id, solution):
|
||||||
|
strategy = objects.Strategy.get_by_name(
|
||||||
|
context, solution.strategy.name)
|
||||||
|
|
||||||
|
action_plan_dict = {
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'audit_id': audit_id,
|
||||||
|
'strategy_id': strategy.id,
|
||||||
|
'state': objects.action_plan.State.RECOMMENDED,
|
||||||
|
'global_efficacy': solution.global_efficacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||||
|
new_action_plan.create()
|
||||||
|
|
||||||
|
return new_action_plan
|
||||||
|
|
||||||
|
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
||||||
|
efficacy_indicators = []
|
||||||
|
for indicator in indicators:
|
||||||
|
efficacy_indicator_dict = {
|
||||||
|
'uuid': utils.generate_uuid(),
|
||||||
|
'name': indicator.name,
|
||||||
|
'description': indicator.description,
|
||||||
|
'unit': indicator.unit,
|
||||||
|
'value': indicator.value,
|
||||||
|
'action_plan_id': action_plan_id,
|
||||||
|
}
|
||||||
|
new_efficacy_indicator = objects.EfficacyIndicator(
|
||||||
|
context, **efficacy_indicator_dict)
|
||||||
|
new_efficacy_indicator.create()
|
||||||
|
|
||||||
|
efficacy_indicators.append(new_efficacy_indicator)
|
||||||
|
return efficacy_indicators
|
||||||
|
|
||||||
|
def _create_action(self, context, _action):
|
||||||
|
try:
|
||||||
|
LOG.debug("Creating the %s in the Watcher database",
|
||||||
|
_action.get("action_type"))
|
||||||
|
|
||||||
|
new_action = objects.Action(context, **_action)
|
||||||
|
new_action.create()
|
||||||
|
|
||||||
|
return new_action
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class BaseActionValidator(object):
|
||||||
|
action_name = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(BaseActionValidator, self).__init__()
|
||||||
|
self._osc = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def osc(self):
|
||||||
|
if not self._osc:
|
||||||
|
self._osc = clients.OpenStackClients()
|
||||||
|
return self._osc
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def validate_parents(self, resource_action_map, action):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _mapping(self, resource_action_map, resource_id, action_uuid,
|
||||||
|
action_type):
|
||||||
|
if resource_id not in resource_action_map:
|
||||||
|
resource_action_map[resource_id] = [(action_uuid,
|
||||||
|
action_type,)]
|
||||||
|
else:
|
||||||
|
resource_action_map[resource_id].append((action_uuid,
|
||||||
|
action_type,))
|
||||||
|
|
||||||
|
|
||||||
|
class MigrationActionValidator(BaseActionValidator):
|
||||||
|
action_name = "migrate"
|
||||||
|
|
||||||
|
def validate_parents(self, resource_action_map, action):
|
||||||
|
instance_uuid = action['input_parameters']['resource_id']
|
||||||
|
host_name = action['input_parameters']['source_node']
|
||||||
|
self._mapping(resource_action_map, instance_uuid, action['uuid'],
|
||||||
|
'migrate')
|
||||||
|
self._mapping(resource_action_map, host_name, action['uuid'],
|
||||||
|
'migrate')
|
||||||
|
|
||||||
|
|
||||||
|
class ResizeActionValidator(BaseActionValidator):
|
||||||
|
action_name = "resize"
|
||||||
|
|
||||||
|
def validate_parents(self, resource_action_map, action):
|
||||||
|
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||||
|
instance_uuid = action['input_parameters']['resource_id']
|
||||||
|
parent_actions = resource_action_map.get(instance_uuid)
|
||||||
|
host_of_instance = nova.get_hostname(
|
||||||
|
nova.get_instance_by_uuid(instance_uuid)[0])
|
||||||
|
self._mapping(resource_action_map, host_of_instance, action['uuid'],
|
||||||
|
'resize')
|
||||||
|
if parent_actions:
|
||||||
|
return [x[0] for x in parent_actions]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeNovaServiceStateActionValidator(BaseActionValidator):
|
||||||
|
action_name = "change_nova_service_state"
|
||||||
|
|
||||||
|
def validate_parents(self, resource_action_map, action):
|
||||||
|
host_name = action['input_parameters']['resource_id']
|
||||||
|
self._mapping(resource_action_map, host_name, action.uuid,
|
||||||
|
'change_nova_service_state')
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class SleepActionValidator(BaseActionValidator):
|
||||||
|
action_name = "sleep"
|
||||||
|
|
||||||
|
def validate_parents(self, resource_action_map, action):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class NOPActionValidator(BaseActionValidator):
|
||||||
|
action_name = "nop"
|
||||||
|
|
||||||
|
def validate_parents(self, resource_action_map, action):
|
||||||
|
return []
|
119
watcher/decision_engine/strategy/strategies/dummy_with_resize.py
Normal file
119
watcher/decision_engine/strategy/strategies/dummy_with_resize.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# 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 oslo_log import log
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
|
from watcher.decision_engine.strategy.strategies import base
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyWithResize(base.DummyBaseStrategy):
|
||||||
|
"""Dummy strategy used for integration testing via Tempest
|
||||||
|
|
||||||
|
*Description*
|
||||||
|
|
||||||
|
This strategy does not provide any useful optimization. Its only purpose
|
||||||
|
is to be used by Tempest tests.
|
||||||
|
|
||||||
|
*Requirements*
|
||||||
|
|
||||||
|
<None>
|
||||||
|
|
||||||
|
*Limitations*
|
||||||
|
|
||||||
|
Do not use in production.
|
||||||
|
|
||||||
|
*Spec URL*
|
||||||
|
|
||||||
|
<None>
|
||||||
|
"""
|
||||||
|
|
||||||
|
NOP = "nop"
|
||||||
|
SLEEP = "sleep"
|
||||||
|
|
||||||
|
def pre_execute(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_execute(self):
|
||||||
|
para1 = self.input_parameters.para1
|
||||||
|
para2 = self.input_parameters.para2
|
||||||
|
LOG.debug("Executing Dummy strategy with para1=%(p1)f, para2=%(p2)s",
|
||||||
|
{'p1': para1, 'p2': para2})
|
||||||
|
parameters = {'message': 'hello World'}
|
||||||
|
self.solution.add_action(action_type=self.NOP,
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
parameters = {'message': 'Welcome'}
|
||||||
|
self.solution.add_action(action_type=self.NOP,
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
self.solution.add_action(action_type=self.SLEEP,
|
||||||
|
input_parameters={'duration': 5.0})
|
||||||
|
self.solution.add_action(
|
||||||
|
action_type='migrate',
|
||||||
|
resource_id='b199db0c-1408-4d52-b5a5-5ca14de0ff36',
|
||||||
|
input_parameters={
|
||||||
|
'source_node': 'server1',
|
||||||
|
'destination_node': 'server2'})
|
||||||
|
|
||||||
|
self.solution.add_action(
|
||||||
|
action_type='migrate',
|
||||||
|
resource_id='8db1b3c1-7938-4c34-8c03-6de14b874f8f',
|
||||||
|
input_parameters={
|
||||||
|
'source_node': 'server1',
|
||||||
|
'destination_node': 'server2'}
|
||||||
|
)
|
||||||
|
self.solution.add_action(
|
||||||
|
action_type='resize',
|
||||||
|
resource_id='8db1b3c1-7938-4c34-8c03-6de14b874f8f',
|
||||||
|
input_parameters={'flavor': 'x1'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_execute(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_name(cls):
|
||||||
|
return "dummy_with_resize"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_display_name(cls):
|
||||||
|
return _("Dummy strategy with resize")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_translatable_display_name(cls):
|
||||||
|
return "Dummy strategy with resize"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_schema(cls):
|
||||||
|
# Mandatory default setting for each element
|
||||||
|
return {
|
||||||
|
"properties": {
|
||||||
|
"para1": {
|
||||||
|
"description": "number parameter example",
|
||||||
|
"type": "number",
|
||||||
|
"default": 3.2,
|
||||||
|
"minimum": 1.0,
|
||||||
|
"maximum": 10.2,
|
||||||
|
},
|
||||||
|
"para2": {
|
||||||
|
"description": "string parameter example",
|
||||||
|
"type": "string",
|
||||||
|
"default": "hello"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -37,7 +37,8 @@ class Action(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
|
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
# Version 1.1: Added 'action_plan' object field
|
# Version 1.1: Added 'action_plan' object field
|
||||||
VERSION = '1.1'
|
# Version 2.0: Removed 'next' object field, Added 'parents' object field
|
||||||
|
VERSION = '2.0'
|
||||||
|
|
||||||
dbapi = db_api.get_instance()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ class Action(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
'action_type': wfields.StringField(nullable=True),
|
'action_type': wfields.StringField(nullable=True),
|
||||||
'input_parameters': wfields.DictField(nullable=True),
|
'input_parameters': wfields.DictField(nullable=True),
|
||||||
'state': wfields.StringField(nullable=True),
|
'state': wfields.StringField(nullable=True),
|
||||||
'next': wfields.IntegerField(nullable=True),
|
'parents': wfields.ListOfStringsField(nullable=True),
|
||||||
|
|
||||||
'action_plan': wfields.ObjectField('ActionPlan', nullable=True),
|
'action_plan': wfields.ObjectField('ActionPlan', nullable=True),
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
# Version 1.1: Added 'audit' and 'strategy' object field
|
# Version 1.1: Added 'audit' and 'strategy' object field
|
||||||
# Version 1.2: audit_id is not nullable anymore
|
# Version 1.2: audit_id is not nullable anymore
|
||||||
VERSION = '1.2'
|
# Version 2.0: Removed 'first_action_id' object field
|
||||||
|
VERSION = '2.0'
|
||||||
|
|
||||||
dbapi = db_api.get_instance()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -104,7 +105,6 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
'uuid': wfields.UUIDField(),
|
'uuid': wfields.UUIDField(),
|
||||||
'audit_id': wfields.IntegerField(),
|
'audit_id': wfields.IntegerField(),
|
||||||
'strategy_id': wfields.IntegerField(),
|
'strategy_id': wfields.IntegerField(),
|
||||||
'first_action_id': wfields.IntegerField(nullable=True),
|
|
||||||
'state': wfields.StringField(nullable=True),
|
'state': wfields.StringField(nullable=True),
|
||||||
'global_efficacy': wfields.FlexibleDictField(nullable=True),
|
'global_efficacy': wfields.FlexibleDictField(nullable=True),
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ def post_get_test_action(**kw):
|
|||||||
del action['action_plan_id']
|
del action['action_plan_id']
|
||||||
action['action_plan_uuid'] = kw.get('action_plan_uuid',
|
action['action_plan_uuid'] = kw.get('action_plan_uuid',
|
||||||
action_plan['uuid'])
|
action_plan['uuid'])
|
||||||
action['next'] = None
|
action['parents'] = None
|
||||||
return action
|
return action
|
||||||
|
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ class TestActionObject(base.TestCase):
|
|||||||
|
|
||||||
def test_action_init(self):
|
def test_action_init(self):
|
||||||
action_dict = api_utils.action_post_data(action_plan_id=None,
|
action_dict = api_utils.action_post_data(action_plan_id=None,
|
||||||
next=None)
|
parents=None)
|
||||||
del action_dict['state']
|
del action_dict['state']
|
||||||
action = api_action.Action(**action_dict)
|
action = api_action.Action(**action_dict)
|
||||||
self.assertEqual(wtypes.Unset, action.state)
|
self.assertEqual(wtypes.Unset, action.state)
|
||||||
@ -67,13 +67,13 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self.assertIn(field, action)
|
self.assertIn(field, action)
|
||||||
|
|
||||||
def test_one(self):
|
def test_one(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
response = self.get_json('/actions')
|
response = self.get_json('/actions')
|
||||||
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
||||||
self._assert_action_fields(response['actions'][0])
|
self._assert_action_fields(response['actions'][0])
|
||||||
|
|
||||||
def test_one_soft_deleted(self):
|
def test_one_soft_deleted(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
action.soft_delete()
|
action.soft_delete()
|
||||||
response = self.get_json('/actions',
|
response = self.get_json('/actions',
|
||||||
headers={'X-Show-Deleted': 'True'})
|
headers={'X-Show-Deleted': 'True'})
|
||||||
@ -84,7 +84,7 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self.assertEqual([], response['actions'])
|
self.assertEqual([], response['actions'])
|
||||||
|
|
||||||
def test_get_one(self):
|
def test_get_one(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
response = self.get_json('/actions/%s' % action['uuid'])
|
response = self.get_json('/actions/%s' % action['uuid'])
|
||||||
self.assertEqual(action.uuid, response['uuid'])
|
self.assertEqual(action.uuid, response['uuid'])
|
||||||
self.assertEqual(action.action_type, response['action_type'])
|
self.assertEqual(action.action_type, response['action_type'])
|
||||||
@ -92,7 +92,7 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self._assert_action_fields(response)
|
self._assert_action_fields(response)
|
||||||
|
|
||||||
def test_get_one_soft_deleted(self):
|
def test_get_one_soft_deleted(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
action.soft_delete()
|
action.soft_delete()
|
||||||
response = self.get_json('/actions/%s' % action['uuid'],
|
response = self.get_json('/actions/%s' % action['uuid'],
|
||||||
headers={'X-Show-Deleted': 'True'})
|
headers={'X-Show-Deleted': 'True'})
|
||||||
@ -104,13 +104,13 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self.assertEqual(404, response.status_int)
|
self.assertEqual(404, response.status_int)
|
||||||
|
|
||||||
def test_detail(self):
|
def test_detail(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
response = self.get_json('/actions/detail')
|
response = self.get_json('/actions/detail')
|
||||||
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
||||||
self._assert_action_fields(response['actions'][0])
|
self._assert_action_fields(response['actions'][0])
|
||||||
|
|
||||||
def test_detail_soft_deleted(self):
|
def test_detail_soft_deleted(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
action.soft_delete()
|
action.soft_delete()
|
||||||
response = self.get_json('/actions/detail',
|
response = self.get_json('/actions/detail',
|
||||||
headers={'X-Show-Deleted': 'True'})
|
headers={'X-Show-Deleted': 'True'})
|
||||||
@ -121,7 +121,7 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self.assertEqual([], response['actions'])
|
self.assertEqual([], response['actions'])
|
||||||
|
|
||||||
def test_detail_against_single(self):
|
def test_detail_against_single(self):
|
||||||
action = obj_utils.create_test_action(self.context, next=None)
|
action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
response = self.get_json('/actions/%s/detail' % action['uuid'],
|
response = self.get_json('/actions/%s/detail' % action['uuid'],
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(404, response.status_int)
|
self.assertEqual(404, response.status_int)
|
||||||
@ -312,18 +312,23 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
set([act['uuid'] for act in response['actions']
|
set([act['uuid'] for act in response['actions']
|
||||||
if act['action_plan_uuid'] == action_plan2.uuid]))
|
if act['action_plan_uuid'] == action_plan2.uuid]))
|
||||||
|
|
||||||
def test_many_with_next_uuid(self):
|
def test_many_with_parents(self):
|
||||||
action_list = []
|
action_list = []
|
||||||
for id_ in range(5):
|
for id_ in range(5):
|
||||||
action = obj_utils.create_test_action(self.context, id=id_,
|
if id_ > 0:
|
||||||
uuid=utils.generate_uuid(),
|
action = obj_utils.create_test_action(
|
||||||
next=id_ + 1)
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
parents=[action_list[id_ - 1]])
|
||||||
|
else:
|
||||||
|
action = obj_utils.create_test_action(
|
||||||
|
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||||
|
parents=[])
|
||||||
action_list.append(action.uuid)
|
action_list.append(action.uuid)
|
||||||
response = self.get_json('/actions')
|
response = self.get_json('/actions')
|
||||||
response_actions = response['actions']
|
response_actions = response['actions']
|
||||||
for id_ in range(4):
|
for id_ in range(4):
|
||||||
self.assertEqual(response_actions[id_]['next_uuid'],
|
self.assertEqual(response_actions[id_]['uuid'],
|
||||||
response_actions[id_ + 1]['uuid'])
|
response_actions[id_ + 1]['parents'][0])
|
||||||
|
|
||||||
def test_many_without_soft_deleted(self):
|
def test_many_without_soft_deleted(self):
|
||||||
action_list = []
|
action_list = []
|
||||||
@ -357,30 +362,6 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
uuids = [s['uuid'] for s in response['actions']]
|
uuids = [s['uuid'] for s in response['actions']]
|
||||||
self.assertEqual(sorted(action_list), sorted(uuids))
|
self.assertEqual(sorted(action_list), sorted(uuids))
|
||||||
|
|
||||||
def test_many_with_sort_key_next_uuid(self):
|
|
||||||
for id_ in range(5):
|
|
||||||
obj_utils.create_test_action(self.context, id=id_,
|
|
||||||
uuid=utils.generate_uuid(),
|
|
||||||
next=id_ + 1)
|
|
||||||
response = self.get_json('/actions/')
|
|
||||||
reference_uuids = [
|
|
||||||
s.get('next_uuid', '') for s in response['actions']
|
|
||||||
]
|
|
||||||
|
|
||||||
response = self.get_json('/actions/?sort_key=next_uuid')
|
|
||||||
|
|
||||||
self.assertEqual(5, len(response['actions']))
|
|
||||||
uuids = [(s['next_uuid'] if 'next_uuid' in s else '')
|
|
||||||
for s in response['actions']]
|
|
||||||
self.assertEqual(sorted(reference_uuids), uuids)
|
|
||||||
|
|
||||||
response = self.get_json('/actions/?sort_key=next_uuid&sort_dir=desc')
|
|
||||||
|
|
||||||
self.assertEqual(5, len(response['actions']))
|
|
||||||
uuids = [(s['next_uuid'] if 'next_uuid' in s else '')
|
|
||||||
for s in response['actions']]
|
|
||||||
self.assertEqual(sorted(reference_uuids, reverse=True), uuids)
|
|
||||||
|
|
||||||
def test_links(self):
|
def test_links(self):
|
||||||
uuid = utils.generate_uuid()
|
uuid = utils.generate_uuid()
|
||||||
obj_utils.create_test_action(self.context, id=1, uuid=uuid)
|
obj_utils.create_test_action(self.context, id=1, uuid=uuid)
|
||||||
@ -393,18 +374,15 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
|
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
|
||||||
|
|
||||||
def test_collection_links(self):
|
def test_collection_links(self):
|
||||||
next = -1
|
parents = None
|
||||||
for id_ in range(5):
|
for id_ in range(5):
|
||||||
action = obj_utils.create_test_action(self.context, id=id_,
|
action = obj_utils.create_test_action(self.context, id=id_,
|
||||||
uuid=utils.generate_uuid(),
|
uuid=utils.generate_uuid(),
|
||||||
next=next)
|
parents=parents)
|
||||||
next = action.id
|
parents = [action.id]
|
||||||
response = self.get_json('/actions/?limit=3')
|
response = self.get_json('/actions/?limit=3')
|
||||||
self.assertEqual(3, len(response['actions']))
|
self.assertEqual(3, len(response['actions']))
|
||||||
|
|
||||||
next_marker = response['actions'][-1]['uuid']
|
|
||||||
self.assertIn(next_marker, response['next'])
|
|
||||||
|
|
||||||
def test_collection_links_default_limit(self):
|
def test_collection_links_default_limit(self):
|
||||||
cfg.CONF.set_override('max_limit', 3, 'api',
|
cfg.CONF.set_override('max_limit', 3, 'api',
|
||||||
enforce_type=True)
|
enforce_type=True)
|
||||||
@ -414,9 +392,6 @@ class TestListAction(api_base.FunctionalTest):
|
|||||||
response = self.get_json('/actions')
|
response = self.get_json('/actions')
|
||||||
self.assertEqual(3, len(response['actions']))
|
self.assertEqual(3, len(response['actions']))
|
||||||
|
|
||||||
next_marker = response['actions'][-1]['uuid']
|
|
||||||
self.assertIn(next_marker, response['next'])
|
|
||||||
|
|
||||||
|
|
||||||
class TestPatch(api_base.FunctionalTest):
|
class TestPatch(api_base.FunctionalTest):
|
||||||
|
|
||||||
@ -426,7 +401,7 @@ class TestPatch(api_base.FunctionalTest):
|
|||||||
obj_utils.create_test_strategy(self.context)
|
obj_utils.create_test_strategy(self.context)
|
||||||
obj_utils.create_test_audit(self.context)
|
obj_utils.create_test_audit(self.context)
|
||||||
obj_utils.create_test_action_plan(self.context)
|
obj_utils.create_test_action_plan(self.context)
|
||||||
self.action = obj_utils.create_test_action(self.context, next=None)
|
self.action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
p = mock.patch.object(db_api.BaseConnection, 'update_action')
|
p = mock.patch.object(db_api.BaseConnection, 'update_action')
|
||||||
self.mock_action_update = p.start()
|
self.mock_action_update = p.start()
|
||||||
self.mock_action_update.side_effect = self._simulate_rpc_action_update
|
self.mock_action_update.side_effect = self._simulate_rpc_action_update
|
||||||
@ -461,7 +436,7 @@ class TestDelete(api_base.FunctionalTest):
|
|||||||
self.strategy = obj_utils.create_test_strategy(self.context)
|
self.strategy = obj_utils.create_test_strategy(self.context)
|
||||||
self.audit = obj_utils.create_test_audit(self.context)
|
self.audit = obj_utils.create_test_audit(self.context)
|
||||||
self.action_plan = obj_utils.create_test_action_plan(self.context)
|
self.action_plan = obj_utils.create_test_action_plan(self.context)
|
||||||
self.action = obj_utils.create_test_action(self.context, next=None)
|
self.action = obj_utils.create_test_action(self.context, parents=None)
|
||||||
p = mock.patch.object(db_api.BaseConnection, 'update_action')
|
p = mock.patch.object(db_api.BaseConnection, 'update_action')
|
||||||
self.mock_action_update = p.start()
|
self.mock_action_update = p.start()
|
||||||
self.mock_action_update.side_effect = self._simulate_rpc_action_update
|
self.mock_action_update.side_effect = self._simulate_rpc_action_update
|
||||||
|
@ -77,14 +77,6 @@ class TestListActionPlan(api_base.FunctionalTest):
|
|||||||
'unit': '%'}],
|
'unit': '%'}],
|
||||||
response['efficacy_indicators'])
|
response['efficacy_indicators'])
|
||||||
|
|
||||||
def test_get_one_with_first_action(self):
|
|
||||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
|
||||||
action = obj_utils.create_test_action(self.context, id=1)
|
|
||||||
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
|
|
||||||
self.assertEqual(action_plan.uuid, response['uuid'])
|
|
||||||
self.assertEqual(action.uuid, response['first_action_uuid'])
|
|
||||||
self._assert_action_plans_fields(response)
|
|
||||||
|
|
||||||
def test_get_one_soft_deleted(self):
|
def test_get_one_soft_deleted(self):
|
||||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
action_plan = obj_utils.create_test_action_plan(self.context)
|
||||||
action_plan.soft_delete()
|
action_plan.soft_delete()
|
||||||
@ -322,7 +314,7 @@ class TestDelete(api_base.FunctionalTest):
|
|||||||
|
|
||||||
def test_delete_action_plan_with_action(self):
|
def test_delete_action_plan_with_action(self):
|
||||||
action = obj_utils.create_test_action(
|
action = obj_utils.create_test_action(
|
||||||
self.context, id=self.action_plan.first_action_id)
|
self.context, id=1)
|
||||||
|
|
||||||
self.delete('/action_plans/%s' % self.action_plan.uuid)
|
self.delete('/action_plans/%s' % self.action_plan.uuid)
|
||||||
ap_response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
|
ap_response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
|
||||||
|
102
watcher/tests/applier/actions/test_resize.py
Normal file
102
watcher/tests/applier/actions/test_resize.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import voluptuous
|
||||||
|
|
||||||
|
from watcher.applier.actions import base as baction
|
||||||
|
from watcher.applier.actions import resize
|
||||||
|
from watcher.common import clients
|
||||||
|
from watcher.common import nova_helper
|
||||||
|
from watcher.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestResize(base.TestCase):
|
||||||
|
|
||||||
|
INSTANCE_UUID = "94ae2f92-b7fd-4da7-9e97-f13504ae98c4"
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestResize, self).setUp()
|
||||||
|
|
||||||
|
self.r_osc_cls = mock.Mock()
|
||||||
|
self.r_helper_cls = mock.Mock()
|
||||||
|
self.r_helper = mock.Mock(spec=nova_helper.NovaHelper)
|
||||||
|
self.r_helper_cls.return_value = self.r_helper
|
||||||
|
self.r_osc = mock.Mock(spec=clients.OpenStackClients)
|
||||||
|
self.r_osc_cls.return_value = self.r_osc
|
||||||
|
|
||||||
|
r_openstack_clients = mock.patch.object(
|
||||||
|
clients, "OpenStackClients", self.r_osc_cls)
|
||||||
|
r_nova_helper = mock.patch.object(
|
||||||
|
nova_helper, "NovaHelper", self.r_helper_cls)
|
||||||
|
|
||||||
|
r_openstack_clients.start()
|
||||||
|
r_nova_helper.start()
|
||||||
|
|
||||||
|
self.addCleanup(r_openstack_clients.stop)
|
||||||
|
self.addCleanup(r_nova_helper.stop)
|
||||||
|
|
||||||
|
self.input_parameters = {
|
||||||
|
"flavor": "x1",
|
||||||
|
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
|
||||||
|
}
|
||||||
|
self.action = resize.Resize(mock.Mock())
|
||||||
|
self.action.input_parameters = self.input_parameters
|
||||||
|
|
||||||
|
def test_parameters(self):
|
||||||
|
params = {baction.BaseAction.RESOURCE_ID:
|
||||||
|
self.INSTANCE_UUID,
|
||||||
|
self.action.FLAVOR: 'x1'}
|
||||||
|
self.action.input_parameters = params
|
||||||
|
self.assertTrue(self.action.validate_parameters())
|
||||||
|
|
||||||
|
def test_parameters_exception_empty_fields(self):
|
||||||
|
parameters = {baction.BaseAction.RESOURCE_ID:
|
||||||
|
self.INSTANCE_UUID,
|
||||||
|
self.action.FLAVOR: None}
|
||||||
|
self.action.input_parameters = parameters
|
||||||
|
exc = self.assertRaises(
|
||||||
|
voluptuous.MultipleInvalid, self.action.validate_parameters)
|
||||||
|
self.assertEqual([(['flavor'], voluptuous.TypeInvalid)],
|
||||||
|
[(e.path, type(e)) for e in exc.errors])
|
||||||
|
|
||||||
|
def test_parameters_exception_flavor(self):
|
||||||
|
parameters = {baction.BaseAction.RESOURCE_ID:
|
||||||
|
self.INSTANCE_UUID,
|
||||||
|
self.action.FLAVOR: None}
|
||||||
|
self.action.input_parameters = parameters
|
||||||
|
exc = self.assertRaises(
|
||||||
|
voluptuous.MultipleInvalid, self.action.validate_parameters)
|
||||||
|
self.assertEqual(
|
||||||
|
[(['flavor'], voluptuous.TypeInvalid)],
|
||||||
|
[(e.path, type(e)) for e in exc.errors])
|
||||||
|
|
||||||
|
def test_parameters_exception_resource_id(self):
|
||||||
|
parameters = {baction.BaseAction.RESOURCE_ID: "EFEF",
|
||||||
|
self.action.FLAVOR: 'x1'}
|
||||||
|
self.action.input_parameters = parameters
|
||||||
|
exc = self.assertRaises(
|
||||||
|
voluptuous.MultipleInvalid, self.action.validate_parameters)
|
||||||
|
self.assertEqual(
|
||||||
|
[(['resource_id'], voluptuous.Invalid)],
|
||||||
|
[(e.path, type(e)) for e in exc.errors])
|
||||||
|
|
||||||
|
def test_execute_resize(self):
|
||||||
|
self.r_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
self.action.execute()
|
||||||
|
self.r_helper.resize_instance.assert_called_once_with(
|
||||||
|
instance_id=self.INSTANCE_UUID, flavor='x1')
|
@ -70,14 +70,15 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.fail(exc)
|
self.fail(exc)
|
||||||
|
|
||||||
def create_action(self, action_type, parameters, next):
|
def create_action(self, action_type, parameters, parents):
|
||||||
action = {
|
action = {
|
||||||
'uuid': utils.generate_uuid(),
|
'uuid': utils.generate_uuid(),
|
||||||
'action_plan_id': 0,
|
'action_plan_id': 0,
|
||||||
'action_type': action_type,
|
'action_type': action_type,
|
||||||
'input_parameters': parameters,
|
'input_parameters': parameters,
|
||||||
'state': objects.action.State.PENDING,
|
'state': objects.action.State.PENDING,
|
||||||
'next': next,
|
'parents': parents,
|
||||||
|
|
||||||
}
|
}
|
||||||
new_action = objects.Action(self.context, **action)
|
new_action = objects.Action(self.context, **action)
|
||||||
new_action.create()
|
new_action.create()
|
||||||
@ -116,7 +117,7 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
|||||||
def test_execute_with_two_actions(self):
|
def test_execute_with_two_actions(self):
|
||||||
actions = []
|
actions = []
|
||||||
second = self.create_action("sleep", {'duration': 0.0}, None)
|
second = self.create_action("sleep", {'duration': 0.0}, None)
|
||||||
first = self.create_action("nop", {'message': 'test'}, second.id)
|
first = self.create_action("nop", {'message': 'test'}, None)
|
||||||
|
|
||||||
actions.append(first)
|
actions.append(first)
|
||||||
actions.append(second)
|
actions.append(second)
|
||||||
@ -132,8 +133,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
|||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
third = self.create_action("nop", {'message': 'next'}, None)
|
third = self.create_action("nop", {'message': 'next'}, None)
|
||||||
second = self.create_action("sleep", {'duration': 0.0}, third.id)
|
second = self.create_action("sleep", {'duration': 0.0}, None)
|
||||||
first = self.create_action("nop", {'message': 'hello'}, second.id)
|
first = self.create_action("nop", {'message': 'hello'}, None)
|
||||||
|
|
||||||
self.check_action_state(first, objects.action.State.PENDING)
|
self.check_action_state(first, objects.action.State.PENDING)
|
||||||
self.check_action_state(second, objects.action.State.PENDING)
|
self.check_action_state(second, objects.action.State.PENDING)
|
||||||
@ -154,8 +155,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
|||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
third = self.create_action("no_exist", {'message': 'next'}, None)
|
third = self.create_action("no_exist", {'message': 'next'}, None)
|
||||||
second = self.create_action("sleep", {'duration': 0.0}, third.id)
|
second = self.create_action("sleep", {'duration': 0.0}, None)
|
||||||
first = self.create_action("nop", {'message': 'hello'}, second.id)
|
first = self.create_action("nop", {'message': 'hello'}, None)
|
||||||
|
|
||||||
self.check_action_state(first, objects.action.State.PENDING)
|
self.check_action_state(first, objects.action.State.PENDING)
|
||||||
self.check_action_state(second, objects.action.State.PENDING)
|
self.check_action_state(second, objects.action.State.PENDING)
|
||||||
|
@ -38,6 +38,7 @@ class TestNovaHelper(base.TestCase):
|
|||||||
self.instance_uuid = "fb5311b7-37f3-457e-9cde-6494a3c59bfe"
|
self.instance_uuid = "fb5311b7-37f3-457e-9cde-6494a3c59bfe"
|
||||||
self.source_node = "ldev-indeedsrv005"
|
self.source_node = "ldev-indeedsrv005"
|
||||||
self.destination_node = "ldev-indeedsrv006"
|
self.destination_node = "ldev-indeedsrv006"
|
||||||
|
self.flavor_name = "x1"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fake_server(*args, **kwargs):
|
def fake_server(*args, **kwargs):
|
||||||
@ -89,6 +90,22 @@ class TestNovaHelper(base.TestCase):
|
|||||||
result = nova_util.set_host_offline("rennes")
|
result = nova_util.set_host_offline("rennes")
|
||||||
self.assertFalse(result)
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
@mock.patch.object(time, 'sleep', mock.Mock())
|
||||||
|
def test_resize_instance(self, mock_glance, mock_cinder,
|
||||||
|
mock_neutron, mock_nova):
|
||||||
|
nova_util = nova_helper.NovaHelper()
|
||||||
|
server = self.fake_server(self.instance_uuid)
|
||||||
|
setattr(server, 'status', 'VERIFY_RESIZE')
|
||||||
|
self.fake_nova_find_list(nova_util, find=server, list=server)
|
||||||
|
is_success = nova_util.resize_instance(self.instance_uuid,
|
||||||
|
self.flavor_name)
|
||||||
|
self.assertTrue(is_success)
|
||||||
|
|
||||||
|
setattr(server, 'status', 'SOMETHING_ELSE')
|
||||||
|
is_success = nova_util.resize_instance(self.instance_uuid,
|
||||||
|
self.flavor_name)
|
||||||
|
self.assertFalse(is_success)
|
||||||
|
|
||||||
@mock.patch.object(time, 'sleep', mock.Mock())
|
@mock.patch.object(time, 'sleep', mock.Mock())
|
||||||
def test_live_migrate_instance(self, mock_glance, mock_cinder,
|
def test_live_migrate_instance(self, mock_glance, mock_cinder,
|
||||||
mock_neutron, mock_nova):
|
mock_neutron, mock_nova):
|
||||||
|
@ -278,28 +278,28 @@ class DbActionTestCase(base.DbTestCase):
|
|||||||
id=1,
|
id=1,
|
||||||
uuid=w_utils.generate_uuid(),
|
uuid=w_utils.generate_uuid(),
|
||||||
audit_id=audit.id,
|
audit_id=audit.id,
|
||||||
first_action_id=None,
|
parents=None,
|
||||||
state=objects.action_plan.State.RECOMMENDED)
|
state=objects.action_plan.State.RECOMMENDED)
|
||||||
action1 = self._create_test_action(
|
action1 = self._create_test_action(
|
||||||
id=1,
|
id=1,
|
||||||
action_plan_id=1,
|
action_plan_id=1,
|
||||||
description='description action 1',
|
description='description action 1',
|
||||||
uuid=w_utils.generate_uuid(),
|
uuid=w_utils.generate_uuid(),
|
||||||
next=None,
|
parents=None,
|
||||||
state=objects.action_plan.State.PENDING)
|
state=objects.action_plan.State.PENDING)
|
||||||
action2 = self._create_test_action(
|
action2 = self._create_test_action(
|
||||||
id=2,
|
id=2,
|
||||||
action_plan_id=2,
|
action_plan_id=2,
|
||||||
description='description action 2',
|
description='description action 2',
|
||||||
uuid=w_utils.generate_uuid(),
|
uuid=w_utils.generate_uuid(),
|
||||||
next=action1['uuid'],
|
parents=[action1['uuid']],
|
||||||
state=objects.action_plan.State.PENDING)
|
state=objects.action_plan.State.PENDING)
|
||||||
action3 = self._create_test_action(
|
action3 = self._create_test_action(
|
||||||
id=3,
|
id=3,
|
||||||
action_plan_id=1,
|
action_plan_id=1,
|
||||||
description='description action 3',
|
description='description action 3',
|
||||||
uuid=w_utils.generate_uuid(),
|
uuid=w_utils.generate_uuid(),
|
||||||
next=action2['uuid'],
|
parents=[action2['uuid']],
|
||||||
state=objects.action_plan.State.ONGOING)
|
state=objects.action_plan.State.ONGOING)
|
||||||
res = self.dbapi.get_action_list(
|
res = self.dbapi.get_action_list(
|
||||||
self.context,
|
self.context,
|
||||||
|
@ -283,13 +283,11 @@ class DbActionPlanTestCase(base.DbTestCase):
|
|||||||
id=1,
|
id=1,
|
||||||
uuid=w_utils.generate_uuid(),
|
uuid=w_utils.generate_uuid(),
|
||||||
audit_id=audit['id'],
|
audit_id=audit['id'],
|
||||||
first_action_id=None,
|
|
||||||
state=ap_objects.State.RECOMMENDED)
|
state=ap_objects.State.RECOMMENDED)
|
||||||
action_plan2 = self._create_test_action_plan(
|
action_plan2 = self._create_test_action_plan(
|
||||||
id=2,
|
id=2,
|
||||||
uuid=w_utils.generate_uuid(),
|
uuid=w_utils.generate_uuid(),
|
||||||
audit_id=audit['id'],
|
audit_id=audit['id'],
|
||||||
first_action_id=action_plan1['id'],
|
|
||||||
state=ap_objects.State.ONGOING)
|
state=ap_objects.State.ONGOING)
|
||||||
|
|
||||||
res = self.dbapi.get_action_plan_list(
|
res = self.dbapi.get_action_plan_list(
|
||||||
|
@ -130,7 +130,7 @@ def get_test_action(**kwargs):
|
|||||||
'resource_id':
|
'resource_id':
|
||||||
'10a47dd1-4874-4298-91cf-eff046dbdb8d'}),
|
'10a47dd1-4874-4298-91cf-eff046dbdb8d'}),
|
||||||
'state': kwargs.get('state', objects.action_plan.State.PENDING),
|
'state': kwargs.get('state', objects.action_plan.State.PENDING),
|
||||||
'next': kwargs.get('next', 2),
|
'parents': kwargs.get('parents', []),
|
||||||
'created_at': kwargs.get('created_at'),
|
'created_at': kwargs.get('created_at'),
|
||||||
'updated_at': kwargs.get('updated_at'),
|
'updated_at': kwargs.get('updated_at'),
|
||||||
'deleted_at': kwargs.get('deleted_at'),
|
'deleted_at': kwargs.get('deleted_at'),
|
||||||
@ -166,7 +166,6 @@ def get_test_action_plan(**kwargs):
|
|||||||
'audit_id': kwargs.get('audit_id', 1),
|
'audit_id': kwargs.get('audit_id', 1),
|
||||||
'strategy_id': kwargs.get('strategy_id', 1),
|
'strategy_id': kwargs.get('strategy_id', 1),
|
||||||
'global_efficacy': kwargs.get('global_efficacy', {}),
|
'global_efficacy': kwargs.get('global_efficacy', {}),
|
||||||
'first_action_id': kwargs.get('first_action_id', 1),
|
|
||||||
'created_at': kwargs.get('created_at'),
|
'created_at': kwargs.get('created_at'),
|
||||||
'updated_at': kwargs.get('updated_at'),
|
'updated_at': kwargs.get('updated_at'),
|
||||||
'deleted_at': kwargs.get('deleted_at'),
|
'deleted_at': kwargs.get('deleted_at'),
|
||||||
|
@ -1,208 +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.
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from watcher.common import utils
|
|
||||||
from watcher.db import api as db_api
|
|
||||||
from watcher.decision_engine.planner import default as pbase
|
|
||||||
from watcher.decision_engine.solution import default as dsol
|
|
||||||
from watcher.decision_engine.strategy import strategies
|
|
||||||
from watcher import objects
|
|
||||||
from watcher.tests.db import base
|
|
||||||
from watcher.tests.db import utils as db_utils
|
|
||||||
from watcher.tests.decision_engine.model import ceilometer_metrics as fake
|
|
||||||
from watcher.tests.decision_engine.model import faker_cluster_state
|
|
||||||
from watcher.tests.objects import utils as obj_utils
|
|
||||||
|
|
||||||
|
|
||||||
class SolutionFaker(object):
|
|
||||||
@staticmethod
|
|
||||||
def build():
|
|
||||||
metrics = fake.FakeCeilometerMetrics()
|
|
||||||
current_state_cluster = faker_cluster_state.FakerModelCollector()
|
|
||||||
strategy = strategies.BasicConsolidation(
|
|
||||||
config=mock.Mock(datasource="ceilometer"))
|
|
||||||
strategy._compute_model = current_state_cluster.generate_scenario_1()
|
|
||||||
strategy.ceilometer = mock.MagicMock(
|
|
||||||
get_statistics=metrics.mock_get_statistics)
|
|
||||||
return strategy.execute()
|
|
||||||
|
|
||||||
|
|
||||||
class SolutionFakerSingleHyp(object):
|
|
||||||
@staticmethod
|
|
||||||
def build():
|
|
||||||
metrics = fake.FakeCeilometerMetrics()
|
|
||||||
current_state_cluster = faker_cluster_state.FakerModelCollector()
|
|
||||||
strategy = strategies.BasicConsolidation(
|
|
||||||
config=mock.Mock(datasource="ceilometer"))
|
|
||||||
strategy._compute_model = (
|
|
||||||
current_state_cluster.generate_scenario_3_with_2_nodes())
|
|
||||||
strategy.ceilometer = mock.MagicMock(
|
|
||||||
get_statistics=metrics.mock_get_statistics)
|
|
||||||
|
|
||||||
return strategy.execute()
|
|
||||||
|
|
||||||
|
|
||||||
class TestActionScheduling(base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestActionScheduling, self).setUp()
|
|
||||||
self.strategy = db_utils.create_test_strategy(name="dummy")
|
|
||||||
self.audit = db_utils.create_test_audit(
|
|
||||||
uuid=utils.generate_uuid(), strategy_id=self.strategy.id)
|
|
||||||
self.default_planner = pbase.DefaultPlanner(mock.Mock())
|
|
||||||
|
|
||||||
def test_schedule_actions(self):
|
|
||||||
solution = dsol.DefaultSolution(
|
|
||||||
goal=mock.Mock(), strategy=self.strategy)
|
|
||||||
|
|
||||||
parameters = {
|
|
||||||
"source_node": "server1",
|
|
||||||
"destination_node": "server2",
|
|
||||||
}
|
|
||||||
solution.add_action(action_type="migrate",
|
|
||||||
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
|
||||||
input_parameters=parameters)
|
|
||||||
|
|
||||||
with mock.patch.object(
|
|
||||||
pbase.DefaultPlanner, "create_action",
|
|
||||||
wraps=self.default_planner.create_action
|
|
||||||
) as m_create_action:
|
|
||||||
self.default_planner.config.weights = {'migrate': 3}
|
|
||||||
action_plan = self.default_planner.schedule(
|
|
||||||
self.context, self.audit.id, solution)
|
|
||||||
|
|
||||||
self.assertIsNotNone(action_plan.uuid)
|
|
||||||
self.assertEqual(1, m_create_action.call_count)
|
|
||||||
filters = {'action_plan_id': action_plan.id}
|
|
||||||
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
|
||||||
self.assertEqual("migrate", actions[0].action_type)
|
|
||||||
|
|
||||||
def test_schedule_two_actions(self):
|
|
||||||
solution = dsol.DefaultSolution(
|
|
||||||
goal=mock.Mock(), strategy=self.strategy)
|
|
||||||
|
|
||||||
parameters = {
|
|
||||||
"source_node": "server1",
|
|
||||||
"destination_node": "server2",
|
|
||||||
}
|
|
||||||
solution.add_action(action_type="migrate",
|
|
||||||
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
|
||||||
input_parameters=parameters)
|
|
||||||
|
|
||||||
solution.add_action(action_type="nop",
|
|
||||||
resource_id="",
|
|
||||||
input_parameters={})
|
|
||||||
|
|
||||||
with mock.patch.object(
|
|
||||||
pbase.DefaultPlanner, "create_action",
|
|
||||||
wraps=self.default_planner.create_action
|
|
||||||
) as m_create_action:
|
|
||||||
self.default_planner.config.weights = {'migrate': 3, 'nop': 0}
|
|
||||||
action_plan = self.default_planner.schedule(
|
|
||||||
self.context, self.audit.id, solution)
|
|
||||||
self.assertIsNotNone(action_plan.uuid)
|
|
||||||
self.assertEqual(2, m_create_action.call_count)
|
|
||||||
# check order
|
|
||||||
filters = {'action_plan_id': action_plan.id}
|
|
||||||
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
|
||||||
self.assertEqual("nop", actions[0].action_type)
|
|
||||||
self.assertEqual("migrate", actions[1].action_type)
|
|
||||||
|
|
||||||
def test_schedule_actions_with_unknown_action(self):
|
|
||||||
solution = dsol.DefaultSolution(
|
|
||||||
goal=mock.Mock(), strategy=self.strategy)
|
|
||||||
|
|
||||||
parameters = {
|
|
||||||
"src_uuid_node": "server1",
|
|
||||||
"dst_uuid_node": "server2",
|
|
||||||
}
|
|
||||||
solution.add_action(action_type="migrate",
|
|
||||||
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
|
||||||
input_parameters=parameters)
|
|
||||||
|
|
||||||
solution.add_action(action_type="new_action_type",
|
|
||||||
resource_id="",
|
|
||||||
input_parameters={})
|
|
||||||
|
|
||||||
with mock.patch.object(
|
|
||||||
pbase.DefaultPlanner, "create_action",
|
|
||||||
wraps=self.default_planner.create_action
|
|
||||||
) as m_create_action:
|
|
||||||
self.default_planner.config.weights = {'migrate': 0}
|
|
||||||
self.assertRaises(KeyError, self.default_planner.schedule,
|
|
||||||
self.context, self.audit.id, solution)
|
|
||||||
self.assertEqual(2, m_create_action.call_count)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDefaultPlanner(base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDefaultPlanner, self).setUp()
|
|
||||||
self.default_planner = pbase.DefaultPlanner(mock.Mock())
|
|
||||||
self.default_planner.config.weights = {
|
|
||||||
'nop': 0,
|
|
||||||
'sleep': 1,
|
|
||||||
'change_nova_service_state': 2,
|
|
||||||
'migrate': 3
|
|
||||||
}
|
|
||||||
|
|
||||||
self.goal = obj_utils.create_test_goal(self.context)
|
|
||||||
self.strategy = obj_utils.create_test_strategy(
|
|
||||||
self.context, goal_id=self.goal.id)
|
|
||||||
obj_utils.create_test_audit_template(
|
|
||||||
self.context, goal_id=self.goal.id, strategy_id=self.strategy.id)
|
|
||||||
|
|
||||||
p = mock.patch.object(db_api.BaseConnection, 'create_action_plan')
|
|
||||||
self.mock_create_action_plan = p.start()
|
|
||||||
self.mock_create_action_plan.side_effect = (
|
|
||||||
self._simulate_action_plan_create)
|
|
||||||
self.addCleanup(p.stop)
|
|
||||||
|
|
||||||
q = mock.patch.object(db_api.BaseConnection, 'create_action')
|
|
||||||
self.mock_create_action = q.start()
|
|
||||||
self.mock_create_action.side_effect = (
|
|
||||||
self._simulate_action_create)
|
|
||||||
self.addCleanup(q.stop)
|
|
||||||
|
|
||||||
def _simulate_action_plan_create(self, action_plan):
|
|
||||||
action_plan.create()
|
|
||||||
return action_plan
|
|
||||||
|
|
||||||
def _simulate_action_create(self, action):
|
|
||||||
action.create()
|
|
||||||
return action
|
|
||||||
|
|
||||||
@mock.patch.object(objects.Strategy, 'get_by_name')
|
|
||||||
def test_schedule_scheduled_empty(self, m_get_by_name):
|
|
||||||
m_get_by_name.return_value = self.strategy
|
|
||||||
audit = db_utils.create_test_audit(
|
|
||||||
goal_id=self.goal.id, strategy_id=self.strategy.id)
|
|
||||||
fake_solution = SolutionFakerSingleHyp.build()
|
|
||||||
action_plan = self.default_planner.schedule(self.context,
|
|
||||||
audit.id, fake_solution)
|
|
||||||
self.assertIsNotNone(action_plan.uuid)
|
|
||||||
|
|
||||||
@mock.patch.object(objects.Strategy, 'get_by_name')
|
|
||||||
def test_scheduler_warning_empty_action_plan(self, m_get_by_name):
|
|
||||||
m_get_by_name.return_value = self.strategy
|
|
||||||
audit = db_utils.create_test_audit(
|
|
||||||
goal_id=self.goal.id, strategy_id=self.strategy.id)
|
|
||||||
fake_solution = SolutionFaker.build()
|
|
||||||
action_plan = self.default_planner.schedule(
|
|
||||||
self.context, audit.id, fake_solution)
|
|
||||||
self.assertIsNotNone(action_plan.uuid)
|
|
@ -16,13 +16,13 @@
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from watcher.decision_engine.planner import default
|
|
||||||
from watcher.decision_engine.planner import manager as planner
|
from watcher.decision_engine.planner import manager as planner
|
||||||
|
from watcher.decision_engine.planner import weight
|
||||||
from watcher.tests import base
|
from watcher.tests import base
|
||||||
|
|
||||||
|
|
||||||
class TestPlannerManager(base.TestCase):
|
class TestPlannerManager(base.TestCase):
|
||||||
def test_load(self):
|
def test_load(self):
|
||||||
cfg.CONF.set_override('planner', "default", group='watcher_planner')
|
cfg.CONF.set_override('planner', "weight", group='watcher_planner')
|
||||||
manager = planner.PlannerManager()
|
manager = planner.PlannerManager()
|
||||||
self.assertIsInstance(manager.load(), default.DefaultPlanner)
|
self.assertIsInstance(manager.load(), weight.WeightPlanner)
|
||||||
|
943
watcher/tests/decision_engine/planner/test_weight_planner.py
Normal file
943
watcher/tests/decision_engine/planner/test_weight_planner.py
Normal file
@ -0,0 +1,943 @@
|
|||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from watcher.common import nova_helper
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.db import api as db_api
|
||||||
|
from watcher.decision_engine.planner import weight as pbase
|
||||||
|
from watcher.decision_engine.solution import default as dsol
|
||||||
|
from watcher.decision_engine.strategy import strategies
|
||||||
|
from watcher import objects
|
||||||
|
from watcher.tests.db import base
|
||||||
|
from watcher.tests.db import utils as db_utils
|
||||||
|
from watcher.tests.decision_engine.model import ceilometer_metrics as fake
|
||||||
|
from watcher.tests.decision_engine.model import faker_cluster_state
|
||||||
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class SolutionFaker(object):
|
||||||
|
@staticmethod
|
||||||
|
def build():
|
||||||
|
metrics = fake.FakerMetricsCollector()
|
||||||
|
current_state_cluster = faker_cluster_state.FakerModelCollector()
|
||||||
|
sercon = strategies.BasicConsolidation(config=mock.Mock())
|
||||||
|
sercon.compute_model = current_state_cluster.generate_scenario_1()
|
||||||
|
sercon.ceilometer = mock.MagicMock(
|
||||||
|
get_statistics=metrics.mock_get_statistics)
|
||||||
|
return sercon.execute()
|
||||||
|
|
||||||
|
|
||||||
|
class SolutionFakerSingleHyp(object):
|
||||||
|
@staticmethod
|
||||||
|
def build():
|
||||||
|
metrics = fake.FakerMetricsCollector()
|
||||||
|
current_state_cluster = faker_cluster_state.FakerModelCollector()
|
||||||
|
sercon = strategies.BasicConsolidation(config=mock.Mock())
|
||||||
|
sercon.compute_model = (
|
||||||
|
current_state_cluster.generate_scenario_3_with_2_nodes())
|
||||||
|
sercon.ceilometer = mock.MagicMock(
|
||||||
|
get_statistics=metrics.mock_get_statistics)
|
||||||
|
|
||||||
|
return sercon.execute()
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionScheduling(base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestActionScheduling, self).setUp()
|
||||||
|
self.strategy = db_utils.create_test_strategy(name="dummy")
|
||||||
|
self.audit = db_utils.create_test_audit(
|
||||||
|
uuid=utils.generate_uuid(), strategy_id=self.strategy.id)
|
||||||
|
self.planner = pbase.WeightPlanner(
|
||||||
|
mock.Mock(
|
||||||
|
weights={
|
||||||
|
'turn_host_to_acpi_s3_state': 10,
|
||||||
|
'resize': 20,
|
||||||
|
'migrate': 30,
|
||||||
|
'sleep': 40,
|
||||||
|
'change_nova_service_state': 50,
|
||||||
|
'nop': 60,
|
||||||
|
'new_action_type': 70,
|
||||||
|
},
|
||||||
|
parallelization={
|
||||||
|
'turn_host_to_acpi_s3_state': 2,
|
||||||
|
'resize': 2,
|
||||||
|
'migrate': 2,
|
||||||
|
'sleep': 1,
|
||||||
|
'change_nova_service_state': 1,
|
||||||
|
'nop': 1,
|
||||||
|
'new_action_type': 70,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_actions(self, m_generate_uuid):
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222",
|
||||||
|
"33333333-3333-3333-3333-333333333333",
|
||||||
|
# "44444444-4444-4444-4444-444444444444",
|
||||||
|
# "55555555-5555-5555-5555-555555555555",
|
||||||
|
# "66666666-6666-6666-6666-666666666666",
|
||||||
|
# "77777777-7777-7777-7777-777777777777",
|
||||||
|
# "88888888-8888-8888-8888-888888888888",
|
||||||
|
# "99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
self.planner.config.weights = {'migrate': 3}
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = []
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_two_actions(self, m_generate_uuid):
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111",
|
||||||
|
"22222222-2222-2222-2222-222222222222",
|
||||||
|
"33333333-3333-3333-3333-333333333333",
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Migrate 1
|
||||||
|
"55555555-5555-5555-5555-555555555555", # Nop 1
|
||||||
|
]
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
# We create the migrate action before but we then schedule
|
||||||
|
# after the nop action
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="nop",
|
||||||
|
input_parameters={"message": "Hello world"})
|
||||||
|
|
||||||
|
self.planner.config.weights = {'migrate': 3, 'nop': 5}
|
||||||
|
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'nop',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['55555555-5555-5555-5555-555555555555'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_actions_with_unknown_action(self, m_generate_uuid):
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # new_action_type
|
||||||
|
"33333333-3333-3333-3333-333333333333",
|
||||||
|
|
||||||
|
]
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"src_uuid_node": "server1",
|
||||||
|
"dst_uuid_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="new_action_type",
|
||||||
|
resource_id="",
|
||||||
|
input_parameters={})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'new_action_type',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
@mock.patch.object(nova_helper.NovaHelper, 'get_instance_by_uuid')
|
||||||
|
def test_schedule_migrate_resize_actions(self, m_nova, m_generate_uuid):
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # Migrate 2
|
||||||
|
"33333333-3333-3333-3333-333333333333", # Migrate 3
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Migrate 4
|
||||||
|
"55555555-5555-5555-5555-555555555555", # Migrate 5
|
||||||
|
"66666666-6666-6666-6666-666666666666", # Resize 1
|
||||||
|
"77777777-7777-7777-7777-777777777777", # Resize 2
|
||||||
|
"88888888-8888-8888-8888-888888888888", # Nop
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
m_nova.return_value = 'server1'
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"flavor": "x1"})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111'],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_3_migrate_1_resize_1_acpi_actions_1_swimlane(
|
||||||
|
self, m_generate_uuid):
|
||||||
|
self.planner.config.parallelization["migrate"] = 1
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # Migrate 2
|
||||||
|
"33333333-3333-3333-3333-333333333333", # Migrate 3
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Resize
|
||||||
|
"55555555-5555-5555-5555-555555555555", # ACPI
|
||||||
|
"66666666-6666-6666-6666-666666666666",
|
||||||
|
"77777777-7777-7777-7777-777777777777",
|
||||||
|
"88888888-8888-8888-8888-888888888888",
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server2",
|
||||||
|
"destination_node": "server3"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="turn_host_to_acpi_s3_state",
|
||||||
|
resource_id="server1",
|
||||||
|
input_parameters={})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111'],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111'],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'}),
|
||||||
|
({'action_type': 'resize',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'},
|
||||||
|
{'action_type': 'turn_host_to_acpi_s3_state',
|
||||||
|
'parents': ['44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': ['22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_migrate_resize_acpi_actions_2_swimlanes(
|
||||||
|
self, m_generate_uuid):
|
||||||
|
self.planner.config.parallelization["migrate"] = 2
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # Migrate 2
|
||||||
|
"33333333-3333-3333-3333-333333333333", # Migrate 3
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Resize
|
||||||
|
"55555555-5555-5555-5555-555555555555", # ACPI
|
||||||
|
"66666666-6666-6666-6666-666666666666",
|
||||||
|
"77777777-7777-7777-7777-777777777777",
|
||||||
|
"88888888-8888-8888-8888-888888888888",
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server2",
|
||||||
|
"destination_node": "server3"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="turn_host_to_acpi_s3_state",
|
||||||
|
resource_id="server1",
|
||||||
|
input_parameters={})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'}),
|
||||||
|
({'action_type': 'resize',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'},
|
||||||
|
{'action_type': 'turn_host_to_acpi_s3_state',
|
||||||
|
'parents': ['44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_migrate_resize_acpi_actions_3_swimlanes(
|
||||||
|
self, m_generate_uuid):
|
||||||
|
self.planner.config.parallelization["migrate"] = 3
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # Migrate 2
|
||||||
|
"33333333-3333-3333-3333-333333333333", # Migrate 3
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Resize
|
||||||
|
"55555555-5555-5555-5555-555555555555", # ACPI
|
||||||
|
"66666666-6666-6666-6666-666666666666",
|
||||||
|
"77777777-7777-7777-7777-777777777777",
|
||||||
|
"88888888-8888-8888-8888-888888888888",
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server2",
|
||||||
|
"destination_node": "server3"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="turn_host_to_acpi_s3_state",
|
||||||
|
resource_id="server1",
|
||||||
|
input_parameters={})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'},
|
||||||
|
{'action_type': 'turn_host_to_acpi_s3_state',
|
||||||
|
'parents': ['44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_three_migrate_two_resize_actions(
|
||||||
|
self, m_generate_uuid):
|
||||||
|
self.planner.config.parallelization["migrate"] = 3
|
||||||
|
self.planner.config.parallelization["resize"] = 2
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # Migrate 2
|
||||||
|
"33333333-3333-3333-3333-333333333333", # Migrate 3
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Resize
|
||||||
|
"55555555-5555-5555-5555-555555555555", # ACPI
|
||||||
|
"66666666-6666-6666-6666-666666666666",
|
||||||
|
"77777777-7777-7777-7777-777777777777",
|
||||||
|
"88888888-8888-8888-8888-888888888888",
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server2",
|
||||||
|
"destination_node": "server3"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="b189db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222',
|
||||||
|
'33333333-3333-3333-3333-333333333333'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, "generate_uuid")
|
||||||
|
def test_schedule_5_migrate_2_resize_actions_for_2_swimlanes(
|
||||||
|
self, m_generate_uuid):
|
||||||
|
self.planner.config.parallelization["migrate"] = 2
|
||||||
|
self.planner.config.parallelization["resize"] = 2
|
||||||
|
m_generate_uuid.side_effect = [
|
||||||
|
"00000000-0000-0000-0000-000000000000", # Action plan
|
||||||
|
"11111111-1111-1111-1111-111111111111", # Migrate 1
|
||||||
|
"22222222-2222-2222-2222-222222222222", # Migrate 2
|
||||||
|
"33333333-3333-3333-3333-333333333333", # Migrate 3
|
||||||
|
"44444444-4444-4444-4444-444444444444", # Migrate 4
|
||||||
|
"55555555-5555-5555-5555-555555555555", # Migrate 5
|
||||||
|
"66666666-6666-6666-6666-666666666666", # Resize 1
|
||||||
|
"77777777-7777-7777-7777-777777777777", # Resize 2
|
||||||
|
"88888888-8888-8888-8888-888888888888", # Nop
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
]
|
||||||
|
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server6"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server2",
|
||||||
|
"destination_node": "server6"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server3",
|
||||||
|
"destination_node": "server6"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server4",
|
||||||
|
"destination_node": "server6"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={"source_node": "server5",
|
||||||
|
"destination_node": "server6"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="DOESNOTMATTER",
|
||||||
|
input_parameters={'flavor': 'x2'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="turn_host_to_acpi_s3_state",
|
||||||
|
resource_id="DOESNOTMATTER")
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WeightPlanner, "create_scheduled_actions",
|
||||||
|
wraps=self.planner.create_scheduled_actions
|
||||||
|
) as m_create_scheduled_actions:
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_scheduled_actions.call_count)
|
||||||
|
action_graph = m_create_scheduled_actions.call_args[0][1]
|
||||||
|
|
||||||
|
expected_edges = \
|
||||||
|
[({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '11111111-1111-1111-1111-111111111111'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': [],
|
||||||
|
'uuid': '22222222-2222-2222-2222-222222222222'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '33333333-3333-3333-3333-333333333333'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333',
|
||||||
|
'44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': ['11111111-1111-1111-1111-111111111111',
|
||||||
|
'22222222-2222-2222-2222-222222222222'],
|
||||||
|
'uuid': '44444444-4444-4444-4444-444444444444'},
|
||||||
|
{'action_type': 'migrate',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333',
|
||||||
|
'44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333',
|
||||||
|
'44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['55555555-5555-5555-5555-555555555555'],
|
||||||
|
'uuid': '66666666-6666-6666-6666-666666666666'}),
|
||||||
|
({'action_type': 'migrate',
|
||||||
|
'parents': ['33333333-3333-3333-3333-333333333333',
|
||||||
|
'44444444-4444-4444-4444-444444444444'],
|
||||||
|
'uuid': '55555555-5555-5555-5555-555555555555'},
|
||||||
|
{'action_type': 'resize',
|
||||||
|
'parents': ['55555555-5555-5555-5555-555555555555'],
|
||||||
|
'uuid': '77777777-7777-7777-7777-777777777777'}),
|
||||||
|
({'action_type': 'resize',
|
||||||
|
'parents': ['55555555-5555-5555-5555-555555555555'],
|
||||||
|
'uuid': '66666666-6666-6666-6666-666666666666'},
|
||||||
|
{'action_type': 'turn_host_to_acpi_s3_state',
|
||||||
|
'parents': ['66666666-6666-6666-6666-666666666666',
|
||||||
|
'77777777-7777-7777-7777-777777777777'],
|
||||||
|
'uuid': '88888888-8888-8888-8888-888888888888'}),
|
||||||
|
({'action_type': 'resize',
|
||||||
|
'parents': ['55555555-5555-5555-5555-555555555555'],
|
||||||
|
'uuid': '77777777-7777-7777-7777-777777777777'},
|
||||||
|
{'action_type': 'turn_host_to_acpi_s3_state',
|
||||||
|
'parents': ['66666666-6666-6666-6666-666666666666',
|
||||||
|
'77777777-7777-7777-7777-777777777777'],
|
||||||
|
'uuid': '88888888-8888-8888-8888-888888888888'})]
|
||||||
|
|
||||||
|
edges = sorted([(src.as_dict(), dst.as_dict())
|
||||||
|
for src, dst in action_graph.edges()],
|
||||||
|
key=lambda pair: pair[0]['uuid'])
|
||||||
|
for src, dst in edges:
|
||||||
|
for key in ('id', 'action_plan', 'action_plan_id', 'created_at',
|
||||||
|
'input_parameters', 'deleted_at', 'updated_at',
|
||||||
|
'state'):
|
||||||
|
del src[key]
|
||||||
|
del dst[key]
|
||||||
|
|
||||||
|
self.assertEqual(len(expected_edges), len(edges))
|
||||||
|
for pair in expected_edges:
|
||||||
|
self.assertIn(pair, edges)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWeightPlanner(base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestWeightPlanner, self).setUp()
|
||||||
|
self.planner = pbase.WeightPlanner(mock.Mock())
|
||||||
|
self.planner.config.weights = {
|
||||||
|
'nop': 0,
|
||||||
|
'sleep': 1,
|
||||||
|
'change_nova_service_state': 2,
|
||||||
|
'migrate': 3
|
||||||
|
}
|
||||||
|
|
||||||
|
self.goal = obj_utils.create_test_goal(self.context)
|
||||||
|
self.strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, goal_id=self.goal.id)
|
||||||
|
obj_utils.create_test_audit_template(
|
||||||
|
self.context, goal_id=self.goal.id, strategy_id=self.strategy.id)
|
||||||
|
|
||||||
|
p = mock.patch.object(db_api.BaseConnection, 'create_action_plan')
|
||||||
|
self.mock_create_action_plan = p.start()
|
||||||
|
self.mock_create_action_plan.side_effect = (
|
||||||
|
self._simulate_action_plan_create)
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
q = mock.patch.object(db_api.BaseConnection, 'create_action')
|
||||||
|
self.mock_create_action = q.start()
|
||||||
|
self.mock_create_action.side_effect = (
|
||||||
|
self._simulate_action_create)
|
||||||
|
self.addCleanup(q.stop)
|
||||||
|
|
||||||
|
def _simulate_action_plan_create(self, action_plan):
|
||||||
|
action_plan.create()
|
||||||
|
return action_plan
|
||||||
|
|
||||||
|
def _simulate_action_create(self, action):
|
||||||
|
action.create()
|
||||||
|
return action
|
||||||
|
|
||||||
|
@mock.patch.object(objects.Strategy, 'get_by_name')
|
||||||
|
def test_scheduler_warning_empty_action_plan(self, m_get_by_name):
|
||||||
|
m_get_by_name.return_value = self.strategy
|
||||||
|
audit = db_utils.create_test_audit(
|
||||||
|
goal_id=self.goal.id, strategy_id=self.strategy.id)
|
||||||
|
fake_solution = mock.MagicMock(efficacy_indicators=[],
|
||||||
|
actions=[])
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, audit.id, fake_solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
@ -0,0 +1,378 @@
|
|||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.common import nova_helper
|
||||||
|
from watcher.common import utils
|
||||||
|
from watcher.db import api as db_api
|
||||||
|
from watcher.decision_engine.planner import workload_stabilization as pbase
|
||||||
|
from watcher.decision_engine.solution import default as dsol
|
||||||
|
from watcher.decision_engine.strategy import strategies
|
||||||
|
from watcher import objects
|
||||||
|
from watcher.tests.db import base
|
||||||
|
from watcher.tests.db import utils as db_utils
|
||||||
|
from watcher.tests.decision_engine.model import ceilometer_metrics as fake
|
||||||
|
from watcher.tests.decision_engine.model import faker_cluster_state
|
||||||
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class SolutionFaker(object):
|
||||||
|
@staticmethod
|
||||||
|
def build():
|
||||||
|
metrics = fake.FakerMetricsCollector()
|
||||||
|
current_state_cluster = faker_cluster_state.FakerModelCollector()
|
||||||
|
sercon = strategies.BasicConsolidation(config=mock.Mock())
|
||||||
|
sercon._compute_model = current_state_cluster.generate_scenario_1()
|
||||||
|
sercon.ceilometer = mock.MagicMock(
|
||||||
|
get_statistics=metrics.mock_get_statistics)
|
||||||
|
return sercon.execute()
|
||||||
|
|
||||||
|
|
||||||
|
class SolutionFakerSingleHyp(object):
|
||||||
|
@staticmethod
|
||||||
|
def build():
|
||||||
|
metrics = fake.FakerMetricsCollector()
|
||||||
|
current_state_cluster = faker_cluster_state.FakerModelCollector()
|
||||||
|
sercon = strategies.BasicConsolidation(config=mock.Mock())
|
||||||
|
sercon._compute_model = (
|
||||||
|
current_state_cluster.generate_scenario_3_with_2_nodes())
|
||||||
|
sercon.ceilometer = mock.MagicMock(
|
||||||
|
get_statistics=metrics.mock_get_statistics)
|
||||||
|
|
||||||
|
return sercon.execute()
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionScheduling(base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestActionScheduling, self).setUp()
|
||||||
|
self.strategy = db_utils.create_test_strategy(name="dummy")
|
||||||
|
self.audit = db_utils.create_test_audit(
|
||||||
|
uuid=utils.generate_uuid(), strategy_id=self.strategy.id)
|
||||||
|
self.planner = pbase.WorkloadStabilizationPlanner(mock.Mock())
|
||||||
|
self.nova_helper = nova_helper.NovaHelper(mock.Mock())
|
||||||
|
|
||||||
|
def test_schedule_actions(self):
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WorkloadStabilizationPlanner, "create_action",
|
||||||
|
wraps=self.planner.create_action
|
||||||
|
) as m_create_action:
|
||||||
|
self.planner.config.weights = {'migrate': 3}
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(1, m_create_action.call_count)
|
||||||
|
filters = {'action_plan_id': action_plan.id}
|
||||||
|
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||||
|
self.assertEqual("migrate", actions[0].action_type)
|
||||||
|
|
||||||
|
def test_schedule_two_actions(self):
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="nop",
|
||||||
|
input_parameters={"message": "Hello world"})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WorkloadStabilizationPlanner, "create_action",
|
||||||
|
wraps=self.planner.create_action
|
||||||
|
) as m_create_action:
|
||||||
|
self.planner.config.weights = {'migrate': 3, 'nop': 5}
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(2, m_create_action.call_count)
|
||||||
|
# check order
|
||||||
|
filters = {'action_plan_id': action_plan.id}
|
||||||
|
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||||
|
self.assertEqual("nop", actions[0].action_type)
|
||||||
|
self.assertEqual("migrate", actions[1].action_type)
|
||||||
|
|
||||||
|
def test_schedule_actions_with_unknown_action(self):
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"src_uuid_node": "server1",
|
||||||
|
"dst_uuid_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="new_action_type",
|
||||||
|
resource_id="",
|
||||||
|
input_parameters={})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WorkloadStabilizationPlanner, "create_action",
|
||||||
|
wraps=self.planner.create_action
|
||||||
|
) as m_create_action:
|
||||||
|
with mock.patch.object(nova_helper, 'NovaHelper') as m_nova:
|
||||||
|
self.planner.config.weights = {'migrate': 0}
|
||||||
|
self.assertRaises(KeyError, self.planner.schedule,
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
assert not m_nova.called
|
||||||
|
self.assertEqual(2, m_create_action.call_count)
|
||||||
|
|
||||||
|
def test_schedule_actions_with_unsupported_action(self):
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"src_uuid_node": "server1",
|
||||||
|
"dst_uuid_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="new_action_type",
|
||||||
|
resource_id="",
|
||||||
|
input_parameters={})
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WorkloadStabilizationPlanner, "create_action",
|
||||||
|
wraps=self.planner.create_action
|
||||||
|
) as m_create_action:
|
||||||
|
with mock.patch.object(nova_helper, 'NovaHelper') as m_nova:
|
||||||
|
self.planner.config.weights = {
|
||||||
|
'turn_host_to_acpi_s3_state': 0,
|
||||||
|
'resize': 1,
|
||||||
|
'migrate': 2,
|
||||||
|
'sleep': 3,
|
||||||
|
'change_nova_service_state': 4,
|
||||||
|
'nop': 5,
|
||||||
|
'new_action_type': 6}
|
||||||
|
self.assertRaises(exception.UnsupportedActionType,
|
||||||
|
self.planner.schedule,
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
assert not m_nova.called
|
||||||
|
self.assertEqual(2, m_create_action.call_count)
|
||||||
|
|
||||||
|
@mock.patch.object(nova_helper.NovaHelper, 'get_instance_by_uuid')
|
||||||
|
def test_schedule_migrate_resize_actions(self, mock_nova):
|
||||||
|
mock_nova.return_value = 'server1'
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters={"flavor": "x1"})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WorkloadStabilizationPlanner, "create_action",
|
||||||
|
wraps=self.planner.create_action
|
||||||
|
) as m_create_action:
|
||||||
|
with mock.patch.object(nova_helper, 'NovaHelper') as m_nova:
|
||||||
|
self.planner.config.weights = {'migrate': 3, 'resize': 2}
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertEqual(1, m_nova.call_count)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(2, m_create_action.call_count)
|
||||||
|
# check order
|
||||||
|
filters = {'action_plan_id': action_plan.id}
|
||||||
|
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||||
|
self.assertEqual("migrate", actions[0].action_type)
|
||||||
|
self.assertEqual("resize", actions[1].action_type)
|
||||||
|
self.assertEqual(actions[0].uuid, actions[1].parents[0])
|
||||||
|
|
||||||
|
def test_schedule_migrate_resize_acpi_s3_actions(self):
|
||||||
|
solution = dsol.DefaultSolution(
|
||||||
|
goal=mock.Mock(), strategy=self.strategy)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
"source_node": "server1",
|
||||||
|
"destination_node": "server2",
|
||||||
|
}
|
||||||
|
parent_migration = "b199db0c-1408-4d52-b5a5-5ca14de0ff36"
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
solution.add_action(action_type="resize",
|
||||||
|
resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters={'flavor': 'x1'})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="f6416850-da28-4047-a547-8c49f53e95fe",
|
||||||
|
input_parameters={"source_node": "server1",
|
||||||
|
"destination_node": "server2"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
resource_id="bb404e74-2caf-447b-bd1e-9234db386ca5",
|
||||||
|
input_parameters={"source_node": "server2",
|
||||||
|
"destination_node": "server3"})
|
||||||
|
|
||||||
|
solution.add_action(action_type="turn_host_to_acpi_s3_state",
|
||||||
|
resource_id="server1",
|
||||||
|
input_parameters={})
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
pbase.WorkloadStabilizationPlanner, "create_action",
|
||||||
|
wraps=self.planner.create_action
|
||||||
|
) as m_create_action:
|
||||||
|
with mock.patch.object(
|
||||||
|
nova_helper, 'NovaHelper') as m_nova:
|
||||||
|
m_nova().get_hostname.return_value = 'server1'
|
||||||
|
m_nova().get_instance_by_uuid.return_value = ['uuid1']
|
||||||
|
self.planner.config.weights = {
|
||||||
|
'turn_host_to_acpi_s3_state': 0,
|
||||||
|
'resize': 1,
|
||||||
|
'migrate': 2,
|
||||||
|
'sleep': 3,
|
||||||
|
'change_nova_service_state': 4,
|
||||||
|
'nop': 5}
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, self.audit.id, solution)
|
||||||
|
self.assertEqual(3, m_nova.call_count)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
self.assertEqual(5, m_create_action.call_count)
|
||||||
|
# check order
|
||||||
|
filters = {'action_plan_id': action_plan.id}
|
||||||
|
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||||
|
self.assertEqual("migrate", actions[0].action_type)
|
||||||
|
self.assertEqual("migrate", actions[1].action_type)
|
||||||
|
self.assertEqual("migrate", actions[2].action_type)
|
||||||
|
self.assertEqual("resize", actions[3].action_type)
|
||||||
|
self.assertEqual("turn_host_to_acpi_s3_state", actions[4].action_type)
|
||||||
|
for action in actions:
|
||||||
|
if action.input_parameters['resource_id'] == parent_migration:
|
||||||
|
parent_migration = action
|
||||||
|
break
|
||||||
|
self.assertEqual(parent_migration.uuid, actions[3].parents[0])
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefaultPlanner(base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDefaultPlanner, self).setUp()
|
||||||
|
self.planner = pbase.WorkloadStabilizationPlanner(mock.Mock())
|
||||||
|
self.planner.config.weights = {
|
||||||
|
'nop': 0,
|
||||||
|
'sleep': 1,
|
||||||
|
'change_nova_service_state': 2,
|
||||||
|
'migrate': 3
|
||||||
|
}
|
||||||
|
|
||||||
|
self.goal = obj_utils.create_test_goal(self.context)
|
||||||
|
self.strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, goal_id=self.goal.id)
|
||||||
|
obj_utils.create_test_audit_template(
|
||||||
|
self.context, goal_id=self.goal.id, strategy_id=self.strategy.id)
|
||||||
|
|
||||||
|
p = mock.patch.object(db_api.BaseConnection, 'create_action_plan')
|
||||||
|
self.mock_create_action_plan = p.start()
|
||||||
|
self.mock_create_action_plan.side_effect = (
|
||||||
|
self._simulate_action_plan_create)
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
q = mock.patch.object(db_api.BaseConnection, 'create_action')
|
||||||
|
self.mock_create_action = q.start()
|
||||||
|
self.mock_create_action.side_effect = (
|
||||||
|
self._simulate_action_create)
|
||||||
|
self.addCleanup(q.stop)
|
||||||
|
|
||||||
|
def _simulate_action_plan_create(self, action_plan):
|
||||||
|
action_plan.create()
|
||||||
|
return action_plan
|
||||||
|
|
||||||
|
def _simulate_action_create(self, action):
|
||||||
|
action.create()
|
||||||
|
return action
|
||||||
|
|
||||||
|
@mock.patch.object(objects.Strategy, 'get_by_name')
|
||||||
|
def test_scheduler_warning_empty_action_plan(self, m_get_by_name):
|
||||||
|
m_get_by_name.return_value = self.strategy
|
||||||
|
audit = db_utils.create_test_audit(
|
||||||
|
goal_id=self.goal.id, strategy_id=self.strategy.id)
|
||||||
|
fake_solution = mock.MagicMock(efficacy_indicators=[],
|
||||||
|
actions=[])
|
||||||
|
action_plan = self.planner.schedule(
|
||||||
|
self.context, audit.id, fake_solution)
|
||||||
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionValidator(base.DbTestCase):
|
||||||
|
INSTANCE_UUID = "94ae2f92-b7fd-4da7-9e97-f13504ae98c4"
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestActionValidator, self).setUp()
|
||||||
|
self.r_osc_cls = mock.Mock()
|
||||||
|
self.r_helper_cls = mock.Mock()
|
||||||
|
self.r_helper = mock.Mock(spec=nova_helper.NovaHelper)
|
||||||
|
self.r_helper_cls.return_value = self.r_helper
|
||||||
|
r_nova_helper = mock.patch.object(
|
||||||
|
nova_helper, "NovaHelper", self.r_helper_cls)
|
||||||
|
|
||||||
|
r_nova_helper.start()
|
||||||
|
|
||||||
|
self.addCleanup(r_nova_helper.stop)
|
||||||
|
|
||||||
|
def test_resize_validate_parents(self):
|
||||||
|
resize_object = pbase.ResizeActionValidator()
|
||||||
|
action = {'uuid': 'fcec56cd-74c1-406b-a7c1-81ef9f0c1393',
|
||||||
|
'input_parameters': {'resource_id': self.INSTANCE_UUID}}
|
||||||
|
resource_action_map = {self.INSTANCE_UUID: [
|
||||||
|
('action_uuid', 'migrate')]}
|
||||||
|
self.r_helper.get_hostname.return_value = 'server1'
|
||||||
|
self.r_helper.get_instance_by_uuid.return_value = ['instance']
|
||||||
|
result = resize_object.validate_parents(resource_action_map, action)
|
||||||
|
self.assertEqual('action_uuid', result[0])
|
||||||
|
|
||||||
|
def test_migrate_validate_parents(self):
|
||||||
|
migrate_object = pbase.MigrationActionValidator()
|
||||||
|
action = {'uuid': '712f1701-4c1b-4076-bfcf-3f23cfec6c3b',
|
||||||
|
'input_parameters': {'source_node': 'server1',
|
||||||
|
'resource_id': self.INSTANCE_UUID}}
|
||||||
|
resource_action_map = {}
|
||||||
|
expected_map = {
|
||||||
|
'94ae2f92-b7fd-4da7-9e97-f13504ae98c4': [
|
||||||
|
('712f1701-4c1b-4076-bfcf-3f23cfec6c3b', 'migrate')],
|
||||||
|
'server1': [
|
||||||
|
('712f1701-4c1b-4076-bfcf-3f23cfec6c3b', 'migrate')]}
|
||||||
|
migrate_object.validate_parents(resource_action_map, action)
|
||||||
|
self.assertEqual(resource_action_map, expected_map)
|
@ -363,29 +363,25 @@ class TestSyncer(base.DbTestCase):
|
|||||||
action_plan1 = objects.ActionPlan(
|
action_plan1 = objects.ActionPlan(
|
||||||
self.ctx, id=1, uuid=utils.generate_uuid(),
|
self.ctx, id=1, uuid=utils.generate_uuid(),
|
||||||
audit_id=audit1.id, strategy_id=strategy1.id,
|
audit_id=audit1.id, strategy_id=strategy1.id,
|
||||||
first_action_id=None, state='DOESNOTMATTER',
|
state='DOESNOTMATTER', global_efficacy={})
|
||||||
global_efficacy={})
|
|
||||||
# Stale after syncing because the goal of the audit has been modified
|
# Stale after syncing because the goal of the audit has been modified
|
||||||
# (compared to the defined fake goals)
|
# (compared to the defined fake goals)
|
||||||
action_plan2 = objects.ActionPlan(
|
action_plan2 = objects.ActionPlan(
|
||||||
self.ctx, id=2, uuid=utils.generate_uuid(),
|
self.ctx, id=2, uuid=utils.generate_uuid(),
|
||||||
audit_id=audit2.id, strategy_id=strategy2.id,
|
audit_id=audit2.id, strategy_id=strategy2.id,
|
||||||
first_action_id=None, state='DOESNOTMATTER',
|
state='DOESNOTMATTER', global_efficacy={})
|
||||||
global_efficacy={})
|
|
||||||
# Stale after syncing because the strategy has been modified
|
# Stale after syncing because the strategy has been modified
|
||||||
# (compared to the defined fake strategies)
|
# (compared to the defined fake strategies)
|
||||||
action_plan3 = objects.ActionPlan(
|
action_plan3 = objects.ActionPlan(
|
||||||
self.ctx, id=3, uuid=utils.generate_uuid(),
|
self.ctx, id=3, uuid=utils.generate_uuid(),
|
||||||
audit_id=audit3.id, strategy_id=strategy3.id,
|
audit_id=audit3.id, strategy_id=strategy3.id,
|
||||||
first_action_id=None, state='DOESNOTMATTER',
|
state='DOESNOTMATTER', global_efficacy={})
|
||||||
global_efficacy={})
|
|
||||||
# Stale after syncing because both the strategy and the related audit
|
# Stale after syncing because both the strategy and the related audit
|
||||||
# have been modified (compared to the defined fake goals/strategies)
|
# have been modified (compared to the defined fake goals/strategies)
|
||||||
action_plan4 = objects.ActionPlan(
|
action_plan4 = objects.ActionPlan(
|
||||||
self.ctx, id=4, uuid=utils.generate_uuid(),
|
self.ctx, id=4, uuid=utils.generate_uuid(),
|
||||||
audit_id=audit4.id, strategy_id=strategy4.id,
|
audit_id=audit4.id, strategy_id=strategy4.id,
|
||||||
first_action_id=None, state='DOESNOTMATTER',
|
state='DOESNOTMATTER', global_efficacy={})
|
||||||
global_efficacy={})
|
|
||||||
|
|
||||||
action_plan1.create()
|
action_plan1.create()
|
||||||
action_plan2.create()
|
action_plan2.create()
|
||||||
@ -575,14 +571,12 @@ class TestSyncer(base.DbTestCase):
|
|||||||
action_plan1 = objects.ActionPlan(
|
action_plan1 = objects.ActionPlan(
|
||||||
self.ctx, id=1, uuid=utils.generate_uuid(),
|
self.ctx, id=1, uuid=utils.generate_uuid(),
|
||||||
audit_id=audit1.id, strategy_id=strategy1.id,
|
audit_id=audit1.id, strategy_id=strategy1.id,
|
||||||
first_action_id=None, state='DOESNOTMATTER',
|
state='DOESNOTMATTER', global_efficacy={})
|
||||||
global_efficacy={})
|
|
||||||
# Stale after syncing because its related goal has been soft deleted
|
# Stale after syncing because its related goal has been soft deleted
|
||||||
action_plan2 = objects.ActionPlan(
|
action_plan2 = objects.ActionPlan(
|
||||||
self.ctx, id=2, uuid=utils.generate_uuid(),
|
self.ctx, id=2, uuid=utils.generate_uuid(),
|
||||||
audit_id=audit2.id, strategy_id=strategy2.id,
|
audit_id=audit2.id, strategy_id=strategy2.id,
|
||||||
first_action_id=None, state='DOESNOTMATTER',
|
state='DOESNOTMATTER', global_efficacy={})
|
||||||
global_efficacy={})
|
|
||||||
|
|
||||||
action_plan1.create()
|
action_plan1.create()
|
||||||
action_plan2.create()
|
action_plan2.create()
|
||||||
|
@ -413,8 +413,8 @@ expected_object_fingerprints = {
|
|||||||
'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b',
|
'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b',
|
||||||
'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b',
|
'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b',
|
||||||
'Audit': '1.2-910522db78b7b1cb59df614754656db4',
|
'Audit': '1.2-910522db78b7b1cb59df614754656db4',
|
||||||
'ActionPlan': '1.2-42709eadf6b2bd228ea87817e8c3e31e',
|
'ActionPlan': '2.0-394f1abbf5d73d7b6675a118fe1a0284',
|
||||||
'Action': '1.1-52c77e4db4ce0aa9480c9760faec61a1',
|
'Action': '2.0-1dd4959a7e7ac30c62ef170fe08dd935',
|
||||||
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',
|
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',
|
||||||
'ScoringEngine': '1.0-4abbe833544000728e17bd9e83f97576',
|
'ScoringEngine': '1.0-4abbe833544000728e17bd9e83f97576',
|
||||||
'Service': '1.0-4b35b99ada9677a882c9de2b30212f35',
|
'Service': '1.0-4b35b99ada9677a882c9de2b30212f35',
|
||||||
|
@ -45,18 +45,19 @@ class TestShowListAction(base.BaseInfraOptimTest):
|
|||||||
|
|
||||||
@test.attr(type='smoke')
|
@test.attr(type='smoke')
|
||||||
def test_show_one_action(self):
|
def test_show_one_action(self):
|
||||||
_, action = self.client.show_action(
|
_, action_uuid = self.client.list_actions(
|
||||||
self.action_plan["first_action_uuid"])
|
action_plan_uuid=self.action_plan['uuid'])['actions'][0]['uuid']
|
||||||
|
_, action = self.client.show_action(action_uuid)
|
||||||
|
|
||||||
self.assertEqual(self.action_plan["first_action_uuid"],
|
self.assertEqual(action_uuid, action['uuid'])
|
||||||
action['uuid'])
|
|
||||||
self.assertEqual("nop", action['action_type'])
|
self.assertEqual("nop", action['action_type'])
|
||||||
self.assertEqual("PENDING", action['state'])
|
self.assertEqual("PENDING", action['state'])
|
||||||
|
|
||||||
@test.attr(type='smoke')
|
@test.attr(type='smoke')
|
||||||
def test_show_action_with_links(self):
|
def test_show_action_with_links(self):
|
||||||
_, action = self.client.show_action(
|
_, action_uuid = self.client.list_actions(
|
||||||
self.action_plan["first_action_uuid"])
|
action_plan_uuid=self.action_plan['uuid'])['actions'][0]['uuid']
|
||||||
|
_, action = self.client.show_action(action_uuid)
|
||||||
self.assertIn('links', action.keys())
|
self.assertIn('links', action.keys())
|
||||||
self.assertEqual(2, len(action['links']))
|
self.assertEqual(2, len(action['links']))
|
||||||
self.assertIn(action['uuid'], action['links'][0]['href'])
|
self.assertIn(action['uuid'], action['links'][0]['href'])
|
||||||
|
Loading…
Reference in New Issue
Block a user