'''
This file is part of RTSLib.
Copyright (c) 2011-2013 by Datera, Inc
Copyright (c) 2011-2014 by Red Hat, Inc.

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.


Description
-----------

Fabrics may differ in how fabric WWNs are represented, as well as
what capabilities they support


Available parameters
--------------------

* features
Lists the target fabric available features. Default value:
("discovery_auth", "acls", "auth", "nps")
example: features = ("discovery_auth", "acls", "auth")
example: features = () # no features supported

Detail of features:

  * tpgts
  The target fabric module is using iSCSI-style target portal group tags.

  * discovery_auth
  The target fabric module supports a fabric-wide authentication for
  discovery.

  * acls
  The target's TPGTs support explicit initiator ACLs.

  * auth
  The target's TPGT's support per-TPG authentication, and
  the target's TPGT's ACLs support per-ACL initiator authentication.
  Fabrics that support auth must support acls.

  * nps
  The TPGTs support iSCSI-like IPv4/IPv6 network portals, using IP:PORT
  group names.

  * nexus
  The TPGTs have a 'nexus' attribute that contains the local initiator
  serial unit. This attribute must be set before being able to create any
  LUNs.

  * wwn_types
  Sets the type of WWN expected by the target fabric. Defaults to 'free'.
  Usually a fabric will only support one type but iSCSI supports more.
  First entry is the "native" wwn type - i.e. if a wwn can be generated, it
  will be of this type.
  Example: wwn_types = ("eui",)
  Current valid types are:

    * free
    Freeform WWN.

    * iqn
    The fabric module targets are using iSCSI-type IQNs.

    * naa
    NAA FC or SAS address type WWN.

    * eui
    EUI-64. See http://en.wikipedia.org/wiki/MAC_address for info on this format.

    * unit_serial
    Disk-type unit serial.

* wwns
This property returns an iterable (either generator or list) of valid
target WWNs for the fabric, if WWNs should be chosen from existing
fabric interfaces. The most common case for this is hardware-set
WWNs. WWNs should conform to rtslib's normalized internal format: the wwn
type (see above), a period, then the wwn with interstitial dividers like
':' removed.

* to_fabric_wwn()
Converts WWNs from normalized format (see above) to whatever the kernel code
expects when getting a wwn. Only needed if different from normalized format.

* kernel_module
Sets the name of the kernel module implementing the fabric modules. If
not specified, it will be assumed to be MODNAME_target_mod, where
MODNAME is the name of the fabric module, from the fabrics list. Note
that you must not specify any .ko or such extension here.
Example: self.kernel_module = "my_module"

* _path
Sets the path of the configfs group used by the fabric module. Defaults to the
name of the module from the fabrics list.
Example: self._path = "%s/%s" % (self.configfs_dir, "my_cfs_dir")

'''

from functools import partial
from glob import iglob as glob
import os
import six

from .node import CFSNode
from .utils import fread, fwrite, normalize_wwn, colonize
from .utils import RTSLibError, modprobe, ignored
from .target import Target
from .utils import _get_auth_attr, _set_auth_attr

version_attributes = set(["lio_version", "version"])
discovery_auth_attributes = set(["discovery_auth"])
target_names_excludes = version_attributes | discovery_auth_attributes


class _BaseFabricModule(CFSNode):
    '''
    Abstract Base clase for Fabric Modules.
    It can load modules, provide information about them and
    handle the configfs housekeeping. After instantiation, whether or
    not the fabric module is loaded depends on if a method requiring
    it (i.e. accessing configfs) is used. This helps limit loaded
    kernel modules to just the fabrics in use.
    '''

    # FabricModule ABC private stuff
    def __init__(self, name):
        '''
        Instantiate a FabricModule object, according to the provided name.
        @param name: the name of the FabricModule object.
        @type name: str
        '''
        super(_BaseFabricModule, self).__init__()
        self.name = name
        self.spec_file = "N/A"
        self._path = "%s/%s" % (self.configfs_dir, self.name)
        self.features = ('discovery_auth', 'acls', 'auth', 'nps', 'tpgts')
        self.wwn_types = ('free',)
        self.kernel_module = "%s_target_mod" % self.name

    # FabricModule public stuff

    def _check_self(self):
        if not self.exists:
            try:
                self._create_in_cfs_ine('any')
            except RTSLibError:
                modprobe(self.kernel_module)
                self._create_in_cfs_ine('any')
        super(_BaseFabricModule, self)._check_self()

    def has_feature(self, feature):
        # Handle a renamed feature
        if feature == 'acls_auth':
            feature = 'auth'
        return feature in self.features

    def _list_targets(self):
        if self.exists:
            for wwn in os.listdir(self.path):
                if os.path.isdir("%s/%s" % (self.path, wwn)) and \
                        wwn not in target_names_excludes:
                    yield Target(self, self.from_fabric_wwn(wwn), 'lookup')

    def _get_version(self):
        if self.exists:
            for attr in version_attributes:
                path = "%s/%s" % (self.path, attr)
                if os.path.isfile(path):
                    return fread(path)
            else:
                raise RTSLibError("Can't find version for fabric module %s"
                                  % self.name)
        else:
            return None

    def to_normalized_wwn(self, wwn):
        '''
        Checks whether or not the provided WWN is valid for this fabric module
        according to the spec, and returns a tuple of our preferred string 
        representation of the wwn, and what type it turned out to be.
        '''
        return normalize_wwn(self.wwn_types, wwn)

    def to_fabric_wwn(self, wwn):
        '''
        Some fabrics need WWNs in a format different than rtslib's internal
        format. These fabrics should override this method.
        '''
        return wwn

    def from_fabric_wwn(self, wwn):
        '''
        Converts from WWN format used in this fabric's LIO configfs to canonical
        format.
        Note: Do not call from wwns(). There's no guarantee fabric wwn format is
        the same as wherever wwns() is reading from.
        '''
        return wwn

    def needs_wwn(self):
        '''
        This fabric requires wwn to be specified when creating a target,
        it cannot be autogenerated.
        '''
        return self.wwns != None

    def _assert_feature(self, feature):
        if not self.has_feature(feature):
            raise RTSLibError("Fabric module %s does not implement "
                              + "the %s feature" % (self.name, feature))

    def clear_discovery_auth_settings(self):
        self._check_self()
        self._assert_feature('discovery_auth')
        self.discovery_mutual_password = ''
        self.discovery_mutual_userid = ''
        self.discovery_password = ''
        self.discovery_userid = ''
        self.discovery_enable_auth = False

    def _get_discovery_enable_auth(self):
        self._check_self()
        self._assert_feature('discovery_auth')
        path = "%s/discovery_auth/enforce_discovery_auth" % self.path
        value = fread(path)
        return bool(int(value))

    def _set_discovery_enable_auth(self, enable):
        self._check_self()
        self._assert_feature('discovery_auth')
        path = "%s/discovery_auth/enforce_discovery_auth" % self.path
        if int(enable):
            enable = 1
        else:
            enable = 0
        fwrite(path, "%s" % enable)

    def _get_discovery_authenticate_target(self):
        self._check_self()
        self._assert_feature('discovery_auth')
        path = "%s/discovery_auth/authenticate_target" % self.path
        return bool(int(fread(path)))

    def _get_wwns(self):
        '''
        Returns either iterable or None. None means fabric allows
        arbitrary WWNs.
        '''
        return None

    def _get_disc_attr(self, *args, **kwargs):
        self._assert_feature('discovery_auth')
        return _get_auth_attr(self, *args, **kwargs)

    def _set_disc_attr(self, *args, **kwargs):
        self._assert_feature('discovery_auth')
        _set_auth_attr(self, *args, **kwargs)

    discovery_enable_auth = \
            property(_get_discovery_enable_auth,
                     _set_discovery_enable_auth,
                     doc="Set or get the discovery enable_auth flag.")
    discovery_authenticate_target = property(_get_discovery_authenticate_target,
            doc="Get the boolean discovery authenticate target flag.")

    discovery_userid = property(partial(_get_disc_attr, attribute='discovery_auth/userid'),
                                partial(_set_disc_attr, attribute='discovery_auth/userid'),
                                doc="Set or get the initiator discovery userid.")
    discovery_password = property(partial(_get_disc_attr, attribute='discovery_auth/password'),
                                  partial(_set_disc_attr, attribute='discovery_auth/password'),
                                  doc="Set or get the initiator discovery password.")
    discovery_mutual_userid = property(partial(_get_disc_attr, attribute='discovery_auth/userid_mutual'),
                                       partial(_set_disc_attr, attribute='discovery_auth/userid_mutual'),
                                       doc="Set or get the mutual discovery userid.")
    discovery_mutual_password = property(partial(_get_disc_attr, attribute='discovery_auth/password_mutual'),
                                         partial(_set_disc_attr, attribute='discovery_auth/password_mutual'),
                                         doc="Set or get the mutual discovery password.")

    targets = property(_list_targets,
                       doc="Get the list of target objects.")

    version = property(_get_version,
                       doc="Get the fabric module version string.")

    wwns = property(_get_wwns,
                    doc="iterable of WWNs present for this fabric")

    def setup(self, fm, err_func):
        '''
        Setup fabricmodule with settings from fm dict.
        '''
        for name, value in six.iteritems(fm):
            if name != 'name':
                try:
                    setattr(self, name, value)
                except:
                    err_func("Could not set fabric %s attribute '%s'" % (fm['name'], name))

    def dump(self):
        d = super(_BaseFabricModule, self).dump()
        d['name'] = self.name
        if self.has_feature("discovery_auth"):
            for attr in ("userid", "password", "mutual_userid", "mutual_password"):
                val = getattr(self, "discovery_" + attr, None)
                if val:
                    d["discovery_" + attr] = val
            d['discovery_enable_auth'] = self.discovery_enable_auth
        return d


class ISCSIFabricModule(_BaseFabricModule):

    def __init__(self):
        super(ISCSIFabricModule, self).__init__('iscsi')
        self.wwn_types = ('iqn', 'naa', 'eui')


class LoopbackFabricModule(_BaseFabricModule):
    def __init__(self):
        super(LoopbackFabricModule, self).__init__('loopback')
        self.features = ("nexus",)
        self.wwn_types = ('naa',)
        self.kernel_module = "tcm_loop"


class SBPFabricModule(_BaseFabricModule):
    def __init__(self):
        super(SBPFabricModule, self).__init__('sbp')
        self.features = ()
        self.wwn_types = ('eui',)
        self.kernel_module = "sbp_target"

    def to_fabric_wwn(self, wwn):
        return wwn[4:]

    def from_fabric_wwn(self, wwn):
        return "eui." + wwn

    # return 1st local guid (is unique) so our share is named uniquely
    @property
    def wwns(self):
        for fname in glob("/sys/bus/firewire/devices/fw*/is_local"):
            if bool(int(fread(fname))):
                guid_path = os.path.dirname(fname) + "/guid"
                yield "eui." + fread(guid_path)[2:]
                break


class Qla2xxxFabricModule(_BaseFabricModule):
    def __init__(self):
        super(Qla2xxxFabricModule, self).__init__('qla2xxx')
        self.features = ("acls",)
        self.wwn_types = ('naa',)
        self.kernel_module = "tcm_qla2xxx"

    def to_fabric_wwn(self, wwn):
        # strip 'naa.' and add colons
        return colonize(wwn[4:])

    def from_fabric_wwn(self, wwn):
        return "naa." + wwn.replace(":", "")

    @property
    def wwns(self):
        for wwn_file in glob("/sys/class/fc_host/host*/port_name"):
            with ignored(IOError):
                if not fread(os.path.dirname(wwn_file)+"/symbolic_name").startswith("fcoe"):
                    yield "naa." + fread(wwn_file)[2:]


class SRPTFabricModule(_BaseFabricModule):
    def __init__(self):
        super(SRPTFabricModule, self).__init__('srpt')
        self.features = ("acls",)
        self.wwn_types = ('ib',)
        self.kernel_module = "ib_srpt"

    def to_fabric_wwn(self, wwn):
        # strip 'ib.' and re-add '0x'
        return "0x" + wwn[3:]

    def from_fabric_wwn(self, wwn):
        return "ib." + wwn[2:]

    @property
    def wwns(self):
        for wwn_file in glob("/sys/class/infiniband/*/ports/*/gids/0"):
            yield "ib." + fread(wwn_file).replace(":", "")


class FCoEFabricModule(_BaseFabricModule):
    def __init__(self):
        super(FCoEFabricModule, self).__init__('tcm_fc')

        self.features = ("acls",)
        self.kernel_module = "tcm_fc"
        self.wwn_types=('naa',)
        self._path = "%s/%s" % (self.configfs_dir, "fc")

    def to_fabric_wwn(self, wwn):
        # strip 'naa.' and add colons
        return colonize(wwn[4:])

    def from_fabric_wwn(self, wwn):
        return "naa." + wwn.replace(":", "")

    @property
    def wwns(self):
        for wwn_file in glob("/sys/class/fc_host/host*/port_name"):
            with ignored(IOError):
                if fread(os.path.dirname(wwn_file)+"/symbolic_name").startswith("fcoe"):
                    yield "naa." + fread(wwn_file)[2:]


class USBGadgetFabricModule(_BaseFabricModule):
    def __init__(self):
        super(USBGadgetFabricModule, self).__init__('usb_gadget')
        self.features = ("nexus",)
        self.wwn_types = ('naa',)
        self.kernel_module = "tcm_usb_gadget"


class VhostFabricModule(_BaseFabricModule):
    def __init__(self):
        super(VhostFabricModule, self).__init__('vhost')
        self.features = ("nexus", "acls", "tpgts")
        self.wwn_types = ('naa',)
        self.kernel_module = "tcm_vhost"

class XenPvScsiFabricModule(_BaseFabricModule):
    def __init__(self):
        super(XenPvScsiFabricModule, self).__init__('xen-pvscsi')
        self._path = "%s/%s" % (self.configfs_dir, 'xen-pvscsi')
        self.features = ("nexus", "tpgts")
        self.wwn_types = ('naa',)
        self.kernel_module = "xen-scsiback"


class IbmvscsisFabricModule(_BaseFabricModule):
    def __init__(self):
        super(IbmvscsisFabricModule, self).__init__('ibmvscsis')
        self.features = ()
        self.kernel_module = "ibmvscsis"

    @property
    def wwns(self):
        for wwn_file in glob("/sys/module/ibmvscsis/drivers/vio:ibmvscsis/*/devspec"):
            name = fread(wwn_file)
            yield name[name.find("@") + 1:]


fabric_modules = {
    "srpt": SRPTFabricModule,
    "iscsi": ISCSIFabricModule,
    "loopback": LoopbackFabricModule,
    "qla2xxx": Qla2xxxFabricModule,
    "sbp": SBPFabricModule,
    "tcm_fc": FCoEFabricModule,
#    "usb_gadget": USBGadgetFabricModule, # very rare, don't show
    "vhost": VhostFabricModule,
    "xen-pvscsi": XenPvScsiFabricModule,
    "ibmvscsis": IbmvscsisFabricModule,
    }

#
# Maintain compatibility with existing FabricModule(fabricname) usage
# e.g. FabricModule('iscsi') returns an ISCSIFabricModule
#
class FabricModule(object):

    def __new__(cls, name):
        return fabric_modules[name]()

    @classmethod
    def all(cls):
        for mod in six.itervalues(fabric_modules):
            yield mod()

    @classmethod
    def list_registered_drivers(cls):
        return os.listdir('/sys/module/target_core_mod/holders')
