#
# platform.py:  Architecture-specific information
#
# Copyright (C) 2009
# Red Hat, Inc.  All rights reserved.
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Authors: Chris Lumens <clumens@redhat.com>
#

import iutil
import parted
import storage
from storage.errors import *
from storage.formats import *
from storage.partspec import *

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

class Platform(object):
    """Platform

       A class containing platform-specific information and methods for use
       during installation.  The intent is to eventually encapsulate all the
       architecture quirks in one place to avoid lots of platform checks
       throughout anaconda."""
    _bootFSTypes = ["ext3"]
    _diskLabelType = "msdos"
    _isEfi = iutil.isEfi()
    _minimumSector = 0
    _packages = []
    _supportsMdRaidBoot = False
    _minBootPartSize = 50
    _maxBootPartSize = 0

    def __init__(self, anaconda):
        """Creates a new Platform object.  This is basically an abstract class.
           You should instead use one of the platform-specific classes as
           returned by getPlatform below.  Not all subclasses need to provide
           all the methods in this class."""
        self.anaconda = anaconda

    def _mntDict(self):
        """Return a dictionary mapping mount points to devices."""
        ret = {}
        for device in [d for d in self.anaconda.storage.devices if d.format.mountable]:
            ret[device.format.mountpoint] = device

        return ret

    def bootDevice(self):
        """Return the device where /boot is mounted."""
        if self.__class__ is Platform:
            raise NotImplementedError("bootDevice not implemented for this platform")

        mntDict = self._mntDict()
        return mntDict.get("/boot", mntDict.get("/"))

    @property
    def defaultBootFSType(self):
        """Return the default filesystem type for the boot partition."""
        return self._bootFSTypes[0]

    @property
    def bootFSTypes(self):
        """Return a list of all valid filesystem types for the boot partition."""
        return self._bootFSTypes

    def bootloaderChoices(self, bl):
        """Return the default list of places to install the bootloader.
           This is returned as a dictionary of locations to (device, identifier)
           tuples.  If there is no boot device, an empty dictionary is
           returned."""
        if self.__class__ is Platform:
            raise NotImplementedError("bootloaderChoices not implemented for this platform")

        bootDev = self.bootDevice()
        ret = {}

        if not bootDev:
            return ret

        if bootDev.type == "mdarray":
            ret["boot"] = (bootDev.name, N_("RAID Device"))
            ret["mbr"] = (bl.drivelist[0], N_("Master Boot Record (MBR)"))
        else:
            ret["boot"] = (bootDev.name, N_("First sector of boot partition"))
            ret["mbr"] = (bl.drivelist[0], N_("Master Boot Record (MBR)"))

        return ret

    def checkBootRequest(self, req):
        """Perform an architecture-specific check on the boot device.  Not all
           platforms may need to do any checks.  Returns a list of errors if
           there is a problem, or [] otherwise."""
        errors = []

        if not req:
            return [_("You have not created a bootable partition.")]

        # most arches can't have boot on RAID
        if req.type == "mdarray":
            if not self.supportsMdRaidBoot:
                errors.append(_("Bootable partitions cannot be on a RAID device."))
            elif req.type == "mdarray" and req.level != 1:
                errors.append(_("Bootable partitions can only be on RAID1 devices."))
            else:
                for p in req.parents:
                    if p.type != "partition":
                        errors.append(_("Bootable RAID1 set members must be partitions."))
                        break

        # can't have bootable partition on LV
        if req.type == "lvmlv":
            errors.append(_("Bootable partitions cannot be on a logical volume."))

        # Make sure /boot is on a supported FS type.  This prevents crazy
        # things like boot on vfat.
        if not req.format.bootable or \
           (getattr(req.format, "mountpoint", None) == "/boot" and
            req.format.type not in self.bootFSTypes):
            errors.append(_("Bootable partitions cannot be on an %s filesystem.") % req.format.type)

        if req.type == "luks/dm-crypt":
            # Handle encrypted boot on a partition.
            errors.append(_("Bootable partitions cannot be on an encrypted block device"))
        else:
            # Handle encrypted boot on more complicated devices.
            for dev in filter(lambda d: d.type == "luks/dm-crypt", self.anaconda.storage.devices):
                if req in self.anaconda.storage.deviceDeps(dev):
                    errors.append(_("Bootable partitions cannot be on an encrypted block device"))

        return errors

    def diskLabelType(self, deviceType):
        """Return the disk label type as a string."""
        return self._diskLabelType

    @property
    def isEfi(self):
        return self._isEfi

    @property
    def minimumSector(self, disk):
        """Return the minimum starting sector for the provided disk."""
        return self._minimumSector

    @property
    def packages (self):
        return self._packages

    def setDefaultPartitioning(self):
        """Return the default platform-specific partitioning information."""
        return [PartSpec(mountpoint="/boot", fstype=self.defaultBootFSType, size=500,
                         weight=self.weight(mountpoint="/boot"))]

    @property
    def supportsMdRaidBoot(self):
        """Does the platform support /boot on MD RAID?"""
        return self._supportsMdRaidBoot

    @property
    def minBootPartSize(self):
        return self._minBootPartSize

    @property
    def maxBootPartSize(self):
        return self._maxBootPartSize

    def validBootPartSize(self, size):
        """ Is the given size (in MB) acceptable for a boot device? """
        if not isinstance(size, int) and not isinstance(size, float):
            return False

        return ((not self.minBootPartSize or size >= self.minBootPartSize)
                and
                (not self.maxBootPartSize or size <= self.maxBootPartSize))

    def weight(self, fstype=None, mountpoint=None):
        """ Given an fstype (as a string) or a mountpoint, return an integer
            for the base sorting weight.  This is used to modify the sort
            algorithm for partition requests, mainly to make sure bootable
            partitions and /boot are placed where they need to be."""
        return 0

class EFI(Platform):
    _bootFSTypes = ["ext4", "ext3", "ext2", "btrfs", "reiserfs", "xfs", "jfs"]
    _diskLabelType = "gpt"
    _minBootPartSize = 50
    _maxBootPartSize = 256

    def bootDevice(self):
        mntDict = self._mntDict()
        return mntDict.get("/boot/efi")

    def bootloaderChoices(self, bl):
        bootDev = self.bootDevice()
        ret = {}

        if not bootDev:
            return ret

        ret["boot"] = (bootDev.name, N_("EFI System Partition"))
        return ret

    def checkBootRequest(self, req):
        """ Perform architecture-specific checks on the boot device.

            Returns a list of error strings.

            NOTE: X86 does not have a separate checkBootRequest method,
                  so this one must work for x86 as well as EFI.
        """
        if not req:
            return [_("You have not created a /boot/efi partition.")]

        errors = Platform.checkBootRequest(self, req)

        if req.format.mountpoint == "/boot/efi":
            if req.format.type != "efi":
                errors.append(_("/boot/efi is not EFI."))

        # Don't try to check the disklabel on lv's etc, using lv for /boot
        # is already checked in the generic Platform.checkBootRequest()
        partitions = []
        if req.type == "partition":
            partitions = [ req ]
        elif req.type == "mdarray":
            partitions = filter(lambda d: d.type == "partition", req.parents)

        # Check that we've got a correct disk label.
        for p in partitions:        
            partedDisk = p.disk.format.partedDisk
            labelType = self.diskLabelType(partedDisk.device.type)
            # Allow using gpt with x86, but not msdos with EFI
            if partedDisk.type != labelType and partedDisk.type != "gpt":
                errors.append(_("%s must have a %s disk label.")
                              % (p.disk.name, labelType.upper()))

        return errors

    def setDefaultPartitioning(self):
        ret = Platform.setDefaultPartitioning(self)

        # Only add the EFI partition to the default set if there's not already
        # one on the system.
        if len(filter(lambda dev: dev.format.type == "efi" and self.validBootPartSize(dev.size),
                      self.anaconda.storage.partitions)) == 0:
            ret.append(PartSpec(mountpoint="/boot/efi", fstype="efi", size=20,
                                maxSize=200, grow=True, weight=self.weight(fstype="efi")))

        return ret

    def weight(self, fstype=None, mountpoint=None):
        if fstype and fstype == "efi" or mountpoint and mountpoint == "/boot/efi":
            return 5000
        elif mountpoint and mountpoint == "/boot":
            return 2000
        else:
            return 0

class Alpha(Platform):
    _diskLabelType = "bsd"

    def checkBootRequest(self, req):
        errors = Platform.checkBootRequest(self, req)

        if not req or req.type != "partition" or not req.disk:
            return errors

        disk = req.disk.format.partedDisk

        # Check that we're a BSD disk label
        if not disk.type == self._diskLabelType:
            errors.append(_("%s must have a bsd disk label.") % req.disk.name)

        # The first free space should start at the beginning of the drive and
        # span for a megabyte or more.
        free = disk.getFirstPartition()
        while free:
            if free.type & parted.PARTITION_FREESPACE:
                break

            free = free.nextPartition()

        if not free or free.geoemtry.start != 1L or free.getSize(unit="MB") < 1:
            errors.append(_("The disk %s requires at least 1MB of free space at the beginning.") % req.disk.name)

        return errors

class IA64(EFI):
    _packages = ["elilo"]

    def __init__(self, anaconda):
        EFI.__init__(self, anaconda)

class PPC(Platform):
    _bootFSTypes = ["ext4", "ext3", "ext2", "btrfs"]
    _packages = ["yaboot"]
    _ppcMachine = iutil.getPPCMachine()
    _supportsMdRaidBoot = True

    @property
    def ppcMachine(self):
        return self._ppcMachine

class IPSeriesPPC(PPC):
    _minBootPartSize = 4
    _maxBootPartSize = 10

    def bootDevice(self):
        bootDev = None

        # We want the first PReP partition.
        for device in self.anaconda.storage.partitions:
            if device.format.type == "prepboot":
                bootDev = device
                break

        return bootDev

    def bootloaderChoices(self, bl):
        ret = {}

        bootDev = self.bootDevice()
        if not bootDev:
            return ret

        if bootDev.type == "mdarray":
            ret["boot"] = (bootDev.name, N_("RAID Device"))
            ret["mbr"] = (bl.drivelist[0], N_("Master Boot Record (MBR)"))
        else:
            ret["boot"] = (bootDev.name, N_("PPC PReP Boot"))

        return ret

    def checkBootRequest(self, req):
        errors = PPC.checkBootRequest(self, req)

        bootPart = getattr(req, "partedPartition", None)
        if not bootPart:
            return errors

        # All of the above just checks the PPC PReP boot partitions.  We still
        # need to make sure that whatever /boot is on also meets these criteria.
        if req == self.bootDevice():
            # However, this check only applies to prepboot.
            if bootPart.geometry.end * bootPart.geometry.device.sectorSize / (1024.0 * 1024) > 4096:
                errors.append(_("The boot partition must be within the first 4MB of the disk."))

            try:
                req = self.anaconda.storage.mountpoints["/boot"]
            except KeyError:
                req = self.anaconda.storage.rootDevice

            return errors + self.checkBootRequest(req)
        else:
            return errors

    def setDefaultPartitioning(self):
        ret = PPC.setDefaultPartitioning(self)
        ret.append(PartSpec(fstype="PPC PReP Boot", size=4,
                            weight=self.weight(fstype="prepboot")))
        return ret

    def weight(self, fstype=None, mountpoint=None):
        if fstype and fstype == "prepboot":
            return 5000
        elif mountpoint and mountpoint == "/boot":
            return 2000
        else:
            return 0

class NewWorldPPC(PPC):
    _diskLabelType = "mac"
    _minBootPartSize = (800.00 / 1024.00)
    _maxBootPartSize = 1

    def bootDevice(self):
        bootDev = None

        for part in self.anaconda.storage.partitions:
            if part.format.type == "appleboot" and self.validBootPartSize(part.size):
                bootDev = part
                # if we're only picking one, it might as well be the first
                break

        return bootDev

    def bootloaderChoices(self, bl):
        ret = {}

        bootDev = self.bootDevice()
        if not bootDev:
            return ret

        if bootDev.type == "mdarray":
            ret["boot"] = (bootDev.name, N_("RAID Device"))
            ret["mbr"] = (bl.drivelist[0], N_("Master Boot Record (MBR)"))
        else:
            ret["boot"] = (bootDev.name, N_("Apple Bootstrap"))
            for (n, device) in enumerate(self.anaconda.storage.partitions):
                if device.format.type == "appleboot" and device.path != bootDev.path:
                    ret["boot%d" % n] = (device.path, N_("Apple Bootstrap"))

        return ret

    def checkBootRequest(self, req):
        errors = PPC.checkBootRequest(self, req)

        if not req or req.type != "partition" or not req.disk:
            return errors

        disk = req.disk.format.partedDisk

        # Check that we're a Mac disk label
        if not disk.type == self._diskLabelType:
            errors.append(_("%s must have a mac disk label.") % req.disk.name)

        # All of the above just checks the appleboot partitions.  We still
        # need to make sure that whatever /boot is on also meets these criteria.
        if req == self.bootDevice():
            try:
                req = self.anaconda.storage.mountpoints["/boot"]
            except KeyError:
                req = self.anaconda.storage.rootDevice

            return errors + self.checkBootRequest(req)
        else:
            return errors

    def setDefaultPartitioning(self):
        ret = Platform.setDefaultPartitioning(self)
        ret.append(PartSpec(fstype="Apple Bootstrap", size=1, maxSize=1,
                            weight=self.weight(fstype="appleboot")))
        return ret

    def weight(self, fstype=None, mountpoint=None):
        if fstype and fstype == "appleboot":
            return 5000
        elif mountpoint and mountpoint == "/boot":
            return 2000
        else:
            return 0

class PS3(PPC):
    _diskLabelType = "msdos"

    def __init__(self, anaconda):
        PPC.__init__(self, anaconda)

class S390(Platform):
    _packages = ["s390utils"]

    def __init__(self, anaconda):
        Platform.__init__(self, anaconda)

    def diskLabelType(self, deviceType):
        """Return the disk label type as a string."""
        if deviceType == parted.DEVICE_DASD:
            return "dasd"
        else:
            return Platform.diskLabelType(self, deviceType)

class Sparc(Platform):
    _diskLabelType = "sun"
    _packages = ["silo"]

    @property
    def minimumSector(self, disk):
        (cylinders, heads, sectors) = disk.device.biosGeometry
        start = long(sectors * heads)
        start /= long(1024 / disk.device.sectorSize)
        return start+1

class X86(EFI):
    _bootFSTypes = ["ext4", "ext3", "ext2", "btrfs", "reiserfs", "xfs", "jfs"]
    _packages = ["grub"]
    _supportsMdRaidBoot = True

    def __init__(self, anaconda):
        EFI.__init__(self, anaconda)

        if self.isEfi:
            self._diskLabelType = "gpt"
        else:
            self._diskLabelType = "msdos"

    def bootDevice(self):
        if self.isEfi:
            return EFI.bootDevice(self)
        else:
            return Platform.bootDevice(self)

    def bootloaderChoices(self, bl):
        if self.isEfi:
            return EFI.bootloaderChoices(self, bl)

        bootDev = self.bootDevice()
        ret = {}

        if not bootDev:
            return {}

        if bootDev.type == "mdarray":
            ret["boot"] = (bootDev.name, N_("RAID Device"))
            ret["mbr"] = (bl.drivelist[0], N_("Master Boot Record (MBR)"))
        else:
            ret["boot"] = (bootDev.name, N_("First sector of boot partition"))
            ret["mbr"] = (bl.drivelist[0], N_("Master Boot Record (MBR)"))

        return ret

    @property
    def maxBootPartSize(self):
        if self.isEfi:
            return EFI._maxBootPartSize
        else:
            return Platform._maxBootPartSize

    @property
    def minBootPartSize(self):
        if self.isEfi:
            return EFI._minBootPartSize
        else:
            return Platform._minBootPartSize

    def setDefaultPartitioning(self):
        if self.isEfi:
            return EFI.setDefaultPartitioning(self)
        else:
            return Platform.setDefaultPartitioning(self)

def getPlatform(anaconda):
    """Check the architecture of the system and return an instance of a
       Platform subclass to match.  If the architecture could not be determined,
       raise an exception."""
    if iutil.isAlpha():
        return Alpha(anaconda)
    elif iutil.isIA64():
        return IA64(anaconda)
    elif iutil.isPPC():
        ppcMachine = iutil.getPPCMachine()

        if (ppcMachine == "PMac" and iutil.getPPCMacGen() == "NewWorld"):
            return NewWorldPPC(anaconda)
        elif ppcMachine in ["iSeries", "pSeries"]:
            return IPSeriesPPC(anaconda)
        elif ppcMachine == "PS3":
            return PS3(anaconda)
        else:
            raise SystemError, "Unsupported PPC machine type"
    elif iutil.isS390():
        return S390(anaconda)
    elif iutil.isSparc():
        return Sparc(anaconda)
    elif iutil.isX86():
        return X86(anaconda)
    else:
        raise SystemError, "Could not determine system architecture."
