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
« 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.
16import ast
17from http import client as http_client
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
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
47LOG = log.getLogger(__name__)
48CONF = cfg.CONF
51class ShareMixin:
52 """Mixin class for Share API Controllers."""
54 def _update(self, *args, **kwargs):
55 db.share_update(*args, **kwargs)
57 def _get(self, *args, **kwargs):
58 return self.share_api.get(*args, **kwargs)
60 def _delete(self, *args, **kwargs):
61 return self.share_api.delete(*args, **kwargs)
63 @wsgi.Controller.authorize('get')
64 def show(self, req, id):
65 """Return data about the given share."""
66 context = req.environ['manila.context']
68 try:
69 share = self.share_api.get(context, id)
70 except exception.NotFound:
71 raise exc.HTTPNotFound()
73 return self._view_builder.detail(req, share)
75 @wsgi.Controller.authorize
76 def delete(self, req, id):
77 """Delete a share."""
78 context = req.environ['manila.context']
80 LOG.info("Delete share with id: %s", id, context=context)
82 try:
83 share = self.share_api.get(context, id)
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)
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)
108 return webob.Response(status_int=http_client.ACCEPTED)
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)
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)
132 def _get_shares(self, req, is_detail):
133 """Returns a list of shares, transformed through view builder."""
134 context = req.environ['manila.context']
136 common._validate_pagination_query(req)
138 search_opts = {}
139 search_opts.update(req.GET)
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')
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')
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
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'])
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')
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)
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'
181 common.remove_invalid_options(
182 context, search_opts, self._get_share_search_options())
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)
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
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')
216 @wsgi.Controller.authorize
217 def update(self, req, id, body):
218 """Update a share."""
219 context = req.environ['manila.context']
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()
224 share_data = body['share']
225 valid_update_keys = (
226 'display_name',
227 'display_description',
228 'is_public',
229 )
231 update_dict = {key: share_data[key]
232 for key in valid_update_keys
233 if key in share_data}
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')
240 try:
241 share = self.share_api.get(context, id)
242 except exception.NotFound:
243 raise exc.HTTPNotFound()
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)
250 update_dict = common.validate_public_share_policy(
251 context, update_dict, api='update')
253 share = self.share_api.update(context, share, update_dict)
254 share.update(update_dict)
255 return self._view_builder.detail(req, share)
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
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']
271 if not self.is_valid_body(body, 'share'):
272 raise exc.HTTPUnprocessableEntity()
274 share = body['share']
275 share = common.validate_public_share_policy(context, share)
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']
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']
291 size = share['size']
292 share_proto = share['share_proto'].upper()
294 msg = ("Create %(share_proto)s share of %(size)s GB" %
295 {'share_proto': share_proto, 'size': size})
296 LOG.info(msg, context=context)
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)
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
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 }
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
337 kwargs['snapshot_id'] = snapshot_id
339 share_network_id = share.get('share_network_id')
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
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)
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)
377 common.check_share_network_is_active(share_network)
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 }
392 display_name = share.get('display_name')
393 display_description = share.get('display_description')
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'))
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
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
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)
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)
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)
469 if share_type:
470 kwargs['share_type'] = share_type
471 if share_network_id:
472 kwargs['share_network_id'] = share_network_id
474 kwargs['scheduler_hints'] = scheduler_hints
475 kwargs['encryption_key_ref'] = encryption_key_ref
477 if req.api_version_request >= api_version.APIVersionRequest("2.84"):
478 kwargs['mount_point_name'] = share.pop('mount_point_name', None)
480 new_share = self.share_api.create(context,
481 share_proto,
482 size,
483 display_name,
484 display_description,
485 **kwargs)
487 return self._view_builder.detail(req, new_share)
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
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."""
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
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)
521 access_deletion_lock = {}
522 share_deletion_lock = {}
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 )
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)
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)
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)
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)
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)
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)
625 except exception.InvalidMetadata as error:
626 raise exc.HTTPBadRequest(explanation=error.msg)
628 except exception.InvalidMetadataSize as error:
629 raise exc.HTTPBadRequest(explanation=error.msg)
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)
636 return self._access_view_builder.view(req, access)
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."""
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 }
650 locks, locks_count = (
651 self.resource_locks_api.get_all(
652 context.elevated(), search_opts=search_opts,
653 show_count=True) or []
654 )
656 # no locks placed, nothing to do
657 if not locks:
658 return
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)
671 if locks_count and not unrestrict:
672 raise_rule_is_locked(share_id)
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)
684 if non_deletable_locks:
685 raise_rule_is_locked(share_id, unrestrict=unrestrict)
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']
692 access_data = body.get('deny_access', body.get('os-deny_access'))
693 access_id = access_data['access_id']
695 self._check_for_access_rule_locks(context, access_data, access_id, id)
697 share = self.share_api.get(context, id)
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)
704 share_network_id = share.get('share_network_id', None)
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)
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)
721 def _access_list(self, req, id, body):
722 """List share access rules."""
723 context = req.environ['manila.context']
725 share = self.share_api.get(context, id)
726 access_rules = self.share_api.access_get_all(context, share)
728 return self._access_view_builder.list_view(req, access_rules)
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')
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)
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)
749 return webob.Response(status_int=http_client.ACCEPTED)
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')
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)
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))
768 return webob.Response(status_int=http_client.ACCEPTED)
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)
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)
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)
794 return share, size, force
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)
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)
808 return share, size
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
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 )
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']
838 try:
839 share_id = id
840 snapshot_id = body['revert']['snapshot_id']
842 share = self.share_api.get(context, share_id)
843 snapshot = self.share_api.get_snapshot(context, snapshot_id)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
943 return webob.Response(status_int=http_client.ACCEPTED)
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()
950 share = body['share']
951 scheduler_hints = share.pop('scheduler_hints', None)
952 encryption_key_ref = share.pop('encryption_key_ref', None)
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)
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()
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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']
1056 LOG.debug("Soft delete share with id: %s", id, context=context)
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)
1070 return webob.Response(status_int=http_client.ACCEPTED)
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']
1081 LOG.debug("Restore share with id: %s", id, context=context)
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)
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)
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)
1103 self.share_api.restore(context, share)
1105 return webob.Response(status_int=http_client.ACCEPTED)
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)
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)
1126 params = body['migration_start']
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)
1132 new_share_network = None
1133 new_share_type = None
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)
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)
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)
1174 return webob.Response(status_int=return_code)
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)
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)
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)
1224 # refresh share model
1225 share = self.share_api.get(context, id)
1227 return self._migration_view_builder.get_progress(req, share, result)
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)
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)
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')
1276 return self._allow_access(*args, **kwargs)
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)
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)
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)
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)
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)
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)
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)
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)
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)
1347 allow_dhss_true = False
1348 if req.api_version_request >= api_version.APIVersionRequest('2.49'):
1349 allow_dhss_true = True
1351 detail = self._manage(req, body, allow_dhss_true=allow_dhss_true)
1352 return detail
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)
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)
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)
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)
1384 if req.api_version_request < api_version.APIVersionRequest("2.42"):
1385 req.GET.pop('with_count', None)
1387 if req.api_version_request < api_version.APIVersionRequest("2.69"):
1388 req.GET.pop('is_soft_deleted', None)
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)
1393 return self._get_shares(req, is_detail=False)
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)
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)
1408 if req.api_version_request < api_version.APIVersionRequest("2.69"):
1409 req.GET.pop('is_soft_deleted', None)
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)
1414 return self._get_shares(req, is_detail=True)
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 = []
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)
1443 return _metadata
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)
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)
1464 context = req.environ['manila.context']
1465 self.share_api.update_share_from_metadata(context, resource_id,
1466 metadata.get('metadata'))
1467 return metadata
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)
1480 context = req.environ['manila.context']
1481 self.share_api.update_share_from_metadata(context, resource_id,
1482 metadata.get('metadata'))
1483 return metadata
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)
1497 context = req.environ['manila.context']
1498 self.share_api.update_share_from_metadata(context, resource_id,
1499 metadata.get('metadata'))
1500 return metadata
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)
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)
1517def create_resource():
1518 return wsgi.Resource(ShareController())