

from __future__ import absolute_import

import errno
import glob
import logging
import os
import re

from collections import namedtuple
from threading import RLock

from vdsm.config import config
from vdsm.common import supervdsm
from vdsm.common.network.address import hosttail_join
from vdsm.network.netinfo.routes import getRouteDeviceTo
from vdsm.storage import devicemapper
from vdsm.storage import iscsiadm
from vdsm.storage import misc
from vdsm.storage import sysfs


class IscsiPortal(namedtuple("IscsiPortal", "hostname, port")):
    def __str__(self):
        return hosttail_join(self.hostname, str(self.port))

    def is_ipv6(self):
        return ":" in self.hostname


class IscsiTarget(namedtuple("IscsiTarget", "portal, tpgt, iqn")):
    def __str__(self):
        return "%s %s" % (self.address, self.iqn)

    @property
    def address(self):
        return "%s,%s" % (self.portal, self.tpgt)


DEFAULT_TPGT = 1

IscsiSession = namedtuple("IscsiSession", "id, iface, target, credentials")

_iscsiadmTransactionLock = RLock()

log = logging.getLogger('storage.ISCSI')


def getDevIscsiSessionId(dev):
    device = os.path.realpath(os.path.join("/sys", "block", dev, "device"))
    if not os.path.exists(device):
        return None
    session = os.path.realpath(os.path.join(device, "../.."))
    return int(os.path.basename(session)[7:])


def getDevIscsiInfo(dev):

    if devIsiSCSI(dev):
        return getSessionInfo(getDevIscsiSessionId(dev))

    return IscsiSession(0, IscsiInterface(""),
                        IscsiTarget(IscsiPortal("", 0), 0, ""), None)


def getSessionInfo(sessionID):
    return supervdsm.getProxy().readSessionInfo(sessionID)


def getIscsiSessionPath(sessionId):
    return os.path.join("/sys", "class", "iscsi_session",
                        "session%d" % sessionId)


def getIscsiConnectionPath(sessionId):
    return os.path.join("/sys", "class", "iscsi_connection",
                        "connection%d:0" % sessionId)


def getIscsiHostPath(sessionID):
    path = os.path.realpath(getIscsiSessionPath(sessionID))
    if not os.path.exists(path):
        raise OSError(errno.ENOENT, "No such session %r" % sessionID)

    host = path.rsplit(os.sep, 4)[-4]
    if not host.startswith('host'):
        raise RuntimeError("Unexpected session path %r" % path)

    return '/sys/class/iscsi_host/' + host


def readSessionInfo(sessionID):
    iscsi_session = getIscsiSessionPath(sessionID)
    iscsi_connection = getIscsiConnectionPath(sessionID)

    if not os.path.isdir(iscsi_session) or not os.path.isdir(iscsi_connection):
        raise OSError(errno.ENOENT, "No such session")

    iqn = sysfs.read(os.path.join(iscsi_session, "targetname"), default="")
    iface = sysfs.read(os.path.join(iscsi_session, "ifacename"), default="")
    tpgt = sysfs.read_int(os.path.join(iscsi_session, "tpgt"))
    username = sysfs.read(os.path.join(iscsi_session, "username"), default="")
    password = sysfs.read_password(os.path.join(iscsi_session, "password"),
                                   default="")
    ip = sysfs.read(os.path.join(iscsi_connection, "persistent_address"),
                    default="")
    port = sysfs.read_int(os.path.join(iscsi_connection, "persistent_port"))

    iscsi_host = getIscsiHostPath(sessionID)
    netdev = sysfs.read(os.path.join(iscsi_host, "netdev"), default="")
    if netdev in ["<NULL>", "(null)"]:
        netdev = None

    iface = IscsiInterface(iface, netIfaceName=netdev)
    portal = IscsiPortal(ip, port)
    target = IscsiTarget(portal, tpgt, iqn)


    if username in ["<NULL>", "(null)", ""]:
        username = None
    if password.value in ["<NULL>", "(null)", ""]:
        password = None
    cred = None
    if username or password:
        cred = ChapCredentials(username, password)

    return IscsiSession(sessionID, iface, target, cred)


def addIscsiNode(iface, target, credentials=None):
    with _iscsiadmTransactionLock:
        iscsiadm.node_new(iface.name, target.address, target.iqn)
        try:
            if credentials is not None:
                for key, value in credentials.getIscsiadmOptions():
                    key = "node.session." + key
                    iscsiadm.node_update(iface.name, target.address,
                                         target.iqn, key, value,
                                         hideValue=True)

            setRpFilterIfNeeded(iface.netIfaceName, target.portal.hostname,
                                True)

            iscsiadm.node_login(iface.name, target.address, target.iqn)

            iscsiadm.node_update(iface.name, target.address, target.iqn,
                                 "node.startup", "manual")
        except:
            removeIscsiNode(iface, target)
            raise


def removeIscsiNode(iface, target):
    with _iscsiadmTransactionLock:
        try:
            iscsiadm.node_disconnect(iface.name, target.address, target.iqn)
        except iscsiadm.IscsiSessionNotFound:
            pass

        iscsiadm.node_delete(iface.name, target.address, target.iqn)
        setRpFilterIfNeeded(iface.netIfaceName, target.portal.hostname, False)


def addIscsiPortal(iface, portal, credentials=None):
    discoverType = "sendtargets"

    with _iscsiadmTransactionLock:
        iscsiadm.discoverydb_new(discoverType, iface.name, str(portal))

        try:
            if credentials is not None:
                for key, value in credentials.getIscsiadmOptions():
                    key = "discovery.sendtargets." + key
                    iscsiadm.discoverydb_update(discoverType, iface.name,
                                                str(portal), key, value,
                                                hideValue=True)

        except:
            deleteIscsiPortal(iface, portal)
            raise


def deleteIscsiPortal(iface, portal):
    discoverType = "sendtargets"
    iscsiadm.discoverydb_delete(discoverType, iface.name, str(portal))


def discoverSendTargets(iface, portal, credentials=None):
    discoverType = "sendtargets"

    with _iscsiadmTransactionLock:
        addIscsiPortal(iface, portal, credentials)
        try:
            targets = iscsiadm.discoverydb_discover(discoverType, iface.name,
                                                    str(portal))
        finally:
            deleteIscsiPortal(iface, portal)

        res = []
        for ip, port, tpgt, iqn in targets:
            res.append(IscsiTarget(IscsiPortal(ip, port), tpgt, iqn))

        return res


def iterateIscsiSessions():
    for sessionDir in glob.iglob("/sys/class/iscsi_session/session*"):
        sessionID = int(os.path.basename(sessionDir)[len("session"):])
        try:
            yield getSessionInfo(sessionID)
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise

            continue


class ChapCredentials(object):
    def __init__(self, username=None, password=None):
        self.username = username
        self.password = password

    def getIscsiadmOptions(self):
        res = [("auth.authmethod", "CHAP")]
        if self.username is not None:
            res.append(("auth.username", self.username))
        if self.password is not None:
            res.append(("auth.password", self.password.value))

        return res

    def __eq__(self, other):
        return (self.__class__ == other.__class__ and
                self.username == other.username and
                self.password == other.password)

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash((self.__class__, self.username, hash(self.password)))


class IscsiInterface(object):

    _fields = {
        "name": ('iface.iscsi_ifacename', 'rw'),
        'transport': ("iface.transport_name", 'r'),
        'hardwareAddress': ("iface.hwaddress", 'rw'),
        'ipAddress': ('iface.ipaddress', 'rw'),
        'initiatorName': ('iface.initiatorname', 'rw'),
        'netIfaceName': ('iface.net_ifacename', 'rw')
    }

    def __getattr__(self, name):
        if name not in self._fields:
            raise AttributeError(name)

        key, mode = self._fields[name]

        if "r" not in mode:
            raise AttributeError(name)

        value = self._conf[key]
        if value is None and not self._loaded:
            self._loaded = True
            self._load()
            return getattr(self, name)

        if value == "<empty>":
            return None

        return value

    def __setattr__(self, name, value):
        if name in ("_conf", "_fields", "_loaded"):
            return object.__setattr__(self, name, value)

        if name not in self._fields:
            raise AttributeError(name)

        key, mode = self._fields[name]
        if "w" not in mode:
            raise AttributeError(name)

        self._conf[key] = value

    def __init__(self, name, hardwareAddress=None, ipAddress=None,
                 initiatorName=None, netIfaceName=None):

        self._conf = {'iface.transport_name': 'tcp'}
        self._conf['iface.net_ifacename'] = netIfaceName

        self.name = name
        self._loaded = False

        if hardwareAddress:
            self.hardwareAddress = hardwareAddress

        if ipAddress:
            self.ipAddress = ipAddress

        if initiatorName:
            self.initiatorName = initiatorName

    @staticmethod
    def fromConf(conf):
        tmp = IscsiInterface("tmp")
        tmp._conf = conf
        return tmp

    def create(self):

        iscsiadm.iface_new(self.name)
        try:
            self.update()
        except:
            self.delete()
            raise

    def update(self):
        for key, value in self._conf.iteritems():
            if value is None or key == 'iface.iscsi_ifacename':
                continue

            iscsiadm.iface_update(self.name, key, value)

    def delete(self):
        iscsiadm.iface_delete(self.name)

    def _load(self):
        conf = iscsiadm.iface_info(self.name)
        conf.update(self._conf)
        self._conf = conf

    def __repr__(self):
        return "<IscsiInterface name='%s' transport='%s' netIfaceName='%s'>" \
            % (self.name, self.transport, self.netIfaceName)


def iterateIscsiInterfaces():
    for iface in iscsiadm.iface_list():
        yield IscsiInterface(iface.ifacename, netIfaceName=iface.net_ifacename)


@misc.samplingmethod
def rescan():
    timeout = config.getint('irs', 'scsi_rescan_maximal_timeout')
    log.debug("Performing SCSI scan, this will take up to %s seconds", timeout)
    rescanOp = iscsiadm.session_rescan_async()
    rescanOp.wait(timeout=timeout)


def devIsiSCSI(dev):
    hostdir = os.path.realpath(os.path.join("/sys/block", dev,
                                            "device/../../.."))
    host = os.path.basename(hostdir)
    iscsi_host = os.path.join(hostdir, "iscsi_host/", host)
    scsi_host = os.path.join(hostdir, "scsi_host/", host)
    proc_name = os.path.join(scsi_host, "proc_name")

    if not os.path.exists(iscsi_host) or not os.path.exists(proc_name):
        return False

    session_id = getDevIscsiSessionId(dev)

    if session_id is None:
        return False

    iscsi_connection = getIscsiConnectionPath(session_id)
    pers_addr = os.path.join(iscsi_connection, "persistent_address")
    pers_port = os.path.join(iscsi_connection, "persistent_port")

    return os.path.exists(pers_addr) and os.path.exists(pers_port)


def getiScsiTarget(dev):
    device = os.path.realpath(os.path.join("/sys/block", dev, "device"))
    sessiondir = os.path.realpath(os.path.join(device, "../.."))
    session = os.path.basename(sessiondir)
    iscsi_session = os.path.join(sessiondir,
                                 "iscsi_session/" + session)
    with open(os.path.join(iscsi_session, "targetname")) as f:
        return f.readline().strip()


def getiScsiSession(dev):
    device = os.path.realpath(os.path.join("/sys/block", dev, "device"))
    sessiondir = os.path.realpath(os.path.join(device, "../.."))
    session = os.path.basename(sessiondir)
    return int(session[len('session'):])


def getDefaultInitiatorName():
    with open("/etc/iscsi/initiatorname.iscsi", "r") as f:
        return f.read().strip().split("=", 1)[1]


def findUnderlyingStorage(devPath):
    os.stat(devPath)
    sessions = []
    try:
        devs = devicemapper.getSlaves(os.path.basename(devPath))
    except:
        devs = [os.path.basename(devPath)]

    for realDev in devs:
        if not devicemapper.isVirtualDevice(realDev):
            sessions.append(getiScsiSession(realDev))
            continue

        for slave in devicemapper.getSlaves(realDev):
            sessions.extend(findUnderlyingStorage(os.path.join("/dev", slave)))

    return sessions


RE_SCSI_SESSION = re.compile(r"^[Ss]ession(\d+)$")


def disconnectFromUndelyingStorage(devPath):
    storageList = findUnderlyingStorage(devPath)
    res = []
    for target in storageList:
        m = RE_SCSI_SESSION.match(target)
        if not m:
            res.append(None)
            continue

        sessionID = m.groups()[0]
        res.append(disconnectiScsiSession(sessionID))

    return res


def disconnectiScsiSession(sessionID):
    sessionID = int(sessionID)
    sessionInfo = getSessionInfo(sessionID)
    try:
        iscsiadm.session_logout(sessionID)
    except iscsiadm.IscsiError as e:
        return e[0]

    netIfaceName = sessionInfo.iface.netIfaceName
    hostname = sessionInfo.target.portal.hostname
    setRpFilterIfNeeded(netIfaceName, hostname, False)

    return 0


def _sessionsUsingNetiface(netIfaceName):
    for session in iterateIscsiSessions():
        if session.iface.netIfaceName == netIfaceName:
            yield session


def setRpFilterIfNeeded(netIfaceName, hostname, loose_mode):
    if netIfaceName is None:
        log.debug("iface.net_ifacename not provided, skipping rp filter setup")
        return

    sessions = _sessionsUsingNetiface(netIfaceName)

    if not any(sessions) and netIfaceName != getRouteDeviceTo(hostname):
        if loose_mode:
            log.info("Setting loose mode rp_filter for device %r." %
                     netIfaceName)
            supervdsm.getProxy().set_rp_filter_loose(netIfaceName)
        else:
            log.info("Setting strict mode rp_filter for device %r." %
                     netIfaceName)
            supervdsm.getProxy().set_rp_filter_strict(netIfaceName)
