# devices/disk.py
# Classes to represent various types of disk-like devices.
#
# Copyright (C) 2009-2014  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties 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 this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): David Lehman <dlehman@redhat.com>
#

import gi
gi.require_version("BlockDev", "2.0")

from gi.repository import BlockDev as blockdev

import os

from .. import errors
from .. import util
from ..devicelibs import disk as disklib
from ..flags import flags
from ..storage_log import log_method_call
from .. import udev
from ..size import Size
from ..tasks import availability

from ..fcoe import fcoe

import logging
log = logging.getLogger("blivet")

from .lib import Tags
from .storage import StorageDevice
from .container import ContainerDevice
from .network import NetworkStorageDevice
from .dm import DMDevice


class DiskDevice(StorageDevice):

    """ A local/generic disk.

        This is not the only kind of device that is treated as a disk. More
        useful than checking isinstance(device, DiskDevice) is checking
        device.is_disk.
    """
    _type = "disk"
    _partitionable = True
    _is_disk = True

    def __init__(self, name, fmt=None,
                 size=None, major=None, minor=None, sysfs_path='',
                 parents=None, serial=None, vendor="", model="", bus="", wwn=None,
                 uuid=None, exists=True):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword fmt: this device's formatting
            :type fmt: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword uuid: universally unique identifier (device -- not fs)
            :type uuid: str
            :keyword sysfs_path: sysfs device path
            :type sysfs_path: str
            :keyword removable: whether or not this is a removable device
            :type removable: bool
            :keyword serial: the ID_SERIAL_RAW, ID_SERIAL or ID_SERIAL_SHORT for
                             this device (which one is available)
            :type serial: str
            :keyword vendor: the manufacturer of this Device
            :type vendor: str
            :keyword model: manufacturer's device model string
            :type model: str
            :keyword bus: the interconnect this device uses
            :type bus: str
            :keyword str wwn: the disk's WWN

            DiskDevices always exist.
        """
        StorageDevice.__init__(self, name, fmt=fmt, size=size,
                               major=major, minor=minor, exists=exists,
                               sysfs_path=sysfs_path, parents=parents,
                               serial=serial, model=model,
                               vendor=vendor, bus=bus, uuid=uuid)

        self.wwn = wwn or None

        try:
            ssd = int(util.get_sysfs_attr(self.sysfs_path, "queue/rotational")) == 0
        except TypeError:  # get_sysfs_attr returns None from all error paths
            ssd = False

        self.tags.add(Tags.local)
        if ssd:
            self.tags.add(Tags.ssd)
        if bus == "usb":
            self.tags.add(Tags.usb)
        if self.removable:
            self.tags.add(Tags.removable)

    def _clear_local_tags(self):
        local_tags = set([Tags.local, Tags.ssd, Tags.usb, Tags.removable])
        self.tags = self.tags.difference(local_tags)

    def __repr__(self):
        s = StorageDevice.__repr__(self)
        s += ("  removable = %(removable)s  wwn = %(wwn)s" % {"removable": self.removable,
                                                              "wwn": self.wwn})
        return s

    @property
    def media_present(self):
        if flags.testing:
            return True

        # Some drivers (cpqarray <blegh>) make block device nodes for
        # controllers with no disks attached and then report a 0 size,
        # treat this as no media present
        return self.exists and self.current_size > Size(0)

    @property
    def description(self):
        return " ".join(s for s in (self.vendor, self.model, self.wwn) if s)

    def _pre_destroy(self):
        """ Destroy the device. """
        log_method_call(self, self.name, status=self.status)
        if not self.media_present:
            raise errors.DeviceError("cannot destroy disk with no media")

        StorageDevice._pre_destroy(self)

    @property
    def _volume(self):
        return disklib.volumes.get(self.path)

    @property
    def raid_system(self):
        return self._volume.system if self._volume is not None else None

    @property
    def raid_level(self):
        return self._volume.raid_type if self._volume is not None else None

    @property
    def raid_stripe_size(self):
        return self._volume.raid_stripe_size if self._volume is not None else None

    @property
    def raid_disk_count(self):
        return self._volume.raid_disk_count if self._volume is not None else None


class DiskFile(DiskDevice):

    """ This is a file that we will pretend is a disk.

        This is intended only for testing purposes. The benefit of this class
        is that you can instantiate a disk-like device with a working disklabel
        class as a non-root user. It is not known how the system will behave if
        partitions are committed to one of these disks.
    """
    _dev_dir = ""

    def __init__(self, name, fmt=None,
                 size=None, major=None, minor=None, sysfs_path='',
                 parents=None, serial=None, vendor="", model="", bus="",
                 exists=True):
        """
            :param str name: the full path to the backing regular file
            :keyword :class:`~.formats.DeviceFormat` fmt: the device's format
        """
        _name = os.path.basename(name)
        self._dev_dir = os.path.dirname(name)

        super(DiskFile, self).__init__(_name, fmt=fmt, size=size,
                                       major=major, minor=minor, sysfs_path=sysfs_path,
                                       parents=parents, serial=serial, vendor=vendor,
                                       model=model, bus=bus, exists=exists)

    #
    # Regular files do not have sysfs entries.
    #
    @property
    def sysfs_path(self):
        return ""

    @sysfs_path.setter
    def sysfs_path(self, value):
        pass

    def update_sysfs_path(self):
        pass

    def read_current_size(self):
        size = Size(0)
        if self.exists and os.path.exists(self.path):
            st = os.stat(self.path)
            size = Size(st.st_size)

        return size


class DMRaidArrayDevice(DMDevice, ContainerDevice):

    """ A dmraid (device-mapper RAID) device """
    _type = "dm-raid array"
    _packages = ["dmraid"]
    _partitionable = True
    _is_disk = True
    _format_class_name = property(lambda s: "dmraidmember")
    _format_uuid_attr = property(lambda s: None)
    _external_dependencies = [availability.BLOCKDEV_DM_PLUGIN_RAID]

    def __init__(self, name, fmt=None,
                 size=None, parents=None, sysfs_path='', wwn=None):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword fmt: this device's formatting
            :type fmt: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword sysfs_path: sysfs device path
            :type sysfs_path: str
            :keyword str wwn: the device's WWN

            DMRaidArrayDevices always exist. Blivet cannot create or destroy
            them.
        """
        super(DMRaidArrayDevice, self).__init__(name, fmt=fmt, size=size,
                                                parents=parents, exists=True,
                                                sysfs_path=sysfs_path)
        self.wwn = wwn or None
        self.tags.add(Tags.local)

    @property
    def devices(self):
        """ Return a list of this array's member device instances. """
        return self.parents

    def deactivate(self):
        """ Deactivate the raid set. """
        log_method_call(self, self.name, status=self.status)
        # This call already checks if the set is not active.
        blockdev.dm.deactivate_raid_set(self.name)

    def activate(self):
        """ Activate the raid set. """
        log_method_call(self, self.name, status=self.status)
        # This call already checks if the set is active.
        blockdev.dm.activate_raid_set(self.name)
        udev.settle()

    def _setup(self, orig=False):
        """ Open, or set up, a device. """
        log_method_call(self, self.name, orig=orig, status=self.status,
                        controllable=self.controllable)
        self.activate()

    def teardown(self, recursive=None):
        """ Close, or tear down, a device. """
        log_method_call(self, self.name, status=self.status,
                        controllable=self.controllable)
        if not self._pre_teardown(recursive=recursive):
            return

        log.debug("not tearing down dmraid device %s", self.name)

    def _add(self, member):
        raise NotImplementedError()

    def _remove(self, member):
        raise NotImplementedError()

    @property
    def description(self):
        return "BIOS RAID set (%s)" % blockdev.dm.get_raid_set_type(self.name)

    @property
    def model(self):
        return self.description

    def dracut_setup_args(self):
        return set(["rd.dm.uuid=%s" % self.name])


class MultipathDevice(DMDevice):

    """ A multipath device """
    _type = "dm-multipath"
    _packages = ["device-mapper-multipath"]
    _partitionable = True
    _is_disk = True
    _external_dependencies = [availability.MULTIPATH_APP]

    def __init__(self, name, fmt=None, size=None, wwn=None,
                 parents=None, sysfs_path=''):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword fmt: this device's formatting
            :type fmt: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword sysfs_path: sysfs device path
            :type sysfs_path: str
            :keyword str wwn: the device's WWN

            MultipathDevices always exist. Blivet cannot create or destroy
            them.
        """
        DMDevice.__init__(self, name, fmt=fmt, size=size,
                          parents=parents, sysfs_path=sysfs_path,
                          exists=True)
        self.wwn = wwn or None

    @property
    def model(self):
        if not self.parents:
            return ""
        return self.parents[0].model

    @property
    def vendor(self):
        if not self.parents:
            return ""
        return self.parents[0].vendor

    @property
    def description(self):
        return "WWID %s" % self.wwn

    def add_parent(self, parent):
        """ Add a parent device to the mpath. """
        log_method_call(self, self.name, status=self.status)
        if self.status:
            self.teardown()
            self.parents.append(parent)
            self.setup()
        else:
            self.parents.append(parent)

    def _add_parent(self, parent):
        super(MultipathDevice, self)._add_parent(parent)
        if Tags.remote not in self.tags and Tags.remote in parent.tags:
            self.tags.add(Tags.remote)

    def _remove_parent(self, parent):
        super(MultipathDevice, self)._remove_parent(parent)
        if Tags.remote in self.tags and Tags.remote in parent.tags and \
           not any(p for p in self.parents if Tags.remote in p.tags and p != parent):
            self.tags.remove(Tags.remote)

    def _setup(self, orig=False):
        """ Open, or set up, a device. """
        log_method_call(self, self.name, orig=orig, status=self.status,
                        controllable=self.controllable)
        udev.settle()
        rc = util.run_program(["multipath", self.name])
        if rc:
            raise errors.MPathError("multipath activation failed for '%s'" %
                                    self.name, hardware_fault=True)

    def _post_setup(self):
        StorageDevice._post_setup(self)
        self.setup_partitions()
        udev.settle()


class iScsiDiskDevice(DiskDevice, NetworkStorageDevice):

    """ An iSCSI disk. """
    _type = "iscsi"
    _packages = ["iscsi-initiator-utils", "dracut-network"]

    def __init__(self, device, **kwargs):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword exists: does this device exist?
            :type exists: bool
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword format: this device's formatting
            :type format: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword str wwn: the disk's WWN
            :keyword target: the name of the iscsi target
            :type target: str
            :keyword lun: lun of the target
            :type node: str
            :keyword iface: name of network interface to use for operation
            :type iface: str
            :keyword initiator: initiator name
            :type initiator: str
            :keyword offload: a partial offload device (qla4xxx)
            :type: bool
            :keyword address: ip address of the target
            :type: str
            :keyword port: port of the target
            :type: str
        """
        # Backward compatibility attributes - to be removed
        self.node = kwargs.pop("node")
        self.ibft = kwargs.pop("ibft")
        self.nic = kwargs.pop("nic")

        self.initiator = kwargs.pop("initiator")
        self.offload = kwargs.pop("offload")
        name = kwargs.pop("name")
        self.target = kwargs.pop("target")
        try:
            self.lun = int(kwargs.pop("lun"))
        except TypeError as e:
            log.warning("Failed to set lun attribute of iscsi disk: %s", e)
            self.lun = None

        self.address = kwargs.pop("address")
        self.port = kwargs.pop("port")
        self.iface = kwargs.pop("iface")
        self.id_path = kwargs.pop("id_path")
        DiskDevice.__init__(self, device, **kwargs)
        NetworkStorageDevice.__init__(self, host_address=self.address, nic=self.iface)
        log.debug("created new iscsi disk %s from target: %s lun: %s portal: %s:%s interface: %s partial offload: %s)",
                  name, self.target, self.lun, self.address, self.port, self.iface, self.offload)

        self._clear_local_tags()

    def dracut_setup_args(self):
        if self.ibft:
            return set(["rd.iscsi.firmware"])

        # qla4xxx partial offload
        if self.node is None:
            return set()

        address = self.node.address
        # surround ipv6 addresses with []
        if ":" in address:
            address = "[%s]" % address

        netroot = "netroot=iscsi:"
        if self.node.username and self.node.password:
            netroot += "%s:%s" % (self.node.username, self.node.password)
            if self.node.r_username and self.node.r_password:
                netroot += ":%s:%s" % (self.node.r_username,
                                       self.node.r_password)

        iface_spec = ""
        if self.nic != "default":
            iface_spec = ":%s:%s" % (self.node.iface, self.nic)
        netroot += "@%s::%d%s::%s" % (address,
                                      self.node.port,
                                      iface_spec,
                                      self.node.name)

        initiator = "rd.iscsi.initiator=%s" % self.initiator

        return set([netroot, initiator])


class FcoeDiskDevice(DiskDevice, NetworkStorageDevice):

    """ An FCoE disk. """
    _type = "fcoe"
    _packages = ["fcoe-utils", "dracut-network"]

    def __init__(self, device, **kwargs):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword exists: does this device exist?
            :type exists: bool
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword format: this device's formatting
            :type format: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword str wwn: the disk's WWN
            :keyword nic: name of NIC to use
            :keyword identifier: ???
        """
        self.nic = kwargs.pop("nic")
        self.identifier = kwargs.pop("identifier")
        self.id_path = kwargs.pop("id_path")
        DiskDevice.__init__(self, device, **kwargs)
        NetworkStorageDevice.__init__(self, nic=self.nic)
        log.debug("created new fcoe disk %s (%s) @ %s",
                  device, self.identifier, self.nic)

        self._clear_local_tags()

    def dracut_setup_args(self):
        dcb = True

        for nic, dcb, _auto_vlan in fcoe().nics:
            if nic == self.nic:
                break
        else:
            return set()

        if dcb:
            dcb_opt = "dcb"
        else:
            dcb_opt = "nodcb"

        if self.nic in fcoe().added_nics:
            return set(["fcoe=%s:%s" % (self.nic, dcb_opt)])
        else:
            return set(["fcoe=edd:%s" % dcb_opt])


class ZFCPDiskDevice(DiskDevice):

    """ A mainframe ZFCP disk. """
    _type = "zfcp"

    def __init__(self, device, **kwargs):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword exists: does this device exist?
            :type exists: bool
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword format: this device's formatting
            :type format: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword str wwn: the disk's WWN
            :keyword hba_id: ???
            :keyword wwpn: ???
            :keyword fcp_lun: ???
        """
        self.hba_id = kwargs.pop("hba_id")
        self.wwpn = kwargs.pop("wwpn")
        self.fcp_lun = kwargs.pop("fcp_lun")
        DiskDevice.__init__(self, device, **kwargs)
        self._clear_local_tags()
        self.tags.add(Tags.remote)

    def __repr__(self):
        s = DiskDevice.__repr__(self)
        s += ("  hba_id = %(hba_id)s  wwpn = %(wwpn)s  fcp_lun = %(fcp_lun)s" %
              {"hba_id": self.hba_id,
               "wwpn": self.wwpn,
               "fcp_lun": self.fcp_lun})
        return s

    @property
    def description(self):
        return "FCP device %(device)s with WWPN %(wwpn)s and LUN %(lun)s" \
               % {'device': self.hba_id,
                  'wwpn': self.wwpn,
                  'lun': self.fcp_lun}

    def dracut_setup_args(self):
        from ..zfcp import has_auto_lun_scan

        # zFCP auto LUN scan needs only the device ID
        if has_auto_lun_scan(self.hba_id):
            dracut_args = set(["rd.zfcp=%s" % self.hba_id])
        else:
            dracut_args = set(["rd.zfcp=%s,%s,%s" % (self.hba_id, self.wwpn, self.fcp_lun,)])

        return dracut_args


class DASDDevice(DiskDevice):

    """ A mainframe DASD. """
    _type = "dasd"

    def __init__(self, device, **kwargs):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword exists: does this device exist?
            :type exists: bool
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword format: this device's formatting
            :type format: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword str wwn: the disk's WWN
            :keyword busid: bus ID
            :keyword opts: options
            :type opts: dict with option name keys and option value values
        """
        self.busid = kwargs.pop('busid')
        self.opts = kwargs.pop('opts')
        DiskDevice.__init__(self, device, **kwargs)

    @property
    def description(self):
        return "DASD device %s" % self.busid

    def get_opts(self):
        return ["%s=%s" % (k, v) for k, v in self.opts.items() if v == '1']

    def dracut_setup_args(self):
        conf = "/etc/dasd.conf"
        line = None
        if os.path.isfile(conf):
            f = open(conf)
            # grab the first line that starts with our bus_id
            for l in f.readlines():
                if l.startswith(self.busid):
                    line = l.rstrip()
                    break

            f.close()

        # See if we got a line.  If not, grab our get_opts
        if not line:
            line = self.busid
            for devopt in self.get_opts():
                line += " %s" % devopt

        # Create a translation mapping from dasd.conf format to module format
        translate = {'use_diag': 'diag',
                     'readonly': 'ro',
                     'erplog': 'erplog',
                     'failfast': 'failfast'}

        # this is a really awkward way of determining if the
        # feature found is actually desired (1, not 0), plus
        # translating that feature into the actual kernel module
        # value
        opts = []
        parts = line.split()
        for chunk in parts[1:]:
            try:
                feat, val = chunk.split('=')
                if int(val):
                    opts.append(translate[feat])
            except (ValueError, KeyError):
                # If we don't know what the feature is (feat not in translate
                # or if we get a val that doesn't cleanly convert to an int
                # we can't do anything with it.
                log.warning("failed to parse dasd feature %s", chunk)

        if opts:
            return set(["rd.dasd=%s(%s)" % (self.busid,
                                            ":".join(opts))])
        else:
            return set(["rd.dasd=%s" % self.busid])


class NVDIMMNamespaceDevice(DiskDevice):

    """ Non-volatile memory namespace """
    _type = "nvdimm"
    _packages = ["ndctl"]

    def __init__(self, device, **kwargs):
        """
            :param name: the device name (generally a device node's basename)
            :type name: str
            :keyword exists: does this device exist?
            :type exists: bool
            :keyword size: the device's size
            :type size: :class:`~.size.Size`
            :keyword parents: a list of parent devices
            :type parents: list of :class:`StorageDevice`
            :keyword format: this device's formatting
            :type format: :class:`~.formats.DeviceFormat` or a subclass of it
            :keyword mode: mode of the namespace
            :type mode: str
            :keyword devname: name of the namespace (e.g. 'namespace0.0')
            :type devname: str
            :keyword sector_size: sector size of the namespace in sector mode
            :type sector_size: str
        """
        self.mode = kwargs.pop("mode")
        self.devname = kwargs.pop("devname")
        self.id_path = kwargs.pop("id_path")
        self._sector_size = kwargs.pop("sector_size")

        DiskDevice.__init__(self, device, **kwargs)

        self._clear_local_tags()
        self.tags.add(Tags.local)
        self.tags.add(Tags.nvdimm)

    def __repr__(self):
        s = DiskDevice.__repr__(self)
        s += ("  mode = %(mode)s  devname = %(devname)s" %
              {"mode": self.mode,
               "devname": self.devname})
        if self.sector_size:
            s += ("  sector size = %(sector_size)s" % {"sector_size": self.sector_size})
        return s

    @property
    def description(self):
        return "NVDIMM namespace %(devname)s in %(mode)s mode exported as %(path)s" \
               % {'devname': self.devname,
                  'mode': self.mode,
                  'path': self.path}

    @property
    def sector_size(self):
        return self._sector_size
