text: # Copyright 2019 Inspur Corp.
  # 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.
  """
  Driver for Inspur InStorage
  """
  
  import ipaddress
  import itertools
  import six
  
  from oslo_config import cfg
  from oslo_log import log
  from oslo_utils import units
  
  from manila import coordination
  from manila import exception
  from manila.i18n import _
  from manila.share import driver
  from manila.share import utils as share_utils
  
  from manila.share.drivers.inspur.instorage.cli_helper import InStorageSSH
  from manila.share.drivers.inspur.instorage.cli_helper import SSHRunner
  
  instorage_opts = [
      cfg.HostAddressOpt(
          'instorage_nas_ip',
          required=True,
          help='IP address for the InStorage.'
      ),
      cfg.PortOpt(
          'instorage_nas_port',
          default=22,
          help='Port number for the InStorage.'
      ),
      cfg.StrOpt(
          'instorage_nas_login',
          required=True,
          help='Username for the InStorage.'
      ),
      cfg.StrOpt(
          'instorage_nas_password',
          required=True,
          secret=True,
          help='Password for the InStorage.'
      ),
      cfg.ListOpt(
          'instorage_nas_pools',
          required=True,
          help='The Storage Pools Manila should use, a comma separated list.'
      )
  ]
  
  CONF = cfg.CONF
  CONF.register_opts(instorage_opts)
  LOG = log.getLogger(__name__)
  
  
  class InStorageShareDriver(driver.ShareDriver):
      """Inspur InStorage NAS driver. Allows for NFS and CIFS NAS.
  
      .. code::none
          Version history:
              1.0.0 - Initial driver.
                      Driver support:
                          share create/delete
                          extend size
                          update_access
                          protocol: NFS/CIFS
      """
  
      VENDOR = 'INSPUR'
      VERSION = '1.0.0'
      PROTOCOL = 'NFS_CIFS'
  
      def __init__(self, *args, **kwargs):
          super(InStorageShareDriver, self).__init__(False, *args, **kwargs)
          self.configuration.append_config_values(instorage_opts)
  
          self.backend_name = self.configuration.safe_get('share_backend_name')
          self.backend_pools = self.configuration.instorage_nas_pools
  
          self.ssh_runner = SSHRunner(**{
              'host': self.configuration.instorage_nas_ip,
              'port': 22,
              'login': self.configuration.instorage_nas_login,
              'password': self.configuration.instorage_nas_password
          })
  
          self.assistant = InStorageAssistant(self.ssh_runner)
  
      def check_for_setup_error(self):
          nodes = self.assistant.get_nodes_info()
          if len(nodes) == 0:
              msg = _('No valid node, be sure the NAS Port IP is configured')
              raise exception.ShareBackendException(msg=msg)
  
          pools = self.assistant.get_available_pools()
          not_exist = set(self.backend_pools).difference(set(pools))
          if not_exist:
              msg = _('Pool %s not exist on the storage system') % not_exist
              raise exception.InvalidParameterValue(msg)
  
      def _update_share_stats(self, **kwargs):
          """Retrieve share stats information."""
  
          try:
              stats = {
                  'share_backend_name': self.backend_name,
                  'vendor_name': self.VENDOR,
                  'driver_version': self.VERSION,
                  'storage_protocol': 'NFS_CIFS',
                  'reserved_percentage':
                      self.configuration.reserved_share_percentage,
                  'max_over_subscription_ratio':
                      self.configuration.max_over_subscription_ratio,
                  'snapshot_support': False,
                  'create_share_from_snapshot_support': False,
                  'revert_to_snapshot_support': False,
                  'qos': False,
                  'total_capacity_gb': 0.0,
                  'free_capacity_gb': 0.0,
                  'pools': []
              }
  
              pools = self.assistant.get_pools_attr(self.backend_pools)
              total_capacity_gb = 0
              free_capacity_gb = 0
              for pool in pools.values():
                  total_capacity_gb += pool['total_capacity_gb']
                  free_capacity_gb += pool['free_capacity_gb']
                  stats['pools'].append(pool)
  
              stats['total_capacity_gb'] = total_capacity_gb
              stats['free_capacity_gb'] = free_capacity_gb
  
              LOG.debug('share status %s', stats)
  
              super(InStorageShareDriver, self)._update_share_stats(stats)
          except Exception:
              msg = _('Unexpected error while trying to get the '
                      'usage stats from array.')
              LOG.exception(msg)
              raise
  
      @staticmethod
      def generate_share_name(share):
          # Generate a name with id of the share as base, and do follows:
          # 1. Remove the '-' in the id string.
          # 2. Transform all alpha to lower case.
          # 3. If the first char of the id is a num,
          #    transform it to an Upper case alpha start from 'A',
          #    such as '0' -> 'A', '1' -> 'B'.
          # e.g.
          # generate_share_name({
          #  'id': '46CF5E85-D618-4023-8727-6A1EA9292954',
          #  ...
          # })
          # returns 'E6cf5e85d618402387276a1ea9292954'
  
          name = share['id'].replace('-', '').lower()
          if name[0] in '0123456789':
              name = chr(ord('A') + ord(name[0]) - ord('0')) + name[1:]
          return name
  
      def get_network_allocations_number(self):
          """Get the number of network interfaces to be created."""
  
          return 0
  
      def create_share(self, context, share, share_server=None):
          """Create a new share instance."""
          share_name = self.generate_share_name(share)
          share_size = share['size']
          share_proto = share['share_proto']
  
          pool_name = share_utils.extract_host(share['host'], level='pool')
  
          self.assistant.create_share(
              share_name,
              pool_name,
              share_size,
              share_proto
          )
  
          return self.assistant.get_export_locations(share_name, share_proto)
  
      def delete_share(self, context, share, share_server=None):
          """Delete the given share instance."""
          share_name = self.generate_share_name(share)
          share_proto = share['share_proto']
  
          self.assistant.delete_share(share_name, share_proto)
  
      def extend_share(self, share, new_size, share_server=None):
          """Extend the share instance's size to new size."""
          share_name = self.generate_share_name(share)
  
          self.assistant.extend_share(share_name, new_size)
  
      def ensure_share(self, context, share, share_server=None):
          """Ensure that the share instance is exported."""
          share_name = self.generate_share_name(share)
          share_proto = share['share_proto']
  
          return self.assistant.get_export_locations(share_name, share_proto)
  
      def update_access(self, context, share, access_rules, add_rules,
                        delete_rules, share_server=None):
          """Update the share instance's access rule."""
          share_name = self.generate_share_name(share)
          share_proto = share['share_proto']
  
          @coordination.synchronized('inspur-instorage-access-' + share_name)
          def _update_access(name, proto, rules, add_rules, delete_rules):
              self.assistant.update_access(
                  name, proto, rules, add_rules, delete_rules
              )
  
          _update_access(
              share_name, share_proto, access_rules, add_rules, delete_rules
          )
  
  
  class InStorageAssistant(object):
  
      NFS_CLIENT_SPEC_PATTERN = (
          '%(ip)s/%(mask)s:%(rights)s:%(all_squash)s:%(root_squash)s'
      )
  
      CIFS_CLIENT_RIGHT_PATTERN = (
          '%(type)s:%(name)s:%(rights)s'
      )
  
      def __init__(self, ssh_runner):
          self.ssh = InStorageSSH(ssh_runner)
  
      @staticmethod
      def handle_keyerror(cmd, out):
          msg = (_('Could not find key in output of command %(cmd)s: %(out)s.')
                 % {'out': out, 'cmd': cmd})
          raise exception.ShareBackendException(msg=msg)
  
      def size_to_gb(self, size):
          new_size = 0
  
          if 'P' in size:
              new_size = int(float(size.rstrip('PB')) * units.Mi)
          elif 'T' in size:
              new_size = int(float(size.rstrip('TB')) * units.Ki)
          elif 'G' in size:
              new_size = int(float(size.rstrip('GB')) * 1)
          elif 'M' in size:
              mb_size = float(size.rstrip('MB'))
              new_size = int((mb_size + units.Ki - 1) / units.Ki)
  
          return new_size
  
      def get_available_pools(self):
          nas_pools = self.ssh.lsnaspool()
          return [pool['pool_name'] for pool in nas_pools]
  
      def get_pools_attr(self, backend_pools):
          pools = {}
          fs_attr = self.ssh.lsfs()
          nas_pools = self.ssh.lsnaspool()
          for pool_attr in nas_pools:
              pool_name = pool_attr['pool_name']
              if pool_name not in backend_pools:
                  continue
  
              total_used_capacity = 0
              total_allocated_capacity = 0
              for fs in fs_attr:
                  if fs['pool_name'] != pool_name:
                      continue
                  allocated = self.size_to_gb(fs['total_capacity'])
                  used = self.size_to_gb(fs['used_capacity'])
  
                  total_allocated_capacity += allocated
                  total_used_capacity += used
  
              available = self.size_to_gb(pool_attr['available_capacity'])
  
              pool = {
                  'pool_name': pool_name,
                  'total_capacity_gb': total_allocated_capacity + available,
                  'free_capacity_gb': available,
                  'allocated_capacity_gb': total_allocated_capacity,
                  'reserved_percentage': 0,
                  'qos': False,
                  'dedupe': False,
                  'compression': False,
                  'thin_provisioning': False,
                  'max_over_subscription_ratio': 0
              }
  
              pools[pool_name] = pool
  
          return pools
  
      def get_nodes_info(self):
          """Return a dictionary containing information of system's nodes."""
          nodes = {}
          resp = self.ssh.lsnasportip()
          for port in resp:
              try:
                  # Port is invalid if it has no IP configured.
                  if port['ip'] == '':
                      continue
  
                  node_name = port['node_name']
                  if node_name not in nodes:
                      nodes[node_name] = {}
  
                  node = nodes[node_name]
                  node[port['id']] = port
              except KeyError:
                  self.handle_keyerror('lsnasportip', port)
  
          return nodes
  
      @staticmethod
      def get_fsname_by_name(name):
          return ('%(fsname)s' % {'fsname': name})[0:32]
  
      @staticmethod
      def get_dirname_by_name(name):
          return ('%(dirname)s' % {'dirname': name})[0:32]
  
      def get_dirpath_by_name(self, name):
          fsname = self.get_fsname_by_name(name)
          dirname = self.get_dirname_by_name(name)
  
          return '/fs/%(fsname)s/%(dirname)s' % {
              'fsname': fsname, 'dirname': dirname
          }
  
      def create_share(self, name, pool, size, proto):
          """Create a share with given info."""
  
          # use one available node as the primary node
          nodes = self.get_nodes_info()
          if len(nodes) == 0:
              msg = _('No valid node, be sure the NAS Port IP is configured')
              raise exception.ShareBackendException(msg=msg)
  
          node_name = [key for key in nodes.keys()][0]
  
          # first create the file system on which share will be created
          fsname = self.get_fsname_by_name(name)
          self.ssh.addfs(fsname, pool, size, node_name)
  
          # then create the directory used for the share
          dirpath = self.get_dirpath_by_name(name)
          self.ssh.addnasdir(dirpath)
  
          # For CIFS, we need to create a CIFS share.
          # For NAS, the share is automatically added when the first
          # 'access spec' is added on it.
          if proto == 'CIFS':
              self.ssh.addcifs(name, dirpath)
  
      def check_share_exist(self, name):
          """Check whether the specified share exist on backend."""
  
          fsname = self.get_fsname_by_name(name)
          for fs in self.ssh.lsfs():
              if fs['fs_name'] == fsname:
                  return True
          return False
  
      def delete_share(self, name, proto):
          """Delete the given share."""
  
          if not self.check_share_exist(name):
              LOG.warning('Share %s does not exist on the backend.', name)
              return
  
          # For CIFS, we have to delete the share first.
          # For NAS, when the last client access spec is removed from
          # it, the share is automatically deleted.
          if proto == 'CIFS':
              self.ssh.rmcifs(name)
  
          # then delete the directory
          dirpath = self.get_dirpath_by_name(name)
          self.ssh.rmnasdir(dirpath)
  
          # at last delete the file system
          fsname = self.get_fsname_by_name(name)
          self.ssh.rmfs(fsname)
  
      def extend_share(self, name, new_size):
          """Extend a given share to a new size.
  
          :param name: the name of the share.
          :param new_size: the new size the share should be.
          :return:
          """
          # first get the original capacity
          old_size = None
          fsname = self.get_fsname_by_name(name)
          for fs in self.ssh.lsfs():
              if fs['fs_name'] == fsname:
                  old_size = self.size_to_gb(fs['total_capacity'])
                  break
  
          if old_size is None:
              msg = _('share %s is not available') % name
              raise exception.ShareBackendException(msg=msg)
  
          LOG.debug('Extend fs %s from %dGB to %dGB', fsname, old_size, new_size)
          self.ssh.expandfs(fsname, new_size - old_size)
  
      def get_export_locations(self, name, share_proto):
          """Get the export locations of a given share.
  
          :param name: the name of the share.
          :param share_proto: the protocol of the share.
          :return: a list of export locations.
          """
  
          if share_proto == 'NFS':
              dirpath = self.get_dirpath_by_name(name)
              pattern = '%(ip)s:' + dirpath
          elif share_proto == 'CIFS':
              pattern = '\\\\%(ip)s\\' + name
          else:
              msg = _('share protocol %s is not supported') % share_proto
              raise exception.ShareBackendException(msg=msg)
  
          # we need get the node so that we know which port ip we can use
          node_name = None
          fsname = self.get_fsname_by_name(name)
          for node in self.ssh.lsnode():
              for fs in self.ssh.lsfs(node['name']):
                  if fs['fs_name'] == fsname:
                      node_name = node['name']
                      break
              if node_name:
                  break
  
          if node_name is None:
              msg = _('share %s is not available') % name
              raise exception.ShareBackendException(msg=msg)
  
          locations = []
          ports = self.ssh.lsnasportip()
          for port in ports:
              if port['node_name'] == node_name and port['ip'] != '':
                  location = pattern % {'ip': port['ip']}
  
                  locations.append({
                      'path': location,
                      'is_admin_only': False,
                      'metadata': {}
                  })
  
          return locations
  
      def classify_nfs_client_spec(self, client_spec, dirpath):
          nfslist = self.ssh.lsnfslist(dirpath)
          if len(nfslist):
              nfsinfo = self.ssh.lsnfsinfo(dirpath)
              spec_set = set([
                  self.NFS_CLIENT_SPEC_PATTERN % i for i in nfsinfo
              ])
          else:
              spec_set = set()
  
          client_spec_set = set(client_spec)
  
          del_spec = spec_set.difference(client_spec_set)
          add_spec = client_spec_set.difference(spec_set)
  
          return list(add_spec), list(del_spec)
  
      def access_rule_to_client_spec(self, access_rule):
          if access_rule['access_type'] != 'ip':
              msg = _('only ip access type is supported when using NFS protocol')
              raise exception.ShareBackendException(msg=msg)
  
          network = ipaddress.ip_network(six.text_type(access_rule['access_to']))
          if network.version != 4:
              msg = _('only IPV4 is accepted when using NFS protocol')
              raise exception.ShareBackendException(msg=msg)
  
          client_spec = self.NFS_CLIENT_SPEC_PATTERN % {
              'ip': six.text_type(network.network_address),
              'mask': six.text_type(network.netmask),
              'rights': access_rule['access_level'],
              'all_squash': 'all_squash',
              'root_squash': 'root_squash'
          }
  
          return client_spec
  
      def update_nfs_access(self, share_name, access_rules, add_rules,
                            delete_rules):
          """Update a NFS share's access rule."""
  
          dirpath = self.get_dirpath_by_name(share_name)
          if add_rules or delete_rules:
              add_spec = [
                  self.access_rule_to_client_spec(r) for r in add_rules
              ]
              del_spec = [
                  self.access_rule_to_client_spec(r) for r in delete_rules
              ]
  
              _, can_del_spec = self.classify_nfs_client_spec(
                  [], dirpath
              )
              to_del_set = set(del_spec)
              can_del_set = set(can_del_spec)
              will_del_set = to_del_set.intersection(can_del_set)
              del_spec = list(will_del_set)
          else:
              access_spec = [
                  self.access_rule_to_client_spec(r) for r in access_rules
              ]
  
              add_spec, del_spec = self.classify_nfs_client_spec(
                  access_spec, dirpath
              )
  
          for spec in del_spec:
              self.ssh.rmnfsclient(dirpath, spec)
          for spec in add_spec:
              self.ssh.addnfsclient(dirpath, spec)
  
      def classify_cifs_rights(self, access_rights, share_name):
          cifsinfo = self.ssh.lscifsinfo(share_name)
          rights_set = set([
              self.CIFS_CLIENT_RIGHT_PATTERN % i for i in cifsinfo
          ])
          access_rights_set = set(access_rights)
  
          del_rights = rights_set.difference(access_rights_set)
          add_rights = access_rights_set.difference(rights_set)
  
          return list(add_rights), list(del_rights)
  
      def access_rule_to_rights(self, access_rule):
          if access_rule['access_type'] != 'user':
              msg = _('only user access type is supported'
                      ' when using CIFS protocol')
              raise exception.ShareBackendException(msg=msg)
  
          rights = self.CIFS_CLIENT_RIGHT_PATTERN % {
              'type': 'LU',
              'name': access_rule['access_to'],
              'rights': access_rule['access_level']
          }
  
          return rights
  
      def update_cifs_access(self, share_name, access_rules, add_rules,
                             delete_rules):
          """Update a CIFS share's access rule."""
  
          if add_rules or delete_rules:
              add_rights = [
                  self.access_rule_to_rights(r) for r in add_rules
              ]
              del_rights = [
                  self.access_rule_to_rights(r) for r in delete_rules
              ]
          else:
              access_rights = [
                  self.access_rule_to_rights(r) for r in access_rules
              ]
  
              add_rights, del_rights = self.classify_cifs_rights(
                  access_rights, share_name
              )
  
          for rights in del_rights:
              self.ssh.rmcifsuser(share_name, rights)
          for rights in add_rights:
              self.ssh.addcifsuser(share_name, rights)
  
      @staticmethod
      def check_access_type(access_type, *rules):
          rule_chain = itertools.chain(*rules)
          if all([r['access_type'] == access_type for r in rule_chain]):
              return True
          else:
              return False
  
      def update_access(self, share_name, share_proto,
                        access_rules, add_rules, delete_rules):
          if share_proto == 'CIFS':
              if self.check_access_type('user', access_rules,
                                        add_rules, delete_rules):
                  self.update_cifs_access(share_name, access_rules,
                                          add_rules, delete_rules)
              else:
                  msg = _("Only %s access type allowed.") % "user"
                  raise exception.InvalidShareAccess(reason=msg)
          elif share_proto == 'NFS':
              if self.check_access_type('ip', access_rules,
                                        add_rules, delete_rules):
                  self.update_nfs_access(share_name, access_rules,
                                         add_rules, delete_rules)
              else:
                  msg = _("Only %s access type allowed.") % "ip"
                  raise exception.InvalidShareAccess(reason=msg)
          else:
              msg = _('share protocol %s is not supported') % share_proto
              raise exception.ShareBackendException(msg=msg)
  