api.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373
  1. # Copyright 2016 Cloudbase Solutions Srl
  2. # All Rights Reserved.
  3. from oslo_config import cfg
  4. from oslo_db import api as db_api
  5. from oslo_db import options as db_options
  6. from oslo_db.sqlalchemy import enginefacade
  7. from oslo_log import log as logging
  8. from oslo_utils import timeutils
  9. from sqlalchemy import func
  10. from sqlalchemy import or_
  11. from sqlalchemy import orm
  12. from sqlalchemy.sql import null
  13. from coriolis.db.sqlalchemy import models
  14. from coriolis import exception
  15. from coriolis import utils
  16. CONF = cfg.CONF
  17. db_options.set_defaults(CONF)
  18. LOG = logging.getLogger(__name__)
  19. _BACKEND_MAPPING = {'sqlalchemy': 'coriolis.db.sqlalchemy.api'}
  20. IMPL = db_api.DBAPI.from_config(CONF, backend_mapping=_BACKEND_MAPPING)
  21. def get_engine():
  22. return IMPL.get_engine()
  23. def get_session():
  24. return IMPL.get_session()
  25. def db_sync(engine, version=None):
  26. """Migrate the database to `version` or the most recent version."""
  27. return IMPL.db_sync(engine, version=version)
  28. def db_version(engine):
  29. """Display the current database version."""
  30. return IMPL.db_version(engine)
  31. def _session(context):
  32. return (context and context.session) or get_session()
  33. def is_user_context(context):
  34. """Indicates if the request context is a normal user."""
  35. if not context:
  36. return False
  37. if not context.user_id or not context.project_id:
  38. return False
  39. if context.is_admin:
  40. return False
  41. return True
  42. def _model_query(context, *args):
  43. session = _session(context)
  44. return session.query(*args)
  45. def _update_sqlalchemy_object_fields(obj, updateable_fields, values_to_update):
  46. """ Updates the given 'values_to_update' on the provided sqlalchemy object
  47. as long as they are included as 'updateable_fields'.
  48. :param obj: object: sqlalchemy object
  49. :param updateable_fields: list(str): list of fields which are updateable
  50. :param values_to_update: dict: dict with the key/vals to update
  51. """
  52. if not isinstance(values_to_update, dict):
  53. raise exception.InvalidInput(
  54. "Properties to update for DB object of type '%s' must be a dict, "
  55. "got the following (type %s): %s" % (
  56. type(obj), type(values_to_update), values_to_update))
  57. non_updateable_fields = set(
  58. values_to_update.keys()).difference(
  59. set(updateable_fields))
  60. if non_updateable_fields:
  61. raise exception.Conflict(
  62. "Fields %s for '%s' database cannot be updated. "
  63. "Only updateable fields are: %s" % (
  64. non_updateable_fields, type(obj), updateable_fields))
  65. for field_name, field_val in values_to_update.items():
  66. if not hasattr(obj, field_name):
  67. raise exception.InvalidInput(
  68. "No region field named '%s' to update." % field_name)
  69. setattr(obj, field_name, field_val)
  70. LOG.debug(
  71. "Successfully updated the following fields on DB object "
  72. "of type '%s': %s" % (type(obj), values_to_update.keys()))
  73. def _get_replica_schedules_filter(context, replica_id=None,
  74. schedule_id=None, expired=True):
  75. now = timeutils.utcnow()
  76. q = _soft_delete_aware_query(context, models.ReplicaSchedule)
  77. q = q.join(models.Replica)
  78. sched_filter = q.filter()
  79. if is_user_context(context):
  80. sched_filter = sched_filter.filter(
  81. models.Replica.project_id == context.tenant)
  82. if replica_id:
  83. sched_filter = sched_filter.filter(
  84. models.Replica.id == replica_id)
  85. if schedule_id:
  86. sched_filter = sched_filter.filter(
  87. models.ReplicaSchedule.id == schedule_id)
  88. if not expired:
  89. sched_filter = sched_filter.filter(
  90. or_(models.ReplicaSchedule.expiration_date == null(),
  91. models.ReplicaSchedule.expiration_date > now))
  92. return sched_filter
  93. def _soft_delete_aware_query(context, *args, **kwargs):
  94. """Query helper that accounts for context's `show_deleted` field.
  95. :param show_deleted: if True, overrides context's show_deleted field.
  96. """
  97. query = _model_query(context, *args)
  98. show_deleted = kwargs.get('show_deleted')
  99. if context and context.show_deleted:
  100. show_deleted = True
  101. if not show_deleted:
  102. query = query.filter_by(deleted_at=None)
  103. return query
  104. @enginefacade.reader
  105. def get_endpoints(context):
  106. q = _soft_delete_aware_query(context, models.Endpoint).options(
  107. orm.joinedload('mapped_regions'))
  108. if is_user_context(context):
  109. q = q.filter(
  110. models.Endpoint.project_id == context.tenant)
  111. return q.filter().all()
  112. @enginefacade.reader
  113. def get_endpoint(context, endpoint_id):
  114. q = _soft_delete_aware_query(context, models.Endpoint).options(
  115. orm.joinedload('mapped_regions'))
  116. if is_user_context(context):
  117. q = q.filter(
  118. models.Endpoint.project_id == context.tenant)
  119. return q.filter(
  120. models.Endpoint.id == endpoint_id).first()
  121. @enginefacade.writer
  122. def add_endpoint(context, endpoint):
  123. endpoint.user_id = context.user
  124. endpoint.project_id = context.tenant
  125. _session(context).add(endpoint)
  126. @enginefacade.writer
  127. def update_endpoint(context, endpoint_id, updated_values):
  128. endpoint = get_endpoint(context, endpoint_id)
  129. if not endpoint:
  130. raise exception.NotFound("Endpoint with ID '%s' found" % endpoint_id)
  131. if not isinstance(updated_values, dict):
  132. raise exception.InvalidInput(
  133. "Update payload for endpoints must be a dict. Got the following "
  134. "(type: %s): %s" % (type(updated_values), updated_values))
  135. def _try_unmap_regions(region_ids):
  136. for region_to_unmap in region_ids:
  137. try:
  138. LOG.debug(
  139. "Attempting to unmap region '%s' from endpoint '%s'",
  140. region_to_unmap, endpoint_id)
  141. delete_endpoint_region_mapping(
  142. context, endpoint_id, region_to_unmap)
  143. except Exception as ex:
  144. LOG.warn(
  145. "Exception occurred while attempting to unmap region '%s' "
  146. "from endpoint '%s'. Ignoring. Error was: %s",
  147. region_to_unmap, endpoint_id,
  148. utils.get_exception_details())
  149. newly_mapped_regions = []
  150. regions_to_unmap = []
  151. # NOTE: `.pop()` required for `_update_sqlalchemy_object_fields` call:
  152. desired_region_mappings = updated_values.pop('mapped_regions', None)
  153. if desired_region_mappings is not None:
  154. # ensure all requested regions exist:
  155. for region_id in desired_region_mappings:
  156. region = get_region(context, region_id)
  157. if not region:
  158. raise exception.NotFound(
  159. "Could not find region with ID '%s' for associating "
  160. "with endpoint '%s' during update process." % (
  161. region_id, endpoint_id))
  162. # get all existing mappings:
  163. existing_region_mappings = [
  164. mapping.region_id
  165. for mapping in get_region_mappings_for_endpoint(
  166. context, endpoint_id)]
  167. # check and add new mappings:
  168. to_map = set(
  169. desired_region_mappings).difference(set(existing_region_mappings))
  170. regions_to_unmap = set(
  171. existing_region_mappings).difference(set(desired_region_mappings))
  172. LOG.debug(
  173. "Remapping regions for endpoint '%s' from %s to %s",
  174. endpoint_id, existing_region_mappings, desired_region_mappings)
  175. region_id = None
  176. try:
  177. for region_id in to_map:
  178. mapping = models.EndpointRegionMapping()
  179. mapping.region_id = region_id
  180. mapping.endpoint_id = endpoint_id
  181. add_endpoint_region_mapping(context, mapping)
  182. newly_mapped_regions.append(region_id)
  183. except Exception as ex:
  184. LOG.warn(
  185. "Exception occurred while adding region mapping for '%s' to "
  186. "endpoint '%s'. Cleaning up created mappings (%s). Error was: "
  187. "%s", region_id, endpoint_id, newly_mapped_regions,
  188. utils.get_exception_details())
  189. _try_unmap_regions(newly_mapped_regions)
  190. raise
  191. updateable_fields = ["name", "description", "connection_info"]
  192. try:
  193. _update_sqlalchemy_object_fields(
  194. endpoint, updateable_fields, updated_values)
  195. except Exception as ex:
  196. LOG.warn(
  197. "Exception occurred while updating fields of endpoint '%s'. "
  198. "Cleaning ""up created mappings (%s). Error was: %s",
  199. endpoint_id, newly_mapped_regions, utils.get_exception_details())
  200. _try_unmap_regions(newly_mapped_regions)
  201. raise
  202. # remove all of the old region mappings:
  203. LOG.debug(
  204. "Unmapping the following regions during update of endpoint '%s': %s",
  205. endpoint_id, regions_to_unmap)
  206. _try_unmap_regions(regions_to_unmap)
  207. @enginefacade.writer
  208. def delete_endpoint(context, endpoint_id):
  209. endpoint = get_endpoint(context, endpoint_id)
  210. args = {"id": endpoint_id}
  211. if is_user_context(context):
  212. args["project_id"] = context.tenant
  213. count = _soft_delete_aware_query(context, models.Endpoint).filter_by(
  214. **args).soft_delete()
  215. if count == 0:
  216. raise exception.NotFound("0 Endpoint entries were soft deleted")
  217. # NOTE(aznashwan): many-to-many tables with soft deletion on either end of
  218. # the association are not handled properly so we must manually delete each
  219. # association ourselves:
  220. for reg in endpoint.mapped_regions:
  221. delete_endpoint_region_mapping(context, endpoint_id, reg.id)
  222. @enginefacade.reader
  223. def get_replica_tasks_executions(context, replica_id, include_tasks=False):
  224. q = _soft_delete_aware_query(context, models.TasksExecution)
  225. q = q.join(models.Replica)
  226. if include_tasks:
  227. q = _get_tasks_with_details_options(q)
  228. if is_user_context(context):
  229. q = q.filter(models.Replica.project_id == context.tenant)
  230. return q.filter(
  231. models.Replica.id == replica_id).all()
  232. @enginefacade.reader
  233. def get_replica_tasks_execution(context, replica_id, execution_id):
  234. q = _soft_delete_aware_query(context, models.TasksExecution).join(
  235. models.Replica)
  236. q = _get_tasks_with_details_options(q)
  237. if is_user_context(context):
  238. q = q.filter(models.Replica.project_id == context.tenant)
  239. return q.filter(
  240. models.Replica.id == replica_id,
  241. models.TasksExecution.id == execution_id).first()
  242. @enginefacade.writer
  243. def add_replica_tasks_execution(context, execution):
  244. if is_user_context(context):
  245. if execution.action.project_id != context.tenant:
  246. raise exception.NotAuthorized()
  247. # include deleted records
  248. max_number = _model_query(
  249. context, func.max(models.TasksExecution.number)).filter_by(
  250. action_id=execution.action.id).first()[0] or 0
  251. execution.number = max_number + 1
  252. _session(context).add(execution)
  253. @enginefacade.writer
  254. def delete_replica_tasks_execution(context, execution_id):
  255. q = _soft_delete_aware_query(context, models.TasksExecution).filter(
  256. models.TasksExecution.id == execution_id)
  257. if is_user_context(context):
  258. if not q.join(models.Replica).filter(
  259. models.Replica.project_id == context.tenant).first():
  260. raise exception.NotAuthorized()
  261. count = q.soft_delete()
  262. if count == 0:
  263. raise exception.NotFound("0 entries were soft deleted")
  264. @enginefacade.reader
  265. def get_replica_schedules(context, replica_id=None, expired=True):
  266. sched_filter = _get_replica_schedules_filter(
  267. context, replica_id=replica_id, expired=expired)
  268. return sched_filter.all()
  269. @enginefacade.reader
  270. def get_replica_schedule(context, replica_id, schedule_id, expired=True):
  271. sched_filter = _get_replica_schedules_filter(
  272. context, replica_id=replica_id, schedule_id=schedule_id,
  273. expired=expired)
  274. return sched_filter.first()
  275. @enginefacade.writer
  276. def update_replica_schedule(context, replica_id, schedule_id,
  277. updated_values, pre_update_callable=None,
  278. post_update_callable=None):
  279. # NOTE(gsamfira): we need to refactor the DB layer a bit to allow
  280. # two-phase transactions or at least allow running these functions
  281. # inside a single transaction block.
  282. schedule = get_replica_schedule(context, replica_id, schedule_id)
  283. if pre_update_callable:
  284. pre_update_callable(schedule=schedule)
  285. for val in ["schedule", "expiration_date", "enabled", "shutdown_instance"]:
  286. if val in updated_values:
  287. setattr(schedule, val, updated_values[val])
  288. if post_update_callable:
  289. # at this point nothing has really been sent to the DB,
  290. # but we may need to act upon the new changes elsewhere
  291. # before we actually commit to the database
  292. post_update_callable(context, schedule)
  293. @enginefacade.writer
  294. def delete_replica_schedule(context, replica_id,
  295. schedule_id, pre_delete_callable=None,
  296. post_delete_callable=None):
  297. # NOTE(gsamfira): we need to refactor the DB layer a bit to allow
  298. # two-phase transactions or at least allow running these functions
  299. # inside a single transaction block.
  300. q = _soft_delete_aware_query(context, models.ReplicaSchedule).filter(
  301. models.ReplicaSchedule.id == schedule_id,
  302. models.ReplicaSchedule.replica_id == replica_id)
  303. schedule = q.first()
  304. if not schedule:
  305. raise exception.NotFound(
  306. "No such schedule")
  307. if is_user_context(context):
  308. if not q.join(models.Replica).filter(
  309. models.Replica.project_id == context.tenant).first():
  310. raise exception.NotAuthorized()
  311. if pre_delete_callable:
  312. pre_delete_callable(context, schedule)
  313. count = q.soft_delete()
  314. if post_delete_callable:
  315. post_delete_callable(context, schedule)
  316. if count == 0:
  317. raise exception.NotFound("0 entries were soft deleted")
  318. @enginefacade.writer
  319. def add_replica_schedule(context, schedule, post_create_callable=None):
  320. # NOTE(gsamfira): we need to refactor the DB layer a bit to allow
  321. # two-phase transactions or at least allow running these functions
  322. # inside a single transaction block.
  323. if schedule.replica.project_id != context.tenant:
  324. raise exception.NotAuthorized()
  325. _session(context).add(schedule)
  326. if post_create_callable:
  327. post_create_callable(context, schedule)
  328. def _get_replica_with_tasks_executions_options(q):
  329. return q.options(orm.joinedload(models.Replica.executions))
  330. @enginefacade.reader
  331. def get_replicas(context,
  332. include_tasks_executions=False,
  333. include_info=False,
  334. to_dict=True):
  335. q = _soft_delete_aware_query(context, models.Replica)
  336. if include_tasks_executions:
  337. q = _get_replica_with_tasks_executions_options(q)
  338. if include_info is False:
  339. q = q.options(orm.defer('info'))
  340. q = q.filter()
  341. if is_user_context(context):
  342. q = q.filter(
  343. models.Replica.project_id == context.tenant)
  344. db_result = q.all()
  345. if to_dict:
  346. return [
  347. i.to_dict(
  348. include_info=include_info,
  349. include_executions=include_tasks_executions)
  350. for i in db_result]
  351. return db_result
  352. @enginefacade.reader
  353. def get_replica(context, replica_id):
  354. q = _soft_delete_aware_query(context, models.Replica)
  355. q = _get_replica_with_tasks_executions_options(q)
  356. if is_user_context(context):
  357. q = q.filter(
  358. models.Replica.project_id == context.tenant)
  359. return q.filter(
  360. models.Replica.id == replica_id).first()
  361. @enginefacade.reader
  362. def get_endpoint_replicas_count(context, endpoint_id):
  363. origin_args = {'origin_endpoint_id': endpoint_id}
  364. q_origin_count = _soft_delete_aware_query(
  365. context, models.Replica).filter_by(**origin_args).count()
  366. destination_args = {'destination_endpoint_id': endpoint_id}
  367. q_destination_count = _soft_delete_aware_query(
  368. context, models.Replica).filter_by(**destination_args).count()
  369. return q_origin_count + q_destination_count
  370. @enginefacade.writer
  371. def add_replica(context, replica):
  372. replica.user_id = context.user
  373. replica.project_id = context.tenant
  374. _session(context).add(replica)
  375. @enginefacade.writer
  376. def _delete_transfer_action(context, cls, id):
  377. args = {"base_id": id}
  378. if is_user_context(context):
  379. args["project_id"] = context.tenant
  380. count = _soft_delete_aware_query(context, cls).filter_by(
  381. **args).soft_delete()
  382. if count == 0:
  383. raise exception.NotFound("0 entries were soft deleted")
  384. _soft_delete_aware_query(context, models.TasksExecution).filter_by(
  385. action_id=id).soft_delete()
  386. @enginefacade.writer
  387. def delete_replica(context, replica_id):
  388. _delete_transfer_action(context, models.Replica, replica_id)
  389. @enginefacade.reader
  390. def get_replica_migrations(context, replica_id):
  391. q = _soft_delete_aware_query(context, models.Migration)
  392. q = q.join("replica")
  393. q = q.options(orm.joinedload("executions"))
  394. if is_user_context(context):
  395. q = q.filter(
  396. models.Migration.project_id == context.tenant)
  397. return q.filter(
  398. models.Replica.id == replica_id).all()
  399. @enginefacade.reader
  400. def get_migrations(context, include_tasks=False,
  401. include_info=False, to_dict=True):
  402. q = _soft_delete_aware_query(context, models.Migration)
  403. if include_tasks:
  404. q = _get_migration_task_query_options(q)
  405. else:
  406. q = q.options(orm.joinedload("executions"))
  407. if include_info is False:
  408. q = q.options(orm.defer('info'))
  409. args = {}
  410. if is_user_context(context):
  411. args["project_id"] = context.tenant
  412. result = q.filter_by(**args).all()
  413. if to_dict:
  414. return [i.to_dict(
  415. include_info=include_info,
  416. include_tasks=include_tasks) for i in result]
  417. return result
  418. def _get_tasks_with_details_options(query):
  419. return query.options(
  420. orm.joinedload("action")).options(
  421. orm.joinedload("tasks").
  422. joinedload("progress_updates")).options(
  423. orm.joinedload("tasks").
  424. joinedload("events"))
  425. def _get_migration_task_query_options(query):
  426. return query.options(
  427. orm.joinedload("executions").
  428. joinedload("tasks").
  429. joinedload("progress_updates")).options(
  430. orm.joinedload("executions").
  431. joinedload("tasks").
  432. joinedload("events")).options(
  433. orm.joinedload("executions").
  434. joinedload("action"))
  435. @enginefacade.reader
  436. def get_migration(context, migration_id):
  437. q = _soft_delete_aware_query(context, models.Migration)
  438. q = _get_migration_task_query_options(q)
  439. args = {"id": migration_id}
  440. if is_user_context(context):
  441. args["project_id"] = context.tenant
  442. return q.filter_by(**args).first()
  443. @enginefacade.writer
  444. def add_migration(context, migration):
  445. migration.user_id = context.user
  446. migration.project_id = context.tenant
  447. _session(context).add(migration)
  448. @enginefacade.writer
  449. def delete_migration(context, migration_id):
  450. _delete_transfer_action(context, models.Migration, migration_id)
  451. @enginefacade.writer
  452. def set_execution_status(
  453. context, execution_id, status, update_action_status=True):
  454. execution = _soft_delete_aware_query(
  455. context, models.TasksExecution).join(
  456. models.TasksExecution.action)
  457. if is_user_context(context):
  458. execution = execution.filter(
  459. models.BaseTransferAction.project_id == context.tenant)
  460. execution = execution.filter(
  461. models.TasksExecution.id == execution_id).first()
  462. if not execution:
  463. raise exception.NotFound(
  464. "Tasks execution not found: %s" % execution_id)
  465. execution.status = status
  466. if update_action_status:
  467. set_action_last_execution_status(
  468. context, execution.action_id, status)
  469. return execution
  470. @enginefacade.reader
  471. def get_action(context, action_id):
  472. action = _soft_delete_aware_query(
  473. context, models.BaseTransferAction)
  474. if is_user_context(context):
  475. action = action.filter(
  476. models.BaseTransferAction.project_id == context.tenant)
  477. action = action.filter(
  478. models.BaseTransferAction.base_id == action_id).first()
  479. if not action:
  480. raise exception.NotFound(
  481. "Transfer action not found: %s" % action_id)
  482. return action
  483. @enginefacade.writer
  484. def set_action_last_execution_status(
  485. context, action_id, last_execution_status):
  486. action = get_action(context, action_id)
  487. action.last_execution_status = last_execution_status
  488. @enginefacade.writer
  489. def update_transfer_action_info_for_instance(
  490. context, action_id, instance, new_instance_info):
  491. """ Updates the info for the given action with the provided dict.
  492. Returns the updated value.
  493. Sub-fields of the dict already in the info will get overwritten entirely!
  494. """
  495. action = get_action(context, action_id)
  496. if not new_instance_info:
  497. LOG.debug(
  498. "No new info provided for action '%s' and instance '%s'. "
  499. "Nothing to update in the DB.",
  500. action_id, instance)
  501. return action.info.get(instance, {})
  502. # Copy is needed, otherwise sqlalchemy won't save the changes
  503. action_info = action.info.copy()
  504. instance_info_old = {}
  505. if instance in action_info:
  506. instance_info_old = action_info[instance]
  507. old_keys = set(instance_info_old.keys())
  508. new_keys = set(new_instance_info.keys())
  509. overwritten_keys = old_keys.intersection(new_keys)
  510. if overwritten_keys:
  511. LOG.debug(
  512. "Overwriting the values of the following keys for info of "
  513. "instance '%s' of action with ID '%s': %s",
  514. instance, action_id, overwritten_keys)
  515. newly_added_keys = new_keys.difference(old_keys)
  516. if newly_added_keys:
  517. LOG.debug(
  518. "The following new keys will be added for info of instance "
  519. "'%s' in action with ID '%s': %s",
  520. instance, action_id, newly_added_keys)
  521. instance_info_old_copy = instance_info_old.copy()
  522. instance_info_old_copy.update(new_instance_info)
  523. action_info[instance] = instance_info_old_copy
  524. action.info = action_info
  525. return action_info[instance]
  526. @enginefacade.writer
  527. def set_transfer_action_result(context, action_id, instance, result):
  528. """ Adds the result for the given 'instance' in the 'transfer_result'
  529. JSON in the 'base_transfer_action' table.
  530. """
  531. action = get_action(context, action_id)
  532. transfer_result = {}
  533. if action.transfer_result:
  534. transfer_result = action.transfer_result.copy()
  535. transfer_result[instance] = result
  536. action.transfer_result = transfer_result
  537. return transfer_result[instance]
  538. @enginefacade.reader
  539. def get_tasks_execution(context, execution_id):
  540. q = _soft_delete_aware_query(context, models.TasksExecution)
  541. q = q.join(models.BaseTransferAction)
  542. q = q.options(orm.joinedload("action"))
  543. q = q.options(orm.joinedload("tasks"))
  544. if is_user_context(context):
  545. q = q.filter(
  546. models.BaseTransferAction.project_id == context.tenant)
  547. execution = q.filter(
  548. models.TasksExecution.id == execution_id).first()
  549. if not execution:
  550. raise exception.NotFound(
  551. "Tasks execution not found: %s" % execution_id)
  552. return execution
  553. def _get_task(context, task_id):
  554. task = _soft_delete_aware_query(context, models.Task).filter_by(
  555. id=task_id).first()
  556. if not task:
  557. raise exception.NotFound("Task not found: %s" % task_id)
  558. return task
  559. @enginefacade.writer
  560. def set_task_status(context, task_id, status, exception_details=None):
  561. task = _get_task(context, task_id)
  562. task.status = status
  563. task.exception_details = exception_details
  564. @enginefacade.writer
  565. def set_task_host_properties(context, task_id, host=None, process_id=None):
  566. task = _get_task(context, task_id)
  567. if host:
  568. task.host = host
  569. if process_id:
  570. task.process_id = process_id
  571. @enginefacade.reader
  572. def get_task(context, task_id):
  573. q = _soft_delete_aware_query(context, models.Task)
  574. return q.filter_by(id=task_id).first()
  575. @enginefacade.writer
  576. def add_task_event(context, task_id, level, message):
  577. task_event = models.TaskEvent()
  578. task_event.task_id = task_id
  579. task_event.level = level
  580. task_event.message = message
  581. _session(context).add(task_event)
  582. @enginefacade.writer
  583. def add_minion_pool_event(context, pool_id, level, message):
  584. pool_event = models.MinionPoolEvent()
  585. pool_event.pool_id = pool_id
  586. pool_event.level = level
  587. pool_event.message = message
  588. _session(context).add(pool_event)
  589. def _get_minion_pool_progress_update(context, pool_id, current_step):
  590. q = _soft_delete_aware_query(context, models.MinionPoolProgressUpdate)
  591. return q.filter(
  592. models.MinionPoolProgressUpdate.pool_id == pool_id,
  593. models.MinionPoolProgressUpdate.current_step == current_step).first()
  594. @enginefacade.reader
  595. def get_minion_pool_progress_step(context, pool_id):
  596. curr_step = 0
  597. q = _soft_delete_aware_query(context, models.MinionPoolProgressUpdate)
  598. last_step = q.filter(
  599. models.MinionPoolProgressUpdate.pool_id == pool_id).order_by(
  600. models.MinionPoolProgressUpdate.current_step.desc()).first()
  601. if last_step:
  602. curr_step = last_step.current_step
  603. return curr_step
  604. @enginefacade.writer
  605. def add_minion_pool_progress_update(context, pool_id, total_steps, message):
  606. current_step = get_minion_pool_progress_step(context, pool_id) + 1
  607. pool_progress_update = models.MinionPoolProgressUpdate(
  608. pool_id=pool_id, current_step=current_step, total_steps=total_steps,
  609. message=message)
  610. _session(context).add(pool_progress_update)
  611. @enginefacade.writer
  612. def update_minion_pool_progress_update(
  613. context, pool_id, step, total_steps, message):
  614. pool_progress_update = _get_minion_pool_progress_update(
  615. context, pool_id, step)
  616. if not pool_progress_update:
  617. pool_progress_update = models.MinionPoolProgressUpdate(
  618. pool_id=pool_id, current_step=step, total_steps=total_steps,
  619. message=message)
  620. _session(context).add(pool_progress_update)
  621. pool_progress_update.pool_id = pool_id
  622. pool_progress_update.current_step = step
  623. pool_progress_update.total_steps = total_steps
  624. pool_progress_update.message = message
  625. def _get_progress_update(context, task_id, current_step):
  626. q = _soft_delete_aware_query(context, models.TaskProgressUpdate)
  627. return q.filter(
  628. models.TaskProgressUpdate.task_id == task_id,
  629. models.TaskProgressUpdate.current_step == current_step).first()
  630. @enginefacade.reader
  631. def get_task_progress_step(context, task_id):
  632. curr_step = 0
  633. q = _soft_delete_aware_query(context, models.TaskProgressUpdate)
  634. last_step = q.filter(
  635. models.TaskProgressUpdate.task_id == task_id).order_by(
  636. models.TaskProgressUpdate.current_step.desc()).first()
  637. if last_step:
  638. curr_step = last_step.current_step
  639. return curr_step
  640. @enginefacade.writer
  641. def add_task_progress_update(context, task_id, total_steps, message):
  642. current_step = get_task_progress_step(context, task_id) + 1
  643. task_progress_update = models.TaskProgressUpdate(
  644. task_id=task_id, current_step=current_step, total_steps=total_steps,
  645. message=message)
  646. _session(context).add(task_progress_update)
  647. @enginefacade.writer
  648. def update_task_progress_update(context, task_id, step, total_steps, message):
  649. task_progress_update = _get_progress_update(context, task_id, step)
  650. if not task_progress_update:
  651. task_progress_update = models.TaskProgressUpdate(
  652. task_id=task_id, current_step=step, total_steps=total_steps,
  653. message=message)
  654. _session(context).add(task_progress_update)
  655. task_progress_update.task_id = task_id
  656. task_progress_update.current_step = step
  657. task_progress_update.total_steps = total_steps
  658. task_progress_update.message = message
  659. @enginefacade.writer
  660. def update_replica(context, replica_id, updated_values):
  661. replica = get_replica(context, replica_id)
  662. if not replica:
  663. raise exception.NotFound("Replica not found")
  664. mapped_info_fields = {
  665. 'destination_environment': 'target_environment'}
  666. updateable_fields = [
  667. "source_environment", "destination_environment", "notes",
  668. "network_map", "storage_mappings",
  669. "origin_minion_pool_id", "destination_minion_pool_id",
  670. "instance_osmorphing_minion_pool_mappings"]
  671. for field in updateable_fields:
  672. if mapped_info_fields.get(field, field) in updated_values:
  673. LOG.debug(
  674. "Updating the '%s' field of Replica '%s' to: '%s'",
  675. field, replica_id, updated_values[
  676. mapped_info_fields.get(field, field)])
  677. setattr(
  678. replica, field,
  679. updated_values[mapped_info_fields.get(field, field)])
  680. non_updateable_fields = set(
  681. updated_values.keys()).difference({
  682. mapped_info_fields.get(field, field)
  683. for field in updateable_fields})
  684. if non_updateable_fields:
  685. LOG.warn(
  686. "The following Replica fields can NOT be updated: %s",
  687. non_updateable_fields)
  688. # the oslo_db library uses this method for both the `created_at` and
  689. # `updated_at` fields
  690. setattr(replica, 'updated_at', timeutils.utcnow())
  691. @enginefacade.writer
  692. def add_region(context, region):
  693. _session(context).add(region)
  694. @enginefacade.reader
  695. def get_regions(context):
  696. q = _soft_delete_aware_query(context, models.Region)
  697. q = q.options(orm.joinedload('mapped_endpoints'))
  698. q = q.options(orm.joinedload('mapped_services'))
  699. return q.all()
  700. @enginefacade.reader
  701. def get_region(context, region_id):
  702. q = _soft_delete_aware_query(context, models.Region)
  703. q = q.options(orm.joinedload('mapped_endpoints'))
  704. q = q.options(orm.joinedload('mapped_services'))
  705. return q.filter(
  706. models.Region.id == region_id).first()
  707. @enginefacade.writer
  708. def update_region(context, region_id, updated_values):
  709. if not region_id:
  710. raise exception.InvalidInput(
  711. "No region ID specified for updating.")
  712. region = get_region(context, region_id)
  713. if not region:
  714. raise exception.NotFound(
  715. "Region with ID '%s' does not exist." % region_id)
  716. updateable_fields = ["name", "description", "enabled"]
  717. _update_sqlalchemy_object_fields(
  718. region, updateable_fields, updated_values)
  719. @enginefacade.writer
  720. def delete_region(context, region_id):
  721. region = get_region(context, region_id)
  722. count = _soft_delete_aware_query(context, models.Region).filter_by(
  723. id=region_id).soft_delete()
  724. if count == 0:
  725. raise exception.NotFound("0 region entries were soft deleted")
  726. # NOTE(aznashwan): many-to-many tables with soft deletion on either end of
  727. # the association are not handled properly so we must manually delete each
  728. # association ourselves:
  729. for endp in region.mapped_endpoints:
  730. delete_endpoint_region_mapping(context, endp.id, region_id)
  731. for svc in region.mapped_services:
  732. delete_service_region_mapping(context, svc.id, region_id)
  733. @enginefacade.writer
  734. def add_endpoint_region_mapping(context, endpoint_region_mapping):
  735. region_id = endpoint_region_mapping.region_id
  736. endpoint_id = endpoint_region_mapping.endpoint_id
  737. if None in [region_id, endpoint_id]:
  738. raise exception.InvalidInput(
  739. "Provided endpoint region mapping params for the region ID "
  740. "('%s') and the endpoint ID ('%s') must both be non-null." % (
  741. region_id, endpoint_id))
  742. _session(context).add(endpoint_region_mapping)
  743. @enginefacade.reader
  744. def get_endpoint_region_mapping(context, endpoint_id, region_id):
  745. q = _soft_delete_aware_query(context, models.EndpointRegionMapping)
  746. q = q.filter(
  747. models.EndpointRegionMapping.region == region_id)
  748. q = q.filter(
  749. models.EndpointRegionMapping.endpoint_id == endpoint_id)
  750. return q.all()
  751. @enginefacade.writer
  752. def delete_endpoint_region_mapping(context, endpoint_id, region_id):
  753. args = {"endpoint_id": endpoint_id, "region_id": region_id}
  754. # TODO(aznashwan): many-to-many realtionships have no sane way of
  755. # supporting soft deletion from the sqlalchemy layer wihout
  756. # writing join condictions, so we hard-`delete()` instead of
  757. # `soft_delete()` util we find a better option:
  758. count = _soft_delete_aware_query(
  759. context, models.EndpointRegionMapping).filter_by(
  760. **args).delete()
  761. if count == 0:
  762. raise exception.NotFound(
  763. "There is no mapping between endpoint '%s' and region '%s'." % (
  764. endpoint_id, region_id))
  765. LOG.debug(
  766. "Deleted mapping between endpoint '%s' and region '%s' from DB",
  767. endpoint_id, region_id)
  768. @enginefacade.reader
  769. def get_region_mappings_for_endpoint(
  770. context, endpoint_id, enabled_regions_only=False):
  771. q = _soft_delete_aware_query(context, models.EndpointRegionMapping)
  772. q = q.join(models.Region)
  773. q = q.filter(
  774. models.EndpointRegionMapping.endpoint_id == endpoint_id)
  775. if enabled_regions_only:
  776. q = q.filter(
  777. models.Region.enabled == True)
  778. return q.all()
  779. @enginefacade.reader
  780. def get_mapped_endpoints_for_region(context, region_id):
  781. q = _soft_delete_aware_query(context, models.Endpoint)
  782. q = q.join(models.EndpointRegionMapping)
  783. q = q.filter(
  784. models.EndpointRegionMapping.endpoint_id == region_id)
  785. return q.all()
  786. @enginefacade.writer
  787. def add_service(context, service):
  788. _session(context).add(service)
  789. @enginefacade.reader
  790. def get_services(context):
  791. q = _soft_delete_aware_query(context, models.Service).options(
  792. orm.joinedload('mapped_regions'))
  793. return q.all()
  794. @enginefacade.reader
  795. def get_service(context, service_id):
  796. q = _soft_delete_aware_query(context, models.Service).options(
  797. orm.joinedload('mapped_regions'))
  798. return q.filter(
  799. models.Service.id == service_id).first()
  800. @enginefacade.reader
  801. def find_service(context, host, binary, topic=None):
  802. args = {"host": host, "binary": binary}
  803. if topic:
  804. args["topic"] = topic
  805. q = _soft_delete_aware_query(context, models.Service).options(
  806. orm.joinedload('mapped_regions')).filter_by(**args)
  807. return q.first()
  808. @enginefacade.writer
  809. def update_service(context, service_id, updated_values):
  810. if not service_id:
  811. raise exception.InvalidInput(
  812. "No service ID specified for updating.")
  813. service = get_service(context, service_id)
  814. if not service:
  815. raise exception.NotFound(
  816. "Service with ID '%s' does not exist." % service_id)
  817. if not isinstance(updated_values, dict):
  818. raise exception.InvalidInput(
  819. "Update payload for services must be a dict. Got the following "
  820. "(type: %s): %s" % (type(updated_values), updated_values))
  821. def _try_unmap_regions(region_ids):
  822. for region_to_unmap in region_ids:
  823. try:
  824. LOG.debug(
  825. "Attempting to unmap region '%s' from service '%s'",
  826. region_to_unmap, service_id)
  827. delete_service_region_mapping(
  828. context, service_id, region_to_unmap)
  829. except Exception as ex:
  830. LOG.warn(
  831. "Exception occurred while attempting to unmap region '%s' "
  832. "from service '%s'. Ignoring. Error was: %s",
  833. region_to_unmap, service_id,
  834. utils.get_exception_details())
  835. newly_mapped_regions = []
  836. regions_to_unmap = []
  837. # NOTE: `.pop()` required for `_update_sqlalchemy_object_fields` call:
  838. desired_region_mappings = updated_values.pop('mapped_regions', None)
  839. if desired_region_mappings is not None:
  840. # ensure all requested regions exist:
  841. for region_id in desired_region_mappings:
  842. region = get_region(context, region_id)
  843. if not region:
  844. raise exception.NotFound(
  845. "Could not find region with ID '%s' for associating "
  846. "with serce '%s' during update process." % (
  847. region_id, service_id))
  848. # get all existing mappings:
  849. existing_region_mappings = [
  850. mapping.region_id
  851. for mapping in get_region_mappings_for_service(
  852. context, service_id)]
  853. # check and add new mappings:
  854. to_map = set(
  855. desired_region_mappings).difference(set(existing_region_mappings))
  856. regions_to_unmap = set(
  857. existing_region_mappings).difference(set(desired_region_mappings))
  858. LOG.debug(
  859. "Remapping regions for service '%s' from %s to %s",
  860. service_id, existing_region_mappings, desired_region_mappings)
  861. region_id = None
  862. try:
  863. for region_id in to_map:
  864. mapping = models.ServiceRegionMapping()
  865. mapping.region_id = region_id
  866. mapping.service_id = service_id
  867. add_service_region_mapping(context, mapping)
  868. newly_mapped_regions.append(region_id)
  869. except Exception as ex:
  870. LOG.warn(
  871. "Exception occurred while adding region mapping for '%s' to "
  872. "service '%s'. Cleaning up created mappings (%s). Error was: "
  873. "%s", region_id, service_id, newly_mapped_regions,
  874. utils.get_exception_details())
  875. _try_unmap_regions(newly_mapped_regions)
  876. raise
  877. updateable_fields = ["enabled", "status", "providers", "specs"]
  878. try:
  879. _update_sqlalchemy_object_fields(
  880. service, updateable_fields, updated_values)
  881. except Exception as ex:
  882. LOG.warn(
  883. "Exception occurred while updating fields of service '%s'. "
  884. "Cleaning ""up created mappings (%s). Error was: %s",
  885. service_id, newly_mapped_regions, utils.get_exception_details())
  886. _try_unmap_regions(newly_mapped_regions)
  887. raise
  888. # remove all of the old region mappings:
  889. LOG.debug(
  890. "Unmapping the following regions during update of service '%s': %s",
  891. service_id, regions_to_unmap)
  892. _try_unmap_regions(regions_to_unmap)
  893. @enginefacade.writer
  894. def delete_service(context, service_id):
  895. service = get_service(context, service_id)
  896. count = _soft_delete_aware_query(context, models.Service).filter_by(
  897. id=service_id).soft_delete()
  898. if count == 0:
  899. raise exception.NotFound("0 service entries were soft deleted")
  900. # NOTE(aznashwan): many-to-many tables with soft deletion on either end of
  901. # the association are not handled properly so we must manually delete each
  902. # association ourselves:
  903. for reg in service.mapped_regions:
  904. delete_service_region_mapping(context, service_id, reg.id)
  905. @enginefacade.writer
  906. def add_service_region_mapping(context, service_region_mapping):
  907. region_id = service_region_mapping.region_id
  908. service_id = service_region_mapping.service_id
  909. if None in [region_id, service_id]:
  910. raise exception.InvalidInput(
  911. "Provided service region mapping params for the region ID "
  912. "('%s') and the service ID ('%s') must both be non-null." % (
  913. region_id, service_id))
  914. _session(context).add(service_region_mapping)
  915. @enginefacade.reader
  916. def get_service_region_mapping(context, service_id, region_id):
  917. q = _soft_delete_aware_query(context, models.ServiceRegionMapping)
  918. q = q.filter(
  919. models.ServiceRegionMapping.region == region_id)
  920. q = q.filter(
  921. models.ServiceRegionMapping.service_id == service_id)
  922. return q.all()
  923. @enginefacade.writer
  924. def delete_service_region_mapping(context, service_id, region_id):
  925. args = {"service_id": service_id, "region_id": region_id}
  926. # TODO(aznashwan): many-to-many realtionships have no sane way of
  927. # supporting soft deletion from the sqlalchemy layer wihout
  928. # writing join condictions, so we hard-`delete()` instead of
  929. # `soft_delete()` util we find a better option:
  930. count = _soft_delete_aware_query(
  931. context, models.ServiceRegionMapping).filter_by(
  932. **args).delete()
  933. if count == 0:
  934. raise exception.NotFound(
  935. "There is no mapping between service '%s' and region '%s'." % (
  936. service_id, region_id))
  937. @enginefacade.reader
  938. def get_region_mappings_for_service(
  939. context, service_id, enabled_regions_only=False):
  940. q = _soft_delete_aware_query(context, models.ServiceRegionMapping)
  941. q = q.join(models.Region)
  942. q = q.filter(
  943. models.ServiceRegionMapping.service_id == service_id)
  944. if enabled_regions_only:
  945. q = q.filter(
  946. models.Region.enabled == True)
  947. return q.all()
  948. @enginefacade.reader
  949. def get_mapped_services_for_region(context, region_id):
  950. q = _soft_delete_aware_query(context, models.Service)
  951. q = q.join(models.ServiceRegionMapping)
  952. q = q.filter(
  953. models.ServiceRegionMapping.service_id == region_id)
  954. return q.all()
  955. @enginefacade.writer
  956. def add_minion_machine(context, minion_machine):
  957. minion_machine.user_id = context.user
  958. minion_machine.project_id = context.tenant
  959. _session(context).add(minion_machine)
  960. @enginefacade.reader
  961. def get_minion_machines(context, allocated_action_id=None):
  962. q = _soft_delete_aware_query(context, models.MinionMachine)
  963. if allocated_action_id:
  964. q = q.filter(
  965. models.MinionMachine.allocated_action == allocated_action_id)
  966. return q.all()
  967. @enginefacade.reader
  968. def get_minion_machine(context, minion_machine_id):
  969. q = _soft_delete_aware_query(context, models.MinionMachine)
  970. return q.filter(
  971. models.MinionMachine.id == minion_machine_id).first()
  972. @enginefacade.writer
  973. def update_minion_machine(context, minion_machine_id, updated_values):
  974. if not minion_machine_id:
  975. raise exception.InvalidInput(
  976. "No minion_machine ID specified for updating.")
  977. minion_machine = get_minion_machine(context, minion_machine_id)
  978. if not minion_machine:
  979. raise exception.NotFound(
  980. "MinionMachine with ID '%s' does not exist." % minion_machine_id)
  981. updateable_fields = [
  982. "connection_info", "provider_properties", "status",
  983. "backup_writer_connection_info", "allocated_action",
  984. "allocated_at"]
  985. _update_sqlalchemy_object_fields(
  986. minion_machine, updateable_fields, updated_values)
  987. @enginefacade.writer
  988. def set_minion_machines_allocation_statuses(
  989. context, minion_machine_ids, action_id, allocation_status,
  990. refresh_allocation_time=True):
  991. machines = get_minion_machines(context)
  992. existing_machine_id_mappings = {
  993. machine.id: machine for machine in machines}
  994. missing = [
  995. mid for mid in minion_machine_ids
  996. if mid not in existing_machine_id_mappings]
  997. if missing:
  998. raise exception.NotFound(
  999. "The following minion machines could not be found: %s" % (
  1000. missing))
  1001. for machine_id in minion_machine_ids:
  1002. machine = existing_machine_id_mappings[machine_id]
  1003. LOG.debug(
  1004. "Changing allocation status in DB for minion machine '%s' "
  1005. "from '%s' to '%s' and allocated action from '%s' to '%s'" % (
  1006. machine.id, machine.status, allocation_status,
  1007. machine.allocated_action, action_id))
  1008. machine.allocated_action = action_id
  1009. if refresh_allocation_time:
  1010. machine.allocated_at = timeutils.utcnow()
  1011. machine.status = allocation_status
  1012. @enginefacade.writer
  1013. def delete_minion_machine(context, minion_machine_id):
  1014. minion_machine = get_minion_machine(context, minion_machine_id)
  1015. # TODO(aznashwan): update models to be soft-delete-aware to
  1016. # avoid needing to hard-delete here:
  1017. count = _soft_delete_aware_query(context, models.MinionMachine).filter_by(
  1018. id=minion_machine_id).delete()
  1019. if count == 0:
  1020. raise exception.NotFound("0 MinionMachine entries were soft deleted")
  1021. @enginefacade.writer
  1022. def add_minion_pool(context, minion_pool):
  1023. minion_pool.user_id = context.user
  1024. minion_pool.project_id = context.tenant
  1025. _session(context).add(minion_pool)
  1026. @enginefacade.writer
  1027. def delete_minion_pool(context, minion_pool_id):
  1028. args = {"id": minion_pool_id}
  1029. if is_user_context(context):
  1030. args["project_id"] = context.tenant
  1031. count = _soft_delete_aware_query(context, models.MinionPool).filter_by(
  1032. **args).soft_delete()
  1033. if count == 0:
  1034. raise exception.NotFound("0 entries were soft deleted")
  1035. @enginefacade.reader
  1036. def get_minion_pool(
  1037. context, minion_pool_id, include_machines=True, include_events=True,
  1038. include_progress_updates=True):
  1039. q = _soft_delete_aware_query(context, models.MinionPool)
  1040. if include_machines:
  1041. q = q.options(orm.joinedload('minion_machines'))
  1042. if include_events:
  1043. q = q.options(orm.joinedload('events'))
  1044. if include_progress_updates:
  1045. q = q.options(orm.joinedload('progress_updates'))
  1046. if is_user_context(context):
  1047. q = q.filter(
  1048. models.MinionPool.project_id == context.tenant)
  1049. return q.filter(
  1050. models.MinionPool.id == minion_pool_id).first()
  1051. @enginefacade.reader
  1052. def get_minion_pools(
  1053. context, include_machines=False, include_events=False,
  1054. include_progress_updates=False, to_dict=True):
  1055. q = _soft_delete_aware_query(context, models.MinionPool)
  1056. q = q.filter()
  1057. if is_user_context(context):
  1058. q = q.filter(
  1059. models.MinionPool.project_id == context.tenant)
  1060. if include_machines:
  1061. q = q.options(orm.joinedload('minion_machines'))
  1062. if include_events:
  1063. q = q.options(orm.joinedload('events'))
  1064. if include_progress_updates:
  1065. q = q.options(orm.joinedload('progress_updates'))
  1066. db_result = q.all()
  1067. if to_dict:
  1068. return [
  1069. i.to_dict(
  1070. include_machines=include_machines,
  1071. include_events=include_events,
  1072. include_progress_updates=include_progress_updates)
  1073. for i in db_result]
  1074. return db_result
  1075. @enginefacade.writer
  1076. def add_minion_pool_execution(context, execution):
  1077. if is_user_context(context):
  1078. if execution.action.project_id != context.tenant:
  1079. raise exception.NotAuthorized()
  1080. # include deleted records
  1081. max_number = _model_query(
  1082. context, func.max(models.TasksExecution.number)).filter_by(
  1083. action_id=execution.action.id).first()[0] or 0
  1084. execution.number = max_number + 1
  1085. _session(context).add(execution)
  1086. @enginefacade.writer
  1087. def set_minion_pool_status(context, minion_pool_id, status):
  1088. pool = get_minion_pool(
  1089. context, minion_pool_id, include_machines=False)
  1090. LOG.debug(
  1091. "Transitioning minion pool '%s' from status '%s' to '%s'in DB",
  1092. minion_pool_id, pool.status, status)
  1093. pool.status = status
  1094. setattr(pool, 'updated_at', timeutils.utcnow())
  1095. @enginefacade.writer
  1096. def update_minion_pool(context, minion_pool_id, updated_values):
  1097. lifecycle = get_minion_pool(
  1098. context, minion_pool_id, include_machines=False)
  1099. if not lifecycle:
  1100. raise exception.NotFound(
  1101. "Minion pool '%s' not found" % minion_pool_id)
  1102. updateable_fields = [
  1103. "minimum_minions", "maximum_minions", "minion_max_idle_time",
  1104. "minion_retention_strategy", "environment_options",
  1105. "shared_resources", "notes", "name", "os_type"]
  1106. for field in updateable_fields:
  1107. if field in updated_values:
  1108. LOG.debug(
  1109. "Updating the '%s' field of Minion Pool '%s' to: '%s'",
  1110. field, minion_pool_id, updated_values[field])
  1111. setattr(lifecycle, field, updated_values[field])
  1112. non_updateable_fields = set(
  1113. updated_values.keys()).difference(updateable_fields)
  1114. if non_updateable_fields:
  1115. LOG.warn(
  1116. "The following Minion Pool fields can NOT be updated: %s",
  1117. non_updateable_fields)
  1118. # the oslo_db library uses this method for both the `created_at` and
  1119. # `updated_at` fields
  1120. setattr(lifecycle, 'updated_at', timeutils.utcnow())