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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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. }
  258. def __init__(self, board_ident):
  259. self.board_ident = board_ident
  260. def _get_services_on_board_collection(self, board_uuid, fields=None):
  261. services = objects.ExposedService.list(pecan.request.context,
  262. board_uuid)
  263. return ExposedCollection.get_list(services,
  264. fields=fields)
  265. @expose.expose(ExposedCollection,
  266. status_code=200)
  267. def get_all(self):
  268. """Retrieve a list of services of a board.
  269. """
  270. rpc_board = api_utils.get_rpc_board(self.board_ident)
  271. cdict = pecan.request.context.to_policy_values()
  272. cdict['project_id'] = rpc_board.project
  273. policy.authorize('iot:service_on_board:get', cdict, cdict)
  274. return self._get_services_on_board_collection(rpc_board.uuid)
  275. @expose.expose(wtypes.text, types.uuid_or_name, body=ServiceAction,
  276. status_code=200)
  277. def action(self, service_ident, ServiceAction):
  278. if not ServiceAction.action:
  279. raise exception.MissingParameterValue(
  280. ("Action is not specified."))
  281. if not ServiceAction.parameters:
  282. ServiceAction.parameters = {}
  283. rpc_board = api_utils.get_rpc_board(self.board_ident)
  284. rpc_service = api_utils.get_rpc_service(service_ident)
  285. try:
  286. cdict = pecan.request.context.to_policy_values()
  287. cdict['owner'] = rpc_board.owner
  288. policy.authorize('iot:service_action:post', cdict, cdict)
  289. except exception:
  290. return exception
  291. rpc_board.check_if_online()
  292. result = pecan.request.rpcapi.action_service(pecan.request.context,
  293. rpc_service.uuid,
  294. rpc_board.uuid,
  295. ServiceAction.action)
  296. return result
  297. class BoardsController(rest.RestController):
  298. """REST controller for Boards."""
  299. _subcontroller_map = {
  300. 'plugins': BoardPluginsController,
  301. 'services': BoardServicesController,
  302. }
  303. invalid_sort_key_list = ['extra', 'location']
  304. _custom_actions = {
  305. 'detail': ['GET'],
  306. }
  307. @pecan.expose()
  308. def _lookup(self, ident, *remainder):
  309. try:
  310. ident = types.uuid_or_name.validate(ident)
  311. except exception.InvalidUuidOrName as e:
  312. pecan.abort('400', e.args[0])
  313. if not remainder:
  314. return
  315. subcontroller = self._subcontroller_map.get(remainder[0])
  316. if subcontroller:
  317. return subcontroller(board_ident=ident), remainder[1:]
  318. def _get_boards_collection(self, status, marker, limit,
  319. sort_key, sort_dir,
  320. project=None,
  321. resource_url=None, fields=None):
  322. limit = api_utils.validate_limit(limit)
  323. sort_dir = api_utils.validate_sort_dir(sort_dir)
  324. marker_obj = None
  325. if marker:
  326. marker_obj = objects.Board.get_by_uuid(pecan.request.context,
  327. marker)
  328. if sort_key in self.invalid_sort_key_list:
  329. raise exception.InvalidParameterValue(
  330. ("The sort_key value %(key)s is an invalid field for "
  331. "sorting") % {'key': sort_key})
  332. filters = {}
  333. # bounding the request to a project
  334. if project:
  335. if pecan.request.context.is_admin:
  336. filters['project_id'] = project
  337. else:
  338. msg = ("Project parameter can be used only "
  339. "by the administrator.")
  340. raise wsme.exc.ClientSideError(msg,
  341. status_code=400)
  342. else:
  343. filters['project_id'] = pecan.request.context.project_id
  344. if status:
  345. filters['status'] = status
  346. boards = objects.Board.list(pecan.request.context, limit, marker_obj,
  347. sort_key=sort_key, sort_dir=sort_dir,
  348. filters=filters)
  349. parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
  350. return BoardCollection.convert_with_links(boards, limit,
  351. url=resource_url,
  352. fields=fields,
  353. **parameters)
  354. @expose.expose(Board, types.uuid_or_name, types.listtype)
  355. def get_one(self, board_ident, fields=None):
  356. """Retrieve information about the given board.
  357. :param board_ident: UUID or logical name of a board.
  358. :param fields: Optional, a list with a specified set of fields
  359. of the resource to be returned.
  360. """
  361. cdict = pecan.request.context.to_policy_values()
  362. policy.authorize('iot:board:get', cdict, cdict)
  363. rpc_board = api_utils.get_rpc_board(board_ident)
  364. return Board.convert_with_links(rpc_board, fields=fields)
  365. @expose.expose(BoardCollection, wtypes.text, types.uuid, int, wtypes.text,
  366. wtypes.text, types.listtype, wtypes.text)
  367. def get_all(self, status=None, marker=None,
  368. limit=None, sort_key='id', sort_dir='asc',
  369. fields=None, project=None):
  370. """Retrieve a list of boards.
  371. :param status: Optional string value to get only board in
  372. that status.
  373. :param marker: pagination marker for large data sets.
  374. :param limit: maximum number of resources to return in a single result.
  375. This value cannot be larger than the value of max_limit
  376. in the [api] section of the ironic configuration, or only
  377. max_limit resources will be returned.
  378. :param sort_key: column to sort results by. Default: id.
  379. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
  380. :param fields: Optional, a list with a specified set of fields
  381. of the resource to be returned.
  382. """
  383. cdict = pecan.request.context.to_policy_values()
  384. policy.authorize('iot:board:get', cdict, cdict)
  385. if fields is None:
  386. fields = _DEFAULT_RETURN_FIELDS
  387. return self._get_boards_collection(status, marker,
  388. limit, sort_key, sort_dir,
  389. fields=fields, project=project)
  390. @expose.expose(Board, body=Board, status_code=201)
  391. def post(self, Board):
  392. """Create a new Board.
  393. :param Board: a Board within the request body.
  394. """
  395. context = pecan.request.context
  396. cdict = context.to_policy_values()
  397. policy.authorize('iot:board:create', cdict, cdict)
  398. if not Board.name:
  399. raise exception.MissingParameterValue(
  400. ("Name is not specified."))
  401. if not Board.code:
  402. raise exception.MissingParameterValue(
  403. ("Code is not specified."))
  404. if not Board.location:
  405. raise exception.MissingParameterValue(
  406. ("Location is not specified."))
  407. if Board.name:
  408. if not api_utils.is_valid_board_name(Board.name):
  409. msg = ("Cannot create board with invalid name %(name)s")
  410. raise wsme.exc.ClientSideError(msg % {'name': Board.name},
  411. status_code=400)
  412. new_Board = objects.Board(pecan.request.context,
  413. **Board.as_dict())
  414. new_Board.owner = pecan.request.context.user_id
  415. new_Board.project = pecan.request.context.project_id
  416. new_Location = objects.Location(pecan.request.context,
  417. **Board.location[0].as_dict())
  418. new_Board = pecan.request.rpcapi.create_board(pecan.request.context,
  419. new_Board, new_Location)
  420. return Board.convert_with_links(new_Board)
  421. @expose.expose(None, types.uuid_or_name, status_code=204)
  422. def delete(self, board_ident):
  423. """Delete a board.
  424. :param board_ident: UUID or logical name of a board.
  425. """
  426. context = pecan.request.context
  427. cdict = context.to_policy_values()
  428. policy.authorize('iot:board:delete', cdict, cdict)
  429. rpc_board = api_utils.get_rpc_board(board_ident)
  430. pecan.request.rpcapi.destroy_board(pecan.request.context,
  431. rpc_board.uuid)
  432. @expose.expose(Board, types.uuid_or_name, body=Board, status_code=200)
  433. def patch(self, board_ident, val_Board):
  434. """Update a board.
  435. :param board_ident: UUID or logical name of a board.
  436. :param Board: values to be changed
  437. :return updated_board: updated_board
  438. """
  439. context = pecan.request.context
  440. cdict = context.to_policy_values()
  441. policy.authorize('iot:board:update', cdict, cdict)
  442. board = api_utils.get_rpc_board(board_ident)
  443. val_Board = val_Board.as_dict()
  444. for key in val_Board:
  445. try:
  446. board[key] = val_Board[key]
  447. except Exception:
  448. pass
  449. updated_board = pecan.request.rpcapi.update_board(
  450. pecan.request.context,
  451. board)
  452. return Board.convert_with_links(updated_board)
  453. @expose.expose(BoardCollection, wtypes.text, types.uuid, int, wtypes.text,
  454. wtypes.text, types.listtype, wtypes.text)
  455. def detail(self, status=None, marker=None,
  456. limit=None, sort_key='id', sort_dir='asc',
  457. fields=None, project=None):
  458. """Retrieve a list of boards.
  459. :param status: Optional string value to get only board in
  460. that status.
  461. :param marker: pagination marker for large data sets.
  462. :param limit: maximum number of resources to return in a single result.
  463. This value cannot be larger than the value of max_limit
  464. in the [api] section of the ironic configuration, or only
  465. max_limit resources will be returned.
  466. :param sort_key: column to sort results by. Default: id.
  467. :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
  468. :param project: Optional string value to get only boards
  469. of the project.
  470. :param fields: Optional, a list with a specified set of fields
  471. of the resource to be returned.
  472. """
  473. cdict = pecan.request.context.to_policy_values()
  474. policy.authorize('iot:board:get', cdict, cdict)
  475. # /detail should only work against collections
  476. parent = pecan.request.path.split('/')[:-1][-1]
  477. if parent != "boards":
  478. raise exception.HTTPNotFound()
  479. return self._get_boards_collection(status, marker,
  480. limit, sort_key, sort_dir,
  481. project=project,
  482. fields=fields)