Coverage for manila/share/drivers/zfsonlinux/utils.py: 99%
164 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 2016 Mirantis 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.
16"""
17Module for storing ZFSonLinux driver utility stuff such as:
18 - Common ZFS code
19 - Share helpers
20"""
22# TODO(vponomaryov): add support of SaMBa
24import abc
26from oslo_log import log
28from manila.common import constants
29from manila import exception
30from manila.i18n import _
31from manila.share import driver
32from manila.share.drivers.ganesha import utils as ganesha_utils
33from manila import utils
35LOG = log.getLogger(__name__)
38def zfs_dataset_synchronized(f):
40 def wrapped_func(self, *args, **kwargs):
41 key = "zfs-dataset-%s" % args[0]
43 @utils.synchronized(key, external=True)
44 def source_func(self, *args, **kwargs):
45 return f(self, *args, **kwargs)
47 return source_func(self, *args, **kwargs)
49 return wrapped_func
52def get_remote_shell_executor(
53 ip, port, conn_timeout, login=None, password=None, privatekey=None,
54 max_size=10):
55 return ganesha_utils.SSHExecutor(
56 ip=ip,
57 port=port,
58 conn_timeout=conn_timeout,
59 login=login,
60 password=password,
61 privatekey=privatekey,
62 max_size=max_size,
63 )
66class ExecuteMixin(driver.ExecuteMixin):
68 def init_execute_mixin(self, *args, **kwargs):
69 """Init method for mixin called in the end of driver's __init__()."""
70 super(ExecuteMixin, self).init_execute_mixin(*args, **kwargs)
71 if self.configuration.zfs_use_ssh:
72 self.ssh_executor = get_remote_shell_executor(
73 ip=self.configuration.zfs_service_ip,
74 port=22,
75 conn_timeout=self.configuration.ssh_conn_timeout,
76 login=self.configuration.zfs_ssh_username,
77 password=self.configuration.zfs_ssh_user_password,
78 privatekey=self.configuration.zfs_ssh_private_key_path,
79 max_size=10,
80 )
81 else:
82 self.ssh_executor = None
84 def execute(self, *cmd, **kwargs):
85 """Common interface for running shell commands."""
86 if kwargs.get('executor'):
87 executor = kwargs.get('executor')
88 elif self.ssh_executor:
89 executor = self.ssh_executor
90 else:
91 executor = self._execute
92 kwargs.pop('executor', None)
93 if cmd[0] == 'sudo':
94 kwargs['run_as_root'] = True
95 cmd = cmd[1:]
96 return executor(*cmd, **kwargs)
98 @utils.retry(retry_param=exception.ProcessExecutionError,
99 interval=5, retries=36, backoff_rate=1)
100 def execute_with_retry(self, *cmd, **kwargs):
101 """Retry wrapper over common shell interface."""
102 try:
103 return self.execute(*cmd, **kwargs)
104 except exception.ProcessExecutionError as e:
105 LOG.warning("Failed to run command, got error: %s", e)
106 raise
108 def _get_option(self, resource_name, option_name, pool_level=False,
109 **kwargs):
110 """Returns value of requested zpool or zfs dataset option."""
111 app = 'zpool' if pool_level else 'zfs'
113 out, err = self.execute(
114 'sudo', app, 'get', option_name, resource_name, **kwargs)
116 data = self.parse_zfs_answer(out)
117 option = data[0]['VALUE']
118 msg_payload = {'option': option_name, 'value': option}
119 LOG.debug("ZFS option %(option)s's value is %(value)s.", msg_payload)
120 return option
122 def parse_zfs_answer(self, string):
123 """Returns list of dicts with data returned by ZFS shell commands."""
124 lines = string.split('\n')
125 if len(lines) < 2:
126 return []
127 keys = list(filter(None, lines[0].split(' ')))
128 data = []
129 for line in lines[1:]:
130 values = list(filter(None, line.split(' ')))
131 if not values:
132 continue
133 data.append(dict(zip(keys, values)))
134 return data
136 def get_zpool_option(self, zpool_name, option_name, **kwargs):
137 """Returns value of requested zpool option."""
138 return self._get_option(zpool_name, option_name, True, **kwargs)
140 def get_zfs_option(self, dataset_name, option_name, **kwargs):
141 """Returns value of requested zfs dataset option."""
142 return self._get_option(dataset_name, option_name, False, **kwargs)
144 def zfs(self, *cmd, **kwargs):
145 """ZFS shell commands executor."""
146 return self.execute('sudo', 'zfs', *cmd, **kwargs)
148 def zfs_with_retry(self, *cmd, **kwargs):
149 """ZFS shell commands executor."""
150 return self.execute_with_retry('sudo', 'zfs', *cmd, **kwargs)
153class NASHelperBase(metaclass=abc.ABCMeta):
154 """Base class for share helpers of 'ZFS on Linux' driver."""
156 def __init__(self, configuration):
157 """Init share helper.
159 :param configuration: share driver 'configuration' instance
160 :return: share helper instance.
161 """
162 self.configuration = configuration
163 self.init_execute_mixin() # pylint: disable=no-member
164 self.verify_setup()
166 @abc.abstractmethod
167 def verify_setup(self):
168 """Performs checks for required stuff."""
170 @abc.abstractmethod
171 def create_exports(self, dataset_name, executor):
172 """Creates share exports."""
174 @abc.abstractmethod
175 def get_exports(self, dataset_name, service, executor):
176 """Gets/reads share exports."""
178 @abc.abstractmethod
179 def remove_exports(self, dataset_name, executor):
180 """Removes share exports."""
182 @abc.abstractmethod
183 def update_access(self, dataset_name, access_rules, add_rules,
184 delete_rules, executor):
185 """Update access rules for specified ZFS dataset."""
188class NFSviaZFSHelper(ExecuteMixin, NASHelperBase):
189 """Helper class for handling ZFS datasets as NFS shares.
191 Kernel and Fuse versions of ZFS have different syntax for setting up access
192 rules, and this Helper designed to satisfy both making autodetection.
193 """
195 @property
196 def is_kernel_version(self):
197 """Says whether Kernel version of ZFS is used or not."""
198 if not hasattr(self, '_is_kernel_version'):
199 try:
200 self.execute('modinfo', 'zfs')
201 self._is_kernel_version = True
202 except exception.ProcessExecutionError as e:
203 LOG.info(
204 "Looks like ZFS kernel module is absent. "
205 "Assuming FUSE version is installed. Error: %s", e)
206 self._is_kernel_version = False
207 return self._is_kernel_version
209 def verify_setup(self):
210 """Performs checks for required stuff."""
211 out, err = self.execute('which', 'exportfs')
212 if not out:
213 raise exception.ZFSonLinuxException(
214 msg=_("Utility 'exportfs' is not installed."))
215 try:
216 self.execute('sudo', 'exportfs')
217 except exception.ProcessExecutionError:
218 LOG.exception("Call of 'exportfs' utility returned error.")
219 raise
221 # Init that class instance attribute on start of manila-share service
222 self.is_kernel_version
224 def create_exports(self, dataset_name, executor=None):
225 """Creates NFS share exports for given ZFS dataset."""
226 return self.get_exports(dataset_name, executor=executor)
228 def get_exports(self, dataset_name, executor=None):
229 """Gets/reads NFS share export for given ZFS dataset."""
230 mountpoint = self.get_zfs_option(
231 dataset_name, 'mountpoint', executor=executor)
232 return [
233 {
234 "path": "%(ip)s:%(mp)s" % {"ip": ip, "mp": mountpoint},
235 "metadata": {
236 },
237 "is_admin_only": is_admin_only,
238 } for ip, is_admin_only in (
239 (self.configuration.zfs_share_export_ip, False),
240 (self.configuration.zfs_service_ip, True))
241 ]
243 @zfs_dataset_synchronized
244 def remove_exports(self, dataset_name, executor=None):
245 """Removes NFS share exports for given ZFS dataset."""
246 sharenfs = self.get_zfs_option(
247 dataset_name, 'sharenfs', executor=executor)
248 if sharenfs == 'off':
249 return
250 self.zfs("set", "sharenfs=off", dataset_name, executor=executor)
252 def _get_parsed_access_to(self, access_to):
253 netmask = utils.cidr_to_netmask(access_to)
254 if netmask == '255.255.255.255':
255 return access_to.split('/')[0]
256 return access_to.split('/')[0] + '/' + netmask
258 @zfs_dataset_synchronized
259 def update_access(self, dataset_name, access_rules, add_rules,
260 delete_rules, make_all_ro=False, executor=None):
261 """Update access rules for given ZFS dataset exported as NFS share."""
262 rw_rules = []
263 ro_rules = []
264 for rule in access_rules:
265 if rule['access_type'].lower() != 'ip':
266 msg = _("Only IP access type allowed for NFS protocol.")
267 raise exception.InvalidShareAccess(reason=msg)
268 if (rule['access_level'] == constants.ACCESS_LEVEL_RW and
269 not make_all_ro):
270 rw_rules.append(self._get_parsed_access_to(rule['access_to']))
271 elif (rule['access_level'] in (constants.ACCESS_LEVEL_RW,
272 constants.ACCESS_LEVEL_RO)):
273 ro_rules.append(self._get_parsed_access_to(rule['access_to']))
274 else:
275 msg = _("Unsupported access level provided - "
276 "%s.") % rule['access_level']
277 raise exception.InvalidShareAccess(reason=msg)
279 rules = []
280 if self.is_kernel_version:
281 if rw_rules:
282 rules.append(
283 "rw=%s,no_root_squash" % ":".join(rw_rules))
284 if ro_rules:
285 rules.append("ro=%s,no_root_squash" % ":".join(ro_rules))
286 rules_str = "sharenfs=" + (','.join(rules) or 'off')
287 else:
288 for rule in rw_rules:
289 rules.append("%s:rw,no_root_squash" % rule)
290 for rule in ro_rules:
291 rules.append("%s:ro,no_root_squash" % rule)
292 rules_str = "sharenfs=" + (' '.join(rules) or 'off')
294 out, err = self.zfs(
295 'list', '-r', dataset_name.split('/')[0], executor=executor)
296 data = self.parse_zfs_answer(out)
297 for datum in data:
298 if datum['NAME'] == dataset_name: 298 ↛ 297line 298 didn't jump to line 297 because the condition on line 298 was always true
299 self.zfs("set", rules_str, dataset_name)
300 break
301 else:
302 LOG.warning(
303 "Dataset with '%(name)s' NAME is absent on backend. "
304 "Access rules were not applied.", {'name': dataset_name})
306 # NOTE(vponomaryov): Setting of ZFS share options does not remove rules
307 # that were added and then removed. So, remove them explicitly.
308 if delete_rules and access_rules:
309 mountpoint = self.get_zfs_option(dataset_name, 'mountpoint')
310 for rule in delete_rules:
311 if rule['access_type'].lower() != 'ip':
312 continue
313 access_to = self._get_parsed_access_to(rule['access_to'])
314 export_location = access_to + ':' + mountpoint
315 self.execute(
316 'sudo', 'exportfs', '-u', export_location,
317 executor=executor,
318 )