Coverage for manila/share/drivers/zfssa/zfssashare.py: 79%

241 statements  

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

1# Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); you may 

4# not use this file except in compliance with the License. You may obtain 

5# a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

12# License for the specific language governing permissions and limitations 

13# under the License. 

14""" 

15ZFS Storage Appliance Manila Share Driver 

16""" 

17 

18import base64 

19import math 

20from oslo_config import cfg 

21from oslo_log import log 

22from oslo_utils import units 

23 

24from manila import exception 

25from manila.i18n import _ 

26from manila.share import driver 

27from manila.share.drivers.zfssa import zfssarest 

28 

29 

30ZFSSA_OPTS = [ 

31 cfg.HostAddressOpt('zfssa_host', 

32 help='ZFSSA management IP address.'), 

33 cfg.HostAddressOpt('zfssa_data_ip', 

34 help='IP address for data.'), 

35 cfg.StrOpt('zfssa_auth_user', 

36 help='ZFSSA management authorized username.'), 

37 cfg.StrOpt('zfssa_auth_password', 

38 secret=True, 

39 help='ZFSSA management authorized user\'s password.'), 

40 cfg.StrOpt('zfssa_pool', 

41 help='ZFSSA storage pool name.'), 

42 cfg.StrOpt('zfssa_project', 

43 help='ZFSSA project name.'), 

44 cfg.StrOpt('zfssa_nas_checksum', default='fletcher4', 

45 help='Controls checksum used for data blocks.'), 

46 cfg.StrOpt('zfssa_nas_compression', default='off', 

47 help='Data compression-off, lzjb, gzip-2, gzip, gzip-9.'), 

48 cfg.StrOpt('zfssa_nas_logbias', default='latency', 

49 help='Controls behavior when servicing synchronous writes.'), 

50 cfg.StrOpt('zfssa_nas_mountpoint', default='', 

51 help='Location of project in ZFS/SA.'), 

52 cfg.StrOpt('zfssa_nas_quota_snap', default='true', 

53 help='Controls whether a share quota includes snapshot.'), 

54 cfg.StrOpt('zfssa_nas_rstchown', default='true', 

55 help='Controls whether file ownership can be changed.'), 

56 cfg.StrOpt('zfssa_nas_vscan', default='false', 

57 help='Controls whether the share is scanned for viruses.'), 

58 cfg.StrOpt('zfssa_rest_timeout', 

59 help='REST connection timeout (in seconds).'), 

60 cfg.StrOpt('zfssa_manage_policy', default='loose', 

61 choices=['loose', 'strict'], 

62 help='Driver policy for share manage. A strict policy checks ' 

63 'for a schema named manila_managed, and makes sure its ' 

64 'value is true. A loose policy does not check for the ' 

65 'schema.') 

66] 

67 

68cfg.CONF.register_opts(ZFSSA_OPTS) 

69 

70LOG = log.getLogger(__name__) 

71 

72 

73def factory_zfssa(): 

74 return zfssarest.ZFSSAApi() 

75 

76 

77class ZFSSAShareDriver(driver.ShareDriver): 

78 """ZFSSA share driver: Supports NFS and CIFS protocols. 

79 

80 Uses ZFSSA RESTful API to create shares and snapshots on backend. 

81 API version history: 

82 

83 1.0 - Initial version. 

84 1.0.1 - Add share shrink/extend feature. 

85 1.0.2 - Add share manage/unmanage feature. 

86 """ 

87 

88 VERSION = '1.0.2' 

89 PROTOCOL = 'NFS_CIFS' 

90 

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

92 super(ZFSSAShareDriver, self).__init__(False, *args, **kwargs) 

93 self.configuration.append_config_values(ZFSSA_OPTS) 

94 self.zfssa = None 

95 self._stats = None 

96 self.mountpoint = '/export/' 

97 lcfg = self.configuration 

98 

99 required = [ 

100 'zfssa_host', 

101 'zfssa_data_ip', 

102 'zfssa_auth_user', 

103 'zfssa_auth_password', 

104 'zfssa_pool', 

105 'zfssa_project' 

106 ] 

107 

108 for prop in required: 

109 if not getattr(lcfg, prop, None): 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true

110 exception_msg = _('%s is required in manila.conf') % prop 

111 LOG.error(exception_msg) 

112 raise exception.InvalidParameterValue(exception_msg) 

113 

114 self.default_args = { 

115 'compression': lcfg.zfssa_nas_compression, 

116 'logbias': lcfg.zfssa_nas_logbias, 

117 'checksum': lcfg.zfssa_nas_checksum, 

118 'vscan': lcfg.zfssa_nas_vscan, 

119 'rstchown': lcfg.zfssa_nas_rstchown, 

120 } 

121 self.share_args = { 

122 'sharedav': 'off', 

123 'shareftp': 'off', 

124 'sharesftp': 'off', 

125 'sharetftp': 'off', 

126 'root_permissions': '777', 

127 'sharenfs': 'sec=sys', 

128 'sharesmb': 'off', 

129 'quota_snap': self.configuration.zfssa_nas_quota_snap, 

130 'reservation_snap': self.configuration.zfssa_nas_quota_snap, 

131 'custom:manila_managed': True, 

132 } 

133 

134 def do_setup(self, context): 

135 """Login, create project, no sharing option enabled.""" 

136 lcfg = self.configuration 

137 LOG.debug("Connecting to host: %s.", lcfg.zfssa_host) 

138 self.zfssa = factory_zfssa() 

139 self.zfssa.set_host(lcfg.zfssa_host, timeout=lcfg.zfssa_rest_timeout) 

140 creds = '%s:%s' % (lcfg.zfssa_auth_user, lcfg.zfssa_auth_password) 

141 auth_str = base64.encodebytes(creds.encode("latin-1"))[:-1] 

142 self.zfssa.login(auth_str) 

143 if lcfg.zfssa_nas_mountpoint == '': 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true

144 self.mountpoint += lcfg.zfssa_project 

145 else: 

146 self.mountpoint += lcfg.zfssa_nas_mountpoint 

147 arg = { 

148 'name': lcfg.zfssa_project, 

149 'sharesmb': 'off', 

150 'sharenfs': 'off', 

151 'mountpoint': self.mountpoint, 

152 } 

153 arg.update(self.default_args) 

154 self.zfssa.create_project(lcfg.zfssa_pool, lcfg.zfssa_project, arg) 

155 self.zfssa.enable_service('nfs') 

156 self.zfssa.enable_service('smb') 

157 

158 schema = { 

159 'property': 'manila_managed', 

160 'description': 'Managed by Manila', 

161 'type': 'Boolean', 

162 } 

163 self.zfssa.create_schema(schema) 

164 

165 def check_for_setup_error(self): 

166 """Check for properly configured pool, project.""" 

167 lcfg = self.configuration 

168 LOG.debug("Verifying pool %s.", lcfg.zfssa_pool) 

169 self.zfssa.verify_pool(lcfg.zfssa_pool) 

170 LOG.debug("Verifying project %s.", lcfg.zfssa_project) 

171 self.zfssa.verify_project(lcfg.zfssa_pool, lcfg.zfssa_project) 

172 

173 def _export_location(self, share): 

174 """Export share's location based on protocol used.""" 

175 lcfg = self.configuration 

176 arg = { 

177 'host': lcfg.zfssa_data_ip, 

178 'mountpoint': self.mountpoint, 

179 'name': share['id'], 

180 } 

181 location = '' 

182 proto = share['share_proto'] 

183 if proto == 'NFS': 

184 location = ("%(host)s:%(mountpoint)s/%(name)s" % arg) 

185 elif proto == 'CIFS': 

186 location = ("\\\\%(host)s\\%(name)s" % arg) 

187 else: 

188 exception_msg = _('Protocol %s is not supported.') % proto 

189 LOG.error(exception_msg) 

190 raise exception.InvalidParameterValue(exception_msg) 

191 LOG.debug("Export location: %s.", location) 

192 return location 

193 

194 def create_arg(self, size): 

195 size = units.Gi * int(size) 

196 arg = { 

197 'quota': size, 

198 'reservation': size, 

199 } 

200 arg.update(self.share_args) 

201 return arg 

202 

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

204 """Create a share and export it based on protocol used. 

205 

206 The created share inherits properties from its project. 

207 """ 

208 lcfg = self.configuration 

209 arg = self.create_arg(share['size']) 

210 arg.update(self.default_args) 

211 arg.update({'name': share['id']}) 

212 

213 if share['share_proto'] == 'CIFS': 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true

214 arg.update({'sharesmb': 'on'}) 

215 LOG.debug("ZFSSAShareDriver.create_share: id=%(name)s, size=%(quota)s", 

216 {'name': arg['name'], 

217 'quota': arg['quota']}) 

218 self.zfssa.create_share(lcfg.zfssa_pool, lcfg.zfssa_project, arg) 

219 return self._export_location(share) 

220 

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

222 """Delete a share. 

223 

224 Shares with existing snapshots can't be deleted. 

225 """ 

226 LOG.debug("ZFSSAShareDriver.delete_share: id=%s", share['id']) 

227 lcfg = self.configuration 

228 self.zfssa.delete_share(lcfg.zfssa_pool, 

229 lcfg.zfssa_project, 

230 share['id']) 

231 

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

233 """Creates a snapshot of the snapshot['share_id'].""" 

234 LOG.debug("ZFSSAShareDriver.create_snapshot: " 

235 "id=%(snap)s share=%(share)s", 

236 {'snap': snapshot['id'], 

237 'share': snapshot['share_id']}) 

238 lcfg = self.configuration 

239 self.zfssa.create_snapshot(lcfg.zfssa_pool, 

240 lcfg.zfssa_project, 

241 snapshot['share_id'], 

242 snapshot['id']) 

243 

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

245 share_server=None, parent_share=None): 

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

247 lcfg = self.configuration 

248 LOG.debug("ZFSSAShareDriver.create_share_from_snapshot: clone=%s", 

249 share['id']) 

250 LOG.debug("ZFSSAShareDriver.create_share_from_snapshot: snapshot=%s", 

251 snapshot['id']) 

252 arg = self.create_arg(share['size']) 

253 details = { 

254 'share': share['id'], 

255 'project': lcfg.zfssa_project, 

256 } 

257 arg.update(details) 

258 

259 if share['share_proto'] == 'CIFS': 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 arg.update({'sharesmb': 'on'}) 

261 self.zfssa.clone_snapshot(lcfg.zfssa_pool, 

262 lcfg.zfssa_project, 

263 snapshot, 

264 share, 

265 arg) 

266 return self._export_location(share) 

267 

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

269 """Delete a snapshot. 

270 

271 Snapshots with existing clones cannot be deleted. 

272 """ 

273 LOG.debug("ZFSSAShareDriver.delete_snapshot: id=%s", snapshot['id']) 

274 lcfg = self.configuration 

275 has_clones = self.zfssa.has_clones(lcfg.zfssa_pool, 

276 lcfg.zfssa_project, 

277 snapshot['share_id'], 

278 snapshot['id']) 

279 if has_clones: 

280 LOG.error("snapshot %s: has clones", snapshot['id']) 

281 raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot['id']) 

282 self.zfssa.delete_snapshot(lcfg.zfssa_pool, 

283 lcfg.zfssa_project, 

284 snapshot['share_id'], 

285 snapshot['id']) 

286 

287 def manage_existing(self, share, driver_options): 

288 """Manage an existing ZFSSA share. 

289 

290 This feature requires an option 'zfssa_name', which specifies the 

291 name of the share as appeared in ZFSSA. 

292 

293 The driver automatically retrieves information from the ZFSSA backend 

294 and returns the correct share size and export location. 

295 """ 

296 if 'zfssa_name' not in driver_options: 

297 msg = _('Name of the share in ZFSSA share has to be ' 

298 'specified in option zfssa_name.') 

299 LOG.error(msg) 

300 raise exception.ShareBackendException(msg=msg) 

301 name = driver_options['zfssa_name'] 

302 try: 

303 details = self._get_share_details(name) 

304 except Exception: 

305 LOG.error('Cannot manage share %s', name) 

306 raise 

307 

308 lcfg = self.configuration 

309 input_export_loc = share['export_locations'][0]['path'] 

310 proto = share['share_proto'] 

311 

312 self._verify_share_to_manage(name, details) 

313 

314 # Get and verify share size: 

315 size_byte = details['quota'] 

316 size_gb = int(math.ceil(size_byte / float(units.Gi))) 

317 if size_byte % units.Gi != 0: 

318 # Round up the size: 

319 new_size_byte = size_gb * units.Gi 

320 free_space = self.zfssa.get_project_stats(lcfg.zfssa_pool, 

321 lcfg.zfssa_project) 

322 

323 diff_space = int(new_size_byte - size_byte) 

324 

325 if diff_space > free_space: 

326 msg = (_('Quota and reservation of share %(name)s need to be ' 

327 'rounded up to %(size)d. But there is not enough ' 

328 'space in the backend.') % {'name': name, 

329 'size': size_gb}) 

330 LOG.error(msg) 

331 raise exception.ManageInvalidShare(reason=msg) 

332 size_byte = new_size_byte 

333 

334 # Get and verify share export location, also update share properties. 

335 arg = { 

336 'host': lcfg.zfssa_data_ip, 

337 'mountpoint': input_export_loc, 

338 'name': share['id'], 

339 } 

340 manage_args = self.default_args.copy() 

341 manage_args.update(self.share_args) 

342 # The ZFSSA share name has to be updated, as Manila generates a new 

343 # share id for each share to be managed. 

344 manage_args.update({'name': share['id'], 

345 'quota': size_byte, 

346 'reservation': size_byte}) 

347 if proto == 'NFS': 

348 export_loc = ("%(host)s:%(mountpoint)s/%(name)s" % arg) 

349 manage_args.update({'sharenfs': 'sec=sys', 

350 'sharesmb': 'off'}) 

351 elif proto == 'CIFS': 

352 export_loc = ("\\\\%(host)s\\%(name)s" % arg) 

353 manage_args.update({'sharesmb': 'on', 

354 'sharenfs': 'off'}) 

355 else: 

356 msg = _('Protocol %s is not supported.') % proto 

357 LOG.error(msg) 

358 raise exception.ManageInvalidShare(reason=msg) 

359 

360 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project, 

361 name, manage_args) 

362 return {'size': size_gb, 'export_locations': export_loc} 

363 

364 def _verify_share_to_manage(self, name, details): 

365 lcfg = self.configuration 

366 

367 if lcfg.zfssa_manage_policy == 'loose': 

368 return 

369 

370 if 'custom:manila_managed' not in details: 

371 msg = (_("Unknown if the share: %s to be managed is " 

372 "already being managed by Manila. Aborting manage " 

373 "share. Please add 'manila_managed' custom schema " 

374 "property to the share and set its value to False." 

375 "Alternatively, set Manila config property " 

376 "'zfssa_manage_policy' to 'loose' to remove this " 

377 "restriction.") % name) 

378 LOG.error(msg) 

379 raise exception.ManageInvalidShare(reason=msg) 

380 

381 if details['custom:manila_managed'] is True: 

382 msg = (_("Share %s is already being managed by Manila.") % name) 

383 LOG.error(msg) 

384 raise exception.ManageInvalidShare(reason=msg) 

385 

386 def unmanage(self, share): 

387 """Removes the specified share from Manila management. 

388 

389 This task involves only changing the custom:manila_managed 

390 property to False. Current accesses to the share will be removed in 

391 ZFSSA, as these accesses are removed in Manila. 

392 """ 

393 name = share['id'] 

394 lcfg = self.configuration 

395 managed = 'custom:manila_managed' 

396 details = self._get_share_details(name) 

397 

398 if (managed not in details) or (details[managed] is not True): 

399 msg = (_("Share %s is not being managed by the current Manila " 

400 "instance.") % name) 

401 LOG.error(msg) 

402 raise exception.UnmanageInvalidShare(reason=msg) 

403 

404 arg = {'custom:manila_managed': False} 

405 if share['share_proto'] == 'NFS': 

406 arg.update({'sharenfs': 'off'}) 

407 elif share['share_proto'] == 'CIFS': 407 ↛ 410line 407 didn't jump to line 410 because the condition on line 407 was always true

408 arg.update({'sharesmb': 'off'}) 

409 else: 

410 msg = (_("ZFSSA does not support %s protocol.") % 

411 share['share_proto']) 

412 LOG.error(msg) 

413 raise exception.UnmanageInvalidShare(reason=msg) 

414 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project, name, arg) 

415 

416 def _get_share_details(self, name): 

417 lcfg = self.configuration 

418 details = self.zfssa.get_share(lcfg.zfssa_pool, 

419 lcfg.zfssa_project, 

420 name) 

421 if not details: 

422 msg = (_("Share %s doesn't exist in ZFSSA.") % name) 

423 LOG.error(msg) 

424 raise exception.ShareResourceNotFound(share_id=name) 

425 return details 

426 

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

428 self._get_share_details(share['id']) 

429 

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

431 """Shrink a share to new_size.""" 

432 lcfg = self.configuration 

433 details = self.zfssa.get_share(lcfg.zfssa_pool, 

434 lcfg.zfssa_project, 

435 share['id']) 

436 used_space = details['space_data'] 

437 new_size_byte = int(new_size) * units.Gi 

438 

439 if used_space > new_size_byte: 

440 LOG.error('%(used).1fGB of share %(id)s is already used. ' 

441 'Cannot shrink to %(newsize)dGB.', 

442 {'used': float(used_space) / units.Gi, 

443 'id': share['id'], 

444 'newsize': new_size}) 

445 raise exception.ShareShrinkingPossibleDataLoss( 

446 share_id=share['id']) 

447 

448 arg = self.create_arg(new_size) 

449 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project, 

450 share['id'], arg) 

451 

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

453 """Extend a share to new_size.""" 

454 lcfg = self.configuration 

455 free_space = self.zfssa.get_project_stats(lcfg.zfssa_pool, 

456 lcfg.zfssa_project) 

457 

458 diff_space = int(new_size - share['size']) * units.Gi 

459 

460 if diff_space > free_space: 

461 msg = (_('There is not enough free space in project %s') 

462 % (lcfg.zfssa_project)) 

463 LOG.error(msg) 

464 raise exception.ShareExtendingError(share_id=share['id'], 

465 reason=msg) 

466 

467 arg = self.create_arg(new_size) 

468 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project, 

469 share['id'], arg) 

470 

471 def allow_access(self, context, share, access, share_server=None): 

472 """Allows access to an NFS share for the specified IP.""" 

473 LOG.debug("ZFSSAShareDriver.allow_access: share=%s", share['id']) 

474 lcfg = self.configuration 

475 if share['share_proto'] == 'NFS': 475 ↛ exitline 475 didn't return from function 'allow_access' because the condition on line 475 was always true

476 self.zfssa.allow_access_nfs(lcfg.zfssa_pool, 

477 lcfg.zfssa_project, 

478 share['id'], 

479 access) 

480 

481 def deny_access(self, context, share, access, share_server=None): 

482 """Deny access to an NFS share for the specified IP.""" 

483 LOG.debug("ZFSSAShareDriver.deny_access: share=%s", share['id']) 

484 lcfg = self.configuration 

485 if share['share_proto'] == 'NFS': 485 ↛ 490line 485 didn't jump to line 490 because the condition on line 485 was always true

486 self.zfssa.deny_access_nfs(lcfg.zfssa_pool, 

487 lcfg.zfssa_project, 

488 share['id'], 

489 access) 

490 elif share['share_proto'] == 'CIFS': 

491 return 

492 

493 def _update_share_stats(self): 

494 """Retrieve stats info from a share.""" 

495 backend_name = self.configuration.safe_get('share_backend_name') 

496 data = dict( 

497 share_backend_name=backend_name or self.__class__.__name__, 

498 vendor_name='Oracle', 

499 driver_version=self.VERSION, 

500 storage_protocol=self.PROTOCOL) 

501 

502 lcfg = self.configuration 

503 (avail, used) = self.zfssa.get_pool_stats(lcfg.zfssa_pool) 

504 if avail: 

505 data['free_capacity_gb'] = int(avail) / units.Gi 

506 if used: 

507 total = int(avail) + int(used) 

508 data['total_capacity_gb'] = total / units.Gi 

509 else: 

510 data['total_capacity_gb'] = 0 

511 else: 

512 data['free_capacity_gb'] = 0 

513 data['total_capacity_gb'] = 0 

514 

515 super(ZFSSAShareDriver, self)._update_share_stats(data) 

516 

517 def get_network_allocations_number(self): 

518 """Returns number of network allocations for creating VIFs.""" 

519 return 0