# __init__.py
# Entry point for anaconda's storage configuration module.
#
# Copyright (C) 2009, 2010, 2011, 2012, 2013  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>
#

__version__ = '0.18.11'

##
## Default stub values for installer-specific stuff that gets set up in
## enable_installer_mode.  These constants are only for use inside this file.
## For use in other blivet files, they must either be passed to the function
## in question or care must be taken so they are imported only after
## enable_installer_mode is called.
##
isys = None
ROOT_PATH = '/'
shortProductName = 'blivet'
productName = 'blivet'
bootLoaderError = Exception
ERROR_RAISE = 0

class ErrorHandler(object):
    def cb(self, *args, **kwargs):
        return ERROR_RAISE

errorHandler = ErrorHandler()

##
## end installer stubs
##

import os
import time
import stat
import errno
import sys
import statvfs
import copy
import tempfile

try:
    import nss.nss
except ImportError:
    nss = None

import parted

from pykickstart.constants import *

from storage_log import log_method_call
from errors import *
from devices import *
from devicetree import DeviceTree
from deviceaction import *
from formats import getFormat
from formats import get_device_format_class
from formats import get_default_filesystem_type
from formats.fs import nodev_filesystems
import devicefactory
from devicelibs.dm import name_from_dm_node
from devicelibs.crypto import generateBackupPassphrase
from devicelibs.edd import get_edd_dict
from udev import udev_trigger
import iscsi
import fcoe
import zfcp
import dasd
import util
import arch
from flags import flags
from platform import platform as _platform
from size import Size

import shelve
import contextlib

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

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

def enable_installer_mode():
    global isys
    global ROOT_PATH
    global shortProductName
    global productName
    global get_bootloader
    global BootLoaderError
    global errorHandler
    global ERROR_RAISE

    from pyanaconda import isys
    from pyanaconda.constants import ROOT_PATH
    from pyanaconda.constants import shortProductName
    from pyanaconda.constants import productName
    from pyanaconda.bootloader import get_bootloader
    from pyanaconda.bootloader import BootLoaderError
    from pyanaconda.errors import errorHandler
    from pyanaconda.errors import ERROR_RAISE

    from pyanaconda.anaconda_log import program_log_lock
    util.program_log_lock = program_log_lock

    flags.installer_mode = True

def storageInitialize(storage, ksdata, protected):
    """ Perform installer-specific storage initialization. """
    from pyanaconda.flags import flags as anaconda_flags
    flags.update_from_anaconda_flags(anaconda_flags)

    # Platform class setup depends on flags, re-initialize it.
    import platform
    platform.reset_platform()

    storage.shutdown()

    # Before we set up the storage system, we need to know which disks to
    # ignore, etc.  Luckily that's all in the kickstart data.
    storage.config.update(ksdata)

    lvm.lvm_vg_blacklist = []

    # Set up the protected partitions list now.
    if protected:
        storage.config.protectedDevSpecs.extend(protected)

    storage.reset()

    if protected and not flags.live_install and not storage.protectedDevices:
        raise UnknownSourceDeviceError(protected)

    # kickstart uses all the disks
    if flags.automated_install:
        if not ksdata.ignoredisk.onlyuse:
            ksdata.ignoredisk.onlyuse = [d.name for d in storage.disks \
                                         if d.name not in ksdata.ignoredisk.ignoredisk]
            log.debug("onlyuse is now: %s" % (",".join(ksdata.ignoredisk.onlyuse)))

def turnOnFilesystems(storage, mountOnly=False):
    """ Perform installer-specific activation of storage configuration. """
    if not flags.installer_mode:
        return

    if not mountOnly:
        if (flags.live_install and not flags.image_install and not storage.fsset.active):
            # turn off any swaps that we didn't turn on
            # needed for live installs
            util.run_program(["swapoff", "-a"])
        storage.devicetree.teardownAll()

        try:
            storage.doIt()
        except FSResizeError as e:
            if errorHandler.cb(e, e.args[0], details=e.args[1]) == ERROR_RAISE:
                raise
        except Exception as e:
            raise

        storage.turnOnSwap()
    # FIXME:  For livecd, skipRoot needs to be True.
    storage.mountFilesystems(raiseErrors=False,
                             readOnly=False,
                             skipRoot=False)

    if not mountOnly:
        writeEscrowPackets(storage)

def writeEscrowPackets(storage):
    escrowDevices = filter(lambda d: d.format.type == "luks" and \
                                     d.format.escrow_cert,
                           storage.devices)

    if not escrowDevices:
        return

    log.debug("escrow: writeEscrowPackets start")

    if not nss:
        log.error("escrow: no nss python module -- aborting")
        return

    nss.nss.nss_init_nodb() # Does nothing if NSS is already initialized

    backupPassphrase = generateBackupPassphrase()
    try:
        for device in escrowDevices:
            log.debug("escrow: device %s: %s" %
                      (repr(device.path), repr(device.format.type)))
            device.format.escrow(ROOT_PATH + "/root",
                                 backupPassphrase)

    except (IOError, RuntimeError) as e:
        # TODO: real error handling
        log.error("failed to store encryption key: %s" % e)

    log.debug("escrow: writeEscrowPackets done")

def empty_device(device, devicetree):
    empty = True
    if device.partitioned:
        partitions = devicetree.getChildren(device)
        empty = all([p.isMagic for p in partitions])
    else:
        empty = (device.format.type is None)

    return empty


class StorageDiscoveryConfig(object):
    """ Class to encapsulate various detection/initialization parameters. """
    def __init__(self):
        # storage configuration variables
        self.ignoreDiskInteractive = False
        self.ignoredDisks = []
        self.exclusiveDisks = []
        self.clearPartType = None
        self.clearPartDisks = []
        self.clearPartDevices = []
        self.initializeDisks = False
        self.protectedDevSpecs = []
        self.diskImages = {}
        self.mpathFriendlyNames = True

        # Whether clearPartitions removes scheduled/non-existent devices and
        # disklabels depends on this flag.
        self.clearNonExistent = False

    def update(self, ksdata):
        self.ignoredDisks = ksdata.ignoredisk.ignoredisk[:]
        self.exclusiveDisks = ksdata.ignoredisk.onlyuse[:]
        self.clearPartType = ksdata.clearpart.type
        self.clearPartDisks = ksdata.clearpart.drives[:]
        self.clearPartDevices = ksdata.clearpart.devices[:]
        self.initializeDisks = ksdata.clearpart.initAll
        self.zeroMbr = ksdata.zerombr.zerombr

class Blivet(object):
    """ Top-level class for managing storage configuration. """
    def __init__(self, ksdata=None):
        """ Create a Blivet instance.

            Keyword Arguments:

                ksdata        -   a pykickstart Handler instance
        """
        self.ksdata = ksdata
        self._bootloader = None

        self.config = StorageDiscoveryConfig()

        # storage configuration variables
        self.doAutoPart = False
        self.clearPartChoice = None
        self.encryptedAutoPart = False
        self.autoPartType = AUTOPART_TYPE_LVM
        self.encryptionPassphrase = None
        self.encryptionCipher = None
        self.escrowCertificates = {}
        self.autoPartEscrowCert = None
        self.autoPartAddBackupPassphrase = False
        self.encryptionRetrofit = False
        self.autoPartitionRequests = []
        self.eddDict = {}

        self.__luksDevs = {}
        self.size_sets = []
        self.setDefaultFSType(get_default_filesystem_type())

        self.iscsi = iscsi.iscsi()
        self.fcoe = fcoe.fcoe()
        self.zfcp = zfcp.ZFCP()
        self.dasd = dasd.DASD()

        self._nextID = 0
        self._dumpFile = "%s/storage.state" % tempfile.gettempdir()

        # these will both be empty until our reset method gets called
        self.devicetree = DeviceTree(conf=self.config,
                                     passphrase=self.encryptionPassphrase,
                                     luksDict=self.__luksDevs,
                                     iscsi=self.iscsi,
                                     dasd=self.dasd)
        self.fsset = FSSet(self.devicetree)
        self.roots = []
        self.services = set()

    def doIt(self):
        self.devicetree.processActions()
        self.doEncryptionPassphraseRetrofits()

        # now set the boot partition's flag
        if self.bootloader and not self.bootloader.skip_bootloader:
            if self.bootloader.stage2_bootable:
                boot = self.bootDevice
            else:
                boot = self.bootLoaderDevice

            if boot.type == "mdarray":
                bootDevs = boot.parents
            else:
                bootDevs = [boot]

            for dev in bootDevs:
                if hasattr(dev, "bootable"):
                    # Dos labels can only have one partition marked as active
                    # and unmarking ie the windows partition is not a good idea
                    skip = False
                    if dev.disk.format.partedDisk.type == "msdos":
                        for p in dev.disk.format.partedDisk.partitions:
                            if p.type == parted.PARTITION_NORMAL and \
                               p.getFlag(parted.PARTITION_BOOT):
                                skip = True
                                break

                    # GPT labeled disks should only have bootable set on the
                    # EFI system partition (parted sets the EFI System GUID on
                    # GPT partitions with the boot flag)
                    if dev.disk.format.labelType == "gpt" and \
                       dev.format.type != "efi":
                           skip = True

                    if skip:
                         log.info("not setting boot flag on %s" % dev.name)
                         continue
                    # hfs+ partitions on gpt can't be marked bootable via
                    # parted
                    if dev.disk.format.partedDisk.type == "gpt" and \
                            dev.format.type == "hfs+":
                        log.info("not setting boot flag on hfs+ partition"
                                 " %s" % dev.name)
                        continue
                    log.info("setting boot flag on %s" % dev.name)
                    dev.bootable = True

                    # Set the boot partition's name on disk labels that support it
                    if dev.partedPartition.disk.supportsFeature(parted.DISK_TYPE_PARTITION_NAME):
                        ped_partition = dev.partedPartition.getPedPartition()
                        ped_partition.set_name(dev.format.name)

                    dev.disk.setup()
                    dev.disk.format.commitToDisk()

        if flags.installer_mode:
            self.dumpState("final")

    @property
    def nextID(self):
        id = self._nextID
        self._nextID += 1
        return id

    def shutdown(self):
        try:
            self.devicetree.teardownAll()
        except Exception as e:
            log.error("failure tearing down device tree: %s" % e)

    def reset(self, cleanupOnly=False):
        """ Reset storage configuration to reflect actual system state.

            This should rescan from scratch but not clobber user-obtained
            information like passphrases, iscsi config, &c

        """
        log.info("resetting Blivet (version %s) instance %s" % (__version__, self))
        if flags.installer_mode:
            # save passphrases for luks devices so we don't have to reprompt
            self.encryptionPassphrase = None
            for device in self.devices:
                if device.format.type == "luks" and device.format.exists:
                    self.__luksDevs[device.format.uuid] = device.format._LUKS__passphrase

        if self.ksdata:
            self.config.update(self.ksdata)

        if flags.installer_mode and not flags.image_install:
            self.iscsi.startup()
            self.fcoe.startup()
            self.zfcp.startup()
            self.dasd.startup(None,
                              self.config.exclusiveDisks,
                              self.config.initializeDisks)
        if self.dasd:
            # Reset the internal dasd list (823534)
            self.dasd.clear_device_list()

        self.devicetree.reset(conf=self.config,
                              passphrase=self.encryptionPassphrase,
                              luksDict=self.__luksDevs,
                              iscsi=self.iscsi,
                              dasd=self.dasd)
        self.devicetree.populate(cleanupOnly=cleanupOnly)
        self.fsset = FSSet(self.devicetree)
        self.eddDict = get_edd_dict(self.partitioned)
        if self.bootloader:
            # clear out bootloader attributes that refer to devices that are
            # no longer in the tree
            self.bootloader.reset()

        self.roots = []
        if flags.installer_mode:
            try:
                self.roots = findExistingInstallations(self.devicetree)
            except StorageError as e:
                log.info("failure detecting existing installations: %s" % e)

            self.dumpState("initial")

        if not flags.installer_mode:
            self.getActiveMounts()

        self.updateBootLoaderDiskList()

    @property
    def unusedDevices(self):
        used_devices = []
        for root in self.roots:
            for device in root.mounts.values() + root.swaps:
                if device not in self.devices:
                    continue

                used_devices.extend(device.ancestors)

        for new in [d for d in self.devicetree.leaves if not d.format.exists]:
            if new in self.swaps or getattr(new.format, "mountpoint", None):
                used_devices.extend(new.ancestors)

        for device in self.partitions:
            if getattr(device, "isLogical", False):
                extended = device.disk.format.extendedPartition.path
                used_devices.append(self.devicetree.getDeviceByPath(extended))

        used = set(used_devices)
        _all = set(self.devices)
        return list(_all.difference(used))

    @property
    def devices(self):
        """ A list of all the devices in the device tree. """
        devices = self.devicetree.devices
        devices.sort(key=lambda d: d.name)
        return devices

    @property
    def disks(self):
        """ A list of the disks in the device tree.

            Ignored disks are not included, as are disks with no media present.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        disks = []
        for device in self.devicetree.devices:
            if device.isDisk:
                if not device.mediaPresent:
                    log.info("Skipping disk: %s: No media present" % device.name)
                    continue
                disks.append(device)
        disks.sort(key=lambda d: d.name, cmp=self.compareDisks)
        return disks

    @property
    def partitioned(self):
        """ A list of the partitioned devices in the device tree.

            Ignored devices are not included, nor disks with no media present.

            Devices of types for which partitioning is not supported are also
            not included.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        partitioned = []
        for device in self.devicetree.devices:
            if not device.partitioned:
                continue

            if not device.mediaPresent:
                log.info("Skipping device: %s: No media present" % device.name)
                continue

            partitioned.append(device)

        partitioned.sort(key=lambda d: d.name)
        return partitioned

    @property
    def partitions(self):
        """ A list of the partitions in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        partitions = self.devicetree.getDevicesByInstance(PartitionDevice)
        partitions.sort(key=lambda d: d.name)
        return partitions

    @property
    def vgs(self):
        """ A list of the LVM Volume Groups in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        vgs = self.devicetree.getDevicesByType("lvmvg")
        vgs.sort(key=lambda d: d.name)
        return vgs

    @property
    def lvs(self):
        """ A list of the LVM Logical Volumes in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        lvs = self.devicetree.getDevicesByType("lvmlv")
        lvs.sort(key=lambda d: d.name)
        return lvs

    @property
    def thinlvs(self):
        thin = self.devicetree.getDevicesByType("lvmthinlv")
        thin.sort(key=lambda d: d.name)
        return thin

    @property
    def thinpools(self):
        pools = self.devicetree.getDevicesByType("lvmthinpool")
        pools.sort(key=lambda d: d.name)
        return pools

    @property
    def pvs(self):
        """ A list of the LVM Physical Volumes in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        devices = self.devicetree.devices
        pvs = [d for d in devices if d.format.type == "lvmpv"]
        pvs.sort(key=lambda d: d.name)
        return pvs

    def unusedPVs(self, vg=None):
        unused = []
        for pv in self.pvs:
            used = False
            for _vg in self.vgs:
                if _vg.dependsOn(pv) and _vg != vg:
                    used = True
                    break
                elif _vg == vg:
                    break
            if not used:
                unused.append(pv)
        return unused

    @property
    def mdarrays(self):
        """ A list of the MD arrays in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        arrays = self.devicetree.getDevicesByType("mdarray")
        arrays.sort(key=lambda d: d.name)
        return arrays

    @property
    def mdcontainers(self):
        """ A list of the MD containers in the device tree. """
        arrays = self.devicetree.getDevicesByType("mdcontainer")
        arrays.sort(key=lambda d: d.name)
        return arrays

    @property
    def mdmembers(self):
        """ A list of the MD member devices in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        devices = self.devicetree.devices
        members = [d for d in devices if d.format.type == "mdmember"]
        members.sort(key=lambda d: d.name)
        return members

    def unusedMDMembers(self, array=None):
        unused = []
        for member in self.mdmembers:
            used = False
            for _array in self.mdarrays + self.mdcontainers:
                if _array.dependsOn(member) and _array != array:
                    used = True
                    break
                elif _array == array:
                    break
            if not used:
                unused.append(member)
        return unused

    @property
    def btrfsVolumes(self):
        return sorted(self.devicetree.getDevicesByType("btrfs volume"),
                      key=lambda d: d.name)

    @property
    def swaps(self):
        """ A list of the swap devices in the device tree.

            This is based on the current state of the device tree and
            does not necessarily reflect the actual on-disk state of the
            system's disks.
        """
        devices = self.devicetree.devices
        swaps = [d for d in devices if d.format.type == "swap"]
        swaps.sort(key=lambda d: d.name)
        return swaps

    @property
    def protectedDevices(self):
        devices = self.devicetree.devices
        protected = [d for d in devices if d.protected]
        protected.sort(key=lambda d: d.name)
        return protected

    @property
    def liveImage(self):
        """ The OS image used by live installs. """
        return None

    def shouldClear(self, device, **kwargs):
        clearPartType = kwargs.get("clearPartType", self.config.clearPartType)
        clearPartDisks = kwargs.get("clearPartDisks",
                                    self.config.clearPartDisks)
        clearPartDevices = kwargs.get("clearPartDevices",
                                      self.config.clearPartDevices)

        for disk in device.disks:
            # this will not include disks with hidden formats like multipath
            # and firmware raid member disks
            if clearPartDisks and disk.name not in clearPartDisks:
                return False

        if not self.config.clearNonExistent:
            if (device.isDisk and not device.format.exists) or \
               (not device.isDisk and not device.exists):
                return False

        # the only devices we want to clear when clearPartType is
        # CLEARPART_TYPE_NONE are uninitialized disks, or disks with no
        # partitions, in clearPartDisks, and then only when we have been asked
        # to initialize disks as needed
        if clearPartType in [CLEARPART_TYPE_NONE, None]:
            if not self.config.initializeDisks or not device.isDisk:
                return False

            if not empty_device(device, self.devicetree):
                return False

        if isinstance(device, PartitionDevice):
            # Never clear the special first partition on a Mac disk label, as
            # that holds the partition table itself.
            # Something similar for the third partition on a Sun disklabel.
            if device.isMagic:
                return False

            # We don't want to fool with extended partitions, freespace, &c
            if not device.isPrimary and not device.isLogical:
                return False

            if clearPartType == CLEARPART_TYPE_LINUX and \
               not device.format.linuxNative and \
               not device.getFlag(parted.PARTITION_LVM) and \
               not device.getFlag(parted.PARTITION_RAID) and \
               not device.getFlag(parted.PARTITION_SWAP):
                return False
        elif device.isDisk:
            if device.partitioned and clearPartType != CLEARPART_TYPE_ALL:
                # if clearPartType is not CLEARPART_TYPE_ALL but we'll still be
                # removing every partition from the disk, return True since we
                # will want to be able to create a new disklabel on this disk
                if not empty_device(device, self.devicetree):
                    return False

            # Never clear disks with hidden formats
            if device.format.hidden:
                return False

            # When clearPartType is CLEARPART_TYPE_LINUX and a disk has non-
            # linux whole-disk formatting, do not clear it. The exception is
            # the case of an uninitialized disk when we've been asked to
            # initialize disks as needed
            if clearPartType == CLEARPART_TYPE_LINUX and \
               not (self.config.initializeDisks and
                    device.format.type is None) and \
               not device.partitioned and not device.format.linuxNative:
                return False

        # Don't clear devices holding install media.
        if device.protected:
            return False

        if clearPartType == CLEARPART_TYPE_LIST and \
           device.name not in clearPartDevices:
            return False

        return True

    def recursiveRemove(self, device):
        log.debug("removing %s" % device.name)
        devices = self.deviceDeps(device)

        # this isn't strictly necessary, but it makes the action list easier to
        # read when removing logical partitions because of the automatic
        # renumbering that happens if you remove them in ascending numerical
        # order
        devices.reverse()

        while devices:
            log.debug("devices to remove: %s" % ([d.name for d in devices],))
            leaves = [d for d in devices if d.isleaf]
            log.debug("leaves to remove: %s" % ([d.name for d in leaves],))
            for leaf in leaves:
                self.destroyDevice(leaf)
                devices.remove(leaf)

        if device.isDisk:
            self.devicetree.registerAction(ActionDestroyFormat(device))
        else:
            self.destroyDevice(device)

    def clearPartitions(self):
        """ Clear partitions and dependent devices from disks.

            Arguments:

                None

            NOTES:

                - Needs some error handling

        """
        # Sort partitions by descending partition number to minimize confusing
        # things like multiple "destroy sda5" actions due to parted renumbering
        # partitions. This can still happen through the UI but it makes sense to
        # avoid it where possible.
        partitions = sorted(self.partitions,
                            key=lambda p: p.partedPartition.number,
                            reverse=True)
        for part in partitions:
            log.debug("clearpart: looking at %s" % part.name)
            if not self.shouldClear(part):
                continue

            self.recursiveRemove(part)
            log.debug("partitions: %s" % [p.getDeviceNodeName() for p in part.partedPartition.disk.partitions])

        # now remove any empty extended partitions
        self.removeEmptyExtendedPartitions()

        # ensure all disks have appropriate disklabels
        for disk in self.disks:
            if not self.shouldClear(disk):
                continue

            log.debug("clearpart: initializing %s" % disk.name)
            self.initializeDisk(disk)

        self.updateBootLoaderDiskList()

    def initializeDisk(self, disk):
        """ (Re)initialize a disk by creating a disklabel on it.

            The disk should not contain any partitions except perhaps for a
            magic partitions on mac and sun disklabels.
        """
        # first, remove magic mac/sun partitions from the parted Disk
        if disk.partitioned:
            magic_partitions = {"mac": 1, "sun": 3}
            if disk.format.labelType in magic_partitions:
                number = magic_partitions[disk.format.labelType]
                # remove the magic partition
                for part in disk.format.partitions:
                    if part.disk == disk and part.partedPartition.number == number:
                        log.debug("removing %s" % part.name)
                        # We can't schedule the magic partition for removal
                        # because parted will not allow us to remove it from the
                        # disk. Still, we need it out of the devicetree.
                        self.devicetree._removeDevice(part, moddisk=False)

        if disk.partitioned and disk.format.partitions:
            raise ValueError("cannot initialize a disk that has partitions")

        # remove existing formatting from the disk
        destroy_action = ActionDestroyFormat(disk)
        self.devicetree.registerAction(destroy_action)

        labelType = _platform.bestDiskLabelType(disk)

        # create a new disklabel on the disk
        newLabel = getFormat("disklabel", device=disk.path,
                             labelType=labelType)
        create_action = ActionCreateFormat(disk, format=newLabel)
        self.devicetree.registerAction(create_action)

    def removeEmptyExtendedPartitions(self):
        for disk in self.partitioned:
            log.debug("checking whether disk %s has an empty extended" % disk.name)
            extended = disk.format.extendedPartition
            logical_parts = disk.format.logicalPartitions
            log.debug("extended is %s ; logicals is %s" % (extended, [p.getDeviceNodeName() for p in logical_parts]))
            if extended and not logical_parts:
                log.debug("removing empty extended partition from %s" % disk.name)
                extended_name = devicePathToName(extended.getDeviceNodeName())
                extended = self.devicetree.getDeviceByName(extended_name)
                self.destroyDevice(extended)

    def getFreeSpace(self, disks=None, clearPartType=None):
        """ Return a dict with free space info for each disk.

            The dict values are 2-tuples: (disk_free, fs_free). fs_free is
            space available by shrinking filesystems. disk_free is space not
            allocated to any partition.

            disks and clearPartType allow specifying a set of disks other than
            self.disks and a clearPartType value other than
            self.config.clearPartType.
        """
        from size import Size
        if disks is None:
            disks = self.disks

        if clearPartType is None:
            clearPartType = self.config.clearPartType

        free = {}
        for disk in disks:
            should_clear = self.shouldClear(disk, clearPartType=clearPartType,
                                            clearPartDisks=[disk.name])
            if should_clear:
                free[disk.name] = (Size(spec="%f mb" % disk.size), 0)
                continue

            disk_free = 0
            fs_free = 0
            if disk.partitioned:
                disk_free = disk.format.free
                for partition in [p for p in self.partitions if p.disk == disk]:
                    # only check actual filesystems since lvm &c require a bunch of
                    # operations to translate free filesystem space into free disk
                    # space
                    should_clear = self.shouldClear(partition,
                                                    clearPartType=clearPartType,
                                                    clearPartDisks=[disk.name])
                    if should_clear:
                        disk_free += partition.size
                    elif hasattr(partition.format, "free"):
                        fs_free += partition.format.free
            elif hasattr(disk.format, "free"):
                fs_free = disk.format.free
            elif disk.format.type is None:
                disk_free = disk.size

            free[disk.name] = (Size(spec="%f mb" % disk_free),
                               Size(spec="%f mb" % fs_free))

        return free

    @property
    def names(self):
        return self.devicetree.names

    def exceptionDisks(self):
        """ Return a list of removable devices to save exceptions to.

            FIXME: This raises the problem that the device tree can be
                   in a state that does not reflect that actual current
                   state of the system at any given point.

                   We need a way to provide direct scanning of disks,
                   partitions, and filesystems without relying on the
                   larger objects' correctness.

                   Also, we need to find devices that have just been made
                   available for the purpose of storing the exception
                   report.
        """
        # When a usb is connected from before the start of the installation,
        # it is not correctly detected.
        udev_trigger(subsystem="block", action="change")
        self.reset()

        dests = []

        for disk in self.disks:
            if not disk.removable and \
                    disk.format is not None  and \
                    disk.format.mountable:
                dests.append([disk.path, disk.name])

        for part in self.partitions:
            if not part.disk.removable:
                continue

            elif part.partedPartition.active and \
                    not part.partedPartition.getFlag(parted.PARTITION_RAID) and \
                    not part.partedPartition.getFlag(parted.PARTITION_LVM) and \
                    part.format is not None and part.format.mountable:
                dests.append([part.path, part.name])

        return dests

    def deviceImmutable(self, device, ignoreProtected=False):
        """ Return any reason the device cannot be modified/removed.

            Return False if the device can be removed.

            Devices that cannot be removed include:

                - protected partitions
                - devices that are part of an md array or lvm vg
                - extended partition containing logical partitions that
                  meet any of the above criteria

        """
        if not isinstance(device, Device):
            raise ValueError("arg1 (%s) must be a Device instance" % device)

        if not ignoreProtected and device.protected and \
           not getattr(device.format, "inconsistentVG", False):
            return _("This partition is holding the data for the hard "
                      "drive install.")
        elif isinstance(device, PartitionDevice) and device.isProtected:
            # LDL formatted DASDs always have one partition, you'd have to
            # reformat the DASD in CDL mode to get rid of it
            return _("You cannot delete a partition of a LDL formatted "
                     "DASD.")
        elif device.format.type == "mdmember":
            for array in self.mdarrays + self.mdcontainers:
                if array.dependsOn(device):
                    if array.minor is not None:
                        return _("This device is part of the RAID "
                                 "device %s.") % (array.path,)
                    else:
                        return _("This device is part of a RAID device.")
        elif device.format.type == "lvmpv":
            if device.format.inconsistentVG:
                return _("This device is part of an inconsistent LVM "
                         "Volume Group.")
            for vg in self.vgs:
                if vg.dependsOn(device):
                    if vg.name is not None:
                        return _("This device is part of the LVM "
                                 "volume group '%s'.") % (vg.name,)
                    else:
                        return _("This device is part of a LVM volume "
                                 "group.")
        elif device.format.type == "luks":
            try:
                luksdev = self.devicetree.getChildren(device)[0]
            except IndexError:
                pass
            else:
                return self.deviceImmutable(luksdev)
        elif isinstance(device, PartitionDevice) and device.isExtended:
            reasons = {}
            for dep in self.deviceDeps(device):
                reason = self.deviceImmutable(dep)
                if reason:
                    reasons[dep.path] = reason
            if reasons:
                msg =  _("This device is an extended partition which "
                         "contains logical partitions that cannot be "
                         "deleted:\n\n")
                for dev in reasons:
                    msg += "%s: %s" % (dev, reasons[dev])
                return msg

        return False

    def deviceDeps(self, device):
        return self.devicetree.getDependentDevices(device)

    def newPartition(self, *args, **kwargs):
        """ Return a new PartitionDevice instance for configuring. """
        if kwargs.has_key("fmt_type"):
            kwargs["format"] = getFormat(kwargs.pop("fmt_type"),
                                         mountpoint=kwargs.pop("mountpoint",
                                                               None),
                                         **kwargs.pop("fmt_args", {}))

        if kwargs.has_key("name"):
            name = kwargs.pop("name")
        else:
            name = "req%d" % self.nextID

        if "weight" not in kwargs:
            fmt = kwargs.get("format")
            if fmt:
                mountpoint = getattr(fmt, "mountpoint", None)

                kwargs["weight"] = _platform.weight(mountpoint=mountpoint,
                                                        fstype=fmt.type)


        return PartitionDevice(name, *args, **kwargs)

    def newMDArray(self, *args, **kwargs):
        """ Return a new MDRaidArrayDevice instance for configuring. """
        if kwargs.has_key("fmt_type"):
            kwargs["format"] = getFormat(kwargs.pop("fmt_type"),
                                         mountpoint=kwargs.pop("mountpoint",
                                                               None),
                                         **kwargs.pop("fmt_args", {}))

        name = kwargs.pop("name", None)
        if name:
            safe_name = self.safeDeviceName(name)
            if safe_name != name:
                log.warning("using '%s' instead of specified name '%s'"
                                % (safe_name, name))
                name = safe_name
        else:
            swap = getattr(kwargs.get("format"), "type", None) == "swap"
            mountpoint = getattr(kwargs.get("format"), "mountpoint", None)
            name = self.suggestDeviceName(prefix=shortProductName,
                                          swap=swap,
                                          mountpoint=mountpoint)

        return MDRaidArrayDevice(name, *args, **kwargs)

    def newVG(self, *args, **kwargs):
        """ Return a new LVMVolumeGroupDevice instance. """
        pvs = kwargs.pop("parents", [])
        for pv in pvs:
            if pv not in self.devices:
                raise ValueError("pv is not in the device tree")

        name = kwargs.pop("name", None)
        if name:
            safe_name = self.safeDeviceName(name)
            if safe_name != name:
                log.warning("using '%s' instead of specified name '%s'"
                                % (safe_name, name))
                name = safe_name
        else:
            hostname = ""
            if self.ksdata and self.ksdata.network.hostname is not None:
                hostname = self.ksdata.network.hostname

            name = self.suggestContainerName(hostname=hostname)

        if name in self.names:
            raise ValueError("name already in use")

        return LVMVolumeGroupDevice(name, pvs, *args, **kwargs)

    def newLV(self, *args, **kwargs):
        """ Return a new LVMLogicalVolumeDevice instance. """
        thin_volume = kwargs.pop("thin_volume", False)
        thin_pool = kwargs.pop("thin_pool", False)
        vg = kwargs.get("parents", [None])[0]
        mountpoint = kwargs.pop("mountpoint", None)
        if kwargs.has_key("fmt_type"):
            kwargs["format"] = getFormat(kwargs.pop("fmt_type"),
                                         mountpoint=mountpoint,
                                         **kwargs.pop("fmt_args", {}))

        name = kwargs.pop("name", None)
        if name:
            # make sure the specified name is sensible
            safe_vg_name = self.safeDeviceName(vg.name)
            full_name = "%s-%s" % (safe_vg_name, name)
            safe_name = self.safeDeviceName(full_name)
            if safe_name != full_name:
                new_name = safe_name[len(safe_vg_name)+1:]
                log.warning("using '%s' instead of specified name '%s'"
                                % (new_name, name))
                name = new_name
        else:
            if kwargs.get("format") and kwargs["format"].type == "swap":
                swap = True
            else:
                swap = False

            prefix = ""
            if thin_pool:
                prefix = "pool"

            name = self.suggestDeviceName(parent=vg,
                                          swap=swap,
                                          mountpoint=mountpoint,
                                          prefix=prefix)

        if "%s-%s" % (vg.name, name) in self.names:
            raise ValueError("name already in use")

        if thin_pool:
            device_class = LVMThinPoolDevice
        elif thin_volume:
            device_class = LVMThinLogicalVolumeDevice
        else:
            device_class = LVMLogicalVolumeDevice

        return device_class(name, *args, **kwargs)

    def newBTRFS(self, *args, **kwargs):
        """ Return a new BTRFSVolumeDevice or BRFSSubVolumeDevice. """
        log.debug("newBTRFS: args = %s ; kwargs = %s" % (args, kwargs))
        name = kwargs.pop("name", None)
        if args:
            name = args[0]

        mountpoint = kwargs.pop("mountpoint", None)

        fmt_args = kwargs.pop("fmt_args", {})
        fmt_args.update({"mountpoint": mountpoint})

        if kwargs.pop("subvol", False):
            dev_class = BTRFSSubVolumeDevice
            # make sure there's a valid parent device
            parents = kwargs.get("parents", [])
            if not parents or len(parents) != 1 or \
               not isinstance(parents[0], BTRFSVolumeDevice):
                raise ValueError("new btrfs subvols require a parent volume")

            # set up the subvol name, using mountpoint if necessary
            if not name:
                # for btrfs this only needs to ensure the subvol name is not
                # already in use within the parent volume
                name = self.suggestDeviceName(mountpoint=mountpoint)
            fmt_args["mountopts"] = "subvol=%s" % name
            kwargs.pop("metaDataLevel", None)
            kwargs.pop("dataLevel", None)
        else:
            dev_class = BTRFSVolumeDevice
            # set up the volume label, using hostname if necessary
            if not name:
                hostname = ""
                if self.ksdata and self.ksdata.network.hostname is not None:
                    hostname = self.ksdata.network.hostname

                name = self.suggestContainerName(hostname=hostname)
            if "label" not in fmt_args:
                fmt_args["label"] = name

        # discard fmt_type since it's btrfs always
        kwargs.pop("fmt_type", None)

        # this is to avoid auto-scheduled format create actions
        device = dev_class(name, **kwargs)
        device.format = getFormat("btrfs", **fmt_args)
        return device

    def newBTRFSSubVolume(self, *args, **kwargs):
        kwargs["subvol"] = True
        return self.newBTRFS(*args, **kwargs)

    def newTmpFS(self, *args, **kwargs):
        """ Return a new TmpFSDevice. """
        return TmpFSDevice(*args, **kwargs)

    def createDevice(self, device):
        """ Schedule creation of a device.

            TODO: We could do some things here like assign the next
                  available raid minor if one isn't already set.
        """
        self.devicetree.registerAction(ActionCreateDevice(device))
        if device.format.type:
            self.devicetree.registerAction(ActionCreateFormat(device))

    def destroyDevice(self, device):
        """ Schedule destruction of a device. """
        if device.format.exists and device.format.type:
            # schedule destruction of any formatting while we're at it
            self.devicetree.registerAction(ActionDestroyFormat(device))

        action = ActionDestroyDevice(device)
        self.devicetree.registerAction(action)

    def formatDevice(self, device, format):
        """ Schedule formatting of a device. """
        self.devicetree.registerAction(ActionDestroyFormat(device))
        self.devicetree.registerAction(ActionCreateFormat(device, format))

    def resetDevice(self, device):
        """ Cancel all scheduled actions and reset formatting. """
        actions = self.devicetree.findActions(device=device)
        for action in reversed(actions):
            self.devicetree.cancelAction(action)

        # make sure any random overridden attributes are reset
        device.format = copy.copy(device.originalFormat)

    def resizeDevice(self, device, new_size):
        classes = []
        if device.resizable:
            classes.append(ActionResizeDevice)

        if device.format.resizable:
            classes.append(ActionResizeFormat)

        if not classes:
            raise ValueError("device cannot be resized")

        # if this is a shrink, schedule the format resize first
        if new_size < device.size:
            classes.reverse()

        for action_class in classes:
            self.devicetree.registerAction(action_class(device, new_size))

    def formatByDefault(self, device):
        """Return whether the device should be reformatted by default."""
        formatlist = ['/boot', '/var', '/tmp', '/usr']
        exceptlist = ['/home', '/usr/local', '/opt', '/var/www']

        if not device.format.linuxNative:
            return False

        if device.format.mountable:
            if not device.format.mountpoint:
                return False

            if device.format.mountpoint == "/" or \
               device.format.mountpoint in formatlist:
                return True

            for p in formatlist:
                if device.format.mountpoint.startswith(p):
                    for q in exceptlist:
                        if device.format.mountpoint.startswith(q):
                            return False
                    return True
        elif device.format.type == "swap":
            return True

        # be safe for anything else and default to off
        return False

    def mustFormat(self, device):
        """ Return a string explaining why the device must be reformatted.

            Return None if the device need not be reformatted.
        """
        if device.format.mountable and device.format.mountpoint == "/":
            return _("You must create a new filesystem on the root device.")

        return None

    def extendedPartitionsSupported(self):
        """ Return whether any disks support extended partitions."""
        for disk in self.partitioned:
            if disk.format.partedDisk.supportsFeature(parted.DISK_TYPE_EXTENDED):
                return True
        return False

    def safeDeviceName(self, name):
        """ Convert a device name to something safe and return that.

            LVM limits lv names to 128 characters. I don't know the limits for
            the other various device types, so I'm going to pick a number so
            that we don't have to have an entire fucking library to determine
            device name limits.
        """
        max_len = 96    # No, you don't need longer names than this. Really.
        tmp = name.strip()
        tmp = tmp.replace("/", "_")
        tmp = re.sub("[^0-9a-zA-Z._-]", "", tmp)
        tmp = tmp.lstrip("_")

        if len(tmp) > max_len:
            tmp = tmp[:max_len]

        return tmp

    def suggestContainerName(self, hostname=None, prefix=""):
        """ Return a reasonable, unused device name. """
        if not prefix:
            prefix = shortProductName

        # try to create a device name incorporating the hostname
        if hostname not in (None, "", 'localhost', 'localhost.localdomain'):
            template = "%s_%s" % (prefix, hostname.split('.')[0].lower())
            template = self.safeDeviceName(template)
        else:
            template = prefix

        if flags.image_install:
            template = "%s_image" % template

        names = self.names
        name = template
        if name in names:
            name = None
            for i in range(100):
                tmpname = "%s%02d" % (template, i,)
                if tmpname not in names:
                    name = tmpname
                    break

            if not name:
                log.error("failed to create device name based on prefix "
                          "'%s' and hostname '%s'" % (prefix, hostname))
                raise RuntimeError("unable to find suitable device name")

        return name

    def suggestDeviceName(self, parent=None, swap=None,
                                  mountpoint=None, prefix=""):
        """ Return a suitable, unused name for a new logical volume. """
        body = ""
        if mountpoint:
            if mountpoint == "/":
                body = "root"
            else:
                body = mountpoint[1:].replace("/", "_")
        elif swap:
            body = "swap"

        if prefix and body:
            body = "_" + body

        template = self.safeDeviceName(prefix + body)
        names = self.names
        name = template
        def full_name(name, parent):
            full = ""
            if parent:
                full = "%s-" % parent.name
            full += name
            return full

        # also include names of any lvs in the parent for the case of the
        # temporary vg in the lvm dialogs, which can contain lvs that are
        # not yet in the devicetree and therefore not in self.names
        if full_name(name, parent) in names or not body:
            for i in range(100):
                name = "%s%02d" % (template, i)
                if full_name(name, parent) not in names:
                    break
                else:
                    name = ""

            if not name:
                log.error("failed to create device name based on parent '%s', "
                          "prefix '%s', mountpoint '%s', swap '%s'"
                          % (parent.name, prefix, mountpoint, swap))
                raise RuntimeError("unable to find suitable device name")

        return name

    def savePassphrase(self, device):
        """ Save a device's LUKS passphrase in case of reset. """
        passphrase = device.format._LUKS__passphrase
        self.__luksDevs[device.format.uuid] = passphrase
        self.devicetree._DeviceTree__luksDevs[device.format.uuid] = passphrase
        self.devicetree._DeviceTree__passphrases.append(passphrase)

    def doEncryptionPassphraseRetrofits(self):
        """ Add the global passphrase to all preexisting LUKS devices.

            This establishes a common passphrase for all encrypted devices
            in the system so that users only have to enter one passphrase
            during system boot.
        """
        if not self.encryptionRetrofit:
            return

        for device in self.devices:
            if device.format.type == "luks" and \
               device.format._LUKS__passphrase != self.encryptionPassphrase:
                log.info("adding new passphrase to preexisting encrypted "
                         "device %s" % device.path)
                try:
                    device.format.addPassphrase(self.encryptionPassphrase)
                except CryptoError:
                    log.error("failed to add new passphrase to existing "
                              "device %s" % device.path)

    def setupDiskImages(self):
        self.devicetree.setDiskImages(self.config.diskImages)
        self.devicetree.setupDiskImages()

    @property
    def fileSystemFreeSpace(self):
        mountpoints = ["/", "/usr"]
        free = 0
        btrfs_volumes = []
        for mountpoint in mountpoints:
            device = self.mountpoints.get(mountpoint)
            if not device:
                continue

            # don't count the size of btrfs volumes repeatedly when multiple
            # subvolumes are present
            if isinstance(device, BTRFSSubVolumeDevice):
                if device.volume in btrfs_volumes:
                    continue
                else:
                    btrfs_volumes.append(device.volume)

            if device.format.exists:
                free += device.format.free
            else:
                free += device.size

        return free

    def sanityCheck(self):
        """ Run a series of tests to verify the storage configuration.

            This function is called at the end of partitioning so that
            we can make sure you don't have anything silly (like no /,
            a really small /, etc).  Returns (errors, warnings) where
            each is a list of strings.
        """
        warnings = []
        errors = []

        if not flags.installer_mode:
            return (errors, warnings)

        checkSizes = [('/usr', 250), ('/tmp', 50), ('/var', 384),
                      ('/home', 100), ('/boot', 75)]
        mustbeonlinuxfs = ['/', '/var', '/tmp', '/usr', '/home', '/usr/share', '/usr/lib']
        mustbeonroot = ['/bin','/dev','/sbin','/etc','/lib','/root', '/mnt', 'lost+found', '/proc']

        filesystems = self.mountpoints
        root = self.fsset.rootDevice
        swaps = self.fsset.swapDevices
        try:
            boot = self.bootDevice
        except (DeviceError, AttributeError):
            boot = None

        if root:
            if root.size < 250:
                warnings.append(_("Your root partition is less than 250 "
                                  "megabytes which is usually too small to "
                                  "install %s.") % (productName,))
        else:
            errors.append(_("You have not defined a root partition (/), "
                            "which is required for installation of %s "
                            "to continue.") % (productName,))

        # Prevent users from installing on s390x with (a) no /boot volume, (b) the
        # root volume on LVM, and (c) the root volume not restricted to a single
        # PV
        # NOTE: There is not really a way for users to create a / volume
        # restricted to a single PV.  The backend support is there, but there are
        # no UI hook-ups to drive that functionality, but I do not personally
        # care.  --dcantrell
        if arch.isS390() and not self.mountpoints.has_key('/boot') and root:
            if root.type == 'lvmlv' and not root.singlePV:
                errors.append(_("This platform requires /boot on a dedicated "
                                "partition or logical volume.  If you do not "
                                "want a /boot volume, you must place / on a "
                                "dedicated non-LVM partition."))

        # FIXME: put a check here for enough space on the filesystems. maybe?

        for (mount, size) in checkSizes:
            if mount in filesystems and filesystems[mount].size < size:
                warnings.append(_("Your %(mount)s partition is less than "
                                  "%(size)s megabytes which is lower than "
                                  "recommended for a normal %(productName)s "
                                  "install.")
                                % {'mount': mount, 'size': size,
                                   'productName': productName})

        for (mount, device) in filesystems.items():
            problem = filesystems[mount].checkSize()
            if problem < 0:
                errors.append(_("Your %(mount)s partition is too small for %(format)s formatting "
                                "(allowable size is %(minSize)d MB to %(maxSize)d MB)")
                              % {"mount": mount, "format": device.format.name,
                                 "minSize": device.minSize, "maxSize": device.maxSize})
            elif problem > 0:
                errors.append(_("Your %(mount)s partition is too large for %(format)s formatting "
                                "(allowable size is %(minSize)d MB to %(maxSize)d MB)")
                              % {"mount":mount, "format": device.format.name,
                                 "minSize": device.minSize, "maxSize": device.maxSize})

        # FIXME: this does not work, but probably should
        """
        usb_disks = []
        firewire_disks = []
        #for disk in self.disks:
            if isys.driveUsesModule(disk.name, ["usb-storage", "ub"]):
                usb_disks.append(disk)
            elif isys.driveUsesModule(disk.name, ["sbp2", "firewire-sbp2"]):
                firewire_disks.append(disk)

        uses_usb = False
        uses_firewire = False
        for device in filesystems.values():
            for disk in usb_disks:
                if device.dependsOn(disk):
                    uses_usb = True
                    break

            for disk in firewire_disks:
                if device.dependsOn(disk):
                    uses_firewire = True
                    break

        if uses_usb:
            warnings.append(_("Installing on a USB device.  This may "
                              "or may not produce a working system."))
        if uses_firewire:
            warnings.append(_("Installing on a FireWire device.  This may "
                              "or may not produce a working system."))
        """

        if self.bootloader and not self.bootloader.skip_bootloader:
            stage1 = self.bootloader.stage1_device
            if not stage1:
                errors.append(_("you have not created a bootloader stage1 "
                                "target device"))
            else:
                self.bootloader.is_valid_stage1_device(stage1)
                errors.extend(self.bootloader.errors)
                warnings.extend(self.bootloader.warnings)

            stage2 = self.bootloader.stage2_device
            if not stage2:
                errors.append(_("You have not created a bootable partition."))
            else:
                self.bootloader.is_valid_stage2_device(stage2)
                errors.extend(self.bootloader.errors)
                warnings.extend(self.bootloader.warnings)
                if not self.bootloader.check():
                    errors.extend(self.bootloader.errors)

            #
            # check that GPT boot disk on BIOS system has a BIOS boot partition
            #
            if _platform.weight(fstype="biosboot") and \
               stage1 and stage1.isDisk and \
               getattr(stage1.format, "labelType", None) == "gpt":
                missing = True
                for part in [p for p in self.partitions if p.disk == stage1]:
                    if part.format.type == "biosboot":
                        missing = False
                        break

                if missing:
                    errors.append(_("Your BIOS-based system needs a special "
                                    "partition to boot from a GPT disk label. "
                                    "To continue, please create a 1MB "
                                    "'biosboot' type partition."))

        if not swaps:
            installed = Size(spec="%s kb" % util.total_memory())
            required = Size(spec="%s kb" % isys.EARLY_SWAP_RAM)

            if installed < required:
                errors.append(_("You have not specified a swap partition.  "
                                "%(requiredMem)s MB of memory is required to continue installation "
                                "without a swap partition, but you only have %(installedMem)s MB.")
                              % {"requiredMem": int(required.convertTo(spec="MB")),
                                 "installedMem": int(installed.convertTo(spec="MB"))})
            else:
                warnings.append(_("You have not specified a swap partition.  "
                                  "Although not strictly required in all cases, "
                                  "it will significantly improve performance "
                                  "for most installations."))
        no_uuid = [s for s in swaps if s.format.exists and not s.format.uuid]
        if no_uuid:
            warnings.append(_("At least one of your swap devices does not have "
                              "a UUID, which is common in swap space created "
                              "using older versions of mkswap. These devices "
                              "will be referred to by device path in "
                              "/etc/fstab, which is not ideal since device "
                              "paths can change under a variety of "
                              "circumstances. "))

        for (mountpoint, dev) in filesystems.items():
            if mountpoint in mustbeonroot:
                errors.append(_("This mount point is invalid.  The %s directory must "
                                "be on the / file system.") % mountpoint)

            if mountpoint in mustbeonlinuxfs and (not dev.format.mountable or not dev.format.linuxNative):
                errors.append(_("The mount point %s must be on a linux file system.") % mountpoint)

        if self.rootDevice and self.rootDevice.format.exists:
            e = self.mustFormat(self.rootDevice)
            if e:
                errors.append(e)

        return (errors, warnings)

    def isProtected(self, device):
        """ Return True is the device is protected. """
        return device.protected

    def checkNoDisks(self):
        """Check that there are valid disk devices."""
        if not self.disks:
            raise NoDisksError()

    def dumpState(self, suffix):
        """ Dump the current device list to the storage shelf. """
        key = "devices.%d.%s" % (time.time(), suffix)
        with contextlib.closing(shelve.open(self._dumpFile)) as shelf:
            shelf[key] = [d.dict for d in self.devices]

    @property
    def packages(self):
        pkgs = set()
        pkgs.update(_platform.packages)

        if self.bootloader:
            pkgs.update(self.bootloader.packages)

        # install support packages for all devices in the system
        for device in self.devices:
            # this takes care of device and filesystem packages
            pkgs.update(device.packages)

        return list(pkgs)

    def write(self):
        if not os.path.isdir("%s/etc" % ROOT_PATH):
            os.mkdir("%s/etc" % ROOT_PATH)

        self.fsset.write()
        self.makeMtab()
        self.iscsi.write(ROOT_PATH, self)
        self.fcoe.write(ROOT_PATH)
        self.zfcp.write(ROOT_PATH)
        self.dasd.write(ROOT_PATH)

    def turnOnSwap(self, upgrading=None):
        self.fsset.turnOnSwap(rootPath=ROOT_PATH,
                              upgrading=upgrading)

    def mountFilesystems(self, raiseErrors=None, readOnly=None, skipRoot=False):
        self.fsset.mountFilesystems(rootPath=ROOT_PATH,
                                    raiseErrors=raiseErrors,
                                    readOnly=readOnly, skipRoot=skipRoot)

    def umountFilesystems(self, ignoreErrors=True, swapoff=True):
        self.fsset.umountFilesystems(ignoreErrors=ignoreErrors, swapoff=swapoff)

    def parseFSTab(self, chroot=None):
        self.fsset.parseFSTab(chroot=chroot)

    def mkDevRoot(self):
        self.fsset.mkDevRoot()

    def createSwapFile(self, device, size):
        self.fsset.createSwapFile(device, size)

    @property
    def bootloader(self):
        if self._bootloader is None and flags.installer_mode:
            self._bootloader = get_bootloader()

        return self._bootloader

    def updateBootLoaderDiskList(self):
        if not self.bootloader:
            return

        boot_disks = [d for d in self.disks if d.partitioned]
        boot_disks.sort(cmp=self.compareDisks, key=lambda d: d.name)
        self.bootloader.set_disk_list(boot_disks)

    def setUpBootLoader(self):
        """ Propagate ksdata into BootLoader.

            :raises BootloaderError: if stage1 setup fails
        """
        if not self.bootloader or not self.ksdata:
            log.warning("either ksdata or bootloader data missing")
            return

        if self.bootloader.skip_bootloader:
            log.info("user specified that bootloader install be skipped")
            return

        # Need to make sure bootDrive has been setup from the latest information
        self.ksdata.bootloader.execute(self, self.ksdata, None)
        self.bootloader.stage1_disk = self.devicetree.resolveDevice(self.ksdata.bootloader.bootDrive)
        self.bootloader.stage2_device = self.bootDevice
        self.bootloader.set_stage1_device(self.devices)

    @property
    def bootDisk(self):
        disk = None
        if self.ksdata:
            spec = self.ksdata.bootloader.bootDrive
            disk = self.devicetree.resolveDevice(spec)
        return disk

    @property
    def bootDevice(self):
        dev = None
        if self.fsset:
            dev = self.mountpoints.get("/boot", self.rootDevice)
        return dev

    @property
    def bootLoaderDevice(self):
        return getattr(self.bootloader, "stage1_device", None)

    @property
    def bootFSTypes(self):
        """A list of all valid filesystem types for the boot partition."""
        fstypes = []
        if self.bootloader:
            fstypes = self.bootloader.stage2_format_types
        return fstypes

    @property
    def defaultBootFSType(self):
        """The default filesystem type for the boot partition."""
        fstype = None
        if self.bootloader:
            fstype = self.bootFSTypes[0]
        return fstype

    @property
    def defaultFSType(self):
        return self._defaultFSType

    def setDefaultFSType(self, newtype):
        """ Set the default fstype for this instance.

            Raise ValueError on invalid input.
        """
        log.debug("trying to set new default fstype to '%s'" % newtype)
        fmt = getFormat(newtype)
        if fmt.type is None:
            raise ValueError("unrecognized value for new default fs type")

        if (not fmt.mountable or not fmt.formattable or not fmt.supported or
            not fmt.linuxNative):
            log.debug("invalid default fstype: %r" % fmt)
            raise ValueError("new value is not valid as a default fs type")

        self._defaultFSType = newtype

    @property
    def mountpoints(self):
        return self.fsset.mountpoints

    @property
    def rootDevice(self):
        return self.fsset.rootDevice

    def makeMtab(self):
        path = "/etc/mtab"
        target = "/proc/self/mounts"
        path = os.path.normpath("%s/%s" % (ROOT_PATH, path))

        if os.path.islink(path):
            # return early if the mtab symlink is already how we like it
            current_target = os.path.normpath(os.path.dirname(path) +
                                              "/" + os.readlink(path))
            if current_target == target:
                return

        if os.path.exists(path):
            os.unlink(path)

        os.symlink(target, path)

    def compareDisks(self, first, second):
        if self.eddDict.has_key(first) and self.eddDict.has_key(second):
            one = self.eddDict[first]
            two = self.eddDict[second]
            if (one < two):
                return -1
            elif (one > two):
                return 1

        # if one is in the BIOS and the other not prefer the one in the BIOS
        if self.eddDict.has_key(first):
            return -1
        if self.eddDict.has_key(second):
            return 1

        if first.startswith("hd"):
            type1 = 0
        elif first.startswith("sd"):
            type1 = 1
        elif (first.startswith("vd") or first.startswith("xvd")):
            type1 = -1
        else:
            type1 = 2

        if second.startswith("hd"):
            type2 = 0
        elif second.startswith("sd"):
            type2 = 1
        elif (second.startswith("vd") or second.startswith("xvd")):
            type2 = -1
        else:
            type2 = 2

        if (type1 < type2):
            return -1
        elif (type1 > type2):
            return 1
        else:
            len1 = len(first)
            len2 = len(second)

            if (len1 < len2):
                return -1
            elif (len1 > len2):
                return 1
            else:
                if (first < second):
                    return -1
                elif (first > second):
                    return 1

        return 0

    def getFSType(self, mountpoint=None):
        """ Return the default filesystem type based on mountpoint. """
        fstype = self.defaultFSType
        if not mountpoint:
            # just return the default
            pass
        elif mountpoint.lower() in ("swap", "biosboot", "prepboot"):
            fstype = mountpoint.lower()
        elif mountpoint == "/boot":
            fstype = self.defaultBootFSType
        elif mountpoint == "/boot/efi":
            if arch.isMactel():
                fstype = "hfs+"
            else:
                fstype = "efi"

        return fstype

    def factoryDevice(self, device_type, size, **kwargs):
        """ Schedule creation of a device based on a top-down specification.

            Arguments:

                device_type         an AUTOPART_TYPE constant (lvm|btrfs|plain)
                size                device's requested size

            Keyword arguments:

                mountpoint          the device's mountpoint
                fstype              the device's filesystem type, or swap
                label               filesystem label
                disks               the set of disks we can allocate from
                encrypted           boolean

                raid_level          (btrfs/md/lvm only) RAID level (string)

                name                name for new device
                container_name      name of requested container

                device              an already-defined but non-existent device
                                    to adjust instead of creating a new device

        """
        log_method_call(self, device_type, size, **kwargs)

        # we can't do anything with existing devices
        #if device and device.exists:
        #    log.info("factoryDevice refusing to change device %s" % device)
        #    return

        if not kwargs.get("fstype"):
            kwargs["fstype"] = self.getFSType(mountpoint=kwargs.get("mountpoint"))
            if kwargs["fstype"] == "swap":
                kwargs["mountpoint"] = None

        if kwargs["fstype"] == "swap" and \
           device_type == devicefactory.DEVICE_TYPE_BTRFS:
            device_type = devicefactory.DEVICE_TYPE_PARTITION

        factory = devicefactory.get_device_factory(self, device_type, size,
                                                   **kwargs)

        if not factory.disks:
            raise StorageError("no disks specified for new device")

        self.size_sets = [] # clear this since there are no growable reqs now
        factory.configure()
        return factory.device

    def copy(self):
        log.debug("starting Blivet copy")
        new = copy.deepcopy(self)
        # go through and re-get partedPartitions from the disks since they
        # don't get deep-copied
        hidden_partitions = [d for d in new.devicetree._hidden
                                if isinstance(d, PartitionDevice)]
        for partition in new.partitions + hidden_partitions:
            if not partition._partedPartition:
                continue

            # update the refs in req_disks as well
            req_disks = []
            for disk in partition.req_disks:
                req_disks.append(new.devicetree.getDeviceByID(disk.id))

            partition.req_disks = req_disks

            p = partition.disk.format.partedDisk.getPartitionByPath(partition.path)
            partition.partedPartition = p

        # make sure the roots reference devices in self.devicetree._devices
        for root in new.roots:
            root.swaps = [new.devicetree.getDeviceByID(d.id) for d in root.swaps]
            # the above will put None in the list if the swap has since been
            # removed, which is problematic
            root.swaps = [s for s in root.swaps if s]

            for (mountpoint, old_dev) in root.mounts.items():
                if old_dev is None:
                    continue

                new_dev = new.devicetree.getDeviceByID(old_dev.id)
                if new_dev is None:
                    # if the device has been removed don't include this
                    # mountpoint at all
                    del root.mounts[mountpoint]

                root.mounts[mountpoint] = new_dev

        log.debug("finished Blivet copy")
        return new

    def getActiveMounts(self):
        """ Reflect active mounts in the appropriate devices' formats. """
        log.info("collecting information about active mounts")
        for line in open("/proc/mounts").readlines():
            try:
                (devspec, mountpoint, fstype, options, rest) = line.split(None,
                                                                          4)
            except IndexError:
                blivet_log.error("failed to parse /proc/mounts line: %s" % line)
                continue

            if fstype == "btrfs":
                # get the subvol name from /proc/self/mountinfo
                for line in open("/proc/self/mountinfo").readlines():
                    fields = line.split()
                    _subvol = fields[3]
                    _mountpoint = fields[4]
                    _devspec = fields[9]
                    if _mountpoint == mountpoint and _devspec == devspec:
                        log.debug("subvol %s" % _subvol)
                        options += ",subvol=%s" % _subvol[1:]

            if fstype in nodev_filesystems:
                if not flags.include_nodev:
                    continue

                log.info("found nodev %s filesystem mounted at %s"
                            % (fstype, mountpoint))
                # nodev filesystems require some special handling.
                # For now, a lot of this is based on the idea that it's a losing
                # battle to require the presence of an FS class for every type
                # of nodev filesystem. Based on that idea, we just instantiate
                # NoDevFS directly and then hack in the fstype as the device
                # attribute.
                fmt = getFormat("nodev")
                fmt.device = fstype

                # NoDevice also needs some special works since they don't have
                # per-instance names in the kernel.
                device = NoDevice(format=fmt)
                n = len([d for d in self.devices if d.format.type == fstype])
                device._name += ".%d" % n
                self.devicetree._addDevice(device)
                devspec = device.name

            device = self.devicetree.resolveDevice(devspec, options=options)
            if device is not None:
                device.format.mountpoint = mountpoint   # for future mounts
                device.format._mountpoint = mountpoint  # active mountpoint
                device.format.mountopts = options

    def updateKSData(self):
        """ Update ksdata to reflect the settings of this Blivet instance. """
        if not self.ksdata or not self.mountpoints:
            return

        # clear out whatever was there before
        self.ksdata.partition.partitions = []
        self.ksdata.logvol.lvList = []
        self.ksdata.raid.raidList = []
        self.ksdata.volgroup.vgList = []
        self.ksdata.btrfs.btrfsList = []

        # iscsi?
        # fcoe?
        # zfcp?
        # dmraid?

        # bootloader

        # ignoredisk
        if self.config.ignoredDisks:
            self.ksdata.ignoredisk.drives = self.config.ignoredDisks[:]
        elif self.config.exclusiveDisks:
            self.ksdata.ignoredisk.onlyuse = self.config.exclusiveDisks[:]

        # autopart
        self.ksdata.autopart.autopart = self.doAutoPart
        self.ksdata.autopart.type = self.autoPartType
        self.ksdata.autopart.encrypted = self.encryptedAutoPart

        # clearpart
        self.ksdata.clearpart.type = self.config.clearPartType
        self.ksdata.clearpart.drives = self.config.clearPartDisks[:]
        self.ksdata.clearpart.devices = self.config.clearPartDevices[:]
        self.ksdata.clearpart.initAll = self.config.initializeDisks
        if self.ksdata.clearpart.type == CLEARPART_TYPE_NONE:
            # Make a list of initialized disks and of removed partitions. If any
            # partitions were removed from disks that were not completely
            # cleared we'll have to use CLEARPART_TYPE_LIST and provide a list
            # of all removed partitions. If no partitions were removed from a
            # disk that was not cleared/reinitialized we can use
            # CLEARPART_TYPE_ALL.
            self.ksdata.clearpart.devices = []
            self.ksdata.clearpart.drives = []
            fresh_disks = [d.name for d in self.disks if d.partitioned and
                                                         not d.format.exists]

            destroy_actions = self.devicetree.findActions(type="destroy",
                                                          object="device")

            cleared_partitions = []
            partial = False
            for action in destroy_actions:
                if action.device.type == "partition":
                    if action.device.disk.name not in fresh_disks:
                        partial = True

                    cleared_partitions.append(action.device.name)

            if not destroy_actions:
                pass
            elif partial:
                # make a list of removed partitions
                self.ksdata.clearpart.type = CLEARPART_TYPE_LIST
                self.ksdata.clearpart.devices = cleared_partitions
            else:
                # if they didn't partially clear any disks, use the shorthand
                self.ksdata.clearpart.type = CLEARPART_TYPE_ALL
                self.ksdata.clearpart.drives = fresh_disks

        if self.doAutoPart:
            return

        # custom storage
        ksMap = {PartitionDevice: ("PartData", "partition"),
                 LVMLogicalVolumeDevice: ("LogVolData", "logvol"),
                 LVMVolumeGroupDevice: ("VolGroupData", "volgroup"),
                 MDRaidArrayDevice: ("RaidData", "raid"),
                 BTRFSDevice: ("BTRFSData", "btrfs")}

        # make a list of ancestors of all used devices
        devices = list(set(a for d in self.mountpoints.values() + self.swaps
                                for a in d.ancestors))
        devices.sort(key=lambda d: len(d.ancestors))
        for device in devices:
            class_attr = None
            list_attr = None
            for cls in ksMap.iterkeys():
                if isinstance(device, cls):
                    class_attr, list_attr = ksMap[cls]
                    break

            if not class_attr or not list_attr:
                log.info("omitting ksdata: %s" % device)
                continue

            cls = getattr(self.ksdata, class_attr)
            data = cls()    # all defaults

            device.populateKSData(data)

            parent = getattr(self.ksdata, list_attr)
            parent.dataList().append(data)

def mountExistingSystem(fsset, rootDevice,
                        allowDirty=None, dirtyCB=None,
                        readOnly=None):
    """ Mount filesystems specified in rootDevice's /etc/fstab file. """
    rootPath = ROOT_PATH
    if dirtyCB is None:
        dirtyCB = lambda l: False

    if readOnly:
        readOnly = "ro"
    else:
        readOnly = ""

    if rootDevice.protected and os.path.ismount("/mnt/install/isodir"):
        util.mount("/mnt/install/isodir",
                   rootPath,
                   fstype=rootDevice.format.type,
                   options="bind")
    else:
        rootDevice.setup()
        rootDevice.format.mount(chroot=rootPath,
                                mountpoint="/",
                                options=readOnly)

    fsset.parseFSTab()

    # check for dirty filesystems
    dirtyDevs = []
    for device in fsset.mountpoints.values():
        if not hasattr(device.format, "needsFSCheck"):
            continue

        try:
            device.setup()
        except DeviceError as e:
            # we'll catch this in the main loop
            continue

        if device.format.needsFSCheck:
            log.info("%s contains a dirty %s filesystem" % (device.path,
                                                            device.format.type))
            dirtyDevs.append(device.path)

    if dirtyDevs and (not allowDirty or dirtyCB(dirtyDevs)):
        raise DirtyFSError("\n".join(dirtyDevs))

    fsset.mountFilesystems(rootPath=ROOT_PATH, readOnly=readOnly, skipRoot=True)


class BlkidTab(object):
    """ Dictionary-like interface to blkid.tab with device path keys """
    def __init__(self, chroot=""):
        self.chroot = chroot
        self.devices = {}

    def parse(self):
        path = "%s/etc/blkid/blkid.tab" % self.chroot
        log.debug("parsing %s" % path)
        with open(path) as f:
            for line in f.readlines():
                # this is pretty ugly, but an XML parser is more work than
                # is justifiable for this purpose
                if not line.startswith("<device "):
                    continue

                line = line[len("<device "):-len("</device>\n")]
                (data, sep, device) = line.partition(">")
                if not device:
                    continue

                self.devices[device] = {}
                for pair in data.split():
                    try:
                        (key, value) = pair.split("=")
                    except ValueError:
                        continue

                    self.devices[device][key] = value[1:-1] # strip off quotes

    def __getitem__(self, key):
        return self.devices[key]

    def get(self, key, default=None):
        return self.devices.get(key, default)


class CryptTab(object):
    """ Dictionary-like interface to crypttab entries with map name keys """
    def __init__(self, devicetree, blkidTab=None, chroot=""):
        self.devicetree = devicetree
        self.blkidTab = blkidTab
        self.chroot = chroot
        self.mappings = {}

    def parse(self, chroot=""):
        """ Parse /etc/crypttab from an existing installation. """
        if not chroot or not os.path.isdir(chroot):
            chroot = ""

        path = "%s/etc/crypttab" % chroot
        log.debug("parsing %s" % path)
        with open(path) as f:
            if not self.blkidTab:
                try:
                    self.blkidTab = BlkidTab(chroot=chroot)
                    self.blkidTab.parse()
                except Exception:
                    self.blkidTab = None

            for line in f.readlines():
                (line, pound, comment) = line.partition("#")
                fields = line.split()
                if not 2 <= len(fields) <= 4:
                    continue
                elif len(fields) == 2:
                    fields.extend(['none', ''])
                elif len(fields) == 3:
                    fields.append('')

                (name, devspec, keyfile, options) = fields

                # resolve devspec to a device in the tree
                device = self.devicetree.resolveDevice(devspec,
                                                       blkidTab=self.blkidTab)
                if device:
                    self.mappings[name] = {"device": device,
                                           "keyfile": keyfile,
                                           "options": options}

    def populate(self):
        """ Populate the instance based on the device tree's contents. """
        for device in self.devicetree.devices:
            # XXX should we put them all in there or just the ones that
            #     are part of a device containing swap or a filesystem?
            #
            #       Put them all in here -- we can filter from FSSet
            if device.format.type != "luks":
                continue

            key_file = device.format.keyFile
            if not key_file:
                key_file = "none"

            options = device.format.options
            if not options:
                options = ""

            self.mappings[device.format.mapName] = {"device": device,
                                                    "keyfile": key_file,
                                                    "options": options}

    def crypttab(self):
        """ Write out /etc/crypttab """
        crypttab = ""
        for name in self.mappings:
            entry = self[name]
            crypttab += "%s UUID=%s %s %s\n" % (name,
                                                entry['device'].format.uuid,
                                                entry['keyfile'],
                                                entry['options'])
        return crypttab                       

    def __getitem__(self, key):
        return self.mappings[key]

    def get(self, key, default=None):
        return self.mappings.get(key, default)

def get_containing_device(path, devicetree):
    """ Return the device that a path resides on. """
    if not os.path.exists(path):
        return None

    st = os.stat(path)
    major = os.major(st.st_dev)
    minor = os.minor(st.st_dev)
    link = "/sys/dev/block/%s:%s" % (major, minor)
    if not os.path.exists(link):
        return None

    try:
        device_name = os.path.basename(os.readlink(link))
    except Exception:
        return None

    if device_name.startswith("dm-"):
        # have I told you lately that I love you, device-mapper?
        device_name = name_from_dm_node(device_name)

    return devicetree.getDeviceByName(device_name)


class FSSet(object):
    """ A class to represent a set of filesystems. """
    def __init__(self, devicetree):
        self.devicetree = devicetree
        self.cryptTab = None
        self.blkidTab = None
        self.origFStab = None
        self.active = False
        self._dev = None
        self._devpts = None
        self._sysfs = None
        self._proc = None
        self._devshm = None
        self._usb = None
        self._selinux = None
        self._run = None
        self.preserveLines = []     # lines we just ignore and preserve

    @property
    def sysfs(self):
        if not self._sysfs:
            self._sysfs = NoDevice(format=getFormat("sysfs",
                                                    device="sysfs",
                                                    mountpoint="/sys"))
        return self._sysfs

    @property
    def dev(self):
        if not self._dev:
            self._dev = DirectoryDevice("/dev", format=getFormat("bind",
                                                                 device="/dev",
                                                                 mountpoint="/dev",
                                                                 exists=True),
                                        exists=True)

        return self._dev

    @property
    def devpts(self):
        if not self._devpts:
            self._devpts = NoDevice(format=getFormat("devpts",
                                                     device="devpts",
                                                     mountpoint="/dev/pts"))
        return self._devpts

    @property
    def proc(self):
        if not self._proc:
            self._proc = NoDevice(format=getFormat("proc",
                                                   device="proc",
                                                   mountpoint="/proc"))
        return self._proc

    @property
    def devshm(self):
        if not self._devshm:
            self._devshm = NoDevice(format=getFormat("tmpfs",
                                                     device="tmpfs",
                                                     mountpoint="/dev/shm"))
        return self._devshm

    @property
    def usb(self):
        if not self._usb:
            self._usb = NoDevice(format=getFormat("usbfs",
                                                  device="usbfs",
                                                  mountpoint="/proc/bus/usb"))
        return self._usb

    @property
    def selinux(self):
        if not self._selinux:
            self._selinux = NoDevice(format=getFormat("selinuxfs",
                                                      device="selinuxfs",
                                                      mountpoint="/sys/fs/selinux"))
        return self._selinux

    @property
    def run(self):
        if not self._run:
            self._run = DirectoryDevice("/run", format=getFormat("bind",
                                                                 device="/run",
                                                                 mountpoint="/run",
                                                                 exists=True),
                                        exists=True)

        return self._run

    @property
    def devices(self):
        return sorted(self.devicetree.devices, key=lambda d: d.path)

    @property
    def mountpoints(self):
        filesystems = {}
        for device in self.devices:
            if device.format.mountable and device.format.mountpoint:
                filesystems[device.format.mountpoint] = device
        return filesystems

    def _parseOneLine(self, (devspec, mountpoint, fstype, options, dump, passno)):
        # no sense in doing any legwork for a noauto entry
        if "noauto" in options.split(","):
            log.info("ignoring noauto entry")
            raise UnrecognizedFSTabEntryError()

        # find device in the tree
        device = self.devicetree.resolveDevice(devspec,
                                               cryptTab=self.cryptTab,
                                               blkidTab=self.blkidTab,
                                               options=options)

        if device:
            # fall through to the bottom of this block
            pass
        elif devspec.startswith("/dev/loop"):
            # FIXME: create devices.LoopDevice
            log.warning("completely ignoring your loop mount")
        elif ":" in devspec and fstype.startswith("nfs"):
            # NFS -- preserve but otherwise ignore
            device = NFSDevice(devspec,
                               exists=True,
                               format=getFormat(fstype,
                                                exists=True,
                                                device=devspec))
        elif devspec.startswith("/") and fstype == "swap":
            # swap file
            device = FileDevice(devspec,
                                parents=get_containing_device(devspec, self.devicetree),
                                format=getFormat(fstype,
                                                 device=devspec,
                                                 exists=True),
                                exists=True)
        elif fstype == "bind" or "bind" in options:
            # bind mount... set fstype so later comparison won't
            # turn up false positives
            fstype = "bind"

            # This is probably not going to do anything useful, so we'll
            # make sure to try again from FSSet.mountFilesystems. The bind
            # mount targets should be accessible by the time we try to do
            # the bind mount from there.
            parents = get_containing_device(devspec, self.devicetree)
            device = DirectoryDevice(devspec, parents=parents, exists=True)
            device.format = getFormat("bind",
                                      device=device.path,
                                      exists=True)
        elif mountpoint in ("/proc", "/sys", "/dev/shm", "/dev/pts",
                            "/sys/fs/selinux", "/proc/bus/usb"):
            # drop these now -- we'll recreate later
            return None
        else:
            # nodev filesystem -- preserve or drop completely?
            format = getFormat(fstype)
            if devspec == "none" or \
               isinstance(format, get_device_format_class("nodev")):
                device = NoDevice(format=format)

        if device is None:
            log.error("failed to resolve %s (%s) from fstab" % (devspec,
                                                                fstype))
            raise UnrecognizedFSTabEntryError()

        device.setup()
        fmt = getFormat(fstype, device=device.path, exists=True)
        if fstype != "auto" and None in (device.format.type, fmt.type):
            log.info("Unrecognized filesystem type for %s (%s)"
                     % (device.name, fstype))
            device.teardown()
            raise UnrecognizedFSTabEntryError()

        # make sure, if we're using a device from the tree, that
        # the device's format we found matches what's in the fstab
        ftype = getattr(fmt, "mountType", fmt.type)
        dtype = getattr(device.format, "mountType", device.format.type)
        if fstype != "auto" and ftype != dtype:
            log.info("fstab says %s at %s is %s" % (dtype, mountpoint, ftype))
            if fmt.testMount():
                device.format = fmt
            else:
                device.teardown()
                raise FSTabTypeMismatchError("%s: detected as %s, fstab says %s"
                                             % (mountpoint, dtype, ftype))
        del ftype
        del dtype

        if device.format.mountable:
            device.format.mountpoint = mountpoint
            device.format.mountopts = options

        # is this useful?
        try:
            device.format.options = options
        except AttributeError:
            pass

        return device

    def parseFSTab(self, chroot=None):
        """ parse /etc/fstab

            preconditions:
                all storage devices have been scanned, including filesystems
            postconditions:

            FIXME: control which exceptions we raise

            XXX do we care about bind mounts?
                how about nodev mounts?
                loop mounts?
        """
        if not chroot or not os.path.isdir(chroot):
            chroot = ROOT_PATH

        path = "%s/etc/fstab" % chroot
        if not os.access(path, os.R_OK):
            # XXX should we raise an exception instead?
            log.info("cannot open %s for read" % path)
            return

        blkidTab = BlkidTab(chroot=chroot)
        try:
            blkidTab.parse()
            log.debug("blkid.tab devs: %s" % blkidTab.devices.keys())
        except Exception as e:
            log.info("error parsing blkid.tab: %s" % e)
            blkidTab = None

        cryptTab = CryptTab(self.devicetree, blkidTab=blkidTab, chroot=chroot)
        try:
            cryptTab.parse(chroot=chroot)
            log.debug("crypttab maps: %s" % cryptTab.mappings.keys())
        except Exception as e:
            log.info("error parsing crypttab: %s" % e)
            cryptTab = None

        self.blkidTab = blkidTab
        self.cryptTab = cryptTab

        with open(path) as f:
            log.debug("parsing %s" % path)

            lines = f.readlines()

            # save the original file
            self.origFStab = ''.join(lines)

            for line in lines:
                # strip off comments
                (line, pound, comment) = line.partition("#")
                fields = line.split()

                if not 4 <= len(fields) <= 6:
                    continue
                elif len(fields) == 4:
                    fields.extend([0, 0])
                elif len(fields) == 5:
                    fields.append(0)

                (devspec, mountpoint, fstype, options, dump, passno) = fields

                try:
                    device = self._parseOneLine((devspec, mountpoint, fstype, options, dump, passno))
                except UnrecognizedFSTabEntryError:
                    # just write the line back out as-is after upgrade
                    self.preserveLines.append(line)
                    continue

                if not device:
                    continue

                if device not in self.devicetree.devices:
                    try:
                        self.devicetree._addDevice(device)
                    except ValueError:
                        # just write duplicates back out post-install
                        self.preserveLines.append(line)

    def turnOnSwap(self, rootPath="", upgrading=None):
        """ Activate the system's swap space. """
        if not flags.installer_mode:
            return

        for device in self.swapDevices:
            if isinstance(device, FileDevice):
                # set up FileDevices' parents now that they are accessible
                targetDir = "%s/%s" % (rootPath, device.path)
                parent = get_containing_device(targetDir, self.devicetree)
                if not parent:
                    log.error("cannot determine which device contains "
                              "directory %s" % device.path)
                    device.parents = []
                    self.devicetree._removeDevice(device)
                    continue
                else:
                    device.parents = [parent]

            while True:
                try:
                    device.setup()
                    device.format.setup()
                except StorageError as e:
                    if errorHandler.cb(e, device) == ERROR_RAISE:
                        raise
                else:
                    break

    def mountFilesystems(self, rootPath="", readOnly=None,
                         skipRoot=False, raiseErrors=None):
        """ Mount the system's filesystems. """
        if not flags.installer_mode:
            return

        devices = self.mountpoints.values() + self.swapDevices
        devices.extend([self.dev, self.devshm, self.devpts, self.sysfs,
                        self.proc, self.selinux, self.usb, self.run])
        devices.sort(key=lambda d: getattr(d.format, "mountpoint", None))

        for device in devices:
            if not device.format.mountable or not device.format.mountpoint:
                continue

            if skipRoot and device.format.mountpoint == "/":
                continue

            options = device.format.options
            if "noauto" in options.split(","):
                continue

            if device.format.type == "bind" and device not in [self.dev, self.run]:
                # set up the DirectoryDevice's parents now that they are
                # accessible
                #
                # -- bind formats' device and mountpoint are always both
                #    under the chroot. no exceptions. none, damn it.
                targetDir = "%s/%s" % (rootPath, device.path)
                parent = get_containing_device(targetDir, self.devicetree)
                if not parent:
                    log.error("cannot determine which device contains "
                              "directory %s" % device.path)
                    device.parents = []
                    self.devicetree._removeDevice(device)
                    continue
                else:
                    device.parents = [parent]

            try:
                device.setup()
            except Exception as e:
                if errorHandler.cb(e, device) == ERROR_RAISE:
                    raise
                else:
                    continue

            if readOnly:
                options = "%s,%s" % (options, readOnly)

            try:
                device.format.setup(options=options,
                                    chroot=rootPath)
            except Exception as e:
                log.error("error mounting %s on %s: %s"
                          % (device.path, device.format.mountpoint, e))
                if errorHandler.cb(e, device) == ERROR_RAISE:
                    raise

        self.active = True

    def umountFilesystems(self, ignoreErrors=True, swapoff=True):
        """ unmount filesystems, except swap if swapoff == False """
        devices = self.mountpoints.values() + self.swapDevices
        devices.extend([self.dev, self.devshm, self.devpts, self.sysfs,
                        self.proc, self.usb, self.selinux, self.run])
        devices.sort(key=lambda d: getattr(d.format, "mountpoint", None))
        devices.reverse()
        for device in devices:
            if (not device.format.mountable) or \
               (device.format.type == "swap" and not swapoff):
                continue

            device.format.teardown()
            device.teardown()

        self.active = False

    def createSwapFile(self, device, size):
        """ Create and activate a swap file under ROOT_PATH. """
        filename = "/SWAP"
        count = 0
        basedir = os.path.normpath("%s/%s" % (ROOT_PATH,
                                              device.format.mountpoint))
        while os.path.exists("%s/%s" % (basedir, filename)) or \
              self.devicetree.getDeviceByName(filename):
            file = os.path.normpath("%s/%s" % (basedir, filename))
            count += 1
            filename = "/SWAP-%d" % count

        dev = FileDevice(filename,
                         size=size,
                         parents=[device],
                         format=getFormat("swap", device=filename))
        dev.create()
        dev.setup()
        dev.format.create()
        dev.format.setup()
        # nasty, nasty
        self.devicetree._addDevice(dev)

    def mkDevRoot(self):
        root = self.rootDevice
        dev = "%s/%s" % (ROOT_PATH, root.path)
        if not os.path.exists("%s/dev/root" %(ROOT_PATH,)) and os.path.exists(dev):
            rdev = os.stat(dev).st_rdev
            os.mknod("%s/dev/root" % (ROOT_PATH,), stat.S_IFBLK | 0600, rdev)

    @property
    def swapDevices(self):
        swaps = []
        for device in self.devices:
            if device.format.type == "swap":
                swaps.append(device)
        return swaps

    @property
    def rootDevice(self):
        for path in ["/", ROOT_PATH]:
            for device in self.devices:
                try:
                    mountpoint = device.format.mountpoint
                except AttributeError:
                    mountpoint = None

                if mountpoint == path:
                    return device

    def write(self):
        """ write out all config files based on the set of filesystems """
        # /etc/fstab
        fstab_path = os.path.normpath("%s/etc/fstab" % ROOT_PATH)
        fstab = self.fstab()
        open(fstab_path, "w").write(fstab)

        # /etc/crypttab
        crypttab_path = os.path.normpath("%s/etc/crypttab" % ROOT_PATH)
        crypttab = self.crypttab()
        origmask = os.umask(0077)
        open(crypttab_path, "w").write(crypttab)
        os.umask(origmask)

        # /etc/mdadm.conf
        mdadm_path = os.path.normpath("%s/etc/mdadm.conf" % ROOT_PATH)
        mdadm_conf = self.mdadmConf()
        if mdadm_conf:
            open(mdadm_path, "w").write(mdadm_conf)

        # /etc/multipath.conf
        if self.devicetree.getDevicesByType("dm-multipath"):
            util.copy_to_system("/etc/multipath.conf")
            util.copy_to_system("/etc/multipath/wwids")
            util.copy_to_system("/etc/multipath/bindings")
        else:
            log.info("not writing out mpath configuration")

    def crypttab(self):
        # if we are upgrading, do we want to update crypttab?
        # gut reaction says no, but plymouth needs the names to be very
        # specific for passphrase prompting
        if not self.cryptTab:
            self.cryptTab = CryptTab(self.devicetree)
            self.cryptTab.populate()

        devices = self.mountpoints.values() + self.swapDevices

        # prune crypttab -- only mappings required by one or more entries
        for name in self.cryptTab.mappings.keys():
            keep = False
            mapInfo = self.cryptTab[name]
            cryptoDev = mapInfo['device']
            for device in devices:
                if device == cryptoDev or device.dependsOn(cryptoDev):
                    keep = True
                    break

            if not keep:
                del self.cryptTab.mappings[name]

        return self.cryptTab.crypttab()

    def mdadmConf(self):
        """ Return the contents of mdadm.conf. """
        arrays = self.devicetree.getDevicesByType("mdarray")
        arrays.extend(self.devicetree.getDevicesByType("mdbiosraidarray"))
        arrays.extend(self.devicetree.getDevicesByType("mdcontainer"))
        # Sort it, this not only looks nicer, but this will also put
        # containers (which get md0, md1, etc.) before their members
        # (which get md127, md126, etc.). and lame as it is mdadm will not
        # assemble the whole stack in one go unless listed in the proper order
        # in mdadm.conf
        arrays.sort(key=lambda d: d.path)
        if not arrays:
            return ""

        conf = "# mdadm.conf written out by anaconda\n"
        conf += "MAILADDR root\n"
        conf += "AUTO +imsm +1.x -all\n"
        devices = self.mountpoints.values() + self.swapDevices
        for array in arrays:
            for device in devices:
                if device == array or device.dependsOn(array):
                    conf += array.mdadmConfEntry
                    break

        return conf

    def fstab (self):
        format = "%-23s %-23s %-7s %-15s %d %d\n"
        fstab = """
#
# /etc/fstab
# Created by anaconda on %s
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
""" % time.asctime()

        devices = sorted(self.mountpoints.values(),
                         key=lambda d: d.format.mountpoint)
        devices += self.swapDevices
        netdevs = self.devicetree.getDevicesByInstance(NetworkStorageDevice)
        for device in devices:
            # why the hell do we put swap in the fstab, anyway?
            if not device.format.mountable and device.format.type != "swap":
                continue

            # Don't write out lines for optical devices, either.
            if isinstance(device, OpticalDevice):
                continue

            fstype = getattr(device.format, "mountType", device.format.type)
            if fstype == "swap":
                mountpoint = "swap"
                options = device.format.options
            else:
                mountpoint = device.format.mountpoint
                options = device.format.options
                if not mountpoint:
                    log.warning("%s filesystem on %s has no mountpoint" % \
                                                            (fstype,
                                                             device.path))
                    continue

            options = options or "defaults"
            for netdev in netdevs:
                if device.dependsOn(netdev):
                    options = options + ",_netdev"
                    break
            if device.encrypted:
                options += ",x-systemd.device-timeout=0"
            devspec = device.fstabSpec
            dump = device.format.dump
            if device.format.check and mountpoint == "/":
                passno = 1
            elif device.format.check:
                passno = 2
            else:
                passno = 0
            fstab = fstab + device.fstabComment
            fstab = fstab + format % (devspec, mountpoint, fstype,
                                      options, dump, passno)

        # now, write out any lines we were unable to process because of
        # unrecognized filesystems or unresolveable device specifications
        for line in self.preserveLines:
            fstab += line

        return fstab

def getReleaseString():
    relName = None
    relVer = None

    try:
        relArch = util.capture_output(["arch"], root=ROOT_PATH).strip()
    except:
        relArch = None

    filename = "%s/etc/redhat-release" % ROOT_PATH
    if os.access(filename, os.R_OK):
        with open(filename) as f:
            try:
                relstr = f.readline().strip()
            except (IOError, AttributeError):
                relstr = ""

        # get the release name and version
        # assumes that form is something
        # like "Red Hat Linux release 6.2 (Zoot)"
        (product, sep, version) = relstr.partition(" release ")
        if sep:
            relName = product
            relVer = version.split()[0]

    return (relArch, relName, relVer)

def findExistingInstallations(devicetree):
    if not os.path.exists(ROOT_PATH):
        util.makedirs(ROOT_PATH)

    roots = []
    for device in devicetree.leaves:
        if not device.format.linuxNative or not device.format.mountable or \
           not device.controllable:
            continue

        try:
            device.setup()
        except Exception as e:
            log.warning("setup of %s failed: %s" % (device.name, e))
            continue

        options = device.format.options + ",ro"
        try:
            device.format.mount(options=options, mountpoint=ROOT_PATH)
        except Exception as e:
            log.warning("mount of %s as %s failed: %s" % (device.name,
                                                          device.format.type,
                                                          e))
            device.teardown()
            continue

        if not os.access(ROOT_PATH + "/etc/fstab", os.R_OK):
            device.teardown(recursive=True)
            continue

        try:
            (arch, product, version) = getReleaseString()
        except ValueError:
            name = _("Linux on %s") % device.name
        else:
            # I'd like to make this finer grained, but it'd be very difficult
            # to translate.
            if not product or not version or not arch:
                name = _("Unknown Linux")
            else:
                name = _("%(product)s Linux %(version)s for %(arch)s") % \
                        {"product": product, "version": version, "arch": arch}

        (mounts, swaps) = parseFSTab(devicetree, chroot=ROOT_PATH)
        device.teardown()
        if not mounts and not swaps:
            # empty /etc/fstab. weird, but I've seen it happen.
            continue
        roots.append(Root(mounts=mounts, swaps=swaps, name=name))

    return roots

class Root(object):
    def __init__(self, mounts=None, swaps=None, name=None):
        # mountpoint key, StorageDevice value
        if not mounts:
            self.mounts = {}
        else:
            self.mounts = mounts

        # StorageDevice
        if not swaps:
            self.swaps = []
        else:
            self.swaps = swaps

        self.name = name    # eg: "Fedora Linux 16 for x86_64", "Linux on sda2"

        if not self.name and "/" in self.mounts:
            self.name = self.mounts["/"].format.uuid

    @property
    def device(self):
        return self.mounts.get("/")

def parseFSTab(devicetree, chroot=None):
    """ parse /etc/fstab and return a tuple of a mount dict and swap list """
    if not chroot or not os.path.isdir(chroot):
        chroot = ROOT_PATH

    mounts = {}
    swaps = []
    path = "%s/etc/fstab" % chroot
    if not os.access(path, os.R_OK):
        # XXX should we raise an exception instead?
        log.info("cannot open %s for read" % path)
        return (mounts, swaps)

    blkidTab = BlkidTab(chroot=chroot)
    try:
        blkidTab.parse()
        log.debug("blkid.tab devs: %s" % blkidTab.devices.keys())
    except Exception as e:
        log.info("error parsing blkid.tab: %s" % e)
        blkidTab = None

    cryptTab = CryptTab(devicetree, blkidTab=blkidTab, chroot=chroot)
    try:
        cryptTab.parse(chroot=chroot)
        log.debug("crypttab maps: %s" % cryptTab.mappings.keys())
    except Exception as e:
        log.info("error parsing crypttab: %s" % e)
        cryptTab = None

    with open(path) as f:
        log.debug("parsing %s" % path)
        for line in f.readlines():
            # strip off comments
            (line, pound, comment) = line.partition("#")
            fields = line.split(None, 4)

            if len(fields) < 5:
                continue

            (devspec, mountpoint, fstype, options, rest) = fields

            # find device in the tree
            device = devicetree.resolveDevice(devspec,
                                              cryptTab=cryptTab,
                                              blkidTab=blkidTab,
                                              options=options)

            if device is None:
                continue

            if fstype != "swap":
                mounts[mountpoint] = device
            else:
                swaps.append(device)

    return (mounts, swaps)
