# __init__.py
# Entry point for anaconda storage formats subpackage.
#
# Copyright (C) 2009  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): Dave Lehman <dlehman@redhat.com>
#

import os

from ..udev import udev_get_device
from ..util import notify_kernel
from ..util import get_sysfs_path_by_name
from ..util import run_program
from ..storage_log import log_method_call
from ..errors import *
from ..devicelibs.dm import dm_node_from_name
from ..devicelibs.mdraid import md_node_from_name
from ..udev import udev_device_get_major, udev_device_get_minor

import gettext
_ = lambda x: gettext.ldgettext("blivet", x)

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


device_formats = {}
def register_device_format(fmt_class):
    if not issubclass(fmt_class, DeviceFormat):
        raise ValueError("arg1 must be a subclass of DeviceFormat")

    device_formats[fmt_class._type] = fmt_class
    log.debug("registered device format class %s as %s" % (fmt_class.__name__,
                                                           fmt_class._type))

default_fstypes = ("ext4", "ext3", "ext2")
def get_default_filesystem_type():
    for fstype in default_fstypes:
        try:
            supported = get_device_format_class(fstype).supported
        except AttributeError:
            supported = None

        if supported:
            return fstype

    raise DeviceFormatError("None of %s is supported by your kernel" % ",".join(default_fstypes))

def getFormat(fmt_type, *args, **kwargs):
    """ Return a DeviceFormat instance based on fmt_type and args.

        Given a device format type and a set of constructor arguments,
        return a DeviceFormat instance.

        Return None if no suitable format class is found.

        Arguments:

            fmt_type -- the name of the format type (eg: 'ext3', 'swap')

        Keyword Arguments:

            The keyword arguments may vary according to the format type,
            but here is the common set:

            device -- path to the device on which the format resides
            uuid -- the UUID of the (preexisting) formatted device
            exists -- whether or not the format exists on the device            
            
    """
    fmt_class = get_device_format_class(fmt_type)
    fmt = None
    if fmt_class:
        fmt = fmt_class(*args, **kwargs)
    try:
        className = fmt.__class__.__name__
    except AttributeError:
        className = None

    # this allows us to store the given type for formats we implement as
    # DeviceFormat.
    if fmt_type and fmt.type is None:
        # this should add/set an instance attribute
        fmt._type = fmt_type
        fmt._name = None

    log.debug("getFormat('%s') returning %s instance" % (fmt_type, className))
    return fmt

def collect_device_format_classes():
    """ Pick up all device format classes from this directory.

        Note: Modules must call register_device_format(FormatClass) in
              order for the format class to be picked up.
    """
    dir = os.path.dirname(__file__)
    for module_file in os.listdir(dir):
        # make sure we're not importing this module
        if module_file.endswith(".py") and module_file != __file__:
            mod_name = module_file[:-3]
            # imputil is deprecated in python 2.6
            try:
                globals()[mod_name] = __import__(mod_name, globals(), locals(), [], -1)
            except ImportError:
                log.error("import of device format module '%s' failed" % mod_name)
                from traceback import format_exc
                log.debug(format_exc())

def get_device_format_class(fmt_type):
    """ Return an appropriate format class based on fmt_type. """
    if not device_formats:
        collect_device_format_classes()

    fmt = device_formats.get(fmt_type)
    if not fmt:
        for fmt_class in device_formats.values():
            if fmt_type and fmt_type == fmt_class._name:
                fmt = fmt_class
                break
            elif fmt_type in fmt_class._udevTypes:
                fmt = fmt_class
                break

    # default to no formatting, AKA "Unknown"
    if not fmt:
        fmt = DeviceFormat

    return fmt

class DeviceFormat(object):
    """ Generic device format. """
    _type = None
    _name = _("Unknown")
    _udevTypes = []
    partedFlag = None
    partedSystem = None
    _formattable = False                # can be formatted
    _supported = False                  # is supported
    _linuxNative = False                # for clearpart
    _packages = []                      # required packages
    _services = []                      # required services
    _resizable = False                  # can be resized
    _maxSize = 0                        # maximum size in MB
    _minSize = 0                        # minimum size in MB
    _dump = False
    _check = False
    _hidden = False                     # hide devices with this formatting?
    _ksMountpoint = None

    def __init__(self, *args, **kwargs):
        """ Create a DeviceFormat instance.

            Keyword Arguments:

                device -- path to the underlying device
                uuid -- this format's UUID
                exists -- indicates whether this is an existing format

        """
        self.device = kwargs.get("device")
        self.uuid = kwargs.get("uuid")
        self.exists = kwargs.get("exists")
        self.options = kwargs.get("options")
        self._majorminor = None

        # don't worry about existence if this is a DeviceFormat instance
        #if self.__class__ is DeviceFormat:
        #    self.exists = True

    def __repr__(self):
        s = ("%(classname)s instance (%(id)s) --\n"
             "  type = %(type)s  name = %(name)s  status = %(status)s\n"
             "  device = %(device)s  uuid = %(uuid)s  exists = %(exists)s\n"
             "  options = %(options)s  supported = %(supported)s"
             "  formattable = %(format)s  resizable = %(resize)s\n" %
             {"classname": self.__class__.__name__, "id": "%#x" % id(self),
              "type": self.type, "name": self.name, "status": self.status,
              "device": self.device, "uuid": self.uuid, "exists": self.exists,
              "options": self.options, "supported": self.supported,
              "format": self.formattable, "resize": self.resizable})
        return s

    @property
    def _existence_str(self):
        exist = "existing"
        if not self.exists:
            exist = "non-existent"
        return exist

    @property
    def desc(self):
        return str(self.type)

    def __str__(self):
        return "%s %s" % (self._existence_str, self.desc)

    @property
    def dict(self):
        d = {"type": self.type, "name": self.name, "device": self.device,
             "uuid": self.uuid, "exists": self.exists,
             "options": self.options, "supported": self.supported,
             "resizable": self.resizable}
        return d

    def _setOptions(self, options):
        self._options = options

    def _getOptions(self):
        return self._options

    options = property(_getOptions, _setOptions)

    def _setDevice(self, devspec):
        if devspec and not devspec.startswith("/"):
            raise ValueError("device must be a fully qualified path")
        self._device = devspec

    def _getDevice(self):
        return self._device

    device = property(lambda f: f._getDevice(),
                      lambda f,d: f._setDevice(d),
                      doc="Full path the device this format occupies")

    @property
    def name(self):
        if self._name:
            name = self._name
        else:
            name = self.type
        return name

    @property
    def type(self):
        return self._type

    def probe(self):
        log_method_call(self, device=self.device,
                        type=self.type, status=self.status)

    def notifyKernel(self):
        log_method_call(self, device=self.device,
                        type=self.type)
        if not self.device:
            return

        if self.device.startswith("/dev/mapper/"):
            try:
                name = dm_node_from_name(os.path.basename(self.device))
            except DMError:
                log.warning("failed to get dm node for %s" % self.device)
                return
        elif self.device.startswith("/dev/md/"):
            try:
                name = md_node_from_name(os.path.basename(self.device))
            except MDRaidError:
                log.warning("failed to get md node for %s" % self.device)
                return
        else:
            name = self.device

        path = get_sysfs_path_by_name(name)
        try:
            notify_kernel(path, action="change")
        except (ValueError, IOError) as e:
            log.warning("failed to notify kernel of change: %s" % e)

    def cacheMajorminor(self):
        """ Cache the value of self.majorminor.

            Once a device node of this format's device disappears (for instance
            after a teardown), it is no longer possible to figure out the value
            of self.majorminor pseudo-unique string. Call this method before
            that happens for caching this.
        """
        self._majorminor = None
        try:
            self.majorminor # this does the caching
        except StorageError:
            # entirely possible there's no majorminor, for instance an
            # LVMVolumeGroup has got no device node and no sysfs path.  In this
            # case obviously, calling majorminor of this object later raises an
            # exception.
            pass
        return self._majorminor

    def create(self, *args, **kwargs):
        log_method_call(self, device=self.device,
                        type=self.type, status=self.status)
        # allow late specification of device path
        device = kwargs.get("device")
        if device:
            self.device = device

        if not os.path.exists(self.device):
            raise FormatCreateError("invalid device specification", self.device)

    def destroy(self, *args, **kwargs):
        log_method_call(self, device=self.device,
                        type=self.type, status=self.status)
        try:
            rc = run_program(["wipefs", "-f", "-a", self.device])
        except OSError as e:
            err = str(e)
        else:
            err = ""
            if rc:
                err = str(rc)

        if err:
            msg = "error wiping old signatures from %s: %s" % (self.device, err)
            raise FormatDestroyError(msg)

        self.exists = False

    def setup(self, *args, **kwargs):
        log_method_call(self, device=self.device,
                        type=self.type, status=self.status)

        if not self.exists:
            raise FormatSetupError("format has not been created")

        if self.status:
            return

        # allow late specification of device path
        device = kwargs.get("device")
        if device:
            self.device = device

        if not self.device or not os.path.exists(self.device):
            raise FormatSetupError("invalid device specification")

    def teardown(self, *args, **kwargs):
        log_method_call(self, device=self.device,
                        type=self.type, status=self.status)

    @property
    def status(self):
        return (self.exists and
                self.__class__ is not DeviceFormat and
                isinstance(self.device, str) and
                self.device and 
                os.path.exists(self.device))

    @property
    def formattable(self):
        """ Can we create formats of this type? """
        return self._formattable

    @property
    def supported(self):
        """ Is this format a supported type? """
        return self._supported

    @property
    def packages(self):
        """ Packages required to manage formats of this type. """
        return self._packages

    @property
    def services(self):
        """ Services required to manage formats of this type. """
        return self._services

    @property
    def resizable(self):
        """ Can formats of this type be resized? """
        return self._resizable and self.exists

    @property
    def linuxNative(self):
        """ Is this format type native to linux? """
        return self._linuxNative

    @property
    def mountable(self):
        """ Is this something we can mount? """
        return False

    @property
    def dump(self):
        """ Whether or not this format will be dumped by dump(8). """
        return self._dump

    @property
    def check(self):
        """ Whether or not this format is checked on boot. """
        return self._check

    @property
    def maxSize(self):
        """ Maximum size (in MB) for this format type. """
        return self._maxSize

    @property
    def minSize(self):
        """ Minimum size (in MB) for this format type. """
        return self._minSize

    @property
    def hidden(self):
        """ Whether devices with this formatting should be hidden in UIs. """
        return self._hidden

    @property
    def majorminor(self):
        """A string suitable for using as a pseudo-unique ID in kickstart."""
        if not self._majorminor:
            # If this is a device-mapper device, we have to get the DM node and
            # build the sysfs path from that.
            try:
                device = dm_node_from_name(os.path.basename(self.device))
            except DMError:
                device = self.device

            try:
                sysfs_path = get_sysfs_path_by_name(device)
            except RuntimeError:
                raise StorageError("DeviceFormat.majorminor: "
                                   "can not get majorminor for '%s'" % device)
            dev = udev_get_device(sysfs_path[4:])

            self._majorminor = "%03d%03d" %\
                (udev_device_get_major(dev), udev_device_get_minor(dev))
        return self._majorminor

    @property
    def ksMountpoint(self):
        return (self._ksMountpoint or self.type or "")

    def populateKSData(self, data):
        data.format = not self.exists
        data.fstype = self.type
        data.mountpoint = self.ksMountpoint

collect_device_format_classes()
