Coverage for watcher/api/controllers/v1/action_plan.py: 86%

296 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-17 12:22 +0000

1# -*- encoding: utf-8 -*- 

2# Copyright 2013 Red Hat, Inc. 

3# All Rights Reserved. 

4# 

5# Licensed under the Apache License, Version 2.0 (the "License"); 

6# you may not use this file except in compliance with the License. 

7# You may obtain a copy of the License at 

8# 

9# http://www.apache.org/licenses/LICENSE-2.0 

10# 

11# Unless required by applicable law or agreed to in writing, software 

12# distributed under the License is distributed on an "AS IS" BASIS, 

13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

14# implied. 

15# See the License for the specific language governing permissions and 

16# limitations under the License. 

17 

18""" 

19An :ref:`Action Plan <action_plan_definition>` specifies a flow of 

20:ref:`Actions <action_definition>` that should be executed in order to satisfy 

21a given :ref:`Goal <goal_definition>`. It also contains an estimated 

22:ref:`global efficacy <efficacy_definition>` alongside a set of 

23:ref:`efficacy indicators <efficacy_indicator_definition>`. 

24 

25An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an 

26:ref:`Audit <audit_definition>` is successful which implies that the 

27:ref:`Strategy <strategy_definition>` 

28which was used has found a :ref:`Solution <solution_definition>` to achieve the 

29:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`. 

30 

31In the default implementation of Watcher, an action plan is composed of 

32a list of successive :ref:`Actions <action_definition>` (i.e., a Workflow of 

33:ref:`Actions <action_definition>` belonging to a unique branch). 

34 

35However, Watcher provides abstract interfaces for many of its components, 

36allowing other implementations to generate and handle more complex :ref:`Action 

37Plan(s) <action_plan_definition>` composed of two types of Action Item(s): 

38 

39- simple :ref:`Actions <action_definition>`: atomic tasks, which means it 

40 can not be split into smaller tasks or commands from an OpenStack point of 

41 view. 

42- composite Actions: which are composed of several simple 

43 :ref:`Actions <action_definition>` 

44 ordered in sequential and/or parallel flows. 

45 

46An :ref:`Action Plan <action_plan_definition>` may be described using 

47standard workflow model description formats such as 

48`Business Process Model and Notation 2.0 (BPMN 2.0) 

49<http://www.omg.org/spec/BPMN/2.0/>`_ or `Unified Modeling Language (UML) 

50<http://www.uml.org/>`_. 

51 

52To see the life-cycle and description of 

53:ref:`Action Plan <action_plan_definition>` states, visit :ref:`the Action Plan 

54state machine <action_plan_state_machine>`. 

55""" 

56 

57from http import HTTPStatus 

58from oslo_log import log 

59from oslo_utils import timeutils 

60import pecan 

61from pecan import rest 

62import wsme 

63from wsme import types as wtypes 

64import wsmeext.pecan as wsme_pecan 

65 

66from watcher._i18n import _ 

67from watcher.api.controllers import base 

68from watcher.api.controllers import link 

69from watcher.api.controllers.v1 import collection 

70from watcher.api.controllers.v1 import efficacy_indicator as efficacyindicator 

71from watcher.api.controllers.v1 import types 

72from watcher.api.controllers.v1 import utils as api_utils 

73from watcher.applier import rpcapi 

74from watcher.common import exception 

75from watcher.common import policy 

76from watcher.common import utils 

77from watcher import objects 

78from watcher.objects import action_plan as ap_objects 

79 

80LOG = log.getLogger(__name__) 

81 

82 

83def hide_fields_in_newer_versions(obj): 

84 """This method hides fields that were added in newer API versions. 

85 

86 Certain node fields were introduced at certain API versions. 

87 These fields are only made available when the request's API version 

88 matches or exceeds the versions when these fields were introduced. 

89 """ 

90 pass 

91 

92 

93class ActionPlanPatchType(types.JsonPatchType): 

94 

95 @staticmethod 

96 def _validate_state(patch): 

97 serialized_patch = {'path': patch.path, 'op': patch.op} 

98 if patch.value is not wtypes.Unset: 

99 serialized_patch['value'] = patch.value 

100 # todo: use state machines to handle state transitions 

101 state_value = patch.value 

102 if state_value and not hasattr(ap_objects.State, state_value): 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 msg = _("Invalid state: %(state)s") 

104 raise exception.PatchError( 

105 patch=serialized_patch, reason=msg % dict(state=state_value)) 

106 

107 @staticmethod 

108 def validate(patch): 

109 if patch.path == "/state": 

110 ActionPlanPatchType._validate_state(patch) 

111 return types.JsonPatchType.validate(patch) 

112 

113 @staticmethod 

114 def internal_attrs(): 

115 return types.JsonPatchType.internal_attrs() 

116 

117 @staticmethod 

118 def mandatory_attrs(): 

119 return ["audit_id", "state"] 

120 

121 

122class ActionPlan(base.APIBase): 

123 """API representation of a action plan. 

124 

125 This class enforces type checking and value constraints, and converts 

126 between the internal object model and the API representation of an 

127 action plan. 

128 """ 

129 

130 _audit_uuid = None 

131 _strategy_uuid = None 

132 _strategy_name = None 

133 _efficacy_indicators = None 

134 

135 def _get_audit_uuid(self): 

136 return self._audit_uuid 

137 

138 def _set_audit_uuid(self, value): 

139 if value == wtypes.Unset: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true

140 self._audit_uuid = wtypes.Unset 

141 elif value and self._audit_uuid != value: 141 ↛ exitline 141 didn't return from function '_set_audit_uuid' because the condition on line 141 was always true

142 try: 

143 audit = objects.Audit.get(pecan.request.context, value) 

144 self._audit_uuid = audit.uuid 

145 self.audit_id = audit.id 

146 except exception.AuditNotFound: 

147 self._audit_uuid = None 

148 

149 def _get_efficacy_indicators(self): 

150 if self._efficacy_indicators is None: 

151 self._set_efficacy_indicators(wtypes.Unset) 

152 return self._efficacy_indicators 

153 

154 def _set_efficacy_indicators(self, value): 

155 efficacy_indicators = [] 

156 if value == wtypes.Unset and not self._efficacy_indicators: 156 ↛ 174line 156 didn't jump to line 174 because the condition on line 156 was always true

157 try: 

158 _efficacy_indicators = objects.EfficacyIndicator.list( 

159 pecan.request.context, 

160 filters={"action_plan_uuid": self.uuid}) 

161 

162 for indicator in _efficacy_indicators: 

163 efficacy_indicator = efficacyindicator.EfficacyIndicator( 

164 context=pecan.request.context, 

165 name=indicator.name, 

166 description=indicator.description, 

167 unit=indicator.unit, 

168 value=float(indicator.value), 

169 ) 

170 efficacy_indicators.append(efficacy_indicator.as_dict()) 

171 self._efficacy_indicators = efficacy_indicators 

172 except exception.EfficacyIndicatorNotFound as exc: 

173 LOG.exception(exc) 

174 elif value and self._efficacy_indicators != value: 

175 self._efficacy_indicators = value 

176 

177 def _get_strategy(self, value): 

178 if value == wtypes.Unset: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 return None 

180 strategy = None 

181 try: 

182 if utils.is_uuid_like(value) or utils.is_int_like(value): 182 ↛ 186line 182 didn't jump to line 186 because the condition on line 182 was always true

183 strategy = objects.Strategy.get( 

184 pecan.request.context, value) 

185 else: 

186 strategy = objects.Strategy.get_by_name( 

187 pecan.request.context, value) 

188 except exception.StrategyNotFound: 

189 pass 

190 if strategy: 190 ↛ 192line 190 didn't jump to line 192 because the condition on line 190 was always true

191 self.strategy_id = strategy.id 

192 return strategy 

193 

194 def _get_strategy_uuid(self): 

195 return self._strategy_uuid 

196 

197 def _set_strategy_uuid(self, value): 

198 if value and self._strategy_uuid != value: 198 ↛ exitline 198 didn't return from function '_set_strategy_uuid' because the condition on line 198 was always true

199 self._strategy_uuid = None 

200 strategy = self._get_strategy(value) 

201 if strategy: 201 ↛ exitline 201 didn't return from function '_set_strategy_uuid' because the condition on line 201 was always true

202 self._strategy_uuid = strategy.uuid 

203 

204 def _get_strategy_name(self): 

205 return self._strategy_name 

206 

207 def _set_strategy_name(self, value): 

208 if value and self._strategy_name != value: 208 ↛ exitline 208 didn't return from function '_set_strategy_name' because the condition on line 208 was always true

209 self._strategy_name = None 

210 strategy = self._get_strategy(value) 

211 if strategy: 211 ↛ exitline 211 didn't return from function '_set_strategy_name' because the condition on line 211 was always true

212 self._strategy_name = strategy.name 

213 

214 uuid = wtypes.wsattr(types.uuid, readonly=True) 

215 """Unique UUID for this action plan""" 

216 

217 audit_uuid = wtypes.wsproperty(types.uuid, _get_audit_uuid, 

218 _set_audit_uuid, 

219 mandatory=True) 

220 """The UUID of the audit this port belongs to""" 

221 

222 strategy_uuid = wtypes.wsproperty( 

223 wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False) 

224 """Strategy UUID the action plan refers to""" 

225 

226 strategy_name = wtypes.wsproperty( 

227 wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False) 

228 """The name of the strategy this action plan refers to""" 

229 

230 efficacy_indicators = wtypes.wsproperty( 

231 types.jsontype, _get_efficacy_indicators, _set_efficacy_indicators, 

232 mandatory=True) 

233 """The list of efficacy indicators associated to this action plan""" 

234 

235 global_efficacy = wtypes.wsattr(types.jsontype, readonly=True) 

236 """The global efficacy of this action plan""" 

237 

238 state = wtypes.text 

239 """This action plan state""" 

240 

241 links = wtypes.wsattr([link.Link], readonly=True) 

242 """A list containing a self link and associated action links""" 

243 

244 hostname = wtypes.wsattr(wtypes.text, mandatory=False) 

245 """Hostname the actionplan is running on""" 

246 

247 def __init__(self, **kwargs): 

248 super(ActionPlan, self).__init__() 

249 self.fields = [] 

250 fields = list(objects.ActionPlan.fields) 

251 for field in fields: 

252 # Skip fields we do not expose. 

253 if not hasattr(self, field): 

254 continue 

255 self.fields.append(field) 

256 setattr(self, field, kwargs.get(field, wtypes.Unset)) 

257 

258 self.fields.append('audit_uuid') 

259 self.fields.append('efficacy_indicators') 

260 

261 setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset)) 

262 fields.append('strategy_uuid') 

263 setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset)) 

264 fields.append('strategy_name') 

265 setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset)) 

266 

267 @staticmethod 

268 def _convert_with_links(action_plan, url, expand=True): 

269 if not expand: 

270 action_plan.unset_fields_except( 

271 ['uuid', 'state', 'efficacy_indicators', 'global_efficacy', 

272 'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name']) 

273 

274 action_plan.links = [ 

275 link.Link.make_link( 

276 'self', url, 

277 'action_plans', action_plan.uuid), 

278 link.Link.make_link( 

279 'bookmark', url, 

280 'action_plans', action_plan.uuid, 

281 bookmark=True)] 

282 return action_plan 

283 

284 @classmethod 

285 def convert_with_links(cls, rpc_action_plan, expand=True): 

286 action_plan = ActionPlan(**rpc_action_plan.as_dict()) 

287 hide_fields_in_newer_versions(action_plan) 

288 return cls._convert_with_links(action_plan, pecan.request.host_url, 

289 expand) 

290 

291 @classmethod 

292 def sample(cls, expand=True): 

293 sample = cls(uuid='9ef4d84c-41e8-4418-9220-ce55be0436af', 

294 state='ONGOING', 

295 created_at=timeutils.utcnow(), 

296 deleted_at=None, 

297 updated_at=timeutils.utcnow()) 

298 sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6' 

299 sample._efficacy_indicators = [{'description': 'Test indicator', 

300 'name': 'test_indicator', 

301 'unit': '%'}] 

302 sample._global_efficacy = {'description': 'Global efficacy', 

303 'name': 'test_global_efficacy', 

304 'unit': '%'} 

305 return cls._convert_with_links(sample, 'http://localhost:9322', expand) 

306 

307 

308class ActionPlanCollection(collection.Collection): 

309 """API representation of a collection of action_plans.""" 

310 

311 action_plans = [ActionPlan] 

312 """A list containing action_plans objects""" 

313 

314 def __init__(self, **kwargs): 

315 self._type = 'action_plans' 

316 

317 @staticmethod 

318 def convert_with_links(rpc_action_plans, limit, url=None, expand=False, 

319 **kwargs): 

320 ap_collection = ActionPlanCollection() 

321 ap_collection.action_plans = [ActionPlan.convert_with_links( 

322 p, expand) for p in rpc_action_plans] 

323 ap_collection.next = ap_collection.get_next(limit, url=url, **kwargs) 

324 return ap_collection 

325 

326 @classmethod 

327 def sample(cls): 

328 sample = cls() 

329 sample.action_plans = [ActionPlan.sample(expand=False)] 

330 return sample 

331 

332 

333class ActionPlansController(rest.RestController): 

334 """REST controller for Actions.""" 

335 

336 def __init__(self): 

337 super(ActionPlansController, self).__init__() 

338 self.applier_client = rpcapi.ApplierAPI() 

339 

340 from_actionsPlans = False 

341 """A flag to indicate if the requests to this controller are coming 

342 from the top-level resource ActionPlan.""" 

343 

344 _custom_actions = { 

345 'start': ['POST'], 

346 'detail': ['GET'] 

347 } 

348 

349 def _get_action_plans_collection(self, marker, limit, 

350 sort_key, sort_dir, expand=False, 

351 resource_url=None, audit_uuid=None, 

352 strategy=None): 

353 additional_fields = ['audit_uuid', 'strategy_uuid', 'strategy_name'] 

354 

355 api_utils.validate_sort_key( 

356 sort_key, list(objects.ActionPlan.fields) + additional_fields) 

357 limit = api_utils.validate_limit(limit) 

358 api_utils.validate_sort_dir(sort_dir) 

359 

360 marker_obj = None 

361 if marker: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true

362 marker_obj = objects.ActionPlan.get_by_uuid( 

363 pecan.request.context, marker) 

364 

365 filters = {} 

366 if audit_uuid: 

367 filters['audit_uuid'] = audit_uuid 

368 

369 if strategy: 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true

370 if utils.is_uuid_like(strategy): 

371 filters['strategy_uuid'] = strategy 

372 else: 

373 filters['strategy_name'] = strategy 

374 

375 need_api_sort = api_utils.check_need_api_sort(sort_key, 

376 additional_fields) 

377 sort_db_key = (sort_key if not need_api_sort 

378 else None) 

379 

380 action_plans = objects.ActionPlan.list( 

381 pecan.request.context, 

382 limit, 

383 marker_obj, sort_key=sort_db_key, 

384 sort_dir=sort_dir, filters=filters) 

385 

386 action_plans_collection = ActionPlanCollection.convert_with_links( 

387 action_plans, limit, url=resource_url, expand=expand, 

388 sort_key=sort_key, sort_dir=sort_dir) 

389 

390 if need_api_sort: 

391 api_utils.make_api_sort(action_plans_collection.action_plans, 

392 sort_key, sort_dir) 

393 

394 return action_plans_collection 

395 

396 @wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text, 

397 wtypes.text, types.uuid, wtypes.text) 

398 def get_all(self, marker=None, limit=None, 

399 sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None): 

400 """Retrieve a list of action plans. 

401 

402 :param marker: pagination marker for large data sets. 

403 :param limit: maximum number of resources to return in a single result. 

404 :param sort_key: column to sort results by. Default: id. 

405 :param sort_dir: direction to sort. "asc" or "desc". Default: asc. 

406 :param audit_uuid: Optional UUID of an audit, to get only actions 

407 for that audit. 

408 :param strategy: strategy UUID or name to filter by 

409 """ 

410 context = pecan.request.context 

411 policy.enforce(context, 'action_plan:get_all', 

412 action='action_plan:get_all') 

413 

414 return self._get_action_plans_collection( 

415 marker, limit, sort_key, sort_dir, 

416 audit_uuid=audit_uuid, strategy=strategy) 

417 

418 @wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text, 

419 wtypes.text, types.uuid, wtypes.text) 

420 def detail(self, marker=None, limit=None, 

421 sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None): 

422 """Retrieve a list of action_plans with detail. 

423 

424 :param marker: pagination marker for large data sets. 

425 :param limit: maximum number of resources to return in a single result. 

426 :param sort_key: column to sort results by. Default: id. 

427 :param sort_dir: direction to sort. "asc" or "desc". Default: asc. 

428 :param audit_uuid: Optional UUID of an audit, to get only actions 

429 for that audit. 

430 :param strategy: strategy UUID or name to filter by 

431 """ 

432 context = pecan.request.context 

433 policy.enforce(context, 'action_plan:detail', 

434 action='action_plan:detail') 

435 

436 # NOTE(lucasagomes): /detail should only work against collections 

437 parent = pecan.request.path.split('/')[:-1][-1] 

438 if parent != "action_plans": 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true

439 raise exception.HTTPNotFound 

440 

441 expand = True 

442 resource_url = '/'.join(['action_plans', 'detail']) 

443 return self._get_action_plans_collection( 

444 marker, limit, sort_key, sort_dir, expand, 

445 resource_url, audit_uuid=audit_uuid, strategy=strategy) 

446 

447 @wsme_pecan.wsexpose(ActionPlan, types.uuid) 

448 def get_one(self, action_plan_uuid): 

449 """Retrieve information about the given action plan. 

450 

451 :param action_plan_uuid: UUID of a action plan. 

452 """ 

453 if self.from_actionsPlans: 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true

454 raise exception.OperationNotPermitted 

455 

456 context = pecan.request.context 

457 action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid) 

458 policy.enforce( 

459 context, 'action_plan:get', action_plan, action='action_plan:get') 

460 

461 return ActionPlan.convert_with_links(action_plan) 

462 

463 @wsme_pecan.wsexpose(None, types.uuid, status_code=HTTPStatus.NO_CONTENT) 

464 def delete(self, action_plan_uuid): 

465 """Delete an action plan. 

466 

467 :param action_plan_uuid: UUID of a action. 

468 """ 

469 context = pecan.request.context 

470 action_plan = api_utils.get_resource( 

471 'ActionPlan', action_plan_uuid, eager=True) 

472 policy.enforce(context, 'action_plan:delete', action_plan, 

473 action='action_plan:delete') 

474 

475 allowed_states = (ap_objects.State.SUCCEEDED, 

476 ap_objects.State.RECOMMENDED, 

477 ap_objects.State.FAILED, 

478 ap_objects.State.SUPERSEDED, 

479 ap_objects.State.CANCELLED) 

480 if action_plan.state not in allowed_states: 

481 raise exception.DeleteError( 

482 state=action_plan.state) 

483 

484 action_plan.soft_delete() 

485 

486 @wsme.validate(types.uuid, [ActionPlanPatchType]) 

487 @wsme_pecan.wsexpose(ActionPlan, types.uuid, 

488 body=[ActionPlanPatchType]) 

489 def patch(self, action_plan_uuid, patch): 

490 """Update an existing action plan. 

491 

492 :param action_plan_uuid: UUID of a action plan. 

493 :param patch: a json PATCH document to apply to this action plan. 

494 """ 

495 if self.from_actionsPlans: 495 ↛ 496line 495 didn't jump to line 496 because the condition on line 495 was never true

496 raise exception.OperationNotPermitted 

497 

498 context = pecan.request.context 

499 action_plan_to_update = api_utils.get_resource( 

500 'ActionPlan', action_plan_uuid, eager=True) 

501 policy.enforce(context, 'action_plan:update', action_plan_to_update, 

502 action='action_plan:update') 

503 

504 try: 

505 action_plan_dict = action_plan_to_update.as_dict() 

506 action_plan = ActionPlan(**api_utils.apply_jsonpatch( 

507 action_plan_dict, patch)) 

508 except api_utils.JSONPATCH_EXCEPTIONS as e: 

509 raise exception.PatchError(patch=patch, reason=e) 

510 

511 launch_action_plan = False 

512 cancel_action_plan = False 

513 

514 # transitions that are allowed via PATCH 

515 allowed_patch_transitions = [ 

516 (ap_objects.State.RECOMMENDED, 

517 ap_objects.State.PENDING), 

518 (ap_objects.State.RECOMMENDED, 

519 ap_objects.State.CANCELLED), 

520 (ap_objects.State.ONGOING, 

521 ap_objects.State.CANCELLING), 

522 (ap_objects.State.PENDING, 

523 ap_objects.State.CANCELLED), 

524 ] 

525 

526 # todo: improve this in blueprint watcher-api-validation 

527 if hasattr(action_plan, 'state'): 527 ↛ 544line 527 didn't jump to line 544 because the condition on line 527 was always true

528 transition = (action_plan_to_update.state, action_plan.state) 

529 if transition not in allowed_patch_transitions: 

530 error_message = _("State transition not allowed: " 

531 "(%(initial_state)s -> %(new_state)s)") 

532 raise exception.PatchError( 

533 patch=patch, 

534 reason=error_message % dict( 

535 initial_state=action_plan_to_update.state, 

536 new_state=action_plan.state)) 

537 

538 if action_plan.state == ap_objects.State.PENDING: 

539 launch_action_plan = True 

540 if action_plan.state == ap_objects.State.CANCELLED: 

541 cancel_action_plan = True 

542 

543 # Update only the fields that have changed 

544 for field in objects.ActionPlan.fields: 

545 try: 

546 patch_val = getattr(action_plan, field) 

547 except AttributeError: 

548 # Ignore fields that aren't exposed in the API 

549 continue 

550 if patch_val == wtypes.Unset: 550 ↛ 551line 550 didn't jump to line 551 because the condition on line 550 was never true

551 patch_val = None 

552 if action_plan_to_update[field] != patch_val: 

553 action_plan_to_update[field] = patch_val 

554 

555 if (field == 'state' and 

556 patch_val == objects.action_plan.State.PENDING): 

557 launch_action_plan = True 

558 

559 action_plan_to_update.save() 

560 

561 # NOTE: if action plan is cancelled from pending or recommended 

562 # state update action state here only 

563 if cancel_action_plan: 

564 filters = {'action_plan_uuid': action_plan.uuid} 

565 actions = objects.Action.list(pecan.request.context, 

566 filters=filters, eager=True) 

567 for a in actions: 567 ↛ 568line 567 didn't jump to line 568 because the loop on line 567 never started

568 a.state = objects.action.State.CANCELLED 

569 a.save() 

570 

571 if launch_action_plan: 

572 self.applier_client.launch_action_plan(pecan.request.context, 

573 action_plan.uuid) 

574 

575 action_plan_to_update = objects.ActionPlan.get_by_uuid( 

576 pecan.request.context, 

577 action_plan_uuid) 

578 return ActionPlan.convert_with_links(action_plan_to_update) 

579 

580 @wsme_pecan.wsexpose(ActionPlan, types.uuid) 

581 def start(self, action_plan_uuid, **kwargs): 

582 """Start an action_plan 

583 

584 :param action_plan_uuid: UUID of an action_plan. 

585 """ 

586 

587 action_plan_to_start = api_utils.get_resource( 

588 'ActionPlan', action_plan_uuid, eager=True) 

589 context = pecan.request.context 

590 

591 policy.enforce(context, 'action_plan:start', action_plan_to_start, 

592 action='action_plan:start') 

593 

594 if action_plan_to_start['state'] != \ 594 ↛ 596line 594 didn't jump to line 596 because the condition on line 594 was never true

595 objects.action_plan.State.RECOMMENDED: 

596 raise exception.StartError( 

597 state=action_plan_to_start.state) 

598 

599 action_plan_to_start['state'] = objects.action_plan.State.PENDING 

600 action_plan_to_start.save() 

601 

602 self.applier_client.launch_action_plan(pecan.request.context, 

603 action_plan_uuid) 

604 action_plan_to_start = objects.ActionPlan.get_by_uuid( 

605 pecan.request.context, action_plan_uuid) 

606 

607 return ActionPlan.convert_with_links(action_plan_to_start)