diff --git a/watcher/api/controllers/v1/collection.py b/watcher/api/controllers/v1/collection.py index 77467e2d4..c3abadde2 100644 --- a/watcher/api/controllers/v1/collection.py +++ b/watcher/api/controllers/v1/collection.py @@ -44,7 +44,7 @@ class Collection(base.APIBase): q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { 'args': q_args, 'limit': limit, - 'marker': self.collection[-1].uuid} + 'marker': getattr(self.collection[-1], "uuid")} return link.Link.make_link('next', pecan.request.host_url, resource_url, next_args).href diff --git a/watcher/api/controllers/v1/goal.py b/watcher/api/controllers/v1/goal.py index 27a0ab6d9..b50ae0513 100644 --- a/watcher/api/controllers/v1/goal.py +++ b/watcher/api/controllers/v1/goal.py @@ -46,61 +46,64 @@ from watcher.api.controllers.v1 import collection from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import utils as api_utils from watcher.common import exception +from watcher.common import utils as common_utils +from watcher import objects CONF = cfg.CONF class Goal(base.APIBase): - """API representation of a action. + """API representation of a goal. This class enforces type checking and value constraints, and converts - between the internal object model and the API representation of a action. + between the internal object model and the API representation of a goal. """ + uuid = types.uuid + """Unique UUID for this goal""" + name = wtypes.text """Name of the goal""" - strategy = wtypes.text - """The strategy associated with the goal""" - - uuid = types.uuid - """Unused field""" + display_name = wtypes.text + """Localized name of the goal""" 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 audit template links""" def __init__(self, **kwargs): super(Goal, self).__init__() self.fields = [] + self.fields.append('uuid') self.fields.append('name') - self.fields.append('strategy') - setattr(self, 'name', kwargs.get('name', - wtypes.Unset)) - setattr(self, 'strategy', kwargs.get('strategy', - wtypes.Unset)) + self.fields.append('display_name') + setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset)) + setattr(self, 'name', kwargs.get('name', wtypes.Unset)) + setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset)) @staticmethod def _convert_with_links(goal, url, expand=True): if not expand: - goal.unset_fields_except(['name', 'strategy']) + goal.unset_fields_except(['uuid', 'name', 'display_name']) goal.links = [link.Link.make_link('self', url, - 'goals', goal.name), + 'goals', goal.uuid), link.Link.make_link('bookmark', url, - 'goals', goal.name, + 'goals', goal.uuid, bookmark=True)] return goal @classmethod def convert_with_links(cls, goal, expand=True): - goal = Goal(**goal) + goal = Goal(**goal.as_dict()) return cls._convert_with_links(goal, pecan.request.host_url, expand) @classmethod def sample(cls, expand=True): - sample = cls(name='27e3153e-d5bf-4b7e-b517-fb518e17f34c', - strategy='action description') + sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', + name='DUMMY', + display_name='Dummy strategy') return cls._convert_with_links(sample, 'http://localhost:9322', expand) @@ -117,27 +120,28 @@ class GoalCollection(collection.Collection): @staticmethod def convert_with_links(goals, limit, url=None, expand=False, **kwargs): - - collection = GoalCollection() - collection.goals = [Goal.convert_with_links(g, expand) for g in goals] + goal_collection = GoalCollection() + goal_collection.goals = [ + Goal.convert_with_links(g, expand) for g in goals] if 'sort_key' in kwargs: reverse = False if kwargs['sort_key'] == 'strategy': if 'sort_dir' in kwargs: reverse = True if kwargs['sort_dir'] == 'desc' else False - collection.goals = sorted( - collection.goals, - key=lambda goal: goal.name, + goal_collection.goals = sorted( + goal_collection.goals, + key=lambda goal: goal.uuid, reverse=reverse) - collection.next = collection.get_next(limit, url=url, **kwargs) - return collection + goal_collection.next = goal_collection.get_next( + limit, url=url, **kwargs) + return goal_collection @classmethod def sample(cls): sample = cls() - sample.actions = [Goal.sample(expand=False)] + sample.goals = [Goal.sample(expand=False)] return sample @@ -154,51 +158,49 @@ class GoalsController(rest.RestController): 'detail': ['GET'], } - def _get_goals_collection(self, limit, - sort_key, sort_dir, expand=False, - resource_url=None, goal_name=None): - + def _get_goals_collection(self, marker, limit, sort_key, sort_dir, + expand=False, resource_url=None): limit = api_utils.validate_limit(limit) api_utils.validate_sort_dir(sort_dir) - goals = [] + sort_db_key = (sort_key if sort_key in objects.Goal.fields.keys() + else None) - if not goal_name and goal_name in CONF.watcher_goals.goals.keys(): - goals.append({'name': goal_name, 'strategy': goals[goal_name]}) - else: - for name, strategy in CONF.watcher_goals.goals.items(): - goals.append({'name': name, 'strategy': strategy}) + marker_obj = None + if marker: + marker_obj = objects.Goal.get_by_uuid( + pecan.request.context, marker) - return GoalCollection.convert_with_links(goals[:limit], limit, + goals = objects.Goal.list(pecan.request.context, limit, marker_obj, + sort_key=sort_db_key, sort_dir=sort_dir) + + return GoalCollection.convert_with_links(goals, limit, url=resource_url, expand=expand, sort_key=sort_key, sort_dir=sort_dir) - @wsme_pecan.wsexpose(GoalCollection, int, wtypes.text, wtypes.text) - def get_all(self, limit=None, - sort_key='name', sort_dir='asc'): + @wsme_pecan.wsexpose(GoalCollection, wtypes.text, + int, wtypes.text, wtypes.text) + def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'): """Retrieve a list of goals. + :param marker: pagination marker for large data sets. :param limit: maximum number of resources to return in a single result. :param sort_key: column to sort results by. Default: id. :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - to get only actions for that goal. """ - return self._get_goals_collection(limit, sort_key, sort_dir) + return self._get_goals_collection(marker, limit, sort_key, sort_dir) @wsme_pecan.wsexpose(GoalCollection, wtypes.text, int, wtypes.text, wtypes.text) - def detail(self, goal_name=None, limit=None, - sort_key='name', sort_dir='asc'): - """Retrieve a list of actions with detail. + def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'): + """Retrieve a list of goals with detail. - :param goal_name: name of a goal, to get only goals for that - action. + :param marker: pagination marker for large data sets. :param limit: maximum number of resources to return in a single result. :param sort_key: column to sort results by. Default: id. :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - to get only goals for that goal. """ # NOTE(lucasagomes): /detail should only work agaist collections parent = pecan.request.path.split('/')[:-1][-1] @@ -206,21 +208,23 @@ class GoalsController(rest.RestController): raise exception.HTTPNotFound expand = True resource_url = '/'.join(['goals', 'detail']) - return self._get_goals_collection(limit, sort_key, sort_dir, - expand, resource_url, goal_name) + return self._get_goals_collection(marker, limit, sort_key, sort_dir, + expand, resource_url) @wsme_pecan.wsexpose(Goal, wtypes.text) - def get_one(self, goal_name): + def get_one(self, goal): """Retrieve information about the given goal. - :param goal_name: name of the goal. + :param goal: UUID or name of the goal. """ if self.from_goals: raise exception.OperationNotPermitted - goals = CONF.watcher_goals.goals - goal = {} - if goal_name in goals.keys(): - goal = {'name': goal_name, 'strategy': goals[goal_name]} + if common_utils.is_uuid_like(goal): + get_goal_func = objects.Goal.get_by_uuid + else: + get_goal_func = objects.Goal.get_by_name - return Goal.convert_with_links(goal) + rpc_goal = get_goal_func(pecan.request.context, goal) + + return Goal.convert_with_links(rpc_goal) diff --git a/watcher/cmd/decisionengine.py b/watcher/cmd/decisionengine.py index df4979248..8bb747496 100644 --- a/watcher/cmd/decisionengine.py +++ b/watcher/cmd/decisionengine.py @@ -28,6 +28,7 @@ from oslo_service import service from watcher._i18n import _LI from watcher.common import service as watcher_service from watcher.decision_engine import manager +from watcher.decision_engine import sync from watcher import version LOG = logging.getLogger(__name__) @@ -41,6 +42,9 @@ def main(): LOG.info(_LI('Starting Watcher Decision Engine service in PID %s'), os.getpid()) + syncer = sync.Syncer() + syncer.sync() + de_service = watcher_service.Service(manager.DecisionEngineManager) launcher = service.launch(CONF, de_service) launcher.wait() diff --git a/watcher/decision_engine/strategy/strategies/base.py b/watcher/decision_engine/strategy/strategies/base.py index 20ca17cb5..8bcb99346 100644 --- a/watcher/decision_engine/strategy/strategies/base.py +++ b/watcher/decision_engine/strategy/strategies/base.py @@ -39,6 +39,7 @@ which are dynamically loaded by Watcher at launch time. import abc import six +from watcher._i18n import _ from watcher.common import clients from watcher.decision_engine.solution import default from watcher.decision_engine.strategy.common import level @@ -52,10 +53,10 @@ class BaseStrategy(object): Solution for a given Goal. """ - def __init__(self, name=None, description=None, osc=None): + def __init__(self, osc=None): """:param osc: an OpenStackClients instance""" - self._name = name - self.description = description + self._name = self.get_name() + self._display_name = self.get_display_name() # default strategy level self._strategy_level = level.StrategyLevel.conservative self._cluster_state_collector = None @@ -63,6 +64,46 @@ class BaseStrategy(object): self._solution = default.DefaultSolution() self._osc = osc + @classmethod + @abc.abstractmethod + def get_name(cls): + """The name of the strategy""" + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_display_name(cls): + """The goal display name for the strategy""" + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_translatable_display_name(cls): + """The translatable msgid of the strategy""" + # Note(v-francoise): Defined here to be used as the translation key for + # other services + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_goal_name(cls): + """The goal name for the strategy""" + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_goal_display_name(cls): + """The translated display name related to the goal of the strategy""" + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_translatable_goal_display_name(cls): + """The translatable msgid related to the goal of the strategy""" + # Note(v-francoise): Defined here to be used as the translation key for + # other services + raise NotImplementedError() + @abc.abstractmethod def execute(self, original_model): """Execute a strategy @@ -88,12 +129,12 @@ class BaseStrategy(object): self._solution = s @property - def name(self): + def id(self): return self._name - @name.setter - def name(self, n): - self._name = n + @property + def display_name(self): + return self._display_name @property def strategy_level(self): @@ -110,3 +151,51 @@ class BaseStrategy(object): @state_collector.setter def state_collector(self, s): self._cluster_state_collector = s + + +@six.add_metaclass(abc.ABCMeta) +class DummyBaseStrategy(BaseStrategy): + + @classmethod + def get_goal_name(cls): + return "DUMMY" + + @classmethod + def get_goal_display_name(cls): + return _("Dummy goal") + + @classmethod + def get_translatable_goal_display_name(cls): + return "Dummy goal" + + +@six.add_metaclass(abc.ABCMeta) +class ServerConsolidationBaseStrategy(BaseStrategy): + + @classmethod + def get_goal_name(cls): + return "SERVER_CONSOLIDATION" + + @classmethod + def get_goal_display_name(cls): + return _("Server consolidation") + + @classmethod + def get_translatable_goal_display_name(cls): + return "Server consolidation" + + +@six.add_metaclass(abc.ABCMeta) +class ThermalOptimizationBaseStrategy(BaseStrategy): + + @classmethod + def get_goal_name(cls): + return "THERMAL_OPTIMIZATION" + + @classmethod + def get_goal_display_name(cls): + return _("Thermal optimization") + + @classmethod + def get_translatable_goal_display_name(cls): + return "Thermal optimization" diff --git a/watcher/decision_engine/strategy/strategies/basic_consolidation.py b/watcher/decision_engine/strategy/strategies/basic_consolidation.py index 583014ec7..27e054885 100644 --- a/watcher/decision_engine/strategy/strategies/basic_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/basic_consolidation.py @@ -29,7 +29,7 @@ order to both minimize energy consumption and comply to the various SLAs. from oslo_log import log -from watcher._i18n import _LE, _LI, _LW +from watcher._i18n import _, _LE, _LI, _LW from watcher.common import exception from watcher.decision_engine.model import hypervisor_state as hyper_state from watcher.decision_engine.model import resource @@ -41,7 +41,7 @@ from watcher.metrics_engine.cluster_history import ceilometer as \ LOG = log.getLogger(__name__) -class BasicConsolidation(base.BaseStrategy): +class BasicConsolidation(base.ServerConsolidationBaseStrategy): """Basic offline consolidation using live migration *Description* @@ -65,17 +65,13 @@ class BasicConsolidation(base.BaseStrategy): """ - DEFAULT_NAME = "basic" - DEFAULT_DESCRIPTION = "Basic offline consolidation" - HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent' 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, - osc=None): + def __init__(self, osc=None): """Basic offline Consolidation using live migration :param name: The name of the strategy (Default: "basic") @@ -84,7 +80,7 @@ class BasicConsolidation(base.BaseStrategy): :param osc: An :py:class:`~watcher.common.clients.OpenStackClients` instance """ - super(BasicConsolidation, self).__init__(name, description, osc) + super(BasicConsolidation, self).__init__(osc) # set default value for the number of released nodes self.number_of_released_nodes = 0 @@ -114,6 +110,18 @@ class BasicConsolidation(base.BaseStrategy): # TODO(jed) bound migration attempts (80 %) self.bound_migration = 0.80 + @classmethod + def get_name(cls): + return "basic" + + @classmethod + def get_display_name(cls): + return _("Basic offline consolidation") + + @classmethod + def get_translatable_display_name(cls): + return "Basic offline consolidation" + @property def ceilometer(self): if self._ceilometer is None: diff --git a/watcher/decision_engine/strategy/strategies/dummy_strategy.py b/watcher/decision_engine/strategy/strategies/dummy_strategy.py index cde34ed5a..2d71b2d15 100644 --- a/watcher/decision_engine/strategy/strategies/dummy_strategy.py +++ b/watcher/decision_engine/strategy/strategies/dummy_strategy.py @@ -18,12 +18,13 @@ # from oslo_log import log +from watcher._i18n import _ from watcher.decision_engine.strategy.strategies import base LOG = log.getLogger(__name__) -class DummyStrategy(base.BaseStrategy): +class DummyStrategy(base.DummyBaseStrategy): """Dummy strategy used for integration testing via Tempest *Description* @@ -44,15 +45,11 @@ class DummyStrategy(base.BaseStrategy): """ - DEFAULT_NAME = "dummy" - DEFAULT_DESCRIPTION = "Dummy Strategy" - NOP = "nop" SLEEP = "sleep" - def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, - osc=None): - super(DummyStrategy, self).__init__(name, description, osc) + def __init__(self, osc=None): + super(DummyStrategy, self).__init__(osc) def execute(self, original_model): LOG.debug("Executing Dummy strategy") @@ -67,3 +64,15 @@ class DummyStrategy(base.BaseStrategy): self.solution.add_action(action_type=self.SLEEP, input_parameters={'duration': 5.0}) return self.solution + + @classmethod + def get_name(cls): + return "dummy" + + @classmethod + def get_display_name(cls): + return _("Dummy strategy") + + @classmethod + def get_translatable_display_name(cls): + return "Dummy strategy" diff --git a/watcher/decision_engine/strategy/strategies/outlet_temp_control.py b/watcher/decision_engine/strategy/strategies/outlet_temp_control.py index 0ce52d9bf..66c6ab209 100644 --- a/watcher/decision_engine/strategy/strategies/outlet_temp_control.py +++ b/watcher/decision_engine/strategy/strategies/outlet_temp_control.py @@ -30,7 +30,7 @@ telemetries to measure thermal/workload status of server. from oslo_log import log -from watcher._i18n import _LE +from watcher._i18n import _, _LE from watcher.common import exception as wexc from watcher.decision_engine.model import resource from watcher.decision_engine.model import vm_state @@ -41,7 +41,7 @@ from watcher.metrics_engine.cluster_history import ceilometer as ceil LOG = log.getLogger(__name__) -class OutletTempControl(base.BaseStrategy): +class OutletTempControl(base.ThermalOptimizationBaseStrategy): """[PoC] Outlet temperature control using live migration *Description* @@ -71,8 +71,6 @@ class OutletTempControl(base.BaseStrategy): https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/approved/outlet-temperature-based-strategy.rst """ # noqa - DEFAULT_NAME = "outlet_temp_control" - DEFAULT_DESCRIPTION = "outlet temperature based migration strategy" # The meter to report outlet temperature in ceilometer METER_NAME = "hardware.ipmi.node.outlet_temperature" # Unit: degree C @@ -80,15 +78,14 @@ class OutletTempControl(base.BaseStrategy): MIGRATION = "migrate" - def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, - osc=None): + def __init__(self, osc=None): """Outlet temperature control using live migration :param name: the name of the strategy :param description: a description of the strategy :param osc: an OpenStackClients object """ - super(OutletTempControl, self).__init__(name, description, osc) + super(OutletTempControl, self).__init__(osc) # the migration plan will be triggered when the outlet temperature # reaches threshold # TODO(zhenzanz): Threshold should be configurable for each audit @@ -96,6 +93,18 @@ class OutletTempControl(base.BaseStrategy): self._meter = self.METER_NAME self._ceilometer = None + @classmethod + def get_name(cls): + return "outlet_temperature" + + @classmethod + def get_display_name(cls): + return _("Outlet temperature based strategy") + + @classmethod + def get_translatable_display_name(cls): + return "Outlet temperature based strategy" + @property def ceilometer(self): if self._ceilometer is None: diff --git a/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py b/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py index b701ae532..4c89dc2cc 100644 --- a/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py @@ -22,7 +22,7 @@ from copy import deepcopy from oslo_log import log import six -from watcher._i18n import _LE, _LI +from watcher._i18n import _, _LE, _LI from watcher.common import exception from watcher.decision_engine.model import hypervisor_state as hyper_state from watcher.decision_engine.model import resource @@ -34,9 +34,11 @@ from watcher.metrics_engine.cluster_history import ceilometer \ LOG = log.getLogger(__name__) -class VMWorkloadConsolidation(base.BaseStrategy): +class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy): """VM Workload Consolidation Strategy. + *Description* + A load consolidation strategy based on heuristic first-fit algorithm which focuses on measured CPU utilization and tries to minimize hosts which have too much or too little load respecting @@ -67,19 +69,39 @@ class VMWorkloadConsolidation(base.BaseStrategy): correctly on all hypervisors within the cluster. This strategy assumes it is possible to live migrate any VM from an active hypervisor to any other active hypervisor. - """ - DEFAULT_NAME = 'vm_workload_consolidation' - DEFAULT_DESCRIPTION = 'VM Workload Consolidation Strategy' + *Requirements* - def __init__(self, name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, - osc=None): - super(VMWorkloadConsolidation, self).__init__(name, description, osc) + * You must have at least 2 physical compute nodes to run this strategy. + + *Limitations* + + + + *Spec URL* + + https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/implemented/zhaw-load-consolidation.rst + """ # noqa + + def __init__(self, osc=None): + super(VMWorkloadConsolidation, self).__init__(osc) self._ceilometer = None self.number_of_migrations = 0 self.number_of_released_hypervisors = 0 self.ceilometer_vm_data_cache = dict() + @classmethod + def get_name(cls): + return "vm_workload_consolidation" + + @classmethod + def get_display_name(cls): + return _("VM Workload Consolidation Strategy") + + @classmethod + def get_translatable_display_name(cls): + return "VM Workload Consolidation Strategy" + @property def ceilometer(self): if self._ceilometer is None: diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index f4a6163d9..a1086464a 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -153,21 +153,16 @@ class Syncer(object): strategy_loader = default.DefaultStrategyLoader() implemented_strategies = strategy_loader.list_available() - # TODO(v-francoise): At this point I only register the goals, but later - # on this will be extended to also populate the strategies map. for _, strategy_cls in implemented_strategies.items(): - # This mapping is a temporary trick where I use the strategy - # DEFAULT_NAME as the goal name because we used to have a 1-to-1 - # mapping between the goal and the strategy. - # TODO(v-francoise): Dissociate the goal name and the strategy name - goals_map[strategy_cls.DEFAULT_NAME] = { - "name": strategy_cls.DEFAULT_NAME, - "display_name": strategy_cls.DEFAULT_DESCRIPTION} + goals_map[strategy_cls.get_goal_name()] = { + "name": strategy_cls.get_goal_name(), + "display_name": + strategy_cls.get_translatable_goal_display_name()} - strategies_map[strategy_cls.__name__] = { - "name": strategy_cls.__name__, - "goal_name": strategy_cls.DEFAULT_NAME, - "display_name": strategy_cls.DEFAULT_DESCRIPTION} + strategies_map[strategy_cls.get_name()] = { + "name": strategy_cls.get_name(), + "goal_name": strategy_cls.get_goal_name(), + "display_name": strategy_cls.get_translatable_display_name()} return discovered_map diff --git a/watcher/tests/api/v1/test_goals.py b/watcher/tests/api/v1/test_goals.py index 2b4e32a5c..ba2d67fc5 100644 --- a/watcher/tests/api/v1/test_goals.py +++ b/watcher/tests/api/v1/test_goals.py @@ -11,60 +11,109 @@ # limitations under the License. from oslo_config import cfg -from watcher.tests.api import base as api_base +from six.moves.urllib import parse as urlparse -CONF = cfg.CONF +from watcher.common import utils +from watcher.tests.api import base as api_base +from watcher.tests.objects import utils as obj_utils class TestListGoal(api_base.FunctionalTest): - def setUp(self): - super(TestListGoal, self).setUp() - # Override the default to get enough goals to test limit on query - cfg.CONF.set_override( - "goals", { - "DUMMY_1": "dummy", "DUMMY_2": "dummy", - "DUMMY_3": "dummy", "DUMMY_4": "dummy", - }, - group='watcher_goals', enforce_type=True) - def _assert_goal_fields(self, goal): - goal_fields = ['name', 'strategy'] + goal_fields = ['uuid', 'name', 'display_name'] for field in goal_fields: self.assertIn(field, goal) def test_one(self): + goal = obj_utils.create_test_goal(self.context) response = self.get_json('/goals') + self.assertEqual(goal.uuid, response['goals'][0]["uuid"]) self._assert_goal_fields(response['goals'][0]) - def test_get_one(self): - goal_name = list(CONF.watcher_goals.goals.keys())[0] - response = self.get_json('/goals/%s' % goal_name) - self.assertEqual(goal_name, response['name']) + def test_get_one_by_uuid(self): + goal = obj_utils.create_test_goal(self.context) + response = self.get_json('/goals/%s' % goal.uuid) + self.assertEqual(goal.uuid, response["uuid"]) + self.assertEqual(goal.name, response["name"]) self._assert_goal_fields(response) + def test_get_one_by_name(self): + goal = obj_utils.create_test_goal(self.context) + response = self.get_json(urlparse.quote( + '/goals/%s' % goal['name'])) + self.assertEqual(goal.uuid, response['uuid']) + self._assert_goal_fields(response) + + def test_get_one_soft_deleted(self): + goal = obj_utils.create_test_goal(self.context) + goal.soft_delete() + response = self.get_json( + '/goals/%s' % goal['uuid'], + headers={'X-Show-Deleted': 'True'}) + self.assertEqual(goal.uuid, response['uuid']) + self._assert_goal_fields(response) + + response = self.get_json( + '/goals/%s' % goal['uuid'], + expect_errors=True) + self.assertEqual(404, response.status_int) + def test_detail(self): - goal_name = list(CONF.watcher_goals.goals.keys())[0] + goal = obj_utils.create_test_goal(self.context) response = self.get_json('/goals/detail') - self.assertEqual(goal_name, response['goals'][0]["name"]) + self.assertEqual(goal.uuid, response['goals'][0]["uuid"]) self._assert_goal_fields(response['goals'][0]) def test_detail_against_single(self): - goal_name = list(CONF.watcher_goals.goals.keys())[0] - response = self.get_json('/goals/%s/detail' % goal_name, + goal = obj_utils.create_test_goal(self.context) + response = self.get_json('/goals/%s/detail' % goal.uuid, expect_errors=True) self.assertEqual(404, response.status_int) def test_many(self): + goal_list = [] + for idx in range(1, 6): + goal = obj_utils.create_test_goal( + self.context, id=idx, + uuid=utils.generate_uuid(), + name='GOAL_{0}'.format(idx)) + goal_list.append(goal.uuid) response = self.get_json('/goals') - self.assertEqual(len(CONF.watcher_goals.goals), - len(response['goals'])) + self.assertTrue(len(response['goals']) > 2) + + def test_many_without_soft_deleted(self): + goal_list = [] + for id_ in [1, 2, 3]: + goal = obj_utils.create_test_goal( + self.context, id=id_, uuid=utils.generate_uuid(), + name='GOAL_{0}'.format(id_)) + goal_list.append(goal.uuid) + for id_ in [4, 5]: + goal = obj_utils.create_test_goal( + self.context, id=id_, uuid=utils.generate_uuid(), + name='GOAL_{0}'.format(id_)) + goal.soft_delete() + response = self.get_json('/goals') + self.assertEqual(3, len(response['goals'])) + uuids = [s['uuid'] for s in response['goals']] + self.assertEqual(sorted(goal_list), sorted(uuids)) def test_goals_collection_links(self): + for idx in range(1, 6): + obj_utils.create_test_goal( + self.context, id=idx, + uuid=utils.generate_uuid(), + name='GOAL_{0}'.format(idx)) response = self.get_json('/goals/?limit=2') self.assertEqual(2, len(response['goals'])) def test_goals_collection_links_default_limit(self): + for idx in range(1, 6): + obj_utils.create_test_goal( + self.context, id=idx, + uuid=utils.generate_uuid(), + name='GOAL_{0}'.format(idx)) cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True) response = self.get_json('/goals') self.assertEqual(3, len(response['goals'])) diff --git a/watcher/tests/cmd/test_decision_engine.py b/watcher/tests/cmd/test_decision_engine.py index 573be5596..0f962351c 100644 --- a/watcher/tests/cmd/test_decision_engine.py +++ b/watcher/tests/cmd/test_decision_engine.py @@ -24,6 +24,7 @@ from oslo_config import cfg from oslo_service import service from watcher.cmd import decisionengine +from watcher.decision_engine import sync from watcher.tests import base @@ -45,6 +46,7 @@ class TestDecisionEngine(base.BaseTestCase): super(TestDecisionEngine, self).tearDown() self.conf._parse_cli_opts = self._parse_cli_opts + @mock.patch.object(sync.Syncer, "sync", mock.Mock()) @mock.patch.object(service, "launch") def test_run_de_app(self, m_launch): decisionengine.main() diff --git a/watcher/tests/db/test_goal.py b/watcher/tests/db/test_goal.py index 3e8d5c03e..841ea64be 100644 --- a/watcher/tests/db/test_goal.py +++ b/watcher/tests/db/test_goal.py @@ -242,17 +242,15 @@ class DbGoalTestCase(base.DbTestCase): self.assertEqual(uuids.sort(), res_uuids.sort()) def test_get_goal_list_with_filters(self): - goal1_uuid = w_utils.generate_uuid() - goal2_uuid = w_utils.generate_uuid() goal1 = self._create_test_goal( id=1, - uuid=goal1_uuid, + uuid=w_utils.generate_uuid(), name="GOAL_1", display_name='Goal 1', ) goal2 = self._create_test_goal( id=2, - uuid=goal2_uuid, + uuid=w_utils.generate_uuid(), name="GOAL_2", display_name='Goal 2', ) diff --git a/watcher/tests/decision_engine/fake_strategies.py b/watcher/tests/decision_engine/fake_strategies.py new file mode 100644 index 000000000..c25658f75 --- /dev/null +++ b/watcher/tests/decision_engine/fake_strategies.py @@ -0,0 +1,80 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from watcher.decision_engine.strategy.strategies import base as base_strategy + + +class FakeStrategy(base_strategy.BaseStrategy): + + GOAL_NAME = NotImplemented + GOAL_DISPLAY_NAME = NotImplemented + NAME = NotImplemented + DISPLAY_NAME = NotImplemented + + @classmethod + def get_name(cls): + return cls.NAME + + @classmethod + def get_display_name(cls): + return cls.DISPLAY_NAME + + @classmethod + def get_translatable_display_name(cls): + return cls.DISPLAY_NAME + + @classmethod + def get_goal_name(cls): + return cls.GOAL_NAME + + @classmethod + def get_goal_display_name(cls): + return cls.GOAL_DISPLAY_NAME + + @classmethod + def get_translatable_goal_display_name(cls): + return cls.GOAL_DISPLAY_NAME + + def execute(self, original_model): + pass + + +class FakeDummy1Strategy1(FakeStrategy): + GOAL_NAME = "DUMMY_1" + GOAL_DISPLAY_NAME = "Dummy 1" + NAME = "STRATEGY_1" + DISPLAY_NAME = "Strategy 1" + + +class FakeDummy1Strategy2(FakeStrategy): + GOAL_NAME = "DUMMY_1" + GOAL_DISPLAY_NAME = "Dummy 1" + NAME = "STRATEGY_2" + DISPLAY_NAME = "Strategy 2" + + +class FakeDummy2Strategy3(FakeStrategy): + GOAL_NAME = "DUMMY_2" + GOAL_DISPLAY_NAME = "Dummy 2" + NAME = "STRATEGY_3" + DISPLAY_NAME = "Strategy 3" + + +class FakeDummy2Strategy4(FakeStrategy): + GOAL_NAME = "DUMMY_2" + GOAL_DISPLAY_NAME = "Other Dummy 2" + NAME = "STRATEGY_4" + DISPLAY_NAME = "Strategy 4" diff --git a/watcher/tests/decision_engine/planner/test_default_planner.py b/watcher/tests/decision_engine/planner/test_default_planner.py index 1ace1a30c..d561cb159 100644 --- a/watcher/tests/decision_engine/planner/test_default_planner.py +++ b/watcher/tests/decision_engine/planner/test_default_planner.py @@ -36,8 +36,7 @@ class SolutionFaker(object): def build(): metrics = fake.FakerMetricsCollector() current_state_cluster = faker_cluster_state.FakerModelCollector() - sercon = strategies.BasicConsolidation("basic", - "Basic offline consolidation") + sercon = strategies.BasicConsolidation() sercon.ceilometer = mock.\ MagicMock(get_statistics=metrics.mock_get_statistics) return sercon.execute(current_state_cluster.generate_scenario_1()) @@ -48,8 +47,7 @@ class SolutionFakerSingleHyp(object): def build(): metrics = fake.FakerMetricsCollector() current_state_cluster = faker_cluster_state.FakerModelCollector() - sercon = strategies.BasicConsolidation("basic", - "Basic offline consolidation") + sercon = strategies.BasicConsolidation() sercon.ceilometer = \ mock.MagicMock(get_statistics=metrics.mock_get_statistics) diff --git a/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py b/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py index b510e0e08..54f29592b 100644 --- a/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py +++ b/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py @@ -32,11 +32,11 @@ class TestDefaultStrategyLoader(base.TestCase): exception.LoadingError, self.strategy_loader.load, None) def test_load_strategy_is_basic(self): - exptected_strategy = 'basic' - selected_strategy = self.strategy_loader.load(exptected_strategy) + expected_strategy = 'basic' + selected_strategy = self.strategy_loader.load(expected_strategy) self.assertEqual( - selected_strategy.name, - exptected_strategy, + selected_strategy.id, + expected_strategy, 'The default strategy should be basic') @patch("watcher.common.loader.default.ExtensionManager") @@ -58,8 +58,8 @@ class TestDefaultStrategyLoader(base.TestCase): strategy_loader = default_loading.DefaultStrategyLoader() loaded_strategy = strategy_loader.load("dummy") - self.assertEqual("dummy", loaded_strategy.name) - self.assertEqual("Dummy Strategy", loaded_strategy.description) + self.assertEqual("dummy", loaded_strategy.id) + self.assertEqual("Dummy strategy", loaded_strategy.display_name) def test_load_dummy_strategy(self): strategy_loader = default_loading.DefaultStrategyLoader() diff --git a/watcher/tests/decision_engine/strategy/strategies/test_dummy_strategy.py b/watcher/tests/decision_engine/strategy/strategies/test_dummy_strategy.py index d1be95412..fae80e1ac 100644 --- a/watcher/tests/decision_engine/strategy/strategies/test_dummy_strategy.py +++ b/watcher/tests/decision_engine/strategy/strategies/test_dummy_strategy.py @@ -23,14 +23,14 @@ from watcher.tests.decision_engine.strategy.strategies import \ class TestDummyStrategy(base.TestCase): def test_dummy_strategy(self): - dummy = strategies.DummyStrategy("dummy", "Dummy strategy") + dummy = strategies.DummyStrategy() fake_cluster = faker_cluster_state.FakerModelCollector() model = fake_cluster.generate_scenario_3_with_2_hypervisors() solution = dummy.execute(model) self.assertEqual(3, len(solution.actions)) def test_check_parameters(self): - dummy = strategies.DummyStrategy("dummy", "Dummy strategy") + dummy = strategies.DummyStrategy() fake_cluster = faker_cluster_state.FakerModelCollector() model = fake_cluster.generate_scenario_3_with_2_hypervisors() solution = dummy.execute(model) diff --git a/watcher/tests/decision_engine/test_sync.py b/watcher/tests/decision_engine/test_sync.py index 167141d9e..efa2a286e 100644 --- a/watcher/tests/decision_engine/test_sync.py +++ b/watcher/tests/decision_engine/test_sync.py @@ -19,38 +19,10 @@ import mock from watcher.common import context from watcher.common import utils from watcher.decision_engine.strategy.loading import default -from watcher.decision_engine.strategy.strategies import base as base_strategy from watcher.decision_engine import sync from watcher import objects from watcher.tests.db import base - - -class FakeStrategy(base_strategy.BaseStrategy): - DEFAULT_NAME = "" - DEFAULT_DESCRIPTION = "" - - def execute(self, original_model): - pass - - -class FakeDummy1Strategy1(FakeStrategy): - DEFAULT_NAME = "DUMMY_1" - DEFAULT_DESCRIPTION = "Dummy 1" - - -class FakeDummy1Strategy2(FakeStrategy): - DEFAULT_NAME = "DUMMY_1" - DEFAULT_DESCRIPTION = "Dummy 1" - - -class FakeDummy2Strategy3(FakeStrategy): - DEFAULT_NAME = "DUMMY_2" - DEFAULT_DESCRIPTION = "Dummy 2" - - -class FakeDummy2Strategy4(FakeStrategy): - DEFAULT_NAME = "DUMMY_2" - DEFAULT_DESCRIPTION = "Other Dummy 2" +from watcher.tests.decision_engine import fake_strategies class TestSyncer(base.DbTestCase): @@ -60,10 +32,14 @@ class TestSyncer(base.DbTestCase): self.ctx = context.make_context() self.m_available_strategies = mock.Mock(return_value={ - FakeDummy1Strategy1.__name__: FakeDummy1Strategy1, - FakeDummy1Strategy2.__name__: FakeDummy1Strategy2, - FakeDummy2Strategy3.__name__: FakeDummy2Strategy3, - FakeDummy2Strategy4.__name__: FakeDummy2Strategy4, + fake_strategies.FakeDummy1Strategy1.get_name(): + fake_strategies.FakeDummy1Strategy1, + fake_strategies.FakeDummy1Strategy2.get_name(): + fake_strategies.FakeDummy1Strategy2, + fake_strategies.FakeDummy2Strategy3.get_name(): + fake_strategies.FakeDummy2Strategy3, + fake_strategies.FakeDummy2Strategy4.get_name(): + fake_strategies.FakeDummy2Strategy4, }) p_strategies = mock.patch.object( @@ -150,8 +126,8 @@ class TestSyncer(base.DbTestCase): name="DUMMY_1", display_name="Dummy 1") ] m_s_list.return_value = [ - objects.Strategy(self.ctx, id=1, name="FakeDummy1Strategy1", - goal_id=1, display_name="Dummy 1") + objects.Strategy(self.ctx, id=1, name="STRATEGY_1", + goal_id=1, display_name="Strategy 1") ] self.syncer.sync() @@ -211,7 +187,7 @@ class TestSyncer(base.DbTestCase): name="DUMMY_1", display_name="Dummy 1") ] m_s_list.return_value = [ - objects.Strategy(self.ctx, id=1, name="FakeDummy1Strategy1", + objects.Strategy(self.ctx, id=1, name="STRATEGY_1", goal_id=1, display_name="original") ] self.syncer.sync() @@ -229,7 +205,7 @@ class TestSyncer(base.DbTestCase): name="DUMMY_1", display_name="Original") goal.create() strategy = objects.Strategy( - self.ctx, id=1, name="FakeDummy1Strategy1", + self.ctx, id=1, name="STRATEGY_1", display_name="Original", goal_id=goal.id) strategy.create() # audit_template = objects.AuditTemplate( @@ -260,8 +236,7 @@ class TestSyncer(base.DbTestCase): {"DUMMY_1", "DUMMY_2"}, set([g.name for g in after_goals])) self.assertEqual( - {'FakeDummy1Strategy1', 'FakeDummy1Strategy2', - 'FakeDummy2Strategy3', 'FakeDummy2Strategy4'}, + {"STRATEGY_1", "STRATEGY_2", "STRATEGY_3", "STRATEGY_4"}, set([s.name for s in after_strategies])) created_goals = [ag for ag in after_goals if ag.uuid not in [bg.uuid for bg in before_goals]] diff --git a/watcher/tests/objects/utils.py b/watcher/tests/objects/utils.py index ee3eb95d1..b37c91f98 100644 --- a/watcher/tests/objects/utils.py +++ b/watcher/tests/objects/utils.py @@ -135,3 +135,30 @@ def create_test_action(context, **kw): action = get_test_action(context, **kw) action.create() return action + + +def get_test_goal(context, **kw): + """Return a Goal object with appropriate attributes. + + NOTE: The object leaves the attributes marked as changed, such + that a create() could be used to commit it to the DB. + """ + db_goal = db_utils.get_test_goal(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del db_goal['id'] + goal = objects.Goal(context) + for key in db_goal: + setattr(goal, key, db_goal[key]) + return goal + + +def create_test_goal(context, **kw): + """Create and return a test goal object. + + Create a goal in the DB and return a Goal object with appropriate + attributes. + """ + goal = get_test_goal(context, **kw) + goal.create() + return goal diff --git a/watcher_tempest_plugin/services/infra_optim/v1/json/client.py b/watcher_tempest_plugin/services/infra_optim/v1/json/client.py index 47bd62710..52cd659e2 100644 --- a/watcher_tempest_plugin/services/infra_optim/v1/json/client.py +++ b/watcher_tempest_plugin/services/infra_optim/v1/json/client.py @@ -234,7 +234,7 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient): def show_goal(self, goal): """Gets a specific goal - :param goal: Name of the goal + :param goal: UUID or Name of the goal :return: Serialized goal as a dictionary """ return self._show_request('/goals', goal) diff --git a/watcher_tempest_plugin/tests/api/admin/test_goal.py b/watcher_tempest_plugin/tests/api/admin/test_goal.py index c3e734d23..bcf4839df 100644 --- a/watcher_tempest_plugin/tests/api/admin/test_goal.py +++ b/watcher_tempest_plugin/tests/api/admin/test_goal.py @@ -40,14 +40,14 @@ class TestShowListGoal(base.BaseInfraOptimTest): _, goal = self.client.show_goal(self.DUMMY_GOAL) self.assertEqual(self.DUMMY_GOAL, goal['name']) - self.assertEqual("dummy", goal['strategy']) + self.assertIn("display_name", goal.keys()) @test.attr(type='smoke') def test_show_goal_with_links(self): _, goal = self.client.show_goal(self.DUMMY_GOAL) self.assertIn('links', goal.keys()) self.assertEqual(2, len(goal['links'])) - self.assertIn(goal['name'], + self.assertIn(goal['uuid'], goal['links'][0]['href']) @test.attr(type="smoke") @@ -58,5 +58,5 @@ class TestShowListGoal(base.BaseInfraOptimTest): # Verify self links. for goal in body['goals']: - self.validate_self_link('goals', goal['name'], + self.validate_self_link('goals', goal['uuid'], goal['links'][0]['href'])