Coverage for manila/share_group/api.py: 91%
255 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 Alex Meade
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.
16"""
17Handles all requests relating to share groups.
18"""
20from oslo_config import cfg
21from oslo_log import log
22from oslo_utils import excutils
23from oslo_utils import strutils
25from manila.api import common as api_common
26from manila.common import constants
27from manila.db import base
28from manila import exception
29from manila.i18n import _
30from manila import quota
31from manila.scheduler import rpcapi as scheduler_rpcapi
32from manila import share
33from manila.share import rpcapi as share_rpcapi
34from manila.share import share_types
36CONF = cfg.CONF
37LOG = log.getLogger(__name__)
38QUOTAS = quota.QUOTAS
41class API(base.Base):
42 """API for interacting with the share manager."""
44 def __init__(self, db_driver=None):
45 self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
46 self.share_rpcapi = share_rpcapi.ShareAPI()
47 self.share_api = share.API()
48 super(API, self).__init__(db_driver)
50 def create(self, context, name=None, description=None,
51 share_type_ids=None, source_share_group_snapshot_id=None,
52 share_network_id=None, share_group_type_id=None,
53 availability_zone_id=None, availability_zone=None):
54 """Create new share group."""
56 share_group_snapshot = None
57 original_share_group = None
58 # NOTE(gouthamr): share_server_id is inherited from the
59 # parent share group if a share group snapshot is specified,
60 # else, it will be set in the share manager.
61 share_server_id = None
62 if source_share_group_snapshot_id:
63 share_group_snapshot = self.db.share_group_snapshot_get(
64 context, source_share_group_snapshot_id)
65 if share_group_snapshot['status'] != constants.STATUS_AVAILABLE:
66 msg = (_("Share group snapshot status must be %s.")
67 % constants.STATUS_AVAILABLE)
68 raise exception.InvalidShareGroupSnapshot(reason=msg)
70 original_share_group = self.db.share_group_get(
71 context, share_group_snapshot['share_group_id'])
72 share_type_ids = [
73 s['share_type_id']
74 for s in original_share_group['share_types']]
75 share_network_id = original_share_group['share_network_id']
76 share_server_id = original_share_group['share_server_id']
77 availability_zone_id = original_share_group['availability_zone_id']
79 # Get share_type_objects
80 share_type_objects = []
81 driver_handles_share_servers = None
82 for share_type_id in (share_type_ids or []):
83 try:
84 share_type_object = share_types.get_share_type(
85 context, share_type_id)
86 except exception.ShareTypeNotFound:
87 msg = _("Share type with id %s could not be found.")
88 raise exception.InvalidInput(msg % share_type_id)
89 share_type_objects.append(share_type_object)
91 extra_specs = share_type_object.get('extra_specs')
92 if extra_specs:
93 share_type_handle_ss = strutils.bool_from_string(
94 extra_specs.get(
95 constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS))
96 if driver_handles_share_servers is None:
97 driver_handles_share_servers = share_type_handle_ss
98 elif not driver_handles_share_servers == share_type_handle_ss:
99 # NOTE(ameade): if the share types have conflicting values
100 # for driver_handles_share_servers then raise bad request
101 msg = _("The specified share_types cannot have "
102 "conflicting values for the "
103 "driver_handles_share_servers extra spec.")
104 raise exception.InvalidInput(reason=msg)
106 if (not share_type_handle_ss) and share_network_id:
107 msg = _("When using a share types with the "
108 "driver_handles_share_servers extra spec as "
109 "False, a share_network_id must not be provided.")
110 raise exception.InvalidInput(reason=msg)
112 share_network = {}
113 try:
114 if share_network_id:
115 share_network = self.db.share_network_get(
116 context, share_network_id)
117 except exception.ShareNetworkNotFound:
118 msg = _("The specified share network does not exist.")
119 raise exception.InvalidInput(reason=msg)
121 if share_network:
122 # Check if share network is active, otherwise raise a BadRequest
123 api_common.check_share_network_is_active(share_network)
125 if (driver_handles_share_servers and 125 ↛ 127line 125 didn't jump to line 127 because the condition on line 125 was never true
126 not (source_share_group_snapshot_id or share_network_id)):
127 msg = _("When using a share type with the "
128 "driver_handles_share_servers extra spec as "
129 "True, a share_network_id must be provided.")
130 raise exception.InvalidInput(reason=msg)
132 try:
133 share_group_type = self.db.share_group_type_get(
134 context, share_group_type_id)
135 except exception.ShareGroupTypeNotFound:
136 msg = _("The specified share group type %s does not exist.")
137 raise exception.InvalidInput(reason=msg % share_group_type_id)
139 supported_share_types = set(
140 [x['share_type_id'] for x in share_group_type['share_types']])
141 supported_share_type_objects = [
142 share_types.get_share_type(context, share_type_id) for
143 share_type_id in supported_share_types
144 ]
146 if not set(share_type_ids or []) <= supported_share_types: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 msg = _("The specified share types must be a subset of the share "
148 "types supported by the share group type.")
149 raise exception.InvalidInput(reason=msg)
151 # Grab share type AZs for scheduling
152 share_types_of_new_group = (
153 share_type_objects or supported_share_type_objects
154 )
155 stype_azs_of_new_group = []
156 stypes_unsupported_in_az = []
157 for stype in share_types_of_new_group:
158 stype_azs = stype.get('extra_specs', {}).get(
159 'availability_zones', '')
160 if stype_azs:
161 stype_azs = stype_azs.split(',')
162 stype_azs_of_new_group.extend(stype_azs)
163 if availability_zone and availability_zone not in stype_azs:
164 # If an AZ is requested, it must be supported by the AZs
165 # configured in each of the share types requested
166 stypes_unsupported_in_az.append((stype['name'],
167 stype['id']))
169 if stypes_unsupported_in_az:
170 msg = _("Share group cannot be created since the following share "
171 "types are not supported within the availability zone "
172 "'%(az)s': (%(stypes)s)")
173 payload = {'az': availability_zone, 'stypes': ''}
174 for type_name, type_id in set(stypes_unsupported_in_az):
175 if payload['stypes']: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 payload['stypes'] += ', '
177 type_name = '%s ' % (type_name or '')
178 payload['stypes'] += type_name + '(ID: %s)' % type_id
179 raise exception.InvalidInput(reason=msg % payload)
181 try:
182 reservations = QUOTAS.reserve(context, share_groups=1)
183 except exception.OverQuota as e:
184 overs = e.kwargs['overs']
185 usages = e.kwargs['usages']
186 quotas = e.kwargs['quotas']
188 def _consumed(name):
189 return (usages[name]['reserved'] + usages[name]['in_use'])
191 if 'share_groups' in overs: 191 ↛ 201line 191 didn't jump to line 201 because the condition on line 191 was always true
192 msg = ("Quota exceeded for '%(s_uid)s' user in '%(s_pid)s' "
193 "project. (%(d_consumed)d of "
194 "%(d_quota)d already consumed).")
195 LOG.warning(msg, {
196 's_pid': context.project_id,
197 's_uid': context.user_id,
198 'd_consumed': _consumed('share_groups'),
199 'd_quota': quotas['share_groups'],
200 })
201 raise exception.ShareGroupsLimitExceeded()
203 options = {
204 'share_group_type_id': share_group_type_id,
205 'source_share_group_snapshot_id': source_share_group_snapshot_id,
206 'share_network_id': share_network_id,
207 'share_server_id': share_server_id,
208 'availability_zone_id': availability_zone_id,
209 'name': name,
210 'description': description,
211 'user_id': context.user_id,
212 'project_id': context.project_id,
213 'status': constants.STATUS_CREATING,
214 'share_types': share_type_ids or supported_share_types
215 }
216 if original_share_group:
217 options['host'] = original_share_group['host']
219 share_group = {}
220 try:
221 share_group = self.db.share_group_create(context, options)
222 if share_group_snapshot:
223 members = self.db.share_group_snapshot_members_get_all(
224 context, source_share_group_snapshot_id)
225 for member in members:
226 share_instance = self.db.share_instance_get(
227 context, member['share_instance_id'])
228 share_type = share_types.get_share_type(
229 context, share_instance['share_type_id'])
230 self.share_api.create(
231 context,
232 member['share_proto'],
233 member['size'],
234 None,
235 None,
236 share_group_id=share_group['id'],
237 share_group_snapshot_member=member,
238 share_type=share_type,
239 availability_zone=availability_zone_id,
240 share_network_id=share_network_id)
241 except Exception:
242 with excutils.save_and_reraise_exception():
243 if share_group:
244 self.db.share_group_destroy(
245 context.elevated(), share_group['id'])
246 QUOTAS.rollback(context, reservations)
248 try:
249 QUOTAS.commit(context, reservations)
250 except Exception:
251 with excutils.save_and_reraise_exception():
252 QUOTAS.rollback(context, reservations)
254 request_spec = {'share_group_id': share_group['id']}
255 request_spec.update(options)
256 request_spec['availability_zones'] = set(stype_azs_of_new_group)
257 request_spec['share_types'] = share_type_objects
258 request_spec['resource_type'] = share_group_type
260 if share_group_snapshot and original_share_group:
261 self.share_rpcapi.create_share_group(
262 context, share_group, original_share_group['host'])
263 else:
264 self.scheduler_rpcapi.create_share_group(
265 context, share_group_id=share_group['id'],
266 request_spec=request_spec, filter_properties={})
268 return share_group
270 def delete(self, context, share_group):
271 """Delete share group."""
273 share_group_id = share_group['id']
274 if not share_group['host']:
275 self.db.share_group_destroy(context.elevated(), share_group_id)
276 return
278 statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
279 if not share_group['status'] in statuses:
280 msg = (_("Share group status must be one of %(statuses)s")
281 % {"statuses": statuses})
282 raise exception.InvalidShareGroup(reason=msg)
284 # NOTE(ameade): check for group_snapshots in the group
285 if self.db.count_share_group_snapshots_in_share_group(
286 context, share_group_id):
287 msg = (_("Cannot delete a share group with snapshots"))
288 raise exception.InvalidShareGroup(reason=msg)
290 # NOTE(ameade): check for shares in the share group
291 if self.db.count_shares_in_share_group(context, share_group_id):
292 msg = (_("Cannot delete a share group with shares"))
293 raise exception.InvalidShareGroup(reason=msg)
295 share_group = self.db.share_group_update(
296 context, share_group_id, {'status': constants.STATUS_DELETING})
298 try:
299 reservations = QUOTAS.reserve(
300 context,
301 share_groups=-1,
302 project_id=share_group['project_id'],
303 user_id=share_group['user_id'],
304 )
305 except exception.OverQuota as e:
306 reservations = None
307 LOG.exception(
308 ("Failed to update quota for deleting share group: %s"), e)
310 try:
311 self.share_rpcapi.delete_share_group(context, share_group)
312 except Exception:
313 with excutils.save_and_reraise_exception():
314 QUOTAS.rollback(context, reservations)
316 if reservations: 316 ↛ exitline 316 didn't return from function 'delete' because the condition on line 316 was always true
317 QUOTAS.commit(
318 context, reservations,
319 project_id=share_group['project_id'],
320 user_id=share_group['user_id'],
321 )
323 def update(self, context, group, fields):
324 return self.db.share_group_update(context, group['id'], fields)
326 def get(self, context, share_group_id):
327 return self.db.share_group_get(context, share_group_id)
329 def get_all(self, context, detailed=True, search_opts=None, sort_key=None,
330 sort_dir=None):
332 if search_opts is None:
333 search_opts = {}
335 LOG.debug("Searching for share_groups by: %s",
336 search_opts)
338 # Get filtered list of share_groups
339 if search_opts.pop('all_tenants', 0) and context.is_admin:
340 share_groups = self.db.share_group_get_all(
341 context, detailed=detailed, filters=search_opts,
342 sort_key=sort_key, sort_dir=sort_dir)
343 else:
344 share_groups = self.db.share_group_get_all_by_project(
345 context, context.project_id, detailed=detailed,
346 filters=search_opts, sort_key=sort_key, sort_dir=sort_dir)
348 return share_groups
350 def create_share_group_snapshot(self, context, name=None, description=None,
351 share_group_id=None):
352 """Create new share group snapshot."""
353 options = {
354 'share_group_id': share_group_id,
355 'name': name,
356 'description': description,
357 'user_id': context.user_id,
358 'project_id': context.project_id,
359 'status': constants.STATUS_CREATING,
360 }
361 share_group = self.db.share_group_get(context, share_group_id)
362 # Check status of group, must be active
363 if not share_group['status'] == constants.STATUS_AVAILABLE:
364 msg = (_("Share group status must be %s")
365 % constants.STATUS_AVAILABLE)
366 raise exception.InvalidShareGroup(reason=msg)
368 # Create members for every share in the group
369 shares = self.db.share_get_all_by_share_group_id(
370 context, share_group_id)
372 # Check status of all shares, they must be active in order to snap
373 # the group
374 for s in shares:
375 if not s['status'] == constants.STATUS_AVAILABLE:
376 msg = (_("Share %(s)s in share group must have status "
377 "of %(status)s in order to create a group snapshot")
378 % {"s": s['id'],
379 "status": constants.STATUS_AVAILABLE})
380 raise exception.InvalidShareGroup(reason=msg)
382 try:
383 reservations = QUOTAS.reserve(context, share_group_snapshots=1)
384 except exception.OverQuota as e:
385 overs = e.kwargs['overs']
386 usages = e.kwargs['usages']
387 quotas = e.kwargs['quotas']
389 def _consumed(name):
390 return (usages[name]['reserved'] + usages[name]['in_use'])
392 if 'share_group_snapshots' in overs: 392 ↛ 402line 392 didn't jump to line 402 because the condition on line 392 was always true
393 msg = ("Quota exceeded for '%(s_uid)s' user in '%(s_pid)s' "
394 "project. (%(d_consumed)d of "
395 "%(d_quota)d already consumed).")
396 LOG.warning(msg, {
397 's_pid': context.project_id,
398 's_uid': context.user_id,
399 'd_consumed': _consumed('share_group_snapshots'),
400 'd_quota': quotas['share_group_snapshots'],
401 })
402 raise exception.ShareGroupSnapshotsLimitExceeded()
404 snap = {}
405 try:
406 snap = self.db.share_group_snapshot_create(context, options)
407 members = []
408 for s in shares:
409 member_options = {
410 'share_group_snapshot_id': snap['id'],
411 'user_id': context.user_id,
412 'project_id': context.project_id,
413 'status': constants.STATUS_CREATING,
414 'size': s['size'],
415 'share_proto': s['share_proto'],
416 'share_instance_id': s.instance['id']
417 }
418 member = self.db.share_group_snapshot_member_create(
419 context, member_options)
420 members.append(member)
422 # Cast to share manager
423 self.share_rpcapi.create_share_group_snapshot(
424 context, snap, share_group['host'])
425 except Exception:
426 with excutils.save_and_reraise_exception():
427 # This will delete the snapshot and all of it's members
428 if snap: 428 ↛ 430line 428 didn't jump to line 430 because the condition on line 428 was always true
429 self.db.share_group_snapshot_destroy(context, snap['id'])
430 QUOTAS.rollback(context, reservations)
432 try:
433 QUOTAS.commit(context, reservations)
434 except Exception:
435 with excutils.save_and_reraise_exception():
436 QUOTAS.rollback(context, reservations)
438 return snap
440 def delete_share_group_snapshot(self, context, snap):
441 """Delete share group snapshot."""
442 snap_id = snap['id']
443 statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
444 share_group = self.db.share_group_get(context, snap['share_group_id'])
445 if not snap['status'] in statuses:
446 msg = (_("Share group snapshot status must be one of"
447 " %(statuses)s") % {"statuses": statuses})
448 raise exception.InvalidShareGroupSnapshot(reason=msg)
450 self.db.share_group_snapshot_update(
451 context, snap_id, {'status': constants.STATUS_DELETING})
453 try:
454 reservations = QUOTAS.reserve(
455 context,
456 share_group_snapshots=-1,
457 project_id=snap['project_id'],
458 user_id=snap['user_id'],
459 )
460 except exception.OverQuota as e:
461 reservations = None
462 LOG.exception(
463 ("Failed to update quota for deleting share group snapshot: "
464 "%s"), e)
466 # Cast to share manager
467 self.share_rpcapi.delete_share_group_snapshot(
468 context, snap, share_group['host'])
470 if reservations:
471 QUOTAS.commit(
472 context, reservations,
473 project_id=snap['project_id'],
474 user_id=snap['user_id'],
475 )
477 def update_share_group_snapshot(self, context, share_group_snapshot,
478 fields):
479 return self.db.share_group_snapshot_update(
480 context, share_group_snapshot['id'], fields)
482 def get_share_group_snapshot(self, context, snapshot_id):
483 return self.db.share_group_snapshot_get(context, snapshot_id)
485 def get_all_share_group_snapshots(self, context, detailed=True,
486 search_opts=None, sort_key=None,
487 sort_dir=None):
488 if search_opts is None:
489 search_opts = {}
490 LOG.debug("Searching for share group snapshots by: %s",
491 search_opts)
493 # Get filtered list of share group snapshots
494 if search_opts.pop('all_tenants', 0) and context.is_admin:
495 share_group_snapshots = self.db.share_group_snapshot_get_all(
496 context, detailed=detailed, filters=search_opts,
497 sort_key=sort_key, sort_dir=sort_dir)
498 else:
499 share_group_snapshots = (
500 self.db.share_group_snapshot_get_all_by_project(
501 context, context.project_id, detailed=detailed,
502 filters=search_opts, sort_key=sort_key, sort_dir=sort_dir,
503 )
504 )
505 return share_group_snapshots
507 def get_all_share_group_snapshot_members(self, context,
508 share_group_snapshot_id):
509 members = self.db.share_group_snapshot_members_get_all(
510 context, share_group_snapshot_id)
511 return members