Add a generic and extensible way to describe the flow of actions
In watcher, an audit generates a set of actions which aims at achieving a given goal (lower energy consumption, ...). It is possible to configure different strategies in order to achieve each goal. Each strategy is written as a Python class which produces a set of actions. Today, the set of possible actions is fixed for a given version of Watcher and enables optimization algorithms to include actions such as instance migration, changing hypervisor state, changing power state (ACPI level, ...). This patchset propose a generic and extensible way to describe the actions and his parameters that we want to add to Action Plan. It also remove the static actions because they are now deprecated. The documentation regarding strategy plugin need to be updated (plugins.rst). DocImpact Partially implements: blueprint watcher-add-actions-via-conf Change-Id: I3d641080e8ad89786abca79a942c8deb2d53355b
This commit is contained in:
@@ -1,45 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
# Copyright (c) 2015 b<>com
|
|
||||||
#
|
|
||||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
import abc
|
|
||||||
|
|
||||||
import six
|
|
||||||
from watcher.decision_engine.strategy.common.level import StrategyLevel
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class BaseAction(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._level = StrategyLevel.conservative
|
|
||||||
self._priority = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def level(self):
|
|
||||||
return self._level
|
|
||||||
|
|
||||||
@level.setter
|
|
||||||
def level(self, l):
|
|
||||||
self._level = l
|
|
||||||
|
|
||||||
@property
|
|
||||||
def priority(self):
|
|
||||||
return self._priority
|
|
||||||
|
|
||||||
@priority.setter
|
|
||||||
def priority(self, p):
|
|
||||||
self._priority = p
|
|
||||||
@@ -1,52 +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 watcher.decision_engine.actions.base import BaseAction
|
|
||||||
from watcher.decision_engine.model.hypervisor_state import HypervisorState
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeHypervisorState(BaseAction):
|
|
||||||
def __init__(self, target):
|
|
||||||
'''The target host to change the state
|
|
||||||
|
|
||||||
:param target: the target hypervisor uuid
|
|
||||||
'''
|
|
||||||
super(ChangeHypervisorState, self).__init__()
|
|
||||||
self._target = target
|
|
||||||
self._state = HypervisorState.ONLINE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@state.setter
|
|
||||||
def state(self, state):
|
|
||||||
self._state = state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target(self):
|
|
||||||
return self._target
|
|
||||||
|
|
||||||
@target.setter
|
|
||||||
def target(self, p):
|
|
||||||
self._target = p
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} ChangeHypervisorState => {}".format(self.target,
|
|
||||||
self.state)
|
|
||||||
@@ -1,72 +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 enum import Enum
|
|
||||||
|
|
||||||
from watcher.decision_engine.actions.base import BaseAction
|
|
||||||
|
|
||||||
|
|
||||||
class MigrationType(Enum):
|
|
||||||
# Total migration time and downtime depend on memory dirtying speed
|
|
||||||
pre_copy = 0
|
|
||||||
# Postcopy transfer a page only once reliability
|
|
||||||
post_copy = 1
|
|
||||||
|
|
||||||
|
|
||||||
class Migrate(BaseAction):
|
|
||||||
def __init__(self, vm, src_hypervisor, dest_hypervisor):
|
|
||||||
"""Request to migrate a virtual machine from a host to another
|
|
||||||
|
|
||||||
:param vm: the virtual machine uuid to migrate
|
|
||||||
:param src_hypervisor: uuid
|
|
||||||
:param dest_hypervisor: uuid
|
|
||||||
"""
|
|
||||||
super(Migrate, self).__init__()
|
|
||||||
self._reserved_disk_iops = 0
|
|
||||||
self._remaining_dirty_pages = 0
|
|
||||||
self._vm = vm
|
|
||||||
self._migration_type = MigrationType.pre_copy
|
|
||||||
self._src_hypervisor = src_hypervisor
|
|
||||||
self._dest_hypervisor = dest_hypervisor
|
|
||||||
|
|
||||||
@property
|
|
||||||
def migration_type(self):
|
|
||||||
return self._migration_type
|
|
||||||
|
|
||||||
@migration_type.setter
|
|
||||||
def migration_type(self, type):
|
|
||||||
self._migration_type = type
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vm(self):
|
|
||||||
return self._vm
|
|
||||||
|
|
||||||
@property
|
|
||||||
def src_hypervisor(self):
|
|
||||||
return self._src_hypervisor
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dest_hypervisor(self):
|
|
||||||
return self._dest_hypervisor
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Migrate {} from {} to {}".format(
|
|
||||||
self.vm,
|
|
||||||
self.src_hypervisor,
|
|
||||||
self.dest_hypervisor)
|
|
||||||
@@ -1,26 +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 watcher.decision_engine.actions.base import BaseAction
|
|
||||||
|
|
||||||
|
|
||||||
class Nop(BaseAction):
|
|
||||||
def __str__(self):
|
|
||||||
return "Nop"
|
|
||||||
@@ -1,52 +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 watcher.decision_engine.actions.base import BaseAction
|
|
||||||
from watcher.decision_engine.model.power_state import PowerState
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePowerState(BaseAction):
|
|
||||||
def __init__(self, target):
|
|
||||||
"""The target host to change the power
|
|
||||||
|
|
||||||
:param target:
|
|
||||||
"""
|
|
||||||
super(ChangePowerState, self).__init__()
|
|
||||||
self._target = target
|
|
||||||
self._power_state = PowerState.g0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def powerstate(self):
|
|
||||||
return self._power_state
|
|
||||||
|
|
||||||
@powerstate.setter
|
|
||||||
def powerstate(self, p):
|
|
||||||
self._power_state = p
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target(self):
|
|
||||||
return self._target
|
|
||||||
|
|
||||||
@target.setter
|
|
||||||
def target(self, t):
|
|
||||||
self._target = t
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "ChangePowerState {} => {} ".format(self.target,
|
|
||||||
self.powerstate)
|
|
||||||
@@ -23,5 +23,5 @@ import six
|
|||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseAuditHandler(object):
|
class BaseAuditHandler(object):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def execute(self):
|
def execute(self, audit_uuid, request_context):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -17,26 +17,19 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher._i18n import _LW
|
from watcher._i18n import _LW
|
||||||
from watcher.common import exception
|
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
from watcher.decision_engine.actions import hypervisor_state
|
|
||||||
from watcher.decision_engine.actions import migration
|
|
||||||
from watcher.decision_engine.actions import nop
|
|
||||||
from watcher.decision_engine.actions import power_state
|
|
||||||
from watcher.decision_engine.planner import base
|
from watcher.decision_engine.planner import base
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO(jed) The default planner is a very simple planner
|
|
||||||
# https://wiki.openstack.org/wiki/NovaOrchestration/WorkflowEngines
|
|
||||||
|
|
||||||
|
|
||||||
class Primitives(enum.Enum):
|
class Primitives(enum.Enum):
|
||||||
LIVE_MIGRATE = 'MIGRATE'
|
LIVE_MIGRATE = 'MIGRATE'
|
||||||
COLD_MIGRATE = 'MIGRATE'
|
COLD_MIGRATE = 'MIGRATE'
|
||||||
@@ -45,32 +38,25 @@ class Primitives(enum.Enum):
|
|||||||
NOP = 'NOP'
|
NOP = 'NOP'
|
||||||
|
|
||||||
|
|
||||||
priority_primitives = {
|
|
||||||
Primitives.NOP.value: 0,
|
|
||||||
Primitives.HYPERVISOR_STATE.value: 1,
|
|
||||||
Primitives.LIVE_MIGRATE.value: 2,
|
|
||||||
Primitives.COLD_MIGRATE.value: 3,
|
|
||||||
Primitives.POWER_STATE.value: 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultPlanner(base.BasePlanner):
|
class DefaultPlanner(base.BasePlanner):
|
||||||
def create_action(self, action_plan_id, action_type, applies_to=None,
|
priorities = {
|
||||||
src=None,
|
'nop': 0,
|
||||||
dst=None,
|
'migrate': 1,
|
||||||
parameter=None,
|
'change_nova_service_state': 2,
|
||||||
description=None):
|
}
|
||||||
uuid = utils.generate_uuid()
|
|
||||||
|
|
||||||
|
def create_action(self,
|
||||||
|
action_plan_id,
|
||||||
|
action_type,
|
||||||
|
applies_to,
|
||||||
|
input_parameters=None):
|
||||||
|
uuid = utils.generate_uuid()
|
||||||
action = {
|
action = {
|
||||||
'uuid': uuid,
|
'uuid': uuid,
|
||||||
'action_plan_id': int(action_plan_id),
|
'action_plan_id': int(action_plan_id),
|
||||||
'action_type': action_type,
|
'action_type': action_type,
|
||||||
'applies_to': applies_to,
|
'applies_to': applies_to,
|
||||||
'src': src,
|
'input_parameters': json.dumps(input_parameters),
|
||||||
'dst': dst,
|
|
||||||
'parameter': parameter,
|
|
||||||
'description': description,
|
|
||||||
'state': objects.action.Status.PENDING,
|
'state': objects.action.Status.PENDING,
|
||||||
'alarm': None,
|
'alarm': None,
|
||||||
'next': None,
|
'next': None,
|
||||||
@@ -83,53 +69,19 @@ class DefaultPlanner(base.BasePlanner):
|
|||||||
|
|
||||||
actions = list(solution.actions)
|
actions = list(solution.actions)
|
||||||
to_schedule = []
|
to_schedule = []
|
||||||
|
|
||||||
for action in actions:
|
for action in actions:
|
||||||
if isinstance(action, migration.Migrate):
|
json_action = self.create_action(action_plan_id=action_plan.id,
|
||||||
# TODO(jed) type
|
action_type=action.get(
|
||||||
primitive = self.create_action(action_plan.id,
|
'action_type'),
|
||||||
Primitives.LIVE_MIGRATE.value,
|
applies_to=action.get(
|
||||||
action.vm.uuid,
|
'applies_to'),
|
||||||
action.src_hypervisor.
|
input_parameters=action.get(
|
||||||
uuid,
|
'input_parameters'))
|
||||||
action.dest_hypervisor.
|
to_schedule.append((self.priorities[action.get('action_type')],
|
||||||
uuid,
|
json_action))
|
||||||
description="{0}".format(
|
|
||||||
action)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif isinstance(action, power_state.ChangePowerState):
|
|
||||||
primitive = self.create_action(action_plan_id=action_plan.id,
|
|
||||||
action_type=Primitives.
|
|
||||||
POWER_STATE.value,
|
|
||||||
applies_to=action.target.uuid,
|
|
||||||
parameter=action.
|
|
||||||
powerstate.
|
|
||||||
value,
|
|
||||||
description="{0}".format(
|
|
||||||
action))
|
|
||||||
elif isinstance(action, hypervisor_state.ChangeHypervisorState):
|
|
||||||
primitive = self.create_action(action_plan_id=action_plan.id,
|
|
||||||
action_type=Primitives.
|
|
||||||
HYPERVISOR_STATE.value,
|
|
||||||
applies_to=action.target.uuid,
|
|
||||||
parameter=action.state.
|
|
||||||
value,
|
|
||||||
description="{0}".format(
|
|
||||||
action))
|
|
||||||
elif isinstance(action, nop.Nop):
|
|
||||||
primitive = self.create_action(action_plan_id=action_plan.id,
|
|
||||||
action_type=Primitives.
|
|
||||||
NOP.value,
|
|
||||||
description="{0}".format(
|
|
||||||
action))
|
|
||||||
else:
|
|
||||||
raise exception.ActionNotFound()
|
|
||||||
priority = priority_primitives[primitive['action_type']]
|
|
||||||
to_schedule.append((priority, primitive))
|
|
||||||
|
|
||||||
# scheduling
|
# scheduling
|
||||||
scheduled = sorted(to_schedule, reverse=False, key=lambda x: (x[0]))
|
scheduled = sorted(to_schedule, key=lambda x: (x[0]))
|
||||||
if len(scheduled) == 0:
|
if len(scheduled) == 0:
|
||||||
LOG.warning(_LW("The action plan is empty"))
|
LOG.warning(_LW("The action plan is empty"))
|
||||||
action_plan.first_action_id = None
|
action_plan.first_action_id = None
|
||||||
@@ -147,6 +99,7 @@ class DefaultPlanner(base.BasePlanner):
|
|||||||
action = self._create_action(context, s_action[1],
|
action = self._create_action(context, s_action[1],
|
||||||
parent_action)
|
parent_action)
|
||||||
parent_action = action
|
parent_action = action
|
||||||
|
|
||||||
return action_plan
|
return action_plan
|
||||||
|
|
||||||
def _create_action_plan(self, context, audit_id):
|
def _create_action_plan(self, context, audit_id):
|
||||||
|
|||||||
@@ -52,7 +52,21 @@ class BaseSolution(object):
|
|||||||
self._origin = m
|
self._origin = m
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def add_change_request(self, r):
|
def add_action(self,
|
||||||
|
action_type,
|
||||||
|
applies_to,
|
||||||
|
input_parameters=None):
|
||||||
|
"""Add a new Action in the Action Plan
|
||||||
|
|
||||||
|
:param action_type: the unique id of an action type defined in
|
||||||
|
entry point 'watcher_actions'
|
||||||
|
:param applies_to: the unique id of the resource to which the
|
||||||
|
`Action` applies.
|
||||||
|
:param input_parameters: An array of input parameters provided as
|
||||||
|
key-value pairs of strings.
|
||||||
|
Each key-pair contains names and values that match what was previously
|
||||||
|
defined in the `Action` type schema.
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abc.abstractproperty
|
@abc.abstractproperty
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.decision_engine.solution.base import BaseSolution
|
from watcher.decision_engine.solution.base import BaseSolution
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@@ -32,8 +33,16 @@ class DefaultSolution(BaseSolution):
|
|||||||
super(DefaultSolution, self).__init__()
|
super(DefaultSolution, self).__init__()
|
||||||
self._actions = []
|
self._actions = []
|
||||||
|
|
||||||
def add_change_request(self, r):
|
def add_action(self, action_type,
|
||||||
self._actions.append(r)
|
applies_to,
|
||||||
|
input_parameters=None):
|
||||||
|
# todo(jed) add https://pypi.python.org/pypi/schema
|
||||||
|
action = {
|
||||||
|
'action_type': action_type,
|
||||||
|
'applies_to': applies_to,
|
||||||
|
'input_parameters': input_parameters
|
||||||
|
}
|
||||||
|
self._actions.append(action)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "\n".join(self._actions)
|
return "\n".join(self._actions)
|
||||||
|
|||||||
@@ -20,18 +20,10 @@
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher._i18n import _LE, _LI, _LW
|
from watcher._i18n import _LE, _LI, _LW
|
||||||
from watcher.common.exception import ClusterEmpty
|
from watcher.common import exception
|
||||||
from watcher.common.exception import ClusterStateNotDefined
|
|
||||||
from watcher.decision_engine.actions.hypervisor_state import \
|
|
||||||
ChangeHypervisorState
|
|
||||||
from watcher.decision_engine.actions.migration import Migrate
|
|
||||||
from watcher.decision_engine.actions.migration import MigrationType
|
|
||||||
from watcher.decision_engine.actions.power_state import ChangePowerState
|
|
||||||
from watcher.decision_engine.model.hypervisor_state import HypervisorState
|
from watcher.decision_engine.model.hypervisor_state import HypervisorState
|
||||||
from watcher.decision_engine.model.power_state import PowerState
|
|
||||||
from watcher.decision_engine.model.resource import ResourceType
|
from watcher.decision_engine.model.resource import ResourceType
|
||||||
from watcher.decision_engine.model.vm_state import VMState
|
from watcher.decision_engine.model.vm_state import VMState
|
||||||
from watcher.decision_engine.strategy.common.level import StrategyLevel
|
|
||||||
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
|
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
|
||||||
from watcher.metrics_engine.cluster_history.ceilometer import \
|
from watcher.metrics_engine.cluster_history.ceilometer import \
|
||||||
CeilometerClusterHistory
|
CeilometerClusterHistory
|
||||||
@@ -43,8 +35,11 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
DEFAULT_NAME = "basic"
|
DEFAULT_NAME = "basic"
|
||||||
DEFAULT_DESCRIPTION = "Basic offline consolidation"
|
DEFAULT_DESCRIPTION = "Basic offline consolidation"
|
||||||
|
|
||||||
host_cpu_usage_metric_name = 'compute.node.cpu.percent'
|
HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent'
|
||||||
instance_cpu_usage_metric_name = 'cpu_util'
|
INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util'
|
||||||
|
|
||||||
|
MIGRATION = "migrate"
|
||||||
|
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
||||||
"""Basic offline Consolidation using live migration
|
"""Basic offline Consolidation using live migration
|
||||||
@@ -125,14 +120,14 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
src_hypervisor,
|
src_hypervisor,
|
||||||
dest_hypervisor,
|
dest_hypervisor,
|
||||||
vm_to_mig):
|
vm_to_mig):
|
||||||
'''check if the migration is possible
|
"""check if the migration is possible
|
||||||
|
|
||||||
:param model: the current state of the cluster
|
:param model: the current state of the cluster
|
||||||
:param src_hypervisor: the current node of the virtual machine
|
:param src_hypervisor: the current node of the virtual machine
|
||||||
:param dest_hypervisor: the destination of the virtual machine
|
:param dest_hypervisor: the destination of the virtual machine
|
||||||
:param vm_to_mig: the virtual machine
|
:param vm_to_mig: the virtual machine
|
||||||
:return: True if the there is enough place otherwise false
|
:return: True if the there is enough place otherwise false
|
||||||
'''
|
"""
|
||||||
if src_hypervisor == dest_hypervisor:
|
if src_hypervisor == dest_hypervisor:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -263,7 +258,7 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
resource_id = "%s_%s" % (hypervisor.uuid, hypervisor.hostname)
|
resource_id = "%s_%s" % (hypervisor.uuid, hypervisor.hostname)
|
||||||
cpu_avg_vm = self.ceilometer. \
|
cpu_avg_vm = self.ceilometer. \
|
||||||
statistic_aggregation(resource_id=resource_id,
|
statistic_aggregation(resource_id=resource_id,
|
||||||
meter_name=self.host_cpu_usage_metric_name,
|
meter_name=self.HOST_CPU_USAGE_METRIC_NAME,
|
||||||
period="7200",
|
period="7200",
|
||||||
aggregate='avg'
|
aggregate='avg'
|
||||||
)
|
)
|
||||||
@@ -272,7 +267,7 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
_LE("No values returned by %(resource_id)s "
|
_LE("No values returned by %(resource_id)s "
|
||||||
"for %(metric_name)s"),
|
"for %(metric_name)s"),
|
||||||
resource_id=resource_id,
|
resource_id=resource_id,
|
||||||
metric_name=self.host_cpu_usage_metric_name,
|
metric_name=self.HOST_CPU_USAGE_METRIC_NAME,
|
||||||
)
|
)
|
||||||
cpu_avg_vm = 100
|
cpu_avg_vm = 100
|
||||||
|
|
||||||
@@ -305,12 +300,12 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
:return: score
|
:return: score
|
||||||
"""
|
"""
|
||||||
if model is None:
|
if model is None:
|
||||||
raise ClusterStateNotDefined()
|
raise exception.ClusterStateNotDefined()
|
||||||
|
|
||||||
vm_cpu_utilization = self.ceilometer. \
|
vm_cpu_utilization = self.ceilometer. \
|
||||||
statistic_aggregation(
|
statistic_aggregation(
|
||||||
resource_id=vm.uuid,
|
resource_id=vm.uuid,
|
||||||
meter_name=self.instance_cpu_usage_metric_name,
|
meter_name=self.INSTANCE_CPU_USAGE_METRIC_NAME,
|
||||||
period="7200",
|
period="7200",
|
||||||
aggregate='avg'
|
aggregate='avg'
|
||||||
)
|
)
|
||||||
@@ -319,7 +314,7 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
_LE("No values returned by %(resource_id)s "
|
_LE("No values returned by %(resource_id)s "
|
||||||
"for %(metric_name)s"),
|
"for %(metric_name)s"),
|
||||||
resource_id=vm.uuid,
|
resource_id=vm.uuid,
|
||||||
metric_name=self.instance_cpu_usage_metric_name,
|
metric_name=self.INSTANCE_CPU_USAGE_METRIC_NAME,
|
||||||
)
|
)
|
||||||
vm_cpu_utilization = 100
|
vm_cpu_utilization = 100
|
||||||
|
|
||||||
@@ -332,22 +327,29 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
0,
|
0,
|
||||||
0)
|
0)
|
||||||
|
|
||||||
def print_utilization(self, model):
|
def add_change_service_state(self, applies_to, state):
|
||||||
if model is None:
|
parameters = {'state': state}
|
||||||
raise ClusterStateNotDefined()
|
self.solution.add_action(action_type=self.CHANGE_NOVA_SERVICE_STATE,
|
||||||
for node_id in model.get_all_hypervisors():
|
applies_to=applies_to,
|
||||||
LOG.debug("{0} utilization {1} % ".
|
input_parameters=parameters)
|
||||||
format(node_id,
|
|
||||||
self.calculate_score_node(
|
def add_migration(self,
|
||||||
model.get_hypervisor_from_id(
|
applies_to,
|
||||||
node_id),
|
migration_type,
|
||||||
model)))
|
src_hypervisor_uuid,
|
||||||
|
dst_hypervisor_uuid):
|
||||||
|
parameters = {'migration_type': migration_type,
|
||||||
|
'src_hypervisor_uuid': src_hypervisor_uuid,
|
||||||
|
'dst_hypervisor_uuid': dst_hypervisor_uuid}
|
||||||
|
self.solution.add_action(action_type=self.MIGRATION,
|
||||||
|
applies_to=applies_to,
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
def execute(self, orign_model):
|
def execute(self, orign_model):
|
||||||
LOG.info(_LI("Initializing Sercon Consolidation"))
|
LOG.info(_LI("Initializing Sercon Consolidation"))
|
||||||
|
|
||||||
if orign_model is None:
|
if orign_model is None:
|
||||||
raise ClusterStateNotDefined()
|
raise exception.ClusterStateNotDefined()
|
||||||
|
|
||||||
# todo(jed) clone model
|
# todo(jed) clone model
|
||||||
current_model = orign_model
|
current_model = orign_model
|
||||||
@@ -358,7 +360,7 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
first = True
|
first = True
|
||||||
size_cluster = len(current_model.get_all_hypervisors())
|
size_cluster = len(current_model.get_all_hypervisors())
|
||||||
if size_cluster == 0:
|
if size_cluster == 0:
|
||||||
raise ClusterEmpty()
|
raise exception.ClusterEmpty()
|
||||||
|
|
||||||
self.compute_attempts(size_cluster)
|
self.compute_attempts(size_cluster)
|
||||||
|
|
||||||
@@ -367,15 +369,10 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
count = current_model.get_mapping(). \
|
count = current_model.get_mapping(). \
|
||||||
get_node_vms_from_id(hypervisor_id)
|
get_node_vms_from_id(hypervisor_id)
|
||||||
if len(count) == 0:
|
if len(count) == 0:
|
||||||
change_power = ChangePowerState(hypervisor)
|
|
||||||
change_power.powerstate = PowerState.g1_S1
|
|
||||||
change_power.level = StrategyLevel.conservative
|
|
||||||
self.solution.add_change_request(change_power)
|
|
||||||
if hypervisor.state == HypervisorState.ONLINE:
|
if hypervisor.state == HypervisorState.ONLINE:
|
||||||
h = ChangeHypervisorState(hypervisor)
|
self.add_change_service_state(hypervisor_id,
|
||||||
h.level = StrategyLevel.aggressive
|
HypervisorState.
|
||||||
h.state = HypervisorState.OFFLINE
|
OFFLINE.value)
|
||||||
self.solution.add_change_request(h)
|
|
||||||
|
|
||||||
while self.get_allowed_migration_attempts() >= unsuccessful_migration:
|
while self.get_allowed_migration_attempts() >= unsuccessful_migration:
|
||||||
if first is not True:
|
if first is not True:
|
||||||
@@ -430,7 +427,6 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
LOG.debug("VM(s) BFD {0}".format(v))
|
LOG.debug("VM(s) BFD {0}".format(v))
|
||||||
|
|
||||||
m = 0
|
m = 0
|
||||||
tmp_vm_migration_schedule = []
|
|
||||||
for vm in v:
|
for vm in v:
|
||||||
for j in range(0, len(s)):
|
for j in range(0, len(s)):
|
||||||
mig_vm = current_model.get_vm_from_id(vm[0])
|
mig_vm = current_model.get_vm_from_id(vm[0])
|
||||||
@@ -448,31 +444,16 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
if current_model.get_mapping(). \
|
if current_model.get_mapping(). \
|
||||||
migrate_vm(mig_vm, mig_src_hypervisor,
|
migrate_vm(mig_vm, mig_src_hypervisor,
|
||||||
mig_dst_hypervisor):
|
mig_dst_hypervisor):
|
||||||
live_migrate = Migrate(mig_vm,
|
self.add_migration(mig_vm.uuid, 'live',
|
||||||
mig_src_hypervisor,
|
mig_src_hypervisor.uuid,
|
||||||
mig_dst_hypervisor)
|
mig_dst_hypervisor.uuid)
|
||||||
# live migration
|
|
||||||
live_migrate.migration_type = \
|
|
||||||
MigrationType.pre_copy
|
|
||||||
live_migrate.level = StrategyLevel.conservative
|
|
||||||
|
|
||||||
tmp_vm_migration_schedule.append(live_migrate)
|
|
||||||
|
|
||||||
if len(current_model.get_mapping().get_node_vms(
|
if len(current_model.get_mapping().get_node_vms(
|
||||||
mig_src_hypervisor)) == 0:
|
mig_src_hypervisor)) == 0:
|
||||||
# TODO(jed) how to manage strategy level
|
self.add_change_service_state(mig_src_hypervisor.
|
||||||
# from conservative to aggressive
|
uuid,
|
||||||
change_power = ChangePowerState(mig_src_hypervisor)
|
HypervisorState.
|
||||||
change_power.powerstate = PowerState.g1_S1
|
OFFLINE.value)
|
||||||
change_power.level = StrategyLevel.conservative
|
|
||||||
tmp_vm_migration_schedule.append(change_power)
|
|
||||||
|
|
||||||
h = ChangeHypervisorState(mig_src_hypervisor)
|
|
||||||
h.level = StrategyLevel.aggressive
|
|
||||||
|
|
||||||
h.state = HypervisorState.OFFLINE
|
|
||||||
tmp_vm_migration_schedule.append(h)
|
|
||||||
|
|
||||||
self.number_of_released_nodes += 1
|
self.number_of_released_nodes += 1
|
||||||
|
|
||||||
m += 1
|
m += 1
|
||||||
@@ -480,11 +461,8 @@ class BasicConsolidation(BaseStrategy):
|
|||||||
if m > 0:
|
if m > 0:
|
||||||
self.number_of_migrations = self.number_of_migrations + m
|
self.number_of_migrations = self.number_of_migrations + m
|
||||||
unsuccessful_migration = 0
|
unsuccessful_migration = 0
|
||||||
for a in tmp_vm_migration_schedule:
|
|
||||||
self.solution.add_change_request(a)
|
|
||||||
else:
|
else:
|
||||||
unsuccessful_migration += 1
|
unsuccessful_migration += 1
|
||||||
# self.print_utilization(current_model)
|
|
||||||
infos = {
|
infos = {
|
||||||
"number_of_migrations": self.number_of_migrations,
|
"number_of_migrations": self.number_of_migrations,
|
||||||
"number_of_nodes_released": self.number_of_released_nodes,
|
"number_of_nodes_released": self.number_of_released_nodes,
|
||||||
|
|||||||
@@ -17,22 +17,27 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
|
|
||||||
|
|
||||||
from watcher.decision_engine.actions.nop import Nop
|
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DummyStrategy(BaseStrategy):
|
class DummyStrategy(BaseStrategy):
|
||||||
|
|
||||||
DEFAULT_NAME = "dummy"
|
DEFAULT_NAME = "dummy"
|
||||||
DEFAULT_DESCRIPTION = "Dummy Strategy"
|
DEFAULT_DESCRIPTION = "Dummy Strategy"
|
||||||
|
|
||||||
|
NOP = "nop"
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
||||||
super(DummyStrategy, self).__init__(name, description)
|
super(DummyStrategy, self).__init__(name, description)
|
||||||
|
|
||||||
def execute(self, model):
|
def execute(self, model):
|
||||||
n = Nop()
|
parameters = {'message': 'hello World'}
|
||||||
self.solution.add_change_request(n)
|
self.solution.add_action(action_type=self.NOP,
|
||||||
|
applies_to="",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
|
# todo(jed) add a new action to test the flow
|
||||||
|
# with two differents actions
|
||||||
return self.solution
|
return self.solution
|
||||||
|
|||||||
@@ -20,11 +20,8 @@ from oslo_log import log
|
|||||||
|
|
||||||
from watcher._i18n import _LE
|
from watcher._i18n import _LE
|
||||||
from watcher.common import exception as wexc
|
from watcher.common import exception as wexc
|
||||||
from watcher.decision_engine.actions.migration import Migrate
|
|
||||||
from watcher.decision_engine.actions.migration import MigrationType
|
|
||||||
from watcher.decision_engine.model.resource import ResourceType
|
from watcher.decision_engine.model.resource import ResourceType
|
||||||
from watcher.decision_engine.model.vm_state import VMState
|
from watcher.decision_engine.model.vm_state import VMState
|
||||||
from watcher.decision_engine.strategy.common.level import StrategyLevel
|
|
||||||
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
|
from watcher.decision_engine.strategy.strategies.base import BaseStrategy
|
||||||
from watcher.metrics_engine.cluster_history.ceilometer import \
|
from watcher.metrics_engine.cluster_history.ceilometer import \
|
||||||
CeilometerClusterHistory
|
CeilometerClusterHistory
|
||||||
@@ -41,6 +38,8 @@ class OutletTempControl(BaseStrategy):
|
|||||||
# Unit: degree C
|
# Unit: degree C
|
||||||
THRESHOLD = 35.0
|
THRESHOLD = 35.0
|
||||||
|
|
||||||
|
MIGRATION = "migrate"
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION):
|
||||||
"""[PoC]Outlet temperature control using live migration
|
"""[PoC]Outlet temperature control using live migration
|
||||||
|
|
||||||
@@ -230,21 +229,19 @@ class OutletTempControl(BaseStrategy):
|
|||||||
LOG.info(_LE("No proper target host could be found"))
|
LOG.info(_LE("No proper target host could be found"))
|
||||||
return self.solution
|
return self.solution
|
||||||
|
|
||||||
dest_servers = sorted(dest_servers,
|
dest_servers = sorted(dest_servers, key=lambda x: (x["outlet_temp"]))
|
||||||
reverse=False,
|
|
||||||
key=lambda x: (x["outlet_temp"]))
|
|
||||||
# always use the host with lowerest outlet temperature
|
# always use the host with lowerest outlet temperature
|
||||||
mig_dst_hypervisor = dest_servers[0]['hv']
|
mig_dst_hypervisor = dest_servers[0]['hv']
|
||||||
# generate solution to migrate the vm to the dest server,
|
# generate solution to migrate the vm to the dest server,
|
||||||
if current_model.get_mapping().migrate_vm(vm_src,
|
if current_model.get_mapping().migrate_vm(vm_src,
|
||||||
mig_src_hypervisor,
|
mig_src_hypervisor,
|
||||||
mig_dst_hypervisor):
|
mig_dst_hypervisor):
|
||||||
live_migrate = Migrate(vm_src,
|
parameters = {'migration_type': 'live',
|
||||||
mig_src_hypervisor,
|
'src_hypervisor_uuid': mig_src_hypervisor,
|
||||||
mig_dst_hypervisor)
|
'dst_hypervisor_uuid': mig_dst_hypervisor}
|
||||||
live_migrate.migration_type = MigrationType.pre_copy
|
self.solution.add_action(action_type=self.MIGRATION,
|
||||||
live_migrate.level = StrategyLevel.conservative
|
applies_to=vm_src,
|
||||||
self.solution.add_change_request(live_migrate)
|
input_parameters=parameters)
|
||||||
|
|
||||||
self.solution.model = current_model
|
self.solution.model = current_model
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def get_test_action(**kwargs):
|
|||||||
'id': kwargs.get('id', 1),
|
'id': kwargs.get('id', 1),
|
||||||
'uuid': kwargs.get('uuid', '10a47dd1-4874-4298-91cf-eff046dbdb8d'),
|
'uuid': kwargs.get('uuid', '10a47dd1-4874-4298-91cf-eff046dbdb8d'),
|
||||||
'action_plan_id': kwargs.get('action_plan_id', 1),
|
'action_plan_id': kwargs.get('action_plan_id', 1),
|
||||||
'action_type': kwargs.get('action_type', 'COLD_MIGRATION'),
|
'action_type': kwargs.get('action_type', 'nop'),
|
||||||
'applies_to': kwargs.get('applies_to',
|
'applies_to': kwargs.get('applies_to',
|
||||||
'10a47dd1-4874-4298-91cf-eff046dbdb8d'),
|
'10a47dd1-4874-4298-91cf-eff046dbdb8d'),
|
||||||
'input_parameters': kwargs.get('input_parameters', {'key1': 'val1',
|
'input_parameters': kwargs.get('input_parameters', {'key1': 'val1',
|
||||||
|
|||||||
@@ -16,19 +16,17 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from watcher.common import exception
|
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
from watcher.db import api as db_api
|
from watcher.db import api as db_api
|
||||||
from watcher.decision_engine.actions.base import BaseAction
|
|
||||||
from watcher.decision_engine.planner.default import DefaultPlanner
|
from watcher.decision_engine.planner.default import DefaultPlanner
|
||||||
from watcher.decision_engine.solution.default import DefaultSolution
|
from watcher.decision_engine.solution.default import DefaultSolution
|
||||||
from watcher.decision_engine.strategy.strategies.basic_consolidation import \
|
from watcher.decision_engine.strategy.strategies.basic_consolidation import \
|
||||||
BasicConsolidation
|
BasicConsolidation
|
||||||
from watcher.tests.db import base
|
from watcher.tests.db import base
|
||||||
from watcher.tests.db import utils as db_utils
|
from watcher.tests.db import utils as db_utils
|
||||||
from watcher.tests.decision_engine.strategy.strategies.faker_cluster_state\
|
from watcher.tests.decision_engine.strategy.strategies.faker_cluster_state \
|
||||||
import FakerModelCollector
|
import FakerModelCollector
|
||||||
from watcher.tests.decision_engine.strategy.strategies.faker_metrics_collector\
|
from watcher.tests.decision_engine.strategy.strategies.faker_metrics_collector \
|
||||||
import FakerMetricsCollector
|
import FakerMetricsCollector
|
||||||
from watcher.tests.objects import utils as obj_utils
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
@@ -39,8 +37,8 @@ class SolutionFaker(object):
|
|||||||
metrics = FakerMetricsCollector()
|
metrics = FakerMetricsCollector()
|
||||||
current_state_cluster = FakerModelCollector()
|
current_state_cluster = FakerModelCollector()
|
||||||
sercon = BasicConsolidation("basic", "Basic offline consolidation")
|
sercon = BasicConsolidation("basic", "Basic offline consolidation")
|
||||||
sercon.ceilometer = mock.MagicMock(
|
sercon.ceilometer = mock.\
|
||||||
get_statistics=metrics.mock_get_statistics)
|
MagicMock(get_statistics=metrics.mock_get_statistics)
|
||||||
return sercon.execute(current_state_cluster.generate_scenario_1())
|
return sercon.execute(current_state_cluster.generate_scenario_1())
|
||||||
|
|
||||||
|
|
||||||
@@ -50,29 +48,32 @@ class SolutionFakerSingleHyp(object):
|
|||||||
metrics = FakerMetricsCollector()
|
metrics = FakerMetricsCollector()
|
||||||
current_state_cluster = FakerModelCollector()
|
current_state_cluster = FakerModelCollector()
|
||||||
sercon = BasicConsolidation("basic", "Basic offline consolidation")
|
sercon = BasicConsolidation("basic", "Basic offline consolidation")
|
||||||
sercon.ceilometer = mock.MagicMock(
|
sercon.ceilometer = \
|
||||||
get_statistics=metrics.mock_get_statistics)
|
mock.MagicMock(get_statistics=metrics.mock_get_statistics)
|
||||||
|
|
||||||
return sercon.execute(
|
return sercon.execute(
|
||||||
current_state_cluster.generate_scenario_3_with_2_hypervisors())
|
current_state_cluster.generate_scenario_3_with_2_hypervisors())
|
||||||
|
|
||||||
|
|
||||||
class TestActionScheduling(base.DbTestCase):
|
class TestActionScheduling(base.DbTestCase):
|
||||||
scenarios = [
|
|
||||||
(str(action_cls), {"fake_action": mock.Mock(spec=action_cls)})
|
|
||||||
for action_cls in BaseAction.__subclasses__()]
|
|
||||||
|
|
||||||
def test_schedule_actions(self):
|
def test_schedule_actions(self):
|
||||||
default_planner = DefaultPlanner()
|
default_planner = DefaultPlanner()
|
||||||
audit = db_utils.create_test_audit(uuid=utils.generate_uuid())
|
audit = db_utils.create_test_audit(uuid=utils.generate_uuid())
|
||||||
dummy_solution = DefaultSolution()
|
solution = DefaultSolution()
|
||||||
dummy_solution.add_change_request(self.fake_action)
|
|
||||||
|
parameters = {
|
||||||
|
"src_uuid_hypervisor": "server1",
|
||||||
|
"dst_uuid_hypervisor": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="migrate",
|
||||||
|
applies_to="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
DefaultPlanner, "create_action",
|
DefaultPlanner, "create_action",
|
||||||
wraps=default_planner.create_action) as m_create_action:
|
wraps=default_planner.create_action) as m_create_action:
|
||||||
action_plan = default_planner.schedule(
|
action_plan = default_planner.schedule(
|
||||||
self.context, audit.id, dummy_solution
|
self.context, audit.id, solution
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsNotNone(action_plan.uuid)
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
@@ -112,14 +113,6 @@ class TestDefaultPlanner(base.DbTestCase):
|
|||||||
audit.id, fake_solution)
|
audit.id, fake_solution)
|
||||||
self.assertIsNotNone(action_plan.uuid)
|
self.assertIsNotNone(action_plan.uuid)
|
||||||
|
|
||||||
def test_schedule_raise(self):
|
|
||||||
audit = db_utils.create_test_audit(uuid=utils.generate_uuid())
|
|
||||||
fake_solution = SolutionFaker.build()
|
|
||||||
fake_solution.actions[0] = "valeur_qcq"
|
|
||||||
self.assertRaises(exception.ActionNotFound,
|
|
||||||
self.default_planner.schedule,
|
|
||||||
self.context, audit.id, fake_solution)
|
|
||||||
|
|
||||||
def test_schedule_scheduled_empty(self):
|
def test_schedule_scheduled_empty(self):
|
||||||
audit = db_utils.create_test_audit(uuid=utils.generate_uuid())
|
audit = db_utils.create_test_audit(uuid=utils.generate_uuid())
|
||||||
fake_solution = SolutionFakerSingleHyp.build()
|
fake_solution = SolutionFakerSingleHyp.build()
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2015 b<>com
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
from watcher.decision_engine.solution.default import DefaultSolution
|
||||||
|
from watcher.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefaultSolution(base.BaseTestCase):
|
||||||
|
def test_default_solution(self):
|
||||||
|
solution = DefaultSolution()
|
||||||
|
parameters = {
|
||||||
|
"src_uuid_hypervisor": "server1",
|
||||||
|
"dst_uuid_hypervisor": "server2",
|
||||||
|
}
|
||||||
|
solution.add_action(action_type="nop",
|
||||||
|
applies_to="b199db0c-1408-4d52-b5a5-5ca14de0ff36",
|
||||||
|
input_parameters=parameters)
|
||||||
|
self.assertEqual(len(solution.actions), 1)
|
||||||
|
expected_action_type = "nop"
|
||||||
|
expected_applies_to = "b199db0c-1408-4d52-b5a5-5ca14de0ff36"
|
||||||
|
expected_parameters = parameters
|
||||||
|
self.assertEqual(solution.actions[0].get('action_type'),
|
||||||
|
expected_action_type)
|
||||||
|
self.assertEqual(solution.actions[0].get('applies_to'),
|
||||||
|
expected_applies_to)
|
||||||
|
self.assertEqual(solution.actions[0].get('input_parameters'),
|
||||||
|
expected_parameters)
|
||||||
@@ -17,15 +17,9 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from mock import MagicMock
|
|
||||||
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.decision_engine.actions.hypervisor_state import \
|
|
||||||
ChangeHypervisorState
|
|
||||||
from watcher.decision_engine.actions.migration import Migrate
|
|
||||||
from watcher.decision_engine.actions.power_state import ChangePowerState
|
|
||||||
from watcher.decision_engine.model.model_root import ModelRoot
|
from watcher.decision_engine.model.model_root import ModelRoot
|
||||||
from watcher.decision_engine.strategy.strategies.basic_consolidation import \
|
from watcher.decision_engine.strategy.strategies.basic_consolidation import \
|
||||||
BasicConsolidation
|
BasicConsolidation
|
||||||
@@ -52,7 +46,7 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
def test_basic_consolidation_score_hypervisor(self):
|
def test_basic_consolidation_score_hypervisor(self):
|
||||||
cluster = self.fake_cluster.generate_scenario_1()
|
cluster = self.fake_cluster.generate_scenario_1()
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
|
|
||||||
node_1_score = 0.023333333333333317
|
node_1_score = 0.023333333333333317
|
||||||
@@ -74,7 +68,7 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
def test_basic_consolidation_score_vm(self):
|
def test_basic_consolidation_score_vm(self):
|
||||||
cluster = self.fake_cluster.generate_scenario_1()
|
cluster = self.fake_cluster.generate_scenario_1()
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
vm_0 = cluster.get_vm_from_id("VM_0")
|
vm_0 = cluster.get_vm_from_id("VM_0")
|
||||||
vm_0_score = 0.023333333333333317
|
vm_0_score = 0.023333333333333317
|
||||||
@@ -97,7 +91,7 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
def test_basic_consolidation_score_vm_disk(self):
|
def test_basic_consolidation_score_vm_disk(self):
|
||||||
cluster = self.fake_cluster.generate_scenario_5_with_vm_disk_0()
|
cluster = self.fake_cluster.generate_scenario_5_with_vm_disk_0()
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
vm_0 = cluster.get_vm_from_id("VM_0")
|
vm_0 = cluster.get_vm_from_id("VM_0")
|
||||||
vm_0_score = 0.023333333333333355
|
vm_0_score = 0.023333333333333355
|
||||||
@@ -106,7 +100,7 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
def test_basic_consolidation_weight(self):
|
def test_basic_consolidation_weight(self):
|
||||||
cluster = self.fake_cluster.generate_scenario_1()
|
cluster = self.fake_cluster.generate_scenario_1()
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
vm_0 = cluster.get_vm_from_id("VM_0")
|
vm_0 = cluster.get_vm_from_id("VM_0")
|
||||||
cores = 16
|
cores = 16
|
||||||
@@ -138,17 +132,12 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
metrics = FakerMetricsCollector()
|
metrics = FakerMetricsCollector()
|
||||||
metrics.empty_one_metric("CPU_COMPUTE")
|
metrics.empty_one_metric("CPU_COMPUTE")
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
|
|
||||||
self.assertRaises(exception.ClusterStateNotDefined,
|
self.assertRaises(exception.ClusterStateNotDefined,
|
||||||
sercon.calculate_score_vm, "VM_1", None)
|
sercon.calculate_score_vm, "VM_1", None)
|
||||||
|
|
||||||
def test_print_utilization_raise_cluster_state_not_found(self):
|
|
||||||
sercon = BasicConsolidation()
|
|
||||||
self.assertRaises(exception.ClusterStateNotDefined,
|
|
||||||
sercon.print_utilization, None)
|
|
||||||
|
|
||||||
def test_check_migration(self):
|
def test_check_migration(self):
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
fake_cluster = FakerModelCollector()
|
fake_cluster = FakerModelCollector()
|
||||||
@@ -182,34 +171,28 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
|
|
||||||
def test_basic_consolidation_migration(self):
|
def test_basic_consolidation_migration(self):
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
|
|
||||||
solution = sercon.execute(
|
solution = sercon.execute(
|
||||||
self.fake_cluster.generate_scenario_2())
|
self.fake_cluster.generate_scenario_3_with_2_hypervisors())
|
||||||
|
|
||||||
actions_counter = Counter(
|
actions_counter = Counter(
|
||||||
[type(action) for action in solution.actions])
|
[action.get('action_type') for action in solution.actions])
|
||||||
|
|
||||||
expected_num_migrations = 0
|
expected_num_migrations = 1
|
||||||
expected_power_state = 0
|
expected_power_state = 0
|
||||||
expected_change_hypervisor_state = 0
|
|
||||||
|
|
||||||
num_migrations = actions_counter.get(Migrate, 0)
|
num_migrations = actions_counter.get("migrate", 0)
|
||||||
num_hypervisor_state_change = actions_counter.get(
|
num_hypervisor_state_change = actions_counter.get(
|
||||||
ChangeHypervisorState, 0)
|
"change_hypervisor_state", 0)
|
||||||
num_power_state_change = actions_counter.get(
|
|
||||||
ChangePowerState, 0)
|
|
||||||
|
|
||||||
self.assertEqual(num_migrations, expected_num_migrations)
|
self.assertEqual(num_migrations, expected_num_migrations)
|
||||||
self.assertEqual(num_hypervisor_state_change, expected_power_state)
|
self.assertEqual(num_hypervisor_state_change, expected_power_state)
|
||||||
self.assertEqual(num_power_state_change,
|
|
||||||
expected_change_hypervisor_state)
|
|
||||||
|
|
||||||
def test_execute_cluster_empty(self):
|
def test_execute_cluster_empty(self):
|
||||||
current_state_cluster = FakerModelCollector()
|
current_state_cluster = FakerModelCollector()
|
||||||
sercon = BasicConsolidation("sercon", "Basic offline consolidation")
|
sercon = BasicConsolidation("sercon", "Basic offline consolidation")
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
model = current_state_cluster.generate_random(0, 0)
|
model = current_state_cluster.generate_random(0, 0)
|
||||||
self.assertRaises(exception.ClusterEmpty, sercon.execute, model)
|
self.assertRaises(exception.ClusterEmpty, sercon.execute, model)
|
||||||
@@ -217,7 +200,7 @@ class TestBasicConsolidation(base.BaseTestCase):
|
|||||||
# calculate_weight
|
# calculate_weight
|
||||||
def test_execute_no_workload(self):
|
def test_execute_no_workload(self):
|
||||||
sercon = BasicConsolidation()
|
sercon = BasicConsolidation()
|
||||||
sercon.ceilometer = MagicMock(
|
sercon.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
|
|
||||||
current_state_cluster = FakerModelCollector()
|
current_state_cluster = FakerModelCollector()
|
||||||
|
|||||||
@@ -17,12 +17,9 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
import mock
|
||||||
from mock import MagicMock
|
|
||||||
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
|
|
||||||
from watcher.decision_engine.actions.migration import Migrate
|
|
||||||
from watcher.decision_engine.model.model_root import ModelRoot
|
from watcher.decision_engine.model.model_root import ModelRoot
|
||||||
from watcher.decision_engine.model.resource import ResourceType
|
from watcher.decision_engine.model.resource import ResourceType
|
||||||
from watcher.decision_engine.strategy.strategies.outlet_temp_control import \
|
from watcher.decision_engine.strategy.strategies.outlet_temp_control import \
|
||||||
@@ -59,7 +56,7 @@ class TestOutletTempControl(base.BaseTestCase):
|
|||||||
def test_group_hosts_by_outlet_temp(self):
|
def test_group_hosts_by_outlet_temp(self):
|
||||||
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
||||||
strategy = OutletTempControl()
|
strategy = OutletTempControl()
|
||||||
strategy.ceilometer = MagicMock(
|
strategy.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
h1, h2 = strategy.group_hosts_by_outlet_temp(model)
|
h1, h2 = strategy.group_hosts_by_outlet_temp(model)
|
||||||
self.assertEqual(h1[0]['hv'].uuid, 'Node_1')
|
self.assertEqual(h1[0]['hv'].uuid, 'Node_1')
|
||||||
@@ -68,7 +65,7 @@ class TestOutletTempControl(base.BaseTestCase):
|
|||||||
def test_choose_vm_to_migrate(self):
|
def test_choose_vm_to_migrate(self):
|
||||||
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
||||||
strategy = OutletTempControl()
|
strategy = OutletTempControl()
|
||||||
strategy.ceilometer = MagicMock(
|
strategy.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
h1, h2 = strategy.group_hosts_by_outlet_temp(model)
|
h1, h2 = strategy.group_hosts_by_outlet_temp(model)
|
||||||
vm_to_mig = strategy.choose_vm_to_migrate(model, h1)
|
vm_to_mig = strategy.choose_vm_to_migrate(model, h1)
|
||||||
@@ -78,7 +75,7 @@ class TestOutletTempControl(base.BaseTestCase):
|
|||||||
def test_filter_dest_servers(self):
|
def test_filter_dest_servers(self):
|
||||||
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
||||||
strategy = OutletTempControl()
|
strategy = OutletTempControl()
|
||||||
strategy.ceilometer = MagicMock(
|
strategy.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
h1, h2 = strategy.group_hosts_by_outlet_temp(model)
|
h1, h2 = strategy.group_hosts_by_outlet_temp(model)
|
||||||
vm_to_mig = strategy.choose_vm_to_migrate(model, h1)
|
vm_to_mig = strategy.choose_vm_to_migrate(model, h1)
|
||||||
@@ -99,14 +96,14 @@ class TestOutletTempControl(base.BaseTestCase):
|
|||||||
def test_execute_cluster_empty(self):
|
def test_execute_cluster_empty(self):
|
||||||
current_state_cluster = FakerModelCollector()
|
current_state_cluster = FakerModelCollector()
|
||||||
strategy = OutletTempControl()
|
strategy = OutletTempControl()
|
||||||
strategy.ceilometer = MagicMock(
|
strategy.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
model = current_state_cluster.generate_random(0, 0)
|
model = current_state_cluster.generate_random(0, 0)
|
||||||
self.assertRaises(exception.ClusterEmpty, strategy.execute, model)
|
self.assertRaises(exception.ClusterEmpty, strategy.execute, model)
|
||||||
|
|
||||||
def test_execute_no_workload(self):
|
def test_execute_no_workload(self):
|
||||||
strategy = OutletTempControl()
|
strategy = OutletTempControl()
|
||||||
strategy.ceilometer = MagicMock(
|
strategy.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
|
|
||||||
current_state_cluster = FakerModelCollector()
|
current_state_cluster = FakerModelCollector()
|
||||||
@@ -118,12 +115,12 @@ class TestOutletTempControl(base.BaseTestCase):
|
|||||||
|
|
||||||
def test_execute(self):
|
def test_execute(self):
|
||||||
strategy = OutletTempControl()
|
strategy = OutletTempControl()
|
||||||
strategy.ceilometer = MagicMock(
|
strategy.ceilometer = mock.MagicMock(
|
||||||
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
statistic_aggregation=self.fake_metrics.mock_get_statistics)
|
||||||
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
model = self.fake_cluster.generate_scenario_3_with_2_hypervisors()
|
||||||
solution = strategy.execute(model)
|
solution = strategy.execute(model)
|
||||||
actions_counter = Counter(
|
actions_counter = Counter(
|
||||||
[type(action) for action in solution.actions])
|
[action.get('action_type') for action in solution.actions])
|
||||||
|
|
||||||
num_migrations = actions_counter.get(Migrate, 0)
|
num_migrations = actions_counter.get("migrate", 0)
|
||||||
self.assertEqual(num_migrations, 1)
|
self.assertEqual(num_migrations, 1)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
# Copyright (c) 2015 b<>com
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from watcher.decision_engine.actions.base import BaseAction
|
|
||||||
from watcher.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestAction(base.TestCase):
|
|
||||||
def test_get_priority(self):
|
|
||||||
ma = BaseAction()
|
|
||||||
ma.priority = 3
|
|
||||||
self.assertEqual(ma.priority, 3)
|
|
||||||
|
|
||||||
def test_get_level(self):
|
|
||||||
ma = BaseAction()
|
|
||||||
ma.level = 5
|
|
||||||
self.assertEqual(ma.level, 5)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
# Copyright (c) 2015 b<>com
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
from watcher.decision_engine.solution.default import DefaultSolution
|
|
||||||
from watcher.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestDefaultSolution(base.BaseTestCase):
|
|
||||||
def test_default_solution(self):
|
|
||||||
solution = DefaultSolution()
|
|
||||||
solution.add_change_request("BLA")
|
|
||||||
self.assertEqual(solution.actions[0], "BLA")
|
|
||||||
Reference in New Issue
Block a user