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
« 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"""
18import base64
19import math
20from oslo_config import cfg
21from oslo_log import log
22from oslo_utils import units
24from manila import exception
25from manila.i18n import _
26from manila.share import driver
27from manila.share.drivers.zfssa import zfssarest
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]
68cfg.CONF.register_opts(ZFSSA_OPTS)
70LOG = log.getLogger(__name__)
73def factory_zfssa():
74 return zfssarest.ZFSSAApi()
77class ZFSSAShareDriver(driver.ShareDriver):
78 """ZFSSA share driver: Supports NFS and CIFS protocols.
80 Uses ZFSSA RESTful API to create shares and snapshots on backend.
81 API version history:
83 1.0 - Initial version.
84 1.0.1 - Add share shrink/extend feature.
85 1.0.2 - Add share manage/unmanage feature.
86 """
88 VERSION = '1.0.2'
89 PROTOCOL = 'NFS_CIFS'
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
99 required = [
100 'zfssa_host',
101 'zfssa_data_ip',
102 'zfssa_auth_user',
103 'zfssa_auth_password',
104 'zfssa_pool',
105 'zfssa_project'
106 ]
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)
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 }
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')
158 schema = {
159 'property': 'manila_managed',
160 'description': 'Managed by Manila',
161 'type': 'Boolean',
162 }
163 self.zfssa.create_schema(schema)
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)
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
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
203 def create_share(self, context, share, share_server=None):
204 """Create a share and export it based on protocol used.
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']})
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)
221 def delete_share(self, context, share, share_server=None):
222 """Delete a share.
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'])
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'])
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)
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)
268 def delete_snapshot(self, context, snapshot, share_server=None):
269 """Delete a snapshot.
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'])
287 def manage_existing(self, share, driver_options):
288 """Manage an existing ZFSSA share.
290 This feature requires an option 'zfssa_name', which specifies the
291 name of the share as appeared in ZFSSA.
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
308 lcfg = self.configuration
309 input_export_loc = share['export_locations'][0]['path']
310 proto = share['share_proto']
312 self._verify_share_to_manage(name, details)
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)
323 diff_space = int(new_size_byte - size_byte)
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
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)
360 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project,
361 name, manage_args)
362 return {'size': size_gb, 'export_locations': export_loc}
364 def _verify_share_to_manage(self, name, details):
365 lcfg = self.configuration
367 if lcfg.zfssa_manage_policy == 'loose':
368 return
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)
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)
386 def unmanage(self, share):
387 """Removes the specified share from Manila management.
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)
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)
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)
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
427 def ensure_share(self, context, share, share_server=None):
428 self._get_share_details(share['id'])
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
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'])
448 arg = self.create_arg(new_size)
449 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project,
450 share['id'], arg)
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)
458 diff_space = int(new_size - share['size']) * units.Gi
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)
467 arg = self.create_arg(new_size)
468 self.zfssa.modify_share(lcfg.zfssa_pool, lcfg.zfssa_project,
469 share['id'], arg)
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)
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
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)
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
515 super(ZFSSAShareDriver, self)._update_share_stats(data)
517 def get_network_allocations_number(self):
518 """Returns number of network allocations for creating VIFs."""
519 return 0