"Fossies" - the Fresh Open Source Software Archive

Member "manila-8.1.4/manila/api/v1/shares.py" (19 Nov 2020, 23068 Bytes) of package /linux/misc/openstack/manila-8.1.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "shares.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 8.1.3_vs_8.1.4.

    1 # Copyright 2013 NetApp
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 """The shares api."""
   17 
   18 import ast
   19 
   20 from oslo_log import log
   21 from oslo_utils import strutils
   22 from oslo_utils import uuidutils
   23 import six
   24 from six.moves import http_client
   25 import webob
   26 from webob import exc
   27 
   28 from manila.api import common
   29 from manila.api.openstack import wsgi
   30 from manila.api.views import share_accesses as share_access_views
   31 from manila.api.views import shares as share_views
   32 from manila.common import constants
   33 from manila import db
   34 from manila import exception
   35 from manila.i18n import _
   36 from manila import share
   37 from manila.share import share_types
   38 from manila import utils
   39 
   40 LOG = log.getLogger(__name__)
   41 
   42 
   43 class ShareMixin(object):
   44     """Mixin class for Share API Controllers."""
   45 
   46     def _update(self, *args, **kwargs):
   47         db.share_update(*args, **kwargs)
   48 
   49     def _get(self, *args, **kwargs):
   50         return self.share_api.get(*args, **kwargs)
   51 
   52     def _delete(self, *args, **kwargs):
   53         return self.share_api.delete(*args, **kwargs)
   54 
   55     def _migrate(self, *args, **kwargs):
   56         return self.share_api.migrate_share(*args, **kwargs)
   57 
   58     def show(self, req, id):
   59         """Return data about the given share."""
   60         context = req.environ['manila.context']
   61 
   62         try:
   63             share = self.share_api.get(context, id)
   64         except exception.NotFound:
   65             raise exc.HTTPNotFound()
   66 
   67         return self._view_builder.detail(req, share)
   68 
   69     def delete(self, req, id):
   70         """Delete a share."""
   71         context = req.environ['manila.context']
   72 
   73         LOG.info("Delete share with id: %s", id, context=context)
   74 
   75         try:
   76             share = self.share_api.get(context, id)
   77 
   78             # NOTE(ameade): If the share is in a share group, we require its
   79             # id be specified as a param.
   80             sg_id_key = 'share_group_id'
   81             if share.get(sg_id_key):
   82                 share_group_id = req.params.get(sg_id_key)
   83                 if not share_group_id:
   84                     msg = _("Must provide '%s' as a request "
   85                             "parameter when deleting a share in a share "
   86                             "group.") % sg_id_key
   87                     raise exc.HTTPBadRequest(explanation=msg)
   88                 elif share_group_id != share.get(sg_id_key):
   89                     msg = _("The specified '%s' does not match "
   90                             "the share group id of the share.") % sg_id_key
   91                     raise exc.HTTPBadRequest(explanation=msg)
   92 
   93             self.share_api.delete(context, share)
   94         except exception.NotFound:
   95             raise exc.HTTPNotFound()
   96         except exception.InvalidShare as e:
   97             raise exc.HTTPForbidden(explanation=six.text_type(e))
   98         except exception.Conflict as e:
   99             raise exc.HTTPConflict(explanation=six.text_type(e))
  100 
  101         return webob.Response(status_int=http_client.ACCEPTED)
  102 
  103     def index(self, req):
  104         """Returns a summary list of shares."""
  105         req.GET.pop('export_location_id', None)
  106         req.GET.pop('export_location_path', None)
  107         req.GET.pop('name~', None)
  108         req.GET.pop('description~', None)
  109         req.GET.pop('description', None)
  110         req.GET.pop('with_count', None)
  111         return self._get_shares(req, is_detail=False)
  112 
  113     def detail(self, req):
  114         """Returns a detailed list of shares."""
  115         req.GET.pop('export_location_id', None)
  116         req.GET.pop('export_location_path', None)
  117         req.GET.pop('name~', None)
  118         req.GET.pop('description~', None)
  119         req.GET.pop('description', None)
  120         req.GET.pop('with_count', None)
  121         return self._get_shares(req, is_detail=True)
  122 
  123     def _get_shares(self, req, is_detail):
  124         """Returns a list of shares, transformed through view builder."""
  125         context = req.environ['manila.context']
  126 
  127         common._validate_pagination_query(req)
  128 
  129         search_opts = {}
  130         search_opts.update(req.GET)
  131 
  132         # Remove keys that are not related to share attrs
  133         sort_key = search_opts.pop('sort_key', 'created_at')
  134         sort_dir = search_opts.pop('sort_dir', 'desc')
  135 
  136         show_count = False
  137         if 'with_count' in search_opts:
  138             show_count = utils.get_bool_from_api_params(
  139                 'with_count', search_opts)
  140             search_opts.pop('with_count')
  141 
  142         # Deserialize dicts
  143         if 'metadata' in search_opts:
  144             search_opts['metadata'] = ast.literal_eval(search_opts['metadata'])
  145         if 'extra_specs' in search_opts:
  146             search_opts['extra_specs'] = ast.literal_eval(
  147                 search_opts['extra_specs'])
  148 
  149         # NOTE(vponomaryov): Manila stores in DB key 'display_name', but
  150         # allows to use both keys 'name' and 'display_name'. It is leftover
  151         # from Cinder v1 and v2 APIs.
  152         if 'name' in search_opts:
  153             search_opts['display_name'] = search_opts.pop('name')
  154         if 'description' in search_opts:
  155             search_opts['display_description'] = search_opts.pop(
  156                 'description')
  157 
  158         # like filter
  159         for key, db_key in (('name~', 'display_name~'),
  160                             ('description~', 'display_description~')):
  161             if key in search_opts:
  162                 search_opts[db_key] = search_opts.pop(key)
  163 
  164         if sort_key == 'name':
  165             sort_key = 'display_name'
  166 
  167         common.remove_invalid_options(
  168             context, search_opts, self._get_share_search_options())
  169 
  170         shares = self.share_api.get_all(
  171             context, search_opts=search_opts, sort_key=sort_key,
  172             sort_dir=sort_dir)
  173         total_count = None
  174         if show_count:
  175             total_count = len(shares)
  176 
  177         if is_detail:
  178             shares = self._view_builder.detail_list(req, shares, total_count)
  179         else:
  180             shares = self._view_builder.summary_list(req, shares, total_count)
  181         return shares
  182 
  183     def _get_share_search_options(self):
  184         """Return share search options allowed by non-admin."""
  185         # NOTE(vponomaryov): share_server_id depends on policy, allow search
  186         #                    by it for non-admins in case policy changed.
  187         #                    Also allow search by extra_specs in case policy
  188         #                    for it allows non-admin access.
  189         return (
  190             'display_name', 'status', 'share_server_id', 'volume_type_id',
  191             'share_type_id', 'snapshot_id', 'host', 'share_network_id',
  192             'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir',
  193             'share_group_id', 'share_group_snapshot_id', 'export_location_id',
  194             'export_location_path', 'display_name~', 'display_description~',
  195             'display_description', 'limit', 'offset'
  196         )
  197 
  198     @wsgi.Controller.authorize
  199     def update(self, req, id, body):
  200         """Update a share."""
  201         context = req.environ['manila.context']
  202 
  203         if not body or 'share' not in body:
  204             raise exc.HTTPUnprocessableEntity()
  205 
  206         share_data = body['share']
  207         valid_update_keys = (
  208             'display_name',
  209             'display_description',
  210             'is_public',
  211         )
  212 
  213         update_dict = {key: share_data[key]
  214                        for key in valid_update_keys
  215                        if key in share_data}
  216 
  217         try:
  218             share = self.share_api.get(context, id)
  219         except exception.NotFound:
  220             raise exc.HTTPNotFound()
  221 
  222         update_dict = common.validate_public_share_policy(
  223             context, update_dict, api='update')
  224 
  225         share = self.share_api.update(context, share, update_dict)
  226         share.update(update_dict)
  227         return self._view_builder.detail(req, share)
  228 
  229     def create(self, req, body):
  230         # Remove share group attributes
  231         body.get('share', {}).pop('share_group_id', None)
  232         share = self._create(req, body)
  233         return share
  234 
  235     @wsgi.Controller.authorize('create')
  236     def _create(self, req, body,
  237                 check_create_share_from_snapshot_support=False,
  238                 check_availability_zones_extra_spec=False):
  239         """Creates a new share."""
  240         context = req.environ['manila.context']
  241 
  242         if not self.is_valid_body(body, 'share'):
  243             raise exc.HTTPUnprocessableEntity()
  244 
  245         share = body['share']
  246         share = common.validate_public_share_policy(context, share)
  247 
  248         # NOTE(rushiagr): Manila API allows 'name' instead of 'display_name'.
  249         if share.get('name'):
  250             share['display_name'] = share.get('name')
  251             del share['name']
  252 
  253         # NOTE(rushiagr): Manila API allows 'description' instead of
  254         #                 'display_description'.
  255         if share.get('description'):
  256             share['display_description'] = share.get('description')
  257             del share['description']
  258 
  259         size = share['size']
  260         share_proto = share['share_proto'].upper()
  261 
  262         msg = ("Create %(share_proto)s share of %(size)s GB" %
  263                {'share_proto': share_proto, 'size': size})
  264         LOG.info(msg, context=context)
  265 
  266         availability_zone_id = None
  267         availability_zone = share.get('availability_zone')
  268         if availability_zone:
  269             try:
  270                 availability_zone_id = db.availability_zone_get(
  271                     context, availability_zone).id
  272             except exception.AvailabilityZoneNotFound as e:
  273                 raise exc.HTTPNotFound(explanation=six.text_type(e))
  274 
  275         share_group_id = share.get('share_group_id')
  276         if share_group_id:
  277             try:
  278                 share_group = db.share_group_get(context, share_group_id)
  279             except exception.ShareGroupNotFound as e:
  280                 raise exc.HTTPNotFound(explanation=six.text_type(e))
  281             sg_az_id = share_group['availability_zone_id']
  282             if availability_zone and availability_zone_id != sg_az_id:
  283                 msg = _("Share cannot have AZ ('%(s_az)s') different than "
  284                         "share group's one (%(sg_az)s).") % {
  285                             's_az': availability_zone_id, 'sg_az': sg_az_id}
  286                 raise exception.InvalidInput(msg)
  287             availability_zone_id = sg_az_id
  288 
  289         kwargs = {
  290             'availability_zone': availability_zone_id,
  291             'metadata': share.get('metadata'),
  292             'is_public': share.get('is_public', False),
  293             'share_group_id': share_group_id,
  294         }
  295 
  296         snapshot_id = share.get('snapshot_id')
  297         if snapshot_id:
  298             snapshot = self.share_api.get_snapshot(context, snapshot_id)
  299         else:
  300             snapshot = None
  301 
  302         kwargs['snapshot_id'] = snapshot_id
  303 
  304         share_network_id = share.get('share_network_id')
  305 
  306         parent_share_type = {}
  307         if snapshot:
  308             # Need to check that share_network_id from snapshot's
  309             # parents share equals to share_network_id from args.
  310             # If share_network_id is empty then update it with
  311             # share_network_id of parent share.
  312             parent_share = self.share_api.get(context, snapshot['share_id'])
  313             parent_share_net_id = parent_share.instance['share_network_id']
  314             parent_share_type = share_types.get_share_type(
  315                 context, parent_share.instance['share_type_id'])
  316             if share_network_id:
  317                 if share_network_id != parent_share_net_id:
  318                     msg = ("Share network ID should be the same as snapshot's"
  319                            " parent share's or empty")
  320                     raise exc.HTTPBadRequest(explanation=msg)
  321             elif parent_share_net_id:
  322                 share_network_id = parent_share_net_id
  323 
  324             # Verify that share can be created from a snapshot
  325             if (check_create_share_from_snapshot_support and
  326                     not parent_share['create_share_from_snapshot_support']):
  327                 msg = (_("A new share may not be created from snapshot '%s', "
  328                          "because the snapshot's parent share does not have "
  329                          "that capability.")
  330                        % snapshot_id)
  331                 LOG.error(msg)
  332                 raise exc.HTTPBadRequest(explanation=msg)
  333 
  334         if share_network_id:
  335             try:
  336                 self.share_api.get_share_network(
  337                     context,
  338                     share_network_id)
  339             except exception.ShareNetworkNotFound as e:
  340                 raise exc.HTTPNotFound(explanation=six.text_type(e))
  341             kwargs['share_network_id'] = share_network_id
  342 
  343         display_name = share.get('display_name')
  344         display_description = share.get('display_description')
  345 
  346         if 'share_type' in share and 'volume_type' in share:
  347             msg = 'Cannot specify both share_type and volume_type'
  348             raise exc.HTTPBadRequest(explanation=msg)
  349         req_share_type = share.get('share_type', share.get('volume_type'))
  350 
  351         share_type = None
  352         if req_share_type:
  353             try:
  354                 if not uuidutils.is_uuid_like(req_share_type):
  355                     share_type = share_types.get_share_type_by_name(
  356                         context, req_share_type)
  357                 else:
  358                     share_type = share_types.get_share_type(
  359                         context, req_share_type)
  360             except (exception.ShareTypeNotFound,
  361                     exception.ShareTypeNotFoundByName):
  362                 msg = _("Share type not found.")
  363                 raise exc.HTTPNotFound(explanation=msg)
  364         elif not snapshot:
  365             def_share_type = share_types.get_default_share_type()
  366             if def_share_type:
  367                 share_type = def_share_type
  368 
  369         # Only use in create share feature. Create share from snapshot
  370         # and create share with share group features not
  371         # need this check.
  372         if (not share_network_id and not snapshot
  373                 and not share_group_id
  374                 and share_type and share_type.get('extra_specs')
  375                 and (strutils.bool_from_string(share_type.get('extra_specs').
  376                      get('driver_handles_share_servers')))):
  377             msg = _('Share network must be set when the '
  378                     'driver_handles_share_servers is true.')
  379             raise exc.HTTPBadRequest(explanation=msg)
  380 
  381         type_chosen = share_type or parent_share_type
  382         if type_chosen and check_availability_zones_extra_spec:
  383             type_azs = type_chosen.get(
  384                 'extra_specs', {}).get('availability_zones', '')
  385             type_azs = type_azs.split(',') if type_azs else []
  386             kwargs['availability_zones'] = type_azs
  387             if (availability_zone and type_azs and
  388                     availability_zone not in type_azs):
  389                 msg = _("Share type %(type)s is not supported within the "
  390                         "availability zone chosen %(az)s.")
  391                 type_chosen = (
  392                     req_share_type or "%s (from source snapshot)" % (
  393                         parent_share_type.get('name') or
  394                         parent_share_type.get('id'))
  395                 )
  396                 payload = {'type': type_chosen, 'az': availability_zone}
  397                 raise exc.HTTPBadRequest(explanation=msg % payload)
  398 
  399         if share_type:
  400             kwargs['share_type'] = share_type
  401         new_share = self.share_api.create(context,
  402                                           share_proto,
  403                                           size,
  404                                           display_name,
  405                                           display_description,
  406                                           **kwargs)
  407 
  408         return self._view_builder.detail(req, new_share)
  409 
  410     @staticmethod
  411     def _any_instance_has_errored_rules(share):
  412         for instance in share['instances']:
  413             access_rules_status = instance['access_rules_status']
  414             if access_rules_status == constants.SHARE_INSTANCE_RULES_ERROR:
  415                 return True
  416         return False
  417 
  418     @wsgi.Controller.authorize('allow_access')
  419     def _allow_access(self, req, id, body, enable_ceph=False,
  420                       allow_on_error_status=False, enable_ipv6=False,
  421                       enable_metadata=False):
  422         """Add share access rule."""
  423         context = req.environ['manila.context']
  424         access_data = body.get('allow_access', body.get('os-allow_access'))
  425         if not enable_metadata:
  426             access_data.pop('metadata', None)
  427         share = self.share_api.get(context, id)
  428 
  429         if (not allow_on_error_status and
  430                 self._any_instance_has_errored_rules(share)):
  431             msg = _("Access rules cannot be added while the share or any of "
  432                     "its replicas or migration copies has its "
  433                     "access_rules_status set to %(instance_rules_status)s. "
  434                     "Deny any rules in %(rule_state)s state and try "
  435                     "again.") % {
  436                 'instance_rules_status': constants.SHARE_INSTANCE_RULES_ERROR,
  437                 'rule_state': constants.ACCESS_STATE_ERROR,
  438             }
  439             raise webob.exc.HTTPBadRequest(explanation=msg)
  440 
  441         access_type = access_data['access_type']
  442         access_to = access_data['access_to']
  443         common.validate_access(access_type=access_type,
  444                                access_to=access_to,
  445                                enable_ceph=enable_ceph,
  446                                enable_ipv6=enable_ipv6)
  447         try:
  448             access = self.share_api.allow_access(
  449                 context, share, access_type, access_to,
  450                 access_data.get('access_level'), access_data.get('metadata'))
  451         except exception.ShareAccessExists as e:
  452             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  453 
  454         except exception.InvalidMetadata as error:
  455             raise exc.HTTPBadRequest(explanation=error.msg)
  456 
  457         except exception.InvalidMetadataSize as error:
  458             raise exc.HTTPBadRequest(explanation=error.msg)
  459 
  460         return self._access_view_builder.view(req, access)
  461 
  462     @wsgi.Controller.authorize('deny_access')
  463     def _deny_access(self, req, id, body):
  464         """Remove share access rule."""
  465         context = req.environ['manila.context']
  466 
  467         access_id = body.get(
  468             'deny_access', body.get('os-deny_access'))['access_id']
  469 
  470         try:
  471             access = self.share_api.access_get(context, access_id)
  472             if access.share_id != id:
  473                 raise exception.NotFound()
  474             share = self.share_api.get(context, id)
  475         except exception.NotFound as error:
  476             raise webob.exc.HTTPNotFound(explanation=six.text_type(error))
  477         self.share_api.deny_access(context, share, access)
  478         return webob.Response(status_int=http_client.ACCEPTED)
  479 
  480     def _access_list(self, req, id, body):
  481         """List share access rules."""
  482         context = req.environ['manila.context']
  483 
  484         share = self.share_api.get(context, id)
  485         access_rules = self.share_api.access_get_all(context, share)
  486 
  487         return self._access_view_builder.list_view(req, access_rules)
  488 
  489     def _extend(self, req, id, body):
  490         """Extend size of a share."""
  491         context = req.environ['manila.context']
  492         share, size = self._get_valid_resize_parameters(
  493             context, id, body, 'os-extend')
  494 
  495         try:
  496             self.share_api.extend(context, share, size)
  497         except (exception.InvalidInput, exception.InvalidShare) as e:
  498             raise webob.exc.HTTPBadRequest(explanation=six.text_type(e))
  499         except exception.ShareSizeExceedsAvailableQuota as e:
  500             raise webob.exc.HTTPForbidden(explanation=six.text_type(e))
  501 
  502         return webob.Response(status_int=http_client.ACCEPTED)
  503 
  504     def _shrink(self, req, id, body):
  505         """Shrink size of a share."""
  506         context = req.environ['manila.context']
  507         share, size = self._get_valid_resize_parameters(
  508             context, id, body, 'os-shrink')
  509 
  510         try:
  511             self.share_api.shrink(context, share, size)
  512         except (exception.InvalidInput, exception.InvalidShare) as e:
  513             raise webob.exc.HTTPBadRequest(explanation=six.text_type(e))
  514 
  515         return webob.Response(status_int=http_client.ACCEPTED)
  516 
  517     def _get_valid_resize_parameters(self, context, id, body, action):
  518         try:
  519             share = self.share_api.get(context, id)
  520         except exception.NotFound as e:
  521             raise webob.exc.HTTPNotFound(explanation=six.text_type(e))
  522 
  523         try:
  524             size = int(body.get(action,
  525                                 body.get(action.split('os-')[-1]))['new_size'])
  526         except (KeyError, ValueError, TypeError):
  527             msg = _("New share size must be specified as an integer.")
  528             raise webob.exc.HTTPBadRequest(explanation=msg)
  529 
  530         return share, size
  531 
  532 
  533 class ShareController(wsgi.Controller, ShareMixin, wsgi.AdminActionsMixin):
  534     """The Shares API v1 controller for the OpenStack API."""
  535     resource_name = 'share'
  536     _view_builder_class = share_views.ViewBuilder
  537 
  538     def __init__(self):
  539         super(ShareController, self).__init__()
  540         self.share_api = share.API()
  541         self._access_view_builder = share_access_views.ViewBuilder()
  542 
  543     @wsgi.action('os-reset_status')
  544     def share_reset_status(self, req, id, body):
  545         """Reset status of a share."""
  546         return self._reset_status(req, id, body)
  547 
  548     @wsgi.action('os-force_delete')
  549     def share_force_delete(self, req, id, body):
  550         """Delete a share, bypassing the check for status."""
  551         return self._force_delete(req, id, body)
  552 
  553     @wsgi.action('os-allow_access')
  554     def allow_access(self, req, id, body):
  555         """Add share access rule."""
  556         return self._allow_access(req, id, body)
  557 
  558     @wsgi.action('os-deny_access')
  559     def deny_access(self, req, id, body):
  560         """Remove share access rule."""
  561         return self._deny_access(req, id, body)
  562 
  563     @wsgi.action('os-access_list')
  564     def access_list(self, req, id, body):
  565         """List share access rules."""
  566         return self._access_list(req, id, body)
  567 
  568     @wsgi.action('os-extend')
  569     def extend(self, req, id, body):
  570         """Extend size of a share."""
  571         return self._extend(req, id, body)
  572 
  573     @wsgi.action('os-shrink')
  574     def shrink(self, req, id, body):
  575         """Shrink size of a share."""
  576         return self._shrink(req, id, body)
  577 
  578 
  579 def create_resource():
  580     return wsgi.Resource(ShareController())