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

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. 

16 

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>`. 

21 

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>`. 

26 

27Some strategies may provide better optimization results but may take more time 

28to find an optimal :ref:`Solution <solution_definition>`. 

29 

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. 

33 

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""" 

38 

39import abc 

40 

41from oslo_config import cfg 

42from oslo_log import log 

43from oslo_utils import strutils 

44 

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 

55 

56LOG = log.getLogger(__name__) 

57CONF = cfg.CONF 

58 

59 

60class StrategyEndpoint(object): 

61 def __init__(self, messaging): 

62 self._messaging = messaging 

63 

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': ''} 

83 

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': ''} 

93 

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': ''} 

105 

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] 

121 

122 

123class BaseStrategy(loadable.Loadable, metaclass=abc.ABCMeta): 

124 """A base class for all the strategies 

125 

126 A Strategy is an algorithm implementation which is able to find a 

127 Solution for a given Goal. 

128 """ 

129 

130 DATASOURCE_METRICS = [] 

131 """Contains all metrics the strategy requires from a datasource to properly 

132 execute""" 

133 

134 MIGRATION = "migrate" 

135 

136 def __init__(self, config, osc=None): 

137 """Constructor: the signature should be identical within the subclasses 

138 

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' 

163 

164 @classmethod 

165 @abc.abstractmethod 

166 def get_name(cls): 

167 """The name of the strategy""" 

168 raise NotImplementedError() 

169 

170 @classmethod 

171 @abc.abstractmethod 

172 def get_display_name(cls): 

173 """The goal display name for the strategy""" 

174 raise NotImplementedError() 

175 

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() 

183 

184 @classmethod 

185 @abc.abstractmethod 

186 def get_goal_name(cls): 

187 """The goal name the strategy achieves""" 

188 raise NotImplementedError() 

189 

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()) 

195 

196 @classmethod 

197 def get_config_opts(cls): 

198 """Defines the configuration options to be associated to this loadable 

199 

200 :return: A list of configuration options relative to this Loadable 

201 :rtype: list of :class:`oslo_config.cfg.Opt` instances 

202 """ 

203 

204 datasources_ops = list(ds_manager.DataSourceManager.metric_map.keys()) 

205 

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 ] 

215 

216 @abc.abstractmethod 

217 def pre_execute(self): 

218 """Pre-execution phase 

219 

220 This can be used to fetch some pre-requisites or data. 

221 """ 

222 raise NotImplementedError() 

223 

224 @abc.abstractmethod 

225 def do_execute(self, audit=None): 

226 """Strategy execution phase 

227 

228 :param audit: An Audit instance 

229 :type audit: :py:class:`~.Audit` instance 

230 

231 This phase is where you should put the main logic of your strategy. 

232 """ 

233 raise NotImplementedError() 

234 

235 @abc.abstractmethod 

236 def post_execute(self): 

237 """Post-execution phase 

238 

239 This can be used to compute the global efficacy 

240 """ 

241 raise NotImplementedError() 

242 

243 def _pre_execute(self): 

244 """Base Pre-execution phase 

245 

246 This will perform basic pre execution operations most strategies 

247 should perform. 

248 """ 

249 

250 LOG.info("Initializing " + self.get_display_name()) 

251 

252 if not self.compute_model: 

253 raise exception.ClusterStateNotDefined() 

254 

255 LOG.debug(self.compute_model.to_string()) 

256 

257 def execute(self, audit=None): 

258 """Execute a strategy 

259 

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() 

268 

269 self.solution.compute_global_efficacy() 

270 

271 return self.solution 

272 

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 

278 

279 @property 

280 def compute_model(self): 

281 """Cluster data model 

282 

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()) 

293 

294 if not self._compute_model: 

295 raise exception.ClusterStateNotDefined() 

296 

297 if self._compute_model.stale: 

298 raise exception.ClusterStateStale() 

299 

300 return self._compute_model 

301 

302 @property 

303 def storage_model(self): 

304 """Cluster data model 

305 

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()) 

316 

317 if not self._storage_model: 

318 raise exception.ClusterStateNotDefined() 

319 

320 if self._storage_model.stale: 

321 raise exception.ClusterStateStale() 

322 

323 return self._storage_model 

324 

325 @property 

326 def baremetal_model(self): 

327 """Cluster data model 

328 

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()) 

339 

340 if not self._baremetal_model: 

341 raise exception.ClusterStateNotDefined() 

342 

343 if self._baremetal_model.stale: 

344 raise exception.ClusterStateStale() 

345 

346 return self._baremetal_model 

347 

348 @classmethod 

349 def get_schema(cls): 

350 """Defines a Schema that the input parameters shall comply to 

351 

352 :return: A jsonschema format (mandatory default setting) 

353 :rtype: dict 

354 """ 

355 return {} 

356 

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

360 

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 

366 

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 

372 

373 @property 

374 def input_parameters(self): 

375 return self._input_parameters 

376 

377 @input_parameters.setter 

378 def input_parameters(self, p): 

379 self._input_parameters = p 

380 

381 @property 

382 def osc(self): 

383 if not self._osc: 

384 self._osc = clients.OpenStackClients() 

385 return self._osc 

386 

387 @property 

388 def solution(self): 

389 return self._solution 

390 

391 @solution.setter 

392 def solution(self, s): 

393 self._solution = s 

394 

395 @property 

396 def audit_scope(self): 

397 return self._audit_scope 

398 

399 @audit_scope.setter 

400 def audit_scope(self, s): 

401 self._audit_scope = s 

402 

403 @property 

404 def name(self): 

405 return self._name 

406 

407 @property 

408 def display_name(self): 

409 return self._display_name 

410 

411 @property 

412 def goal(self): 

413 return self._goal 

414 

415 @property 

416 def strategy_level(self): 

417 return self._strategy_level 

418 

419 @strategy_level.setter 

420 def strategy_level(self, s): 

421 self._strategy_level = s 

422 

423 @property 

424 def state_collector(self): 

425 return self._cluster_state_collector 

426 

427 @state_collector.setter 

428 def state_collector(self, s): 

429 self._cluster_state_collector = s 

430 

431 @property 

432 def planner(self): 

433 return self._planner 

434 

435 @planner.setter 

436 def planner(self, s): 

437 self._planner = s 

438 

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 

454 

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) 

467 

468 

469class DummyBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

470 

471 @classmethod 

472 def get_goal_name(cls): 

473 return "dummy" 

474 

475 @classmethod 

476 def get_config_opts(cls): 

477 """Override base class config options as do not use datasource """ 

478 

479 return [] 

480 

481 

482class UnclassifiedStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

483 """This base class is used to ease the development of new strategies 

484 

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 """ 

490 

491 @classmethod 

492 def get_goal_name(cls): 

493 return "unclassified" 

494 

495 

496class ServerConsolidationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

497 

498 REASON_FOR_DISABLE = 'watcher_disabled' 

499 

500 @classmethod 

501 def get_goal_name(cls): 

502 return "server_consolidation" 

503 

504 

505class ThermalOptimizationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

506 

507 @classmethod 

508 def get_goal_name(cls): 

509 return "thermal_optimization" 

510 

511 

512class WorkloadStabilizationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

513 

514 def __init__(self, *args, **kwargs): 

515 super(WorkloadStabilizationBaseStrategy, self 

516 ).__init__(*args, **kwargs) 

517 self._planner = 'workload_stabilization' 

518 

519 @classmethod 

520 def get_goal_name(cls): 

521 return "workload_balancing" 

522 

523 

524class NoisyNeighborBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

525 

526 @classmethod 

527 def get_goal_name(cls): 

528 return "noisy_neighbor" 

529 

530 

531class SavingEnergyBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

532 

533 @classmethod 

534 def get_goal_name(cls): 

535 return "saving_energy" 

536 

537 @classmethod 

538 def get_config_opts(cls): 

539 """Override base class config options as do not use datasource """ 

540 

541 return [] 

542 

543 

544class ZoneMigrationBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

545 

546 @classmethod 

547 def get_goal_name(cls): 

548 return "hardware_maintenance" 

549 

550 @classmethod 

551 def get_config_opts(cls): 

552 """Override base class config options as do not use datasource """ 

553 

554 return [] 

555 

556 

557class HostMaintenanceBaseStrategy(BaseStrategy, metaclass=abc.ABCMeta): 

558 

559 REASON_FOR_MAINTAINING = 'watcher_maintaining' 

560 

561 @classmethod 

562 def get_goal_name(cls): 

563 return "cluster_maintaining" 

564 

565 @classmethod 

566 def get_config_opts(cls): 

567 """Override base class config options as do not use datasource """ 

568 

569 return []