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 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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 ExposedService(base.APIBase):
  127. service = types.uuid_or_name
  128. board_uuid = types.uuid_or_name
  129. public_port = wsme.types.IntegerType()
  130. def __init__(self, **kwargs):
  131. self.fields = []
  132. fields = list(objects.ExposedService.fields)
  133. fields.remove('board_uuid')
  134. for k in fields:
  135. # Skip fields we do not expose.
  136. if not hasattr(self, k):
  137. continue
  138. self.fields.append(k)
  139. setattr(self, k, kwargs.get(k, wtypes.Unset))
  140. setattr(self, 'service', kwargs.get('service_uuid', wtypes.Unset))
  141. class ExposedCollection(collection.Collection):
  142. """API representation of a collection of injection."""
  143. exposed = [ExposedService]
  144. def __init__(self, **kwargs):
  145. self._type = 'exposed'
  146. @staticmethod
  147. def get_list(exposed, fields=None):
  148. collection = ExposedCollection()
  149. collection.exposed = [ExposedService(**n.as_dict())
  150. for n in exposed]
  151. return collection
  152. class PluginAction(base.APIBase):
  153. action = wsme.wsattr(wtypes.text)
  154. parameters = types.jsontype
  155. class ServiceAction(base.APIBase):
  156. action = wsme.wsattr(wtypes.text)
  157. parameters = types.jsontype
  158. class BoardPluginsController(rest.RestController):
  159. def __init__(self, board_ident):
  160. self.board_ident = board_ident
  161. def _get_plugins_on_board_collection(self, board_uuid, fields=None):
  162. injections = objects.InjectionPlugin.list(pecan.request.context,
  163. board_uuid)
  164. return InjectionCollection.get_list(injections,
  165. fields=fields)
  166. @expose.expose(InjectionCollection,
  167. status_code=200)
  168. def get_all(self):
  169. """Retrieve a list of plugins of a board.
  170. """
  171. rpc_board = api_utils.get_rpc_board(self.board_ident)
  172. cdict = pecan.request.context.to_policy_values()
  173. cdict['owner'] = rpc_board.owner
  174. policy.authorize('iot:plugin_on_board:get', cdict, cdict)
  175. return self._get_plugins_on_board_collection(rpc_board.uuid)
  176. @expose.expose(wtypes.text, types.uuid_or_name, body=PluginAction,
  177. status_code=200)
  178. def post(self, plugin_ident, PluginAction):
  179. if not PluginAction.action:
  180. raise exception.MissingParameterValue(
  181. ("Action is not specified."))
  182. if not PluginAction.parameters:
  183. PluginAction.parameters = {}
  184. rpc_board = api_utils.get_rpc_board(self.board_ident)
  185. rpc_plugin = api_utils.get_rpc_plugin(plugin_ident)
  186. try:
  187. cdict = pecan.request.context.to_policy_values()
  188. cdict['owner'] = rpc_board.owner
  189. policy.authorize('iot:plugin_action:post', cdict, cdict)
  190. if not rpc_plugin.public:
  191. cdict = pecan.request.context.to_policy_values()
  192. cdict['owner'] = rpc_plugin.owner
  193. policy.authorize('iot:plugin_action:post', cdict, cdict)
  194. except exception:
  195. return exception
  196. rpc_board.check_if_online()
  197. if objects.plugin.want_customs_params(PluginAction.action):
  198. valid_keys = list(rpc_plugin.parameters.keys())
  199. if not all(k in PluginAction.parameters for k in valid_keys):
  200. raise exception.InvalidParameterValue(
  201. "Parameters are different from the valid ones")
  202. result = pecan.request.rpcapi.action_plugin(pecan.request.context,
  203. rpc_plugin.uuid,
  204. rpc_board.uuid,
  205. PluginAction.action,
  206. PluginAction.parameters)
  207. return result
  208. @expose.expose(wtypes.text, body=InjectionPlugin,
  209. status_code=200)
  210. def put(self, Injection):
  211. """inject a plugin into a board.
  212. :param plugin_ident: UUID or logical name of a plugin.
  213. :param board_ident: UUID or logical name of a board.
  214. """
  215. if not Injection.plugin:
  216. raise exception.MissingParameterValue(
  217. ("Plugin is not specified."))
  218. if not Injection.onboot:
  219. Injection.onboot = False
  220. rpc_board = api_utils.get_rpc_board(self.board_ident)
  221. rpc_plugin = api_utils.get_rpc_plugin(Injection.plugin)
  222. try:
  223. cdict = pecan.request.context.to_policy_values()
  224. cdict['owner'] = rpc_board.owner
  225. policy.authorize('iot:plugin_inject:put', cdict, cdict)
  226. if not rpc_plugin.public:
  227. cdict = pecan.request.context.to_policy_values()
  228. cdict['owner'] = rpc_plugin.owner
  229. policy.authorize('iot:plugin_inject:put', cdict, cdict)
  230. except exception:
  231. return exception
  232. rpc_board.check_if_online()
  233. result = pecan.request.rpcapi.inject_plugin(pecan.request.context,
  234. rpc_plugin.uuid,
  235. rpc_board.uuid,
  236. Injection.onboot)
  237. return result
  238. @expose.expose(wtypes.text, types.uuid_or_name,
  239. status_code=204)
  240. def delete(self, plugin_uuid):
  241. """Remove a plugin from a board.
  242. :param plugin_ident: UUID or logical name of a plugin.
  243. :param board_ident: UUID or logical name of a board.
  244. """
  245. rpc_board = api_utils.get_rpc_board(self.board_ident)
  246. cdict = pecan.request.context.to_policy_values()
  247. cdict['owner'] = rpc_board.owner
  248. policy.authorize('iot:plugin_remove:delete', cdict, cdict)
  249. rpc_board.check_if_online()
  250. rpc_plugin = api_utils.get_rpc_plugin(plugin_uuid)
  251. return pecan.request.rpcapi.remove_plugin(pecan.request.context,
  252. rpc_plugin.uuid,
  253. rpc_board.uuid)
  254. class BoardServicesController(rest.RestController):
  255. _custom_actions = {
  256. 'action': ['POST'],
  257. 'restore': ['GET']
  258. }
  259. def __init__(self, board_ident):
  260. self.board_ident = board_ident
  261. def _get_services_on_board_collection(self, board_uuid, fields=None):
  262. services = objects.ExposedService.list(pecan.request.context,
  263. board_uuid)
  264. return ExposedCollection.get_list(services,
  265. fields=fields)
  266. @expose.expose(ExposedCollection,
  267. status_code=200)
  268. def get_all(self):
  269. """Retrieve a list of services of a board.
  270. """
  271. rpc_board = api_utils.get_rpc_board(self.board_ident)
  272. cdict = pecan.request.context.to_policy_values()
  273. cdict['project_id'] = rpc_board.project
  274. policy.authorize('iot:service_on_board:get', cdict, cdict)
  275. return self._get_services_on_board_collection(rpc_board.uuid)
  276. @expose.expose(wtypes.text, types.uuid_or_name, body=ServiceAction,
  277. status_code=200)
  278. def action(self, service_ident, ServiceAction):
  279. if not ServiceAction.action:
  280. raise exception.MissingParameterValue(
  281. ("Action is not specified."))
  282. rpc_board = api_utils.get_rpc_board(self.board_ident)
  283. rpc_service = api_utils.get_rpc_service(service_ident)
  284. try:
  285. cdict = pecan.request.context.to_policy_values()
  286. cdict['owner'] = rpc_board.owner
  287. policy.authorize('iot:service_action:post', cdict, cdict)
  288. except exception:
  289. return exception
  290. rpc_board.check_if_online()
  291. result = pecan.request.rpcapi.action_service(pecan.request.context,
  292. rpc_service.uuid,
  293. rpc_board.uuid,
  294. ServiceAction.action)
  295. return result
  296. @expose.expose(ExposedCollection,
  297. status_code=200)
  298. def restore(self):
  299. rpc_board = api_utils.get_rpc_board(self.board_ident)
  300. try:
  301. cdict = pecan.request.context.to_policy_values()
  302. cdict['owner'] = rpc_board.owner
  303. policy.authorize('iot:service_action:post', cdict, cdict)
  304. except exception:
  305. return exception
  306. rpc_board.check_if_online()
  307. pecan.request.rpcapi.restore_services_on_board(
  308. pecan.request.context,
  309. rpc_board.uuid)
  310. return self._get_services_on_board_collection(rpc_board.uuid)
  311. class BoardsController(rest.RestController):
  312. """REST controller for Boards."""
  313. _subcontroller_map = {
  314. 'plugins': BoardPluginsController,
  315. 'services': BoardServicesController,
  316. }
  317. invalid_sort_key_list = ['extra', 'location']
  318. _custom_actions = {
  319. 'detail': ['GET'],
  320. }
  321. @pecan.expose()
  322. def _lookup(self, ident, *remainder):
  323. try:
  324. ident = types.uuid_or_name.validate(ident)
  325. except exception.InvalidUuidOrName as e:
  326. pecan.abort('400', e.args[0])
  327. if not remainder:
  328. return
  329. subcontroller = self._subcontroller_map.get(remainder[0])
  330. if subcontroller:
  331. return subcontroller(board_ident=ident), remainder[1:]
  332. def _get_boards_collection(self, status, marker, limit,
  333. sort_key, sort_dir,
  334. project=None,
  335. resource_url=None, fields=None):
  336. limit = api_utils.validate_limit(limit)
  337. sort_dir = api_utils.validate_sort_dir(sort_dir)
  338. marker_obj = None
  339. if marker:
  340. marker_obj = objects.Board.get_by_uuid(pecan.request.context,
  341. marker)
  342. if sort_key in self.invalid_sort_key_list:
  343. raise exception.InvalidParameterValue(
  344. ("The sort_key value %(key)s is an invalid field for "
  345. "sorting") % {'key': sort_key})
  346. filters = {}
  347. # bounding the request to a project
  348. if project:
  349. if pecan.request.context.is_admin:
  350. filters['project_id'] = project
  351. else:
  352. msg = ("Project parameter can be used only "
  353. "by the administrator.")
  354. raise wsme.exc.ClientSideError(msg,
  355. status_code=400)
  356. else:
  357. filters['project_id'] = pecan.request.context.project_id
  358. if status:
  359. filters['status'] = status
  360. boards = objects.Board.list(pecan.request.context, limit, marker_obj,
  361. sort_key=sort_key, sort_dir=sort_dir,
  362. filters=filters)
  363. parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
  364. return BoardCollection.convert_with_links(boards, limit,
  365. url=resource_url,
  366. fields=fields,
  367. **parameters)
  368. @expose.expose(Board, types.uuid_or_name, types.listtype)
  369. def get_one(self, board_ident, fields=None):
  370. """Retrieve information about the given board.
  371. :param board_ident: UUID or logical name of a board.
  372. :param fields: Optional, a list with a specified set of fields
  373. of the resource to be returned.
  374. """
  375. cdict = pecan.request.context.to_policy_values()
  376. policy.authorize('iot:board:get', cdict, cdict)
  377. rpc_board = api_utils.get_rpc_board(board_ident)
  378. return Board.convert_with_links(rpc_board, fields=fields)
  379. @expose.expose(BoardCollection, wtypes.text, types.uuid, int, wtypes.text,
  380. wtypes.text, types.listtype, wtypes.text)
  381. def get_all(self, status=None, marker=None,
  382. limit=None, sort_key='id', sort_dir='asc',
  383. fields=None, project=None):
  384. """Retrieve a list of boards.
  385. :param status: Optional string value to get only board in
  386. that status.
  387. :param marker: pagination marker for large data sets.
  388. :param limit: maximum number of resources to return in a single result.
  389. This value cannot be larger than the value of max_limit
  390. in the [api] section of the ironic configuration, or only
  391. max_limit resources will be returned.
  392. :param sort_key: column to sort results by. Default: id.
  393. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
  394. :param fields: Optional, a list with a specified set of fields
  395. of the resource to be returned.
  396. """
  397. cdict = pecan.request.context.to_policy_values()
  398. policy.authorize('iot:board:get', cdict, cdict)
  399. if fields is None:
  400. fields = _DEFAULT_RETURN_FIELDS
  401. return self._get_boards_collection(status, marker,
  402. limit, sort_key, sort_dir,
  403. fields=fields, project=project)
  404. @expose.expose(Board, body=Board, status_code=201)
  405. def post(self, Board):
  406. """Create a new Board.
  407. :param Board: a Board within the request body.
  408. """
  409. context = pecan.request.context
  410. cdict = context.to_policy_values()
  411. policy.authorize('iot:board:create', cdict, cdict)
  412. if not Board.name:
  413. raise exception.MissingParameterValue(
  414. ("Name is not specified."))
  415. if not Board.code:
  416. raise exception.MissingParameterValue(
  417. ("Code is not specified."))
  418. if not Board.location:
  419. raise exception.MissingParameterValue(
  420. ("Location is not specified."))
  421. if Board.name:
  422. if not api_utils.is_valid_board_name(Board.name):
  423. msg = ("Cannot create board with invalid name %(name)s")
  424. raise wsme.exc.ClientSideError(msg % {'name': Board.name},
  425. status_code=400)
  426. new_Board = objects.Board(pecan.request.context,
  427. **Board.as_dict())
  428. new_Board.owner = pecan.request.context.user_id
  429. new_Board.project = pecan.request.context.project_id
  430. new_Location = objects.Location(pecan.request.context,
  431. **Board.location[0].as_dict())
  432. new_Board = pecan.request.rpcapi.create_board(pecan.request.context,
  433. new_Board, new_Location)
  434. return Board.convert_with_links(new_Board)
  435. @expose.expose(None, types.uuid_or_name, status_code=204)
  436. def delete(self, board_ident):
  437. """Delete a board.
  438. :param board_ident: UUID or logical name of a board.
  439. """
  440. context = pecan.request.context
  441. cdict = context.to_policy_values()
  442. policy.authorize('iot:board:delete', cdict, cdict)
  443. rpc_board = api_utils.get_rpc_board(board_ident)
  444. pecan.request.rpcapi.destroy_board(pecan.request.context,
  445. rpc_board.uuid)
  446. @expose.expose(Board, types.uuid_or_name, body=Board, status_code=200)
  447. def patch(self, board_ident, val_Board):
  448. """Update a board.
  449. :param board_ident: UUID or logical name of a board.
  450. :param Board: values to be changed
  451. :return updated_board: updated_board
  452. """
  453. context = pecan.request.context
  454. cdict = context.to_policy_values()
  455. policy.authorize('iot:board:update', cdict, cdict)
  456. board = api_utils.get_rpc_board(board_ident)
  457. val_Board = val_Board.as_dict()
  458. for key in val_Board:
  459. try:
  460. board[key] = val_Board[key]
  461. except Exception:
  462. pass
  463. updated_board = pecan.request.rpcapi.update_board(
  464. pecan.request.context,
  465. board)
  466. return Board.convert_with_links(updated_board)
  467. @expose.expose(BoardCollection, wtypes.text, types.uuid, int, wtypes.text,
  468. wtypes.text, types.listtype, wtypes.text)
  469. def detail(self, status=None, marker=None,
  470. limit=None, sort_key='id', sort_dir='asc',
  471. fields=None, project=None):
  472. """Retrieve a list of boards.
  473. :param status: Optional string value to get only board in
  474. that status.
  475. :param marker: pagination marker for large data sets.
  476. :param limit: maximum number of resources to return in a single result.
  477. This value cannot be larger than the value of max_limit
  478. in the [api] section of the ironic configuration, or only
  479. max_limit resources will be returned.
  480. :param sort_key: column to sort results by. Default: id.
  481. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
  482. :param project: Optional string value to get only boards
  483. of the project.
  484. :param fields: Optional, a list with a specified set of fields
  485. of the resource to be returned.
  486. """
  487. cdict = pecan.request.context.to_policy_values()
  488. policy.authorize('iot:board:get', cdict, cdict)
  489. # /detail should only work against collections
  490. parent = pecan.request.path.split('/')[:-1][-1]
  491. if parent != "boards":
  492. raise exception.HTTPNotFound()
  493. return self._get_boards_collection(status, marker,
  494. limit, sort_key, sort_dir,
  495. project=project,
  496. fields=fields)