Fuel UI
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013 Mirantis, Inc.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. from datetime import datetime
  16. from decorator import decorator
  17. from oslo_serialization import jsonutils
  18. import six
  19. import traceback
  20. import yaml
  21. from distutils.version import StrictVersion
  22. from sqlalchemy import exc as sa_exc
  23. import web
  24. from nailgun.api.v1.validators.base import BaseDefferedTaskValidator
  25. from nailgun.api.v1.validators.base import BasicValidator
  26. from nailgun.api.v1.validators.orchestrator_graph import \
  27. GraphSolverTasksValidator
  28. from nailgun import consts
  29. from nailgun.db import db
  30. from nailgun import errors
  31. from nailgun.logger import logger
  32. from nailgun import objects
  33. from nailgun.objects.serializers.base import BasicSerializer
  34. from nailgun.orchestrator import orchestrator_graph
  35. from nailgun.settings import settings
  36. from nailgun import transactions
  37. from nailgun import utils
  38. def forbid_client_caching(handler):
  39. if web.ctx.path.startswith("/api"):
  40. web.header('Cache-Control',
  41. 'store, no-cache, must-revalidate,'
  42. ' post-check=0, pre-check=0')
  43. web.header('Pragma', 'no-cache')
  44. dt = datetime.fromtimestamp(0).strftime(
  45. '%a, %d %b %Y %H:%M:%S GMT'
  46. )
  47. web.header('Expires', dt)
  48. return handler()
  49. def load_db_driver(handler):
  50. """Wrap all handlers calls so transaction is handled accordingly
  51. rollback if something wrong or commit changes otherwise. Please note,
  52. only HTTPError should be raised up from this function. All another
  53. possible errors should be handled.
  54. """
  55. try:
  56. # execute handler and commit changes if all is ok
  57. response = handler()
  58. db.commit()
  59. return response
  60. except web.HTTPError:
  61. # a special case: commit changes if http error ends with
  62. # 200, 201, 202, etc
  63. if web.ctx.status.startswith('2'):
  64. db.commit()
  65. else:
  66. db.rollback()
  67. raise
  68. except (sa_exc.IntegrityError, sa_exc.DataError) as exc:
  69. # respond a "400 Bad Request" if database constraints were broken
  70. db.rollback()
  71. raise BaseHandler.http(400, exc.message)
  72. except Exception:
  73. db.rollback()
  74. raise
  75. finally:
  76. db.remove()
  77. class BaseHandler(object):
  78. validator = BasicValidator
  79. serializer = BasicSerializer
  80. fields = []
  81. @classmethod
  82. def render(cls, instance, fields=None):
  83. return cls.serializer.serialize(
  84. instance,
  85. fields=fields or cls.fields
  86. )
  87. @classmethod
  88. def http(cls, status_code, msg="", err_list=None, headers=None):
  89. """Raise an HTTP status code.
  90. Useful for returning status
  91. codes like 401 Unauthorized or 403 Forbidden.
  92. :param status_code: the HTTP status code as an integer
  93. :param msg: the message to send along, as a string
  94. :param err_list: list of fields with errors
  95. :param headers: the headers to send along, as a dictionary
  96. """
  97. class _nocontent(web.HTTPError):
  98. message = 'No Content'
  99. def __init__(self):
  100. super(_nocontent, self).__init__(
  101. status='204 No Content',
  102. data=self.message
  103. )
  104. class _range_not_satisfiable(web.HTTPError):
  105. message = 'Requested Range Not Satisfiable'
  106. def __init__(self):
  107. super(_range_not_satisfiable, self).__init__(
  108. status='416 Range Not Satisfiable',
  109. data=self.message
  110. )
  111. exc_status_map = {
  112. 200: web.ok,
  113. 201: web.created,
  114. 202: web.accepted,
  115. 204: _nocontent,
  116. 301: web.redirect,
  117. 302: web.found,
  118. 400: web.badrequest,
  119. 401: web.unauthorized,
  120. 403: web.forbidden,
  121. 404: web.notfound,
  122. 405: web.nomethod,
  123. 406: web.notacceptable,
  124. 409: web.conflict,
  125. 410: web.gone,
  126. 415: web.unsupportedmediatype,
  127. 416: _range_not_satisfiable,
  128. 500: web.internalerror,
  129. }
  130. # web.py has a poor exception design: some of them receive
  131. # the `message` argument and some of them not. the only
  132. # solution to set custom message is to assign message directly
  133. # to the `data` attribute. though, that won't work for
  134. # the `internalerror` because it tries to do magic with
  135. # application context without explicit `message` argument.
  136. try:
  137. exc = exc_status_map[status_code](message=msg)
  138. except TypeError:
  139. exc = exc_status_map[status_code]()
  140. exc.data = msg
  141. exc.err_list = err_list or []
  142. exc.status_code = status_code
  143. headers = headers or {}
  144. for key, value in headers.items():
  145. web.header(key, value)
  146. return exc
  147. @classmethod
  148. def checked_data(cls, validate_method=None, **kwargs):
  149. try:
  150. data = kwargs.pop('data', web.data())
  151. method = validate_method or cls.validator.validate
  152. valid_data = method(data, **kwargs)
  153. except (
  154. errors.InvalidInterfacesInfo,
  155. errors.InvalidMetadata
  156. ) as exc:
  157. objects.Notification.create({
  158. "topic": "error",
  159. "message": exc.message
  160. })
  161. raise cls.http(400, exc.message)
  162. except (
  163. errors.NotAllowed
  164. ) as exc:
  165. raise cls.http(403, exc.message)
  166. except (
  167. errors.AlreadyExists
  168. ) as exc:
  169. raise cls.http(409, exc.message)
  170. except (
  171. errors.InvalidData,
  172. errors.NodeOffline,
  173. errors.NoDeploymentTasks,
  174. errors.UnavailableRelease,
  175. errors.CannotCreate,
  176. errors.CannotUpdate,
  177. errors.CannotDelete,
  178. errors.CannotFindExtension,
  179. ) as exc:
  180. raise cls.http(400, exc.message)
  181. except (
  182. errors.ObjectNotFound,
  183. ) as exc:
  184. raise cls.http(404, exc.message)
  185. except Exception as exc:
  186. raise cls.http(500, traceback.format_exc())
  187. return valid_data
  188. def get_object_or_404(self, obj, *args, **kwargs):
  189. """Get object instance by ID
  190. :http: 404 when not found
  191. :returns: object instance
  192. """
  193. log_404 = kwargs.pop("log_404", None)
  194. log_get = kwargs.pop("log_get", None)
  195. uid = kwargs.get("id", (args[0] if args else None))
  196. if uid is None:
  197. if log_404:
  198. getattr(logger, log_404[0])(log_404[1])
  199. raise self.http(404, u'Invalid ID specified')
  200. else:
  201. instance = obj.get_by_uid(uid)
  202. if not instance:
  203. raise self.http(404, u'{0} not found'.format(obj.__name__))
  204. if log_get:
  205. getattr(logger, log_get[0])(log_get[1])
  206. return instance
  207. def get_objects_list_or_404(self, obj, ids):
  208. """Get list of objects
  209. :param obj: model object
  210. :param ids: list of ids
  211. :http: 404 when not found
  212. :returns: list of object instances
  213. """
  214. node_query = obj.filter_by_id_list(None, ids)
  215. objects_count = obj.count(node_query)
  216. if len(set(ids)) != objects_count:
  217. raise self.http(404, '{0} not found'.format(obj.__name__))
  218. return list(node_query)
  219. def raise_task(self, task):
  220. if task.status in [consts.TASK_STATUSES.ready,
  221. consts.TASK_STATUSES.error]:
  222. status = 200
  223. else:
  224. status = 202
  225. raise self.http(status, objects.Task.to_json(task))
  226. @staticmethod
  227. def get_param_as_set(param_name, delimiter=',', default=None):
  228. """Parse array param from web.input()
  229. :param param_name: parameter name in web.input()
  230. :type param_name: str
  231. :param delimiter: delimiter
  232. :type delimiter: str
  233. :returns: list of items
  234. :rtype: set of str or None
  235. """
  236. if param_name in web.input():
  237. param = getattr(web.input(), param_name)
  238. if param == '':
  239. return set()
  240. else:
  241. return set(six.moves.map(
  242. six.text_type.strip,
  243. param.split(delimiter))
  244. )
  245. else:
  246. return default
  247. @staticmethod
  248. def get_requested_mime():
  249. accept = web.ctx.env.get("HTTP_ACCEPT", "application/json")
  250. accept = accept.strip().split(',')[0]
  251. accept = accept.split(';')[0]
  252. return accept
  253. def json_resp(data):
  254. if isinstance(data, (dict, list)) or data is None:
  255. return jsonutils.dumps(data)
  256. else:
  257. return data
  258. @decorator
  259. def handle_errors(func, cls, *args, **kwargs):
  260. try:
  261. return func(cls, *args, **kwargs)
  262. except web.HTTPError as http_error:
  263. if http_error.status_code != 204:
  264. web.header('Content-Type', 'application/json', unique=True)
  265. if http_error.status_code >= 400:
  266. http_error.data = json_resp({
  267. "message": http_error.data,
  268. "errors": http_error.err_list
  269. })
  270. else:
  271. http_error.data = json_resp(http_error.data)
  272. raise
  273. except errors.NailgunException as exc:
  274. logger.exception('NailgunException occured')
  275. http_error = BaseHandler.http(400, exc.message)
  276. web.header('Content-Type', 'text/plain')
  277. raise http_error
  278. # intercepting all errors to avoid huge HTML output
  279. except Exception as exc:
  280. logger.exception('Unexpected exception occured')
  281. http_error = BaseHandler.http(
  282. 500,
  283. (
  284. traceback.format_exc(exc)
  285. if settings.DEVELOPMENT
  286. else 'Unexpected exception, please check logs'
  287. )
  288. )
  289. http_error.data = json_resp(http_error.data)
  290. web.header('Content-Type', 'text/plain')
  291. raise http_error
  292. @decorator
  293. def validate(func, cls, *args, **kwargs):
  294. request_validation_needed = True
  295. resource_type = "single"
  296. if issubclass(
  297. cls.__class__,
  298. CollectionHandler
  299. ) and not func.func_name == "POST":
  300. resource_type = "collection"
  301. if (
  302. func.func_name in ("GET", "DELETE") or
  303. getattr(cls.__class__, 'validator', None) is None or
  304. (resource_type == "single" and not cls.validator.single_schema) or
  305. (resource_type == "collection" and not cls.validator.collection_schema)
  306. ):
  307. request_validation_needed = False
  308. if request_validation_needed:
  309. BaseHandler.checked_data(
  310. cls.validator.validate_request,
  311. resource_type=resource_type
  312. )
  313. return func(cls, *args, **kwargs)
  314. @decorator
  315. def serialize(func, cls, *args, **kwargs):
  316. """Set context-type of response based on Accept header.
  317. This decorator checks Accept header received from client
  318. and returns corresponding wrapper (only JSON is currently
  319. supported). It can be used as is:
  320. @handle_errors
  321. @validate
  322. @serialize
  323. def GET(self):
  324. ...
  325. """
  326. accepted_types = (
  327. "application/json",
  328. "application/x-yaml",
  329. "*/*"
  330. )
  331. accept = cls.get_requested_mime()
  332. if accept not in accepted_types:
  333. raise BaseHandler.http(415)
  334. resp = func(cls, *args, **kwargs)
  335. if accept == 'application/x-yaml':
  336. web.header('Content-Type', 'application/x-yaml', unique=True)
  337. return yaml.dump(resp, default_flow_style=False)
  338. else:
  339. # default is json
  340. web.header('Content-Type', 'application/json', unique=True)
  341. return jsonutils.dumps(resp)
  342. class SingleHandler(BaseHandler):
  343. single = None
  344. validator = BasicValidator
  345. @handle_errors
  346. @serialize
  347. def GET(self, obj_id):
  348. """:returns: JSONized REST object.
  349. :http: * 200 (OK)
  350. * 404 (object not found in db)
  351. """
  352. obj = self.get_object_or_404(self.single, obj_id)
  353. return self.single.to_dict(obj)
  354. @handle_errors
  355. @validate
  356. @serialize
  357. def PUT(self, obj_id):
  358. """:returns: JSONized REST object.
  359. :http: * 200 (OK)
  360. * 404 (object not found in db)
  361. """
  362. obj = self.get_object_or_404(self.single, obj_id)
  363. data = self.checked_data(
  364. self.validator.validate_update,
  365. instance=obj
  366. )
  367. self.single.update(obj, data)
  368. return self.single.to_dict(obj)
  369. @handle_errors
  370. @validate
  371. def DELETE(self, obj_id):
  372. """:returns: Empty string
  373. :http: * 204 (object successfully deleted)
  374. * 404 (object not found in db)
  375. """
  376. obj = self.get_object_or_404(
  377. self.single,
  378. obj_id
  379. )
  380. self.checked_data(
  381. self.validator.validate_delete,
  382. instance=obj
  383. )
  384. self.single.delete(obj)
  385. raise self.http(204)
  386. class Pagination(object):
  387. """Get pagination scope from init or HTTP request arguments"""
  388. def convert(self, x):
  389. """ret. None if x=None, else ret. x as int>=0; else raise 400"""
  390. val = x
  391. if val is not None:
  392. if type(val) is not int:
  393. try:
  394. val = int(x)
  395. except ValueError:
  396. raise BaseHandler.http(400, 'Cannot convert "%s" to int'
  397. % x)
  398. # raise on negative values
  399. if val < 0:
  400. raise BaseHandler.http(400, 'Negative limit/offset not \
  401. allowed')
  402. return val
  403. def get_order_by(self, order_by):
  404. if order_by:
  405. order_by = [s.strip() for s in order_by.split(',') if s.strip()]
  406. return order_by if order_by else None
  407. def __init__(self, limit=None, offset=None, order_by=None):
  408. if limit is not None or offset is not None or order_by is not None:
  409. # init with provided arguments
  410. self.limit = self.convert(limit)
  411. self.offset = self.convert(offset)
  412. self.order_by = self.get_order_by(order_by)
  413. else:
  414. # init with HTTP arguments
  415. self.limit = self.convert(web.input(limit=None).limit)
  416. self.offset = self.convert(web.input(offset=None).offset)
  417. self.order_by = self.get_order_by(web.input(order_by=None)
  418. .order_by)
  419. class CollectionHandler(BaseHandler):
  420. collection = None
  421. validator = BasicValidator
  422. eager = ()
  423. def get_scoped_query_and_range(self, pagination=None, filter_by=None):
  424. """Get filtered+paged collection query and collection.ContentRange obj
  425. Return a scoped query, and if pagination is requested then also return
  426. ContentRange object (see NailgunCollection.content_range) to allow to
  427. set Content-Range header (outside of this functon).
  428. If pagination is not set/requested, return query to all collection's
  429. objects.
  430. Allows getting object count without getting objects - via
  431. content_range if pagination.limit=0.
  432. :param pagination: Pagination object
  433. :param filter_by: filter dict passed to query.filter_by(\*\*dict)
  434. :type filter_by: dict
  435. :returns: SQLAlchemy query and ContentRange object
  436. """
  437. pagination = pagination or Pagination()
  438. query = None
  439. content_range = None
  440. if self.collection and self.collection.single.model:
  441. query, content_range = self.collection.scope(pagination, filter_by)
  442. if content_range:
  443. if not content_range.valid:
  444. raise self.http(416, 'Requested range "%s" cannot be '
  445. 'satisfied' % content_range)
  446. return query, content_range
  447. def set_content_range(self, content_range):
  448. """Set Content-Range header to indicate partial data
  449. :param content_range: NailgunCollection.content_range named tuple
  450. """
  451. txt = 'objects {x.first}-{x.last}/{x.total}'.format(x=content_range)
  452. web.header('Content-Range', txt)
  453. @handle_errors
  454. @validate
  455. @serialize
  456. def GET(self):
  457. """:returns: Collection of JSONized REST objects.
  458. :http: * 200 (OK)
  459. * 400 (Bad Request)
  460. * 406 (requested range not satisfiable)
  461. """
  462. query, content_range = self.get_scoped_query_and_range()
  463. if content_range:
  464. self.set_content_range(content_range)
  465. q = self.collection.eager(query, self.eager)
  466. return self.collection.to_list(q)
  467. @handle_errors
  468. @validate
  469. def POST(self):
  470. """:returns: JSONized REST object.
  471. :http: * 201 (object successfully created)
  472. * 400 (invalid object data specified)
  473. * 409 (object with such parameters already exists)
  474. """
  475. data = self.checked_data()
  476. try:
  477. new_obj = self.collection.create(data)
  478. except errors.CannotCreate as exc:
  479. raise self.http(400, exc.message)
  480. raise self.http(201, self.collection.single.to_json(new_obj))
  481. class DBSingletonHandler(BaseHandler):
  482. """Manages an object that is supposed to have only one entry in the DB"""
  483. single = None
  484. validator = BasicValidator
  485. not_found_error = "Object not found in the DB"
  486. def get_one_or_404(self):
  487. try:
  488. instance = self.single.get_one(fail_if_not_found=True)
  489. except errors.ObjectNotFound:
  490. raise self.http(404, self.not_found_error)
  491. return instance
  492. @handle_errors
  493. @validate
  494. @serialize
  495. def GET(self):
  496. """Get singleton object from DB
  497. :http: * 200 (OK)
  498. * 404 (Object not found in DB)
  499. """
  500. instance = self.get_one_or_404()
  501. return self.single.to_dict(instance)
  502. @handle_errors
  503. @validate
  504. @serialize
  505. def PUT(self):
  506. """Change object in DB
  507. :http: * 200 (OK)
  508. * 400 (Invalid data)
  509. * 404 (Object not present in DB)
  510. """
  511. data = self.checked_data(self.validator.validate_update)
  512. instance = self.get_one_or_404()
  513. self.single.update(instance, data)
  514. return self.single.to_dict(instance)
  515. @handle_errors
  516. @validate
  517. @serialize
  518. def PATCH(self):
  519. """Update object
  520. :http: * 200 (OK)
  521. * 400 (Invalid data)
  522. * 404 (Object not present in DB)
  523. """
  524. data = self.checked_data(self.validator.validate_update)
  525. instance = self.get_one_or_404()
  526. instance.update(utils.dict_merge(
  527. self.single.serializer.serialize(instance), data
  528. ))
  529. return self.single.to_dict(instance)
  530. class OrchestratorDeploymentTasksHandler(SingleHandler):
  531. """Handler for deployment graph serialization."""
  532. validator = GraphSolverTasksValidator
  533. @handle_errors
  534. @validate
  535. @serialize
  536. def GET(self, obj_id):
  537. """:returns: Deployment tasks
  538. :http: * 200 OK
  539. * 404 (object not found)
  540. """
  541. obj = self.get_object_or_404(self.single, obj_id)
  542. end = web.input(end=None).end
  543. start = web.input(start=None).start
  544. graph_type = web.input(graph_type=None).graph_type or None
  545. # web.py depends on [] to understand that there will be multiple inputs
  546. include = web.input(include=[]).include
  547. # merged (cluster + plugins + release) tasks is returned for cluster
  548. # but the own release tasks is returned for release
  549. tasks = self.single.get_deployment_tasks(obj, graph_type=graph_type)
  550. if end or start:
  551. graph = orchestrator_graph.GraphSolver(tasks)
  552. for t in tasks:
  553. if StrictVersion(t.get('version')) >= \
  554. StrictVersion(consts.TASK_CROSS_DEPENDENCY):
  555. raise self.http(400, (
  556. 'Both "start" and "end" parameters are not allowed '
  557. 'for task-based deployment.'))
  558. try:
  559. return graph.filter_subgraph(
  560. end=end, start=start, include=include).node.values()
  561. except errors.TaskNotFound as e:
  562. raise self.http(400, 'Cannot find task {0} by its '
  563. 'name.'.format(e.task_name))
  564. return tasks
  565. @handle_errors
  566. @validate
  567. @serialize
  568. def PUT(self, obj_id):
  569. """:returns: Deployment tasks
  570. :http: * 200 (OK)
  571. * 400 (invalid data specified)
  572. * 404 (object not found in db)
  573. """
  574. obj = self.get_object_or_404(self.single, obj_id)
  575. graph_type = web.input(graph_type=None).graph_type or None
  576. data = self.checked_data(
  577. self.validator.validate_update,
  578. instance=obj
  579. )
  580. deployment_graph = objects.DeploymentGraph.get_for_model(
  581. obj, graph_type=graph_type)
  582. if deployment_graph:
  583. objects.DeploymentGraph.update(
  584. deployment_graph, {'tasks': data})
  585. else:
  586. deployment_graph = objects.DeploymentGraph.create_for_model(
  587. {'tasks': data}, obj, graph_type=graph_type)
  588. return objects.DeploymentGraph.get_tasks(deployment_graph)
  589. def POST(self, obj_id):
  590. """Creation of metadata disallowed
  591. :http: * 405 (method not supported)
  592. """
  593. raise self.http(405, 'Create not supported for this entity')
  594. def DELETE(self, obj_id):
  595. """Deletion of metadata disallowed
  596. :http: * 405 (method not supported)
  597. """
  598. raise self.http(405, 'Delete not supported for this entity')
  599. class TransactionExecutorHandler(BaseHandler):
  600. def start_transaction(self, cluster, options):
  601. """Starts new transaction.
  602. :param cluster: the cluster object
  603. :param options: the transaction parameters
  604. :return: JSONized task object
  605. """
  606. try:
  607. manager = transactions.TransactionsManager(cluster.id)
  608. self.raise_task(manager.execute(**options))
  609. except errors.ObjectNotFound as e:
  610. raise self.http(404, e.message)
  611. except errors.DeploymentAlreadyStarted as e:
  612. raise self.http(409, e.message)
  613. except errors.InvalidData as e:
  614. raise self.http(400, e.message)
  615. # TODO(enchantner): rewrite more handlers to inherit from this
  616. # and move more common code here, this is deprecated handler
  617. class DeferredTaskHandler(TransactionExecutorHandler):
  618. """Abstract Deferred Task Handler"""
  619. validator = BaseDefferedTaskValidator
  620. single = objects.Task
  621. log_message = u"Starting deferred task on environment '{env_id}'"
  622. log_error = u"Error during execution of deferred task " \
  623. u"on environment '{env_id}': {error}"
  624. task_manager = None
  625. @classmethod
  626. def get_options(cls):
  627. return {}
  628. @classmethod
  629. def get_transaction_options(cls, cluster, options):
  630. """Finds graph for this action."""
  631. return None
  632. @handle_errors
  633. @validate
  634. def PUT(self, cluster_id):
  635. """:returns: JSONized Task object.
  636. :http: * 202 (task successfully executed)
  637. * 400 (invalid object data specified)
  638. * 404 (environment is not found)
  639. * 409 (task with such parameters already exists)
  640. """
  641. cluster = self.get_object_or_404(
  642. objects.Cluster,
  643. cluster_id,
  644. log_404=(
  645. u"warning",
  646. u"Error: there is no cluster "
  647. u"with id '{0}' in DB.".format(cluster_id)
  648. )
  649. )
  650. logger.info(self.log_message.format(env_id=cluster_id))
  651. try:
  652. options = self.get_options()
  653. except ValueError as e:
  654. raise self.http(400, six.text_type(e))
  655. try:
  656. self.validator.validate(cluster)
  657. except errors.NailgunException as e:
  658. raise self.http(400, e.message)
  659. if objects.Release.is_lcm_supported(cluster.release):
  660. # try to get new graph to run transaction manager
  661. try:
  662. transaction_options = self.get_transaction_options(
  663. cluster, options
  664. )
  665. except errors.NailgunException as e:
  666. logger.exception("Failed to get transaction options")
  667. raise self.http(400, msg=six.text_type(e))
  668. if transaction_options:
  669. return self.start_transaction(cluster, transaction_options)
  670. try:
  671. task_manager = self.task_manager(cluster_id=cluster.id)
  672. task = task_manager.execute(**options)
  673. except (
  674. errors.AlreadyExists,
  675. errors.StopAlreadyRunning
  676. ) as exc:
  677. raise self.http(409, exc.message)
  678. except (
  679. errors.DeploymentNotRunning,
  680. errors.NoDeploymentTasks,
  681. errors.WrongNodeStatus,
  682. errors.UnavailableRelease,
  683. errors.CannotBeStopped,
  684. ) as exc:
  685. raise self.http(400, exc.message)
  686. except Exception as exc:
  687. logger.error(
  688. self.log_error.format(
  689. env_id=cluster_id,
  690. error=str(exc)
  691. )
  692. )
  693. # let it be 500
  694. raise
  695. self.raise_task(task)