# Copyright (C) 2016 Red Hat, Inc.
# This file is part of libsan.
#
# libsan is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# libsan is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with libsan.  If not, see <http://www.gnu.org/licenses/>.

"""scsi.py: Module to manipulate SCSI devices."""

__author__ = "Bruno Goncalves"
__copyright__ = "Copyright (c) 2016 Red Hat, Inc. All rights reserved."

import os.path
import re  # regex
from os import listdir, readlink

import libsan.host.linux
import libsan.host.lvm
import libsan.host.md
import libsan.host.mp
import libsan.host.net
import libsan.misc.array
import libsan.misc.size
from libsan import _print
from libsan.host.cmdline import exists, run

# I'm still note sure whether to use /sys/class/scsi_disk or /sys/class/scsi_device

sys_disk_path = "/sys/class/scsi_disk"
host_path = "/sys/class/scsi_host"

# add /lib/udev to PATH because scsi_id is located there on RHEL7
os.environ["PATH"] += ":/lib/udev"


def get_regex_scsi_id():
    return "([0-9]+):([0-9]+):([0-9]+):([0-9]+)"


def is_scsi_device(scsi_device):
    """ """
    return bool(re.match("^sd[a-z]+$", scsi_device))


def get_scsi_disk_ids():
    """Return an array of scsi_ids. If an scsi_device name is given as
    parameter, then just the id of the device is returned (TODO)
    The arguments are:
    \tNone
    \tDevice name: eg. sda
    Returns:
    \tarray: Return an array of SCSI IDs
    """
    cmd = "ls %s" % sys_disk_path
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        return None
    device_ids = output.split()
    return device_ids


def get_scsi_disk_name(device_id):
    if not device_id:
        _print("FAIL: get_scsi_disk_name() requires scsi_device_id as parameter")
        return None

    cmd = f"ls {sys_disk_path}/{device_id}/device/block"
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        return None
    device_name = output
    return device_name


def get_scsi_disk_vendor(device_id):
    if not device_id:
        _print("FAIL: get_scsi_disk_vendor() requires scsi_device_id as parameter")
        return None

    cmd = f"cat {sys_disk_path}/{device_id}/device/vendor"
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        return None
    device_vendor = output
    return device_vendor


def get_scsi_disk_model(device_id):
    if not device_id:
        _print("FAIL: get_scsi_disk_model() requires scsi_device_id as parameter")
        return None

    cmd = f"cat {sys_disk_path}/{device_id}/device/model"
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        return None
    device_model = output
    return device_model


def query_all_scsi_disks(scsi_disk=None):
    """Query information of all SCSI disks and return them as a dict
    SCSI ID is the dict key.
    If an SCSI disk is given as argument, return its info
    Parameter:
    \tscsi_disk (option):        SCSI disk name. eg: 'sda'
    """
    disk_ids = get_scsi_disk_ids()
    if not disk_ids:
        # Could not find any SCSI device
        return None

    scsi_disks = {}
    for disk_id in disk_ids:
        scsi_name = get_scsi_disk_name(disk_id)
        if scsi_disk and scsi_name and scsi_disk != scsi_name:
            # optmization in case we requested specific disk, do not query all
            continue
        if not scsi_name:
            _print("WARN: Could not get scsi_name for disk_id '%s'." % disk_id)
            scsi_wwid = logical_block_size = physical_block_size = None
            scsi_wwn = udev_wwn = size_bytes = state = timeout = None
        else:
            scsi_wwid = wwid_of_disk(scsi_name)
            scsi_wwn = wwn_of_disk(scsi_name)
            udev_wwn = udev_wwn_of_disk(scsi_name)
            size_bytes = size_of_disk(scsi_name)
            logical_block_size = get_logical_block_size_of_disk(scsi_name)
            physical_block_size = get_physical_block_size_of_disk(scsi_name)
            state = disk_sys_check(scsi_name)
            timeout = timeout_of_disk(scsi_name)
        scsi_vendor = get_scsi_disk_vendor(disk_id)
        scsi_model = get_scsi_disk_model(disk_id)
        m = re.match(get_regex_scsi_id(), disk_id)
        host_id = None
        driver = None
        if m:
            host_id = m.group(1)
            driver = scsi_driver_of_host_id(host_id)
        scsi_info = {
            "name": scsi_name,
            "wwid": scsi_wwid,
            "wwn": scsi_wwn,  # Uses scsi_id to query WWN
            "udev_wwn": udev_wwn,  # Used udevadm to query WWN
            "size": size_bytes,
            "size_human": libsan.misc.size.size_bytes_2_size_human(size_bytes),
            "logical_block_size": logical_block_size,
            "physical_block_size": physical_block_size,
            "state": state,
            "timeout": timeout,
            "vendor": scsi_vendor,
            "model": scsi_model,
            "host_id": host_id,
            "driver": driver,
            "scsi_id": disk_id,
        }
        scsi_disks[disk_id] = scsi_info

    if scsi_disk:
        for disk_id in list(scsi_disks.keys()):
            if scsi_disk == scsi_disks[disk_id]["name"]:
                return scsi_disks[disk_id]
        return None

    return scsi_disks


def get_scsi_name_by_vendor(vendor):
    """Query information of all SCSI disks and return all scsi device names that
    are from the requested vendor
    Parameter:
    \tvendor:        SCSI disk Vendor. eg: 'LIO'
    Return:
    \tList:          List of SCSI names
    """
    if not vendor:
        _print("FAIL: get_scsi_name_by_vendor() - requires vendor parameter")
        return None

    all_scsi_disks_info = query_all_scsi_disks()
    if not all_scsi_disks_info:
        return None
    scsi_names = []
    for scsi_info in list(all_scsi_disks_info.values()):
        if "vendor" in list(scsi_info.keys()) and scsi_info["vendor"] == vendor:
            scsi_names.append(scsi_info["name"])
    return scsi_names


def scsi_host_of_scsi_name(scsi_name):
    """ """
    if not scsi_name:
        _print("FAIL: scsi_host_of_scsi_name() - requires scsi_name parameter")
        return None

    scsi_disk_info = query_all_scsi_disks(scsi_name)
    if not scsi_disk_info:
        _print("WARN: scsi_host_of_scsi_name() did not query info for %s" % scsi_name)
        return None
    return scsi_disk_info["host_id"]


def scsi_name_2_scsi_id(scsi_name):
    """ """
    if not scsi_name:
        _print("FAIL: scsi_name_2_scsi_id() - requires scsi_name parameter")
        return None

    scsi_disk_info = query_all_scsi_disks(scsi_name)
    if not scsi_disk_info:
        _print("WARN: scsi_name_2_scsi_id() did not query info for %s" % scsi_name)
        return None
    return scsi_disk_info["scsi_id"]


def delete_disk(device_name):
    """device_name:    eg. sda"""
    if not device_name:
        _print("FAIL: delete_disk() requires scsi_device_name as parameter")
        return None

    device_id = scsi_name_2_scsi_id(device_name)
    if not device_id:
        _print("FAIL: delete_disk() could not find disk %s" % device_name)
        return None

    cmd = f'echo "1" >  {sys_disk_path}/{device_id}/device/delete'
    retcode = run(cmd, verbose=False)
    if retcode != 0:
        return None
    return True


def get_hosts(somethings=None):
    """Return a list with all SCSI hosts.
    The arguments are:
    \tNone
    or
    \tscsi_disk     eg. sda
    or
    \tscsi_id       eg. 3:0:0:1
    Returns:
    \tHost list     if no problem executing command
    \tNone          if something went wrong
    """

    cmd = "ls %s" % host_path
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        return None
    # remove 'host' prefix
    output = re.sub("host", "", output)
    all_host_ids = output.split()
    if not somethings:
        return all_host_ids

    scsi_host_ids = None
    # If somethings is a single string, convert it to list
    for something in somethings if not isinstance(somethings, str) else [somethings]:
        if something is None:
            _print("FAIL: get_hosts() - Invalid input")
            print(somethings)
            return None
        m = re.match(get_regex_scsi_id(), something)
        if m and m.group(1) in all_host_ids:
            if not scsi_host_ids:
                scsi_host_ids = []
            if m.group(1) not in scsi_host_ids:
                scsi_host_ids.append(m.group(1))
        else:
            # Assume it is scsi_disk name, such as sda
            scsi_id = scsi_name_2_scsi_id(something)
            m = re.match(get_regex_scsi_id(), scsi_id)
            if m and m.group(1) in all_host_ids:
                if not scsi_host_ids:
                    scsi_host_ids = []
                if m.group(1) not in scsi_host_ids:
                    scsi_host_ids.append(m.group(1))

    if not scsi_host_ids:
        return None
    scsi_host_ids = libsan.misc.array.dedup(scsi_host_ids)
    return scsi_host_ids


def query_scsi_host_info(host_id):
    """Usage
        query_scsi_host_info(scsi_host_id)
    Purpose
        Save sysfs info of "/sys/class/scsi_host/host$scsi_host_id" to
            scsi_host_info
        We also check these folders:
            /sys/class/iscsi_host/host$scsi_host_id/
            /sys/class/fc_host/host$scsi_host_id/
    Parameter
        scsi_host_id           # like '0' for host0
    Returns
        scsi_host_info
            or
        None
    """
    if not host_id:
        _print("FAIL: query_scsi_host_info() - requires host_id")
        return None

    sysfs_folder = "/sys/class/scsi_host/host%s" % host_id
    if not os.path.isdir(sysfs_folder):
        _print("FAIL: %s is not a valid directory" % host_id)
        return None

    scsi_host_info = {}
    scsi_host_info["scsi_host_id"] = host_id
    scsi_host_info["pci_id"] = pci_id_of_host_id(host_id)
    scsi_host_info["driver"] = scsi_driver_of_host_id(host_id)

    param_files = [f for f in listdir(sysfs_folder) if os.path.isfile(os.path.join(sysfs_folder, f))]
    for param in param_files:
        ret, output = run(f"cat {sysfs_folder}/{param}", return_output=True, verbose=False)
        if ret != 0:
            # For some reason could not read the file
            continue
        scsi_host_info[param] = ", ".join(output.split("\n"))

    sysfs_hosts = ["/sys/class/iscsi_host/host%s" % host_id, "/sys/class/fc_host/host%s" % host_id]
    for sysfs_host in sysfs_hosts:
        if not os.path.isdir(sysfs_host):
            continue
        host_files = [x for x in listdir(sysfs_host) if os.path.isfile(os.path.join(sysfs_host, x))]
        for param in host_files:
            ret, output = run(f"cat {sysfs_host}/{param}", return_output=True, verbose=False)
            if ret != 0:
                # For some reason could not read the file
                continue
            scsi_host_info[param] = ", ".join(output.split("\n"))

    return scsi_host_info


def scsi_driver_of_host_id(host_id):
    """ """
    if not host_id:
        _print("FAIL: scsi_driver_of_host_id() - requires host_id parameter")
        return None
    scsi_drv_sysfs = "/sys/class/scsi_host/host%s/proc_name" % host_id
    if not os.path.isfile(scsi_drv_sysfs):
        _print("FAIL: %s is not a valid path" % scsi_drv_sysfs)
        return None

    _, output = run("cat %s" % scsi_drv_sysfs, return_output=True, verbose=False)
    scsi_driver = output
    if scsi_driver == "(null)" or scsi_driver == "":
        # Driver information was not exported, let try to find it out some other way
        lpfc_sysfs_file = "/sys/class/scsi_host/host%s/lpfc_drvr_version" % host_id
        if os.path.isfile(lpfc_sysfs_file):
            return "lpfc"

        driver_sysfs_file = "/sys/class/scsi_host/host%s/driver_name" % host_id
        if os.path.isfile(driver_sysfs_file):
            _, output = run("cat %s" % driver_sysfs_file, return_output=True, verbose=False)
            return output

        model_sysfs_file = "/sys/class/scsi_host/host%s/model_name" % host_id
        if os.path.isfile(model_sysfs_file):
            _, output = run("cat %s" % model_sysfs_file, return_output=True, verbose=False)
            if re.match("^QLE", output):
                return "qla2xxx"

        symbolic_sysfs_file = "/sys/class/fc_host/host%s/symbolic_name" % host_id
        if os.path.isfile(symbolic_sysfs_file):
            _, output = run("cat %s" % symbolic_sysfs_file, return_output=True, verbose=False)
            if re.search("bnx2fc", output):
                return "bnx2fc"

        pci_id = pci_id_of_host_id(host_id)
        if pci_id:
            lspci_regex = r"Kernel modules:\s+(\S+)"
            if not exists("lspci"):
                _print("FAIL: pciutils is not installed. Can't query driver name using pci_id.")
                return None
            _, output = run('lspci -s "%s" -v | grep "Kernel modules:"' % pci_id, return_output=True, verbose=False)
            if output:
                m = re.search(lspci_regex, output)
                if m:
                    return m.group(1)

        _print("FAIL: Could not get driver name for SCSI host%s" % host_id)
        return None
    else:
        # Intel cards export driver name as fcoe, but the network driver used is ixgbe for transport is ixgbe
        if scsi_driver == "fcoe":
            drv_version_path = "/sys/class/fc_host/host%s/driver_version" % host_id
            _, output = run("cat %s" % drv_version_path, return_output=True, verbose=False)
            if re.search("ixgbe", output):
                return "ixgbe"
    return scsi_driver


def pci_id_of_host_id(host_id):
    """Usage
        pci_id_of(scsi_host_id);
    Purpose
        Find out which PCI ID providing the SCSI Host via:
            readlink("/sys/class/scsi_host/host'scsi_host_id'");
    Parameter
        $scsi_host_id           # like '0' for host0
    Returns
        pci_id                 # like '0000:00:1c.0'
    """
    if not host_id:
        _print("FAIL: pci_id_of_host_id() - requires host_id parameter")
        return None
    sys_path = "/sys/class/scsi_host/host%s" % host_id
    if not os.path.exists(sys_path):
        _print("FAIL: %s is not a valid path" % sys_path)
        return None

    link_path = readlink(sys_path)

    regex_pci_id = libsan.host.linux.get_regex_pci_id()
    m = re.search(f"({regex_pci_id})/host{host_id}/scsi_host", link_path)
    # _print("DEBUG: pci_id_of_host_id - %s" % link_path)
    if m:
        return m.group(1)

    # for example ixgbe need to check the PCI ID from the network device
    # check for network interface name
    net_regex = re.compile(r"devices/virtual/net/(.*)\.")
    m = net_regex.search(link_path)
    if m:
        return libsan.host.net.get_pci_id_of_nic(m.group(1))

    return None


def rescan_host(host=None, verbose=True):
    """Rescan for devices for specific host
    If no host is given it will scan all SCSI hosts
    The arguments are:
    \tHost:      eg. 1 for host1
    Returns:
    \tTrue if no problem executing command
    \tFalse if something went wrong
    """
    host_list = []
    if host:
        host_list = [host]
    else:
        scsi_hosts = get_hosts()
        for host in scsi_hosts:
            host_list.append(host)

    error = 0

    if not host_list:
        _print("WARN: No host found on server to rescan")
        return True

    for host in host_list:
        if verbose:
            _print("INFO: Rescanning host%s" % host)
        cmd = f'echo "- - -" > {host_path}/host{host}/scan'
        retcode, _ = run(cmd, return_output=True, verbose=verbose)
        if retcode != 0:
            error += 1
            _print("FAIL: there was some problem scanning host%s" % host)

    if error:
        return False

    return True


def rescan_disk(scsi_disk=None):
    """Rescan an specific SCSI disk.
    If no disk is given, rescann all SCSI disks
    echo 1 > /sys/block/<scsi_disk>/device/rescan
    \tHost:      eg. 1 for host1
    Returns:
    \tTrue if no problem executing command
    \tFalse if something went wrong
    """
    scsi_disks = []

    if scsi_disk:
        scsi_disks = [scsi_disk]
    else:
        ids = get_scsi_disk_ids()
        for _id in ids:
            scsi_disks.append(get_scsi_disk_name(_id))

    error = 0
    for scsi_disk in scsi_disks:
        cmd = "echo 1 > /sys/block/%s/device/rescan" % scsi_disk
        retcode, _ = run(cmd, return_output=True)
        if retcode != 0:
            _print("FAIL: Could not rescan %s" % scsi_disk)
            error += 1

    if error:
        return False

    return True


def size_of_disk(scsi_disk):
    """
    Usage
        size_of_disk(disk)
    Purpose
        Given an scsi_disk name. Eg. sda
    Parameter
        scsi_disk
    Returns
        size in bytes

    """
    if not scsi_disk:
        return None

    logical_block_size = get_logical_block_size_of_disk(scsi_disk)

    if not logical_block_size:
        return None

    cmd = "cat /sys/block/%s/size" % scsi_disk
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        _print("FAIL: size_of_disk() - Could not get sector size for disk %s" % scsi_disk)
        print(output)
        return None
    if not output:
        return None

    sector_size = output

    return int(logical_block_size) * int(sector_size)


def get_logical_block_size_of_disk(scsi_disk):
    cmd = "cat /sys/block/%s/queue/logical_block_size" % scsi_disk
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        _print("FAIL: get_logical_block_size_of_disk() - Could not get logical block size for disk %s" % scsi_disk)
        print(output)
        return None
    if not output:
        return None
    return output


def get_physical_block_size_of_disk(scsi_disk):
    cmd = "cat /sys/block/%s/queue/physical_block_size" % scsi_disk
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        _print("FAIL: get_physical_block_size_of_disk() - Could not get size for disk %s" % scsi_disk)
        print(output)
        return None
    if not output:
        return None
    return output


def wwid_of_disk(scsi_disk=None):
    """Usage
        wwid_of_disk(disk)
    Purpose
        Given an scsi_disk name. Eg. sda, mpatha
    Parameter
        scsi_disk   device to get wwid for
    Returns
        wwid:       eg. 360fff19abdd9f5fb943525d45126ca27
    """
    if not scsi_disk:
        _print("FAIL: wwid_of_disk() - requires scsi_disk parameter")
        return None

    if libsan.host.linux.is_dm_device(scsi_disk):
        return libsan.host.linux.get_udev_property(scsi_disk, "DM_SERIAL")

    return libsan.host.linux.get_udev_property(scsi_disk, "ID_SERIAL")


def scsi_ids_of_wwid(wwid):
    """Usage
        scsi_ids_of_wwid(wwid)
    Purpose
        Find out all SCSI ID for WWID.
    Parameter
        wwid
    Returns
        scsi_ids
    """

    if not wwid:
        _print("FAIL: scsi_ids_of_wwid(): Got NULL input for WWID")
        return None

    scsi_ids = None
    all_scsi_info = query_all_scsi_disks()
    if not all_scsi_info:
        # Could not find any SCSI device
        return None

    for _id in list(all_scsi_info.keys()):
        if all_scsi_info[_id]["wwid"] == wwid:
            if not scsi_ids:
                scsi_ids = []
            scsi_ids.append(_id)
    if scsi_ids:
        scsi_ids = libsan.misc.array.dedup(scsi_ids)
    return scsi_ids


def wwn_of_disk(scsi_disk):
    """Usage
        wwn_of_disk(disk)
    Purpose
        Given an scsi_disk name. Eg. sda
    Parameter
        scsi_disk
    Returns
        wwid:       eg. 0x60a980003246694a412b45673342616e
    """
    if not scsi_disk:
        _print("FAIL: wwn_of_disk() - requires scsi_disk parameter")
        return None

    key_regex = "ID_WWN_WITH_EXTENSION=(.*)"

    # cmd = "udevadm info --name=%s --query=all" % scsi_disk
    # retcode, output = run(cmd, return_output=True, verbose=False)
    # if (retcode != 0):
    # #_print("FAIL: wwn_of_disk() - Could not query %s" % scsi_disk)
    # #print output

    # return None

    # udev_wwn = None
    # lines = output.split("\n")
    # for line in lines:
    # m = re.search(key_regex, line)
    # if m:
    # udev_wwn = m.group(1)

    cmd = "scsi_id --whitelisted --export /dev/%s" % scsi_disk
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        # _print("FAIL: wwn_of_disk() - Could not query %s" % scsi_disk)
        # print output
        return None

    lines = output.split("\n")
    for line in lines:
        m = re.search(key_regex, line)
        if m:
            scsi_wwn = m.group(1)
            return scsi_wwn

    # if udev_wwn and scsi_wwn:
    # if udev_wwn == scsi_wwn:
    # return udev_wwn
    # _print("udevadm WWN is %s" % udev_wwn)
    # _print("scsi_id WWN is %s" % scsi_wwn)
    # _print("FAIL: wwn_of_disk() - udevadm WWN and scsi_id WWN for %s do not match" % scsi_disk)
    # return None

    return None


def udev_wwn_of_disk(scsi_disk):
    """Usage
        udev_wwn_of_disk(disk)
    Purpose
        Given an scsi_disk name. Eg. sda
    Parameter
        scsi_disk
    Returns
        wwid:       eg. 0x60a980003246694a412b45673342616e
    """
    if not scsi_disk:
        _print("FAIL: udev_wwn_of_disk() - requires scsi_disk parameter")
        return None

    key_regex = "ID_WWN_WITH_EXTENSION=(.*)"

    cmd = "udevadm info --name=%s --query=all" % scsi_disk
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        # _print("FAIL: udev_wwn_of_disk() - Could not query %s" % scsi_disk)
        # print output
        return None

    lines = output.split("\n")
    for line in lines:
        m = re.search(key_regex, line)
        if m:
            udev_wwn = m.group(1)
            return udev_wwn
    return None


def query_scsi_driver_info(driver):
    """ """
    if not driver:
        _print("FAIL: query_scsi_driver_info() - requires driver parameter")
        return None

    all_scsi_host_ids = get_hosts()
    if not all_scsi_host_ids:
        _print("FAIL: query_scsi_driver_info() - Host does not have any SCSI host")
        return None

    driver_host_ids = []
    # Check which SCSI hosts are using the driver we want
    for host_id in all_scsi_host_ids:
        if scsi_driver_of_host_id(host_id) == driver:
            driver_host_ids.append(host_id)

    if not driver_host_ids:
        _print("FAIL:  No SCSI disk found from driver %s" % driver)
        return None

    scsi_driver_info = {}
    scsi_driver_info["scsi_host"] = {}
    for host_id in driver_host_ids:
        scsi_driver_info["scsi_host"][host_id] = query_scsi_host_info(host_id)

    scsi_driver_info["driver_name"] = driver
    # Add general driver info to this dict
    scsi_driver_info.update(libsan.host.linux.get_driver_info(driver))

    return scsi_driver_info


def get_free_disks(
    exclude_boot_device=True,
    exclude_lvm_device=True,
    exclude_mpath_device=True,
    exclude_md_device=True,
    exclude_partitioned_device=True,
    wipefs=False,
    filter_only=None,
):
    """Return a dict of free SCSI devices.
    By default it excludes devices used for boot, lvm or multipath
    Optional "filter_only" argument should be a dict. Eg. filter_only={'state': 'running'}
    Optional "wipefs" will call wipefs to remove ALL signatures(FS, partitions, VDO etc.) found on disks
    which are not used(will fail on mounted FS). This is useful to clean disks if tests are not cleaning up properly.
    """
    all_scsi_disks = query_all_scsi_disks()
    if not all_scsi_disks:
        # could not find any SCSI disk
        return None

    pvs = libsan.host.lvm.pv_query()
    md_devices = libsan.host.md.md_query()
    boot_dev = libsan.host.linux.get_boot_device()
    boot_wwid = None
    # if for some reason we boot from a single disk, but this disk is part of multipath device
    # the mpath device should be skipped as well
    if boot_dev:
        boot_wwid = libsan.host.linux.get_device_wwid(boot_dev)

    all_mp_info = None
    if (libsan.host.mp.is_multipathd_running()) and exclude_mpath_device:
        all_mp_info = libsan.host.mp.multipath_query_all()
        if all_mp_info and "by_wwid" not in list(all_mp_info.keys()):
            # Fail querying mpath, setting it back to None
            all_mp_info = None

    chosen_disks = {}
    for scsi_disk in list(all_scsi_disks.keys()):
        scsi_info = all_scsi_disks[scsi_disk]
        # Skip if mpath device is used for boot
        if boot_wwid == scsi_info["wwid"] and exclude_boot_device:
            _print("DEBUG: get_free_disks() - skip %s as it is used for boot" % scsi_info["name"])
            continue

        # Skip if disk is used by multipath
        if all_mp_info and scsi_info["wwid"] in list(all_mp_info["by_wwid"].keys()) and exclude_mpath_device:
            _print("DEBUG: get_free_disks() - skip %s as it is used for mpath" % scsi_info["name"])
            continue

        # Skip if it is used by Soft RAID
        if md_devices and exclude_md_device:
            used_by_md = False
            for md_dev in md_devices:
                storage_devs = libsan.host.md.md_get_storage_dev(md_dev)
                if not storage_devs:
                    continue
                for dev in storage_devs:
                    dev_wwid = wwid_of_disk(dev)
                    if not dev_wwid:
                        continue
                    if dev_wwid == scsi_info["wwid"]:
                        _print("DEBUG: get_free_disks() - skip %s as it is used for md" % scsi_info["name"])
                        used_by_md = True
                        continue
            if used_by_md:
                continue
        # Skip if filter_only is specified
        filtered = False
        if filter_only is not None:
            for key in filter_only:
                if scsi_info[key] != filter_only[key]:
                    _print(
                        "DEBUG: get_free_disks() - filtered {} as {} is not {}".format(
                            scsi_info["name"], key, filter_only[key]
                        )
                    )
                    filtered = True
                    continue
        if filtered:
            continue

        # Skip if it is used by LVM
        if pvs and exclude_lvm_device and "/dev/" + scsi_info["name"] in pvs:
            _print("DEBUG: get_free_disks() - skip %s as it is used for LVM" % scsi_info["name"])
            continue

        # skip size=0 disk, maybe it's usb-storage
        if "size" in scsi_info and scsi_info["size"] == 0:
            continue

        # Check partitions
        if libsan.host.linux.get_partitions("/dev/%s" % scsi_info["name"]):
            if exclude_partitioned_device and not wipefs:
                _print("DEBUG: get_free_disks() - skip %s as it has partitions" % scsi_info["name"])
                continue
            # clear any partitions and filesystems found on disk
            _print("DEBUG: Running wipefs on %s" % scsi_info["name"])
            ret, output = libsan.host.linux.wipefs("/dev/%s" % scsi_info["name"], wipe_all=True)
            if not ret:
                _print(f"DEBUG: get_free_disks() - skip {scsi_info['name']} wipefs failed")
                continue

        chosen_disks[scsi_info["name"]] = scsi_info
    return chosen_disks


def scsi_device_2_scsi_name(scsi_device):
    """Convert an specific SCSI device to scsi_name
    Eg. /dev/sdap1 => sda
    """
    scsi_dev_regex = r"/dev\/(sd.*)"
    m = re.match(scsi_dev_regex, scsi_device)
    if m:
        # remove partition if it has
        device_name = m.group(1)
        m = re.match(r"(.*)\d+", device_name)
        if m:
            device_name = m.group(1)
        return device_name
    # does not seem to be a valid SCSI device
    return None


def disk_sys_trigger(scsi_disk, action):
    """Usage
        disk_sys_trigger(scsi_disk, action)
    Purpose
        Bring disk online/offline, via
            /sys/block/sdX/device/state
        action cound be 'UP|DOWN|other', for UP, we change it into 'running'.
        for DOWN, we change it into 'offline'.
    Parameter
        scsi_disk      # like 'sda'
        action         # 'UP|DOWN|other'
    Returns
        True               # got expected /sys/block/sdX/device/state
            or
        False
    """
    if not scsi_disk or not action:
        _print("FAIL: disk_sys_trigger() - requires scsi_disk and action parameters")
        return False

    sys_path = "/sys/block/%s/device/state" % scsi_disk
    if not os.path.isfile(sys_path):
        _print("FAIL: No such file: %s" % sys_path)
        return False

    if action == "UP":
        action = "running"
    if action == "DOWN":
        action = "offline"

    cmd = f"echo {action} > {sys_path}"
    retcode, output = run(cmd, return_output=True, verbose=False)
    if retcode != 0:
        _print("FAIL: disk_sys_trigger() - Could not execute %s" % cmd)
        print(output)
        return None

    # cmd = "cat %s 2>/dev/null" % sys_path
    new_state = disk_sys_check(scsi_disk)
    if not new_state:
        _print("FAIL: disk_sys_trigger() - Could not get state of %s" % scsi_disk)
        return False

    if action != new_state:
        _print(f"FAIL: disk_sys_trigger() - Current state is '{new_state}' expected '{action}'")
        return False

    return True


def disk_sys_check(scsi_disk):
    """Usage
        disk_sys_check(scsi_disk)
    Purpose
        Check state of specific disk
            /sys/block/sdX/device/state
    Parameter
        scsi_disk      # like 'sda'
    Returns
        running/offline       # got expected /sys/block/sdX/device/state
            or
        None
    """
    if not scsi_disk:
        _print("FAIL: disk_sys_check() - requires scsi_disk parameter")
        return None

    sys_path = "/sys/block/%s/device/state" % scsi_disk
    if not os.path.isfile(sys_path):
        _print("FAIL: disk_sys_check() - No such file: %s" % sys_path)
        return None

    cmd = "cat %s 2>/dev/null" % sys_path
    _, state = run(cmd, return_output=True, verbose=False)
    if not state:
        _print("FAIL: disk_sys_check() - Could not read from %s" % sys_path)
        return None

    return state


def timeout_of_disk(scsi_disk):
    """Usage
        timeout_of_disk(scsi_disk)
    Purpose
        Check timeout of specific disk
            /sys/block/sdX/device/timeout
    Parameter
        scsi_disk      # like 'sda'
    Returns
        timeout in seconds       # got expected /sys/block/sdX/device/timeout
            or
        None
    """
    if not scsi_disk:
        _print("FAIL: timeout_of_disk() - requires scsi_disk parameter")
        return None

    sys_path = "/sys/block/%s/device/timeout" % scsi_disk
    if not os.path.isfile(sys_path):
        _print("FAIL: timeout_of_disk() - No such file: %s" % sys_path)
        return None

    cmd = "cat %s 2>/dev/null" % sys_path
    _, timeout = run(cmd, return_output=True, verbose=False)
    if not timeout:
        _print("FAIL: timeout_of_disk() - Could not read from %s" % sys_path)
        return None

    return timeout
