"""
Basic iscsi support for Linux host with the help of commands
iscsiadm and tgtadm.

This include the basic operates such as login and get device name by
target name. And it can support the real iscsi access and emulated
iscsi in localhost then access it.
"""


import re
import os
import logging
from autotest.client import os_dep
from autotest.client.shared import utils, error
from virttest import utils_selinux

ISCSI_CONFIG_FILE = "/etc/iscsi/initiatorname.iscsi"


def iscsi_get_sessions():
    """
    Get the iscsi sessions activated
    """
    cmd = "iscsiadm --mode session"

    output = utils.system_output(cmd, ignore_status=True)
    sessions = []
    if "No active sessions" not in output:
        for session in output.splitlines():
            ip_addr = session.split()[2].split(',')[0]
            target = session.split()[3]
            sessions.append((ip_addr, target))
    return sessions


def iscsi_get_nodes():
    """
    Get the iscsi nodes
    """
    cmd = "iscsiadm --mode node"

    output = utils.system_output(cmd, ignore_status=True)
    pattern = r"(\d+\.\d+\.\d+\.\d+|\[.+\]):\d+,\d+\s+([\w\.\-:\d]+)"
    nodes = []
    if "No records found" not in output:
        nodes = re.findall(pattern, output)
    return nodes


def iscsi_login(target_name, portal):
    """
    Login to a target with the target name

    :param target_name: Name of the target
    :params portal: Hostname/Ip for iscsi server
    """
    cmd = "iscsiadm --mode node --login --targetname %s" % target_name
    cmd += " --portal %s" % portal
    output = utils.system_output(cmd)

    target_login = ""
    if "successful" in output:
        target_login = target_name

    return target_login


def iscsi_node_del(target_name=None):
    """
    Delete target node record, if the target name is not set then delete
    all target node records.

    :params target_name: Name of the target.
    """
    node_list = iscsi_get_nodes()
    cmd = ''
    if target_name:
        for node_tup in node_list:
            if target_name in node_tup:
                cmd = "iscsiadm -m node -o delete -T %s " % target_name
                cmd += "--portal %s" % node_tup[0]
                utils.system(cmd, ignore_status=True)
                break
        if not cmd:
            logging.error("The target '%s' for delete is not in target node"
                          " record", target_name)
    else:
        for node_tup in node_list:
            cmd = "iscsiadm -m node -o delete -T %s " % node_tup[1]
            cmd += "--portal %s" % node_tup[0]
            utils.system(cmd, ignore_status=True)


def iscsi_logout(target_name=None):
    """
    Logout from a target. If the target name is not set then logout all
    targets.

    :params target_name: Name of the target.
    """
    if target_name:
        cmd = "iscsiadm --mode node --logout -T %s" % target_name
    else:
        cmd = "iscsiadm --mode node --logout all"
    output = utils.system_output(cmd)

    target_logout = ""
    if "successful" in output:
        target_logout = target_name

    return target_logout


def iscsi_discover(portal_ip):
    """
    Query from iscsi server for available targets

    :param portal_ip: Ip for iscsi server
    """
    cmd = "iscsiadm -m discovery -t sendtargets -p %s" % portal_ip
    output = utils.system_output(cmd, ignore_status=True)

    session = ""
    if "Invalid" in output:
        logging.debug(output)
    else:
        session = output
    return session


class _IscsiComm(object):

    """
    Provide an interface to complete the similar initialization
    """

    def __init__(self, params, root_dir):
        """
        common __init__ function used to initialize iSCSI service

        :param params:      parameters dict for iSCSI
        :param root_dir:    path for image
        """
        self.target = params.get("target")
        self.export_flag = False
        self.restart_tgtd = 'yes' == params.get("restart_tgtd", "no")
        if params.get("portal_ip"):
            self.portal_ip = params.get("portal_ip")
        else:
            self.portal_ip = "127.0.0.1"
        if params.get("iscsi_thread_id"):
            self.id = params.get("iscsi_thread_id")
        else:
            self.id = utils.generate_random_string(4)
        self.initiator = params.get("initiator")

        # CHAP AUTHENTICATION
        self.chap_flag = False
        self.chap_user = params.get("chap_user")
        self.chap_passwd = params.get("chap_passwd")
        if self.chap_user and self.chap_passwd:
            self.chap_flag = True

        if params.get("emulated_image"):
            self.initiator = None
            emulated_image = params.get("emulated_image")
            self.emulated_image = os.path.join(root_dir, emulated_image)
            self.device = "device.%s" % os.path.basename(self.emulated_image)
            self.emulated_id = ""
            self.emulated_size = params.get("image_size")
            self.unit = self.emulated_size[-1].upper()
            self.emulated_size = self.emulated_size[:-1]
            # maps K,M,G,T => (count, bs)
            emulated_size = {'K': (1, 1),
                             'M': (1, 1024),
                             'G': (1024, 1024),
                             'T': (1024, 1048576),
                             }
            if emulated_size.has_key(self.unit):
                block_size = emulated_size[self.unit][1]
                size = int(self.emulated_size) * emulated_size[self.unit][0]
                self.emulated_expect_size = block_size * size
                self.create_cmd = ("dd if=/dev/zero of=%s count=%s bs=%sK"
                                   % (self.emulated_image, size, block_size))
        else:
            self.device = None

    def logged_in(self):
        """
        Check if the session is login or not.
        """
        sessions = iscsi_get_sessions()
        login = False
        if self.target in map(lambda x: x[1], sessions):
            login = True
        return login

    def portal_visible(self):
        """
        Check if the portal can be found or not.
        """
        return bool(re.findall("%s$" % self.target,
                               iscsi_discover(self.portal_ip), re.M))

    def set_initiatorName(self, id, name):
        """
        back up and set up the InitiatorName
        """
        if os.path.isfile("%s" % ISCSI_CONFIG_FILE):
            logging.debug("Try to update iscsi initiatorname")
            cmd = "mv %s %s-%s" % (ISCSI_CONFIG_FILE, ISCSI_CONFIG_FILE, id)
            utils.system(cmd)
            fd = open(ISCSI_CONFIG_FILE, 'w')
            fd.write("InitiatorName=%s" % name)
            fd.close()
            utils.system("service iscsid restart")

    def login(self):
        """
        Login session for both real iscsi device and emulated iscsi.
        Include env check and setup.
        """
        login_flag = False
        if self.portal_visible():
            login_flag = True
        elif self.initiator:
            self.set_initiatorName(id=self.id, name=self.initiator)
            if self.portal_visible():
                login_flag = True
        elif self.emulated_image:
            self.export_target()
            # If both iSCSI server and iSCSI client are on localhost.
            # It's necessary to set up the InitiatorName.
            if "127.0.0.1" in self.portal_ip:
                self.set_initiatorName(id=self.id, name=self.target)
            if self.portal_visible():
                login_flag = True

        if login_flag:
            iscsi_login(self.target, self.portal_ip)

    def get_device_name(self):
        """
        Get device name from the target name.
        """
        cmd = "iscsiadm -m session -P 3"
        device_name = ""
        if self.logged_in():
            output = utils.system_output(cmd)
            pattern = r"Target:\s+%s.*?disk\s(\w+)\s+\S+\srunning" % self.target
            device_name = re.findall(pattern, output, re.S)
            try:
                device_name = "/dev/%s" % device_name[0]
            except IndexError:
                logging.error("Can not find target '%s' after login.", self.target)
        else:
            logging.error("Session is not logged in yet.")
        return device_name

    def set_chap_auth_initiator(self):
        """
        Set CHAP authentication for initiator.
        """
        name_dict = {'node.session.auth.authmethod': 'CHAP'}
        name_dict['node.session.auth.username'] = self.chap_user
        name_dict['node.session.auth.password'] = self.chap_passwd
        for name in name_dict.keys():
            cmd = "iscsiadm --mode node --targetname %s " % self.target
            cmd += "--op update --name %s --value %s" % (name, name_dict[name])
            try:
                utils.system(cmd)
            except error.CmdError:
                logging.error("Fail to set CHAP authentication for initiator")

    def logout(self):
        """
        Logout from target.
        """
        if self.logged_in():
            iscsi_logout(self.target)

    def cleanup(self):
        """
        Clean up env after iscsi used.
        """
        self.logout()
        iscsi_node_del(self.target)
        if os.path.isfile("%s-%s" % (ISCSI_CONFIG_FILE, self.id)):
            cmd = "mv %s-%s %s" % (ISCSI_CONFIG_FILE, self.id, ISCSI_CONFIG_FILE)
            utils.system(cmd)
            cmd = "service iscsid restart"
            utils.system(cmd)
        if self.export_flag:
            self.delete_target()


class IscsiTGT(_IscsiComm):

    """
    iscsi support TGT backend used in RHEL6.
    """

    def __init__(self, params, root_dir):
        """
        initialize TGT backend for iSCSI

        :param params: parameters dict for TGT backend of iSCSI.
        """
        super(IscsiTGT, self).__init__(params, root_dir)

    def get_target_id(self):
        """
        Get target id from image name. Only works for emulated iscsi device
        """
        cmd = "tgtadm --lld iscsi --mode target --op show"
        target_info = utils.system_output(cmd)
        target_id = ""
        for line in re.split("\n", target_info):
            if re.findall("Target\s+(\d+)", line):
                target_id = re.findall("Target\s+(\d+)", line)[0]
            if re.findall("Backing store path:\s+(/+.+)", line):
                if self.emulated_image in line:
                    break
        else:
            target_id = ""

        return target_id

    def get_chap_accounts(self):
        """
        Get all CHAP authentication accounts
        """
        cmd = "tgtadm --lld iscsi --op show --mode account"
        all_accounts = utils.system_output(cmd)
        if all_accounts:
            all_accounts = map(str.strip, all_accounts.splitlines()[1:])
        return all_accounts

    def add_chap_account(self):
        """
        Add CHAP authentication account
        """
        try:
            cmd = "tgtadm --lld iscsi --op new --mode account"
            cmd += " --user %s" % self.chap_user
            cmd += " --password %s" % self.chap_passwd
            utils.system(cmd)
        except error.CmdError, err:
            logging.error("Fail to add account: %s", err)

        # Check the new add account exist
        if self.chap_user not in self.get_chap_accounts():
            logging.error("Can't find account %s" % self.chap_user)

    def delete_chap_account(self):
        """
        Delete the CHAP authentication account
        """
        if self.chap_user in self.get_chap_accounts():
            cmd = "tgtadm --lld iscsi --op delete --mode account"
            cmd += " --user %s" % self.chap_user
            utils.system(cmd)

    def get_target_account_info(self):
        """
        Get the target account information
        """
        cmd = "tgtadm --lld iscsi --mode target --op show"
        target_info = utils.system_output(cmd)
        pattern = r"Target\s+\d:\s+%s" % self.target
        pattern += ".*Account information:\s(.*)ACL information"
        try:
            target_account = re.findall(pattern, target_info,
                                        re.S)[0].strip().splitlines()
        except IndexError:
            target_account = []
        return map(str.strip, target_account)

    def set_chap_auth_target(self):
        """
        Set CHAP authentication on a target, it will require authentication
        before an initiator is allowed to log in and access devices.
        """
        if self.chap_user not in self.get_chap_accounts():
            self.add_chap_account()
        if self.chap_user in self.get_target_account_info():
            logging.debug("Target %s already has account %s", self.target,
                          self.chap_user)
        else:
            cmd = "tgtadm --lld iscsi --op bind --mode account"
            cmd += " --tid %s --user %s" % (self.emulated_id, self.chap_user)
            utils.system(cmd)

    def export_target(self):
        """
        Export target in localhost for emulated iscsi
        """
        selinux_mode = None

        if not os.path.isfile(self.emulated_image):
            utils.system(self.create_cmd)
        else:
            emulated_image_size = os.path.getsize(self.emulated_image) / 1024
            if emulated_image_size != self.emulated_expect_size:
                # No need to remvoe, rebuild is fine
                utils.system(self.create_cmd)
        cmd = "tgtadm --lld iscsi --mode target --op show"
        try:
            output = utils.system_output(cmd)
        except error.CmdError:
            utils.system("service tgtd restart")
            output = utils.system_output(cmd)
        if not re.findall("%s$" % self.target, output, re.M):
            logging.debug("Need to export target in host")

            # Set selinux to permissive mode to make sure iscsi target
            # export successfully
            if utils_selinux.is_enforcing():
                selinux_mode = utils_selinux.get_status()
                utils_selinux.set_status("permissive")

            output = utils.system_output(cmd)
            used_id = re.findall("Target\s+(\d+)", output)
            emulated_id = 1
            while str(emulated_id) in used_id:
                emulated_id += 1
            self.emulated_id = str(emulated_id)
            cmd = "tgtadm --mode target --op new --tid %s" % self.emulated_id
            cmd += " --lld iscsi --targetname %s" % self.target
            utils.system(cmd)
            cmd = "tgtadm --lld iscsi --op bind --mode target "
            cmd += "--tid %s -I ALL" % self.emulated_id
            utils.system(cmd)
        else:
            target_strs = re.findall("Target\s+(\d+):\s+%s$" %
                                     self.target, output, re.M)
            self.emulated_id = target_strs[0].split(':')[0].split()[-1]

        cmd = "tgtadm --lld iscsi --mode target --op show"
        try:
            output = utils.system_output(cmd)
        except error.CmdError:   # In case service stopped
            utils.system("service tgtd restart")
            output = utils.system_output(cmd)

        # Create a LUN with emulated image
        if re.findall(self.emulated_image, output, re.M):
            # Exist already
            logging.debug("Exported image already exists.")
            self.export_flag = True
        else:
            tgt_str = re.search(r'.*(Target\s+\d+:\s+%s\s*.*)$' % self.target,
                                output, re.DOTALL)
            if tgt_str:
                luns = len(re.findall("\s+LUN:\s(\d+)",
                                      tgt_str.group(1), re.M))
            else:
                luns = len(re.findall("\s+LUN:\s(\d+)", output, re.M))
            cmd = "tgtadm --mode logicalunit --op new "
            cmd += "--tid %s --lld iscsi " % self.emulated_id
            cmd += "--lun %s " % luns
            cmd += "--backing-store %s" % self.emulated_image
            utils.system(cmd)
            self.export_flag = True

        # Restore selinux
        if selinux_mode is not None:
            utils_selinux.set_status(selinux_mode)

        if self.chap_flag:
            # Set CHAP authentication on the exported target
            self.set_chap_auth_target()
            # Set CHAP authentication for initiator to login target
            if self.portal_visible():
                self.set_chap_auth_initiator()

    def delete_target(self):
        """
        Delete target from host.
        """
        cmd = "tgtadm --lld iscsi --mode target --op show"
        output = utils.system_output(cmd)
        if re.findall("%s$" % self.target, output, re.M):
            if self.emulated_id:
                cmd = "tgtadm --lld iscsi --mode target --op delete "
                cmd += "--tid %s" % self.emulated_id
                utils.system(cmd)
        if self.restart_tgtd:
            cmd = "service tgtd restart"
            utils.system(cmd)


class IscsiLIO(_IscsiComm):

    """
    iscsi support class for LIO backend used in RHEL7.
    """

    def __init__(self, params, root_dir):
        """
        initialize LIO backend for iSCSI

        :param params: parameters dict for LIO backend of iSCSI
        """
        super(IscsiLIO, self).__init__(params, root_dir)

    def get_target_id(self):
        """
        Get target id from image name.
        """
        cmd = "targetcli ls /iscsi 1"
        target_info = utils.system_output(cmd)
        target = None
        for line in re.split("\n", target_info)[1:]:
            if re.findall("o-\s\S+\s[\.]+\s\[TPGs:\s\d\]$", line):
                # eg: iqn.2015-05.com.example:iscsi.disk
                try:
                    target = re.findall("iqn[\.]\S+:\S+", line)[0]
                except IndexError:
                    logging.info("No found target in %s", line)
                    continue
            else:
                continue

            cmd = "targetcli ls /iscsi/%s/tpg1/luns" % target
            luns_info = utils.system_output(cmd)
            for lun_line in re.split("\n", luns_info):
                if re.findall("o-\slun\d+", lun_line):
                    if self.emulated_image in lun_line:
                        break
                    else:
                        target = None
        return target

    def set_chap_acls_target(self):
        """
        set CHAP(acls) authentication on a target.
        it will require authentication
        before an initiator is allowed to log in and access devices.

        notice:
            Individual ACL entries override common TPG Authentication,
            which can be set by set_chap_auth_target().
        """
        # Enable ACL nodes
        acls_cmd = "targetcli /iscsi/%s/tpg1/ " % self.target
        attr_cmd = "set attribute generate_node_acls=0"
        utils.system(acls_cmd + attr_cmd)

        # Create user and allow access
        acls_cmd = ("targetcli /iscsi/%s/tpg1/acls/ create %s:client"
                    % (self.target, self.target.split(":")[0]))
        output = utils.system_output(acls_cmd)
        if "Created Node ACL" not in output:
            raise error.TestFail("Failed to create ACL. (%s)", output)

        comm_cmd = ("targetcli /iscsi/%s/tpg1/acls/%s:client/"
                    % (self.target, self.target.split(":")[0]))
        # Set userid
        userid_cmd = "%s set auth userid=%s" % (comm_cmd, self.chap_user)
        output = utils.system_output(userid_cmd)
        if self.chap_user not in output:
            raise error.TestFail("Failed to set user. (%s)", output)

        # Set password
        passwd_cmd = "%s set auth password=%s" % (comm_cmd, self.chap_passwd)
        output = utils.system_output(passwd_cmd)
        if self.chap_passwd not in output:
            raise error.TestFail("Failed to set password. (%s)", output)

        # Save configuration
        utils.system("targetcli / saveconfig")

    def set_chap_auth_target(self):
        """
        set up authentication information for every single initiator,
        which provides the capability to define common login information
        for all Endpoints in a TPG
        """
        auth_cmd = "targetcli /iscsi/%s/tpg1/ " % self.target
        attr_cmd = ("set attribute %s %s %s" %
                    ("demo_mode_write_protect=0",
                     "generate_node_acls=1",
                     "cache_dynamic_acls=1"))
        utils.system(auth_cmd + attr_cmd)

        # Set userid
        userid_cmd = "%s set auth userid=%s" % (auth_cmd, self.chap_user)
        output = utils.system_output(userid_cmd)
        if self.chap_user not in output:
            raise error.TestFail("Failed to set user. (%s)", output)

        # Set password
        passwd_cmd = "%s set auth password=%s" % (auth_cmd, self.chap_passwd)
        output = utils.system_output(passwd_cmd)
        if self.chap_passwd not in output:
            raise error.TestFail("Failed to set password. (%s)", output)

        # Save configuration
        utils.system("targetcli / saveconfig")

    def export_target(self):
        """
        Export target in localhost for emulated iscsi
        """
        selinux_mode = None

        # create image disk
        if not os.path.isfile(self.emulated_image):
            utils.system(self.create_cmd)
        else:
            emulated_image_size = os.path.getsize(self.emulated_image) / 1024
            if emulated_image_size != self.emulated_expect_size:
                # No need to remvoe, rebuild is fine
                utils.system(self.create_cmd)

        # confirm if the target exists and create iSCSI target
        cmd = "targetcli ls /iscsi 1"
        output = utils.system_output(cmd)
        if not re.findall("%s$" % self.target, output, re.M):
            logging.debug("Need to export target in host")

            # Set selinux to permissive mode to make sure
            # iscsi target export successfully
            if utils_selinux.is_enforcing():
                selinux_mode = utils_selinux.get_status()
                utils_selinux.set_status("permissive")

            # In fact, We've got two options here
            #
            # 1) Create a block backstore that usually provides the best
            #    performance. We can use a block device like /dev/sdb or
            #    a logical volume previously created,
            #     (lvcreate -name lv_iscsi -size 1G vg)
            # 2) Create a fileio backstore,
            #    which enables the local file system cache.
            #
            # This class Only works for emulated iscsi device,
            # So fileio backstore is enough and safe.

            # Create a fileio backstore
            device_cmd = ("targetcli /backstores/fileio/ create %s %s" %
                          (self.device, self.emulated_image))
            output = utils.system_output(device_cmd)
            if "Created fileio" not in output:
                raise error.TestFail("Failed to create fileio %s. (%s)",
                                     self.device, output)

            # Create an IQN with a target named target_name
            target_cmd = "targetcli /iscsi/ create %s" % self.target
            output = utils.system_output(target_cmd)
            if "Created target" not in output:
                raise error.TestFail("Failed to create target %s. (%s)",
                                     self.target, output)

            check_portal = "targetcli /iscsi/%s/tpg1/portals ls" % self.target
            if "0.0.0.0:3260" not in utils.system_output(check_portal):
                # Create portal
                # 0.0.0.0 means binding to INADDR_ANY
                # and using default IP port 3260
                portal_cmd = ("targetcli /iscsi/%s/tpg1/portals/ create %s"
                              % (self.target, "0.0.0.0"))
                output = utils.system_output(portal_cmd)
                if "Created network portal" not in output:
                    raise error.TestFail("Failed to create portal. (%s)",
                                         output)

            # Create lun
            lun_cmd = "targetcli /iscsi/%s/tpg1/luns/ " % self.target
            dev_cmd = "create /backstores/fileio/%s" % self.device
            output = utils.system_output(lun_cmd + dev_cmd)
            if "Created LUN" not in output:
                raise error.TestFail("Failed to create lun. (%s)",
                                     output)

            # Set firewall if it's enabled
            output = utils.system_output("firewall-cmd --state",
                                         ignore_status=True)
            if re.findall("^running", output, re.M):
                # firewall is running
                utils.system("firewall-cmd --permanent --add-port=3260/tcp")
                utils.system("firewall-cmd --reload")

            # Restore selinux
            if selinux_mode is not None:
                utils_selinux.set_status(selinux_mode)

            self.export_flag = True
        else:
            logging.info("Target %s has already existed!" % self.target)

        if self.chap_flag:
            # Set CHAP authentication on the exported target
            self.set_chap_auth_target()
            # Set CHAP authentication for initiator to login target
            if self.portal_visible():
                self.set_chap_auth_initiator()
        else:
            # To enable that so-called "demo mode" TPG operation,
            # disable all authentication for the corresponding Endpoint.
            # which means grant access to all initiators,
            # so that they can access all LUNs in the TPG
            # without further authentication.
            auth_cmd = "targetcli /iscsi/%s/tpg1/ " % self.target
            attr_cmd = ("set attribute %s %s %s %s" %
                        ("authentication=0",
                         "demo_mode_write_protect=0",
                         "generate_node_acls=1",
                         "cache_dynamic_acls=1"))
            output = utils.system_output(auth_cmd + attr_cmd)
            logging.info("Define access rights: %s" % output)

        # Save configuration
        utils.system("targetcli / saveconfig")

    def delete_target(self):
        """
        Delete target from host.
        """
        # Delete block
        if self.device is not None:
            cmd = "targetcli /backstores/fileio ls"
            output = utils.system_output(cmd)
            if re.findall("%s" % self.device, output, re.M):
                dev_del = ("targetcli /backstores/fileio/ delete %s"
                           % self.device)
                utils.system(dev_del)

        # Delete IQN
        cmd = "targetcli ls /iscsi 1"
        output = utils.system_output(cmd)
        if re.findall("%s" % self.target, output, re.M):
            del_cmd = "targetcli /iscsi delete %s" % self.target
            utils.system(del_cmd)

        # Clear all configuration to avoid restoring
        cmd = "targetcli clearconfig confirm=True"
        utils.system(cmd)


class Iscsi(object):

    """
    Basic iSCSI support class,
    which will handle the emulated iscsi export and
    access to both real iscsi and emulated iscsi device.

    The class support different kinds of iSCSI backend (TGT and LIO),
    and return ISCSI instance.
    """
    @staticmethod
    def create_iSCSI(params, root_dir="/tmp"):
        iscsi_instance = None
        try:
            os_dep.command("iscsiadm")
            os_dep.command("tgtadm")
            iscsi_instance = IscsiTGT(params, root_dir)
        except ValueError:
            try:
                os_dep.command("iscsiadm")
                os_dep.command("targetcli")
                iscsi_instance = IscsiLIO(params, root_dir)
            except ValueError:
                pass

        return iscsi_instance
