Coverage for watcher/decision_engine/strategy/strategies/base.py: 77%
269 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
1# -*- encoding: utf-8 -*-
2# Copyright (c) 2015 b<>com
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
17"""
18A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is
19able to find a :ref:`Solution <solution_definition>` for a given
20:ref:`Goal <goal_definition>`.
22There may be several potential strategies which are able to achieve the same
23:ref:`Goal <goal_definition>`. This is why it is possible to configure which
24specific :ref:`Strategy <strategy_definition>` should be used for each
25:ref:`Goal <goal_definition>`.
27Some strategies may provide better optimization results but may take more time
28to find an optimal :ref:`Solution <solution_definition>`.
30When a new :ref:`Goal <goal_definition>` is added to the Watcher configuration,
31at least one default associated :ref:`Strategy <strategy_definition>` should be
32provided as well.
34:ref:`Some default implementations are provided <watcher_strategies>`, but it
35is possible to :ref:`develop new implementations <implement_strategy_plugin>`
36which are dynamically loaded by Watcher at launch time.
37"""
39import abc
41from oslo_config import cfg
42from oslo_log import log
43from oslo_utils import strutils
45from watcher.common import clients
46from watcher.common import context
47from watcher.common import exception
48from watcher.common.loader import loadable
49from watcher.common import utils
50from watcher.decision_engine.datasources import manager as ds_manager
51from watcher.decision_engine.loading import default as loading
52from watcher.decision_engine.model.collector import manager
53from watcher.decision_engine.solution import default
54from watcher.decision_engine.strategy.common import level
56LOG = log.getLogger(__name__)
57CONF = cfg.CONF
60class StrategyEndpoint(object):
61 def __init__(self, messaging):
62 self._messaging = messaging
64 def _collect_metrics(self, strategy, datasource):
65 metrics = []
66 if not datasource: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true
67 return {'type': 'Metrics', 'state': metrics,
68 'mandatory': False, 'comment': ''}
69 else:
70 ds_metrics = datasource.list_metrics()
71 if ds_metrics is None: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 raise exception.DataSourceNotAvailable(
73 datasource=datasource.NAME)
74 else:
75 for metric in strategy.DATASOURCE_METRICS:
76 original_metric_name = datasource.METRIC_MAP.get(metric)
77 if original_metric_name in ds_metrics:
78 metrics.append({original_metric_name: 'available'})
79 else:
80 metrics.append({original_metric_name: 'not available'})
81 return {'type': 'Metrics', 'state': metrics,
82 'mandatory': False, 'comment': ''}
84 def _get_datasource_status(self, strategy, datasource):
85 if not datasource: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 state = "Datasource is not presented for this strategy"
87 else:
88 state = "%s: %s" % (datasource.NAME,
89 datasource.check_availability())
90 return {'type': 'Datasource',
91 'state': state,
92 'mandatory': True, 'comment': ''}
94 def _get_cdm(self, strategy):
95 models = []
96 for model in ['compute_model', 'storage_model', 'baremetal_model']:
97 try:
98 getattr(strategy, model)
99 except Exception:
100 models.append({model: 'not available'})
101 else:
102 models.append({model: 'available'})
103 return {'type': 'CDM', 'state': models,
104 'mandatory': True, 'comment': ''}
106 def get_strategy_info(self, context, strategy_name):
107 strategy = loading.DefaultStrategyLoader().load(strategy_name)
108 try:
109 is_datasources = getattr(strategy.config, 'datasources', None)
110 if is_datasources:
111 datasource = getattr(strategy, 'datasource_backend')
112 else:
113 datasource = getattr(strategy, strategy.config.datasource)
114 except (AttributeError, IndexError):
115 datasource = []
116 available_datasource = self._get_datasource_status(strategy,
117 datasource)
118 available_metrics = self._collect_metrics(strategy, datasource)
119 available_cdm = self._get_cdm(strategy)
120 return [available_datasource, available_metrics, available_cdm]
123class BaseStrategy(loadable.Loadable, metaclass=abc.ABCMeta):
124 """A base class for all the strategies
126 A Strategy is an algorithm implementation which is able to find a
127 Solution for a given Goal.
128 """
130 DATASOURCE_METRICS = []
131 """Contains all metrics the strategy requires from a datasource to properly
132 execute"""
134 MIGRATION = "migrate"
136 def __init__(self, config, osc=None):
137 """Constructor: the signature should be identical within the subclasses
139 :param config: Configuration related to this plugin
140 :type config: :py:class:`~.Struct`
141 :param osc: An OpenStackClients instance
142 :type osc: :py:class:`~.OpenStackClients` instance
143 """
144 super(BaseStrategy, self).__init__(config)
145 self.ctx = context.make_context()
146 self._name = self.get_name()
147 self._display_name = self.get_display_name()
148 self._goal = self.get_goal()
149 # default strategy level
150 self._strategy_level = level.StrategyLevel.conservative
151 self._cluster_state_collector = None
152 # the solution given by the strategy
153 self._solution = default.DefaultSolution(goal=self.goal, strategy=self)
154 self._osc = osc
155 self._collector_manager = None
156 self._compute_model = None
157 self._storage_model = None
158 self._baremetal_model = None
159 self._input_parameters = utils.Struct()
160 self._audit_scope = None
161 self._datasource_backend = None
162 self._planner = 'weight'
164 @classmethod
165 @abc.abstractmethod
166 def get_name(cls):
167 """The name of the strategy"""
168 raise NotImplementedError()
170 @classmethod
171 @abc.abstractmethod
172 def get_display_name(cls):
173 """The goal display name for the strategy"""
174 raise NotImplementedError()
176 @classmethod
177 @abc.abstractmethod
178 def get_translatable_display_name(cls):
179 """The translatable msgid of the strategy"""
180 # Note(v-francoise): Defined here to be used as the translation key for
181 # other services
182 raise NotImplementedError()
184 @classmethod
185 @abc.abstractmethod
186 def get_goal_name(cls):
187 """The goal name the strategy achieves"""
188 raise NotImplementedError()
190 @classmethod
191 def get_goal(cls):
192 """The goal the strategy achieves"""
193 goal_loader = loading.DefaultGoalLoader()
194 return goal_loader.load(cls.get_goal_name())
196 @classmethod
197 def get_config_opts(cls):
198 """Defines the configuration options to be associated to this loadable
200 :return: A list of configuration options relative to this Loadable
201 :rtype: list of :class:`oslo_config.cfg.Opt` instances
202 """
204 datasources_ops = list(ds_manager.DataSourceManager.metric_map.keys())
206 return [
207 cfg.ListOpt(
208 "datasources",
209 help="Datasources to use in order to query the needed metrics."
210 " This option overrides the global preference."
211 " options: {0}".format(datasources_ops),
212 item_type=cfg.types.String(choices=datasources_ops),
213 default=None)
214 ]
216 @abc.abstractmethod
217 def pre_execute(self):
218 """Pre-execution phase
220 This can be used to fetch some pre-requisites or data.
221 """
222 raise NotImplementedError()
224 @abc.abstractmethod
225 def do_execute(self, audit=None):
226 """Strategy execution phase
228 :param audit: An Audit instance
229 :type audit: :py:class:`~.Audit` instance
231 This phase is where you should put the main logic of your strategy.
232 """
233 raise NotImplementedError()
235 @abc.abstractmethod
236 def post_execute(self):
237 """Post-execution phase
239 This can be used to compute the global efficacy
240 """
241 raise NotImplementedError()
243 def _pre_execute(self):
244 """Base Pre-execution phase
246 This will perform basic pre execution operations most strategies
247 should perform.
248 """
250 LOG.info("Initializing " + self.get_display_name())
252 if not self.compute_model:
253 raise exception.ClusterStateNotDefined()
255 LOG.debug(self.compute_model.to_string())
257 def execute(self, audit=None):
258 """Execute a strategy
260 :param audit: An Audit instance
261 :type audit: :py:class:`~.Audit` instance
262 :return: A computed solution (via a placement algorithm)
263 :rtype: :py:class:`~.BaseSolution` instance
264 """
265 self.pre_execute()
266 self.do_execute(audit=audit)
267 self.post_execute()
269 self.solution.compute_global_efficacy()
271 return self.solution
273 @property
274 def collector_manager(self):
275 if self._collector_manager is None: 275 ↛ 277line 275 didn't jump to line 277 because the condition on line 275 was always true
276 self._collector_manager = manager.CollectorManager()
277 return self._collector_manager
279 @property
280 def compute_model(self):
281 """Cluster data model
283 :returns: Cluster data model the strategy is executed on
284 :rtype model: :py:class:`~.ModelRoot` instance
285 """
286 if self._compute_model is None: 286 ↛ 294line 286 didn't jump to line 294 because the condition on line 286 was always true
287 collector = self.collector_manager.get_cluster_model_collector(
288 'compute', osc=self.osc)
289 audit_scope_handler = collector.get_audit_scope_handler(
290 audit_scope=self.audit_scope)
291 self._compute_model = audit_scope_handler.get_scoped_model(
292 collector.get_latest_cluster_data_model())
294 if not self._compute_model:
295 raise exception.ClusterStateNotDefined()
297 if self._compute_model.stale:
298 raise exception.ClusterStateStale()
300 return self._compute_model
302 @property
303 def storage_model(self):
304 """Cluster data model
306 :returns: Cluster data model the strategy is executed on
307 :rtype model: :py:class:`~.ModelRoot` instance
308 """
309 if self._storage_model is None:
310 collector = self.collector_manager.get_cluster_model_collector(
311 'storage', osc=self.osc)
312 audit_scope_handler = collector.get_audit_scope_handler(
313 audit_scope=self.audit_scope)
314 self._storage_model = audit_scope_handler.get_scoped_model(
315 collector.get_latest_cluster_data_model())
317 if not self._storage_model:
318 raise exception.ClusterStateNotDefined()
320 if self._storage_model.stale:
321 raise exception.ClusterStateStale()
323 return self._storage_model
325 @property
326 def baremetal_model(self):
327 """Cluster data model
329 :returns: Cluster data model the strategy is executed on
330 :rtype model: :py:class:`~.ModelRoot` instance
331 """
332 if self._baremetal_model is None:
333 collector = self.collector_manager.get_cluster_model_collector(
334 'baremetal', osc=self.osc)
335 audit_scope_handler = collector.get_audit_scope_handler(
336 audit_scope=self.audit_scope)
337 self._baremetal_model = audit_scope_handler.get_scoped_model(
338 collector.get_latest_cluster_data_model())
340 if not self._baremetal_model:
341 raise exception.ClusterStateNotDefined()
343 if self._baremetal_model.stale:
344 raise exception.ClusterStateStale()
346 return self._baremetal_model
348 @classmethod
349 def get_schema(cls):
350 """Defines a Schema that the input parameters shall comply to
352 :return: A jsonschema format (mandatory default setting)
353 :rtype: dict
354 """
355 return {}
357 @property
358 def datasource_backend(self):
359 if not self._datasource_backend: 359 ↛ 371line 359 didn't jump to line 371 because the condition on line 359 was always true
361 # Load the global preferred datasources order but override it
362 # if the strategy has a specific datasources config
363 datasources = CONF.watcher_datasources
364 if self.config.datasources:
365 datasources = self.config
367 self._datasource_backend = ds_manager.DataSourceManager(
368 config=datasources,
369 osc=self.osc
370 ).get_backend(self.DATASOURCE_METRICS)
371 return self._datasource_backend
373 @property
374 def input_parameters(self):
375 return self._input_parameters
377 @input_parameters.setter
378 def input_parameters(self, p):
379 self._input_parameters = p
381 @property
382 def osc(self):
383 if not self._osc:
384 self._osc = clients.OpenStackClients()
385 return self._osc
387 @property
388 def solution(self):
389 return self._solution
391 @solution.setter
392 def solution(self, s):
393 self._solution = s
395 @property
396 def audit_scope(self):
397 return self._audit_scope
399 @audit_scope.setter
400 def audit_scope(self, s):
401 self._audit_scope = s
403 @property
404 def name(self):
405 return self._name
407 @property
408 def display_name(self):
409 return self._display_name
411 @property
412 def goal(self):
413 return self._goal
415 @property
416 def strategy_level(self):
417 return self._strategy_level
419 @strategy_level.setter
420 def strategy_level(self, s):
421 self._strategy_level = s
423 @property
424 def state_collector(self):
425 return self._cluster_state_collector
427 @state_collector.setter
428 def state_collector(self, s):
429 self._cluster_state_collector = s
431 @property
432 def planner(self):
433 return self._planner
435 @planner.setter
436 def planner(self, s):
437 self._planner = s
439 def filter_instances_by_audit_tag(self, instances):
440 if not self.config.check_optimize_metadata: 440 ↛ 441line 440 didn't jump to line 441 because the condition on line 440 was never true
441 return instances
442 instances_to_migrate = []
443 for instance in instances:
444 optimize = True
445 if instance.metadata: 445 ↛ 451line 445 didn't jump to line 451 because the condition on line 445 was always true
446 try:
447 optimize = strutils.bool_from_string(
448 instance.metadata.get('optimize'))
449 except ValueError:
450 optimize = False
451 if optimize: 451 ↛ 443line 451 didn't jump to line 443 because the condition on line 451 was always true
452 instances_to_migrate.append(instance)
453 return instances_to_migrate
455 def add_action_migrate(self,
456 instance,
457 migration_type,
458 source_node,
459 destination_node):
460 parameters = {'migration_type': migration_type,
461 'source_node': source_node.hostname,
462 'destination_node': destination_node.hostname,
463 'resource_name': instance.name}
464 self.solution.add_action(action_type=self.MIGRATION,
465 resource_id=instance.uuid,
466 input_parameters=parameters)
469class DummyBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
471 @classmethod
472 def get_goal_name(cls):
473 return "dummy"
475 @classmethod
476 def get_config_opts(cls):
477 """Override base class config options as do not use datasource """
479 return []
482class UnclassifiedStrategy(BaseStrategy, metaclass=abc.ABCMeta):
483 """This base class is used to ease the development of new strategies
485 The goal defined within this strategy can be used to simplify the
486 documentation explaining how to implement a new strategy plugin by
487 omitting the need for the strategy developer to define a goal straight
488 away.
489 """
491 @classmethod
492 def get_goal_name(cls):
493 return "unclassified"
496class ServerConsolidationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
498 REASON_FOR_DISABLE = 'watcher_disabled'
500 @classmethod
501 def get_goal_name(cls):
502 return "server_consolidation"
505class ThermalOptimizationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
507 @classmethod
508 def get_goal_name(cls):
509 return "thermal_optimization"
512class WorkloadStabilizationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
514 def __init__(self, *args, **kwargs):
515 super(WorkloadStabilizationBaseStrategy, self
516 ).__init__(*args, **kwargs)
517 self._planner = 'workload_stabilization'
519 @classmethod
520 def get_goal_name(cls):
521 return "workload_balancing"
524class NoisyNeighborBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
526 @classmethod
527 def get_goal_name(cls):
528 return "noisy_neighbor"
531class SavingEnergyBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
533 @classmethod
534 def get_goal_name(cls):
535 return "saving_energy"
537 @classmethod
538 def get_config_opts(cls):
539 """Override base class config options as do not use datasource """
541 return []
544class ZoneMigrationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
546 @classmethod
547 def get_goal_name(cls):
548 return "hardware_maintenance"
550 @classmethod
551 def get_config_opts(cls):
552 """Override base class config options as do not use datasource """
554 return []
557class HostMaintenanceBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta):
559 REASON_FOR_MAINTAINING = 'watcher_maintaining'
561 @classmethod
562 def get_goal_name(cls):
563 return "cluster_maintaining"
565 @classmethod
566 def get_config_opts(cls):
567 """Override base class config options as do not use datasource """
569 return []