Coverage for manila/api/v2/share_snapshots.py: 91%

369 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2013 NetApp 

2# Copyright 2015 EMC Corporation. 

3# All Rights Reserved. 

4# 

5# Licensed under the Apache License, Version 2.0 (the "License"); you may 

6# not use this file except in compliance with the License. You may obtain 

7# a copy of the License at 

8# 

9# http://www.apache.org/licenses/LICENSE-2.0 

10# 

11# Unless required by applicable law or agreed to in writing, software 

12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

14# License for the specific language governing permissions and limitations 

15# under the License. 

16 

17"""The share snapshots api.""" 

18 

19import ast 

20from http import client as http_client 

21 

22from oslo_log import log 

23import webob 

24from webob import exc 

25 

26from manila.api import common 

27from manila.api.openstack import api_version_request as api_version 

28from manila.api.openstack import wsgi 

29from manila.api.schemas import share_snapshots as schema 

30from manila.api.v2 import metadata 

31from manila.api import validation 

32from manila.api.views import share_snapshots as snapshot_views 

33from manila.common import constants 

34from manila import db 

35from manila.db import api as db_api 

36from manila import exception 

37from manila.i18n import _ 

38from manila import policy 

39from manila import share 

40from manila import utils 

41 

42LOG = log.getLogger(__name__) 

43 

44 

45class ShareSnapshotMixin: 

46 """Mixin class for Share Snapshot Controllers.""" 

47 

48 def _update(self, *args, **kwargs): 

49 db.share_snapshot_update(*args, **kwargs) 

50 

51 def _get(self, *args, **kwargs): 

52 return self.share_api.get_snapshot(*args, **kwargs) 

53 

54 def _delete(self, *args, **kwargs): 

55 return self.share_api.delete_snapshot(*args, **kwargs) 

56 

57 def show(self, req, id): 

58 """Return data about the given snapshot.""" 

59 context = req.environ['manila.context'] 

60 

61 try: 

62 snapshot = self.share_api.get_snapshot(context, id) 

63 

64 # Snapshot with no instances is filtered out. 

65 if snapshot.get('status') is None: 

66 raise exc.HTTPNotFound() 

67 except exception.NotFound: 

68 raise exc.HTTPNotFound() 

69 

70 return self._view_builder.detail(req, snapshot) 

71 

72 def delete(self, req, id): 

73 """Delete a snapshot.""" 

74 context = req.environ['manila.context'] 

75 

76 LOG.info("Delete snapshot with id: %s", id, context=context) 

77 policy.check_policy(context, 'share', 'delete_snapshot') 

78 

79 try: 

80 snapshot = self.share_api.get_snapshot(context, id) 

81 self.share_api.delete_snapshot(context, snapshot) 

82 except exception.NotFound: 

83 raise exc.HTTPNotFound() 

84 return webob.Response(status_int=http_client.ACCEPTED) 

85 

86 def index(self, req): 

87 """Returns a summary list of snapshots.""" 

88 req.GET.pop('name~', None) 

89 req.GET.pop('description~', None) 

90 req.GET.pop('description', None) 

91 return self._get_snapshots(req, is_detail=False) 

92 

93 def detail(self, req): 

94 """Returns a detailed list of snapshots.""" 

95 req.GET.pop('name~', None) 

96 req.GET.pop('description~', None) 

97 req.GET.pop('description', None) 

98 return self._get_snapshots(req, is_detail=True) 

99 

100 def _get_snapshots(self, req, is_detail): 

101 """Returns a list of snapshots.""" 

102 context = req.environ['manila.context'] 

103 

104 search_opts = {} 

105 search_opts.update(req.GET) 

106 params = common.get_pagination_params(req) 

107 limit, offset = [params.get('limit'), params.get('offset')] 

108 

109 # Remove keys that are not related to share attrs 

110 search_opts.pop('limit', None) 

111 search_opts.pop('offset', None) 

112 

113 show_count = False 

114 if 'with_count' in search_opts: 

115 show_count = utils.get_bool_from_api_params( 

116 'with_count', search_opts) 

117 search_opts.pop('with_count') 

118 

119 sort_key, sort_dir = common.get_sort_params(search_opts) 

120 key_dict = {"name": "display_name", 

121 "description": "display_description"} 

122 for key in key_dict: 

123 if sort_key == key: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true

124 sort_key = key_dict[key] 

125 

126 # NOTE(vponomaryov): Manila stores in DB key 'display_name', but 

127 # allows to use both keys 'name' and 'display_name'. It is leftover 

128 # from Cinder v1 and v2 APIs. 

129 if 'name' in search_opts: 

130 search_opts['display_name'] = search_opts.pop('name') 

131 if 'description' in search_opts: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true

132 search_opts['display_description'] = search_opts.pop( 

133 'description') 

134 

135 # Deserialize dicts 

136 if req.api_version_request >= api_version.APIVersionRequest("2.73"): 

137 if 'metadata' in search_opts: 

138 try: 

139 search_opts['metadata'] = ast.literal_eval( 

140 search_opts['metadata']) 

141 except ValueError: 

142 msg = _('Invalid value for metadata filter.') 

143 raise webob.exc.HTTPBadRequest(explanation=msg) 

144 else: 

145 search_opts.pop('metadata', None) 

146 

147 # like filter 

148 for key, db_key in (('name~', 'display_name~'), 

149 ('description~', 'display_description~')): 

150 if key in search_opts: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true

151 search_opts[db_key] = search_opts.pop(key) 

152 

153 common.remove_invalid_options(context, search_opts, 

154 self._get_snapshots_search_options()) 

155 

156 total_count = None 

157 if show_count: 

158 count, snapshots = self.share_api.get_all_snapshots_with_count( 

159 context, search_opts=search_opts, limit=limit, offset=offset, 

160 sort_key=sort_key, sort_dir=sort_dir) 

161 total_count = count 

162 else: 

163 snapshots = self.share_api.get_all_snapshots( 

164 context, search_opts=search_opts, limit=limit, offset=offset, 

165 sort_key=sort_key, sort_dir=sort_dir) 

166 

167 if is_detail: 

168 snapshots = self._view_builder.detail_list( 

169 req, snapshots, total_count) 

170 else: 

171 snapshots = self._view_builder.summary_list( 

172 req, snapshots, total_count) 

173 return snapshots 

174 

175 def _get_snapshots_search_options(self): 

176 """Return share snapshot search options allowed by non-admin.""" 

177 return ('display_name', 'status', 'share_id', 'size', 'display_name~', 

178 'display_description~', 'display_description', 'metadata') 

179 

180 def update(self, req, id, body): 

181 """Update a snapshot.""" 

182 context = req.environ['manila.context'] 

183 policy.check_policy(context, 'share', 'snapshot_update') 

184 

185 if not body or 'snapshot' not in body: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true

186 raise exc.HTTPUnprocessableEntity() 

187 

188 snapshot_data = body['snapshot'] 

189 valid_update_keys = ( 

190 'display_name', 

191 'display_description', 

192 ) 

193 

194 update_dict = {key: snapshot_data[key] 

195 for key in valid_update_keys 

196 if key in snapshot_data} 

197 

198 common.check_display_field_length( 

199 update_dict.get('display_name'), 'display_name') 

200 common.check_display_field_length( 

201 update_dict.get('display_description'), 'display_description') 

202 

203 try: 

204 snapshot = self.share_api.get_snapshot(context, id) 

205 except exception.NotFound: 

206 raise exc.HTTPNotFound() 

207 

208 snapshot = self.share_api.snapshot_update(context, snapshot, 

209 update_dict) 

210 snapshot.update(update_dict) 

211 return self._view_builder.detail(req, snapshot) 

212 

213 @wsgi.response(202) 

214 def create(self, req, body): 

215 """Creates a new snapshot.""" 

216 context = req.environ['manila.context'] 

217 

218 if not self.is_valid_body(body, 'snapshot'): 

219 raise exc.HTTPUnprocessableEntity() 

220 

221 snapshot = body['snapshot'] 

222 

223 share_id = snapshot['share_id'] 

224 share = self.share_api.get(context, share_id) 

225 

226 # Verify that share can be snapshotted 

227 if not share['snapshot_support']: 

228 msg = _("Snapshots cannot be created for share '%s' " 

229 "since it does not have that capability.") % share_id 

230 LOG.error(msg) 

231 raise exc.HTTPUnprocessableEntity(explanation=msg) 

232 

233 # we do not allow soft delete share with snapshot, and also 

234 # do not allow create snapshot for shares in recycle bin, 

235 # since it will lead to auto delete share failed. 

236 if share['is_soft_deleted']: 

237 msg = _("Snapshots cannot be created for share '%s' " 

238 "since it has been soft deleted.") % share_id 

239 raise exc.HTTPForbidden(explanation=msg) 

240 

241 LOG.info("Create snapshot from share %s", 

242 share_id, context=context) 

243 

244 # NOTE(rushiagr): v2 API allows name instead of display_name 

245 if 'name' in snapshot: 245 ↛ 253line 245 didn't jump to line 253 because the condition on line 245 was always true

246 snapshot['display_name'] = snapshot.get('name') 

247 common.check_display_field_length( 

248 snapshot['display_name'], 'name') 

249 del snapshot['name'] 

250 

251 # NOTE(rushiagr): v2 API allows description instead of 

252 # display_description 

253 if 'description' in snapshot: 253 ↛ 259line 253 didn't jump to line 259 because the condition on line 253 was always true

254 snapshot['display_description'] = snapshot.get('description') 

255 common.check_display_field_length( 

256 snapshot['display_description'], 'description') 

257 del snapshot['description'] 

258 

259 kwargs = {} 

260 if req.api_version_request >= api_version.APIVersionRequest("2.73"): 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true

261 if snapshot.get('metadata'): 

262 metadata = snapshot.get('metadata') 

263 kwargs.update({ 

264 'metadata': metadata, 

265 }) 

266 

267 new_snapshot = self.share_api.create_snapshot( 

268 context, 

269 share, 

270 snapshot.get('display_name'), 

271 snapshot.get('display_description'), 

272 **kwargs) 

273 return self._view_builder.detail( 

274 req, dict(new_snapshot.items())) 

275 

276 

277class ShareSnapshotsController( 

278 ShareSnapshotMixin, 

279 wsgi.Controller, 

280 metadata.MetadataController, 

281 wsgi.AdminActionsMixin, 

282): 

283 """The Share Snapshots API V2 controller for the OpenStack API.""" 

284 

285 resource_name = 'share_snapshot' 

286 _view_builder_class = snapshot_views.ViewBuilder 

287 

288 def __init__(self): 

289 super().__init__() 

290 self.share_api = share.API() 

291 

292 @wsgi.Controller.authorize('unmanage_snapshot') 

293 def _unmanage(self, req, id, body=None, allow_dhss_true=False): 

294 """Unmanage a share snapshot.""" 

295 context = req.environ['manila.context'] 

296 

297 LOG.info("Unmanage share snapshot with id: %s.", id) 

298 

299 try: 

300 snapshot = self.share_api.get_snapshot(context, id) 

301 

302 share = self.share_api.get(context, snapshot['share_id']) 

303 if not allow_dhss_true and share.get('share_server_id'): 

304 msg = _("Operation 'unmanage_snapshot' is not supported for " 

305 "snapshots of shares that are created with share" 

306 " servers (created with share-networks).") 

307 raise exc.HTTPForbidden(explanation=msg) 

308 elif share.get('has_replicas'): 

309 msg = _("Share %s has replicas. Snapshots of this share " 

310 "cannot currently be unmanaged until all replicas " 

311 "are removed.") % share['id'] 

312 raise exc.HTTPConflict(explanation=msg) 

313 elif snapshot['status'] in constants.TRANSITIONAL_STATUSES: 

314 msg = _("Snapshot with transitional state cannot be " 

315 "unmanaged. Snapshot '%(s_id)s' is in '%(state)s' " 

316 "state.") % {'state': snapshot['status'], 

317 's_id': snapshot['id']} 

318 raise exc.HTTPForbidden(explanation=msg) 

319 

320 self.share_api.unmanage_snapshot(context, snapshot, share['host']) 

321 except (exception.ShareSnapshotNotFound, exception.ShareNotFound) as e: 

322 raise exc.HTTPNotFound(explanation=e.msg) 

323 

324 return webob.Response(status_int=http_client.ACCEPTED) 

325 

326 @wsgi.Controller.authorize('manage_snapshot') 

327 def _manage(self, req, body): 

328 """Instruct Manila to manage an existing snapshot. 

329 

330 Required HTTP Body: 

331 

332 .. code-block:: json 

333 

334 { 

335 "snapshot": 

336 { 

337 "share_id": <Manila share id>, 

338 "provider_location": <A string parameter that identifies 

339 the snapshot on the backend> 

340 } 

341 } 

342 

343 Optional elements in 'snapshot' are: 

344 name A name for the new snapshot. 

345 description A description for the new snapshot. 

346 driver_options Driver specific dicts for the existing snapshot. 

347 """ 

348 

349 context = req.environ['manila.context'] 

350 snapshot_data = self._validate_manage_parameters(context, body) 

351 

352 # NOTE(vponomaryov): compatibility actions are required between API and 

353 # DB layers for 'name' and 'description' API params that are 

354 # represented in DB as 'display_name' and 'display_description' 

355 # appropriately. 

356 name = snapshot_data.get('display_name', 

357 snapshot_data.get('name')) 

358 description = snapshot_data.get( 

359 'display_description', snapshot_data.get('description')) 

360 

361 share_id = snapshot_data['share_id'] 

362 snapshot = { 

363 'share_id': share_id, 

364 'provider_location': snapshot_data['provider_location'], 

365 'display_name': name, 

366 'display_description': description, 

367 } 

368 if req.api_version_request >= api_version.APIVersionRequest("2.73"): 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true

369 if snapshot_data.get('metadata'): 

370 metadata = snapshot_data.get('metadata') 

371 snapshot.update({ 

372 'metadata': metadata, 

373 }) 

374 

375 try: 

376 share_ref = self.share_api.get(context, share_id) 

377 except exception.NotFound: 

378 raise exception.ShareNotFound(share_id=share_id) 

379 if share_ref.get('is_soft_deleted'): 

380 msg = _("Can not manage snapshot for share '%s' " 

381 "since it has been soft deleted.") % share_id 

382 raise exc.HTTPForbidden(explanation=msg) 

383 

384 driver_options = snapshot_data.get('driver_options', {}) 

385 

386 try: 

387 snapshot_ref = self.share_api.manage_snapshot(context, snapshot, 

388 driver_options, 

389 share=share_ref) 

390 except (exception.ShareNotFound, exception.ShareSnapshotNotFound) as e: 

391 raise exc.HTTPNotFound(explanation=e.msg) 

392 except (exception.InvalidShare, 

393 exception.ManageInvalidShareSnapshot) as e: 

394 raise exc.HTTPConflict(explanation=e.msg) 

395 

396 return self._view_builder.detail(req, snapshot_ref) 

397 

398 def _validate_manage_parameters(self, context, body): 

399 if not (body and self.is_valid_body(body, 'snapshot')): 

400 msg = _("Snapshot entity not found in request body.") 

401 raise exc.HTTPUnprocessableEntity(explanation=msg) 

402 

403 data = body['snapshot'] 

404 

405 required_parameters = ('share_id', 'provider_location') 

406 self._validate_parameters(data, required_parameters) 

407 

408 return data 

409 

410 def _validate_parameters(self, data, required_parameters, 

411 fix_response=False): 

412 

413 if fix_response: 

414 exc_response = exc.HTTPBadRequest 

415 else: 

416 exc_response = exc.HTTPUnprocessableEntity 

417 

418 for parameter in required_parameters: 

419 if parameter not in data: 

420 msg = _("Required parameter %s not found.") % parameter 

421 raise exc_response(explanation=msg) 

422 if not data.get(parameter): 

423 msg = _("Required parameter %s is empty.") % parameter 

424 raise exc_response(explanation=msg) 

425 if not isinstance(data[parameter], str): 

426 msg = _("Parameter %s must be a string.") % parameter 

427 raise exc_response(explanation=msg) 

428 

429 def _check_if_share_share_network_is_active(self, context, snapshot): 

430 share_network_id = snapshot['share'].get('share_network_id') 

431 if share_network_id: 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true

432 share_network = db_api.share_network_get( 

433 context, share_network_id) 

434 common.check_share_network_is_active(share_network) 

435 

436 def _allow(self, req, id, body, enable_ipv6=False): 

437 context = req.environ['manila.context'] 

438 

439 if not (body and self.is_valid_body(body, 'allow_access')): 

440 msg = _("Access data not found in request body.") 

441 raise exc.HTTPBadRequest(explanation=msg) 

442 

443 access_data = body.get('allow_access') 

444 

445 required_parameters = ('access_type', 'access_to') 

446 self._validate_parameters(access_data, required_parameters, 

447 fix_response=True) 

448 

449 access_type = access_data['access_type'] 

450 access_to = access_data['access_to'] 

451 

452 common.validate_access(access_type=access_type, 

453 access_to=access_to, 

454 enable_ipv6=enable_ipv6) 

455 

456 snapshot = self.share_api.get_snapshot(context, id) 

457 

458 self._check_if_share_share_network_is_active(context, snapshot) 

459 

460 self._check_mount_snapshot_support(context, snapshot) 

461 

462 try: 

463 access = self.share_api.snapshot_allow_access( 

464 context, snapshot, access_type, access_to) 

465 except exception.ShareSnapshotAccessExists as e: 

466 raise webob.exc.HTTPBadRequest(explanation=e.msg) 

467 

468 return self._view_builder.detail_access(req, access) 

469 

470 def _deny(self, req, id, body): 

471 context = req.environ['manila.context'] 

472 

473 if not (body and self.is_valid_body(body, 'deny_access')): 

474 msg = _("Access data not found in request body.") 

475 raise exc.HTTPBadRequest(explanation=msg) 

476 

477 access_data = body.get('deny_access') 

478 

479 self._validate_parameters( 

480 access_data, ('access_id',), fix_response=True) 

481 

482 access_id = access_data['access_id'] 

483 

484 snapshot = self.share_api.get_snapshot(context, id) 

485 

486 self._check_mount_snapshot_support(context, snapshot) 

487 

488 self._check_if_share_share_network_is_active(context, snapshot) 

489 

490 access = self.share_api.snapshot_access_get(context, access_id) 

491 

492 if access['share_snapshot_id'] != snapshot['id']: 

493 msg = _("Access rule provided is not associated with given" 

494 " snapshot.") 

495 raise webob.exc.HTTPBadRequest(explanation=msg) 

496 

497 self.share_api.snapshot_deny_access(context, snapshot, access) 

498 return webob.Response(status_int=http_client.ACCEPTED) 

499 

500 def _check_mount_snapshot_support(self, context, snapshot): 

501 share = self.share_api.get(context, snapshot['share_id']) 

502 if not share['mount_snapshot_support']: 

503 msg = _("Cannot control access to the snapshot %(snap)s since the " 

504 "parent share %(share)s does not support mounting its " 

505 "snapshots.") % {'snap': snapshot['id'], 

506 'share': share['id']} 

507 raise exc.HTTPBadRequest(explanation=msg) 

508 

509 def _access_list(self, req, snapshot_id): 

510 context = req.environ['manila.context'] 

511 

512 snapshot = self.share_api.get_snapshot(context, snapshot_id) 

513 self._check_mount_snapshot_support(context, snapshot) 

514 access_list = self.share_api.snapshot_access_get_all(context, snapshot) 

515 

516 return self._view_builder.detail_list_access(req, access_list) 

517 

518 @wsgi.Controller.api_version('2.0', '2.6') 

519 @wsgi.action('os-reset_status') 

520 def snapshot_reset_status_legacy(self, req, id, body): 

521 return self._reset_status(req, id, body) 

522 

523 @wsgi.Controller.api_version('2.7') 

524 @wsgi.action('reset_status') 

525 def snapshot_reset_status(self, req, id, body): 

526 return self._reset_status(req, id, body) 

527 

528 @wsgi.Controller.api_version('2.0', '2.6') 

529 @wsgi.action('os-force_delete') 

530 def snapshot_force_delete_legacy(self, req, id, body): 

531 return self._force_delete(req, id, body) 

532 

533 @wsgi.Controller.api_version('2.7') 

534 @wsgi.action('force_delete') 

535 def snapshot_force_delete(self, req, id, body): 

536 return self._force_delete(req, id, body) 

537 

538 @wsgi.Controller.api_version('2.12') 

539 @wsgi.response(202) 

540 def manage(self, req, body): 

541 return self._manage(req, body) 

542 

543 @wsgi.Controller.api_version('2.12', '2.48') 

544 @wsgi.action('unmanage') 

545 def unmanage(self, req, id, body=None): 

546 return self._unmanage(req, id, body) 

547 

548 @wsgi.Controller.api_version('2.49') # noqa 

549 @wsgi.action('unmanage') 

550 def unmanage(self, req, id, # pylint: disable=function-redefined # noqa F811 

551 body=None): 

552 return self._unmanage(req, id, body, allow_dhss_true=True) 

553 

554 @wsgi.Controller.api_version('2.32') 

555 @wsgi.action('allow_access') 

556 @wsgi.response(202) 

557 @wsgi.Controller.authorize 

558 def allow_access(self, req, id, body=None): 

559 enable_ipv6 = False 

560 if req.api_version_request >= api_version.APIVersionRequest("2.38"): 

561 enable_ipv6 = True 

562 return self._allow(req, id, body, enable_ipv6) 

563 

564 @wsgi.Controller.api_version('2.32') 

565 @wsgi.action('deny_access') 

566 @wsgi.Controller.authorize 

567 def deny_access(self, req, id, body=None): 

568 return self._deny(req, id, body) 

569 

570 @wsgi.Controller.api_version('2.32') 

571 @wsgi.Controller.authorize 

572 def access_list(self, req, snapshot_id): 

573 return self._access_list(req, snapshot_id) 

574 

575 @wsgi.Controller.api_version("2.0") 

576 @validation.request_query_schema(schema.index_request_query, "2.0", "2.35") 

577 @validation.request_query_schema( 

578 schema.index_request_query_v236, "2.36", "2.72") 

579 @validation.request_query_schema( 

580 schema.index_request_query_v273, "2.73", "2.78") 

581 @validation.request_query_schema(schema.index_request_query_v279, "2.79") 

582 @validation.response_body_schema(schema.index_response_body) 

583 def index(self, req): 

584 """Returns a summary list of shares.""" 

585 if req.api_version_request < api_version.APIVersionRequest("2.36"): 

586 req.GET.pop('name~', None) 

587 req.GET.pop('description~', None) 

588 req.GET.pop('description', None) 

589 

590 if req.api_version_request < api_version.APIVersionRequest("2.79"): 

591 req.GET.pop('with_count', None) 

592 

593 return self._get_snapshots(req, is_detail=False) 

594 

595 @wsgi.Controller.api_version("2.0") 

596 def detail(self, req): 

597 """Returns a detailed list of shares.""" 

598 if req.api_version_request < api_version.APIVersionRequest("2.36"): 598 ↛ 602line 598 didn't jump to line 602 because the condition on line 598 was always true

599 req.GET.pop('name~', None) 

600 req.GET.pop('description~', None) 

601 req.GET.pop('description', None) 

602 return self._get_snapshots(req, is_detail=True) 

603 

604 @wsgi.Controller.api_version("2.73") 

605 @wsgi.Controller.authorize("get_metadata") 

606 def index_metadata(self, req, resource_id): 

607 """Returns the list of metadata for a given share snapshot.""" 

608 return self._index_metadata(req, resource_id) 

609 

610 @wsgi.Controller.api_version("2.73") 

611 @wsgi.Controller.authorize("update_metadata") 

612 def create_metadata(self, req, resource_id, body): 

613 return self._create_metadata(req, resource_id, body) 

614 

615 @wsgi.Controller.api_version("2.73") 

616 @wsgi.Controller.authorize("update_metadata") 

617 def update_all_metadata(self, req, resource_id, body): 

618 return self._update_all_metadata(req, resource_id, body) 

619 

620 @wsgi.Controller.api_version("2.73") 

621 @wsgi.Controller.authorize("update_metadata") 

622 def update_metadata_item(self, req, resource_id, body, key): 

623 return self._update_metadata_item(req, resource_id, body, key) 

624 

625 @wsgi.Controller.api_version("2.73") 

626 @wsgi.Controller.authorize("get_metadata") 

627 def show_metadata(self, req, resource_id, key): 

628 return self._show_metadata(req, resource_id, key) 

629 

630 @wsgi.Controller.api_version("2.73") 

631 @wsgi.Controller.authorize("delete_metadata") 

632 def delete_metadata(self, req, resource_id, key): 

633 return self._delete_metadata(req, resource_id, key) 

634 

635 

636def create_resource(): 

637 return wsgi.Resource(ShareSnapshotsController())