Coverage for watcher/db/sqlalchemy/api.py: 94%

643 statements  

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

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

2# 

3# Copyright 2013 Hewlett-Packard Development Company, L.P. 

4# 

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

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

7# 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, WITHOUT 

13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

14# License for the specific language governing permissions and limitations 

15# under the License. 

16 

17"""SQLAlchemy storage backend.""" 

18 

19import collections 

20import datetime 

21import operator 

22import threading 

23 

24from oslo_config import cfg 

25from oslo_db import api as oslo_db_api 

26from oslo_db import exception as db_exc 

27from oslo_db.sqlalchemy import enginefacade 

28from oslo_db.sqlalchemy import utils as db_utils 

29from oslo_utils import timeutils 

30from sqlalchemy.inspection import inspect 

31from sqlalchemy.orm import exc 

32from sqlalchemy.orm import joinedload 

33 

34from watcher._i18n import _ 

35from watcher.common import exception 

36from watcher.common import utils 

37from watcher.db import api 

38from watcher.db.sqlalchemy import models 

39from watcher import objects 

40 

41CONF = cfg.CONF 

42 

43_CONTEXT = threading.local() 

44 

45 

46def get_backend(): 

47 """The backend is this module itself.""" 

48 return Connection() 

49 

50 

51def _session_for_read(): 

52 return enginefacade.reader.using(_CONTEXT) 

53 

54 

55# NOTE(tylerchristie) Please add @oslo_db_api.retry_on_deadlock decorator to 

56# any new methods using _session_for_write (as deadlocks happen on write), so 

57# that oslo_db is able to retry in case of deadlocks. 

58def _session_for_write(): 

59 return enginefacade.writer.using(_CONTEXT) 

60 

61 

62def add_identity_filter(query, value): 

63 """Adds an identity filter to a query. 

64 

65 Filters results by ID, if supplied value is a valid integer. 

66 Otherwise attempts to filter results by UUID. 

67 

68 :param query: Initial query to add filter to. 

69 :param value: Value for filtering results by. 

70 :return: Modified query. 

71 """ 

72 if utils.is_int_like(value): 

73 return query.filter_by(id=value) 

74 elif utils.is_uuid_like(value): 74 ↛ 77line 74 didn't jump to line 77 because the condition on line 74 was always true

75 return query.filter_by(uuid=value) 

76 else: 

77 raise exception.InvalidIdentity(identity=value) 

78 

79 

80def _paginate_query(model, limit=None, marker=None, sort_key=None, 

81 sort_dir=None, query=None): 

82 sort_keys = ['id'] 

83 if sort_key and sort_key not in sort_keys: 

84 sort_keys.insert(0, sort_key) 

85 query = db_utils.paginate_query(query, model, limit, sort_keys, 

86 marker=marker, sort_dir=sort_dir) 

87 return query.all() 

88 

89 

90class JoinMap(utils.Struct): 

91 """Mapping for the Join-based queries""" 

92 

93 

94NaturalJoinFilter = collections.namedtuple( 

95 'NaturalJoinFilter', ['join_fieldname', 'join_model']) 

96 

97 

98class Connection(api.BaseConnection): 

99 """SqlAlchemy connection.""" 

100 

101 valid_operators = { 

102 "": operator.eq, 

103 "eq": operator.eq, 

104 "neq": operator.ne, 

105 "gt": operator.gt, 

106 "gte": operator.ge, 

107 "lt": operator.lt, 

108 "lte": operator.le, 

109 "in": lambda field, choices: field.in_(choices), 

110 "notin": lambda field, choices: field.notin_(choices), 

111 } 

112 

113 def __init__(self): 

114 super(Connection, self).__init__() 

115 

116 def __add_simple_filter(self, query, model, fieldname, value, operator_): 

117 field = getattr(model, fieldname) 

118 

119 if (fieldname != 'deleted' and value and 

120 field.type.python_type is datetime.datetime): 

121 if not isinstance(value, datetime.datetime): 

122 value = timeutils.parse_isotime(value) 

123 

124 return query.filter(self.valid_operators[operator_](field, value)) 

125 

126 def __add_join_filter(self, query, model, fieldname, value, operator_): 

127 query = query.join(model) 

128 return self.__add_simple_filter(query, model, fieldname, 

129 value, operator_) 

130 

131 def __decompose_filter(self, raw_fieldname): 

132 """Decompose a filter name into its 2 subparts 

133 

134 A filter can take 2 forms: 

135 

136 - "<FIELDNAME>" which is a syntactic sugar for "<FIELDNAME>__eq" 

137 - "<FIELDNAME>__<OPERATOR>" where <OPERATOR> is the comparison operator 

138 to be used. 

139 

140 Available operators are: 

141 

142 - eq 

143 - neq 

144 - gt 

145 - gte 

146 - lt 

147 - lte 

148 - in 

149 - notin 

150 """ 

151 separator = '__' 

152 fieldname, separator, operator_ = raw_fieldname.partition(separator) 

153 

154 if operator_ and operator_ not in self.valid_operators: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true

155 raise exception.InvalidOperator( 

156 operator=operator_, valid_operators=self.valid_operators) 

157 

158 return fieldname, operator_ 

159 

160 def _add_filters(self, query, model, filters=None, 

161 plain_fields=None, join_fieldmap=None): 

162 """Generic way to add filters to a Watcher model 

163 

164 Each filter key provided by the `filters` parameter will be decomposed 

165 into 2 pieces: the field name and the comparison operator 

166 

167 - "": By default, the "eq" is applied if no operator is provided 

168 - "eq", which stands for "equal" : e.g. {"state__eq": "PENDING"} 

169 will result in the "WHERE state = 'PENDING'" clause. 

170 - "neq", which stands for "not equal" : e.g. {"state__neq": "PENDING"} 

171 will result in the "WHERE state != 'PENDING'" clause. 

172 - "gt", which stands for "greater than" : e.g. 

173 {"created_at__gt": "2016-06-06T10:33:22.063176"} will result in the 

174 "WHERE created_at > '2016-06-06T10:33:22.063176'" clause. 

175 - "gte", which stands for "greater than or equal to" : e.g. 

176 {"created_at__gte": "2016-06-06T10:33:22.063176"} will result in the 

177 "WHERE created_at >= '2016-06-06T10:33:22.063176'" clause. 

178 - "lt", which stands for "less than" : e.g. 

179 {"created_at__lt": "2016-06-06T10:33:22.063176"} will result in the 

180 "WHERE created_at < '2016-06-06T10:33:22.063176'" clause. 

181 - "lte", which stands for "less than or equal to" : e.g. 

182 {"created_at__lte": "2016-06-06T10:33:22.063176"} will result in the 

183 "WHERE created_at <= '2016-06-06T10:33:22.063176'" clause. 

184 - "in": e.g. {"state__in": ('SUCCEEDED', 'FAILED')} will result in the 

185 "WHERE state IN ('SUCCEEDED', 'FAILED')" clause. 

186 

187 :param query: a :py:class:`sqlalchemy.orm.query.Query` instance 

188 :param model: the model class the filters should relate to 

189 :param filters: dict with the following structure {"fieldname": value} 

190 :param plain_fields: a :py:class:`sqlalchemy.orm.query.Query` instance 

191 :param join_fieldmap: a :py:class:`sqlalchemy.orm.query.Query` instance 

192 """ 

193 soft_delete_mixin_fields = ['deleted', 'deleted_at'] 

194 timestamp_mixin_fields = ['created_at', 'updated_at'] 

195 filters = filters or {} 

196 

197 # Special case for 'deleted' because it is a non-boolean flag 

198 if 'deleted' in filters: 

199 deleted_filter = filters.pop('deleted') 

200 op = 'eq' if not bool(deleted_filter) else 'neq' 

201 filters['deleted__%s' % op] = 0 

202 

203 plain_fields = tuple( 

204 (list(plain_fields) or []) + 

205 soft_delete_mixin_fields + 

206 timestamp_mixin_fields) 

207 join_fieldmap = join_fieldmap or {} 

208 

209 for raw_fieldname, value in filters.items(): 

210 fieldname, operator_ = self.__decompose_filter(raw_fieldname) 

211 if fieldname in plain_fields: 

212 query = self.__add_simple_filter( 

213 query, model, fieldname, value, operator_) 

214 elif fieldname in join_fieldmap: 

215 join_field, join_model = join_fieldmap[fieldname] 

216 query = self.__add_join_filter( 

217 query, join_model, join_field, value, operator_) 

218 

219 return query 

220 

221 @staticmethod 

222 def _get_relationships(model): 

223 return inspect(model).relationships 

224 

225 @staticmethod 

226 def _set_eager_options(model, query): 

227 relationships = inspect(model).relationships 

228 for relationship in relationships: 

229 if not relationship.uselist: 229 ↛ 228line 229 didn't jump to line 228 because the condition on line 229 was always true

230 # We have a One-to-X relationship 

231 query = query.options(joinedload( 

232 getattr(model, relationship.key))) 

233 return query 

234 

235 @oslo_db_api.retry_on_deadlock 

236 def _create(self, model, values): 

237 with _session_for_write() as session: 

238 obj = model() 

239 cleaned_values = {k: v for k, v in values.items() 

240 if k not in self._get_relationships(model)} 

241 obj.update(cleaned_values) 

242 session.add(obj) 

243 session.flush() 

244 return obj 

245 

246 def _get(self, context, model, fieldname, value, eager): 

247 with _session_for_read() as session: 

248 query = session.query(model) 

249 if eager: 

250 query = self._set_eager_options(model, query) 

251 

252 query = query.filter(getattr(model, fieldname) == value) 

253 if not context.show_deleted: 

254 query = query.filter(model.deleted_at.is_(None)) 

255 

256 try: 

257 obj = query.one() 

258 except exc.NoResultFound: 

259 raise exception.ResourceNotFound(name=model.__name__, id=value) 

260 

261 return obj 

262 

263 @staticmethod 

264 @oslo_db_api.retry_on_deadlock 

265 def _update(model, id_, values): 

266 with _session_for_write() as session: 

267 query = session.query(model) 

268 query = add_identity_filter(query, id_) 

269 try: 

270 ref = query.with_for_update().one() 

271 except exc.NoResultFound: 

272 raise exception.ResourceNotFound(name=model.__name__, id=id_) 

273 

274 ref.update(values) 

275 

276 return ref 

277 

278 @staticmethod 

279 @oslo_db_api.retry_on_deadlock 

280 def _soft_delete(model, id_): 

281 with _session_for_write() as session: 

282 query = session.query(model) 

283 query = add_identity_filter(query, id_) 

284 try: 

285 row = query.one() 

286 except exc.NoResultFound: 

287 raise exception.ResourceNotFound(name=model.__name__, id=id_) 

288 

289 row.soft_delete(session) 

290 

291 return row 

292 

293 @staticmethod 

294 @oslo_db_api.retry_on_deadlock 

295 def _destroy(model, id_): 

296 with _session_for_write() as session: 

297 query = session.query(model) 

298 query = add_identity_filter(query, id_) 

299 

300 try: 

301 query.one() 

302 except exc.NoResultFound: 

303 raise exception.ResourceNotFound(name=model.__name__, id=id_) 

304 

305 query.delete() 

306 

307 def _get_model_list(self, model, add_filters_func, context, filters=None, 

308 limit=None, marker=None, sort_key=None, sort_dir=None, 

309 eager=False): 

310 with _session_for_read() as session: 

311 query = session.query(model) 

312 if eager: 

313 query = self._set_eager_options(model, query) 

314 query = add_filters_func(query, filters) 

315 if not context.show_deleted: 

316 query = query.filter(model.deleted_at.is_(None)) 

317 return _paginate_query(model, limit, marker, 

318 sort_key, sort_dir, query) 

319 

320 # NOTE(erakli): _add_..._filters methods should be refactored to have same 

321 # content. join_fieldmap should be filled with JoinMap instead of dict 

322 

323 def _add_goals_filters(self, query, filters): 

324 if filters is None: 

325 filters = {} 

326 

327 plain_fields = ['uuid', 'name', 'display_name'] 

328 

329 return self._add_filters( 

330 query=query, model=models.Goal, filters=filters, 

331 plain_fields=plain_fields) 

332 

333 def _add_strategies_filters(self, query, filters): 

334 plain_fields = ['uuid', 'name', 'display_name', 'goal_id'] 

335 join_fieldmap = JoinMap( 

336 goal_uuid=NaturalJoinFilter( 

337 join_fieldname="uuid", join_model=models.Goal), 

338 goal_name=NaturalJoinFilter( 

339 join_fieldname="name", join_model=models.Goal)) 

340 return self._add_filters( 

341 query=query, model=models.Strategy, filters=filters, 

342 plain_fields=plain_fields, join_fieldmap=join_fieldmap) 

343 

344 def _add_audit_templates_filters(self, query, filters): 

345 if filters is None: 

346 filters = {} 

347 

348 plain_fields = ['uuid', 'name', 'goal_id', 'strategy_id'] 

349 join_fieldmap = JoinMap( 

350 goal_uuid=NaturalJoinFilter( 

351 join_fieldname="uuid", join_model=models.Goal), 

352 goal_name=NaturalJoinFilter( 

353 join_fieldname="name", join_model=models.Goal), 

354 strategy_uuid=NaturalJoinFilter( 

355 join_fieldname="uuid", join_model=models.Strategy), 

356 strategy_name=NaturalJoinFilter( 

357 join_fieldname="name", join_model=models.Strategy), 

358 ) 

359 

360 return self._add_filters( 

361 query=query, model=models.AuditTemplate, filters=filters, 

362 plain_fields=plain_fields, join_fieldmap=join_fieldmap) 

363 

364 def _add_audits_filters(self, query, filters): 

365 if filters is None: 

366 filters = {} 

367 

368 plain_fields = ['uuid', 'audit_type', 'state', 'goal_id', 

369 'strategy_id', 'hostname'] 

370 join_fieldmap = { 

371 'goal_uuid': ("uuid", models.Goal), 

372 'goal_name': ("name", models.Goal), 

373 'strategy_uuid': ("uuid", models.Strategy), 

374 'strategy_name': ("name", models.Strategy), 

375 } 

376 

377 return self._add_filters( 

378 query=query, model=models.Audit, filters=filters, 

379 plain_fields=plain_fields, join_fieldmap=join_fieldmap) 

380 

381 def _add_action_plans_filters(self, query, filters): 

382 if filters is None: 

383 filters = {} 

384 

385 plain_fields = ['uuid', 'state', 'audit_id', 'strategy_id'] 

386 join_fieldmap = JoinMap( 

387 audit_uuid=NaturalJoinFilter( 

388 join_fieldname="uuid", join_model=models.Audit), 

389 strategy_uuid=NaturalJoinFilter( 

390 join_fieldname="uuid", join_model=models.Strategy), 

391 strategy_name=NaturalJoinFilter( 

392 join_fieldname="name", join_model=models.Strategy), 

393 ) 

394 

395 return self._add_filters( 

396 query=query, model=models.ActionPlan, filters=filters, 

397 plain_fields=plain_fields, join_fieldmap=join_fieldmap) 

398 

399 def _add_actions_filters(self, query, filters): 

400 if filters is None: 

401 filters = {} 

402 

403 plain_fields = ['uuid', 'state', 'action_plan_id'] 

404 join_fieldmap = { 

405 'action_plan_uuid': ("uuid", models.ActionPlan), 

406 } 

407 

408 query = self._add_filters( 

409 query=query, model=models.Action, filters=filters, 

410 plain_fields=plain_fields, join_fieldmap=join_fieldmap) 

411 

412 if 'audit_uuid' in filters: 

413 with _session_for_read() as session: 

414 stmt = session.query(models.ActionPlan).join( 

415 models.Audit, 

416 models.Audit.id == models.ActionPlan.audit_id)\ 

417 .filter_by(uuid=filters['audit_uuid']).subquery() 

418 query = query.filter_by(action_plan_id=stmt.c.id) 

419 

420 return query 

421 

422 def _add_efficacy_indicators_filters(self, query, filters): 

423 if filters is None: 

424 filters = {} 

425 

426 plain_fields = ['uuid', 'name', 'unit', 'schema', 'action_plan_id'] 

427 join_fieldmap = JoinMap( 

428 action_plan_uuid=NaturalJoinFilter( 

429 join_fieldname="uuid", join_model=models.ActionPlan), 

430 ) 

431 

432 return self._add_filters( 

433 query=query, model=models.EfficacyIndicator, filters=filters, 

434 plain_fields=plain_fields, join_fieldmap=join_fieldmap) 

435 

436 def _add_scoring_engine_filters(self, query, filters): 

437 if filters is None: 

438 filters = {} 

439 

440 plain_fields = ['id', 'description'] 

441 

442 return self._add_filters( 

443 query=query, model=models.ScoringEngine, filters=filters, 

444 plain_fields=plain_fields) 

445 

446 def _add_action_descriptions_filters(self, query, filters): 

447 if not filters: 

448 filters = {} 

449 

450 plain_fields = ['id', 'action_type'] 

451 

452 return self._add_filters( 

453 query=query, model=models.ActionDescription, filters=filters, 

454 plain_fields=plain_fields) 

455 

456 def _add_services_filters(self, query, filters): 

457 if not filters: 

458 filters = {} 

459 

460 plain_fields = ['id', 'name', 'host'] 

461 

462 return self._add_filters( 

463 query=query, model=models.Service, filters=filters, 

464 plain_fields=plain_fields) 

465 

466 # ### GOALS ### # 

467 

468 def get_goal_list(self, *args, **kwargs): 

469 return self._get_model_list(models.Goal, 

470 self._add_goals_filters, 

471 *args, **kwargs) 

472 

473 def create_goal(self, values): 

474 # ensure defaults are present for new goals 

475 if not values.get('uuid'): 

476 values['uuid'] = utils.generate_uuid() 

477 

478 try: 

479 goal = self._create(models.Goal, values) 

480 except db_exc.DBDuplicateEntry: 

481 raise exception.GoalAlreadyExists(uuid=values['uuid']) 

482 return goal 

483 

484 def _get_goal(self, context, fieldname, value, eager): 

485 try: 

486 return self._get(context, model=models.Goal, 

487 fieldname=fieldname, value=value, eager=eager) 

488 except exception.ResourceNotFound: 

489 raise exception.GoalNotFound(goal=value) 

490 

491 def get_goal_by_id(self, context, goal_id, eager=False): 

492 return self._get_goal( 

493 context, fieldname="id", value=goal_id, eager=eager) 

494 

495 def get_goal_by_uuid(self, context, goal_uuid, eager=False): 

496 return self._get_goal( 

497 context, fieldname="uuid", value=goal_uuid, eager=eager) 

498 

499 def get_goal_by_name(self, context, goal_name, eager=False): 

500 return self._get_goal( 

501 context, fieldname="name", value=goal_name, eager=eager) 

502 

503 def destroy_goal(self, goal_id): 

504 try: 

505 return self._destroy(models.Goal, goal_id) 

506 except exception.ResourceNotFound: 

507 raise exception.GoalNotFound(goal=goal_id) 

508 

509 def update_goal(self, goal_id, values): 

510 if 'uuid' in values: 

511 raise exception.Invalid( 

512 message=_("Cannot overwrite UUID for an existing Goal.")) 

513 

514 try: 

515 return self._update(models.Goal, goal_id, values) 

516 except exception.ResourceNotFound: 

517 raise exception.GoalNotFound(goal=goal_id) 

518 

519 def soft_delete_goal(self, goal_id): 

520 try: 

521 return self._soft_delete(models.Goal, goal_id) 

522 except exception.ResourceNotFound: 

523 raise exception.GoalNotFound(goal=goal_id) 

524 

525 # ### STRATEGIES ### # 

526 

527 def get_strategy_list(self, *args, **kwargs): 

528 return self._get_model_list(models.Strategy, 

529 self._add_strategies_filters, 

530 *args, **kwargs) 

531 

532 def create_strategy(self, values): 

533 # ensure defaults are present for new strategies 

534 if not values.get('uuid'): 

535 values['uuid'] = utils.generate_uuid() 

536 

537 try: 

538 strategy = self._create(models.Strategy, values) 

539 except db_exc.DBDuplicateEntry: 

540 raise exception.StrategyAlreadyExists(uuid=values['uuid']) 

541 return strategy 

542 

543 def _get_strategy(self, context, fieldname, value, eager): 

544 try: 

545 return self._get(context, model=models.Strategy, 

546 fieldname=fieldname, value=value, eager=eager) 

547 except exception.ResourceNotFound: 

548 raise exception.StrategyNotFound(strategy=value) 

549 

550 def get_strategy_by_id(self, context, strategy_id, eager=False): 

551 return self._get_strategy( 

552 context, fieldname="id", value=strategy_id, eager=eager) 

553 

554 def get_strategy_by_uuid(self, context, strategy_uuid, eager=False): 

555 return self._get_strategy( 

556 context, fieldname="uuid", value=strategy_uuid, eager=eager) 

557 

558 def get_strategy_by_name(self, context, strategy_name, eager=False): 

559 return self._get_strategy( 

560 context, fieldname="name", value=strategy_name, eager=eager) 

561 

562 def destroy_strategy(self, strategy_id): 

563 try: 

564 return self._destroy(models.Strategy, strategy_id) 

565 except exception.ResourceNotFound: 

566 raise exception.StrategyNotFound(strategy=strategy_id) 

567 

568 def update_strategy(self, strategy_id, values): 

569 if 'uuid' in values: 

570 raise exception.Invalid( 

571 message=_("Cannot overwrite UUID for an existing Strategy.")) 

572 

573 try: 

574 return self._update(models.Strategy, strategy_id, values) 

575 except exception.ResourceNotFound: 

576 raise exception.StrategyNotFound(strategy=strategy_id) 

577 

578 def soft_delete_strategy(self, strategy_id): 

579 try: 

580 return self._soft_delete(models.Strategy, strategy_id) 

581 except exception.ResourceNotFound: 

582 raise exception.StrategyNotFound(strategy=strategy_id) 

583 

584 # ### AUDIT TEMPLATES ### # 

585 

586 def get_audit_template_list(self, *args, **kwargs): 

587 return self._get_model_list(models.AuditTemplate, 

588 self._add_audit_templates_filters, 

589 *args, **kwargs) 

590 

591 def create_audit_template(self, values): 

592 # ensure defaults are present for new audit_templates 

593 if not values.get('uuid'): 

594 values['uuid'] = utils.generate_uuid() 

595 

596 with _session_for_write() as session: 

597 query = session.query(models.AuditTemplate) 

598 query = query.filter_by(name=values.get('name'), 

599 deleted_at=None) 

600 

601 if len(query.all()) > 0: 

602 raise exception.AuditTemplateAlreadyExists( 

603 audit_template=values['name']) 

604 

605 try: 

606 audit_template = self._create(models.AuditTemplate, values) 

607 except db_exc.DBDuplicateEntry: 

608 raise exception.AuditTemplateAlreadyExists( 

609 audit_template=values['name']) 

610 return audit_template 

611 

612 def _get_audit_template(self, context, fieldname, value, eager): 

613 try: 

614 return self._get(context, model=models.AuditTemplate, 

615 fieldname=fieldname, value=value, eager=eager) 

616 except exception.ResourceNotFound: 

617 raise exception.AuditTemplateNotFound(audit_template=value) 

618 

619 def get_audit_template_by_id(self, context, audit_template_id, 

620 eager=False): 

621 return self._get_audit_template( 

622 context, fieldname="id", value=audit_template_id, eager=eager) 

623 

624 def get_audit_template_by_uuid(self, context, audit_template_uuid, 

625 eager=False): 

626 return self._get_audit_template( 

627 context, fieldname="uuid", value=audit_template_uuid, eager=eager) 

628 

629 def get_audit_template_by_name(self, context, audit_template_name, 

630 eager=False): 

631 return self._get_audit_template( 

632 context, fieldname="name", value=audit_template_name, eager=eager) 

633 

634 def destroy_audit_template(self, audit_template_id): 

635 try: 

636 return self._destroy(models.AuditTemplate, audit_template_id) 

637 except exception.ResourceNotFound: 

638 raise exception.AuditTemplateNotFound( 

639 audit_template=audit_template_id) 

640 

641 def update_audit_template(self, audit_template_id, values): 

642 if 'uuid' in values: 

643 raise exception.Invalid( 

644 message=_("Cannot overwrite UUID for an existing " 

645 "Audit Template.")) 

646 try: 

647 return self._update( 

648 models.AuditTemplate, audit_template_id, values) 

649 except exception.ResourceNotFound: 

650 raise exception.AuditTemplateNotFound( 

651 audit_template=audit_template_id) 

652 

653 def soft_delete_audit_template(self, audit_template_id): 

654 try: 

655 return self._soft_delete(models.AuditTemplate, audit_template_id) 

656 except exception.ResourceNotFound: 

657 raise exception.AuditTemplateNotFound( 

658 audit_template=audit_template_id) 

659 

660 # ### AUDITS ### # 

661 

662 def get_audit_list(self, *args, **kwargs): 

663 return self._get_model_list(models.Audit, 

664 self._add_audits_filters, 

665 *args, **kwargs) 

666 

667 def create_audit(self, values): 

668 # ensure defaults are present for new audits 

669 if not values.get('uuid'): 

670 values['uuid'] = utils.generate_uuid() 

671 

672 with _session_for_write() as session: 

673 query = session.query(models.Audit) 

674 query = query.filter_by(name=values.get('name'), 

675 deleted_at=None) 

676 

677 if len(query.all()) > 0: 

678 raise exception.AuditAlreadyExists( 

679 audit=values['name']) 

680 

681 if values.get('state') is None: 

682 values['state'] = objects.audit.State.PENDING 

683 

684 if not values.get('auto_trigger'): 

685 values['auto_trigger'] = False 

686 

687 try: 

688 audit = self._create(models.Audit, values) 

689 except db_exc.DBDuplicateEntry: 

690 raise exception.AuditAlreadyExists(audit=values['uuid']) 

691 return audit 

692 

693 def _get_audit(self, context, fieldname, value, eager): 

694 try: 

695 return self._get(context, model=models.Audit, 

696 fieldname=fieldname, value=value, eager=eager) 

697 except exception.ResourceNotFound: 

698 raise exception.AuditNotFound(audit=value) 

699 

700 def get_audit_by_id(self, context, audit_id, eager=False): 

701 return self._get_audit( 

702 context, fieldname="id", value=audit_id, eager=eager) 

703 

704 def get_audit_by_uuid(self, context, audit_uuid, eager=False): 

705 return self._get_audit( 

706 context, fieldname="uuid", value=audit_uuid, eager=eager) 

707 

708 def get_audit_by_name(self, context, audit_name, eager=False): 

709 return self._get_audit( 

710 context, fieldname="name", value=audit_name, eager=eager) 

711 

712 def destroy_audit(self, audit_id): 

713 def is_audit_referenced(session, audit_id): 

714 """Checks whether the audit is referenced by action_plan(s).""" 

715 query = session.query(models.ActionPlan) 

716 query = self._add_action_plans_filters( 

717 query, {'audit_id': audit_id}) 

718 return query.count() != 0 

719 

720 with _session_for_write() as session: 

721 query = session.query(models.Audit) 

722 query = add_identity_filter(query, audit_id) 

723 

724 try: 

725 audit_ref = query.one() 

726 except exc.NoResultFound: 

727 raise exception.AuditNotFound(audit=audit_id) 

728 

729 if is_audit_referenced(session, audit_ref['id']): 

730 raise exception.AuditReferenced(audit=audit_id) 

731 

732 query.delete() 

733 

734 def update_audit(self, audit_id, values): 

735 if 'uuid' in values: 

736 raise exception.Invalid( 

737 message=_("Cannot overwrite UUID for an existing " 

738 "Audit.")) 

739 

740 try: 

741 return self._update(models.Audit, audit_id, values) 

742 except exception.ResourceNotFound: 

743 raise exception.AuditNotFound(audit=audit_id) 

744 

745 def soft_delete_audit(self, audit_id): 

746 try: 

747 return self._soft_delete(models.Audit, audit_id) 

748 except exception.ResourceNotFound: 

749 raise exception.AuditNotFound(audit=audit_id) 

750 

751 # ### ACTIONS ### # 

752 

753 def get_action_list(self, *args, **kwargs): 

754 return self._get_model_list(models.Action, 

755 self._add_actions_filters, 

756 *args, **kwargs) 

757 

758 def create_action(self, values): 

759 # ensure defaults are present for new actions 

760 if not values.get('uuid'): 

761 values['uuid'] = utils.generate_uuid() 

762 

763 if values.get('state') is None: 

764 values['state'] = objects.action.State.PENDING 

765 

766 try: 

767 action = self._create(models.Action, values) 

768 except db_exc.DBDuplicateEntry: 

769 raise exception.ActionAlreadyExists(uuid=values['uuid']) 

770 return action 

771 

772 def _get_action(self, context, fieldname, value, eager): 

773 try: 

774 return self._get(context, model=models.Action, 

775 fieldname=fieldname, value=value, eager=eager) 

776 except exception.ResourceNotFound: 

777 raise exception.ActionNotFound(action=value) 

778 

779 def get_action_by_id(self, context, action_id, eager=False): 

780 return self._get_action( 

781 context, fieldname="id", value=action_id, eager=eager) 

782 

783 def get_action_by_uuid(self, context, action_uuid, eager=False): 

784 return self._get_action( 

785 context, fieldname="uuid", value=action_uuid, eager=eager) 

786 

787 def destroy_action(self, action_id): 

788 with _session_for_write() as session: 

789 query = session.query(models.Action) 

790 query = add_identity_filter(query, action_id) 

791 count = query.delete() 

792 if count != 1: 

793 raise exception.ActionNotFound(action_id) 

794 

795 def update_action(self, action_id, values): 

796 # NOTE(dtantsur): this can lead to very strange errors 

797 if 'uuid' in values: 

798 raise exception.Invalid( 

799 message=_("Cannot overwrite UUID for an existing Action.")) 

800 

801 return self._do_update_action(action_id, values) 

802 

803 @staticmethod 

804 def _do_update_action(action_id, values): 

805 with _session_for_write() as session: 

806 query = session.query(models.Action) 

807 query = add_identity_filter(query, action_id) 

808 try: 

809 ref = query.with_for_update().one() 

810 except exc.NoResultFound: 

811 raise exception.ActionNotFound(action=action_id) 

812 

813 ref.update(values) 

814 return ref 

815 

816 def soft_delete_action(self, action_id): 

817 try: 

818 return self._soft_delete(models.Action, action_id) 

819 except exception.ResourceNotFound: 

820 raise exception.ActionNotFound(action=action_id) 

821 

822 # ### ACTION PLANS ### # 

823 

824 def get_action_plan_list(self, *args, **kwargs): 

825 return self._get_model_list(models.ActionPlan, 

826 self._add_action_plans_filters, 

827 *args, **kwargs) 

828 

829 def create_action_plan(self, values): 

830 # ensure defaults are present for new audits 

831 if not values.get('uuid'): 

832 values['uuid'] = utils.generate_uuid() 

833 

834 try: 

835 action_plan = self._create(models.ActionPlan, values) 

836 except db_exc.DBDuplicateEntry: 

837 raise exception.ActionPlanAlreadyExists(uuid=values['uuid']) 

838 return action_plan 

839 

840 def _get_action_plan(self, context, fieldname, value, eager): 

841 try: 

842 return self._get(context, model=models.ActionPlan, 

843 fieldname=fieldname, value=value, eager=eager) 

844 except exception.ResourceNotFound: 

845 raise exception.ActionPlanNotFound(action_plan=value) 

846 

847 def get_action_plan_by_id(self, context, action_plan_id, eager=False): 

848 return self._get_action_plan( 

849 context, fieldname="id", value=action_plan_id, eager=eager) 

850 

851 def get_action_plan_by_uuid(self, context, action_plan_uuid, eager=False): 

852 return self._get_action_plan( 

853 context, fieldname="uuid", value=action_plan_uuid, eager=eager) 

854 

855 def destroy_action_plan(self, action_plan_id): 

856 def is_action_plan_referenced(session, action_plan_id): 

857 """Checks whether the action_plan is referenced by action(s).""" 

858 query = session.query(models.Action) 

859 query = self._add_actions_filters( 

860 query, {'action_plan_id': action_plan_id}) 

861 return query.count() != 0 

862 

863 with _session_for_write() as session: 

864 query = session.query(models.ActionPlan) 

865 query = add_identity_filter(query, action_plan_id) 

866 

867 try: 

868 action_plan_ref = query.one() 

869 except exc.NoResultFound: 

870 raise exception.ActionPlanNotFound(action_plan=action_plan_id) 

871 

872 if is_action_plan_referenced(session, action_plan_ref['id']): 

873 raise exception.ActionPlanReferenced( 

874 action_plan=action_plan_id) 

875 

876 query.delete() 

877 

878 def update_action_plan(self, action_plan_id, values): 

879 if 'uuid' in values: 

880 raise exception.Invalid( 

881 message=_("Cannot overwrite UUID for an existing " 

882 "Action Plan.")) 

883 

884 return self._do_update_action_plan(action_plan_id, values) 

885 

886 @staticmethod 

887 def _do_update_action_plan(action_plan_id, values): 

888 with _session_for_write() as session: 

889 query = session.query(models.ActionPlan) 

890 query = add_identity_filter(query, action_plan_id) 

891 try: 

892 ref = query.with_for_update().one() 

893 except exc.NoResultFound: 

894 raise exception.ActionPlanNotFound(action_plan=action_plan_id) 

895 

896 ref.update(values) 

897 return ref 

898 

899 def soft_delete_action_plan(self, action_plan_id): 

900 try: 

901 return self._soft_delete(models.ActionPlan, action_plan_id) 

902 except exception.ResourceNotFound: 

903 raise exception.ActionPlanNotFound(action_plan=action_plan_id) 

904 

905 # ### EFFICACY INDICATORS ### # 

906 

907 def get_efficacy_indicator_list(self, *args, **kwargs): 

908 eff_ind_models = self._get_model_list( 

909 models.EfficacyIndicator, 

910 self._add_efficacy_indicators_filters, 

911 *args, **kwargs 

912 ) 

913 for indicator in eff_ind_models: 

914 if indicator.data is not None: 

915 # jgilaber: use the data value since it stores the value 

916 # properly, see https://bugs.launchpad.net/watcher/+bug/2103458 

917 # for more details 

918 indicator.value = indicator.data 

919 else: 

920 # if data is None, it means that we're reading data from a 

921 # database created before the 15f7375ca737 revision, use the 

922 # value column 

923 indicator.data = indicator.value 

924 # store the new column in the database 

925 self._update(models.EfficacyIndicator, 

926 indicator.id, indicator) 

927 

928 return eff_ind_models 

929 

930 def create_efficacy_indicator(self, values): 

931 # ensure defaults are present for new efficacy indicators 

932 if not values.get('uuid'): 

933 values['uuid'] = utils.generate_uuid() 

934 

935 # jgilaber: use the data column since it stores the value 

936 # properly, see https://bugs.launchpad.net/watcher/+bug/2103458 

937 # for more details 

938 values['data'] = values.get('value') 

939 try: 

940 efficacy_indicator = self._create(models.EfficacyIndicator, values) 

941 except db_exc.DBDuplicateEntry: 

942 raise exception.EfficacyIndicatorAlreadyExists(uuid=values['uuid']) 

943 return efficacy_indicator 

944 

945 def _get_efficacy_indicator(self, context, fieldname, value, eager): 

946 try: 

947 efficacy_indicator = self._get(context, 

948 model=models.EfficacyIndicator, 

949 fieldname=fieldname, 

950 value=value, eager=eager) 

951 

952 if efficacy_indicator.data is not None: 

953 # jgilaber: use the data value since it stores the value 

954 # properly, see https://bugs.launchpad.net/watcher/+bug/2103458 

955 # for more details 

956 efficacy_indicator.value = efficacy_indicator.data 

957 else: 

958 # if data is None, it means that we're reading data from a 

959 # database created before the 15f7375ca737 revision, use the 

960 # value column 

961 efficacy_indicator.data = efficacy_indicator.value 

962 # store the new column in the database 

963 self._update(models.EfficacyIndicator, efficacy_indicator.id, 

964 efficacy_indicator) 

965 return efficacy_indicator 

966 except exception.ResourceNotFound: 

967 raise exception.EfficacyIndicatorNotFound(efficacy_indicator=value) 

968 

969 def get_efficacy_indicator_by_id(self, context, efficacy_indicator_id, 

970 eager=False): 

971 return self._get_efficacy_indicator( 

972 context, fieldname="id", 

973 value=efficacy_indicator_id, eager=eager) 

974 

975 def get_efficacy_indicator_by_uuid(self, context, efficacy_indicator_uuid, 

976 eager=False): 

977 return self._get_efficacy_indicator( 

978 context, fieldname="uuid", 

979 value=efficacy_indicator_uuid, eager=eager) 

980 

981 def get_efficacy_indicator_by_name(self, context, efficacy_indicator_name, 

982 eager=False): 

983 return self._get_efficacy_indicator( 

984 context, fieldname="name", 

985 value=efficacy_indicator_name, eager=eager) 

986 

987 def update_efficacy_indicator(self, efficacy_indicator_id, values): 

988 if 'uuid' in values: 

989 raise exception.Invalid( 

990 message=_("Cannot overwrite UUID for an existing " 

991 "efficacy indicator.")) 

992 

993 try: 

994 return self._update( 

995 models.EfficacyIndicator, efficacy_indicator_id, values) 

996 except exception.ResourceNotFound: 

997 raise exception.EfficacyIndicatorNotFound( 

998 efficacy_indicator=efficacy_indicator_id) 

999 

1000 def soft_delete_efficacy_indicator(self, efficacy_indicator_id): 

1001 try: 

1002 return self._soft_delete( 

1003 models.EfficacyIndicator, efficacy_indicator_id) 

1004 except exception.ResourceNotFound: 

1005 raise exception.EfficacyIndicatorNotFound( 

1006 efficacy_indicator=efficacy_indicator_id) 

1007 

1008 def destroy_efficacy_indicator(self, efficacy_indicator_id): 

1009 try: 

1010 return self._destroy( 

1011 models.EfficacyIndicator, efficacy_indicator_id) 

1012 except exception.ResourceNotFound: 

1013 raise exception.EfficacyIndicatorNotFound( 

1014 efficacy_indicator=efficacy_indicator_id) 

1015 

1016 # ### SCORING ENGINES ### # 

1017 

1018 def get_scoring_engine_list(self, *args, **kwargs): 

1019 return self._get_model_list(models.ScoringEngine, 

1020 self._add_scoring_engine_filters, 

1021 *args, **kwargs) 

1022 

1023 def create_scoring_engine(self, values): 

1024 # ensure defaults are present for new scoring engines 

1025 if not values.get('uuid'): 

1026 values['uuid'] = utils.generate_uuid() 

1027 

1028 try: 

1029 scoring_engine = self._create(models.ScoringEngine, values) 

1030 except db_exc.DBDuplicateEntry: 

1031 raise exception.ScoringEngineAlreadyExists(uuid=values['uuid']) 

1032 return scoring_engine 

1033 

1034 def _get_scoring_engine(self, context, fieldname, value, eager): 

1035 try: 

1036 return self._get(context, model=models.ScoringEngine, 

1037 fieldname=fieldname, value=value, eager=eager) 

1038 except exception.ResourceNotFound: 

1039 raise exception.ScoringEngineNotFound(scoring_engine=value) 

1040 

1041 def get_scoring_engine_by_id(self, context, scoring_engine_id, 

1042 eager=False): 

1043 return self._get_scoring_engine( 

1044 context, fieldname="id", value=scoring_engine_id, eager=eager) 

1045 

1046 def get_scoring_engine_by_uuid(self, context, scoring_engine_uuid, 

1047 eager=False): 

1048 return self._get_scoring_engine( 

1049 context, fieldname="uuid", value=scoring_engine_uuid, eager=eager) 

1050 

1051 def get_scoring_engine_by_name(self, context, scoring_engine_name, 

1052 eager=False): 

1053 return self._get_scoring_engine( 

1054 context, fieldname="name", value=scoring_engine_name, eager=eager) 

1055 

1056 def destroy_scoring_engine(self, scoring_engine_id): 

1057 try: 

1058 return self._destroy(models.ScoringEngine, scoring_engine_id) 

1059 except exception.ResourceNotFound: 

1060 raise exception.ScoringEngineNotFound( 

1061 scoring_engine=scoring_engine_id) 

1062 

1063 def update_scoring_engine(self, scoring_engine_id, values): 

1064 if 'uuid' in values: 

1065 raise exception.Invalid( 

1066 message=_("Cannot overwrite UUID for an existing " 

1067 "Scoring Engine.")) 

1068 

1069 try: 

1070 return self._update( 

1071 models.ScoringEngine, scoring_engine_id, values) 

1072 except exception.ResourceNotFound: 

1073 raise exception.ScoringEngineNotFound( 

1074 scoring_engine=scoring_engine_id) 

1075 

1076 def soft_delete_scoring_engine(self, scoring_engine_id): 

1077 try: 

1078 return self._soft_delete( 

1079 models.ScoringEngine, scoring_engine_id) 

1080 except exception.ResourceNotFound: 

1081 raise exception.ScoringEngineNotFound( 

1082 scoring_engine=scoring_engine_id) 

1083 

1084 # ### SERVICES ### # 

1085 

1086 def get_service_list(self, *args, **kwargs): 

1087 return self._get_model_list(models.Service, 

1088 self._add_services_filters, 

1089 *args, **kwargs) 

1090 

1091 def create_service(self, values): 

1092 try: 

1093 service = self._create(models.Service, values) 

1094 except db_exc.DBDuplicateEntry: 

1095 raise exception.ServiceAlreadyExists(name=values['name'], 

1096 host=values['host']) 

1097 return service 

1098 

1099 def _get_service(self, context, fieldname, value, eager): 

1100 try: 

1101 return self._get(context, model=models.Service, 

1102 fieldname=fieldname, value=value, eager=eager) 

1103 except exception.ResourceNotFound: 

1104 raise exception.ServiceNotFound(service=value) 

1105 

1106 def get_service_by_id(self, context, service_id, eager=False): 

1107 return self._get_service( 

1108 context, fieldname="id", value=service_id, eager=eager) 

1109 

1110 def get_service_by_name(self, context, service_name, eager=False): 

1111 return self._get_service( 

1112 context, fieldname="name", value=service_name, eager=eager) 

1113 

1114 def destroy_service(self, service_id): 

1115 try: 

1116 return self._destroy(models.Service, service_id) 

1117 except exception.ResourceNotFound: 

1118 raise exception.ServiceNotFound(service=service_id) 

1119 

1120 def update_service(self, service_id, values): 

1121 try: 

1122 return self._update(models.Service, service_id, values) 

1123 except exception.ResourceNotFound: 

1124 raise exception.ServiceNotFound(service=service_id) 

1125 

1126 def soft_delete_service(self, service_id): 

1127 try: 

1128 return self._soft_delete(models.Service, service_id) 

1129 except exception.ResourceNotFound: 

1130 raise exception.ServiceNotFound(service=service_id) 

1131 

1132 # ### ACTION_DESCRIPTIONS ### # 

1133 

1134 def get_action_description_list(self, *args, **kwargs): 

1135 return self._get_model_list(models.ActionDescription, 

1136 self._add_action_descriptions_filters, 

1137 *args, **kwargs) 

1138 

1139 def create_action_description(self, values): 

1140 try: 

1141 action_description = self._create(models.ActionDescription, values) 

1142 except db_exc.DBDuplicateEntry: 

1143 raise exception.ActionDescriptionAlreadyExists( 

1144 action_type=values['action_type']) 

1145 return action_description 

1146 

1147 def _get_action_description(self, context, fieldname, value, eager): 

1148 try: 

1149 return self._get(context, model=models.ActionDescription, 

1150 fieldname=fieldname, value=value, eager=eager) 

1151 except exception.ResourceNotFound: 

1152 raise exception.ActionDescriptionNotFound(action_id=value) 

1153 

1154 def get_action_description_by_id(self, context, 

1155 action_id, eager=False): 

1156 return self._get_action_description( 

1157 context, fieldname="id", value=action_id, eager=eager) 

1158 

1159 def get_action_description_by_type(self, context, 

1160 action_type, eager=False): 

1161 return self._get_action_description( 

1162 context, fieldname="action_type", value=action_type, eager=eager) 

1163 

1164 def destroy_action_description(self, action_id): 

1165 try: 

1166 return self._destroy(models.ActionDescription, action_id) 

1167 except exception.ResourceNotFound: 

1168 raise exception.ActionDescriptionNotFound( 

1169 action_id=action_id) 

1170 

1171 def update_action_description(self, action_id, values): 

1172 try: 

1173 return self._update(models.ActionDescription, 

1174 action_id, values) 

1175 except exception.ResourceNotFound: 

1176 raise exception.ActionDescriptionNotFound( 

1177 action_id=action_id) 

1178 

1179 def soft_delete_action_description(self, action_id): 

1180 try: 

1181 return self._soft_delete(models.ActionDescription, action_id) 

1182 except exception.ResourceNotFound: 

1183 raise exception.ActionDescriptionNotFound( 

1184 action_id=action_id)