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