
from client import StorageConnectorClient, Helper
import os, time, socket, threading, sys

class fcClientLVM(StorageConnectorClient):
    """
    Implementation of the Fiber Channel Storage Connector client for HANA.

    It requires multipathing to be set up with Linux dm-multipath and the sg3_utils package to be installed,
    due to the usage of the sg_persist tool.

    Note: sufficient rights must be granted without the need for typing the password, this can be achieved by:

         echo "<sid>adm ALL=NOPASSWD: /sbin/multipath, /sbin/multipathd, /etc/init.d/multipathd, /usr/bin/sg_persist, /bin/mount, /bin/umount, /bin/kill, /usr/bin/lsof

    Note: if using the --storage_cfg parameter during installation (or during addhost with already enabled Storage Connector API, the installer
    will take care of setting /etc/sudoers correctly)
    """

    apiVersion = 2

    # prout-type to fence the devices
    prType = 6  # exclusive access, registrants only

    # the persistent reservation key used by all nodes of a system
    prKey = None
    
    interval = 1
    retries = 20


    def __init__(self, *args, **kwargs):
        # delegate construction to base class
        super(fcClientLVM, self).__init__(*args, **kwargs)

    def about(self):
        return {"provider_company" :        "SAP",
                "provider_name" :           "fcClientLVM",
                "provider_description" :    "Generic Fiber Channel Storage Connector. For any Fiber Channel storages that fulfill the usual Linux kernel interfaces as well as support the SCSI-3 standard.",
                "provider_version" :        "1.0"}
    
    @staticmethod
    def sudoers():
        """Gives information about the necessary sudo permission to be set."""
        return """ALL=NOPASSWD: /sbin/multipath, /sbin/multipathd, /etc/init.d/multipathd, /usr/bin/sg_persist, /bin/mount, /bin/umount, /bin/kill, /usr/bin/lsof, /sbin/vgchange, /sbin/vgscan, /usr/bin/systemctl, /usr/sbin/lsof, /usr/sbin/xfs_repair"""

    def getVolumeGroup(self, lvmname):
        if lvmname.count("-") != 1:
            msg = "exactly one dash is expected to separate volume group and logical volume: '%s'" % lvmname
            print msg
            self.tracer.fatal(msg)
            raise Exception(msg)

        return lvmname.rsplit("-", 1)[0]

    # request the storage server to activate a LUN for this host
    def attach(self, storages):
        """Attaches storages on this host."""
        self.tracer.info("%s.attach method called" % self.__class__.__name__)

        # a unique key for the host
        self.prKey = self._generateUniqueHostKey(socket.gethostname())

        self.hookAttachBegin(globals(), locals())

        threads = []
        threadLock = threading.Lock()
        firstDev = True

        self._cfg.reload()
        (self.prType, self.prTypeReservationText) = self.getPRType(defaultPrType = self.prType)

        # loop over storages given by HDB
        for storage in storages:
            self.hookAttachBeginDevice(globals(), locals())
            attachMode = "trying to" if storage.get("testOnly") is False else "testing"
            self.tracer.info("%s attach for partition %s, usage type %s on path %s" % (attachMode, storage.get("partition"), storage.get("usage_type"), storage.get("path")))

            # retrieve all parameters for this storage (unique key => (partition, usageType) )
            if not self._cfg.hasParameter(storage.get("partition"), storage.get("usage_type"), "lvmname"):
                msg = "no storage with key (partiton, usageType) = (%s, %s) configured" % (storage.get("partition"), storage.get("usage_type"))
                print msg
                self.tracer.fatal(msg)
                raise Exception(msg)

            if firstDev: # some migration stuff when switching from prType 6 to 5:
                (code, output) = Helper._runOsCommand("sudo /sbin/multipath -r", self.tracer)
                if code != 0 or "reject" in output.lower():
                    self._reloadMultipathDaemon()
                firstDev = False

            thread = AttachLVMThread(self,threadLock,storage.get("partition"),storage.get("usage_type"),storage.get("path"),storage.get("testOnly"))
            thread.start()

            if not self._cfg.hasParameter("*","*","sequentialattach"):
                threads.append(thread) # save thread for mutual joining
            else:
                self.tracer.info("sequential attach is set in global.ini")
                thread.join() # attach to a usage_type immediately as a fallback

        for t in threads:
            t.join()

        self.flushMultipath(self.prType)

        self.hookAttachEnd(globals(), locals())

        return 0

    def detach(self, storages):
        self.hookDetachBegin(globals(), locals())

        self.tracer.info("%s.detach method called" % self.__class__.__name__)

        # loop over storages given by HDB
        for storage in storages:

            self.hookDetachBeginDevice(globals(), locals())

            connectionData = self._getConnectionDataForLun(storage.get("partition"), storage.get("usage_type"))

            # set variables for better code readability
            try:
                lvmname = connectionData["lvmname"]
            except:
                raise Exception("lvmname not set in global.ini")

            (c ,o) = Helper._runOsCommand("sudo /sbin/vgscan", self.tracer)
            if c != 0:
                msg = "vgscan failed for device: %s" % o
                print msg
                self.tracer.fatal(msg)
                raise Exception(msg)

            time.sleep(1)

            vg = self.getVolumeGroup(lvmname)
            (c ,o) = Helper._runOsCommand("sudo /sbin/vgchange -ay %s" % vg, self.tracer)
            if c != 0:
                msg = "vgchange failed for volume group '%s': %s" % (vg, o)
                print msg
                self.tracer.fatal(msg)
                raise Exception(msg)

            ############

            devices = {}

            device = "/dev/mapper/%s" % lvmname
            found = False
            for i in range(1, 4):
                if os.path.exists(device):
                    found = True
                    break;
                time.sleep(1)

            if not found:
                raise Exception("LVM device '%s' not found" % device)
            
            linkIsCreated = True
            dmdev = os.path.realpath(device).split("/")[-1]
            if not dmdev.startswith("dm-"):
                linkIsCreated = False
                for i in range(1, 11):  # poll up to 10 seconds, because /dev/disk/by-id is updated ASYNC only by the kernel!
                    (c, o) = Helper._run2PipedOsCommand("ls -ls /dev/disk/by-id", "grep -w %s" % lvmname, self.tracer)
                    dmdev = o.split("/")[-1]
                    time.sleep(1)
                    if dmdev.startswith("dm-"):
                        linkIsCreated = True
                        break;

            if not linkIsCreated:
                msg = "unable to resolve '%s' to a 'dm-X' name" % lvmname
                print msg
                self.tracer.fatal(msg)
                raise Exception(msg)

            dmdev = dmdev.strip()

            self.tracer.info("device is %s" % dmdev)
            
            wwid = None
            (code, output) = Helper._runOsCommand("ls /sys/block/%s/slaves" % dmdev, self.tracer)
            for dm in output.split():
                (code2, output2) = Helper._runOsCommand("sudo /sbin/multipath -l", self.tracer)
                for l in output2.split("\n"):
                    if " %s " % dm in l:
                        try:
                            wwid = l.split("(")[1].split(")")[0]   # with alias
                        except:
                            wwid = l.split()[0]  #without alias

                        devices[wwid] = "/dev/mapper/%s" % lvmname

            if wwid == None:
                msg = "unable to resolve LVM device '%s' to LUNs" % dmdev
                print msg
                self.tracer.fatal(msg)
                raise Exception(msg)

            for (wwid, dev) in devices.items():
                self.tracer.info("/dev/mapper/%s is part of %s and will be used for reservation" % (wwid, dev))

            
            # extract necessary information
            path = os.path.realpath(storage.get("path"))

            ###################################################################################################

            self.hookDetachPreUmount(globals(), locals())

            # check current mounts
            mountList = self._isMounted(path)

            # if no device is attached at all, end processing
            if len(mountList) != 0:
                # try to unmount device and wait up to 1 minute
                self._forcedUnmount(dmdev, path, 12)
            else:
                (c ,o) = Helper._runOsCommand("sudo /sbin/vgchange -an %s" % vg, self.tracer)
                if c != 0:
                    msg = "vgchange failed for device '%s' : %s" % (vg, o)
                    print msg
                    self.tracer.fatal(msg)
                    raise Exception(msg)

                msg = "no device attached to path '%s'" % path
                print msg
                self.tracer.info(msg)
                continue

            # cleanup of the devices => in normal case |mountList|=1
            for (d, _) in mountList:
                # look up current single devices
                dmdev = os.path.realpath(d).split("/")[-1]
                device = d

                vg = device.rsplit("/", 1)[1].rsplit("-", 1)[0]
                (c ,o) = Helper._runOsCommand("sudo /sbin/vgchange -an %s" % vg, self.tracer)
                if c != 0:
                    msg = "vgchange failed for device '%s' : %s" % (vg, o)
                    print msg
                    self.tracer.fatal(msg)
                    raise Exception(msg)
                
                self.hookDetachPrePRClear(globals(), locals())
    
                msg = "detached device '%s' from path '%s'" % (device, path)
                print msg
                self.tracer.info(msg)
                
                self.hookDetachEndDevice(globals(), locals())

        ###################################################################################################

        (self.prType, _) = self.getPRType(defaultPrType = self.prType)
        self.flushMultipath(self.prType)

        self.hookDetachEnd(globals(), locals())

        return 0


    def info(self, paths):

        self.tracer.info("%s.info method called" % self.__class__.__name__)

        mappings = []

        # retrieve information for each path given by HDB
        for path in paths:
            # determine real OS path without symlinks and retrieve the mounted devices
            path = os.path.realpath(path)
            mountList = self._isMounted(path)

            # no device is mounted
            if len(mountList) == 0:
                mappings.append({"path" : path, "OS Device Name" : "not_connected"})
                continue

            if len(mountList) > 1:
                mappings.append({"path" : path, "OS Device Name" : "found_multiple_mounts_on_path"})
                continue

            devices = {}
            for ml in mountList:
                dmdev = os.path.realpath(ml[0]).split("/")[-1]
                (code, output) = Helper._runOsCommand("ls /sys/block/%s/slaves" % dmdev, self.tracer)
                for dm in output.split():
                    (code2, output2) = Helper._runOsCommand("sudo /sbin/multipath -l", self.tracer)
                    for l in output2.split("\n"):
                        if " %s " % dm in l:
                            try:
                                wwid = l.split("(")[1].split(")")[0]   # with alais
                            except:
                                wwid = l.split()[0]  #without alias

                            devices[wwid] = ml[0]

            # filesystem type
            (code, output) = Helper._run2PipedOsCommand("cat /proc/mounts", "grep -w %s" % path)
            if not code == 0:
                self.tracer.warning("error running cat /proc/mounts: code %s: %s" % (code, output))
                fstype = "?"
            else:
                fstype = output.split()[2]

            wwids= []
            for wwid, dev in devices.items():
                wwids.append(wwid)

            # combine all extracted information
            mappings.append({
                "path" : path,
                "OS Filesystem Type" : fstype,
                "WWIDs" : str(wwids),
                })

        return mappings


    def hookBeginAttach(self, globals, locals):
        pass

    def hookAttachBegin(self, globals, locals):
        pass

    def hookAttachBeginDevice(self, globals, locals):
        pass

    def hookAttachPrePRClear(self, globals, locals):
        pass

    def hookAttachPreCleanupUmount(self, globals, locals):
        pass

    def hookAttachPrePRRegister(self, globals, locals):
        pass

    def hookAttachPreMount(self, globals, locals):
        pass

    def hookAttachEndDevice(self, globals, locals):
        pass

    def hookAttachEnd(self, globals, locals):
        pass


    def hookDetachBegin(self, globals, locals):
        pass

    def hookDetachBeginDevice(self, globals, locals):
        pass

    def hookDetachPreUmount(self, globals, locals):
        pass

    def hookDetachPrePRClear(self, globals, locals):
        pass

    def hookDetachEndDevice(self, globals, locals):
        pass

    def hookDetachEnd(self, globals, locals):
        pass

class AttachLVMThread(threading.Thread):
    def __init__(self, caller, attachLock, partition, usageType, path, testOnly):
        threading.Thread.__init__(self)
        self.name = usageType + "-" + str(partition)
        self.caller = caller
        self.partition = partition
        self.usageType = usageType
        self.path = os.path.realpath(path)
        self.testOnly = testOnly
        self.attachLock = attachLock
        self.firstDev = True

    def join(self):
        threading.Thread.join(self)
        if self.exc:
            msg = "Thread '%s' threw an exception: %s" % (self.getName(), self.exc[1])
            new_exc = Exception(msg)
            raise new_exc.__class__, new_exc, self.exc[2]

    def run(self):
        self.exc = None
        try:
            self.excRun()
        except:
            self.exc = sys.exc_info()

    def excRun(self):
        connectionData = self.caller._getConnectionDataForLun(self.partition, self.usageType)

        # set variables for better code readability
        try:
            lvmname = connectionData["lvmname"]
        except:
            raise Exception("lvmname not set in global.ini")

        (c ,o) = Helper._runOsCommand("sudo /sbin/vgscan", self.caller.tracer)
        if c != 0:
            msg = "vgscan failed for device: %s" % o
            print msg
            self.caller.tracer.fatal(msg)
            raise Exception(msg)

        time.sleep(1)

        vg = self.caller.getVolumeGroup(lvmname)
        (c ,o) = Helper._runOsCommand("sudo /sbin/vgchange -ay %s" % vg, self.caller.tracer)
        if c != 0:
            msg = "vgchange failed for volume group '%s': %s" % (vg, o)
            print msg
            self.caller.tracer.fatal(msg)
            raise Exception(msg)

        ############

        device = "/dev/mapper/%s" % lvmname
        found = False
        for i in range(1, 4):
            if os.path.exists(device):
                found = True
                break;
            time.sleep(1)

        if not found:
            raise Exception("LVM device '%s' not found" % device)

        linkIsCreated = True
        dmdev = os.path.realpath(device).split("/")[-1]
        if not dmdev.startswith("dm-"):
            linkIsCreated = False
            for i in range(1, 11):  # poll up to 10 seconds, because /dev/disk/by-id is updated ASYNC only by the kernel!
                (c, o) = Helper._run2PipedOsCommand("ls -ls /dev/disk/by-id", "grep -w %s" % lvmname, self.caller.tracer)
                dmdev = o.split("/")[-1]
                time.sleep(1)
                if dmdev.startswith("dm-"):
                    linkIsCreated = True
                    break;

        if not linkIsCreated:
            msg = "unable to resolve '%s' to a 'dm-X' name" % lvmname
            print msg
            self.caller.tracer.fatal(msg)
            raise Exception(msg)

        dmdev = dmdev.strip()

        self.caller.tracer.info("device is %s" % dmdev)

        devices = {}
        (code, output) = Helper._runOsCommand("ls /sys/block/%s/slaves" % dmdev, self.caller.tracer)
        slaveDevices = output.split()
        for dm in slaveDevices:
            (code2, output2) = Helper._runOsCommand("sudo /sbin/multipath -l", self.caller.tracer)
            for l in output2.split("\n"):
                if " %s " % dm in l:
                    try:
                        wwid = l.split("(")[1].split(")")[0]   # with alias
                    except:
                        wwid = l.split()[0]  #without alias

                    if len(wwid) < 8:
                        msg = "slave device '%s' of '%s' resolves to invalid WWID '%s'" % (dm, dmdev, wwid)
                        print msg
                        self.caller.tracer.fatal(msg)
                        raise Exception(msg)

                    devices[wwid] = "/dev/mapper/%s" % lvmname

        if not slaveDevices or not len(slaveDevices) is len(devices):
            msg = "unable to resolve LVM device '%s' with slaves (%s) to LUNs" % (dmdev, ",".join(slaveDevices),)
            print msg
            self.caller.tracer.fatal(msg)
            raise Exception(msg)

        for (wwid, dev) in devices.items():
            self.caller.tracer.info("/dev/mapper/%s is part of %s and will be used for reservation" % (wwid, dev))

        ###################################################################################################
        self.caller.hookAttachPreCleanupUmount(globals(), locals())

        if not self.testOnly:
            mountList = set()
            mountList.update(self.caller._isMounted(self.path))
            for (wwid, dev) in devices.items():
                mountList.update(self.caller._isMounted(dev))
            self.caller.unmountEverything(mountList, self.path)

        ###################################################################################################

        (code, output) = Helper._runOsCommand("sudo /sbin/multipath -l", self.caller.tracer)
        firstDevices, singleDevices = [], []
        for (wwid, dev) in devices.items():
            devicesPerLUN = self.caller.extractSingleDevices(output, wwid)
            singleDevices.extend(devicesPerLUN)
            firstDevices.append(devicesPerLUN[0])

        if self.testOnly:
            return

        self.caller.hookAttachPrePRClear(globals(), locals())

        success = False
        for d in singleDevices:
            self.caller.handlePRUnitAttention(d)
            Helper._runOsCommand("sudo /usr/bin/sg_persist --out --register --param-sark=%s %s" % (self.caller.prKey, d), self.caller.tracer)

            # migration code to handle changed usage of reservation keys
            (c, o) = Helper._runOsCommand("sudo /usr/bin/sg_persist -i -k %s" % d, self.caller.tracer)
            if c != 0 or "0x%s" % self.caller.prKey not in o:
                for l in o.split("\n"):
                    if l.strip().startswith("0x"):
                        oldKey = l.strip()[2:]
                        if oldKey != self.caller.prKey:
                            self.caller.handlePRUnitAttention(d)
                            Helper._runOsCommand("sudo /usr/bin/sg_persist --out --register --param-sark=%s %s" % (oldKey, d), self.caller.tracer)
                            self.caller.handlePRUnitAttention(d)
                            Helper._runOsCommand("sudo /usr/bin/sg_persist --out --clear --param-rk=%s %s" % (oldKey, d), self.caller.tracer)
                            break

                self.caller.handlePRUnitAttention(d)
                Helper._runOsCommand("sudo /usr/bin/sg_persist --out --register --param-sark=%s %s" % (self.caller.prKey, d), self.caller.tracer)

        # check if key was registered
        for d in singleDevices:
            (c, o) = Helper._runOsCommand("sudo /usr/bin/sg_persist -i -k %s" % d, self.caller.tracer)
            if c != 0 or "0x%s" % self.caller.prKey not in o:
                msg = "reservation key was not registered on device %s" % d
                print msg
                self.caller.tracer.fatal(msg)
                raise Exception(msg)

        ###################################################################################################

        self.caller.hookAttachPrePRRegister(globals(), locals())

        # the following is only necessary for one device per LUN
        for firstDevForReservation in firstDevices:
            self.caller.handlePRUnitAttention(firstDevForReservation)

            reservations = []
            (c, o) = Helper._runOsCommand("sudo /usr/bin/sg_persist -r %s" % firstDevForReservation, self.caller.tracer)
            for l in o.split("\n"):
                if l.strip().startswith("Key=0x"):
                    key = l.strip()[6:]
                    if key == self.caller.prKey:
                        self.caller.tracer.info("a reservation for this host is already active, re-write reservation")
                        continue
                    reservations.append(key)
                    self.caller.tracer.info("found another reservation active for key=%s, taking over" % key)

            if len(reservations) == 0:
                # reserve with prKey
                Helper._runOsCommand("sudo /usr/bin/sg_persist --out --reserve --param-rk=%s --prout-type=%s %s" %
                                     (self.caller.prKey, self.caller.prType, firstDevForReservation), self.caller.tracer)
            else:
                # if there are multiple other keys, there is still only one reservation to take, try to preempt for all key
                for k in reservations:
                    # preempt with prKey
                    Helper._runOsCommand("sudo /usr/bin/sg_persist --out --preempt --param-sark=%s --param-rk=%s --prout-type=%s %s" %
                                          (k, self.caller.prKey, self.caller.prType, firstDevForReservation), self.caller.tracer)

        for d in singleDevices:
            self.caller.handlePRUnitAttention(d)

        # check actual reservation
        for firstDevForReservation in firstDevices:
            (code, output) = Helper._runOsCommand("sudo /usr/bin/sg_persist -r %s" % firstDevForReservation, self.caller.tracer)
            if code != 0 or self.caller.prTypeReservationText not in output or "0x%s" % self.caller.prKey not in output:
                msg = "reservation of Persistent Reservation key failed for device '%s': %s" % (device, output)
                print msg
                self.caller.tracer.fatal(msg)
                raise Exception(msg)

        ###################################################################################################

        vg = lvmname.rsplit("-", 1)[0]
        (c ,o) = Helper._runOsCommand("sudo /sbin/vgchange -ay %s" % vg, self.caller.tracer)
        if c != 0:
            msg = "vgchange failed for device '%s': %s" % (vg, o)
            print msg
            self.caller.tracer.fatal(msg)
            raise Exception(msg)

        ###################################################################################################

        self.caller.hookAttachPreMount(globals(), locals())

        self.caller.attachMount(connectionData, device, self.path)

        self.caller.hookAttachEndDevice(globals(), locals())
