Internet of Things resource management service for OpenStack clouds.
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.

board.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  2. # not use this file except in compliance with the License. You may obtain
  3. # a copy of the License at
  4. #
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. #
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. # License for the specific language governing permissions and limitations
  11. # under the License.
  12. from iotronic.api.controllers import base
  13. from iotronic.api.controllers import link
  14. from iotronic.api.controllers.v1 import collection
  15. from iotronic.api.controllers.v1 import location as loc
  16. from iotronic.api.controllers.v1 import types
  17. from iotronic.api.controllers.v1 import utils as api_utils
  18. from iotronic.api import expose
  19. from iotronic.common import exception
  20. from iotronic.common import policy
  21. from iotronic import objects
  22. import pecan
  23. from pecan import rest
  24. import wsme
  25. from wsme import types as wtypes
  26. _DEFAULT_RETURN_FIELDS = ('name', 'code', 'status', 'uuid', 'session', 'type')
  27. class Board(base.APIBase):
  28. """API representation of a board.
  29. """
  30. uuid = types.uuid
  31. code = wsme.wsattr(wtypes.text)
  32. status = wsme.wsattr(wtypes.text)
  33. name = wsme.wsattr(wtypes.text)
  34. type = wsme.wsattr(wtypes.text)
  35. owner = types.uuid
  36. session = wsme.wsattr(wtypes.text)
  37. project = types.uuid
  38. mobile = types.boolean
  39. links = wsme.wsattr([link.Link], readonly=True)
  40. location = wsme.wsattr([loc.Location])
  41. extra = types.jsontype
  42. def __init__(self, **kwargs):
  43. self.fields = []
  44. fields = list(objects.Board.fields)
  45. for k in fields:
  46. # Skip fields we do not expose.
  47. if not hasattr(self, k):
  48. continue
  49. self.fields.append(k)
  50. setattr(self, k, kwargs.get(k, wtypes.Unset))
  51. @staticmethod
  52. def _convert_with_links(board, url, fields=None):
  53. board_uuid = board.uuid
  54. if fields is not None:
  55. board.unset_fields_except(fields)
  56. # rel_name, url, resource, resource_args,
  57. # bookmark=False, type=wtypes.Unset
  58. board.links = [link.Link.make_link('self', url, 'boards',
  59. board_uuid),
  60. link.Link.make_link('bookmark', url, 'boards',
  61. board_uuid, bookmark=True)
  62. ]
  63. return board
  64. @classmethod
  65. def convert_with_links(cls, rpc_board, fields=None):
  66. board = Board(**rpc_board.as_dict())
  67. try:
  68. session = objects.SessionWP.get_session_by_board_uuid(
  69. pecan.request.context, board.uuid)
  70. board.session = session.session_id
  71. except Exception:
  72. board.session = None
  73. try:
  74. list_loc = objects.Location.list_by_board_uuid(
  75. pecan.request.context, board.uuid)
  76. board.location = loc.Location.convert_with_list(list_loc)
  77. except Exception:
  78. board.location = []
  79. # to enable as soon as a better session and location management
  80. # is implemented
  81. # if fields is not None:
  82. # api_utils.check_for_invalid_fields(fields, board_dict)
  83. return cls._convert_with_links(board,
  84. pecan.request.public_url,
  85. fields=fields)
  86. class BoardCollection(collection.Collection):
  87. """API representation of a collection of boards."""
  88. boards = [Board]
  89. """A list containing boards objects"""
  90. def __init__(self, **kwargs):
  91. self._type = 'boards'
  92. @staticmethod
  93. def convert_with_links(boards, limit, url=None, fields=None, **kwargs):
  94. collection = BoardCollection()
  95. collection.boards = [Board.convert_with_links(n, fields=fields)
  96. for n in boards]
  97. collection.next = collection.get_next(limit, url=url, **kwargs)
  98. return collection
  99. class InjectionPlugin(base.APIBase):
  100. plugin = types.uuid_or_name
  101. board_uuid = types.uuid_or_name
  102. status = wtypes.text
  103. onboot = types.boolean
  104. def __init__(self, **kwargs):
  105. self.fields = []
  106. fields = list(objects.InjectionPlugin.fields)
  107. fields.remove('board_uuid')
  108. for k in fields:
  109. # Skip fields we do not expose.
  110. if not hasattr(self, k):
  111. continue
  112. self.fields.append(k)
  113. setattr(self, k, kwargs.get(k, wtypes.Unset))
  114. setattr(self, 'plugin', kwargs.get('plugin_uuid', wtypes.Unset))
  115. class InjectionCollection(collection.Collection):
  116. """API representation of a collection of injection."""
  117. injections = [InjectionPlugin]
  118. def __init__(self, **kwargs):
  119. self._type = 'injections'
  120. @staticmethod
  121. def get_list(injections, fields=None):
  122. collection = InjectionCollection()
  123. collection.injections = [InjectionPlugin(**n.as_dict())
  124. for n in injections]
  125. return collection
  126. class PluginAction(base.APIBase):
  127. action = wsme.wsattr(wtypes.text)
  128. parameters = types.jsontype
  129. class BoardPluginsController(rest.RestController):
  130. def __init__(self, board_ident):
  131. self.board_ident = board_ident
  132. def _get_plugins_on_board_collection(self, board_uuid, fields=None):
  133. injections = objects.InjectionPlugin.list(pecan.request.context,
  134. board_uuid)
  135. return InjectionCollection.get_list(injections,
  136. fields=fields)
  137. @expose.expose(InjectionCollection,
  138. status_code=200)
  139. def get_all(self):
  140. """Retrieve a list of plugins of a board.
  141. """
  142. rpc_board = api_utils.get_rpc_board(self.board_ident)
  143. cdict = pecan.request.context.to_policy_values()
  144. cdict['owner'] = rpc_board.owner
  145. policy.authorize('iot:plugin_on_board:get', cdict, cdict)
  146. return self._get_plugins_on_board_collection(rpc_board.uuid)
  147. @expose.expose(wtypes.text, types.uuid_or_name, body=PluginAction,
  148. status_code=200)
  149. def post(self, plugin_ident, PluginAction):
  150. if not PluginAction.action:
  151. raise exception.MissingParameterValue(
  152. ("Action is not specified."))
  153. if not PluginAction.parameters:
  154. PluginAction.parameters = {}
  155. rpc_board = api_utils.get_rpc_board(self.board_ident)
  156. rpc_plugin = api_utils.get_rpc_plugin(plugin_ident)
  157. try:
  158. cdict = pecan.request.context.to_policy_values()
  159. cdict['owner'] = rpc_board.owner
  160. policy.authorize('iot:plugin_action:post', cdict, cdict)
  161. if not rpc_plugin.public:
  162. cdict = pecan.request.context.to_policy_values()
  163. cdict['owner'] = rpc_plugin.owner
  164. policy.authorize('iot:plugin_action:post', cdict, cdict)
  165. except exception:
  166. return exception
  167. rpc_board.check_if_online()
  168. if objects.plugin.want_customs_params(PluginAction.action):
  169. valid_keys = list(rpc_plugin.parameters.keys())
  170. if not all(k in PluginAction.parameters for k in valid_keys):
  171. raise exception.InvalidParameterValue(
  172. "Parameters are different from the valid ones")
  173. result = pecan.request.rpcapi.action_plugin(pecan.request.context,
  174. rpc_plugin.uuid,
  175. rpc_board.uuid,
  176. PluginAction.action,
  177. PluginAction.parameters)
  178. return result
  179. @expose.expose(wtypes.text, body=InjectionPlugin,
  180. status_code=200)
  181. def put(self, Injection):
  182. """inject a plugin into a board.
  183. :param plugin_ident: UUID or logical name of a plugin.
  184. :param board_ident: UUID or logical name of a board.
  185. """
  186. if not Injection.plugin:
  187. raise exception.MissingParameterValue(
  188. ("Plugin is not specified."))
  189. if not Injection.onboot:
  190. Injection.onboot = False
  191. rpc_board = api_utils.get_rpc_board(self.board_ident)
  192. rpc_plugin = api_utils.get_rpc_plugin(Injection.plugin)
  193. try:
  194. cdict = pecan.request.context.to_policy_values()
  195. cdict['owner'] = rpc_board.owner
  196. policy.authorize('iot:plugin_inject:put', cdict, cdict)
  197. if not rpc_plugin.public:
  198. cdict = pecan.request.context.to_policy_values()
  199. cdict['owner'] = rpc_plugin.owner
  200. policy.authorize('iot:plugin_inject:put', cdict, cdict)
  201. except exception:
  202. return exception
  203. rpc_board.check_if_online()
  204. result = pecan.request.rpcapi.inject_plugin(pecan.request.context,
  205. rpc_plugin.uuid,
  206. rpc_board.uuid,
  207. Injection.onboot)
  208. return result
  209. @expose.expose(wtypes.text, types.uuid_or_name,
  210. status_code=204)
  211. def delete(self, plugin_uuid):
  212. """Remove a plugin from a board.
  213. :param plugin_ident: UUID or logical name of a plugin.
  214. :param board_ident: UUID or logical name of a board.
  215. """
  216. rpc_board = api_utils.get_rpc_board(self.board_ident)
  217. cdict = pecan.request.context.to_policy_values()
  218. cdict['owner'] = rpc_board.owner
  219. policy.authorize('iot:plugin_remove:delete', cdict, cdict)
  220. rpc_board.check_if_online()
  221. rpc_plugin = api_utils.get_rpc_plugin(plugin_uuid)
  222. return pecan.request.rpcapi.remove_plugin(pecan.request.context,
  223. rpc_plugin.uuid,
  224. rpc_board.uuid)
  225. class BoardsController(rest.RestController):
  226. """REST controller for Boards."""
  227. _subcontroller_map = {
  228. 'plugins': BoardPluginsController,
  229. }
  230. invalid_sort_key_list = ['extra', 'location']
  231. _custom_actions = {
  232. 'detail': ['GET'],
  233. }
  234. @pecan.expose()
  235. def _lookup(self, ident, *remainder):
  236. try:
  237. ident = types.uuid_or_name.validate(ident)
  238. except exception.InvalidUuidOrName as e:
  239. pecan.abort('400', e.args[0])
  240. if not remainder:
  241. return
  242. subcontroller = self._subcontroller_map.get(remainder[0])
  243. if subcontroller:
  244. return subcontroller(board_ident=ident), remainder[1:]
  245. def _get_boards_collection(self, status, marker, limit,
  246. sort_key, sort_dir,
  247. project=None,
  248. resource_url=None, fields=None):
  249. limit = api_utils.validate_limit(limit)
  250. sort_dir = api_utils.validate_sort_dir(sort_dir)
  251. marker_obj = None
  252. if marker:
  253. marker_obj = objects.Board.get_by_uuid(pecan.request.context,
  254. marker)
  255. if sort_key in self.invalid_sort_key_list:
  256. raise exception.InvalidParameterValue(
  257. ("The sort_key value %(key)s is an invalid field for "
  258. "sorting") % {'key': sort_key})
  259. filters = {}
  260. # bounding the request to a project
  261. if project:
  262. if pecan.request.context.is_admin:
  263. filters['project_id'] = project
  264. else:
  265. msg = ("Project parameter can be used only "
  266. "by the administrator.")
  267. raise wsme.exc.ClientSideError(msg,
  268. status_code=400)
  269. else:
  270. filters['project_id'] = pecan.request.context.project_id
  271. if status:
  272. filters['status'] = status
  273. boards = objects.Board.list(pecan.request.context, limit, marker_obj,
  274. sort_key=sort_key, sort_dir=sort_dir,
  275. filters=filters)
  276. parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
  277. return BoardCollection.convert_with_links(boards, limit,
  278. url=resource_url,
  279. fields=fields,
  280. **parameters)
  281. @expose.expose(Board, types.uuid_or_name, types.listtype)
  282. def get_one(self, board_ident, fields=None):
  283. """Retrieve information about the given board.
  284. :param board_ident: UUID or logical name of a board.
  285. :param fields: Optional, a list with a specified set of fields
  286. of the resource to be returned.
  287. """
  288. cdict = pecan.request.context.to_policy_values()
  289. policy.authorize('iot:board:get', cdict, cdict)
  290. rpc_board = api_utils.get_rpc_board(board_ident)
  291. return Board.convert_with_links(rpc_board, fields=fields)
  292. @expose.expose(BoardCollection, wtypes.text, types.uuid, int, wtypes.text,
  293. wtypes.text, types.listtype, wtypes.text)
  294. def get_all(self, status=None, marker=None,
  295. limit=None, sort_key='id', sort_dir='asc',
  296. fields=None, project=None):
  297. """Retrieve a list of boards.
  298. :param status: Optional string value to get only board in
  299. that status.
  300. :param marker: pagination marker for large data sets.
  301. :param limit: maximum number of resources to return in a single result.
  302. This value cannot be larger than the value of max_limit
  303. in the [api] section of the ironic configuration, or only
  304. max_limit resources will be returned.
  305. :param sort_key: column to sort results by. Default: id.
  306. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
  307. :param fields: Optional, a list with a specified set of fields
  308. of the resource to be returned.
  309. """
  310. cdict = pecan.request.context.to_policy_values()
  311. policy.authorize('iot:board:get', cdict, cdict)
  312. if fields is None:
  313. fields = _DEFAULT_RETURN_FIELDS
  314. return self._get_boards_collection(status, marker,
  315. limit, sort_key, sort_dir,
  316. fields=fields, project=project)
  317. @expose.expose(Board, body=Board, status_code=201)
  318. def post(self, Board):
  319. """Create a new Board.
  320. :param Board: a Board within the request body.
  321. """
  322. context = pecan.request.context
  323. cdict = context.to_policy_values()
  324. policy.authorize('iot:board:create', cdict, cdict)
  325. if not Board.name:
  326. raise exception.MissingParameterValue(
  327. ("Name is not specified."))
  328. if not Board.code:
  329. raise exception.MissingParameterValue(
  330. ("Code is not specified."))
  331. if not Board.location:
  332. raise exception.MissingParameterValue(
  333. ("Location is not specified."))
  334. if Board.name:
  335. if not api_utils.is_valid_board_name(Board.name):
  336. msg = ("Cannot create board with invalid name %(name)s")
  337. raise wsme.exc.ClientSideError(msg % {'name': Board.name},
  338. status_code=400)
  339. new_Board = objects.Board(pecan.request.context,
  340. **Board.as_dict())
  341. new_Board.owner = pecan.request.context.user_id
  342. new_Board.project = pecan.request.context.project_id
  343. new_Location = objects.Location(pecan.request.context,
  344. **Board.location[0].as_dict())
  345. new_Board = pecan.request.rpcapi.create_board(pecan.request.context,
  346. new_Board, new_Location)
  347. return Board.convert_with_links(new_Board)
  348. @expose.expose(None, types.uuid_or_name, status_code=204)
  349. def delete(self, board_ident):
  350. """Delete a board.
  351. :param board_ident: UUID or logical name of a board.
  352. """
  353. context = pecan.request.context
  354. cdict = context.to_policy_values()
  355. policy.authorize('iot:board:delete', cdict, cdict)
  356. rpc_board = api_utils.get_rpc_board(board_ident)
  357. pecan.request.rpcapi.destroy_board(pecan.request.context,
  358. rpc_board.uuid)
  359. @expose.expose(Board, types.uuid_or_name, body=Board, status_code=200)
  360. def patch(self, board_ident, val_Board):
  361. """Update a board.
  362. :param board_ident: UUID or logical name of a board.
  363. :param Board: values to be changed
  364. :return updated_board: updated_board
  365. """
  366. context = pecan.request.context
  367. cdict = context.to_policy_values()
  368. policy.authorize('iot:board:update', cdict, cdict)
  369. board = api_utils.get_rpc_board(board_ident)
  370. val_Board = val_Board.as_dict()
  371. for key in val_Board:
  372. try:
  373. board[key] = val_Board[key]
  374. except Exception:
  375. pass
  376. updated_board = pecan.request.rpcapi.update_board(
  377. pecan.request.context,
  378. board)
  379. return Board.convert_with_links(updated_board)
  380. @expose.expose(BoardCollection, wtypes.text, types.uuid, int, wtypes.text,
  381. wtypes.text, types.listtype, wtypes.text)
  382. def detail(self, status=None, marker=None,
  383. limit=None, sort_key='id', sort_dir='asc',
  384. fields=None, project=None):
  385. """Retrieve a list of boards.
  386. :param status: Optional string value to get only board in
  387. that status.
  388. :param marker: pagination marker for large data sets.
  389. :param limit: maximum number of resources to return in a single result.
  390. This value cannot be larger than the value of max_limit
  391. in the [api] section of the ironic configuration, or only
  392. max_limit resources will be returned.
  393. :param sort_key: column to sort results by. Default: id.
  394. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
  395. :param project: Optional string value to get only boards
  396. of the project.
  397. :param fields: Optional, a list with a specified set of fields
  398. of the resource to be returned.
  399. """
  400. cdict = pecan.request.context.to_policy_values()
  401. policy.authorize('iot:board:get', cdict, cdict)
  402. # /detail should only work against collections
  403. parent = pecan.request.path.split('/')[:-1][-1]
  404. if parent != "boards":
  405. raise exception.HTTPNotFound()
  406. return self._get_boards_collection(status, marker,
  407. limit, sort_key, sort_dir,
  408. project=project,
  409. fields=fields)