Coverage for manila/share/drivers/macrosan/macrosan_helper.py: 95%

347 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright (c) 2022 MacroSAN Technologies Co., Ltd. 

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 

16import re 

17 

18from oslo_config import cfg 

19from oslo_log import log 

20from oslo_utils import units 

21 

22from manila import exception 

23from manila.i18n import _ 

24from manila.share.drivers.macrosan import macrosan_constants as constants 

25from manila.share.drivers.macrosan import rest_helper 

26from manila.share import utils as share_utils 

27 

28CONF = cfg.CONF 

29 

30LOG = log.getLogger(__name__) 

31 

32 

33class MacrosanHelper(object): 

34 def __init__(self, configuration): 

35 self.configuration = configuration 

36 self.rest = rest_helper.RestHelper(self.configuration) 

37 self.snapshot_support = False 

38 self.replication_support = False 

39 self.pools = self.configuration.macrosan_share_pools 

40 

41 def check_share_service(self): 

42 nfs_service = self.rest._get_nfs_service_status() 

43 if nfs_service['serviceStatus'] not in [constants.NFS_NON_CONFIG, 

44 constants.NFS_ENABLED, 

45 constants.NFS_DISABLED]: 

46 raise exception.MacrosanBackendExeption( 

47 reason=_("nfs service exception. Please check backend")) 

48 elif nfs_service['serviceStatus'] == constants.NFS_NON_CONFIG: 

49 self.rest._config_nfs_service() 

50 self.rest._start_nfs_service() 

51 elif nfs_service['serviceStatus'] == constants.NFS_DISABLED: 

52 if (nfs_service['nfs3Status'] == constants.NFS_NON_SUPPORTED and 52 ↛ 55line 52 didn't jump to line 55 because the condition on line 52 was always true

53 nfs_service['nfs4Status'] == constants.NFS_NON_SUPPORTED): 

54 self.rest._config_nfs_service() 

55 self.rest._start_nfs_service() 

56 else: 

57 if (nfs_service['nfs3Status'] == constants.NFS_NON_SUPPORTED and 

58 nfs_service['nfs4Status'] == constants.NFS_NON_SUPPORTED): 

59 self.rest._config_nfs_service() 

60 

61 cifs_status = self.rest._get_cifs_service_status() 

62 if cifs_status == constants.CIFS_EXCEPTION: 

63 raise exception.MacrosanBackendExeption( 

64 reason=_("cifs service exception. Please check backend")) 

65 elif cifs_status == constants.CIFS_NON_CONFIG: 

66 """need config first, then start service""" 

67 self.rest._config_cifs_service() 

68 self.rest._start_cifs_service() 

69 elif cifs_status == constants.CIFS_DISABLED: 

70 self.rest._start_cifs_service() 

71 status = self.rest._get_cifs_service_status() 

72 if status == constants.CIFS_SHARE_MODE: 72 ↛ exitline 72 didn't return from function 'check_share_service' because the condition on line 72 was always true

73 self.rest._config_cifs_service() 

74 elif cifs_status == constants.CIFS_SHARE_MODE: 

75 self.rest._config_cifs_service() 

76 

77 def do_setup(self): 

78 """get token""" 

79 self.rest.login() 

80 

81 def create_share(self, share, share_server=None): 

82 """Create a share""" 

83 pool_name, share_name, proto = self._get_share_instance_pnp(share) 

84 share_size = ''.join((str(share['size']), 'GB')) 

85 

86 # first create filesystem 

87 self.rest._create_filesystem(fs_name=share_name, 

88 pool_name=pool_name, 

89 filesystem_quota=share_size) 

90 

91 share_path = self._generate_share_path(share_name) 

92 # second create filesystem dir 

93 self.rest._create_filesystem_dir(share_path) 

94 # third create nfs or cifs share 

95 if proto == 'NFS': 

96 self.rest._create_nfs_share(share_path=share_path) 

97 else: 

98 user_name = 'manilanobody' 

99 user_passwd = 'manilanobody' 

100 group_name = 'manilanobody' 

101 ret = self._ensure_user(user_name, user_passwd, group_name) 

102 if not ret: 

103 self.rest._delete_filesystem(share_name) 

104 raise exception.MacrosanBackendExeption( 

105 reason=(_( 

106 'Failed to create share %(share)s. Reason: ' 

107 'username %(user_name)s error.') 

108 % {'share': share_name, 'user_name': user_name})) 

109 

110 rw_list = [user_name] 

111 rw_list_type = ['0'] 

112 self.rest._create_cifs_share(share_name=share_name, 

113 share_path=share_path, 

114 rw_list=rw_list, 

115 rw_list_type=rw_list_type) 

116 

117 location = self._get_location_path(share_path, share_name, proto) 

118 return location 

119 

120 def delete_share(self, share, share_server=None): 

121 """Delete a share.""" 

122 pool, share_name, proto = self._get_share_instance_pnp(share) 

123 share_path = self._generate_share_path(share_name) 

124 

125 backend_share = self._get_share(share_path, proto) 

126 if not backend_share: 

127 LOG.error(f'Share {share_name} not found.') 

128 filesystem = self.rest._get_filesystem(share_name) 

129 if filesystem: 129 ↛ exitline 129 didn't return from function 'delete_share' because the condition on line 129 was always true

130 self.rest._delete_filesystem(share_name) 

131 else: 

132 if proto == 'NFS': 

133 self.rest._delete_nfs_share(share_path) 

134 else: 

135 self.rest._delete_cifs_share(share_name, share_path) 

136 self.rest._delete_filesystem(share_name) 

137 

138 def extend_share(self, share, new_size, share_server=None): 

139 """Extend share""" 

140 pool, share_name, proto = self._get_share_instance_pnp(share) 

141 share_path = self._generate_share_path(share_name) 

142 backend_share = self._get_share(share_path, proto) 

143 if not backend_share: 

144 msg = f"Can't find the share by share name: {share_name}." 

145 msg = _(msg) 

146 LOG.error(msg) 

147 raise exception.ShareResourceNotFound(share_id=share['id']) 

148 

149 # storage size logic already in manila/share/api.py extend func 

150 # size param need unit 

151 new_size = ''.join((str(new_size), 'GB')) 

152 self.rest._update_share_size(share_name, new_size) 

153 

154 def shrink_share(self, share, new_size, share_server=None): 

155 """Shrink share""" 

156 pool, share_name, proto = self._get_share_instance_pnp(share) 

157 share_path = self._generate_share_path(share_name) 

158 backend_share = self._get_share(share_path, proto) 

159 if not backend_share: 

160 msg = f"Can't find the share by share name: {share_name}." 

161 msg = _(msg) 

162 LOG.error(msg) 

163 raise exception.ShareResourceNotFound(share_id=share['id']) 

164 

165 filesystem_info = self.rest._get_filesystem(share_name) 

166 used_size = self._unit_convert_toGB(filesystem_info['usedCapacity']) 

167 if new_size <= used_size: 

168 raise exception.ShareShrinkingPossibleDataLoss( 

169 share_id=share['id']) 

170 # storage size logic already in manila/share/api.py shrink func 

171 new_size = ''.join((str(new_size), 'GB')) 

172 self.rest._update_share_size(share_name, new_size) 

173 

174 def ensure_share(self, share, share_server=None): 

175 """Enusre that share is exported""" 

176 pool, share_name, proto = self._get_share_instance_pnp(share) 

177 share_path = self._generate_share_path(share_name) 

178 backend_share = self._get_share(share_path, proto) 

179 if not backend_share: 

180 raise exception.ShareResourceNotFound(share_id=share['id']) 

181 

182 location = self._get_location_path(share_path, share_name, proto) 

183 return [location] 

184 

185 def _allow_access(self, share, access, share_server=None): 

186 """Allow access to the share.""" 

187 pool, share_name, proto = self._get_share_instance_pnp(share) 

188 share_path = self._generate_share_path(share_name) 

189 access_level = access['access_level'] 

190 share_id = share['id'] 

191 if access_level not in ('rw', 'ro'): 

192 raise exception.InvalidShareAccess( 

193 reason=(_('Unsupported access level: %s.') % access_level)) 

194 if proto == 'NFS': 

195 self._allow_nfs_access(share_path, share_name, access, share_id) 

196 elif proto == 'CIFS': 196 ↛ exitline 196 didn't return from function '_allow_access' because the condition on line 196 was always true

197 self._allow_cifs_access(share_path, share_name, access, share_id) 

198 

199 def _allow_nfs_access(self, share_path, share_name, access, share_id): 

200 """Allow nfs access.""" 

201 access_type = access['access_type'] 

202 access_to = access['access_to'] 

203 access_level = access['access_level'] 

204 # Only use 'ip', 

205 # input "*" replace all, or ip 172.0.1.11 , 

206 # or ip network segment 172.0.1.11/255.255.0.0 172.0.1.11/16 

207 if access_type != 'ip': 

208 message = (_('NFS shares only allow IP access types. ' 

209 'access_type: %(access_type)s') % 

210 {'access_type': access_type}) 

211 raise exception.InvalidShareAccess(reason=message) 

212 backend_share = self.rest._get_nfs_share(share_path) 

213 if not backend_share: 

214 msg = (_("Can't find the share by share name: %s.") 

215 % share_name) 

216 LOG.error(msg) 

217 raise exception.ShareResourceNotFound(share_id=share_id) 

218 

219 if access_to == '0.0.0.0/0': 

220 access_to = '*' 

221 share_client = self.rest._get_access_from_nfs_share(share_path, 

222 access_to) 

223 

224 if share_client: 

225 if access_level != share_client['accessRight']: 225 ↛ exitline 225 didn't return from function '_allow_nfs_access' because the condition on line 225 was always true

226 self.rest._change_nfs_access_rest(share_path, 

227 access_to, 

228 access_level) 

229 else: 

230 self.rest._allow_nfs_access_rest(share_path, 

231 access_to, 

232 access_level) 

233 

234 def _allow_cifs_access(self, share_path, share_name, access, share_id): 

235 """Allow cifs access.""" 

236 access_type = access['access_type'] 

237 access_to = access['access_to'] 

238 access_level = access['access_level'] 

239 if access_type != 'user': 

240 message = _('Only user access type is ' 

241 'allowed for CIFS shares.') 

242 raise exception.InvalidShareAccess(reason=message) 

243 

244 backend_share = self.rest._get_cifs_share(share_path) 

245 if not backend_share: 

246 msg = (_("Can't find the share by share name: %s.") 

247 % share_name) 

248 LOG.error(msg) 

249 raise exception.ShareResourceNotFound(share_id=share_id) 

250 

251 share_client = self.rest._get_access_from_cifs_share(share_path, 

252 access_to) 

253 if share_client: 

254 if access_level != share_client['accessRight']: 254 ↛ exitline 254 didn't return from function '_allow_cifs_access' because the condition on line 254 was always true

255 self.rest._change_cifs_access_rest(share_path, 

256 access_to, 

257 access_level, 

258 share_client['ugType']) 

259 else: 

260 self.rest._allow_cifs_access_rest(share_path, 

261 access_to, 

262 access_level) 

263 

264 def _deny_access(self, share, access, share_server=None): 

265 """Deny access to the share.""" 

266 pool, share_name, proto = self._get_share_instance_pnp(share) 

267 share_path = self._generate_share_path(share_name) 

268 

269 if proto == 'NFS': 

270 self._deny_nfs_access(share_path, share_name, access) 

271 else: 

272 self._deny_cifs_access(share_path, share_name, access) 

273 

274 def _deny_nfs_access(self, share_path, share_name, access): 

275 """Deny nfs access.""" 

276 access_type = access['access_type'] 

277 access_to = access['access_to'] 

278 if access_type != 'ip': 

279 LOG.error('Only IP access types are allowed ' 

280 'for NFS shares.') 

281 return 

282 if access_to == '0.0.0.0/0': 

283 access_to = '*' 

284 share_client = self.rest._get_access_from_nfs_share(share_path, 

285 access_to) 

286 if not share_client: 

287 LOG.error(f'Could not list the share access for share ' 

288 f'{share_name}') 

289 return 

290 self.rest._delete_nfs_access_rest(share_path, access_to) 

291 

292 def _deny_cifs_access(self, share_path, share_name, access): 

293 """Deny cifs access.""" 

294 access_type = access['access_type'] 

295 access_to = access['access_to'] 

296 if access_type != 'user': 

297 LOG.error('Only USER access types are allowed ' 

298 'for CIFS shares.') 

299 return 

300 share_client = self.rest._get_access_from_cifs_share(share_path, 

301 access_to) 

302 if not share_client: 

303 LOG.error(f'Could not list the share access for share ' 

304 f'{share_name}') 

305 return 

306 self.rest._delete_cifs_access_rest(share_path, 

307 share_client['ugName'], 

308 share_client['ugType']) 

309 

310 def _clear_access(self, share, share_server=None): 

311 """Remove all access rules of the share""" 

312 pool, share_name, proto = self._get_share_instance_pnp(share) 

313 share_path = self._generate_share_path(share_name) 

314 access_list = self._get_all_access_from_share(share_path, proto) 

315 if not access_list: 

316 LOG.error(f'Could not list the share access for share ' 

317 f'{share_name}') 

318 return 

319 

320 if proto == 'NFS': 

321 for share_access in access_list: 

322 # IPv4 Address Blocks Reserved for Documentation 

323 if share_access['access_to'] == '192.0.2.0': 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true

324 continue 

325 self.rest._delete_nfs_access_rest(share_path, 

326 share_access['access_to']) 

327 elif proto == 'CIFS': 327 ↛ exitline 327 didn't return from function '_clear_access' because the condition on line 327 was always true

328 for share_access in access_list: 

329 if (share_access['access_to'] == 'manilanobody' 

330 and share_access['ugType'] == '0'): 

331 continue 

332 self.rest._delete_cifs_access_rest(share_path, 

333 share_access['access_to'], 

334 share_access['ugType']) 

335 

336 def update_access(self, share, access_rules, add_rules, 

337 delete_rules, share_server=None): 

338 """Update access rules list.""" 

339 access_updates = {} 

340 if not (add_rules or delete_rules): 

341 self._clear_access(share, share_server) 

342 for access_rule in access_rules: 

343 try: 

344 self._allow_access(share, access_rule, share_server) 

345 except exception.InvalidShareAccess as e: 

346 msg = f'Failed to allow {access_rule["access_level"]} ' \ 

347 f'access to {access_rule["access_to"]}, reason {e}' 

348 msg = _(msg) 

349 LOG.error(msg) 

350 access_updates.update( 

351 {access_rule['access_id']: {'state': 'error'}}) 

352 else: 

353 for access_rule in delete_rules: 

354 self._deny_access(share, access_rule, share_server) 

355 for access_rule in add_rules: 

356 try: 

357 self._allow_access(share, access_rule, share_server) 

358 except exception.InvalidShareAccess as e: 

359 msg = f'Failed to allow {access_rule["access_level"]} ' \ 

360 f'access to {access_rule["access_to"]}, reason {e}' 

361 msg = _(msg) 

362 LOG.error(msg) 

363 access_updates.update( 

364 {access_rule['access_id']: {'state': 'error'}}) 

365 return access_updates 

366 

367 def _get_all_access_from_share(self, share_path, share_proto): 

368 access_list = [] 

369 if share_proto == 'NFS': 

370 access_list = self.rest._get_all_nfs_access_rest(share_path) 

371 elif share_proto == 'CIFS': 371 ↛ 373line 371 didn't jump to line 373 because the condition on line 371 was always true

372 access_list = self.rest._get_all_cifs_access_rest(share_path) 

373 return access_list 

374 

375 def _ensure_user(self, user_name, user_passwd, group_name): 

376 ret_user = self.rest._query_user(user_name) 

377 if ret_user == constants.USER_NOT_EXIST: 

378 ret_group = self.rest._query_group(group_name) 

379 if ret_group not in [constants.GROUP_NOT_EXIST, 

380 constants.GROUP_EXIST]: 

381 msg = f'Failed to use group {group_name}' 

382 msg = _(msg) 

383 raise exception.InvalidInput(reason=msg) 

384 elif ret_group == constants.GROUP_NOT_EXIST: 384 ↛ 386line 384 didn't jump to line 386 because the condition on line 384 was always true

385 self.rest._add_localgroup(group_name) 

386 self.rest._add_localuser(user_name, user_passwd, group_name) 

387 return True 

388 elif ret_user == constants.USER_EXIST: 

389 return True 

390 else: 

391 return False 

392 

393 def update_share_stats(self, dict_data): 

394 """Update pools info""" 

395 result = self.rest._get_all_pool() 

396 dict_data["pools"] = [] 

397 for pool_name in self.pools: 

398 pool_capacity = self._get_pool_capacity(pool_name, result) 

399 if pool_capacity: 

400 pool = { 

401 'pool_name': pool_name, 

402 'total_capacity_gb': pool_capacity['totalcapacity'], 

403 'free_capacity_gb': pool_capacity['freecapacity'], 

404 'allocated_capacity_gb': 

405 pool_capacity['allocatedcapacity'], 

406 'reserved_percentage': 

407 self.configuration.reserved_share_percentage, 

408 'reserved_snapshot_percentage': 

409 self.configuration 

410 .reserved_share_from_snapshot_percentage 

411 or self.configuration.reserved_share_percentage, 

412 'reserved_share_extend_percentage': 

413 self.configuration.reserved_share_extend_percentage 

414 or self.configuration.reserved_share_percentage, 

415 'dedupe': False, 

416 'compression': False, 

417 'qos': False, 

418 'thin_provisioning': False, 

419 'snapshot_support': self.snapshot_support, 

420 'create_share_from_snapshot_support': 

421 self.snapshot_support, 

422 } 

423 

424 dict_data["pools"].append(pool) 

425 

426 if not dict_data['pools']: 

427 msg = _("StoragePool is None") 

428 LOG.error(msg) 

429 raise exception.InvalidInput(reason=msg) 

430 

431 def _get_pool_capacity(self, pool_name, result): 

432 """Get total,allocated,free capacity of the pools""" 

433 pool_info = self._find_pool_info(pool_name, result) 

434 

435 if pool_info: 435 ↛ 447line 435 didn't jump to line 447 because the condition on line 435 was always true

436 total_capacity = int(self._unit_convert_toGB( 

437 pool_info['totalcapacity'])) 

438 free_capacity = int(self._unit_convert_toGB( 

439 pool_info['freecapacity'])) 

440 allocated_capacity = int(self._unit_convert_toGB( 

441 pool_info['allocatedcapacity'])) 

442 

443 pool_info['totalcapacity'] = total_capacity 

444 pool_info['freecapacity'] = free_capacity 

445 pool_info['allocatedcapacity'] = allocated_capacity 

446 

447 return pool_info 

448 

449 def _unit_convert_toGB(self, capacity): 

450 """Convert unit to GB""" 

451 capacity = capacity.upper() 

452 

453 try: 

454 # get unit char array and use join connect to string 

455 unit = re.findall(r'[A-Z]', capacity) 

456 unit = ''.join(unit) 

457 except BaseException: 

458 unit = '' 

459 # get capacity size,unit is GB 

460 capacity = capacity.replace(unit, '') 

461 capacity = float(capacity) 

462 if unit in ['B', '']: 

463 capacity = capacity / units.Gi 

464 elif unit in ['K', 'KB']: 

465 capacity = capacity / units.Mi 

466 elif unit in ['M', 'MB']: 

467 capacity = capacity / units.Ki 

468 elif unit in ['G', 'GB']: 

469 capacity = capacity 

470 elif unit in ['T', 'TB']: 470 ↛ 472line 470 didn't jump to line 472 because the condition on line 470 was always true

471 capacity = capacity * units.Ki 

472 elif unit in ['E', 'EB']: 

473 capacity = capacity * units.Mi 

474 

475 capacity = '%.0f' % capacity 

476 

477 return float(capacity) 

478 

479 def _generate_share_name(self, share): 

480 share_name = 'manila_%s' % share['id'] 

481 return self._format_name(share_name) 

482 

483 def _format_name(self, name): 

484 """format name to meet the backend requirements""" 

485 name = name[0: 31] 

486 name = name.replace('-', '_') 

487 return name 

488 

489 def _generate_share_path(self, share_name): 

490 """add '/' as path""" 

491 share_path = r'/%(path)s/%(dirName)s' % { 

492 'path': share_name.replace("-", "_"), 

493 'dirName': share_name.replace("-", "_") 

494 } 

495 return share_path 

496 

497 def _get_location_path(self, share_path, share_name, share_proto, ip=None): 

498 location = None 

499 if ip is None: 499 ↛ 501line 499 didn't jump to line 501 because the condition on line 499 was always true

500 ip = self.configuration.macrosan_nas_ip 

501 share_proto = share_proto.upper() 

502 if share_proto == 'NFS': 

503 location = f'{ip}:{share_path}' 

504 elif share_proto == 'CIFS': 504 ↛ 506line 504 didn't jump to line 506 because the condition on line 504 was always true

505 location = f'\\\\{ip}\\{share_name}' 

506 return location 

507 

508 def _get_share_instance_pnp(self, share_instance): 

509 

510 proto = share_instance['share_proto'].upper() 

511 share_name = self._generate_share_name(share_instance) 

512 pool = share_utils.extract_host(share_instance['host'], level='pool') 

513 if not pool: 

514 msg = _("Pool doesn't exist in host field.") 

515 raise exception.InvalidHost(reason=msg) 

516 

517 if proto != 'NFS' and proto != 'CIFS': 

518 msg = f'Share protocol {proto} is not supported.' 

519 msg = _(msg) 

520 raise exception.MacrosanBackendExeption(reason=msg) 

521 

522 return pool, share_name, proto 

523 

524 def _get_share(self, share_path, proto): 

525 return (self.rest._get_nfs_share(share_path) 

526 if proto == 'NFS' 

527 else self.rest._get_cifs_share(share_path)) 

528 

529 def _find_pool_info(self, pool_name, result): 

530 if pool_name is None: 530 ↛ 531line 530 didn't jump to line 531 because the condition on line 530 was never true

531 return 

532 pool_info = {} 

533 pool_name = pool_name.strip() 

534 

535 for item in result.get('data', []): 

536 if pool_name == item['name']: 

537 pool_info['name'] = item['name'] 

538 pool_info['totalcapacity'] = item['size'] 

539 pool_info['allocatedcapacity'] = item['allocated'] 

540 pool_info['freecapacity'] = item['free'] 

541 pool_info['health'] = item['health'] 

542 pool_info['rw'] = item['rwStatus'] 

543 break 

544 

545 return pool_info