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

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. 

15 

16""" 

17Handles all requests relating to share groups. 

18""" 

19 

20from oslo_config import cfg 

21from oslo_log import log 

22from oslo_utils import excutils 

23from oslo_utils import strutils 

24 

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 

35 

36CONF = cfg.CONF 

37LOG = log.getLogger(__name__) 

38QUOTAS = quota.QUOTAS 

39 

40 

41class API(base.Base): 

42 """API for interacting with the share manager.""" 

43 

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) 

49 

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.""" 

55 

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) 

69 

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'] 

78 

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) 

90 

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) 

105 

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) 

111 

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) 

120 

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) 

124 

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) 

131 

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) 

138 

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 ] 

145 

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) 

150 

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'])) 

168 

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) 

180 

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'] 

187 

188 def _consumed(name): 

189 return (usages[name]['reserved'] + usages[name]['in_use']) 

190 

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

202 

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'] 

218 

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) 

247 

248 try: 

249 QUOTAS.commit(context, reservations) 

250 except Exception: 

251 with excutils.save_and_reraise_exception(): 

252 QUOTAS.rollback(context, reservations) 

253 

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 

259 

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={}) 

267 

268 return share_group 

269 

270 def delete(self, context, share_group): 

271 """Delete share group.""" 

272 

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 

277 

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) 

283 

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) 

289 

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) 

294 

295 share_group = self.db.share_group_update( 

296 context, share_group_id, {'status': constants.STATUS_DELETING}) 

297 

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) 

309 

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) 

315 

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 ) 

322 

323 def update(self, context, group, fields): 

324 return self.db.share_group_update(context, group['id'], fields) 

325 

326 def get(self, context, share_group_id): 

327 return self.db.share_group_get(context, share_group_id) 

328 

329 def get_all(self, context, detailed=True, search_opts=None, sort_key=None, 

330 sort_dir=None): 

331 

332 if search_opts is None: 

333 search_opts = {} 

334 

335 LOG.debug("Searching for share_groups by: %s", 

336 search_opts) 

337 

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) 

347 

348 return share_groups 

349 

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) 

367 

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) 

371 

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) 

381 

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'] 

388 

389 def _consumed(name): 

390 return (usages[name]['reserved'] + usages[name]['in_use']) 

391 

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

403 

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) 

421 

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) 

431 

432 try: 

433 QUOTAS.commit(context, reservations) 

434 except Exception: 

435 with excutils.save_and_reraise_exception(): 

436 QUOTAS.rollback(context, reservations) 

437 

438 return snap 

439 

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) 

449 

450 self.db.share_group_snapshot_update( 

451 context, snap_id, {'status': constants.STATUS_DELETING}) 

452 

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) 

465 

466 # Cast to share manager 

467 self.share_rpcapi.delete_share_group_snapshot( 

468 context, snap, share_group['host']) 

469 

470 if reservations: 

471 QUOTAS.commit( 

472 context, reservations, 

473 project_id=snap['project_id'], 

474 user_id=snap['user_id'], 

475 ) 

476 

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) 

481 

482 def get_share_group_snapshot(self, context, snapshot_id): 

483 return self.db.share_group_snapshot_get(context, snapshot_id) 

484 

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) 

492 

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 

506 

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