#!/usr/bin/python3
# PYTHON_ARGCOMPLETE_OK

import os
import argparse
import logging
import yaml
from abc import ABC, abstractmethod

try:
    import argcomplete
except Exception:
    pass


class Config:
    """
    Configuration off this app
    """

    def __init__(self):
        """
        Load configuration
        """
        with open("/etc/ferreus-orbit/config.yaml", 'r') as f:
            self.config = yaml.load(f, Loader=yaml.SafeLoader)

    def get(self, key):
        """
        Get a configuration key
        """
        return self.config[key]


class SystemTool(ABC):
    """
    Abstract class
    """

    def __init__(self):
        """
        Run run method in subclass and affect returned list to entries
        """
        self.entries = self.run()

    @abstractmethod
    def run(self) -> list:
        """
        Meant to be implemented in parent classes

        Returns:
            list
        """
        return []

    def get(self, **kwargs) -> dict:
        """
        Return one item from entries matching arbitrary keyword arguments

        Parameters:
            key=key_value

        Returns:
            disct: dict for entry key_value
        """
        lookup = []
        result = {}
        for item in self.entries:
            if any(item.get(key) == value for key, value in kwargs.items()):
                lookup.append(item)
        if not lookup:
            logger.error(f"{self.__class__.__name__} found no entry matching '{kwargs}'")
        elif len(lookup) > 1:
            logger.error(f"{self.__class__.__name__} found more than 1 entry matching '{kwargs}', skipping: {lookup}")
        else:
            result = lookup[0]
        return result


class FStab(SystemTool):
    """
    Class for retrieve informations of fstab file
    """

    def run(self) -> list:
        """
        Get informations of fstab file

        Returns:
            list: list of mountpoint
        """
        logger.info("Retrieve fstab infos")
        data = []
        with open("/etc/fstab", "r") as file:
            for line in file:
                fstab = {'commented': False}
                if line.startswith('#'):
                    line = line[1:]
                    fstab['commented'] = True
                fields = line.split()
                try:
                    if len(fields) == 6:
                        fstab['d'] = int(fields[4])
                        fstab['p'] = int(fields[5])
                        dev = fields[0].split('=')
                        if len(dev) == 1:
                            fstab['device'] = dev[0]
                        elif len(dev) == 2:
                            fstab['uuid'] = dev[0]
                            fstab['device'] = dev[1]
                        fstab['mountpoint'] = fields[1]
                        fstab['fs'] = fields[2]
                        fstab['options'] = fields[3].split(',')
                        data.append(fstab)
                except Exception:
                    pass
        return data

    def get(self, select: bool = False, **kwargs) -> dict:
        """
        Return one item from entries matching arbitrary keyword arguments

        Parameters:
            select (bool): if selection mode

        Returns:
            dict: dict of device
        """
        lookup = []
        result = {}
        for item in self.entries:
            if any(item.get(key) == value for key, value in kwargs.items()):
                lookup.append(item)
        if not lookup or len(lookup) > 1:
            logger.info(f"{self.__class__.__name__} found no entry matching '{kwargs}'or more than 1 entries found")
            if select:
                list_disks = {}
                for disk in self.entries:
                    if disk['commented']:
                        list_disks[f"{disk['mountpoint']} with old UUID: {disk['device']}"] = disk
                result = selectFromDict(list_disks, 'Mountpoint')
            else:
                result = lookup[LSblk.select]
                LSblk.select += 1
        else:
            result = lookup[0]
        return result

    def save(self):
        """
        Save FStab.entries to /etc/fstab
        And backup old fstab in /etc/fstab.bak

        Raises:
            Exception
        """
        subprocess.call(['cp', '/etc/fstab', '/etc/fstab.bak'])
        try:
            with open("/etc/fstab", "w") as file:
                file.write('# This file was modified by ferreus-orbit\n')
                file.write('# a backup file was create in /etc/fstab.bak\n')
                file.write('\n')
                for entry in self.entries:
                    comment = '#' if entry['commented'] else ''
                    device = f"{entry['uuid'].upper()}={entry['device']}" if 'uuid' in entry else entry['device']
                    options = ','.join(entry['options'])
                    file.write(f"{comment}{device}\t{entry['mountpoint']}\t{entry['fs']}\t{options}\t{entry['d']}\t{entry['p']}\n")
        except Exception as e:
            print("fstab file writing error, restore backup file")
            subprocess.call(['cp', '/etc/fstab.bak', '/etc/fstab'])
            raise Exception(e)


class LSblk(SystemTool):
    """
    Class for retrieve informations by lsblk command
    """
    select = 0

    def run(self) -> list:
        """
        Get informations by lsblk command

        Returns:
            list: list of device
        """
        logger.info("Retrieve lsblk infos")
        cmd = "lsblk --json --bytes -T --output MOUNTPOINT,UUID,PATH,SIZE,SERIAL,REV,WWN,VENDOR,MODEL".split()
        result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=3)
        disks = [disk for disk in json.loads(result.stdout).get("blockdevices") if 'children' not in disk and disk['size'] > 10000000000000]
        return disks

    def get(self, select: bool = False, **kwargs) -> dict:
        """
        Return one item from entries matching arbitrary keyword arguments

        Parameters:
            select (bool): if selection mode

        Returns:
            dict: dict of device
        """
        lookup = []
        result = {}
        for item in self.entries:
            if any(item.get(key) == value for key, value in kwargs.items()):
                lookup.append(item)
        if not lookup:
            logger.info(f"{self.__class__.__name__} found no entry matching '{kwargs}'")
        elif len(lookup) > 1:
            logger.warning(f"{self.__class__.__name__} found more than 1 disk found for '{kwargs}', result: {lookup}")
            if select:
                list_disks = {}
                for disk in lookup:
                    list_disks[f"{disk['path']}: {disk['vendor']} {disk['model']}, Serial {disk['serial']}"] = disk
                result = selectFromDict(list_disks, 'Disk')
            else:
                result = lookup[LSblk.select]
                LSblk.select += 1
        else:
            result = lookup[0]
        return result


class LShw(SystemTool):
    """
    Class for retrieve informations by lshw command
    """
    select = 0

    def run(self) -> list:
        """
        Get informations by lshw command

        Returns:
            list: list of device
        """
        logger.info("Retrieve lsw infos")
        cmd = "lshw -c disk -json".split()
        result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
        hws = json.loads(result.stdout)
        disks = []
        for hw in hws:
            disk = dict((k, hw[k]) for k in ['vendor', 'product', 'serial', 'size', 'version'] if k in hw)
            names = hw['logicalname'] if isinstance(hw['logicalname'], list) else [hw['logicalname']]
            for name in names:
                if name.startswith('/dev'):
                    disk['device'] = name
                elif name.startswith('/srv/node'):
                    disk['mountpoint'] = name
            disks.append(disk)
        return disks

    def get(self, select: bool = False, **kwargs) -> dict:
        """
        Return one item from entries matching arbitrary keyword arguments

        Parameters:
            select (bool): if selection mode

        Returns:
            dict: dict of device
        """
        lookup = []
        result = {}
        for item in self.entries:
            if any(item.get(key) == value for key, value in kwargs.items()):
                lookup.append(item)
        if not lookup:
            logger.info(f"{self.__class__.__name__} found no entry matching '{kwargs}'")
        elif len(lookup) > 1:
            logger.warning(f"{self.__class__.__name__} found more than 1 disk found for '{kwargs}', result: {lookup}")
        else:
            result = lookup[0]
        return result


class Raid(SystemTool):
    """
    Class for retrieve informations by lsblk command
    """

    def run(self) -> list:
        """
        Get informations by raid (storcli64 or ssacli) command

        Returns:
            list: list of drive
        """
        logger.info("Retrieve Storcli infos")
        # Retrieve Controller
        cmd = ["storcli64", "/c0", "show", "J"]
        try:
            result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
            data = json.loads(result.stdout)
            self.raid_card = data['Controllers'][0]['Response Data']['Product Name']
        except Exception as e:
            logger.error(f"Storcli to retrieve controllers error: {e}")
            logger.error(result.stderr)
            return self.load_ssacli()
        return self.load_storcli()

    def load_storcli(self) -> list:
        """
        Get informations by storcli64 command

        Returns:
            list: list of drive
        """
        raid_cards = conf.get('raid_cards')
        if self.raid_card in raid_cards and 'path_all' in raid_cards[self.raid_card]:
            raid_path_all = raid_cards[self.raid_card]['path_all']
        else:
            raid_path_all = '/call/eall/sall'
        # Retrieve informations off all slot
        cmd = ["storcli64", raid_path_all, "show", "all", "J"]
        try:
            result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
            self.data_all = json.loads(result.stdout)
        except Exception as e:
            logger.critical(f"Storcli to retrieve all infos error: {e}")
            logger.error(result.stderr)
            return []
        # Retrtieve informations of virtual devices
        cmd = ["storcli64", "/call/vall", "show", "all", "J"]
        vd = {}
        try:
            result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
            data = json.loads(result.stdout)
            for controller in data['Controllers']:
                ctl = controller['Command Status']['Controller']
                response_data = controller['Response Data']
                for key, value in response_data.items():
                    if key.startswith('/c'):
                        vd[key.replace(f"/c{ctl}/v", '')] = {}
                    elif key.startswith('PDs for VD '):
                        slot = f"/c{ctl}/e{value[0]['EID:Slt'].replace(':', '/s')}"
                        vd[key.replace('PDs for VD ', '')]['slot'] = slot
                    elif key.startswith('VD'):
                        vdid = key.split()[0].replace('VD', '')
                        vd[vdid]['wwn'] = f"0x{value['SCSI NAA Id']}"
        except Exception as e:
            logger.warning(f"Storcli to retrieve VD error : {e}")
            logger.warning(result.stderr)

        self.raid0_wwn = {}
        for value in vd.values():
            self.raid0_wwn[value['slot']] = value['wwn']
        drives = []
        for controller in self.data_all['Controllers']:
            response_data = controller['Response Data']
            iterator = iter(response_data.items())
            for key, value in iterator:
                keys = key.split()
                if f"{keys[0]} {keys[1]}" == key:
                    slot = keys[1]
                    state = value[0]["State"]
                    key, value = next(iterator)
                    serial = value[f"Drive {slot} Device attributes"]['SN']
                    vendor = value[f"Drive {slot} Device attributes"]['Manufacturer Id']
                    model = value[f"Drive {slot} Device attributes"]['Model Number']
                    rev = value[f"Drive {slot} Device attributes"]['Firmware Revision']
                    if slot in self.raid0_wwn:
                        wwn = self.raid0_wwn[slot]
                        type = "RAID0"
                    else:
                        wwn = value[f"Drive {slot} Device attributes"]['WWN']
                        raid_cards = conf.get('raid_cards')
                        if self.raid_card in raid_cards and 'transform_wwn' in raid_cards[self.raid_card]:
                            iwwn = int(wwn, 16)
                            iwwn = iwwn - raid_cards[self.raid_card]['transform_wwn']
                            wwn = hex(iwwn).lower()
                        else:
                            wwn = f"0x{wwn.lower()}"
                        type = "JBOD"
                    drives.append({'slot': slot,
                                   'serial': serial,
                                   'vendor': vendor,
                                   'model': model,
                                   'rev': rev,
                                   'wwn': wwn,
                                   'state': state,
                                   'type': type
                                   })
        return drives

    def load_ssacli(self) -> list:
        """
        Get informations by raid ssacli command

        Returns:
            list: list of drive
        """
        drives = []
        try:
            cmd = ["ssacli", "ctrl", "all", "show", "detail"]
            result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
            for line in result.stdout.split('\n'):
                elem = line.split(':')
                if elem[0].strip().lower() == 'slot':
                    self.ctrl_slot = elem[1].strip()
            self.raid_card = "ssacli"
            cmd = ["ssacli", "ctrl", f"slot={self.ctrl_slot}", 'pd', 'all', "show", "detail"]
            result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
            slot, serial, vendor, model, rev, wwn, state, type = '', '', '', '', '', '', '', ''
            for line in result.stdout.split('\n'):
                if line.strip().startswith("physicaldrive"):
                    if slot != '':
                        drives.append({'slot': slot,
                                       'serial': serial,
                                       'vendor': vendor,
                                       'model': model,
                                       'rev': rev,
                                       'wwn': f"0x{wwn.lower()}",
                                       'state': state,
                                       'type': type
                                       })
                        slot, serial, vendor, model, rev, wwn, state, type = '', '', '', '', '', '', '', ''
                    slot = line.split()[1]
                else:
                    elem = line.split(':')
                    if len(elem) > 1:
                        key = elem[0].strip().lower()
                        value = elem[1].strip()
                        if key == "status":
                            state = value
                        elif key == "serial number":
                            serial = value
                        elif key == "model":
                            values = value.split()
                            vendor = values[0].strip()
                            model = values[1].strip()
                        elif key == "firmware revision":
                            rev = value
                        elif key == "wwid":
                            wwn = value
            drives.append({'slot': slot,
                           'serial': serial,
                           'vendor': vendor,
                           'model': model,
                           'rev': rev,
                           'wwn': f"0x{wwn.lower()}",
                           'state': state,
                           'type': type
                           })
            return drives
        except Exception as e:
            logger.error(f"ssacli to retrieve controllers error: {e}")
            logger.error(result.stderr)
            return []

    def update_firmware(self, path: str, fw: str, mode: str = '') -> bool:
        """
        Parameters:
            fw (str): filename of firmware
            mode (str): mode of update (see storcli documentation)

        Returns:
            bool: True if update is OK
        """
        if self.raid_card == 'ssacli':
            return False
        txt = "Normal mode" if mode else f"mode {mode}"
        logger.warning(f"Try update firmware with storcli in {txt}")
        cmd = ['storcli64',
               path,
               'download',
               f"src=\"{conf.get('fw_dir')}/{fw}\""]
        if mode:
            cmd.append(f"mode={mode}")
        try:
            subprocess.run(cmd,
                           stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL,
                           capture_output=False,
                           check=True, timeout=30)
            print("Update of Firmware OK")
            return True
        except Exception as e:
            logger.error(f"Update firmware for {path} in {txt} failed !")
            logger.error(e)
            return False

    def configure_raid0(self, path: str) -> bool:
        """
        Configure a VD in Raid 0 with the physical drive in controller, enclosure and slot

        Parameters:
            path (str): storcli path of device to configure

        Returns:
            bool: True Raid correctly created
        """
        if self.raid_card == 'ssacli':
            return False
        controller = ''
        enclosure = ''
        slot = ''
        for i in path.split('/'):
            if i.startswith('c'):
                controller = i[1:]
            elif i.startswith('e'):
                enclosure = i[1:]
            elif i.startswith('s'):
                slot = i[1:]
        cmd = ["storcli64", f"/c{controller}/vall", "delete", "preservedCache"]
        subprocess.run(cmd, timeout=30)
        cmd = ["storcli64", path, "show", "J"]
        try:
            result = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=30)
            data = json.loads(result.stdout)
            state = data['Controllers'][0]['Response Data']['Drive Information'][0]["State"]
            if state == 'UGood':
                cmd = ["storcli64", f"{path}", "set", "good", "force"]
                subprocess.run(cmd, timeout=30)
                cmd = ["storcli64", f"/c{controller}", "add", "vd", "r0", f"drives=\"{enclosure}:{slot}\"", "wb", "ra"]
                try:
                    subprocess.run(cmd, check=True, timeout=30)
                except Exception:
                    cmd = ["storcli64", f"/c{controller}", "add", "vd", "r0", f"drives=\"{enclosure}:{slot}\"", "wb", "ra"]
                    try:
                        subprocess.run(cmd, check=True, timeout=30)
                        return True
                    except Exception as e:
                        print(f"Configure Raid0 for physical drive {path} failed")
                        logger.error(e)
            else:
                print(f"Physical drive {path} not in UGood state")
                return False
        except Exception as e:
            print(e)
            logger.error(e)
            return False

    def get_state(self, path: str) -> str:
        """
        Get state of device

        Parameters:
            path (str): storcli path of device to configure

        Returns:
            str: State of device
        """
        return self.get(slot=path)

    def locate(self, path: str, locate: bool):
        """
        Locate or unlocate a device

        Parameters:
            path (str): raid path of device to configure
            locate: start or stop locate
        """
        if self.raid_card == 'ssacli':
            cmd = ['ssacli', 'ctrl', f"slot={self.ctrl_slot}", 'pd', path, 'modify', f"led={'on' if locate else 'off'}"]
        else:
            cmd = ["storcli64", self.slot, 'start' if locate else 'stop', 'locate']
        try:
            subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except Exception as e:
            logger.warning(f"Locate error: {e}")


class SwiftRecon:
    """
    Class for retrieve informations with swift recon request
    """

    def __init__(self):
        """
        Get informations with a swift recon request
        """
        hostname = socket.getfqdn()
        self._recon = f"http://{hostname}:6200/recon"
        self.unmounted = self.get_unmounted()
        self.devices = self.get_devices()

    def get_unmounted(self) -> list:
        """
        Get unmounted swift device

        Returns:
            list: list of unmounted swift device
        """
        response = requests.get(f"{self._recon}/unmounted", timeout=3)
        response.raise_for_status()
        return [x["device"] for x in response.json() if x["mounted"] is False]

    def get_devices(self) -> list:
        """
        Get list of swift device

        Return:
            list: list of swift device
        """
        response = requests.get(f"{self._recon}/devices", timeout=3)
        response.raise_for_status()
        data = response.json()
        if len(data) > 1:
            logger.warning("more than 1 key in response")
        if conf.get('mount_dir') not in data:
            logger.warning(f"{conf.get('mount_dir')} not in response")
        return sorted(data[conf.get('mount_dir')])


class Disk(dict):
    """
    Class for control disk
    """
    field_names = [
        'mountpoint',
        'device',
        'size',
        'vendor',
        'model',
        'fw',
        'slot',
        'serial',
        'uuid',
        'wwn',
        'state',
        'mounted']
    list = []
    fstab = None
    lsblk = None
    lshw = None
    raid = None
    recon = None

    def __init__(self, *args, **kwargs):
        """
        Init a disk with default value and update it with *args if necessary
        """
        default = {}
        for f in [d for d in Disk.field_names if d != 'mounted']:
            default[f] = ''
        default['mounted'] = False
        default.update(*args)
        super().__init__(default, **kwargs)
        self.__dict__ = self
        Disk.list.append(self)

    def get_fildered(self) -> dict:
        """
        Return a dict with filtered keys

        Returns:
            dict: dict of keys filtered
        """
        return {key: value for (key, value) in dict(self).items() if key in Disk.field_names}

    def print(self, format: str = None):
        """
        Print disk informations

        Parameters:
            format (str): Output format
        """
        colors = conf.get('colors')
        out = self.get_fildered()
        if format == 'json':
            print(json.dumps(out, indent=4))
        elif format == 'yaml':
            print(yaml.dump(out, sort_keys=False))
        elif format == 'csv':
            writer = csv.DictWriter(sys.stdout, fieldnames=Disk.field_names)
            writer.writeheader()
            writer.writerows([out])
        elif format == 'textile':
            print()
            for key, value in out.items():
                print(f"|{key.upper()}|{value}|")
            print()
        else:
            if not self.mounted:
                print(f"{colors['term']['red']}")
            for key, value in out.items():
                print(f"{key.upper()}:\t{value}".expandtabs(12))
            print(f"{colors['term']['neutral']}")

    def locate(self, format: str = None):
        """
        Active disk location LED and print "Locate ON"

        Parameters:
            format (str): Output format
        """
        if format == 'textile':
            print("*_Locate ON_*")
        elif format is None:
            print("Locate ON")
        if self.slot:
            if Disk.raid is None:
                Disk.raid = Raid()
            Disk.raid.locate(self.slot, True)
        else:
            cmd = ["ledctl", f"locate={self.device}"]
            try:
                subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            except Exception as e:
                logger.warning(f"Locate error: {e}")

    def unlocate(self, format: str = None):
        """
        Unactive disk location LED and print "Locate OFF"

        Parameters:
            format (str): Output format
        """
        if format == 'textile':
            print("*_Locate OFF-*")
        elif format is None:
            print("Locate OFF")
        if self.slot:
            if Disk.raid is None:
                Disk.raid = Raid()
            Disk.raid.locate(self.slot, False)
        else:
            cmd = ["ledctl", f"locate_off={self.device}"]
            try:
                subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            except Exception as e:
                logger.warning(f"Unlocate error: {e}")

    def load(self):
        """
        Load disk informations
        """
        lsblk_info = None
        if not self.mounted:
            logger.warning(f"{self.mountpoint} is unmounted")
        else:
            lsblk_info = Disk.lsblk.get(mountpoint=self.mountpoint)
        fstab_info = Disk.fstab.get(mountpoint=self.mountpoint)
        if not fstab_info:
            logger.warning(f"{self.mountpoint} not found in fstab")
        self.uuid = fstab_info.get("device")
        if not lsblk_info and not self.mounted:
            logger.warning(f"{self.mountpoint} is mounted according to swift but could not find it with lsblk")
        elif not lsblk_info and self.uuid:
            lsblk_info = Disk.lsblk.get(uuid=self.uuid)
        if not lsblk_info:
            logger.warning(f"{self.mountpoint} could not find any matching block device")
            lsblk_info = Disk.lsblk.get(mountpoint=None)
        self.device = lsblk_info.get("path")
        lshw_info = Disk.lshw.get(device=self.device)
        if lshw_info:
            self.size = bytes_to_human_readable(int(lshw_info.get("size"))) if lshw_info.get("size") else None
            self.serial = lshw_info.get("serial")
            self.fw = lshw_info.get("version")
            self.vendor = lshw_info.get("vendor")
            self.model = lshw_info.get("product")
        else:
            self.size = bytes_to_human_readable(int(lsblk_info.get("size"))) if lsblk_info.get("size") else None
            self.serial = lsblk_info.get("serial")
            self.fw = lsblk_info.get("rev")
            self.vendor = lsblk_info.get("vendor")
            self.model = lsblk_info.get("model")
        self.wwn = lsblk_info.get("wwn")
        self.type = "Direct"
        self.slot = ""
        self.state = ""
        if self.wwn and len(Disk.raid.entries) > 0:
            raid_info = Disk.raid.get(wwn=self.wwn)
            if not len(raid_info):
                raid_info = Disk.raid.get(serial=self.serial)
            if len(raid_info):
                self.slot = raid_info['slot']
                self.vendor = raid_info['vendor'].strip()
                self.model = raid_info['model'].strip()
                self.fw = raid_info['rev']
                self.serial = raid_info['serial']
                self.state = f"{raid_info['type']} {raid_info['state']}"
                self.wwn = raid_info['wwn']

    def compare_fw_version(self) -> (int, str):
        """
        Compare disk firmware revbision with minimal revision needed

        Returns:
            int: 0 if firmware is OK, non-zero if update firmware is needed
            str: filename of firmware
        """
        Disk.lsblk = LSblk()
        firmware = Disk.lsblk.get(path=self.device, select=True)
        self.device = firmware['path']
        with open(conf.get('fw_conf_file')) as f:
            for line in f:
                elem = line.strip().split(',')
                if firmware['model'] == elem[0]:
                    result = subprocess.run(['dpkg', '--compare-versions', firmware['rev'], 'lt', elem[2]],
                                            stdout=subprocess.DEVNULL,
                                            stderr=subprocess.DEVNULL,
                                            capture_output=False,
                                            timeout=3)
                    return result.returncode, elem[3]
        return -1, None

    def update_fw_by_OpenSeaChest(self, fw: str) -> bool:
        """
        Update firmware with OpenSeaChest

        Parameters:
            fw (str): filename of firmware

        Returns:
            bool: True if update is OK
        """
        logger.warning("Try update firmware with openSeaChest_Firmware")
        result = subprocess.run(['openSeaChest_Firmware',
                                 '-d',
                                 self.device,
                                 '--downloadFW',
                                 f"{conf.get('fw_dir')}/{fw}"],
                                stdout=subprocess.DEVNULL,
                                stderr=subprocess.DEVNULL,
                                capture_output=False,
                                timeout=30)
        if result.returncode in [0, 32, 33]:
            print("Update of Firmware OK")
            return True
        else:
            logger.warning("Update with openSeaChest_Firmware failed !")
            logger.warning(result.stderr)
            return False

    def update_fw_by_storcli(self, fw: str, mode: str = '') -> bool:
        """
        Update firmware with storcli64

        Parameters:
            fw (str): filename of firmware
            mode (str): mode of update (see storcli documentation)

        Returns:
            bool: True if update is OK
        """
        return Disk.raid.update_firmware(self.path, fw, mode)

    def update_fw(self):
        """
        Update firmware
        """
        print(f"Update {self.device} firmware...")
        ret, fw = self.compare_fw_version()
        if ret != 0:
            print('Firmware version OK')
        else:
            if not self.update_fw_by_OpenSeaChest(fw):
                if not self.update_fw_by_strocli(fw):
                    if not self.update_fw_by_strocli(fw, mode=5):
                        logger.error(f"Update {self.device} firmware failed !")

    def configure_raid0(self) -> bool:
        """
        Configure a Raid0 virtual drive with a phisical Drive

        Returns:
            bool: True if VD correctly created
        """
        if "RAID0" in self.state.upper():
            return Disk.raid.configure_raid0(self.path)
        return True

    def get_uuid(self) -> str:
        """
        Get UUID of a device

        Returns:
            str: Disk UUID
        """
        logger.info(f"Get new UUID of {self.device}")
        Disk.lsblk = LSblk()
        return Disk.lsblk.get(path=self.device)['uuid']

    def mkfs(self) -> bool:
        """
        Create FS with mkfs.xfs

        Returns:
            bool: True if FS as correctly created
        """
        print(f"Format {self.device}...")
        result = subprocess.run(['mkfs.xfs',
                                 '-f',
                                 self.device],
                                capture_output=True,
                                timeout=60)
        if result.returncode == 0:
            print("Disk formatting in XFS")
            time.sleep(5)
            return True
        else:
            logger.error(f"Format of {self.device} failed !")
            logger.error(result.stderr)
            return False

    def modif_fstab(self, new_uuid: str) -> bool:
        """
        Modify fstab

        Parameters:
            new_uuid (str): new UUID of device

        Returns:
            bool: True if correctly changed
        """
        print(f"Uncomment and change UUID in fstab, {self.uuid} to {new_uuid}")
        Disk.fstab = FStab()
        fstab_entry = Disk.fstab.get(device=self.uuid, select=True)
        fstab_entry['commented'] = False
        fstab_entry['device'] = new_uuid
        Disk.fstab.save()
        try:
            subprocess.run(['systemctl', 'daemon-reload'],
                           stderr=subprocess.DEVNULL,
                           timeout=3)
            time .sleep(1)
            return True
        except Exception as e:
            logger.error(e)
            return False

    def format(self):
        """
        Format Disk and modify fstab
        """
        if self.mkfs():
            self.modif_fstab(self.get_uuid())

    def mounting(self) -> bool:
        """
        Mount a disk

        Returns:
            bool: True if disk correctly mounting
        """
        print(f"Mounting {self.mountpoint}")
        result = subprocess.run(['mount', self.mountpoint],
                                capture_output=True,
                                timeout=10)
        if result.returncode == 0:
            return self.wait_mounted()
        else:
            logger.error(f"Mount of {self.mountpoint} failed !")
            logger.error(result.stderr)
            return False

    def wait_mounted(self, timeout: int = 60) -> bool:
        """
        Wait for disk effectively mounted

        Parameters:
            timeout (int): timeout in seconds

        Returns:
            bool: True if disk correctly mounted, False if timeout reached
        """
        cpt = 0
        while not os.path.ismount(self.mountpoint):
            cpt += 1
            time.sleep(1)
            if cpt > timeout:
                print(f"{self.mountpoint} not effectively mounted !")
                logger.error(f"{self.mountpoint} not effectively mounted !")
                return False
        print(f"{self.mountpoint} mounted")
        return True

    def change_owner(self) -> bool:
        """
        Change owner of a mountpoint

        Returns:
            bool: True if change owner is OK
        """
        print(f"Changing disk's owner of {self.mountpoint}")
        uid = pwd.getpwnam('swift')
        try:
            os.chown(self.mountpoint, uid.pw_uid, uid.pw_gid)
            print(f"Owner of {self.mountpoint} is swift:swift")
            stat = os.stat(self.mountpoint)
            if stat.st_uid == uid.pw_uid and stat.st_gid == uid.pw_gid:
                return True
            else:
                print(f"Changing disk's owner of {self.mountpoint} failed !")
                logger.error(f"Changing disk's owner of {self.mountpoint} failed !")
        except Exception as e:
            print(f"Changing disk's owner of {self.mountpoint} failed !")
            logger.error(e)

    def mount(self):
        """
        Mount a disk, if mount and change owner are OK, reload disk informations and save Disks list
        """
        if self.mounting():
            if self.change_owner():
                clear_kern_legacy()
                print(f"Disk {self.mountpoint} correctly replace !")
            Disk.init_cache()
            self.load()
            Disk.save()

    def replace(self):
        """
        Replace a disk and stop blinking LED locate disk
        """
        ret = True
        if not args.no_update_firmware:
            self.update_fw()
            ret = self.configure_raid0()
        if not args.no_format and ret:
            self.format()
        if not args.no_mount and ret:
            self.mount()
        self.unlocate()

    @classmethod
    def get_by_key(cls, **kwargs) -> list:
        """
        Get a disk by a property

        returns:
            list: list of disks
        """
        lookup = []
        for item in Disk.list:
            if any(item.get(key) == value for key, value in kwargs.items()):
                lookup.append(item)
        if not lookup:
            return [Disk(**kwargs)]
        return lookup

    @classmethod
    def get_list_filtered(cls):
        """
        Get a list of disks filtered
        """
        return [disk.get_fildered() for disk in Disk.list]

    @classmethod
    def init_cache(cls):
        """
        Initialize cache
        """
        Disk.fstab = FStab()
        Disk.lsblk = LSblk()
        Disk.lshw = LShw()
        Disk.raid = Raid()
        Disk.recon = SwiftRecon()

    @classmethod
    def load_from_file(cls, refresh: bool = False):
        """
        Load information from a json file

        Parameters:
            refresh (bool): If it's necessary to refresh informations
        """
        Disk.recon = SwiftRecon()
        if os.path.exists(conf.get('disks_info_file')):
            m_time = os.path.getmtime(conf.get('disks_info_file'))
            dt_m = datetime.datetime.fromtimestamp(m_time)
            logger.info(f"Using cache file {conf.get('disks_info_file')} modified {dt_m}")
            try:
                with open(conf.get('disks_info_file'), 'r') as f:
                    disks = json.load(f)
                    for disk in disks:
                        d = Disk(disk)
                        d.mounted = d.mountpoint.split('/')[-1] not in Disk.recon.unmounted
                        if not d.mounted:
                            if Disk.raid is None:
                                Disk.raid = Raid()
                            d.state = Disk.raid.get_state(d.slot)
            except Exception:
                Disk.load_all()
                return
            if refresh:
                Disk.load_all()
        else:
            Disk.load_all()
            return

    @classmethod
    def load_all(cls):
        """
        Load all disks informations
        """
        Disk.init_cache()
        logger.info("swift devices:", Disk.recon.devices)
        logger.info("swift unmounted:", Disk.recon.unmounted)
        for swift_device in Disk.recon.devices:
            mountpoint = f"{conf.get('mount_dir')}/{swift_device}"
            mounted = swift_device not in Disk.recon.unmounted
            disk = Disk.get_by_key(mountpoint=mountpoint)[0]
            disk.mounted = mounted
            disk.load()
        Disk.save()

    @classmethod
    def print_all(cls, format: str = None):
        """
        Print all disks informations

        Parameters:
            format (str): format of output
        """
        colors = conf.get('colors')
        if len(Disk.list) == 0:
            logger.warning("No disks found")
            return
        if 'mountpoint' not in Disk.field_names:
            Disk.field_names.insert(0, 'mountpoint')
        if 'mounted' not in Disk.field_names:
            Disk.field_names.append('mounted')
        disks = Disk.get_list_filtered()
        if format == 'json':
            print(json.dumps(disks, indent=4))
        elif format == 'yaml':
            print(yaml.dump(disks, sort_keys=False))
        elif format == 'csv':
            writer = csv.DictWriter(sys.stdout, fieldnames=Disk.field_names)
            writer.writeheader()
            writer.writerows(disks)
        elif format == 'textile':
            fn = Disk.field_names.copy()
            any_unmounted = 0
            for f in fn:
                print(f"|_.{f.upper()}", end='')
            print('|')
            for disk in disks:
                if not disk['mounted']:
                    any_unmounted += 1
                    print(f"{colors['textile']['red']}", end='')
                for f in fn:
                    print(f"|{disk[f]}", end='')
                print('|')
            if any_unmounted > 1:
                print("WARNING : for disks unmounted (RED), mapping between device and mountpoint isn't sure !")
        else:
            fn = [word.upper() for word in Disk.field_names]
            any_unmounted = 0
            ptable = prettytable.PrettyTable()
            ptable.field_names = fn
            for disk in disks:
                if not disk['mounted']:
                    any_unmounted += 1
                    row = {key: f"{colors['term']['red']}{value}{colors['term']['neutral']}" for key, value in disk.items()}
                else:
                    row = disk
                ptable.add_row(row.values())
                try:
                    ptable.set_style(prettytable.PLAIN_COLUMNS)
                except Exception:
                    pass
            print(ptable.get_string(sortby='MOUNTPOINT', fields=fn))
            if any_unmounted > 1:
                print("WARNING : for disks unmounted (RED), mapping between device and mountpoint isn't sure !")

    @classmethod
    def save(cls):
        """
        Save disks informations to a json file
        """
        with open(conf.get('disks_info_file'), 'w') as f:
            json.dump(Disk.list, f, indent=4)


def selectFromDict(options: dict, name: str) -> object:
    """
    Load information from a json file

    Parameters:
        options (dict): dict of options
        name (str): Description of selection

    Returns:
        object: the selection
    """
    index = 0
    indexValidList = []
    print('Select a ' + name + ':')
    for optionName in options:
        index = index + 1
        indexValidList.extend([options[optionName]])
        print(str(index) + ') ' + optionName)
    inputValid = False
    while not inputValid:
        inputRaw = input(name + ': ')
        inputNo = int(inputRaw) - 1
        if inputNo > -1 and inputNo < len(indexValidList):
            selected = indexValidList[inputNo]
            str_selected = str(selected)
            print(f"Selected {name}: {str_selected}")
            inputValid = True
            break
        else:
            print('Please select a valid ' + name + ' number')
    return selected


def bytes_to_human_readable(bytes: int) -> str:
    """
    Transform bytes to human readable size

    Parameters:
        bytes (int): the size in bytes

    Returns:
        str: human readable size
    """
    if not isinstance(bytes, int) or bytes <= 0:
        raise ValueError("input must be a positive integer")
    units = ["B", "KB", "MB", "GB", "TB", "PB"]
    index = 0
    size = bytes
    while size >= 1000 and index < len(units) - 1:
        size /= 1000
        index += 1
    return f"{int(size)}{units[index]}"


def clear_kern_legacy():
    """
    Clean the kernel legacy log
    """
    if os.path.exists("/var/log/kern-legacy.log"):
        with open("/var/log/kern-legacy.log", 'w') as f:
            f.write('')


def test_filter(string: str) -> str:
    """
    Test validity of a filter argument

    Parameters:
        string (str): argument to test

    Returns:
        str: valid arguments

    Raises:
        argparse.ArgumentTypeError: raise error if isn't a valid argument
    """
    path = os.path.join('/', 'srv', 'node', string)
    if os.path.isdir(path) or os.path.exists(string) or len(string.split('=')) == 2:
        return string
    else:
        raise argparse.ArgumentTypeError(f"Filter {string} not valid !")


def parse_args() -> argparse.Namespace:
    """
    Prepare arguments off application

    Returns:
        argparse.Namespace: the namespace off this argument parser
    """
    parser = argparse.ArgumentParser(description="Utility to retrieve information from physical disks")
    parser.add_argument("-v", "--verbose", action="count", default=0, help="Number of repeat increase log level")
    parser.add_argument('-f', '--format', choices=['json', 'yaml', 'textile', 'csv'], help='Output format (textile is for redmine)')
    parser.add_argument('-r', '--refresh', action='store_true', help='Import from file')
    subparsers = parser.add_subparsers(help="Sub-command help", required=True, dest='action')
    parser_list = subparsers.add_parser('list', help="List disk info")
    parser_list.set_defaults(func=fo_list)
    parser_list.add_argument('-o', '--output', help=f"Fields to output ({','.join(Disk.field_names)}), separate by comma. 'mountpoint' and 'mounted automatically added")
    parser_info = subparsers.add_parser('info', help="Display information about one disk")
    parser_info.set_defaults(func=fo_info)
    group = parser_info.add_mutually_exclusive_group()
    group.add_argument('-l', '--locate', action='store_true', help='Locate (start blinking) disk')
    group.add_argument('-u', '--unlocate', action='store_true', help='Unlocate (stop blinking) disk')
    parser_info.add_argument('filter', type=test_filter, help=f"Filter by mountpoint (without {conf.get('mount_dir')}/), device(/dev/sdX) or by key=value")
    parser_info.add_argument('-o', '--output', help=f"Fields to output ({','.join(Disk.field_names)}), separate by comma")
    parser_replace = subparsers.add_parser('replace', help="Replace a disk")
    parser_replace.set_defaults(func=fo_replace)
    parser_replace.add_argument('filter', type=test_filter, help=f"Filter by mountpoint (without {conf.get('mount_dir')}/), device(/dev/sdX) or by key=value")
    parser_replace.add_argument('-U', '--no-update_firmware', action='store_true', help='Don\'t update firmware of disk')
    parser_replace.add_argument('-F', '--no-format', action='store_true', help='Don\'t format disk in Xfs')
    parser_replace.add_argument('-M', '--no-mount', action='store_true', help='Don\'t modify fstab and mount disk')
    try:
        argcomplete.autocomplete(parser)
    except Exception:
        pass
    return parser.parse_args()


def setup_logging(verbosity) -> logging.Logger:
    """
    Configure the logger

    Returns:
        logging.Logger: the logger
    """
    format = "%(levelname)s: %(message)s"
    verbosity = verbosity if verbosity < 5 else 4
    level = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.FATAL]
    level.reverse()
    logging.basicConfig(level=level[verbosity], format=format)
    return logging.getLogger(__name__)


def fo_list():
    """
    Callback function for list subcommand
    """
    Disk.print_all(args.format)


def fo_info():
    """
    Callback function for info subcommand
    """
    path = os.path.join('/', 'srv', 'node', args.filter)
    if os.path.isdir(path):
        disks = Disk.get_by_key(mountpoint=path)
    elif os.path.exists(args.filter):
        disks = Disk.get_by_key(device=args.filter)
    else:
        filter = args.filter.split('=')
        arg = {filter[0].lower(): filter[1]}
        disks = Disk.get_by_key(**arg)
    for disk in disks:
        disk.print(args.format)
        if args.locate:
            disk.locate(args.format)
        elif args.unlocate:
            disk.unlocate(args.format)


def fo_replace():
    """
    Callback function for replace subcommand
    """
    path = os.path.join('/', 'srv', 'node', args.filter)
    if os.path.isdir(path):
        disks = Disk.get_by_key(mountpoint=path)
    elif os.path.exists(args.filter):
        disks = Disk.get_by_key(device=args.filter)
    else:
        filter = args.filter.split('=')
        arg = {filter[0].lower(): filter[1]}
        disks = Disk.get_by_key(**arg)
    if len(disks) > 1:
        li = {}
        for disk in disks:
            li[f"{disk.mountpoint}, {disk.device} (SN: {disk.serial})"] = disk
        disk = selectFromDict(li, " Select correct device")
    elif len(disks) == 0:
        print(f"No device matching with {args.filter}")
        logger.warning(f"No device matching with {args.filter}")
        return
    else:
        disk = disks[0]
    disk.replace()


if __name__ == "__main__":
    if os.geteuid() != 0:
        exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.")
    conf = Config()
    args = parse_args()
    logger = setup_logging(args.verbose)
    import json
    import csv
    import subprocess
    import socket
    import requests
    import prettytable
    import sys
    import time
    import pwd
    import datetime

    Disk.load_from_file(args.refresh)
    if 'output' in args and args.output:
        Disk.field_names = args.output.split(',')
    args.func()
