OpenStack Networking (Neutron)
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.
 
 
 
 

841 lines
37 KiB

  1. # Copyright (c) 2012 OpenStack Foundation.
  2. # All Rights Reserved.
  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. import collections
  16. import copy
  17. from neutron_lib.api import attributes
  18. from neutron_lib.api import faults
  19. from neutron_lib.callbacks import events
  20. from neutron_lib.callbacks import registry
  21. from neutron_lib import constants
  22. from neutron_lib.db import api as db_api
  23. from neutron_lib import exceptions
  24. from neutron_lib import rpc as n_rpc
  25. from neutron_lib.services import constants as service_const
  26. from oslo_log import log as logging
  27. from oslo_policy import policy as oslo_policy
  28. from oslo_utils import excutils
  29. import webob.exc
  30. from neutron._i18n import _
  31. from neutron.api import api_common
  32. from neutron.api.v2 import resource as wsgi_resource
  33. from neutron import policy
  34. from neutron import quota
  35. from neutron.quota import resource_registry
  36. LOG = logging.getLogger(__name__)
  37. class Controller(object):
  38. LIST = 'list'
  39. SHOW = 'show'
  40. CREATE = 'create'
  41. UPDATE = 'update'
  42. DELETE = 'delete'
  43. @property
  44. def plugin(self):
  45. return self._plugin
  46. @property
  47. def resource(self):
  48. return self._resource
  49. @property
  50. def attr_info(self):
  51. return self._attr_info
  52. @property
  53. def member_actions(self):
  54. return self._member_actions
  55. @property
  56. def allow_pagination(self):
  57. return self._allow_pagination
  58. @property
  59. def allow_sorting(self):
  60. return self._allow_sorting
  61. def _init_policy_attrs(self):
  62. """Create the list of attributes required by policy.
  63. If the attribute map contains a tenant_id policy, then include
  64. project_id to bring the resource into the brave new world.
  65. :return: sorted list of attributes required by policy
  66. """
  67. policy_attrs = {name for (name, info) in self._attr_info.items()
  68. if info.get('required_by_policy')}
  69. if 'tenant_id' in policy_attrs:
  70. policy_attrs.add('project_id')
  71. # Could use list(), but sorted() makes testing easier.
  72. return sorted(policy_attrs)
  73. def __init__(self, plugin, collection, resource, attr_info,
  74. allow_bulk=False, member_actions=None, parent=None,
  75. allow_pagination=False, allow_sorting=False):
  76. if member_actions is None:
  77. member_actions = []
  78. self._plugin = plugin
  79. self._collection = collection.replace('-', '_')
  80. self._resource = resource.replace('-', '_')
  81. self._attr_info = attr_info
  82. self._allow_bulk = allow_bulk
  83. self._allow_pagination = allow_pagination
  84. self._allow_sorting = allow_sorting
  85. self._native_bulk = self._is_native_bulk_supported()
  86. self._native_pagination = self._is_native_pagination_supported()
  87. self._native_sorting = self._is_native_sorting_supported()
  88. self._filter_validation = self._is_filter_validation_supported()
  89. self._policy_attrs = self._init_policy_attrs()
  90. self._notifier = n_rpc.get_notifier('network')
  91. self._member_actions = member_actions
  92. self._primary_key = self._get_primary_key()
  93. if self._allow_pagination and self._native_pagination:
  94. # Native pagination need native sorting support
  95. if not self._native_sorting:
  96. raise exceptions.Invalid(
  97. _("Native pagination depend on native sorting")
  98. )
  99. if not self._allow_sorting:
  100. LOG.info("Allow sorting is enabled because native "
  101. "pagination requires native sorting")
  102. self._allow_sorting = True
  103. self.parent = parent
  104. if parent:
  105. self._parent_id_name = '%s_id' % parent['member_name']
  106. parent_part = '_%s' % parent['member_name']
  107. else:
  108. self._parent_id_name = None
  109. parent_part = ''
  110. self._plugin_handlers = {
  111. self.LIST: 'get%s_%s' % (parent_part, self._collection),
  112. self.SHOW: 'get%s_%s' % (parent_part, self._resource)
  113. }
  114. for action in [self.CREATE, self.UPDATE, self.DELETE]:
  115. self._plugin_handlers[action] = '%s%s_%s' % (action, parent_part,
  116. self._resource)
  117. def _get_primary_key(self, default_primary_key='id'):
  118. for key, value in self._attr_info.items():
  119. if value.get('primary_key', False):
  120. return key
  121. return default_primary_key
  122. def _is_native_bulk_supported(self):
  123. native_bulk_attr_name = ("_%s__native_bulk_support"
  124. % self._plugin.__class__.__name__)
  125. return getattr(self._plugin, native_bulk_attr_name, False)
  126. def _is_native_pagination_supported(self):
  127. return api_common.is_native_pagination_supported(self._plugin)
  128. def _is_native_sorting_supported(self):
  129. return api_common.is_native_sorting_supported(self._plugin)
  130. def _is_filter_validation_supported(self):
  131. return api_common.is_filter_validation_supported(self._plugin)
  132. def _exclude_attributes_by_policy(self, context, data):
  133. """Identifies attributes to exclude according to authZ policies.
  134. Return a list of attribute names which should be stripped from the
  135. response returned to the user because the user is not authorized
  136. to see them.
  137. """
  138. attributes_to_exclude = []
  139. for attr_name in data.keys():
  140. # TODO(amotoki): At now, all attribute maps have tenant_id and
  141. # determine excluded attributes based on tenant_id.
  142. # We need to migrate tenant_id to project_id later
  143. # as attr_info is referred to in various places and we need
  144. # to check all logis carefully.
  145. if attr_name == 'project_id':
  146. continue
  147. attr_data = self._attr_info.get(attr_name)
  148. if attr_data and attr_data['is_visible']:
  149. if policy.check(
  150. context,
  151. '%s:%s' % (self._plugin_handlers[self.SHOW],
  152. attr_name),
  153. data,
  154. might_not_exist=True,
  155. pluralized=self._collection):
  156. # this attribute is visible, check next one
  157. continue
  158. # if the code reaches this point then either the policy check
  159. # failed or the attribute was not visible in the first place
  160. attributes_to_exclude.append(attr_name)
  161. # TODO(amotoki): As mentioned in the above TODO,
  162. # we treat project_id and tenant_id equivalently.
  163. # This should be migrated to project_id in Ocata.
  164. if attr_name == 'tenant_id':
  165. attributes_to_exclude.append('project_id')
  166. return attributes_to_exclude
  167. def _view(self, context, data, fields_to_strip=None):
  168. """Build a view of an API resource.
  169. :param context: the neutron context
  170. :param data: the object for which a view is being created
  171. :param fields_to_strip: attributes to remove from the view
  172. :returns: a view of the object which includes only attributes
  173. visible according to API resource declaration and authZ policies.
  174. """
  175. fields_to_strip = ((fields_to_strip or []) +
  176. self._exclude_attributes_by_policy(context, data))
  177. return self._filter_attributes(data, fields_to_strip)
  178. def _filter_attributes(self, data, fields_to_strip=None):
  179. if not fields_to_strip:
  180. return data
  181. return dict(item for item in data.items()
  182. if (item[0] not in fields_to_strip))
  183. def _do_field_list(self, original_fields):
  184. fields_to_add = None
  185. # don't do anything if fields were not specified in the request
  186. if original_fields:
  187. fields_to_add = [attr for attr in self._policy_attrs
  188. if attr not in original_fields]
  189. original_fields.extend(self._policy_attrs)
  190. return original_fields, fields_to_add
  191. def __getattr__(self, name):
  192. if name in self._member_actions:
  193. @db_api.retry_db_errors
  194. def _handle_action(request, id, **kwargs):
  195. arg_list = [request.context, id]
  196. # Ensure policy engine is initialized
  197. policy.init()
  198. # Fetch the resource and verify if the user can access it
  199. try:
  200. parent_id = kwargs.get(self._parent_id_name)
  201. resource = self._item(request,
  202. id,
  203. do_authz=True,
  204. field_list=None,
  205. parent_id=parent_id)
  206. except oslo_policy.PolicyNotAuthorized:
  207. msg = _('The resource could not be found.')
  208. raise webob.exc.HTTPNotFound(msg)
  209. body = kwargs.pop('body', None)
  210. # Explicit comparison with None to distinguish from {}
  211. if body is not None:
  212. arg_list.append(body)
  213. # It is ok to raise a 403 because accessibility to the
  214. # object was checked earlier in this method
  215. policy.enforce(request.context,
  216. name,
  217. resource,
  218. pluralized=self._collection)
  219. ret_value = getattr(self._plugin, name)(*arg_list, **kwargs)
  220. # It is simply impossible to predict whether one of this
  221. # actions alters resource usage. For instance a tenant port
  222. # is created when a router interface is added. Therefore it is
  223. # important to mark as dirty resources whose counters have
  224. # been altered by this operation
  225. resource_registry.set_resources_dirty(request.context)
  226. return ret_value
  227. return _handle_action
  228. else:
  229. raise AttributeError()
  230. def _get_pagination_helper(self, request):
  231. if self._allow_pagination and self._native_pagination:
  232. return api_common.PaginationNativeHelper(request,
  233. self._primary_key)
  234. elif self._allow_pagination:
  235. return api_common.PaginationEmulatedHelper(request,
  236. self._primary_key)
  237. return api_common.NoPaginationHelper(request, self._primary_key)
  238. def _get_sorting_helper(self, request):
  239. if self._allow_sorting and self._native_sorting:
  240. return api_common.SortingNativeHelper(request, self._attr_info)
  241. elif self._allow_sorting:
  242. return api_common.SortingEmulatedHelper(request, self._attr_info)
  243. return api_common.NoSortingHelper(request, self._attr_info)
  244. def _items(self, request, do_authz=False, parent_id=None):
  245. """Retrieves and formats a list of elements of the requested entity."""
  246. # NOTE(salvatore-orlando): The following ensures that fields which
  247. # are needed for authZ policy validation are not stripped away by the
  248. # plugin before returning.
  249. original_fields, fields_to_add = self._do_field_list(
  250. api_common.list_args(request, 'fields'))
  251. filters = api_common.get_filters(
  252. request, self._attr_info,
  253. ['fields', 'sort_key', 'sort_dir',
  254. 'limit', 'marker', 'page_reverse'],
  255. is_filter_validation_supported=self._filter_validation)
  256. kwargs = {'filters': filters,
  257. 'fields': original_fields}
  258. sorting_helper = self._get_sorting_helper(request)
  259. pagination_helper = self._get_pagination_helper(request)
  260. sorting_helper.update_args(kwargs)
  261. sorting_helper.update_fields(original_fields, fields_to_add)
  262. pagination_helper.update_args(kwargs)
  263. pagination_helper.update_fields(original_fields, fields_to_add)
  264. if parent_id:
  265. kwargs[self._parent_id_name] = parent_id
  266. obj_getter = getattr(self._plugin, self._plugin_handlers[self.LIST])
  267. obj_list = obj_getter(request.context, **kwargs)
  268. obj_list = sorting_helper.sort(obj_list)
  269. obj_list = pagination_helper.paginate(obj_list)
  270. # Check authz
  271. if do_authz:
  272. # FIXME(salvatore-orlando): obj_getter might return references to
  273. # other resources. Must check authZ on them too.
  274. # Omit items from list that should not be visible
  275. tmp_list = []
  276. for obj in obj_list:
  277. self._set_parent_id_into_ext_resources_request(
  278. request, obj, parent_id, is_get=True)
  279. if policy.check(
  280. request.context, self._plugin_handlers[self.SHOW],
  281. obj, plugin=self._plugin, pluralized=self._collection):
  282. tmp_list.append(obj)
  283. obj_list = tmp_list
  284. # Use the first element in the list for discriminating which attributes
  285. # should be filtered out because of authZ policies
  286. # fields_to_add contains a list of attributes added for request policy
  287. # checks but that were not required by the user. They should be
  288. # therefore stripped
  289. fields_to_strip = fields_to_add or []
  290. if obj_list:
  291. fields_to_strip += self._exclude_attributes_by_policy(
  292. request.context, obj_list[0])
  293. collection = {self._collection:
  294. [self._filter_attributes(
  295. obj, fields_to_strip=fields_to_strip)
  296. for obj in obj_list]}
  297. pagination_links = pagination_helper.get_links(obj_list)
  298. if pagination_links:
  299. collection[self._collection + "_links"] = pagination_links
  300. # Synchronize usage trackers, if needed
  301. resource_registry.resync_resource(
  302. request.context, self._resource, request.context.tenant_id)
  303. return collection
  304. def _item(self, request, id, do_authz=False, field_list=None,
  305. parent_id=None):
  306. """Retrieves and formats a single element of the requested entity."""
  307. kwargs = {'fields': field_list}
  308. action = self._plugin_handlers[self.SHOW]
  309. if parent_id:
  310. kwargs[self._parent_id_name] = parent_id
  311. obj_getter = getattr(self._plugin, action)
  312. obj = obj_getter(request.context, id, **kwargs)
  313. self._set_parent_id_into_ext_resources_request(
  314. request, obj, parent_id, is_get=True)
  315. # Check authz
  316. # FIXME(salvatore-orlando): obj_getter might return references to
  317. # other resources. Must check authZ on them too.
  318. if do_authz:
  319. policy.enforce(request.context,
  320. action,
  321. obj,
  322. pluralized=self._collection)
  323. return obj
  324. @db_api.retry_db_errors
  325. def index(self, request, **kwargs):
  326. """Returns a list of the requested entity."""
  327. parent_id = kwargs.get(self._parent_id_name)
  328. # Ensure policy engine is initialized
  329. policy.init()
  330. return self._items(request, True, parent_id)
  331. @db_api.retry_db_errors
  332. def show(self, request, id, **kwargs):
  333. """Returns detailed information about the requested entity."""
  334. try:
  335. # NOTE(salvatore-orlando): The following ensures that fields
  336. # which are needed for authZ policy validation are not stripped
  337. # away by the plugin before returning.
  338. field_list, added_fields = self._do_field_list(
  339. api_common.list_args(request, "fields"))
  340. parent_id = kwargs.get(self._parent_id_name)
  341. # Ensure policy engine is initialized
  342. policy.init()
  343. return {self._resource:
  344. self._view(request.context,
  345. self._item(request,
  346. id,
  347. do_authz=True,
  348. field_list=field_list,
  349. parent_id=parent_id),
  350. fields_to_strip=added_fields)}
  351. except oslo_policy.PolicyNotAuthorized:
  352. # To avoid giving away information, pretend that it
  353. # doesn't exist
  354. msg = _('The resource could not be found.')
  355. raise webob.exc.HTTPNotFound(msg)
  356. def _emulate_bulk_create(self, obj_creator, request, body, parent_id=None):
  357. objs = []
  358. try:
  359. for item in body[self._collection]:
  360. kwargs = {self._resource: item}
  361. if parent_id:
  362. kwargs[self._parent_id_name] = parent_id
  363. fields_to_strip = self._exclude_attributes_by_policy(
  364. request.context, item)
  365. objs.append(self._filter_attributes(
  366. obj_creator(request.context, **kwargs),
  367. fields_to_strip=fields_to_strip))
  368. return objs
  369. # Note(salvatore-orlando): broad catch as in theory a plugin
  370. # could raise any kind of exception
  371. except Exception:
  372. with excutils.save_and_reraise_exception():
  373. for obj in objs:
  374. obj_deleter = getattr(self._plugin,
  375. self._plugin_handlers[self.DELETE])
  376. try:
  377. kwargs = ({self._parent_id_name: parent_id}
  378. if parent_id else {})
  379. obj_deleter(request.context, obj['id'], **kwargs)
  380. except Exception:
  381. # broad catch as our only purpose is to log the
  382. # exception
  383. LOG.exception("Unable to undo add for "
  384. "%(resource)s %(id)s",
  385. {'resource': self._resource,
  386. 'id': obj['id']})
  387. # TODO(salvatore-orlando): The object being processed when the
  388. # plugin raised might have been created or not in the db.
  389. # We need a way for ensuring that if it has been created,
  390. # it is then deleted
  391. def create(self, request, body=None, **kwargs):
  392. self._notifier.info(request.context,
  393. self._resource + '.create.start',
  394. body)
  395. return self._create(request, body, **kwargs)
  396. @db_api.retry_db_errors
  397. def _create(self, request, body, **kwargs):
  398. """Creates a new instance of the requested entity."""
  399. parent_id = kwargs.get(self._parent_id_name)
  400. try:
  401. body = Controller.prepare_request_body(
  402. request.context, body, True, self._resource, self._attr_info,
  403. allow_bulk=self._allow_bulk)
  404. except Exception as e:
  405. LOG.warning("An exception happened while processing the request "
  406. "body. The exception message is [%s].", e)
  407. raise e
  408. action = self._plugin_handlers[self.CREATE]
  409. # Check authz
  410. if self._collection in body:
  411. # Have to account for bulk create
  412. items = body[self._collection]
  413. else:
  414. items = [body]
  415. # Ensure policy engine is initialized
  416. policy.init()
  417. # Store requested resource amounts grouping them by tenant
  418. # This won't work with multiple resources. However because of the
  419. # current structure of this controller there will hardly be more than
  420. # one resource for which reservations are being made
  421. request_deltas = collections.defaultdict(int)
  422. for item in items:
  423. self._validate_network_tenant_ownership(request,
  424. item[self._resource])
  425. # For ext resources policy check, we support two types, such as
  426. # parent_id is in request body, another type is parent_id is in
  427. # request url, which we can get from kwargs.
  428. self._set_parent_id_into_ext_resources_request(
  429. request, item[self._resource], parent_id)
  430. policy.enforce(request.context,
  431. action,
  432. item[self._resource],
  433. pluralized=self._collection)
  434. if 'tenant_id' not in item[self._resource]:
  435. # no tenant_id - no quota check
  436. continue
  437. tenant_id = item[self._resource]['tenant_id']
  438. request_deltas[tenant_id] += 1
  439. # Quota enforcement
  440. reservations = []
  441. try:
  442. for (tenant, delta) in request_deltas.items():
  443. reservation = quota.QUOTAS.make_reservation(
  444. request.context,
  445. tenant,
  446. {self._resource: delta},
  447. self._plugin)
  448. reservations.append(reservation)
  449. except exceptions.QuotaResourceUnknown as e:
  450. # We don't want to quota this resource
  451. LOG.debug(e)
  452. def notify(create_result):
  453. # Ensure usage trackers for all resources affected by this API
  454. # operation are marked as dirty
  455. with db_api.CONTEXT_WRITER.using(request.context):
  456. # Commit the reservation(s)
  457. for reservation in reservations:
  458. quota.QUOTAS.commit_reservation(
  459. request.context, reservation.reservation_id)
  460. resource_registry.set_resources_dirty(request.context)
  461. notifier_method = self._resource + '.create.end'
  462. self._notifier.info(request.context,
  463. notifier_method,
  464. create_result)
  465. registry.publish(self._resource, events.BEFORE_RESPONSE, self,
  466. payload=events.APIEventPayload(
  467. request.context, notifier_method, action,
  468. request_body=body,
  469. states=({}, create_result,),
  470. collection_name=self._collection))
  471. return create_result
  472. def do_create(body, bulk=False, emulated=False):
  473. kwargs = {self._parent_id_name: parent_id} if parent_id else {}
  474. if bulk and not emulated:
  475. obj_creator = getattr(self._plugin, "%s_bulk" % action)
  476. else:
  477. obj_creator = getattr(self._plugin, action)
  478. try:
  479. if emulated:
  480. return self._emulate_bulk_create(obj_creator, request,
  481. body, parent_id)
  482. else:
  483. if self._collection in body:
  484. # This is weird but fixing it requires changes to the
  485. # plugin interface
  486. kwargs.update({self._collection: body})
  487. else:
  488. kwargs.update({self._resource: body})
  489. return obj_creator(request.context, **kwargs)
  490. except Exception:
  491. # In case of failure the plugin will always raise an
  492. # exception. Cancel the reservation
  493. with excutils.save_and_reraise_exception():
  494. for reservation in reservations:
  495. quota.QUOTAS.cancel_reservation(
  496. request.context, reservation.reservation_id)
  497. if self._collection in body and self._native_bulk:
  498. # plugin does atomic bulk create operations
  499. objs = do_create(body, bulk=True)
  500. # Use first element of list to discriminate attributes which
  501. # should be removed because of authZ policies
  502. fields_to_strip = self._exclude_attributes_by_policy(
  503. request.context, objs[0])
  504. return notify({self._collection: [self._filter_attributes(
  505. obj, fields_to_strip=fields_to_strip)
  506. for obj in objs]})
  507. else:
  508. if self._collection in body:
  509. # Emulate atomic bulk behavior
  510. objs = do_create(body, bulk=True, emulated=True)
  511. return notify({self._collection: objs})
  512. else:
  513. obj = do_create(body)
  514. return notify({self._resource: self._view(request.context,
  515. obj)})
  516. def delete(self, request, id, **kwargs):
  517. """Deletes the specified entity."""
  518. if request.body:
  519. msg = _('Request body is not supported in DELETE.')
  520. raise webob.exc.HTTPBadRequest(msg)
  521. self._notifier.info(request.context,
  522. self._resource + '.delete.start',
  523. {self._resource + '_id': id})
  524. return self._delete(request, id, **kwargs)
  525. @db_api.retry_db_errors
  526. def _delete(self, request, id, **kwargs):
  527. action = self._plugin_handlers[self.DELETE]
  528. # Check authz
  529. policy.init()
  530. parent_id = kwargs.get(self._parent_id_name)
  531. obj = self._item(request, id, parent_id=parent_id)
  532. try:
  533. policy.enforce(request.context,
  534. action,
  535. obj,
  536. pluralized=self._collection)
  537. except oslo_policy.PolicyNotAuthorized:
  538. # To avoid giving away information, pretend that it
  539. # doesn't exist if policy does not authorize SHOW
  540. with excutils.save_and_reraise_exception() as ctxt:
  541. if not policy.check(request.context,
  542. self._plugin_handlers[self.SHOW],
  543. obj,
  544. pluralized=self._collection):
  545. ctxt.reraise = False
  546. msg = _('The resource could not be found.')
  547. raise webob.exc.HTTPNotFound(msg)
  548. obj_deleter = getattr(self._plugin, action)
  549. obj_deleter(request.context, id, **kwargs)
  550. # A delete operation usually alters resource usage, so mark affected
  551. # usage trackers as dirty
  552. resource_registry.set_resources_dirty(request.context)
  553. notifier_method = self._resource + '.delete.end'
  554. result = {self._resource: self._view(request.context, obj)}
  555. notifier_payload = {self._resource + '_id': id}
  556. notifier_payload.update(result)
  557. self._notifier.info(request.context,
  558. notifier_method,
  559. notifier_payload)
  560. registry.publish(self._resource, events.BEFORE_RESPONSE, self,
  561. payload=events.APIEventPayload(
  562. request.context, notifier_method, action,
  563. states=({}, obj, result,),
  564. collection_name=self._collection))
  565. def update(self, request, id, body=None, **kwargs):
  566. """Updates the specified entity's attributes."""
  567. try:
  568. payload = body.copy()
  569. except AttributeError:
  570. msg = _("Invalid format: %s") % request.body
  571. raise exceptions.BadRequest(resource='body', msg=msg)
  572. payload['id'] = id
  573. self._notifier.info(request.context,
  574. self._resource + '.update.start',
  575. payload)
  576. return self._update(request, id, body, **kwargs)
  577. @db_api.retry_db_errors
  578. def _update(self, request, id, body, **kwargs):
  579. try:
  580. body = Controller.prepare_request_body(
  581. request.context, body, False, self._resource, self._attr_info,
  582. allow_bulk=self._allow_bulk)
  583. except Exception as e:
  584. LOG.warning("An exception happened while processing the request "
  585. "body. The exception message is [%s].", e)
  586. raise e
  587. action = self._plugin_handlers[self.UPDATE]
  588. # Load object to check authz
  589. # but pass only attributes in the original body and required
  590. # by the policy engine to the policy 'brain'
  591. field_list = [name for (name, value) in self._attr_info.items()
  592. if (value.get('required_by_policy') or
  593. value.get('primary_key') or
  594. 'default' not in value)]
  595. # Ensure policy engine is initialized
  596. policy.init()
  597. parent_id = kwargs.get(self._parent_id_name)
  598. # If the parent_id exist, we should get orig_obj with
  599. # self._parent_id_name field.
  600. if parent_id and self._parent_id_name not in field_list:
  601. field_list.append(self._parent_id_name)
  602. orig_obj = self._item(request, id, field_list=field_list,
  603. parent_id=parent_id)
  604. orig_object_copy = copy.copy(orig_obj)
  605. orig_obj.update(body[self._resource])
  606. # Make a list of attributes to be updated to inform the policy engine
  607. # which attributes are set explicitly so that it can distinguish them
  608. # from the ones that are set to their default values.
  609. orig_obj[constants.ATTRIBUTES_TO_UPDATE] = body[self._resource].keys()
  610. # Then get the ext_parent_id, format to ext_parent_parent_resource_id
  611. if self._parent_id_name in orig_obj:
  612. self._set_parent_id_into_ext_resources_request(
  613. request, orig_obj, parent_id)
  614. try:
  615. policy.enforce(request.context,
  616. action,
  617. orig_obj,
  618. pluralized=self._collection)
  619. except oslo_policy.PolicyNotAuthorized:
  620. # To avoid giving away information, pretend that it
  621. # doesn't exist if policy does not authorize SHOW
  622. with excutils.save_and_reraise_exception() as ctxt:
  623. if not policy.check(request.context,
  624. self._plugin_handlers[self.SHOW],
  625. orig_obj,
  626. pluralized=self._collection):
  627. ctxt.reraise = False
  628. msg = _('The resource could not be found.')
  629. raise webob.exc.HTTPNotFound(msg)
  630. if self._native_bulk and hasattr(self._plugin, "%s_bulk" % action):
  631. obj_updater = getattr(self._plugin, "%s_bulk" % action)
  632. else:
  633. obj_updater = getattr(self._plugin, action)
  634. kwargs = {self._resource: body}
  635. if parent_id:
  636. kwargs[self._parent_id_name] = parent_id
  637. obj = obj_updater(request.context, id, **kwargs)
  638. # Usually an update operation does not alter resource usage, but as
  639. # there might be side effects it might be worth checking for changes
  640. # in resource usage here as well (e.g: a tenant port is created when a
  641. # router interface is added)
  642. resource_registry.set_resources_dirty(request.context)
  643. result = {self._resource: self._view(request.context, obj)}
  644. notifier_method = self._resource + '.update.end'
  645. self._notifier.info(request.context, notifier_method, result)
  646. registry.publish(self._resource, events.BEFORE_RESPONSE, self,
  647. payload=events.APIEventPayload(
  648. request.context, notifier_method, action,
  649. request_body=body,
  650. states=(orig_object_copy, result,),
  651. collection_name=self._collection))
  652. return result
  653. @staticmethod
  654. def prepare_request_body(context, body, is_create, resource, attr_info,
  655. allow_bulk=False):
  656. """Verifies required attributes are in request body.
  657. Also checking that an attribute is only specified if it is allowed
  658. for the given operation (create/update).
  659. Attribute with default values are considered to be optional.
  660. body argument must be the deserialized body.
  661. """
  662. collection = resource + "s"
  663. if not body:
  664. raise webob.exc.HTTPBadRequest(_("Resource body required"))
  665. LOG.debug("Request body: %(body)s", {'body': body})
  666. try:
  667. if collection in body:
  668. if not allow_bulk:
  669. raise webob.exc.HTTPBadRequest(_("Bulk operation "
  670. "not supported"))
  671. if not body[collection]:
  672. raise webob.exc.HTTPBadRequest(_("Resources required"))
  673. try:
  674. bulk_body = [
  675. Controller.prepare_request_body(
  676. context, item if resource in item
  677. else {resource: item}, is_create, resource,
  678. attr_info, allow_bulk) for item in body[collection]
  679. ]
  680. return {collection: bulk_body}
  681. except Exception as e:
  682. LOG.warning(
  683. "An exception happened while processing the request "
  684. "body. The exception message is [%s].", e)
  685. raise e
  686. res_dict = body.get(resource)
  687. except (AttributeError, TypeError):
  688. msg = _("Body contains invalid data")
  689. raise webob.exc.HTTPBadRequest(msg)
  690. if res_dict is None:
  691. msg = _("Unable to find '%s' in request body") % resource
  692. raise webob.exc.HTTPBadRequest(msg)
  693. if not isinstance(res_dict, dict):
  694. msg = _("Object '%s' contains invalid data") % resource
  695. raise webob.exc.HTTPBadRequest(msg)
  696. attr_ops = attributes.AttributeInfo(attr_info)
  697. attr_ops.populate_project_id(context, res_dict, is_create)
  698. attributes.populate_project_info(attr_info)
  699. attr_ops.verify_attributes(res_dict)
  700. if is_create: # POST
  701. attr_ops.fill_post_defaults(
  702. res_dict, exc_cls=webob.exc.HTTPBadRequest)
  703. else: # PUT
  704. for attr, attr_vals in attr_info.items():
  705. if attr in res_dict and not attr_vals['allow_put']:
  706. msg = _("Cannot update read-only attribute %s") % attr
  707. raise webob.exc.HTTPBadRequest(msg)
  708. attr_ops.convert_values(res_dict, exc_cls=webob.exc.HTTPBadRequest)
  709. return body
  710. def _validate_network_tenant_ownership(self, request, resource_item):
  711. # TODO(salvatore-orlando): consider whether this check can be folded
  712. # in the policy engine
  713. if (request.context.is_admin or request.context.is_advsvc or
  714. self._resource not in ('port', 'subnet')):
  715. return
  716. network = self._plugin.get_network(
  717. request.context,
  718. resource_item['network_id'])
  719. # do not perform the check on shared networks
  720. if network.get('shared'):
  721. return
  722. network_owner = network['tenant_id']
  723. if network_owner != resource_item['tenant_id']:
  724. # NOTE(kevinbenton): we raise a 404 to hide the existence of the
  725. # network from the tenant since they don't have access to it.
  726. msg = _('The resource could not be found.')
  727. raise webob.exc.HTTPNotFound(msg)
  728. def _set_parent_id_into_ext_resources_request(
  729. self, request, resource_item, parent_id, is_get=False):
  730. if not parent_id:
  731. return
  732. # This will pass most create/update/delete cases
  733. if not is_get and (request.context.is_admin or
  734. request.context.is_advsvc or
  735. self.parent['member_name'] not in
  736. service_const.EXT_PARENT_RESOURCE_MAPPING or
  737. resource_item.get(self._parent_id_name)):
  738. return
  739. # Then we arrive here, that means the request or get obj contains
  740. # ext_parent. If this func is called by list/get, and it contains
  741. # _parent_id_name. We need to re-add the ex_parent prefix to policy.
  742. if is_get:
  743. if (not request.context.is_admin or
  744. not request.context.is_advsvc and
  745. self.parent['member_name'] in
  746. service_const.EXT_PARENT_RESOURCE_MAPPING):
  747. resource_item.setdefault(
  748. "%s_%s" % (constants.EXT_PARENT_PREFIX,
  749. self._parent_id_name),
  750. parent_id)
  751. # If this func is called by create/update/delete, we just add.
  752. else:
  753. resource_item.setdefault(
  754. "%s_%s" % (constants.EXT_PARENT_PREFIX, self._parent_id_name),
  755. parent_id)
  756. def create_resource(collection, resource, plugin, params, allow_bulk=False,
  757. member_actions=None, parent=None, allow_pagination=False,
  758. allow_sorting=False):
  759. controller = Controller(plugin, collection, resource, params, allow_bulk,
  760. member_actions=member_actions, parent=parent,
  761. allow_pagination=allow_pagination,
  762. allow_sorting=allow_sorting)
  763. return wsgi_resource.Resource(controller, faults.FAULT_MAP)