Coverage for manila/api/v2/shares.py: 87%

945 statements  

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

1# Copyright (c) 2015 Mirantis inc. 

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 

16import ast 

17from http import client as http_client 

18 

19from oslo_config import cfg 

20from oslo_log import log 

21from oslo_utils import strutils 

22from oslo_utils import uuidutils 

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 shares as schema 

30from manila.api.v2 import metadata 

31from manila.api.v2 import share_manage 

32from manila.api.v2 import share_unmanage 

33from manila.api import validation 

34from manila.api.views import share_accesses as share_access_views 

35from manila.api.views import share_migration as share_migration_views 

36from manila.api.views import shares as share_views 

37from manila.common import constants 

38from manila import db 

39from manila import exception 

40from manila.i18n import _ 

41from manila.lock import api as resource_locks 

42from manila import policy 

43from manila import share 

44from manila.share import share_types 

45from manila import utils 

46 

47LOG = log.getLogger(__name__) 

48CONF = cfg.CONF 

49 

50 

51class ShareMixin: 

52 """Mixin class for Share API Controllers.""" 

53 

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

55 db.share_update(*args, **kwargs) 

56 

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

58 return self.share_api.get(*args, **kwargs) 

59 

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

61 return self.share_api.delete(*args, **kwargs) 

62 

63 @wsgi.Controller.authorize('get') 

64 def show(self, req, id): 

65 """Return data about the given share.""" 

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

67 

68 try: 

69 share = self.share_api.get(context, id) 

70 except exception.NotFound: 

71 raise exc.HTTPNotFound() 

72 

73 return self._view_builder.detail(req, share) 

74 

75 @wsgi.Controller.authorize 

76 def delete(self, req, id): 

77 """Delete a share.""" 

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

79 

80 LOG.info("Delete share with id: %s", id, context=context) 

81 

82 try: 

83 share = self.share_api.get(context, id) 

84 

85 # NOTE(ameade): If the share is in a share group, we require its 

86 # id be specified as a param. 

87 sg_id_key = 'share_group_id' 

88 if share.get(sg_id_key): 

89 share_group_id = req.params.get(sg_id_key) 

90 if not share_group_id: 

91 msg = _("Must provide '%s' as a request " 

92 "parameter when deleting a share in a share " 

93 "group.") % sg_id_key 

94 raise exc.HTTPBadRequest(explanation=msg) 

95 elif share_group_id != share.get(sg_id_key): 

96 msg = _("The specified '%s' does not match " 

97 "the share group id of the share.") % sg_id_key 

98 raise exc.HTTPBadRequest(explanation=msg) 

99 

100 self.share_api.delete(context, share) 

101 except exception.NotFound: 

102 raise exc.HTTPNotFound() 

103 except exception.InvalidShare as e: 

104 raise exc.HTTPForbidden(explanation=e.msg) 

105 except exception.Conflict as e: 

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

107 

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

109 

110 @wsgi.Controller.authorize("get_all") 

111 def index(self, req): 

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

113 req.GET.pop('export_location_id', None) 

114 req.GET.pop('export_location_path', None) 

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

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

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

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

119 return self._get_shares(req, is_detail=False) 

120 

121 @wsgi.Controller.authorize("get_all") 

122 def detail(self, req): 

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

124 req.GET.pop('export_location_id', None) 

125 req.GET.pop('export_location_path', None) 

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

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

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

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

130 return self._get_shares(req, is_detail=True) 

131 

132 def _get_shares(self, req, is_detail): 

133 """Returns a list of shares, transformed through view builder.""" 

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

135 

136 common._validate_pagination_query(req) 

137 

138 search_opts = {} 

139 search_opts.update(req.GET) 

140 

141 # Remove keys that are not related to share attrs 

142 sort_key = search_opts.pop('sort_key', 'created_at') 

143 sort_dir = search_opts.pop('sort_dir', 'desc') 

144 

145 show_count = False 

146 if 'with_count' in search_opts: 

147 show_count = utils.get_bool_from_api_params( 

148 'with_count', search_opts) 

149 search_opts.pop('with_count') 

150 

151 if 'is_soft_deleted' in search_opts: 

152 is_soft_deleted = utils.get_bool_from_api_params( 

153 'is_soft_deleted', search_opts) 

154 search_opts['is_soft_deleted'] = is_soft_deleted 

155 

156 # Deserialize dicts 

157 if 'metadata' in search_opts: 

158 search_opts['metadata'] = ast.literal_eval(search_opts['metadata']) 

159 if 'extra_specs' in search_opts: 

160 search_opts['extra_specs'] = ast.literal_eval( 

161 search_opts['extra_specs']) 

162 

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

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

165 # from Cinder v1 and v2 APIs. 

166 if 'name' in search_opts: 

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

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

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

170 'description') 

171 

172 # like filter 

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

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

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

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

177 

178 if sort_key == 'name': 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 sort_key = 'display_name' 

180 

181 common.remove_invalid_options( 

182 context, search_opts, self._get_share_search_options()) 

183 

184 total_count = None 

185 if show_count: 

186 count, shares = self.share_api.get_all_with_count( 

187 context, search_opts=search_opts, sort_key=sort_key, 

188 sort_dir=sort_dir) 

189 total_count = count 

190 else: 

191 shares = self.share_api.get_all( 

192 context, search_opts=search_opts, sort_key=sort_key, 

193 sort_dir=sort_dir) 

194 

195 if is_detail: 

196 shares = self._view_builder.detail_list(req, shares, total_count) 

197 else: 

198 shares = self._view_builder.summary_list(req, shares, total_count) 

199 return shares 

200 

201 def _get_share_search_options(self): 

202 """Return share search options allowed by non-admin.""" 

203 # NOTE(vponomaryov): share_server_id depends on policy, allow search 

204 # by it for non-admins in case policy changed. 

205 # Also allow search by extra_specs in case policy 

206 # for it allows non-admin access. 

207 return ( 

208 'display_name', 'status', 'share_server_id', 'volume_type_id', 

209 'share_type_id', 'snapshot_id', 'host', 'share_network_id', 

210 'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir', 

211 'share_group_id', 'share_group_snapshot_id', 'export_location_id', 

212 'export_location_path', 'display_name~', 'display_description~', 

213 'display_description', 'limit', 'offset', 'is_soft_deleted', 

214 'mount_point_name') 

215 

216 @wsgi.Controller.authorize 

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

218 """Update a share.""" 

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

220 

221 if not body or 'share' not in body: 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true

222 raise exc.HTTPUnprocessableEntity() 

223 

224 share_data = body['share'] 

225 valid_update_keys = ( 

226 'display_name', 

227 'display_description', 

228 'is_public', 

229 ) 

230 

231 update_dict = {key: share_data[key] 

232 for key in valid_update_keys 

233 if key in share_data} 

234 

235 common.check_display_field_length( 

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

237 common.check_display_field_length( 

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

239 

240 try: 

241 share = self.share_api.get(context, id) 

242 except exception.NotFound: 

243 raise exc.HTTPNotFound() 

244 

245 if share.get('is_soft_deleted'): 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 msg = _("Share '%s cannot be updated, " 

247 "since it has been soft deleted.") % share['id'] 

248 raise exc.HTTPForbidden(explanation=msg) 

249 

250 update_dict = common.validate_public_share_policy( 

251 context, update_dict, api='update') 

252 

253 share = self.share_api.update(context, share, update_dict) 

254 share.update(update_dict) 

255 return self._view_builder.detail(req, share) 

256 

257 def create(self, req, body): 

258 # Remove share group attributes 

259 body.get('share', {}).pop('share_group_id', None) 

260 share = self._create(req, body) 

261 return share 

262 

263 @wsgi.Controller.authorize('create') 

264 def _create(self, req, body, 

265 check_create_share_from_snapshot_support=False, 

266 check_availability_zones_extra_spec=False, 

267 scheduler_hints=None, encryption_key_ref=None): 

268 """Creates a new share.""" 

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

270 

271 if not self.is_valid_body(body, 'share'): 

272 raise exc.HTTPUnprocessableEntity() 

273 

274 share = body['share'] 

275 share = common.validate_public_share_policy(context, share) 

276 

277 # NOTE(rushiagr): Manila API allows 'name' instead of 'display_name'. 

278 if share.get('name'): 

279 share['display_name'] = share.get('name') 

280 common.check_display_field_length(share['display_name'], 'name') 

281 del share['name'] 

282 

283 # NOTE(rushiagr): Manila API allows 'description' instead of 

284 # 'display_description'. 

285 if share.get('description'): 

286 share['display_description'] = share.get('description') 

287 common.check_display_field_length( 

288 share['display_description'], 'description') 

289 del share['description'] 

290 

291 size = share['size'] 

292 share_proto = share['share_proto'].upper() 

293 

294 msg = ("Create %(share_proto)s share of %(size)s GB" % 

295 {'share_proto': share_proto, 'size': size}) 

296 LOG.info(msg, context=context) 

297 

298 availability_zone_id = None 

299 availability_zone = share.get('availability_zone') 

300 if availability_zone: 

301 try: 

302 availability_zone_db = db.availability_zone_get( 

303 context, availability_zone) 

304 availability_zone_id = availability_zone_db.id 

305 availability_zone = availability_zone_db.name 

306 except exception.AvailabilityZoneNotFound as e: 

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

308 

309 share_group_id = share.get('share_group_id') 

310 if share_group_id: 

311 try: 

312 share_group = db.share_group_get(context, share_group_id) 

313 except exception.ShareGroupNotFound as e: 

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

315 sg_az_id = share_group['availability_zone_id'] 

316 if availability_zone and availability_zone_id != sg_az_id: 

317 msg = _("Share cannot have AZ ('%(s_az)s') different than " 

318 "share group's one (%(sg_az)s).") % { 

319 's_az': availability_zone_id, 'sg_az': sg_az_id} 

320 raise exception.InvalidInput(msg) 

321 availability_zone = db.availability_zone_get( 

322 context, sg_az_id).name 

323 

324 kwargs = { 

325 'availability_zone': availability_zone, 

326 'metadata': share.get('metadata'), 

327 'is_public': share.get('is_public', False), 

328 'share_group_id': share_group_id, 

329 } 

330 

331 snapshot_id = share.get('snapshot_id') 

332 if snapshot_id: 

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

334 else: 

335 snapshot = None 

336 

337 kwargs['snapshot_id'] = snapshot_id 

338 

339 share_network_id = share.get('share_network_id') 

340 

341 parent_share_type = {} 

342 if snapshot: 

343 # Need to check that share_network_id from snapshot's 

344 # parents share equals to share_network_id from args. 

345 # If share_network_id is empty then update it with 

346 # share_network_id of parent share. 

347 parent_share = self.share_api.get(context, snapshot['share_id']) 

348 parent_share_net_id = parent_share.instance['share_network_id'] 

349 parent_share_type = share_types.get_share_type( 

350 context, parent_share.instance['share_type_id']) 

351 if share_network_id: 

352 if share_network_id != parent_share_net_id: 

353 msg = ("Share network ID should be the same as snapshot's" 

354 " parent share's or empty") 

355 raise exc.HTTPBadRequest(explanation=msg) 

356 elif parent_share_net_id: 

357 share_network_id = parent_share_net_id 

358 

359 # Verify that share can be created from a snapshot 

360 if (check_create_share_from_snapshot_support and 

361 not parent_share['create_share_from_snapshot_support']): 

362 msg = (_("A new share may not be created from snapshot '%s', " 

363 "because the snapshot's parent share does not have " 

364 "that capability.") 

365 % snapshot_id) 

366 LOG.error(msg) 

367 raise exc.HTTPBadRequest(explanation=msg) 

368 

369 if share_network_id: 

370 try: 

371 share_network = self.share_api.get_share_network( 

372 context, 

373 share_network_id) 

374 except exception.ShareNetworkNotFound as e: 

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

376 

377 common.check_share_network_is_active(share_network) 

378 

379 if availability_zone_id: 

380 subnets = ( 

381 db.share_network_subnets_get_all_by_availability_zone_id( 

382 context, share_network_id, 

383 availability_zone_id=availability_zone_id)) 

384 if not subnets: 

385 msg = _("A share network subnet was not found for the " 

386 "requested availability zone.") 

387 raise exc.HTTPBadRequest(explanation=msg) 

388 kwargs['az_request_multiple_subnet_support_map'] = { 

389 availability_zone_id: len(subnets) > 1, 

390 } 

391 

392 display_name = share.get('display_name') 

393 display_description = share.get('display_description') 

394 

395 if 'share_type' in share and 'volume_type' in share: 395 ↛ 396line 395 didn't jump to line 396 because the condition on line 395 was never true

396 msg = 'Cannot specify both share_type and volume_type' 

397 raise exc.HTTPBadRequest(explanation=msg) 

398 req_share_type = share.get('share_type', share.get('volume_type')) 

399 

400 share_type = None 

401 if req_share_type: 

402 try: 

403 if not uuidutils.is_uuid_like(req_share_type): 

404 share_type = share_types.get_share_type_by_name( 

405 context, req_share_type) 

406 else: 

407 share_type = share_types.get_share_type( 

408 context, req_share_type) 

409 except (exception.ShareTypeNotFound, 

410 exception.ShareTypeNotFoundByName): 

411 msg = _("Share type not found.") 

412 raise exc.HTTPNotFound(explanation=msg) 

413 except exception.InvalidShareType as e: 

414 raise exc.HTTPBadRequest(explanation=e.message) 

415 elif not snapshot: 

416 def_share_type = share_types.get_default_share_type() 

417 if def_share_type: 

418 share_type = def_share_type 

419 

420 # Only use in create share feature. Create share from snapshot 

421 # and create share with share group features not 

422 # need this check. 

423 if share_type and share_type.get('extra_specs'): 

424 dhss = (strutils.bool_from_string( 

425 share_type.get('extra_specs').get( 

426 'driver_handles_share_servers'))) 

427 else: 

428 dhss = False 

429 

430 if (not share_network_id and not snapshot 

431 and not share_group_id 

432 and dhss): 

433 msg = _('Share network must be set when the ' 

434 'driver_handles_share_servers is true.') 

435 raise exc.HTTPBadRequest(explanation=msg) 

436 

437 type_chosen = share_type or parent_share_type 

438 if type_chosen and check_availability_zones_extra_spec: 

439 type_azs = type_chosen.get( 

440 'extra_specs', {}).get('availability_zones', '') 

441 type_azs = type_azs.split(',') if type_azs else [] 

442 kwargs['availability_zones'] = type_azs 

443 if (availability_zone and type_azs and 

444 availability_zone not in type_azs): 

445 msg = _("Share type %(type)s is not supported within the " 

446 "availability zone chosen %(az)s.") 

447 type_chosen = ( 

448 req_share_type or "%s (from source snapshot)" % ( 

449 parent_share_type.get('name') or 

450 parent_share_type.get('id')) 

451 ) 

452 payload = {'type': type_chosen, 'az': availability_zone} 

453 raise exc.HTTPBadRequest(explanation=msg % payload) 

454 

455 if share_type and encryption_key_ref: 

456 type_enc = share_type.get( 

457 'extra_specs', {}).get('encryption_support') 

458 if type_enc not in constants.SUPPORTED_ENCRYPTION_TYPES: 

459 msg = _("Share type %(type)s extra-specs 'encryption_support' " 

460 "is missing valid value e.g. share, share_server.") 

461 payload = {'type': share_type} 

462 raise exc.HTTPBadRequest(explanation=msg % payload) 

463 if not dhss: 

464 msg = _("Share type %(type)s must set dhss=True for share " 

465 "encryption.") 

466 payload = {'type': share_type} 

467 raise exc.HTTPBadRequest(explanation=msg % payload) 

468 

469 if share_type: 

470 kwargs['share_type'] = share_type 

471 if share_network_id: 

472 kwargs['share_network_id'] = share_network_id 

473 

474 kwargs['scheduler_hints'] = scheduler_hints 

475 kwargs['encryption_key_ref'] = encryption_key_ref 

476 

477 if req.api_version_request >= api_version.APIVersionRequest("2.84"): 

478 kwargs['mount_point_name'] = share.pop('mount_point_name', None) 

479 

480 new_share = self.share_api.create(context, 

481 share_proto, 

482 size, 

483 display_name, 

484 display_description, 

485 **kwargs) 

486 

487 return self._view_builder.detail(req, new_share) 

488 

489 @staticmethod 

490 def _any_instance_has_errored_rules(share): 

491 for instance in share['instances']: 

492 access_rules_status = instance['access_rules_status'] 

493 if access_rules_status == constants.SHARE_INSTANCE_RULES_ERROR: 

494 return True 

495 return False 

496 

497 def _create_access_locks( 

498 self, context, access, lock_deletion=False, lock_visibility=False, 

499 lock_reason=None): 

500 """Creates locks for access rules and rollback if it fails.""" 

501 

502 # We must populate project_id and user_id in the access object, as this 

503 # is not in this entity 

504 access['project_id'] = context.project_id 

505 access['user_id'] = context.user_id 

506 

507 def raise_lock_failed(resource_id, lock_action, 

508 resource_type='access rule'): 

509 word_mapping = { 

510 constants.RESOURCE_ACTION_SHOW: 'visibility', 

511 constants.RESOURCE_ACTION_DELETE: 'deletion' 

512 } 

513 msg = _("Failed to lock the %(action)s of the %(resource_type)s " 

514 "%(resource_id)s.") % { 

515 'action': word_mapping[lock_action], 

516 'resource_id': resource_id, 

517 'resource_type': resource_type 

518 } 

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

520 

521 access_deletion_lock = {} 

522 share_deletion_lock = {} 

523 

524 if lock_deletion: 

525 try: 

526 access_deletion_lock = self.resource_locks_api.create( 

527 context, resource_id=access['id'], 

528 resource_type='access_rule', 

529 resource_action=constants.RESOURCE_ACTION_DELETE, 

530 resource=access, lock_reason=lock_reason) 

531 except Exception: 

532 raise_lock_failed( 

533 access['id'], constants.RESOURCE_ACTION_DELETE 

534 ) 

535 try: 

536 share_lock_reason = ( 

537 constants.SHARE_LOCKED_BY_ACCESS_LOCK_REASON % { 

538 'lock_id': access_deletion_lock['id'] 

539 } 

540 ) 

541 share_deletion_lock = self.resource_locks_api.create( 

542 context, resource_id=access['share_id'], 

543 resource_type='share', 

544 resource_action=constants.RESOURCE_ACTION_DELETE, 

545 lock_reason=share_lock_reason) 

546 except Exception: 

547 self.resource_locks_api.delete( 

548 context, access_deletion_lock['id']) 

549 raise_lock_failed( 

550 access['share_id'], constants.RESOURCE_ACTION_DELETE, 

551 resource_type='share' 

552 ) 

553 

554 if lock_visibility: 

555 try: 

556 self.resource_locks_api.create( 

557 context, resource_id=access['id'], 

558 resource_type='access_rule', 

559 resource_action=constants.RESOURCE_ACTION_SHOW, 

560 resource=access, lock_reason=lock_reason) 

561 except Exception: 

562 # If a deletion lock was placed and the visibility wasn't, 

563 # we should rollback the deletion lock. 

564 if access_deletion_lock: 564 ↛ 565line 564 didn't jump to line 565 because the condition on line 564 was never true

565 self.resource_locks_api.delete( 

566 context, access_deletion_lock['id']) 

567 if share_deletion_lock: 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true

568 self.resource_locks_api.delete( 

569 context, share_deletion_lock['id']) 

570 raise_lock_failed(access['id'], constants.RESOURCE_ACTION_SHOW) 

571 

572 @wsgi.Controller.authorize('allow_access') 

573 def _allow_access(self, req, id, body, enable_ceph=False, 

574 allow_on_error_status=False, enable_ipv6=False, 

575 enable_metadata=False, allow_on_error_state=False, 

576 lock_visibility=False, lock_deletion=False, 

577 lock_reason=None): 

578 """Add share access rule.""" 

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

580 access_data = body.get('allow_access', body.get('os-allow_access')) 

581 if not enable_metadata: 

582 access_data.pop('metadata', None) 

583 share = self.share_api.get(context, id) 

584 

585 if share.get('is_soft_deleted'): 585 ↛ 586line 585 didn't jump to line 586 because the condition on line 585 was never true

586 msg = _("Cannot allow access for share '%s' " 

587 "since it has been soft deleted.") % id 

588 raise exc.HTTPForbidden(explanation=msg) 

589 share_network_id = share.get('share_network_id') 

590 if share_network_id: 

591 share_network = db.share_network_get(context, share_network_id) 

592 common.check_share_network_is_active(share_network) 

593 

594 if (not allow_on_error_status and 

595 self._any_instance_has_errored_rules(share)): 

596 msg = _("Access rules cannot be added while the share or any of " 

597 "its replicas or migration copies has its " 

598 "access_rules_status set to %(instance_rules_status)s. " 

599 "Deny any rules in %(rule_state)s state and try " 

600 "again.") % { 

601 'instance_rules_status': constants.SHARE_INSTANCE_RULES_ERROR, 

602 'rule_state': constants.ACCESS_STATE_ERROR, 

603 } 

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

605 

606 if not (lock_visibility or lock_deletion) and lock_reason: 

607 msg = _("Lock reason can only be specified when locking the " 

608 "visibility or the deletion of an access rule.") 

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

610 

611 access_type = access_data['access_type'] 

612 access_to = access_data['access_to'] 

613 common.validate_access(access_type=access_type, 

614 access_to=access_to, 

615 enable_ceph=enable_ceph, 

616 enable_ipv6=enable_ipv6) 

617 try: 

618 access = self.share_api.allow_access( 

619 context, share, access_type, access_to, 

620 access_data.get('access_level'), access_data.get('metadata'), 

621 allow_on_error_state) 

622 except exception.ShareAccessExists as e: 

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

624 

625 except exception.InvalidMetadata as error: 

626 raise exc.HTTPBadRequest(explanation=error.msg) 

627 

628 except exception.InvalidMetadataSize as error: 

629 raise exc.HTTPBadRequest(explanation=error.msg) 

630 

631 if lock_deletion or lock_visibility: 

632 self._create_access_locks( 

633 context, access, lock_deletion=lock_deletion, 

634 lock_visibility=lock_visibility, lock_reason=lock_reason) 

635 

636 return self._access_view_builder.view(req, access) 

637 

638 def _check_for_access_rule_locks(self, context, access_data, access_id, 

639 share_id): 

640 """Fetches locks for access rules and attempts deleting them.""" 

641 

642 # ensure the requester is asking to remove the restrictions of the rule 

643 unrestrict = access_data.get('unrestrict', False) 

644 search_opts = { 

645 'resource_id': access_id, 

646 'resource_action': constants.RESOURCE_ACTION_DELETE, 

647 'all_projects': True, 

648 } 

649 

650 locks, locks_count = ( 

651 self.resource_locks_api.get_all( 

652 context.elevated(), search_opts=search_opts, 

653 show_count=True) or [] 

654 ) 

655 

656 # no locks placed, nothing to do 

657 if not locks: 

658 return 

659 

660 def raise_rule_is_locked(share_id, unrestrict=False): 

661 msg = _( 

662 "Cannot deny access for share '%s' since it has been " 

663 "locked. Please remove the locks and retry the " 

664 "operation") % share_id 

665 if unrestrict: 

666 msg = _( 

667 "Unable to drop access rule restrictions that are not " 

668 "placed by you.") 

669 raise exc.HTTPForbidden(explanation=msg) 

670 

671 if locks_count and not unrestrict: 

672 raise_rule_is_locked(share_id) 

673 

674 non_deletable_locks = [] 

675 for lock in locks: 

676 try: 

677 self.resource_locks_api.ensure_context_can_delete_lock( 

678 context, lock['id']) 

679 except (exception.NotAuthorized, exception.ResourceLockNotFound): 

680 # If it is not found, then it means that the context doesn't 

681 # have access to this resource and should be denied. 

682 non_deletable_locks.append(lock) 

683 

684 if non_deletable_locks: 

685 raise_rule_is_locked(share_id, unrestrict=unrestrict) 

686 

687 @wsgi.Controller.authorize('deny_access') 

688 def _deny_access(self, req, id, body, allow_on_error_state=False): 

689 """Remove share access rule.""" 

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

691 

692 access_data = body.get('deny_access', body.get('os-deny_access')) 

693 access_id = access_data['access_id'] 

694 

695 self._check_for_access_rule_locks(context, access_data, access_id, id) 

696 

697 share = self.share_api.get(context, id) 

698 

699 if share.get('is_soft_deleted'): 699 ↛ 700line 699 didn't jump to line 700 because the condition on line 699 was never true

700 msg = _("Cannot deny access for share '%s' " 

701 "since it has been soft deleted.") % id 

702 raise exc.HTTPForbidden(explanation=msg) 

703 

704 share_network_id = share.get('share_network_id', None) 

705 

706 if share_network_id: 

707 share_network = db.share_network_get(context, share_network_id) 

708 common.check_share_network_is_active(share_network) 

709 

710 try: 

711 access = self.share_api.access_get(context, access_id) 

712 if access.share_id != id: 

713 raise exception.NotFound() 

714 share = self.share_api.get(context, id) 

715 except exception.NotFound as error: 

716 raise webob.exc.HTTPNotFound(explanation=error.message) 

717 self.share_api.deny_access(context, share, access, 

718 allow_on_error_state) 

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

720 

721 def _access_list(self, req, id, body): 

722 """List share access rules.""" 

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

724 

725 share = self.share_api.get(context, id) 

726 access_rules = self.share_api.access_get_all(context, share) 

727 

728 return self._access_view_builder.list_view(req, access_rules) 

729 

730 @wsgi.Controller.authorize("extend") 

731 def _extend(self, req, id, body): 

732 """Extend size of a share.""" 

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

734 share, size, force = self._get_valid_extend_parameters( 

735 context, id, body, 'os-extend') 

736 

737 if share.get('is_soft_deleted'): 737 ↛ 738line 737 didn't jump to line 738 because the condition on line 737 was never true

738 msg = _("Cannot extend share '%s' " 

739 "since it has been soft deleted.") % id 

740 raise exc.HTTPForbidden(explanation=msg) 

741 

742 try: 

743 self.share_api.extend(context, share, size, force=force) 

744 except (exception.InvalidInput, exception.InvalidShare) as e: 

745 raise webob.exc.HTTPBadRequest(explanation=str(e)) 

746 except exception.ShareSizeExceedsAvailableQuota as e: 

747 raise webob.exc.HTTPForbidden(explanation=e.message) 

748 

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

750 

751 @wsgi.Controller.authorize("shrink") 

752 def _shrink(self, req, id, body): 

753 """Shrink size of a share.""" 

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

755 share, size = self._get_valid_shrink_parameters( 

756 context, id, body, 'os-shrink') 

757 

758 if share.get('is_soft_deleted'): 758 ↛ 759line 758 didn't jump to line 759 because the condition on line 758 was never true

759 msg = _("Cannot shrink share '%s' " 

760 "since it has been soft deleted.") % id 

761 raise exc.HTTPForbidden(explanation=msg) 

762 

763 try: 

764 self.share_api.shrink(context, share, size) 

765 except (exception.InvalidInput, exception.InvalidShare) as e: 

766 raise webob.exc.HTTPBadRequest(explanation=str(e)) 

767 

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

769 

770 def _get_valid_extend_parameters(self, context, id, body, action): 

771 try: 

772 share = self.share_api.get(context, id) 

773 except exception.NotFound as e: 

774 raise webob.exc.HTTPNotFound(explanation=e.message) 

775 

776 try: 

777 size = int(body.get(action, body.get('extend'))['new_size']) 

778 except (KeyError, ValueError, TypeError): 

779 msg = _("New share size must be specified as an integer.") 

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

781 

782 # force is True means share extend will extend directly, is False 

783 # means will go through scheduler. Default value is False, 

784 try: 

785 force = strutils.bool_from_string(body.get( 

786 action, body.get('extend'))['force'], strict=True) 

787 except KeyError: 

788 force = False 

789 except (ValueError, TypeError): 

790 msg = (_('Invalid boolean force : %(value)s') % 

791 {'value': body.get('extend')['force']}) 

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

793 

794 return share, size, force 

795 

796 def _get_valid_shrink_parameters(self, context, id, body, action): 

797 try: 

798 share = self.share_api.get(context, id) 

799 except exception.NotFound as e: 

800 raise webob.exc.HTTPNotFound(explanation=e.message) 

801 

802 try: 

803 size = int(body.get(action, body.get('shrink'))['new_size']) 

804 except (KeyError, ValueError, TypeError): 

805 msg = _("New share size must be specified as an integer.") 

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

807 

808 return share, size 

809 

810 

811class ShareController( 

812 wsgi.Controller, 

813 ShareMixin, 

814 share_manage.ShareManageMixin, 

815 share_unmanage.ShareUnmanageMixin, 

816 metadata.MetadataController, 

817 wsgi.AdminActionsMixin, 

818): 

819 """The Shares API v2 controller for the OpenStack API.""" 

820 resource_name = 'share' 

821 _view_builder_class = share_views.ViewBuilder 

822 

823 def __init__(self): 

824 super(ShareController, self).__init__() 

825 self.share_api = share.API() 

826 self.resource_locks_api = resource_locks.API() 

827 self._access_view_builder = share_access_views.ViewBuilder() 

828 self._migration_view_builder = share_migration_views.ViewBuilder() 

829 self._conf_admin_only_metadata_keys = getattr( 

830 CONF, 'admin_only_metadata', [] 

831 ) 

832 

833 @wsgi.Controller.authorize('revert_to_snapshot') 

834 def _revert(self, req, id, body=None): 

835 """Revert a share to a snapshot.""" 

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

837 

838 try: 

839 share_id = id 

840 snapshot_id = body['revert']['snapshot_id'] 

841 

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

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

844 

845 if share.get('is_soft_deleted'): 

846 msg = _("Share '%s cannot revert to snapshot, " 

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

848 raise exc.HTTPForbidden(explanation=msg) 

849 

850 # Ensure share supports reverting to a snapshot 

851 if not share['revert_to_snapshot_support']: 

852 msg_args = {'share_id': share_id, 'snap_id': snapshot_id} 

853 msg = _('Share %(share_id)s may not be reverted to snapshot ' 

854 '%(snap_id)s, because the share does not have that ' 

855 'capability.') 

856 raise exc.HTTPBadRequest(explanation=msg % msg_args) 

857 

858 # Ensure requested share & snapshot match. 

859 if share['id'] != snapshot['share_id']: 

860 msg_args = {'share_id': share_id, 'snap_id': snapshot_id} 

861 msg = _('Snapshot %(snap_id)s is not associated with share ' 

862 '%(share_id)s.') 

863 raise exc.HTTPBadRequest(explanation=msg % msg_args) 

864 

865 # Ensure share status is 'available'. 

866 if share['status'] != constants.STATUS_AVAILABLE: 

867 msg_args = { 

868 'share_id': share_id, 

869 'state': share['status'], 

870 'available': constants.STATUS_AVAILABLE, 

871 } 

872 msg = _("Share %(share_id)s is in '%(state)s' state, but it " 

873 "must be in '%(available)s' state to be reverted to a " 

874 "snapshot.") 

875 raise exc.HTTPConflict(explanation=msg % msg_args) 

876 

877 # Ensure snapshot status is 'available'. 

878 if snapshot['status'] != constants.STATUS_AVAILABLE: 

879 msg_args = { 

880 'snap_id': snapshot_id, 

881 'state': snapshot['status'], 

882 'available': constants.STATUS_AVAILABLE, 

883 } 

884 msg = _("Snapshot %(snap_id)s is in '%(state)s' state, but it " 

885 "must be in '%(available)s' state to be restored.") 

886 raise exc.HTTPConflict(explanation=msg % msg_args) 

887 

888 # Ensure a long-running task isn't active on the share 

889 if share.is_busy: 

890 msg_args = {'share_id': share_id} 

891 msg = _("Share %(share_id)s may not be reverted while it has " 

892 "an active task.") 

893 raise exc.HTTPConflict(explanation=msg % msg_args) 

894 

895 # Ensure the snapshot is the most recent one. 

896 latest_snapshot = self.share_api.get_latest_snapshot_for_share( 

897 context, share_id) 

898 if not latest_snapshot: 

899 msg_args = {'share_id': share_id} 

900 msg = _("Could not determine the latest snapshot for share " 

901 "%(share_id)s.") 

902 raise exc.HTTPBadRequest(explanation=msg % msg_args) 

903 if latest_snapshot['id'] != snapshot_id: 

904 msg_args = { 

905 'share_id': share_id, 

906 'snap_id': snapshot_id, 

907 'latest_snap_id': latest_snapshot['id'], 

908 } 

909 msg = _("Snapshot %(snap_id)s may not be restored because " 

910 "it is not the most recent snapshot of share " 

911 "%(share_id)s. Currently the latest snapshot is " 

912 "%(latest_snap_id)s.") 

913 raise exc.HTTPConflict(explanation=msg % msg_args) 

914 

915 # Ensure the access rules are not in the process of updating 

916 for instance in share['instances']: 

917 access_rules_status = instance['access_rules_status'] 

918 if access_rules_status != constants.ACCESS_STATE_ACTIVE: 

919 msg_args = { 

920 'share_id': share_id, 

921 'snap_id': snapshot_id, 

922 'state': constants.ACCESS_STATE_ACTIVE 

923 } 

924 msg = _("Snapshot %(snap_id)s belongs to a share " 

925 "%(share_id)s which has access rules that are " 

926 "not %(state)s.") 

927 raise exc.HTTPConflict(explanation=msg % msg_args) 

928 

929 msg_args = {'share_id': share_id, 'snap_id': snapshot_id} 

930 msg = 'Reverting share %(share_id)s to snapshot %(snap_id)s.' 

931 LOG.info(msg, msg_args) 

932 

933 self.share_api.revert_to_snapshot(context, share, snapshot) 

934 except exception.ShareNotFound as e: 

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

936 except exception.ShareSnapshotNotFound as e: 

937 raise exc.HTTPBadRequest(explanation=e.msg) 

938 except exception.ShareSizeExceedsAvailableQuota as e: 

939 raise exc.HTTPForbidden(explanation=e.msg) 

940 except exception.ReplicationException as e: 

941 raise exc.HTTPBadRequest(explanation=e.msg) 

942 

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

944 

945 @wsgi.Controller.api_version("2.90") 

946 def create(self, req, body): 

947 if not self.is_valid_body(body, 'share'): 947 ↛ 948line 947 didn't jump to line 948 because the condition on line 947 was never true

948 raise exc.HTTPUnprocessableEntity() 

949 

950 share = body['share'] 

951 scheduler_hints = share.pop('scheduler_hints', None) 

952 encryption_key_ref = share.pop('encryption_key_ref', None) 

953 

954 return self._create( 

955 req, body, 

956 check_create_share_from_snapshot_support=True, 

957 check_availability_zones_extra_spec=True, 

958 scheduler_hints=scheduler_hints, 

959 encryption_key_ref=encryption_key_ref) 

960 

961 @wsgi.Controller.api_version("2.65", "2.89") 

962 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

963 if not self.is_valid_body(body, 'share'): 

964 raise exc.HTTPUnprocessableEntity() 

965 

966 share = body['share'] 

967 scheduler_hints = share.pop('scheduler_hints', None) 

968 if req.api_version_request < api_version.APIVersionRequest("2.67"): 

969 if scheduler_hints: 

970 scheduler_hints.pop('only_host', None) 

971 

972 return self._create( 

973 req, body, 

974 check_create_share_from_snapshot_support=True, 

975 check_availability_zones_extra_spec=True, 

976 scheduler_hints=scheduler_hints) 

977 

978 @wsgi.Controller.api_version("2.48", "2.64") # noqa 

979 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

980 return self._create(req, body, 

981 check_create_share_from_snapshot_support=True, 

982 check_availability_zones_extra_spec=True) 

983 

984 @wsgi.Controller.api_version("2.31", "2.47") # noqa 

985 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

986 return self._create( 

987 req, body, check_create_share_from_snapshot_support=True) 

988 

989 @wsgi.Controller.api_version("2.24", "2.30") # noqa 

990 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

991 body.get('share', {}).pop('share_group_id', None) 

992 return self._create(req, body, 

993 check_create_share_from_snapshot_support=True) 

994 

995 @wsgi.Controller.api_version("2.0", "2.23") # noqa 

996 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

997 body.get('share', {}).pop('share_group_id', None) 

998 return self._create(req, body) 

999 

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

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

1002 @validation.request_body_schema(schema.reset_status_request_body, '2.0', '2.6') # noqa: E501 

1003 @validation.response_body_schema(schema.reset_status_response_body) 

1004 def share_reset_status_legacy(self, req, id, body): 

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

1006 try: 

1007 share = self.share_api.get(context, id) 

1008 except exception.NotFound: 

1009 raise exc.HTTPNotFound("Share %s not found" % id) 

1010 if share.get('is_soft_deleted'): 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true

1011 msg = _("status cannot be reset for share '%s' " 

1012 "since it has been soft deleted.") % id 

1013 raise exc.HTTPForbidden(explanation=msg) 

1014 return self._reset_status(req, id, body, resource=share) 

1015 

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

1017 @wsgi.action('reset_status') 

1018 @wsgi.Controller.authorize('reset_status') 

1019 @validation.request_body_schema(schema.reset_status_request_body_v27, '2.7') # noqa: E501 

1020 @validation.response_body_schema(schema.reset_status_response_body) 

1021 def share_reset_status(self, req, id, body): 

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

1023 try: 

1024 share = self.share_api.get(context, id) 

1025 except exception.NotFound: 

1026 raise exc.HTTPNotFound("Share %s not found" % id) 

1027 if share.get('is_soft_deleted'): 1027 ↛ 1028line 1027 didn't jump to line 1028 because the condition on line 1027 was never true

1028 msg = _("status cannot be reset for share '%s' " 

1029 "since it has been soft deleted.") % id 

1030 raise exc.HTTPForbidden(explanation=msg) 

1031 return self._reset_status(req, id, body, resource=share) 

1032 

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

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

1035 @validation.request_body_schema(schema.force_delete_request_body) 

1036 @validation.response_body_schema(schema.force_delete_response_body) 

1037 def share_force_delete_legacy(self, req, id, body): 

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

1039 

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

1041 @wsgi.action('force_delete') 

1042 @validation.request_body_schema(schema.force_delete_request_body_v27) 

1043 @validation.response_body_schema(schema.force_delete_response_body) 

1044 def share_force_delete(self, req, id, body): 

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

1046 

1047 @wsgi.Controller.api_version('2.69') 

1048 @wsgi.action('soft_delete') 

1049 @wsgi.Controller.authorize('soft_delete') 

1050 @validation.request_body_schema(schema.soft_delete_request_body) 

1051 @validation.response_body_schema(schema.soft_delete_response_body) 

1052 def share_soft_delete(self, req, id, body): 

1053 """Soft delete a share.""" 

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

1055 

1056 LOG.debug("Soft delete share with id: %s", id, context=context) 

1057 

1058 try: 

1059 share = self.share_api.get(context, id) 

1060 self.share_api.soft_delete(context, share) 

1061 except exception.NotFound: 

1062 raise exc.HTTPNotFound() 

1063 except exception.InvalidShare as e: 

1064 raise exc.HTTPForbidden(explanation=e.msg) 

1065 except exception.ShareBusyException as e: 

1066 raise exc.HTTPForbidden(explanation=e.msg) 

1067 except exception.Conflict as e: 

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

1069 

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

1071 

1072 @wsgi.Controller.api_version('2.69') 

1073 @wsgi.action('restore') 

1074 @wsgi.Controller.authorize("restore") 

1075 @validation.request_body_schema(schema.restore_request_body) 

1076 @validation.response_body_schema(schema.restore_response_body) 

1077 def share_restore(self, req, id, body): 

1078 """Restore a share from recycle bin.""" 

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

1080 

1081 LOG.debug("Restore share with id: %s", id, context=context) 

1082 

1083 try: 

1084 share = self.share_api.get(context, id) 

1085 except exception.NotFound: 

1086 msg = _("No share exists with ID %s.") 

1087 raise exc.HTTPNotFound(explanation=msg % id) 

1088 

1089 # If the share not exist in Recycle Bin, the API will return 

1090 # success directly. 

1091 is_soft_deleted = share.get('is_soft_deleted') 

1092 if not is_soft_deleted: 1092 ↛ 1093line 1092 didn't jump to line 1093 because the condition on line 1092 was never true

1093 return webob.Response(status_int=http_client.OK) 

1094 

1095 # If the share has reached the expired time, and is been deleting, 

1096 # it too late to restore the share. 

1097 if share['status'] in [constants.STATUS_DELETING, 

1098 constants.STATUS_ERROR_DELETING]: 

1099 msg = _("Share %s is being deleted or has suffered an error " 

1100 "during deletion, cannot be restored.") 

1101 raise exc.HTTPForbidden(explanation=msg % id) 

1102 

1103 self.share_api.restore(context, share) 

1104 

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

1106 

1107 @wsgi.Controller.api_version('2.29', experimental=True) 

1108 @wsgi.action("migration_start") 

1109 @wsgi.Controller.authorize 

1110 @validation.request_body_schema(schema.migration_start_request_body) 

1111 @validation.response_body_schema(schema.migration_start_response_body) 

1112 def migration_start(self, req, id, body): 

1113 """Migrate a share to the specified host.""" 

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

1115 try: 

1116 share = self.share_api.get(context, id) 

1117 except exception.NotFound: 

1118 msg = _("Share %s not found.") % id 

1119 raise exc.HTTPNotFound(explanation=msg) 

1120 

1121 if share.get('is_soft_deleted'): 1121 ↛ 1122line 1121 didn't jump to line 1122 because the condition on line 1121 was never true

1122 msg = _("Migration cannot start for share '%s' " 

1123 "since it has been soft deleted.") % id 

1124 raise exception.InvalidShare(reason=msg) 

1125 

1126 params = body['migration_start'] 

1127 

1128 bool_params = ['preserve_metadata', 'writable', 'nondisruptive', 

1129 'preserve_snapshots', 'force_host_assisted_migration'] 

1130 bool_param_values = utils.check_params_are_boolean(bool_params, params) 

1131 

1132 new_share_network = None 

1133 new_share_type = None 

1134 

1135 new_share_network_id = params.get('new_share_network_id', None) 

1136 if new_share_network_id: 

1137 try: 

1138 new_share_network = db.share_network_get( 

1139 context, new_share_network_id) 

1140 except exception.NotFound: 

1141 msg = _("Share network %s not " 

1142 "found.") % new_share_network_id 

1143 raise exc.HTTPBadRequest(explanation=msg) 

1144 common.check_share_network_is_active(new_share_network) 

1145 else: 

1146 share_network_id = share.get('share_network_id', None) 

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

1148 current_share_network = db.share_network_get( 

1149 context, share_network_id) 

1150 common.check_share_network_is_active(current_share_network) 

1151 

1152 new_share_type_id = params.get('new_share_type_id', None) 

1153 if new_share_type_id: 

1154 try: 

1155 new_share_type = db.share_type_get( 

1156 context, new_share_type_id) 

1157 except exception.NotFound: 

1158 msg = _("Share type %s not found.") % new_share_type_id 

1159 raise exc.HTTPBadRequest(explanation=msg) 

1160 

1161 try: 

1162 return_code = self.share_api.migration_start( 

1163 context, share, params['host'], 

1164 bool_param_values['force_host_assisted_migration'], 

1165 bool_param_values['preserve_metadata'], 

1166 bool_param_values['writable'], 

1167 bool_param_values['nondisruptive'], 

1168 bool_param_values['preserve_snapshots'], 

1169 new_share_network=new_share_network, 

1170 new_share_type=new_share_type) 

1171 except exception.Conflict as e: 

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

1173 

1174 return webob.Response(status_int=return_code) 

1175 

1176 @wsgi.Controller.api_version('2.22', experimental=True) 

1177 @wsgi.action("migration_complete") 

1178 @validation.request_body_schema(schema.migration_complete_request_body) 

1179 @validation.response_body_schema(schema.migration_complete_response_body) 

1180 @wsgi.Controller.authorize 

1181 def migration_complete(self, req, id, body): 

1182 """Invokes 2nd phase of share migration.""" 

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

1184 try: 

1185 share = self.share_api.get(context, id) 

1186 except exception.NotFound: 

1187 msg = _("Share %s not found.") % id 

1188 raise exc.HTTPNotFound(explanation=msg) 

1189 self.share_api.migration_complete(context, share) 

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

1191 

1192 @wsgi.Controller.api_version('2.22', experimental=True) 

1193 @wsgi.action("migration_cancel") 

1194 @wsgi.Controller.authorize 

1195 @validation.request_body_schema(schema.migration_cancel_request_body) 

1196 @validation.response_body_schema(schema.migration_cancel_response_body) 

1197 def migration_cancel(self, req, id, body): 

1198 """Attempts to cancel share migration.""" 

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

1200 try: 

1201 share = self.share_api.get(context, id) 

1202 except exception.NotFound: 

1203 msg = _("Share %s not found.") % id 

1204 raise exc.HTTPNotFound(explanation=msg) 

1205 self.share_api.migration_cancel(context, share) 

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

1207 

1208 @wsgi.Controller.api_version('2.22', experimental=True) 

1209 @wsgi.action("migration_get_progress") 

1210 @wsgi.Controller.authorize 

1211 @validation.request_body_schema(schema.migration_get_progress_request_body) 

1212 @validation.response_body_schema(schema.migration_get_progress_response_body, '2.22', '2.58') # noqa: E501 

1213 @validation.response_body_schema(schema.migration_get_progress_response_body_v259, '2.59') # noqa: E501 

1214 def migration_get_progress(self, req, id, body): 

1215 """Retrieve share migration progress for a given share.""" 

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

1217 try: 

1218 share = self.share_api.get(context, id) 

1219 except exception.NotFound: 

1220 msg = _("Share %s not found.") % id 

1221 raise exc.HTTPNotFound(explanation=msg) 

1222 result = self.share_api.migration_get_progress(context, share) 

1223 

1224 # refresh share model 

1225 share = self.share_api.get(context, id) 

1226 

1227 return self._migration_view_builder.get_progress(req, share, result) 

1228 

1229 @wsgi.Controller.api_version('2.22', experimental=True) 

1230 @wsgi.action("reset_task_state") 

1231 @wsgi.Controller.authorize 

1232 @validation.request_body_schema(schema.reset_task_state_request_body) 

1233 @validation.response_body_schema(schema.reset_task_state_response_body) 

1234 def reset_task_state(self, req, id, body): 

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

1236 try: 

1237 share = self.share_api.get(context, id) 

1238 except exception.NotFound: 

1239 raise exception.ShareNotFound(share_id=id) 

1240 if share.get('is_soft_deleted'): 

1241 msg = _("task state cannot be reset for share '%s' " 

1242 "since it has been soft deleted.") % id 

1243 raise exc.HTTPForbidden(explanation=msg) 

1244 return self._reset_status(req, id, body, status_attr='task_state', 

1245 resource=share) 

1246 

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

1248 @wsgi.action('os-allow_access') 

1249 def allow_access_legacy(self, req, id, body): 

1250 """Add share access rule.""" 

1251 return self._allow_access(req, id, body) 

1252 

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

1254 @wsgi.action('allow_access') 

1255 def allow_access(self, req, id, body): 

1256 """Add share access rule.""" 

1257 args = (req, id, body) 

1258 kwargs = {} 

1259 if req.api_version_request >= api_version.APIVersionRequest("2.13"): 

1260 kwargs['enable_ceph'] = True 

1261 if req.api_version_request >= api_version.APIVersionRequest("2.28"): 

1262 kwargs['allow_on_error_status'] = True 

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

1264 kwargs['enable_ipv6'] = True 

1265 if req.api_version_request >= api_version.APIVersionRequest("2.45"): 

1266 kwargs['enable_metadata'] = True 

1267 if req.api_version_request >= api_version.APIVersionRequest("2.74"): 

1268 kwargs['allow_on_error_state'] = True 

1269 if req.api_version_request >= api_version.APIVersionRequest("2.82"): 

1270 access_data = body.get('allow_access') 

1271 kwargs['lock_visibility'] = access_data.get( 

1272 'lock_visibility', False) 

1273 kwargs['lock_deletion'] = access_data.get('lock_deletion', False) 

1274 kwargs['lock_reason'] = access_data.get('lock_reason') 

1275 

1276 return self._allow_access(*args, **kwargs) 

1277 

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

1279 @wsgi.action('os-deny_access') 

1280 def deny_access_legacy(self, req, id, body): 

1281 """Remove share access rule.""" 

1282 return self._deny_access(req, id, body) 

1283 

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

1285 @wsgi.action('deny_access') 

1286 def deny_access(self, req, id, body): 

1287 """Remove share access rule.""" 

1288 args = (req, id, body) 

1289 kwargs = {} 

1290 if req.api_version_request >= api_version.APIVersionRequest("2.74"): 

1291 kwargs['allow_on_error_state'] = True 

1292 return self._deny_access(*args, **kwargs) 

1293 

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

1295 @wsgi.action('os-access_list') 

1296 def access_list_legacy(self, req, id, body): 

1297 """List share access rules.""" 

1298 return self._access_list(req, id, body) 

1299 

1300 @wsgi.Controller.api_version('2.7', '2.44') 

1301 @wsgi.action('access_list') 

1302 def access_list(self, req, id, body): 

1303 """List share access rules.""" 

1304 return self._access_list(req, id, body) 

1305 

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

1307 @wsgi.action('os-extend') 

1308 @validation.request_body_schema(schema.extend_request_body) 

1309 @validation.response_body_schema(schema.extend_response_body) 

1310 def extend_legacy(self, req, id, body): 

1311 """Extend size of a share.""" 

1312 body.get('os-extend', {}).pop('force', None) 

1313 return self._extend(req, id, body) 

1314 

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

1316 @wsgi.action('extend') 

1317 @validation.request_body_schema(schema.extend_request_body_v27, '2.7', '2.63') # noqa: E501 

1318 @validation.request_body_schema(schema.extend_request_body_v264, '2.64') 

1319 @validation.response_body_schema(schema.extend_response_body) 

1320 def extend(self, req, id, body): 

1321 """Extend size of a share.""" 

1322 if req.api_version_request < api_version.APIVersionRequest('2.64'): 

1323 body.get('extend', {}).pop('force', None) 

1324 return self._extend(req, id, body) 

1325 

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

1327 @wsgi.action('os-shrink') 

1328 @validation.request_body_schema(schema.shrink_request_body) 

1329 @validation.response_body_schema(schema.shrink_response_body) 

1330 def shrink_legacy(self, req, id, body): 

1331 """Shrink size of a share.""" 

1332 return self._shrink(req, id, body) 

1333 

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

1335 @wsgi.action('shrink') 

1336 @validation.request_body_schema(schema.shrink_request_body_v27, '2.7') 

1337 @validation.response_body_schema(schema.shrink_response_body) 

1338 def shrink(self, req, id, body): 

1339 """Shrink size of a share.""" 

1340 return self._shrink(req, id, body) 

1341 

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

1343 def manage(self, req, body): 

1344 if req.api_version_request < api_version.APIVersionRequest('2.8'): 

1345 body.get('share', {}).pop('is_public', None) 

1346 

1347 allow_dhss_true = False 

1348 if req.api_version_request >= api_version.APIVersionRequest('2.49'): 

1349 allow_dhss_true = True 

1350 

1351 detail = self._manage(req, body, allow_dhss_true=allow_dhss_true) 

1352 return detail 

1353 

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

1355 @wsgi.action('unmanage') 

1356 @validation.request_body_schema(schema.unmanage_request_body) 

1357 @validation.response_body_schema(schema.unmanage_response_body) 

1358 def unmanage(self, req, id, body): 

1359 allow_dhss_true = False 

1360 if req.api_version_request >= api_version.APIVersionRequest('2.49'): 

1361 allow_dhss_true = True 

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

1363 

1364 @wsgi.Controller.api_version('2.27') 

1365 @wsgi.action('revert') 

1366 @validation.request_body_schema(schema.revert_request_body) 

1367 @validation.response_body_schema(schema.revert_response_body) 

1368 def revert(self, req, id, body): 

1369 return self._revert(req, id, body) 

1370 

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

1372 @wsgi.Controller.authorize("get_all") 

1373 def index(self, req): 

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

1375 if req.api_version_request < api_version.APIVersionRequest("2.35"): 

1376 req.GET.pop('export_location_id', None) 

1377 req.GET.pop('export_location_path', None) 

1378 

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

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

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

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

1383 

1384 if req.api_version_request < api_version.APIVersionRequest("2.42"): 

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

1386 

1387 if req.api_version_request < api_version.APIVersionRequest("2.69"): 

1388 req.GET.pop('is_soft_deleted', None) 

1389 

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

1391 req.GET.pop('encryption_key_ref', None) 

1392 

1393 return self._get_shares(req, is_detail=False) 

1394 

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

1396 @wsgi.Controller.authorize("get_all") 

1397 def detail(self, req): 

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

1399 if req.api_version_request < api_version.APIVersionRequest("2.35"): 

1400 req.GET.pop('export_location_id', None) 

1401 req.GET.pop('export_location_path', None) 

1402 

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

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

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

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

1407 

1408 if req.api_version_request < api_version.APIVersionRequest("2.69"): 

1409 req.GET.pop('is_soft_deleted', None) 

1410 

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

1412 req.GET.pop('encryption_key_ref', None) 

1413 

1414 return self._get_shares(req, is_detail=True) 

1415 

1416 def _validate_metadata_for_update(self, req, share_id, metadata, 

1417 delete=True): 

1418 persistent_keys = set(self._conf_admin_only_metadata_keys) 

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

1420 if set(metadata).intersection(persistent_keys): 

1421 try: 

1422 policy.check_policy( 

1423 context, 'share', 'update_admin_only_metadata') 

1424 except exception.PolicyNotAuthorized: 

1425 msg = _("Cannot set or update admin only metadata.") 

1426 LOG.exception(msg) 

1427 raise exc.HTTPForbidden(explanation=msg) 

1428 persistent_keys = [] 

1429 

1430 current_share_metadata = db.share_metadata_get(context, share_id) 

1431 if delete: 

1432 _metadata = metadata 

1433 for key in persistent_keys: 

1434 if key in current_share_metadata: 

1435 _metadata[key] = current_share_metadata[key] 

1436 else: 

1437 metadata_copy = metadata.copy() 

1438 for key in persistent_keys: 

1439 metadata_copy.pop(key, None) 

1440 _metadata = current_share_metadata.copy() 

1441 _metadata.update(metadata_copy) 

1442 

1443 return _metadata 

1444 

1445 # NOTE: (ashrod98) original metadata method and policy overrides 

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

1447 @wsgi.Controller.authorize("get_share_metadata") 

1448 def index_metadata(self, req, resource_id): 

1449 """Returns the list of metadata for a given share.""" 

1450 return self._index_metadata(req, resource_id) 

1451 

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

1453 @wsgi.Controller.authorize("update_share_metadata") 

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

1455 if not self.is_valid_body(body, 'metadata'): 1455 ↛ 1456line 1455 didn't jump to line 1456 because the condition on line 1455 was never true

1456 expl = _('Malformed request body') 

1457 raise exc.HTTPBadRequest(explanation=expl) 

1458 _metadata = self._validate_metadata_for_update(req, resource_id, 

1459 body['metadata'], 

1460 delete=False) 

1461 body['metadata'] = _metadata 

1462 metadata = self._create_metadata(req, resource_id, body) 

1463 

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

1465 self.share_api.update_share_from_metadata(context, resource_id, 

1466 metadata.get('metadata')) 

1467 return metadata 

1468 

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

1470 @wsgi.Controller.authorize("update_share_metadata") 

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

1472 if not self.is_valid_body(body, 'metadata'): 1472 ↛ 1473line 1472 didn't jump to line 1473 because the condition on line 1472 was never true

1473 expl = _('Malformed request body') 

1474 raise exc.HTTPBadRequest(explanation=expl) 

1475 _metadata = self._validate_metadata_for_update(req, resource_id, 

1476 body['metadata']) 

1477 body['metadata'] = _metadata 

1478 metadata = self._update_all_metadata(req, resource_id, body) 

1479 

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

1481 self.share_api.update_share_from_metadata(context, resource_id, 

1482 metadata.get('metadata')) 

1483 return metadata 

1484 

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

1486 @wsgi.Controller.authorize("update_share_metadata") 

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

1488 if not self.is_valid_body(body, 'meta'): 

1489 expl = _('Malformed request body') 

1490 raise exc.HTTPBadRequest(explanation=expl) 

1491 _metadata = self._validate_metadata_for_update(req, resource_id, 

1492 body['metadata'], 

1493 delete=False) 

1494 body['metadata'] = _metadata 

1495 metadata = self._update_metadata_item(req, resource_id, body, key) 

1496 

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

1498 self.share_api.update_share_from_metadata(context, resource_id, 

1499 metadata.get('metadata')) 

1500 return metadata 

1501 

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

1503 @wsgi.Controller.authorize("get_share_metadata") 

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

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

1506 

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

1508 @wsgi.Controller.authorize("delete_share_metadata") 

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

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

1511 if key in self._conf_admin_only_metadata_keys: 1511 ↛ 1512line 1511 didn't jump to line 1512 because the condition on line 1511 was never true

1512 policy.check_policy(context, 'share', 

1513 'update_admin_only_metadata') 

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

1515 

1516 

1517def create_resource(): 

1518 return wsgi.Resource(ShareController())