text: # 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
  import six
  
  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)
  
  
  @six.add_metaclass(abc.ABCMeta)
  class NASHelperBase(object):
      """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,
                  )
  