a73b299374
Python2 is no longer supported, so in this patch set we remove the usage of the six (py2 and py3 compatibility library) in favor of py3 syntax. Change-Id: I3ddfad568a1b578bee23a6d1a96de9551e336bb4
319 lines
12 KiB
Python
319 lines
12 KiB
Python
# Copyright 2016 Mirantis Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Module for storing ZFSonLinux driver utility stuff such as:
|
|
- Common ZFS code
|
|
- Share helpers
|
|
"""
|
|
|
|
# TODO(vponomaryov): add support of SaMBa
|
|
|
|
import abc
|
|
|
|
from oslo_log import log
|
|
|
|
from manila.common import constants
|
|
from manila import exception
|
|
from manila.i18n import _
|
|
from manila.share import driver
|
|
from manila.share.drivers.ganesha import utils as ganesha_utils
|
|
from manila import utils
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
def zfs_dataset_synchronized(f):
|
|
|
|
def wrapped_func(self, *args, **kwargs):
|
|
key = "zfs-dataset-%s" % args[0]
|
|
|
|
@utils.synchronized(key, external=True)
|
|
def source_func(self, *args, **kwargs):
|
|
return f(self, *args, **kwargs)
|
|
|
|
return source_func(self, *args, **kwargs)
|
|
|
|
return wrapped_func
|
|
|
|
|
|
def get_remote_shell_executor(
|
|
ip, port, conn_timeout, login=None, password=None, privatekey=None,
|
|
max_size=10):
|
|
return ganesha_utils.SSHExecutor(
|
|
ip=ip,
|
|
port=port,
|
|
conn_timeout=conn_timeout,
|
|
login=login,
|
|
password=password,
|
|
privatekey=privatekey,
|
|
max_size=max_size,
|
|
)
|
|
|
|
|
|
class ExecuteMixin(driver.ExecuteMixin):
|
|
|
|
def init_execute_mixin(self, *args, **kwargs):
|
|
"""Init method for mixin called in the end of driver's __init__()."""
|
|
super(ExecuteMixin, self).init_execute_mixin(*args, **kwargs)
|
|
if self.configuration.zfs_use_ssh:
|
|
self.ssh_executor = get_remote_shell_executor(
|
|
ip=self.configuration.zfs_service_ip,
|
|
port=22,
|
|
conn_timeout=self.configuration.ssh_conn_timeout,
|
|
login=self.configuration.zfs_ssh_username,
|
|
password=self.configuration.zfs_ssh_user_password,
|
|
privatekey=self.configuration.zfs_ssh_private_key_path,
|
|
max_size=10,
|
|
)
|
|
else:
|
|
self.ssh_executor = None
|
|
|
|
def execute(self, *cmd, **kwargs):
|
|
"""Common interface for running shell commands."""
|
|
if kwargs.get('executor'):
|
|
executor = kwargs.get('executor')
|
|
elif self.ssh_executor:
|
|
executor = self.ssh_executor
|
|
else:
|
|
executor = self._execute
|
|
kwargs.pop('executor', None)
|
|
if cmd[0] == 'sudo':
|
|
kwargs['run_as_root'] = True
|
|
cmd = cmd[1:]
|
|
return executor(*cmd, **kwargs)
|
|
|
|
@utils.retry(retry_param=exception.ProcessExecutionError,
|
|
interval=5, retries=36, backoff_rate=1)
|
|
def execute_with_retry(self, *cmd, **kwargs):
|
|
"""Retry wrapper over common shell interface."""
|
|
try:
|
|
return self.execute(*cmd, **kwargs)
|
|
except exception.ProcessExecutionError as e:
|
|
LOG.warning("Failed to run command, got error: %s", e)
|
|
raise
|
|
|
|
def _get_option(self, resource_name, option_name, pool_level=False,
|
|
**kwargs):
|
|
"""Returns value of requested zpool or zfs dataset option."""
|
|
app = 'zpool' if pool_level else 'zfs'
|
|
|
|
out, err = self.execute(
|
|
'sudo', app, 'get', option_name, resource_name, **kwargs)
|
|
|
|
data = self.parse_zfs_answer(out)
|
|
option = data[0]['VALUE']
|
|
msg_payload = {'option': option_name, 'value': option}
|
|
LOG.debug("ZFS option %(option)s's value is %(value)s.", msg_payload)
|
|
return option
|
|
|
|
def parse_zfs_answer(self, string):
|
|
"""Returns list of dicts with data returned by ZFS shell commands."""
|
|
lines = string.split('\n')
|
|
if len(lines) < 2:
|
|
return []
|
|
keys = list(filter(None, lines[0].split(' ')))
|
|
data = []
|
|
for line in lines[1:]:
|
|
values = list(filter(None, line.split(' ')))
|
|
if not values:
|
|
continue
|
|
data.append(dict(zip(keys, values)))
|
|
return data
|
|
|
|
def get_zpool_option(self, zpool_name, option_name, **kwargs):
|
|
"""Returns value of requested zpool option."""
|
|
return self._get_option(zpool_name, option_name, True, **kwargs)
|
|
|
|
def get_zfs_option(self, dataset_name, option_name, **kwargs):
|
|
"""Returns value of requested zfs dataset option."""
|
|
return self._get_option(dataset_name, option_name, False, **kwargs)
|
|
|
|
def zfs(self, *cmd, **kwargs):
|
|
"""ZFS shell commands executor."""
|
|
return self.execute('sudo', 'zfs', *cmd, **kwargs)
|
|
|
|
def zfs_with_retry(self, *cmd, **kwargs):
|
|
"""ZFS shell commands executor."""
|
|
return self.execute_with_retry('sudo', 'zfs', *cmd, **kwargs)
|
|
|
|
|
|
class NASHelperBase(metaclass=abc.ABCMeta):
|
|
"""Base class for share helpers of 'ZFS on Linux' driver."""
|
|
|
|
def __init__(self, configuration):
|
|
"""Init share helper.
|
|
|
|
:param configuration: share driver 'configuration' instance
|
|
:return: share helper instance.
|
|
"""
|
|
self.configuration = configuration
|
|
self.init_execute_mixin() # pylint: disable=no-member
|
|
self.verify_setup()
|
|
|
|
@abc.abstractmethod
|
|
def verify_setup(self):
|
|
"""Performs checks for required stuff."""
|
|
|
|
@abc.abstractmethod
|
|
def create_exports(self, dataset_name, executor):
|
|
"""Creates share exports."""
|
|
|
|
@abc.abstractmethod
|
|
def get_exports(self, dataset_name, service, executor):
|
|
"""Gets/reads share exports."""
|
|
|
|
@abc.abstractmethod
|
|
def remove_exports(self, dataset_name, executor):
|
|
"""Removes share exports."""
|
|
|
|
@abc.abstractmethod
|
|
def update_access(self, dataset_name, access_rules, add_rules,
|
|
delete_rules, executor):
|
|
"""Update access rules for specified ZFS dataset."""
|
|
|
|
|
|
class NFSviaZFSHelper(ExecuteMixin, NASHelperBase):
|
|
"""Helper class for handling ZFS datasets as NFS shares.
|
|
|
|
Kernel and Fuse versions of ZFS have different syntax for setting up access
|
|
rules, and this Helper designed to satisfy both making autodetection.
|
|
"""
|
|
|
|
@property
|
|
def is_kernel_version(self):
|
|
"""Says whether Kernel version of ZFS is used or not."""
|
|
if not hasattr(self, '_is_kernel_version'):
|
|
try:
|
|
self.execute('modinfo', 'zfs')
|
|
self._is_kernel_version = True
|
|
except exception.ProcessExecutionError as e:
|
|
LOG.info(
|
|
"Looks like ZFS kernel module is absent. "
|
|
"Assuming FUSE version is installed. Error: %s", e)
|
|
self._is_kernel_version = False
|
|
return self._is_kernel_version
|
|
|
|
def verify_setup(self):
|
|
"""Performs checks for required stuff."""
|
|
out, err = self.execute('which', 'exportfs')
|
|
if not out:
|
|
raise exception.ZFSonLinuxException(
|
|
msg=_("Utility 'exportfs' is not installed."))
|
|
try:
|
|
self.execute('sudo', 'exportfs')
|
|
except exception.ProcessExecutionError:
|
|
LOG.exception("Call of 'exportfs' utility returned error.")
|
|
raise
|
|
|
|
# Init that class instance attribute on start of manila-share service
|
|
self.is_kernel_version
|
|
|
|
def create_exports(self, dataset_name, executor=None):
|
|
"""Creates NFS share exports for given ZFS dataset."""
|
|
return self.get_exports(dataset_name, executor=executor)
|
|
|
|
def get_exports(self, dataset_name, executor=None):
|
|
"""Gets/reads NFS share export for given ZFS dataset."""
|
|
mountpoint = self.get_zfs_option(
|
|
dataset_name, 'mountpoint', executor=executor)
|
|
return [
|
|
{
|
|
"path": "%(ip)s:%(mp)s" % {"ip": ip, "mp": mountpoint},
|
|
"metadata": {
|
|
},
|
|
"is_admin_only": is_admin_only,
|
|
} for ip, is_admin_only in (
|
|
(self.configuration.zfs_share_export_ip, False),
|
|
(self.configuration.zfs_service_ip, True))
|
|
]
|
|
|
|
@zfs_dataset_synchronized
|
|
def remove_exports(self, dataset_name, executor=None):
|
|
"""Removes NFS share exports for given ZFS dataset."""
|
|
sharenfs = self.get_zfs_option(
|
|
dataset_name, 'sharenfs', executor=executor)
|
|
if sharenfs == 'off':
|
|
return
|
|
self.zfs("set", "sharenfs=off", dataset_name, executor=executor)
|
|
|
|
def _get_parsed_access_to(self, access_to):
|
|
netmask = utils.cidr_to_netmask(access_to)
|
|
if netmask == '255.255.255.255':
|
|
return access_to.split('/')[0]
|
|
return access_to.split('/')[0] + '/' + netmask
|
|
|
|
@zfs_dataset_synchronized
|
|
def update_access(self, dataset_name, access_rules, add_rules,
|
|
delete_rules, make_all_ro=False, executor=None):
|
|
"""Update access rules for given ZFS dataset exported as NFS share."""
|
|
rw_rules = []
|
|
ro_rules = []
|
|
for rule in access_rules:
|
|
if rule['access_type'].lower() != 'ip':
|
|
msg = _("Only IP access type allowed for NFS protocol.")
|
|
raise exception.InvalidShareAccess(reason=msg)
|
|
if (rule['access_level'] == constants.ACCESS_LEVEL_RW and
|
|
not make_all_ro):
|
|
rw_rules.append(self._get_parsed_access_to(rule['access_to']))
|
|
elif (rule['access_level'] in (constants.ACCESS_LEVEL_RW,
|
|
constants.ACCESS_LEVEL_RO)):
|
|
ro_rules.append(self._get_parsed_access_to(rule['access_to']))
|
|
else:
|
|
msg = _("Unsupported access level provided - "
|
|
"%s.") % rule['access_level']
|
|
raise exception.InvalidShareAccess(reason=msg)
|
|
|
|
rules = []
|
|
if self.is_kernel_version:
|
|
if rw_rules:
|
|
rules.append(
|
|
"rw=%s,no_root_squash" % ":".join(rw_rules))
|
|
if ro_rules:
|
|
rules.append("ro=%s,no_root_squash" % ":".join(ro_rules))
|
|
rules_str = "sharenfs=" + (','.join(rules) or 'off')
|
|
else:
|
|
for rule in rw_rules:
|
|
rules.append("%s:rw,no_root_squash" % rule)
|
|
for rule in ro_rules:
|
|
rules.append("%s:ro,no_root_squash" % rule)
|
|
rules_str = "sharenfs=" + (' '.join(rules) or 'off')
|
|
|
|
out, err = self.zfs(
|
|
'list', '-r', dataset_name.split('/')[0], executor=executor)
|
|
data = self.parse_zfs_answer(out)
|
|
for datum in data:
|
|
if datum['NAME'] == dataset_name:
|
|
self.zfs("set", rules_str, dataset_name)
|
|
break
|
|
else:
|
|
LOG.warning(
|
|
"Dataset with '%(name)s' NAME is absent on backend. "
|
|
"Access rules were not applied.", {'name': dataset_name})
|
|
|
|
# NOTE(vponomaryov): Setting of ZFS share options does not remove rules
|
|
# that were added and then removed. So, remove them explicitly.
|
|
if delete_rules and access_rules:
|
|
mountpoint = self.get_zfs_option(dataset_name, 'mountpoint')
|
|
for rule in delete_rules:
|
|
if rule['access_type'].lower() != 'ip':
|
|
continue
|
|
access_to = self._get_parsed_access_to(rule['access_to'])
|
|
export_location = access_to + ':' + mountpoint
|
|
self.execute(
|
|
'sudo', 'exportfs', '-u', export_location,
|
|
executor=executor,
|
|
)
|