#!/usr/bin/python2.7
"""

dhutil:    Datera Host Utility

Usage:
    dhutil
        --basename <string>
        --template <string>
        --size <string> (GB)
        --count <int>
        --numreplicas <int>
        --cleanall
        --showall
        --mkfs
        --mpmap
        --dirprefix <string>
        --chown <string>
        --fstype <string>

    'basename' corresponds to the app_instance
    'size' is interpreted as number of GB.
    'count' is the number of app_instances to create
    'numreplicas' is the number of replicas to create
    'template' must refer to an existing template
    'cleanall' applies only to a given 'basename'
    'showall' takes no other args/options
    'mkfs' will create ext4 file systems on the volumes created
    'mpmap' will show the hostside multipath volume mapping
    'dirprefix' specifies a mount point prefix for the volume mount
    'chown' specifies chown:chgrp for the mount point
    'fstype currently xfs (default) or ext4

"""
import sys
import os
import subprocess
import platform
import time
import argparse
import linecache
import re
import multiprocessing as mp
from dfs_sdk import DateraApi

SLEEP = 10
NUMREPLICAS = 3
DISK_BY_PATH = "/dev/disk/by-path"
SYS_BLOCK = "/sys/block"


def usage():
    print """
Usage:
    dhutil
        --basename <string>
        --template <string>
        --size <string> (GB)
        --count <int>
        --numreplicas <int>
        --cleanall
        --showall
        --mkfs
        --mpmap
        --dirprefix <string>
        --chown <string>
        --fstype <string>

    'basename' corresponds to the app_instance
    'size' is interpreted as number of GB.
    'count' is the number of app_instances to create
    'numreplicas' is the number of replicas to create
    'template' must refer to an existing template
    'cleanall' applies only to a given 'basename'
    'showall' takes no other args/options
    'mkfs' will create ext4 file systems on the volumes created
    'mpmap' will show the hostside multipath volume mapping
    'dirprefix' specifies a mount point prefix for the volume mount
    'chown' specifies chown:chgrp for the mount point
    'fstype currently xfs (default) or ext4


"""

    exit(1)

#
# Create Volume within a storage instance.
# Takes storage_instance as input parameter
#


def vol_create(si, name, args):
    if args.numreplicas:
        numreplicas = args.numreplicas
    else:
        numreplicas = NUMREPLICAS
    vol = si.volumes.create(
        name=name, size=int(args.size),
        replica_count=int(numreplicas))
    return vol['uuid']


def initiator_create(api, name, id):
    iis = api.initiators.list()
    for i in iis:
        if i['id'] == id:
            print "Initiator: (%s) %s,  already exists" % (i['name'], id)
            return i
    print "Creating Initiator: (%s) %s" % (name, id)
    return api.initiators.create(name=name, id=id)


def PrintException():
    exc_type, exc_obj, tb = sys.exc_info()
    f = tb.tb_frame
    lineno = tb.tb_lineno
    filename = f.f_code.co_filename
    linecache.checkcache(filename)
    line = linecache.getline(filename, lineno, f.f_globals)
    print 'EXCEPTION IN ({}, LINE {} "{}"): {}'.format(
        filename, lineno, line.strip(), exc_obj)


def run_cmd(cmd):
    print cmd
    os.system(cmd)


def target_logout_and_node_cleanup(api, node, name):
    for ai in api.app_instances.list():
        for si in ai.storage_instances.list():
            if name in ai['name']:
                print si['access']['ips']
                if 'iqn' in si['access']:
                    run_cmd("iscsiadm -m node -u -T %s" % si['access']['iqn'])
                    run_cmd("iscsiadm -m node -T %s --op=delete" % si['access']['iqn'])
                for ip in si['access']['ips']:
                    run_cmd("iscsiadm -m discoverydb -t st -p %s:3260 --op=delete" % ip)
    #restart iscsi
    run_cmd("systemctl restart iscsi")

def target_login(node, si, initiator, sleep=SLEEP):
    # Wait a bit ...
    if sleep != 0:
        print "Wait %s for storage to come online " % sleep
        time.sleep(sleep)
    iqn = si['access']['iqn']
    print "IQN: " + iqn
    for ip in si['access']['ips']:
        print "IP ADDRS = ", ip
        cmd = "iscsiadm -m node -T {} --portal {} --op=new".format(iqn, ip)
        cmd = cmd + " > /dev/null 2>&1"
        run_cmd(cmd)
        cmd = "iscsiadm -m node -T {} --portal {} -l > /dev/null 2>&1"
        run_cmd(cmd.format(iqn, ip))


def clean_all(api, target):
    for app_instance in api.app_instances.list():
        if not app_instance["name"].startswith(target):
            continue
        print "Delete app instance " + app_instance['name']
        app_instance.set(admin_state="offline", force=True)
        app_instance.delete()
    for initiator in api.initiators.list():
        if not initiator["name"].startswith(target):
            continue
        print "Delete initiator " + initiator['name']
        initiator.delete()


def sn_to_hostname(api, sn):
    uuid = sn.split("/")[2]
    sn = api.storage_nodes.get(uuid)
    return sn['name']

def si_to_hostname(api, si):
    sn_name = 'undef'
    for sn in si['active_storage_nodes']:
        uuid = sn.split("/")[2]
        sn = api.storage_nodes.get(uuid)
	sn_name = 'undef'
	sn_name = sn['name']
    #todo, this only returns the last active server in the list 
    return sn_name

def show_all(api):
    for at in api.app_templates.list():
        print "App_Template: " + at['name']
    for ai in api.app_instances.list():
        print "App_instance: " + ai['name']
        print "    admin_state: ", ai['admin_state']
        for si in ai.storage_instances.list():
            print "    -Storage_instance: " + si['name']
            for i in si.acl_policy.initiators.list():
                print "        +Initiators: %s  (%s)" % (i['id'], i['name'])
            if 'iqn' in si['access']:
                print "        +IQN: " + si['access']['iqn']
            for ip in si['access']['ips']:
                print "        +ACCESS IP: " + ip
            for sn in si['active_storage_nodes']:
                print "        +ACTIVE_STORAGE_NODES: " + sn_to_hostname(
                      api, sn)
            for v in si.volumes.list():
                print "        =Volume: {},   Size : {},   UUID : {} ".format(
                    v['name'], v['size'], v['uuid'])


# [0] = dmpath, [1] = mntpoint, [2] = cmdargs
def mkfs(args):
    if args[2].fstype == "ext4":
        cmd = "mkfs.ext4 {} ; mkdir -p /{}; mount {} /{}".format(
            args[0], args[1], args[0], args[1])
    else:
        cmd = "mkfs.xfs {} ; mkdir -p /{}; mount {} /{}".format(
            args[0], args[1], args[0], args[1])
    print cmd
    os.system(cmd)
    if args[2].chown:
        os.system("chown -R {} /{}".format(args[2].chown, args[1]))


def iqn_to_sd(iqn):
    for f in os.listdir(DISK_BY_PATH):
        if iqn in f:
            return os.path.basename(
                os.readlink(DISK_BY_PATH + "/" + f))


def sd_to_dm(sd):
    for f in os.listdir(SYS_BLOCK):
        t = "{}/{}/slaves/{}".format(SYS_BLOCK, f, sd)
        if os.path.islink(t):
            return f


def dm_to_mapper(dm):
    fname = "{}/{}/dm/name".format(SYS_BLOCK, dm)
    with open(fname, 'r') as f:
        mapper = f.read().strip()
    f.closed
    return mapper


def mpmap(api, args):
    dist = platform.dist()[0]
    map = {}
    mntmap = []

    for ai in api.app_instances.list():
        for si in ai.storage_instances.list():
            for v in si.volumes.list():
                id = v['uuid']
                if not args.basename or args.basename in ai['name']:
                    map[id] = {'aname': ai['name'], 'sname': si['name'],
                               'vname': v['name'], 'id': id,
                               'nodename': si_to_hostname(api,si) }
                    if 'iqn' in si['access']:
                        map[id]['iqn'] = si['access']['iqn']

    for m in map:
        sd = iqn_to_sd(map[m]['iqn'])
        if sd:
            dm = sd_to_dm(sd)
            if not dm:
                print "No DM for sd: ", sd
                sys.exit(-1)
            mapper = dm_to_mapper(dm)
            if not mapper:
                print "No mapper entry for dm: ", dm
                sys.exit(-1)
            dmpath = "/dev/mapper/" + mapper
            str = "HOST-DM: {}"
            str += "   DATERA: {}/{}/{}   IQN: {}   "
            str += "MAPPER: /dev/mapper/{}  NODE:{}"
            print str.format(
                dm, map[m]['aname'], map[m]['sname'],
                map[m]['vname'], map[m]['iqn'], dm_to_mapper(dm),
                map[m]['nodename'])

            if args.mkfs:
                    # ASSUME "singleton" 1:1:1 (app_inst:storage_inst:vol)
                    mntpoint = map[m]['aname']
                    # Change any remaining '_' to '-'
                    mntpoint = re.sub(r'_', "-", mntpoint)
                    if args.dirprefix:
                        mntpoint = args.dirprefix + "/" + mntpoint
                    mntmap.append([dmpath, mntpoint, args])

    if args.mkfs:
        pool = mp.Pool()
        list(pool.imap_unordered(mkfs, mntmap))


def chktempl(api, templ):
    for at in api.app_templates.list():
        if at['name'] == templ:
            return True
    return False


def unmount(name):
    cmd = "mount |grep %s | awk '{print $3}'" % name
    for l in os.popen(cmd).readlines():
        line = l.rstrip()
        if line == "/":
           print "skipping unmount of /"
           return None
        p = os.path.basename(line)
        run_cmd("umount %s" % line)
        run_cmd("rm -rf %s" % line)


def nocreds():
    print
    print "Credentials needed of form 'user:password:IPAddr'"
    print "supplied in DTSCREDS environment variable"
    print
    sys.exit(-1)


def chk_args(args):

    # the only options allowed without basename
    if not args.basename and not args.showall and not args.mpmap:
        print "ERROR: most options require basename"
        usage()

    # need at least one of these
    if not args.basename and not args.showall and not args.mpmap:
        print "ERROR:  Need atleast 'basename', 'showall' or 'mpmap'"
        usage()

    # options required if basename
    if args.basename and not (args.template or args.count or
                              args.cleanall or args.mpmap or args.mkfs):
        print "ERROR: Missing required arguments for 'basename'"
        usage()

    # options not allowed with cleanall
    if args.cleanall and (args.size or args.count or args.showall or
                          args.mkfs or args.mpmap or args.dirprefix or
                          args.chown or args.template or
                          args.fstype or args.numreplicas):
        print "ERROR: 'cleanall' include extraneous options"
        usage()

    # option combinations required
    if args.count and not (args.size or args.template):
        print "ERROR: 'count' requires 'size' or 'template'"
        usage()

    if args.template and not args.count:
        print "ERROR: 'template' requires 'count'"
        usage()

    if args.fstype and not args.mkfs:
        print "ERROR: 'fstype' requires 'mkfs'"
        usage()

    # options not allowed together
    if (args.size or args.numreplicas) and args.template:
        print "ERROR: 'size' and 'numreplicas' not allowed with 'template'"
        usage()

    # options with specific required values
    if args.fstype and args.fstype not in ['ext4', 'xfs']:
        print "ERROR: Only 'xfs' and 'ext4' are allowed for fstype"
        usage()

def iscsiadm_chk():
    try:
        subprocess.check_output(["iscsiadm", "--version"])
    except OSError as e:
        print 
        print "iscsiadm not available."
        print "Please install :"
        print "      RH/CentOS: 'yum install iscsi-initiator-utils'"
        print "      Ubuntu:    'apt-get install open-iscsi'"
        print
        sys.exit(1)


def mpath_chk():
    try:
        subprocess.check_output(["multipath", "-v0", ])
    except OSError as e:
        print 
        print "multipath not available."
        print "Please install :"
        print "      RH/CentOS: 'yum install device-mapper-multipath'"
        print "      Ubuntu:    'apt-get install multipath-tools'"
        print
        sys.exit(1)


def lsscsi_chk():
    try:
        subprocess.check_output(["lsscsi", "-t", ])
    except OSError as e:
        print 
        print "lsscsi not available."
        print "Please install :"
        print "      RH/CentOS: 'yum install lsscsi'"
        print "      Ubuntu:    'apt-get install lsscsi'"
        print
        sys.exit(1)


def dbck():
    # Check for docker bug: https://github.com/docker/docker/issues/7101"
    try:
        subprocess.check_output(
            "grep sysfs /proc/mounts | grep ro", shell=True)
        try:
            subprocess.call("mount -o rw,remount sysfs /sys")
            sys.exit(0)
        except Exception as e:
            print "Encountered https://github.com/docker/docker/issues/7101"
            print "and cannot remount /sys.  Need to be root?"
            sys.exit(1)
    except Exception as e:
        # sysfs is not mounted 'ro'
        pass


def main():

    if (len(sys.argv) < 2):
        usage()

    dbck()
    iscsiadm_chk()
    mpath_chk()
    lsscsi_chk()
    parser = argparse.ArgumentParser(description='dhutil')
    parser.add_argument('--basename', action="store", dest="basename")
    parser.add_argument('--template', action="store", dest="template")
    parser.add_argument('--size', action="store", dest="size")
    parser.add_argument('--count', action="store", dest='count', type=int)
    parser.add_argument('--numreplicas', action="store", dest='numreplicas',
                        type=int)
    parser.add_argument('--cleanall', action="store_true")
    parser.add_argument('--showall', action="store_true")
    parser.add_argument('--mkfs', action="store_true")
    parser.add_argument('--mpmap', action="store_true")
    parser.add_argument('--dirprefix', action="store", dest='dirprefix')
    parser.add_argument('--chown', action="store", dest='chown')
    parser.add_argument('--fstype', action="store", dest='fstype')
    args = parser.parse_args()

    creds = os.getenv("DTSCREDS")
    if not creds or creds == "":
        nocreds()
    user, password, ipaddr = creds.split(":")

    basename = args.basename

    if basename and len(basename) < 3:
        print "Validation Error:  %s : must be at least 3 chars" % basename
        sys.exit(-1)

    # Make sure args make sense
    chk_args(args)

    try:
        print "Connecting to : ", ipaddr
        api = DateraApi(username=user, password=password, hostname=ipaddr)
    except Exception as e:
        PrintException()
        sys.exit(1)

    host = os.uname()[1].split('.')[0]

    if args.cleanall:
        unmount(args.basename)
        target_logout_and_node_cleanup(api, host, basename)
        clean_all(api, basename)
        sys.exit(0)

    if args.showall:
        show_all(api)
        if args.mpmap:
            mpmap(api, args)
        sys.exit(0)

    if args.mpmap and not args.count:
        mpmap(api, args)
        sys.exit(0)

    cmd = "grep '^InitiatorName' /etc/iscsi/initiatorname.iscsi"
    cmd = cmd + " | sed -e 's/InitiatorName=//'"
    iscsi_initiator = os.popen(cmd).read().strip()
    ii = initiator_create(api, host, iscsi_initiator)

    #
    # Semantics:  If a template exists, create args.count instances
    #             from that template.
    #             If a template does not exist, create args.count instances
    #             Assumes 1:1:1 ratio of AppInstance:StorageInstance:Volume
    # TODO:       Allow non-singleton configurations

    # Make sure template exists
    if args.template:
        if chktempl(api, args.template):
            # Create N app_instances from the template
            for n in range(1, args.count + 1):
                appname = "%s-%d" % (args.basename, n)
                tname = "/app_templates/" + args.template
                ai = api.app_instances.create(name=appname, app_template=tname)
                print "Created app_instance: ", ai['name']
                for si in ai.storage_instances.list():
                    si.acl_policy.initiators.add(ii)
            # Login to the storage
            print "Waiting 10 for storage to come online ..."
            time.sleep(10)
            for ai in api.app_instances.list():
                if args.basename in ai['name']:
                    for si in ai.storage_instances.list():
                        target_login(host, si, iscsi_initiator, sleep=0)
            run_cmd("lsscsi -t")
        else:
            print args.template, " : does not exist!"
            sys.exit(-1)
    elif args.count:
        #
        # Create N app_instances, storage_instance, and add initiator
        #
        for i in range(1, args.count + 1):
            bname = "{}_{}".format(basename, i)
            ai = api.app_instances.create(name=bname)
            si = ai.storage_instances.create(name=bname)
            si.acl_policy.initiators.add(ii)
            volname = "{}_vol".format(bname)
            uuid = vol_create(si, volname, args)
            print "Created Volume: %s (%s)" % (volname, uuid)

        print "Waiting 10 for storage to come online ..."
        time.sleep(10)
        for ai in api.app_instances.list():
            if basename in ai['name']:
                si = ai.storage_instances.list()[0]
                target_login(host, si, iscsi_initiator, sleep=0)

        run_cmd("lsscsi -t")

    if args.mkfs:
        mpmap(api, args)


if __name__ == '__main__':
    main()
