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.

base.py 34KB

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