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

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. 

15 

16"""The volumes extension.""" 

17 

18from oslo_utils import strutils 

19from webob import exc 

20 

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 

37 

38 

39def _translate_volume_detail_view(context, vol): 

40 """Maps keys for volumes details view.""" 

41 

42 d = _translate_volume_summary_view(context, vol) 

43 

44 # No additional data / lookups at the moment 

45 

46 return d 

47 

48 

49def _translate_volume_summary_view(context, vol): 

50 """Maps keys for volumes summary view.""" 

51 d = {} 

52 

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'] 

58 

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 ] 

79 

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 

83 

84 else: 

85 d['attachments'] = [{}] 

86 

87 d['displayName'] = vol['display_name'] 

88 d['displayDescription'] = vol['display_description'] 

89 

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'] 

94 

95 d['snapshotId'] = vol['snapshot_id'] 

96 

97 if vol.get('volume_metadata'): 

98 d['metadata'] = vol.get('volume_metadata') 

99 else: 

100 d['metadata'] = {} 

101 

102 return d 

103 

104 

105class VolumeController(wsgi.Controller): 

106 """The Volumes API controller for the OpenStack API.""" 

107 

108 def __init__(self): 

109 super(VolumeController, self).__init__() 

110 self.volume_api = cinder.API() 

111 

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}) 

120 

121 try: 

122 vol = self.volume_api.get(context, id) 

123 except exception.VolumeNotFound as e: 

124 raise exc.HTTPNotFound(explanation=e.format_message()) 

125 

126 return {'volume': _translate_volume_detail_view(context, vol)} 

127 

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}) 

136 

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()) 

143 

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) 

153 

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) 

163 

164 def _items(self, req, entity_maker): 

165 """Returns a list of volumes, transformed through entity_maker.""" 

166 context = req.environ['nova.context'] 

167 

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} 

172 

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}) 

181 

182 vol = body['volume'] 

183 

184 vol_type = vol.get('volume_type') 

185 metadata = vol.get('metadata') 

186 snapshot_id = vol.get('snapshot_id', None) 

187 

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 

195 

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'] 

199 

200 availability_zone = vol.get('availability_zone') 

201 

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()) 

217 

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} 

223 

224 location = '%s/%s' % (req.url, new_volume['id']) 

225 

226 return wsgi.ResponseObject(result, headers=dict(location=location)) 

227 

228 

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. 

236 

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 """ 

247 

248 d = {} 

249 

250 if not show_attachment_id_bdm_uuid: 

251 d['id'] = bdm.volume_id 

252 

253 d['volumeId'] = bdm.volume_id 

254 

255 d['serverId'] = bdm.instance_uuid 

256 

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 

259 

260 if show_tag: 

261 d['tag'] = bdm.tag 

262 

263 if show_delete_on_termination: 

264 d['delete_on_termination'] = bdm.delete_on_termination 

265 

266 if show_attachment_id_bdm_uuid: 

267 d['attachment_id'] = bdm.attachment_id 

268 d['bdm_uuid'] = bdm.uuid 

269 

270 return d 

271 

272 

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) 

284 

285 

286class VolumeAttachmentController(wsgi.Controller): 

287 """The volume attachment API controller for the OpenStack API. 

288 

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) 

291 

292 """ 

293 

294 def __init__(self): 

295 self.compute_api = compute.API() 

296 self.volume_api = cinder.API() 

297 super(VolumeAttachmentController, self).__init__() 

298 

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}) 

308 

309 bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( 

310 context, instance.uuid) 

311 limited_list = common.limited(bdms, req) 

312 

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) 

328 

329 return {'volumeAttachments': results} 

330 

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}) 

339 

340 volume_id = id 

341 

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) 

350 

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 } 

364 

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}) 

377 

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) 

383 

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) 

388 

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()) 

412 

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} 

428 

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()) 

436 

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()) 

449 

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) 

463 

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 

471 

472 try: 

473 bdm = objects.BlockDeviceMapping.get_by_volume_and_instance( 

474 context, volume_id, instance.uuid) 

475 

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()) 

495 

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') 

507 

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}) 

522 

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) 

532 

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}) 

542 

543 volume_id = id 

544 

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()) 

553 

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) 

562 

563 if bdm.is_root: 

564 msg = _("Cannot detach a root device volume") 

565 raise exc.HTTPBadRequest(explanation=msg) 

566 

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) 

578 

579 

580def _translate_snapshot_detail_view(context, vol): 

581 """Maps keys for snapshots details view.""" 

582 

583 d = _translate_snapshot_summary_view(context, vol) 

584 

585 # NOTE(gagupta): No additional data / lookups at the moment 

586 return d 

587 

588 

589def _translate_snapshot_summary_view(context, vol): 

590 """Maps keys for snapshots summary view.""" 

591 d = {} 

592 

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 

602 

603 

604class SnapshotController(wsgi.Controller): 

605 """The Snapshots API controller for the OpenStack API.""" 

606 

607 def __init__(self): 

608 self.volume_api = cinder.API() 

609 super(SnapshotController, self).__init__() 

610 

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}) 

619 

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()) 

624 

625 return {'snapshot': _translate_snapshot_detail_view(context, vol)} 

626 

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}) 

635 

636 try: 

637 self.volume_api.delete_snapshot(context, id) 

638 except exception.SnapshotNotFound as e: 

639 raise exc.HTTPNotFound(explanation=e.format_message()) 

640 

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) 

650 

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) 

660 

661 def _items(self, req, entity_maker): 

662 """Returns a list of snapshots, transformed through entity_maker.""" 

663 context = req.environ['nova.context'] 

664 

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} 

669 

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}) 

678 

679 snapshot = body['snapshot'] 

680 volume_id = snapshot['volume_id'] 

681 

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 

688 

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()) 

695 

696 retval = _translate_snapshot_detail_view(context, new_snapshot) 

697 return {'snapshot': retval}