Coverage for manila/share/drivers/tegile/tegile.py: 98%

227 statements  

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

1# Copyright (c) 2016 by Tegile Systems, Inc. 

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

16Share driver for Tegile storage. 

17""" 

18 

19import json 

20import requests 

21 

22from oslo_config import cfg 

23from oslo_log import log 

24 

25from manila import exception 

26from manila.i18n import _ 

27from manila.share import driver 

28from manila.share import utils as share_utils 

29from manila import utils 

30 

31tegile_opts = [ 

32 cfg.HostAddressOpt('tegile_nas_server', 

33 help='Tegile NAS server hostname or IP address.'), 

34 cfg.StrOpt('tegile_nas_login', 

35 help='User name for the Tegile NAS server.'), 

36 cfg.StrOpt('tegile_nas_password', 

37 secret=True, 

38 help='Password for the Tegile NAS server.'), 

39 cfg.StrOpt('tegile_default_project', 

40 help='Create shares in this project')] 

41 

42 

43CONF = cfg.CONF 

44CONF.register_opts(tegile_opts) 

45 

46LOG = log.getLogger(__name__) 

47DEFAULT_API_SERVICE = 'openstack' 

48TEGILE_API_PATH = 'zebi/api' 

49TEGILE_LOCAL_CONTAINER_NAME = 'Local' 

50TEGILE_SNAPSHOT_PREFIX = 'Manual-S-' 

51VENDOR = 'Tegile Systems Inc.' 

52DEFAULT_BACKEND_NAME = 'Tegile' 

53VERSION = '1.0.0' 

54DEBUG_LOGGING = False # For debugging purposes 

55 

56 

57def debugger(func): 

58 """Returns a wrapper that wraps func. 

59 

60 The wrapper will log the entry and exit points of the function. 

61 """ 

62 

63 def wrapper(*args, **kwds): 

64 if DEBUG_LOGGING: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 LOG.debug('Entering %(classname)s.%(funcname)s', 

66 { 

67 'classname': args[0].__class__.__name__, 

68 'funcname': func.__name__, 

69 }) 

70 LOG.debug('Arguments: %(args)s, %(kwds)s', 

71 { 

72 'args': args[1:], 

73 'kwds': kwds, 

74 }) 

75 f_result = func(*args, **kwds) 

76 if DEBUG_LOGGING: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 LOG.debug('Exiting %(classname)s.%(funcname)s', 

78 { 

79 'classname': args[0].__class__.__name__, 

80 'funcname': func.__name__, 

81 }) 

82 LOG.debug('Results: %(result)s', 

83 {'result': f_result}) 

84 return f_result 

85 

86 return wrapper 

87 

88 

89class TegileAPIExecutor(object): 

90 def __init__(self, classname, hostname, username, password): 

91 self._classname = classname 

92 self._hostname = hostname 

93 self._username = username 

94 self._password = password 

95 

96 def __call__(self, *args, **kwargs): 

97 return self._send_api_request(*args, **kwargs) 

98 

99 @debugger 

100 @utils.retry(retry_param=(requests.ConnectionError, requests.Timeout), 

101 interval=30, 

102 retries=3, 

103 backoff_rate=1) 

104 def _send_api_request(self, method, params=None, 

105 request_type='post', 

106 api_service=DEFAULT_API_SERVICE, 

107 fine_logging=DEBUG_LOGGING): 

108 if params is not None: 

109 params = json.dumps(params) 

110 

111 url = 'https://%s/%s/%s/%s' % (self._hostname, 

112 TEGILE_API_PATH, 

113 api_service, 

114 method) 

115 if fine_logging: 

116 LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, ' 

117 'url: %(url)s', { 

118 'classname': self._classname, 

119 'method': method, 

120 'url': url, 

121 }) 

122 if request_type == 'post': 

123 if fine_logging: 

124 LOG.debug('TegileAPIExecutor(%(classname)s) ' 

125 'method: %(method)s, payload: %(payload)s', 

126 { 

127 'classname': self._classname, 

128 'method': method, 

129 'payload': params, 

130 }) 

131 req = requests.post(url, 

132 data=params, 

133 auth=(self._username, self._password), 

134 verify=False) 

135 else: 

136 req = requests.get(url, 

137 auth=(self._username, self._password), 

138 verify=False) 

139 

140 if fine_logging: 

141 LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, ' 

142 'return code: %(retcode)s', 

143 { 

144 'classname': self._classname, 

145 'method': method, 

146 'retcode': req, 

147 }) 

148 try: 

149 response = req.json() 

150 if fine_logging: 

151 LOG.debug('TegileAPIExecutor(%(classname)s) ' 

152 'method: %(method)s, response: %(response)s', 

153 { 

154 'classname': self._classname, 

155 'method': method, 

156 'response': response, 

157 }) 

158 except ValueError: 

159 # Some APIs don't return output and that's fine 

160 response = '' 

161 req.close() 

162 

163 if req.status_code != 200: 

164 raise exception.TegileAPIException(response=req.text) 

165 

166 return response 

167 

168 

169class TegileShareDriver(driver.ShareDriver): 

170 """Tegile NAS driver. Allows for NFS and CIFS NAS storage usage.""" 

171 def __init__(self, *args, **kwargs): 

172 super(TegileShareDriver, self).__init__(False, *args, **kwargs) 

173 

174 LOG.warning('Tegile share driver has been deprecated and will be ' 

175 'removed in a future release.') 

176 

177 self.configuration.append_config_values(tegile_opts) 

178 self._default_project = (self.configuration.safe_get( 

179 "tegile_default_project") or 'openstack') 

180 self._backend_name = (self.configuration.safe_get('share_backend_name') 

181 or CONF.share_backend_name 

182 or DEFAULT_BACKEND_NAME) 

183 self._hostname = self.configuration.safe_get('tegile_nas_server') 

184 username = self.configuration.safe_get('tegile_nas_login') 

185 password = self.configuration.safe_get('tegile_nas_password') 

186 self._api = TegileAPIExecutor(self.__class__.__name__, 

187 self._hostname, 

188 username, 

189 password) 

190 

191 @debugger 

192 def create_share(self, context, share, share_server=None): 

193 """Is called to create share.""" 

194 share_name = share['name'] 

195 share_proto = share['share_proto'] 

196 

197 pool_name = share_utils.extract_host(share['host'], level='pool') 

198 

199 params = (pool_name, self._default_project, share_name, share_proto) 

200 

201 # Share name coming from the backend is the most reliable. Sometimes 

202 # a few options in Tegile array could cause sharename to be different 

203 # from the one passed to it. Eg. 'projectname-sharename' instead 

204 # of 'sharename' if inherited share properties are selected. 

205 ip, real_share_name = self._api('createShare', params).split() 

206 

207 LOG.info("Created share %(sharename)s, share id %(shid)s.", 

208 {'sharename': share_name, 'shid': share['id']}) 

209 

210 return self._get_location_path(real_share_name, share_proto, ip) 

211 

212 @debugger 

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

214 """Is called to extend share. 

215 

216 There is no resize for Tegile shares. 

217 We just adjust the quotas. The API is still called 'resizeShare'. 

218 """ 

219 

220 self._adjust_size(share, new_size, share_server) 

221 

222 @debugger 

223 def shrink_share(self, shrink_share, shrink_size, share_server=None): 

224 """Uses resize_share to shrink a share. 

225 

226 There is no shrink for Tegile shares. 

227 We just adjust the quotas. The API is still called 'resizeShare'. 

228 """ 

229 self._adjust_size(shrink_share, shrink_size, share_server) 

230 

231 @debugger 

232 def _adjust_size(self, share, new_size, share_server=None): 

233 pool, project, share_name = self._get_pool_project_share_name(share) 

234 params = ('%s/%s/%s/%s' % (pool, 

235 TEGILE_LOCAL_CONTAINER_NAME, 

236 project, 

237 share_name), 

238 str(new_size), 

239 'GB') 

240 self._api('resizeShare', params) 

241 

242 @debugger 

243 def delete_share(self, context, share, share_server=None): 

244 """Is called to remove share.""" 

245 pool, project, share_name = self._get_pool_project_share_name(share) 

246 params = ('%s/%s/%s/%s' % (pool, 

247 TEGILE_LOCAL_CONTAINER_NAME, 

248 project, 

249 share_name), 

250 True, 

251 False) 

252 

253 self._api('deleteShare', params) 

254 

255 @debugger 

256 def create_snapshot(self, context, snapshot, share_server=None): 

257 """Is called to create snapshot.""" 

258 snap_name = snapshot['name'] 

259 

260 pool, project, share_name = self._get_pool_project_share_name( 

261 snapshot['share']) 

262 

263 share = { 

264 'poolName': '%s' % pool, 

265 'projectName': '%s' % project, 

266 'name': share_name, 

267 'availableSize': 0, 

268 'totalSize': 0, 

269 'datasetPath': '%s/%s/%s' % 

270 (pool, 

271 TEGILE_LOCAL_CONTAINER_NAME, 

272 project), 

273 'mountpoint': share_name, 

274 'local': 'true', 

275 } 

276 

277 params = (share, snap_name, False) 

278 

279 LOG.info('Creating snapshot for share_name=%(shr)s' 

280 ' snap_name=%(name)s', 

281 {'shr': share_name, 'name': snap_name}) 

282 

283 self._api('createShareSnapshot', params) 

284 

285 @debugger 

286 def create_share_from_snapshot(self, context, share, snapshot, 

287 share_server=None, parent_share=None): 

288 """Create a share from a snapshot - clone a snapshot.""" 

289 pool, project, share_name = self._get_pool_project_share_name(share) 

290 

291 params = ('%s/%s/%s/%s@%s%s' % (pool, 

292 TEGILE_LOCAL_CONTAINER_NAME, 

293 project, 

294 snapshot['share_name'], 

295 TEGILE_SNAPSHOT_PREFIX, 

296 snapshot['name'], 

297 ), 

298 share_name, 

299 True, 

300 ) 

301 

302 ip, real_share_name = self._api('cloneShareSnapshot', 

303 params).split() 

304 

305 share_proto = share['share_proto'] 

306 return self._get_location_path(real_share_name, share_proto, ip) 

307 

308 @debugger 

309 def delete_snapshot(self, context, snapshot, share_server=None): 

310 """Is called to remove snapshot.""" 

311 pool, project, share_name = self._get_pool_project_share_name( 

312 snapshot['share']) 

313 params = ('%s/%s/%s/%s@%s%s' % (pool, 

314 TEGILE_LOCAL_CONTAINER_NAME, 

315 project, 

316 share_name, 

317 TEGILE_SNAPSHOT_PREFIX, 

318 snapshot['name']), 

319 False) 

320 

321 self._api('deleteShareSnapshot', params) 

322 

323 @debugger 

324 def ensure_share(self, context, share, share_server=None): 

325 """Invoked to sure that share is exported.""" 

326 

327 # Fetching share name from server, because some configuration 

328 # options can cause sharename different from the OpenStack share name 

329 pool, project, share_name = self._get_pool_project_share_name(share) 

330 params = [ 

331 '%s/%s/%s/%s' % (pool, 

332 TEGILE_LOCAL_CONTAINER_NAME, 

333 project, 

334 share_name), 

335 ] 

336 ip, real_share_name = self._api('getShareIPAndMountPoint', 

337 params).split() 

338 

339 share_proto = share['share_proto'] 

340 location = self._get_location_path(real_share_name, share_proto, ip) 

341 return [location] 

342 

343 @debugger 

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

345 """Allow access to the share.""" 

346 share_proto = share['share_proto'] 

347 access_type = access['access_type'] 

348 access_level = access['access_level'] 

349 access_to = access['access_to'] 

350 

351 self._check_share_access(share_proto, access_type) 

352 

353 pool, project, share_name = self._get_pool_project_share_name(share) 

354 params = ('%s/%s/%s/%s' % (pool, 

355 TEGILE_LOCAL_CONTAINER_NAME, 

356 project, 

357 share_name), 

358 share_proto, 

359 access_type, 

360 access_to, 

361 access_level) 

362 

363 self._api('shareAllowAccess', params) 

364 

365 @debugger 

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

367 """Deny access to the share.""" 

368 share_proto = share['share_proto'] 

369 access_type = access['access_type'] 

370 access_level = access['access_level'] 

371 access_to = access['access_to'] 

372 

373 self._check_share_access(share_proto, access_type) 

374 

375 pool, project, share_name = self._get_pool_project_share_name(share) 

376 params = ('%s/%s/%s/%s' % (pool, 

377 TEGILE_LOCAL_CONTAINER_NAME, 

378 project, 

379 share_name), 

380 share_proto, 

381 access_type, 

382 access_to, 

383 access_level) 

384 

385 self._api('shareDenyAccess', params) 

386 

387 def _check_share_access(self, share_proto, access_type): 

388 if share_proto == 'CIFS' and access_type != 'user': 

389 reason = ('Only USER access type is allowed for ' 

390 'CIFS shares.') 

391 LOG.warning(reason) 

392 raise exception.InvalidShareAccess(reason=reason) 

393 elif share_proto == 'NFS' and access_type not in ('ip', 'user'): 

394 reason = ('Only IP or USER access types are allowed for ' 

395 'NFS shares.') 

396 LOG.warning(reason) 

397 raise exception.InvalidShareAccess(reason=reason) 

398 elif share_proto not in ('NFS', 'CIFS'): 

399 reason = ('Unsupported protocol \"%s\" specified for ' 

400 'access rule.') % share_proto 

401 raise exception.InvalidShareAccess(reason=reason) 

402 

403 @debugger 

404 def update_access(self, context, share, access_rules, add_rules, 

405 delete_rules, update_rules, share_server=None): 

406 if not (add_rules or delete_rules): 

407 # Recovery mode 

408 pool, project, share_name = ( 

409 self._get_pool_project_share_name(share)) 

410 share_proto = share['share_proto'] 

411 params = ('%s/%s/%s/%s' % (pool, 

412 TEGILE_LOCAL_CONTAINER_NAME, 

413 project, 

414 share_name), 

415 share_proto) 

416 

417 # Clears all current ACLs 

418 # Remove ip and user ACLs if share_proto is NFS 

419 # Remove user ACLs if share_proto is CIFS 

420 self._api('clearAccessRules', params) 

421 

422 # Looping through all rules. 

423 # Will have one API call per rule. 

424 for access in access_rules: 

425 self._allow_access(context, share, access, share_server) 

426 else: 

427 # Adding/Deleting specific rules 

428 for access in delete_rules: 

429 self._deny_access(context, share, access, share_server) 

430 for access in add_rules: 

431 self._allow_access(context, share, access, share_server) 

432 

433 @debugger 

434 def _update_share_stats(self, **kwargs): 

435 """Retrieve stats info.""" 

436 

437 try: 

438 data = self._api(method='getArrayStats', 

439 request_type='get', 

440 fine_logging=False) 

441 # fixing values coming back here as String to float 

442 for pool in data.get('pools', []): 

443 pool['total_capacity_gb'] = float( 

444 pool.get('total_capacity_gb', 0)) 

445 pool['free_capacity_gb'] = float( 

446 pool.get('free_capacity_gb', 0)) 

447 pool['allocated_capacity_gb'] = float( 

448 pool.get('allocated_capacity_gb', 0)) 

449 

450 pool['qos'] = pool.pop('QoS_support', False) 

451 pool['reserved_percentage'] = ( 

452 self.configuration.reserved_share_percentage) 

453 pool['reserved_snapshot_percentage'] = ( 

454 self.configuration.reserved_share_from_snapshot_percentage 

455 or self.configuration.reserved_share_percentage) 

456 pool['reserved_share_extend_percentage'] = ( 

457 self.configuration.reserved_share_extend_percentage 

458 or self.configuration.reserved_share_percentage) 

459 pool['dedupe'] = True 

460 pool['compression'] = True 

461 pool['thin_provisioning'] = True 

462 pool['max_over_subscription_ratio'] = ( 

463 self.configuration.max_over_subscription_ratio) 

464 

465 data['share_backend_name'] = self._backend_name 

466 data['vendor_name'] = VENDOR 

467 data['driver_version'] = VERSION 

468 data['storage_protocol'] = 'NFS_CIFS' 

469 data['snapshot_support'] = True 

470 data['create_share_from_snapshot_support'] = True 

471 data['qos'] = False 

472 

473 super(TegileShareDriver, self)._update_share_stats(data) 

474 except Exception: 

475 msg = _('Unexpected error while trying to get the ' 

476 'usage stats from array.') 

477 LOG.exception(msg) 

478 raise 

479 

480 @debugger 

481 def get_pool(self, share): 

482 """Returns pool name where share resides. 

483 

484 :param share: The share hosted by the driver. 

485 :return: Name of the pool where given share is hosted. 

486 """ 

487 pool = share_utils.extract_host(share['host'], level='pool') 

488 return pool 

489 

490 @debugger 

491 def get_network_allocations_number(self): 

492 """Get number of network interfaces to be created.""" 

493 return 0 

494 

495 @debugger 

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

497 if ip is None: 

498 ip = self._hostname 

499 if share_proto == 'NFS': 

500 location = '%s:%s' % (ip, share_name) 

501 elif share_proto == 'CIFS': 

502 location = r'\\%s\%s' % (ip, share_name) 

503 else: 

504 message = _('Invalid NAS protocol supplied: %s.') % share_proto 

505 raise exception.InvalidInput(message) 

506 

507 export_location = { 

508 'path': location, 

509 'is_admin_only': False, 

510 'metadata': { 

511 'preferred': True, 

512 }, 

513 } 

514 return export_location 

515 

516 @debugger 

517 def _get_pool_project_share_name(self, share): 

518 pool = share_utils.extract_host(share['host'], level='pool') 

519 project = self._default_project 

520 

521 share_name = share['name'] 

522 

523 return pool, project, share_name