Coverage for nova/api/openstack/compute/volumes.py: 95%
384 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2011 Justin Santa Barbara
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.
16"""The volumes extension."""
18from oslo_utils import strutils
19from webob import exc
21from nova.api.openstack import api_version_request
22from nova.api.openstack.api_version_request \
23 import MAX_PROXY_API_SUPPORT_VERSION
24from nova.api.openstack import common
25from nova.api.openstack.compute.schemas import volume_attachment as volume_attachment_schema # noqa: E501
26from nova.api.openstack.compute.schemas import volumes as volumes_schema
27from nova.api.openstack import wsgi
28from nova.api import validation
29from nova.compute import api as compute
30from nova.compute import vm_states
31from nova import exception
32from nova.i18n import _
33from nova import objects
34from nova.policies import volumes as vol_policies
35from nova.policies import volumes_attachments as va_policies
36from nova.volume import cinder
39def _translate_volume_detail_view(context, vol):
40 """Maps keys for volumes details view."""
42 d = _translate_volume_summary_view(context, vol)
44 # No additional data / lookups at the moment
46 return d
49def _translate_volume_summary_view(context, vol):
50 """Maps keys for volumes summary view."""
51 d = {}
53 d['id'] = vol['id']
54 d['status'] = vol['status']
55 d['size'] = vol['size']
56 d['availabilityZone'] = vol['availability_zone']
57 d['createdAt'] = vol['created_at']
59 if vol['attach_status'] == 'attached':
60 # NOTE(ildikov): The attachments field in the volume info that
61 # Cinder sends is converted to an OrderedDict with the
62 # instance_uuid as key to make it easier for the multiattach
63 # feature to check the required information. Multiattach will
64 # be enable in the Nova API in Newton.
65 # The format looks like the following:
66 # attachments = {'instance_uuid': {
67 # 'attachment_id': 'attachment_uuid',
68 # 'mountpoint': '/dev/sda/
69 # }
70 # }
71 attachment = list(vol['attachments'].items())[0]
72 d['attachments'] = [
73 {
74 'id': vol['id'],
75 'volumeId': vol['id'],
76 'serverId': attachment[0],
77 }
78 ]
80 mountpoint = attachment[1].get('mountpoint')
81 if mountpoint: 81 ↛ 87line 81 didn't jump to line 87 because the condition on line 81 was always true
82 d['attachments'][0]['device'] = mountpoint
84 else:
85 d['attachments'] = [{}]
87 d['displayName'] = vol['display_name']
88 d['displayDescription'] = vol['display_description']
90 if vol['volume_type_id'] and vol.get('volume_type'): 90 ↛ 93line 90 didn't jump to line 93 because the condition on line 90 was always true
91 d['volumeType'] = vol['volume_type']['name']
92 else:
93 d['volumeType'] = vol['volume_type_id']
95 d['snapshotId'] = vol['snapshot_id']
97 if vol.get('volume_metadata'):
98 d['metadata'] = vol.get('volume_metadata')
99 else:
100 d['metadata'] = {}
102 return d
105class VolumeController(wsgi.Controller):
106 """The Volumes API controller for the OpenStack API."""
108 def __init__(self):
109 super(VolumeController, self).__init__()
110 self.volume_api = cinder.API()
112 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
113 @wsgi.expected_errors(404)
114 @validation.query_schema(volumes_schema.show_query)
115 def show(self, req, id):
116 """Return data about the given volume."""
117 context = req.environ['nova.context']
118 context.can(vol_policies.POLICY_NAME % 'show',
119 target={'project_id': context.project_id})
121 try:
122 vol = self.volume_api.get(context, id)
123 except exception.VolumeNotFound as e:
124 raise exc.HTTPNotFound(explanation=e.format_message())
126 return {'volume': _translate_volume_detail_view(context, vol)}
128 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
129 @wsgi.response(202)
130 @wsgi.expected_errors((400, 404))
131 def delete(self, req, id):
132 """Delete a volume."""
133 context = req.environ['nova.context']
134 context.can(vol_policies.POLICY_NAME % 'delete',
135 target={'project_id': context.project_id})
137 try:
138 self.volume_api.delete(context, id)
139 except exception.InvalidInput as e:
140 raise exc.HTTPBadRequest(explanation=e.format_message())
141 except exception.VolumeNotFound as e:
142 raise exc.HTTPNotFound(explanation=e.format_message())
144 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
145 @wsgi.expected_errors(())
146 @validation.query_schema(volumes_schema.index_query)
147 def index(self, req):
148 """Returns a summary list of volumes."""
149 context = req.environ['nova.context']
150 context.can(vol_policies.POLICY_NAME % 'list',
151 target={'project_id': context.project_id})
152 return self._items(req, entity_maker=_translate_volume_summary_view)
154 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
155 @wsgi.expected_errors(())
156 @validation.query_schema(volumes_schema.detail_query)
157 def detail(self, req):
158 """Returns a detailed list of volumes."""
159 context = req.environ['nova.context']
160 context.can(vol_policies.POLICY_NAME % 'detail',
161 target={'project_id': context.project_id})
162 return self._items(req, entity_maker=_translate_volume_detail_view)
164 def _items(self, req, entity_maker):
165 """Returns a list of volumes, transformed through entity_maker."""
166 context = req.environ['nova.context']
168 volumes = self.volume_api.get_all(context)
169 limited_list = common.limited(volumes, req)
170 res = [entity_maker(context, vol) for vol in limited_list]
171 return {'volumes': res}
173 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
174 @wsgi.expected_errors((400, 403, 404))
175 @validation.schema(volumes_schema.create)
176 def create(self, req, body):
177 """Creates a new volume."""
178 context = req.environ['nova.context']
179 context.can(vol_policies.POLICY_NAME % 'create',
180 target={'project_id': context.project_id})
182 vol = body['volume']
184 vol_type = vol.get('volume_type')
185 metadata = vol.get('metadata')
186 snapshot_id = vol.get('snapshot_id', None)
188 if snapshot_id is not None:
189 try:
190 snapshot = self.volume_api.get_snapshot(context, snapshot_id)
191 except exception.SnapshotNotFound as e:
192 raise exc.HTTPNotFound(explanation=e.format_message())
193 else:
194 snapshot = None
196 size = vol.get('size', None)
197 if size is None and snapshot is not None: 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true
198 size = snapshot['volume_size']
200 availability_zone = vol.get('availability_zone')
202 try:
203 new_volume = self.volume_api.create(
204 context,
205 size,
206 vol.get('display_name'),
207 vol.get('display_description'),
208 snapshot=snapshot,
209 volume_type=vol_type,
210 metadata=metadata,
211 availability_zone=availability_zone
212 )
213 except exception.InvalidInput as err:
214 raise exc.HTTPBadRequest(explanation=err.format_message())
215 except exception.OverQuota as err:
216 raise exc.HTTPForbidden(explanation=err.format_message())
218 # TODO(vish): Instance should be None at db layer instead of
219 # trying to lazy load, but for now we turn it into
220 # a dict to avoid an error.
221 retval = _translate_volume_detail_view(context, dict(new_volume))
222 result = {'volume': retval}
224 location = '%s/%s' % (req.url, new_volume['id'])
226 return wsgi.ResponseObject(result, headers=dict(location=location))
229def _translate_attachment_detail_view(
230 bdm,
231 show_tag=False,
232 show_delete_on_termination=False,
233 show_attachment_id_bdm_uuid=False,
234):
235 """Maps keys for attachment details view.
237 :param bdm: BlockDeviceMapping object for an attached volume
238 :param show_tag: True if the "tag" field should be in the response, False
239 to exclude the "tag" field from the response
240 :param show_delete_on_termination: True if the "delete_on_termination"
241 field should be in the response, False to exclude the
242 "delete_on_termination" field from the response
243 :param show_attachment_id_bdm_uuid: True if the "attachment_id" and
244 "bdm_uuid" fields should be in the response. Also controls when the
245 "id" field is included.
246 """
248 d = {}
250 if not show_attachment_id_bdm_uuid:
251 d['id'] = bdm.volume_id
253 d['volumeId'] = bdm.volume_id
255 d['serverId'] = bdm.instance_uuid
257 if bdm.device_name: 257 ↛ 260line 257 didn't jump to line 260 because the condition on line 257 was always true
258 d['device'] = bdm.device_name
260 if show_tag:
261 d['tag'] = bdm.tag
263 if show_delete_on_termination:
264 d['delete_on_termination'] = bdm.delete_on_termination
266 if show_attachment_id_bdm_uuid:
267 d['attachment_id'] = bdm.attachment_id
268 d['bdm_uuid'] = bdm.uuid
270 return d
273def _check_request_version(req, min_version, method, server_id, server_state):
274 if not api_version_request.is_supported(req, min_version=min_version):
275 exc_inv = exception.InstanceInvalidState(
276 attr='vm_state',
277 instance_uuid=server_id,
278 state=server_state,
279 method=method)
280 common.raise_http_conflict_for_instance_invalid_state(
281 exc_inv,
282 method,
283 server_id)
286class VolumeAttachmentController(wsgi.Controller):
287 """The volume attachment API controller for the OpenStack API.
289 A child resource of the server. Note that we use the volume id
290 as the ID of the attachment (though this is not guaranteed externally)
292 """
294 def __init__(self):
295 self.compute_api = compute.API()
296 self.volume_api = cinder.API()
297 super(VolumeAttachmentController, self).__init__()
299 @wsgi.expected_errors(404)
300 @validation.query_schema(volumes_schema.index_query_275, '2.75')
301 @validation.query_schema(volumes_schema.index_query, '2.0', '2.74')
302 def index(self, req, server_id):
303 """Returns the list of volume attachments for a given instance."""
304 context = req.environ['nova.context']
305 instance = common.get_instance(self.compute_api, context, server_id)
306 context.can(va_policies.POLICY_ROOT % 'index',
307 target={'project_id': instance.project_id})
309 bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
310 context, instance.uuid)
311 limited_list = common.limited(bdms, req)
313 results = []
314 show_tag = api_version_request.is_supported(req, '2.70')
315 show_delete_on_termination = api_version_request.is_supported(
316 req, '2.79')
317 show_attachment_id_bdm_uuid = api_version_request.is_supported(
318 req, '2.89')
319 for bdm in limited_list:
320 if bdm.volume_id: 320 ↛ 319line 320 didn't jump to line 319 because the condition on line 320 was always true
321 va = _translate_attachment_detail_view(
322 bdm,
323 show_tag=show_tag,
324 show_delete_on_termination=show_delete_on_termination,
325 show_attachment_id_bdm_uuid=show_attachment_id_bdm_uuid,
326 )
327 results.append(va)
329 return {'volumeAttachments': results}
331 @wsgi.expected_errors(404)
332 @validation.query_schema(volume_attachment_schema.show_query)
333 def show(self, req, server_id, id):
334 """Return data about the given volume attachment."""
335 context = req.environ['nova.context']
336 instance = common.get_instance(self.compute_api, context, server_id)
337 context.can(va_policies.POLICY_ROOT % 'show',
338 target={'project_id': instance.project_id})
340 volume_id = id
342 try:
343 bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
344 context, volume_id, instance.uuid)
345 except exception.VolumeBDMNotFound:
346 msg = (_("Instance %(instance)s is not attached "
347 "to volume %(volume)s") %
348 {'instance': server_id, 'volume': volume_id})
349 raise exc.HTTPNotFound(explanation=msg)
351 show_tag = api_version_request.is_supported(req, '2.70')
352 show_delete_on_termination = api_version_request.is_supported(
353 req, '2.79')
354 show_attachment_id_bdm_uuid = api_version_request.is_supported(
355 req, '2.89')
356 return {
357 'volumeAttachment': _translate_attachment_detail_view(
358 bdm,
359 show_tag=show_tag,
360 show_delete_on_termination=show_delete_on_termination,
361 show_attachment_id_bdm_uuid=show_attachment_id_bdm_uuid,
362 )
363 }
365 # TODO(mriedem): This API should return a 202 instead of a 200 response.
366 @wsgi.expected_errors((400, 403, 404, 409))
367 @validation.schema(volumes_schema.create_volume_attachment, '2.0', '2.48')
368 @validation.schema(volumes_schema.create_volume_attachment_v249, '2.49',
369 '2.78')
370 @validation.schema(volumes_schema.create_volume_attachment_v279, '2.79')
371 def create(self, req, server_id, body):
372 """Attach a volume to an instance."""
373 context = req.environ['nova.context']
374 instance = common.get_instance(self.compute_api, context, server_id)
375 context.can(va_policies.POLICY_ROOT % 'create',
376 target={'project_id': instance.project_id})
378 volume_id = body['volumeAttachment']['volumeId']
379 device = body['volumeAttachment'].get('device')
380 tag = body['volumeAttachment'].get('tag')
381 delete_on_termination = body['volumeAttachment'].get(
382 'delete_on_termination', False)
384 if instance.vm_state in (vm_states.SHELVED,
385 vm_states.SHELVED_OFFLOADED):
386 _check_request_version(req, '2.20', 'attach_volume',
387 server_id, instance.vm_state)
389 try:
390 supports_multiattach = common.supports_multiattach_volume(req)
391 device = self.compute_api.attach_volume(
392 context, instance, volume_id, device, tag=tag,
393 supports_multiattach=supports_multiattach,
394 delete_on_termination=delete_on_termination)
395 except exception.VolumeNotFound as e:
396 raise exc.HTTPNotFound(explanation=e.format_message())
397 except (exception.InstanceIsLocked,
398 exception.DevicePathInUse) as e:
399 raise exc.HTTPConflict(explanation=e.format_message())
400 except exception.InstanceInvalidState as state_error:
401 common.raise_http_conflict_for_instance_invalid_state(state_error,
402 'attach_volume', server_id)
403 except (exception.InvalidVolume,
404 exception.InvalidDevicePath,
405 exception.InvalidInput,
406 exception.VolumeTaggedAttachNotSupported,
407 exception.MultiattachNotSupportedOldMicroversion,
408 exception.MultiattachToShelvedNotSupported) as e:
409 raise exc.HTTPBadRequest(explanation=e.format_message())
410 except exception.TooManyDiskDevices as e:
411 raise exc.HTTPForbidden(explanation=e.format_message())
413 # The attach is async
414 # NOTE(mriedem): It would be nice to use
415 # _translate_attachment_summary_view here but that does not include
416 # the 'device' key if device is None or the empty string which would
417 # be a backward incompatible change.
418 attachment = {}
419 attachment['id'] = volume_id
420 attachment['serverId'] = server_id
421 attachment['volumeId'] = volume_id
422 attachment['device'] = device
423 if api_version_request.is_supported(req, '2.70'):
424 attachment['tag'] = tag
425 if api_version_request.is_supported(req, '2.79'):
426 attachment['delete_on_termination'] = delete_on_termination
427 return {'volumeAttachment': attachment}
429 def _update_volume_swap(self, req, instance, id, body):
430 context = req.environ['nova.context']
431 old_volume_id = id
432 try:
433 old_volume = self.volume_api.get(context, old_volume_id)
434 except exception.VolumeNotFound as e:
435 raise exc.HTTPNotFound(explanation=e.format_message())
437 new_volume_id = body['volumeAttachment']['volumeId']
438 try:
439 new_volume = self.volume_api.get(context, new_volume_id)
440 except exception.VolumeNotFound as e:
441 # NOTE: This BadRequest is different from the above NotFound even
442 # though the same VolumeNotFound exception. This is intentional
443 # because new_volume_id is specified in a request body and if a
444 # nonexistent resource in the body (not URI) the code should be
445 # 400 Bad Request as API-WG guideline. On the other hand,
446 # old_volume_id is specified with URI. So it is valid to return
447 # NotFound response if that is not existent.
448 raise exc.HTTPBadRequest(explanation=e.format_message())
450 try:
451 self.compute_api.swap_volume(context, instance, old_volume,
452 new_volume)
453 except exception.VolumeBDMNotFound as e:
454 raise exc.HTTPNotFound(explanation=e.format_message())
455 except (exception.InvalidVolume,
456 exception.MultiattachSwapVolumeNotSupported) as e:
457 raise exc.HTTPBadRequest(explanation=e.format_message())
458 except exception.InstanceIsLocked as e:
459 raise exc.HTTPConflict(explanation=e.format_message())
460 except exception.InstanceInvalidState as state_error:
461 common.raise_http_conflict_for_instance_invalid_state(state_error,
462 'swap_volume', instance.uuid)
464 def _update_volume_regular(self, req, instance, id, body):
465 context = req.environ['nova.context']
466 att = body['volumeAttachment']
467 # NOTE(danms): We may be doing an update of regular parameters in
468 # the midst of a swap operation, so to find the original BDM, we need
469 # to use the old volume ID, which is the one in the path.
470 volume_id = id
472 try:
473 bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
474 context, volume_id, instance.uuid)
476 # NOTE(danms): The attachment id is just the (current) volume id
477 if 'id' in att and att['id'] != volume_id:
478 raise exc.HTTPBadRequest(explanation='The id property is '
479 'not mutable')
480 if 'serverId' in att and att['serverId'] != instance.uuid:
481 raise exc.HTTPBadRequest(explanation='The serverId property '
482 'is not mutable')
483 if 'device' in att and att['device'] != bdm.device_name:
484 raise exc.HTTPBadRequest(explanation='The device property is '
485 'not mutable')
486 if 'tag' in att and att['tag'] != bdm.tag:
487 raise exc.HTTPBadRequest(explanation='The tag property is '
488 'not mutable')
489 if 'delete_on_termination' in att:
490 bdm.delete_on_termination = strutils.bool_from_string(
491 att['delete_on_termination'], strict=True)
492 bdm.save()
493 except exception.VolumeBDMNotFound as e:
494 raise exc.HTTPNotFound(explanation=e.format_message())
496 @wsgi.response(202)
497 @wsgi.expected_errors((400, 404, 409))
498 @validation.schema(volumes_schema.update_volume_attachment, '2.0', '2.84')
499 @validation.schema(volumes_schema.update_volume_attachment_v285,
500 min_version='2.85')
501 def update(self, req, server_id, id, body):
502 context = req.environ['nova.context']
503 instance = common.get_instance(self.compute_api, context, server_id)
504 attachment = body['volumeAttachment']
505 volume_id = attachment['volumeId']
506 only_swap = not api_version_request.is_supported(req, '2.85')
508 # NOTE(brinzhang): If the 'volumeId' requested by the user is
509 # different from the 'id' in the url path, or only swap is allowed by
510 # the microversion, we should check the swap volume policy.
511 # otherwise, check the volume update policy.
512 # NOTE(gmann) We pass empty target to policy enforcement. This API
513 # is called by cinder which does not have correct project_id where
514 # server belongs to. By passing the empty target, we make sure that
515 # we do not check the requester project_id and allow users with
516 # allowed role to perform the swap volume.
517 if only_swap or id != volume_id:
518 context.can(va_policies.POLICY_ROOT % 'swap', target={})
519 else:
520 context.can(va_policies.POLICY_ROOT % 'update',
521 target={'project_id': instance.project_id})
523 if only_swap:
524 # NOTE(danms): Original behavior is always call swap on PUT
525 self._update_volume_swap(req, instance, id, body)
526 else:
527 # NOTE(danms): New behavior is update any supported attachment
528 # properties first, and then call swap if volumeId differs
529 self._update_volume_regular(req, instance, id, body)
530 if id != volume_id:
531 self._update_volume_swap(req, instance, id, body)
533 @wsgi.response(202)
534 @wsgi.expected_errors((400, 403, 404, 409))
535 def delete(self, req, server_id, id):
536 """Detach a volume from an instance."""
537 context = req.environ['nova.context']
538 instance = common.get_instance(self.compute_api, context, server_id,
539 expected_attrs=['device_metadata'])
540 context.can(va_policies.POLICY_ROOT % 'delete',
541 target={'project_id': instance.project_id})
543 volume_id = id
545 if instance.vm_state in (vm_states.SHELVED,
546 vm_states.SHELVED_OFFLOADED):
547 _check_request_version(req, '2.20', 'detach_volume',
548 server_id, instance.vm_state)
549 try:
550 volume = self.volume_api.get(context, volume_id)
551 except exception.VolumeNotFound as e:
552 raise exc.HTTPNotFound(explanation=e.format_message())
554 try:
555 bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
556 context, volume_id, instance.uuid)
557 except exception.VolumeBDMNotFound:
558 msg = (_("Instance %(instance)s is not attached "
559 "to volume %(volume)s") %
560 {'instance': server_id, 'volume': volume_id})
561 raise exc.HTTPNotFound(explanation=msg)
563 if bdm.is_root:
564 msg = _("Cannot detach a root device volume")
565 raise exc.HTTPBadRequest(explanation=msg)
567 try:
568 self.compute_api.detach_volume(context, instance, volume)
569 except exception.InvalidVolume as e:
570 raise exc.HTTPBadRequest(explanation=e.format_message())
571 except exception.InvalidInput as e:
572 raise exc.HTTPBadRequest(explanation=e.format_message())
573 except (exception.InstanceIsLocked, exception.ServiceUnavailable) as e:
574 raise exc.HTTPConflict(explanation=e.format_message())
575 except exception.InstanceInvalidState as state_error:
576 common.raise_http_conflict_for_instance_invalid_state(state_error,
577 'detach_volume', server_id)
580def _translate_snapshot_detail_view(context, vol):
581 """Maps keys for snapshots details view."""
583 d = _translate_snapshot_summary_view(context, vol)
585 # NOTE(gagupta): No additional data / lookups at the moment
586 return d
589def _translate_snapshot_summary_view(context, vol):
590 """Maps keys for snapshots summary view."""
591 d = {}
593 d['id'] = vol['id']
594 d['volumeId'] = vol['volume_id']
595 d['status'] = vol['status']
596 # NOTE(gagupta): We map volume_size as the snapshot size
597 d['size'] = vol['volume_size']
598 d['createdAt'] = vol['created_at']
599 d['displayName'] = vol['display_name']
600 d['displayDescription'] = vol['display_description']
601 return d
604class SnapshotController(wsgi.Controller):
605 """The Snapshots API controller for the OpenStack API."""
607 def __init__(self):
608 self.volume_api = cinder.API()
609 super(SnapshotController, self).__init__()
611 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
612 @wsgi.expected_errors(404)
613 @validation.query_schema(volumes_schema.snapshot_show_query)
614 def show(self, req, id):
615 """Return data about the given snapshot."""
616 context = req.environ['nova.context']
617 context.can(vol_policies.POLICY_NAME % 'snapshots:show',
618 target={'project_id': context.project_id})
620 try:
621 vol = self.volume_api.get_snapshot(context, id)
622 except exception.SnapshotNotFound as e:
623 raise exc.HTTPNotFound(explanation=e.format_message())
625 return {'snapshot': _translate_snapshot_detail_view(context, vol)}
627 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
628 @wsgi.response(202)
629 @wsgi.expected_errors(404)
630 def delete(self, req, id):
631 """Delete a snapshot."""
632 context = req.environ['nova.context']
633 context.can(vol_policies.POLICY_NAME % 'snapshots:delete',
634 target={'project_id': context.project_id})
636 try:
637 self.volume_api.delete_snapshot(context, id)
638 except exception.SnapshotNotFound as e:
639 raise exc.HTTPNotFound(explanation=e.format_message())
641 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
642 @wsgi.expected_errors(())
643 @validation.query_schema(volumes_schema.index_query)
644 def index(self, req):
645 """Returns a summary list of snapshots."""
646 context = req.environ['nova.context']
647 context.can(vol_policies.POLICY_NAME % 'snapshots:list',
648 target={'project_id': context.project_id})
649 return self._items(req, entity_maker=_translate_snapshot_summary_view)
651 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
652 @wsgi.expected_errors(())
653 @validation.query_schema(volumes_schema.detail_query)
654 def detail(self, req):
655 """Returns a detailed list of snapshots."""
656 context = req.environ['nova.context']
657 context.can(vol_policies.POLICY_NAME % 'snapshots:detail',
658 target={'project_id': context.project_id})
659 return self._items(req, entity_maker=_translate_snapshot_detail_view)
661 def _items(self, req, entity_maker):
662 """Returns a list of snapshots, transformed through entity_maker."""
663 context = req.environ['nova.context']
665 snapshots = self.volume_api.get_all_snapshots(context)
666 limited_list = common.limited(snapshots, req)
667 res = [entity_maker(context, snapshot) for snapshot in limited_list]
668 return {'snapshots': res}
670 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
671 @wsgi.expected_errors((400, 403))
672 @validation.schema(volumes_schema.snapshot_create)
673 def create(self, req, body):
674 """Creates a new snapshot."""
675 context = req.environ['nova.context']
676 context.can(vol_policies.POLICY_NAME % 'snapshots:create',
677 target={'project_id': context.project_id})
679 snapshot = body['snapshot']
680 volume_id = snapshot['volume_id']
682 force = snapshot.get('force', False)
683 force = strutils.bool_from_string(force, strict=True)
684 if force:
685 create_func = self.volume_api.create_snapshot_force
686 else:
687 create_func = self.volume_api.create_snapshot
689 try:
690 new_snapshot = create_func(context, volume_id,
691 snapshot.get('display_name'),
692 snapshot.get('display_description'))
693 except exception.OverQuota as e:
694 raise exc.HTTPForbidden(explanation=e.format_message())
696 retval = _translate_snapshot_detail_view(context, new_snapshot)
697 return {'snapshot': retval}