From ea67d9f7ba768a14180e384e56d7e254e940299d Mon Sep 17 00:00:00 2001
From: "Samantha N. Bueno" <sbueno+anaconda@redhat.com>
Date: Mon, 28 Mar 2016 15:27:19 -0400
Subject: [PATCH 3/7] Mv installer-specific functions to install. Create
 InstallerStorageConfig.

Two major things happen here:

* Create InstallerStorageConfig is created in blivet.osinstall
This is a subclass of blivet.blivet.Blivet, except it also contains
some installer-specific attrs and functions. Speaking of which--

* Mv installer-specific functions and properties from Blivet ->
InstallerStorageConfig. That list is:
    empty_devices
    unused_devices (prop)
    should_clear
    clear_partitions
    format_by_default
    must_format
    turn_on_swap
    mount_filesystems
    umount_filesystems
    parse_fstab
    mk_dev_root
    create_swap_file
    make_mtab
    add_fstab_swap
    remove_fstab_swap
    set_fstab_swap
These were all removed from Blivet and are now in InstallerStorageConfig.
---
 blivet/blivet.py    | 277 +------------------------
 blivet/osinstall.py | 585 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 585 insertions(+), 277 deletions(-)

diff --git a/blivet/blivet.py b/blivet/blivet.py
index 95fc2de..9de73d7 100644
--- a/blivet/blivet.py
+++ b/blivet/blivet.py
@@ -28,7 +28,7 @@ import time
 import parted
 import functools
 
-from pykickstart.constants import AUTOPART_TYPE_LVM, CLEARPART_TYPE_ALL, CLEARPART_TYPE_LINUX, CLEARPART_TYPE_LIST, CLEARPART_TYPE_NONE
+from pykickstart.constants import AUTOPART_TYPE_LVM, CLEARPART_TYPE_ALL, CLEARPART_TYPE_LIST, CLEARPART_TYPE_NONE
 
 from .storage_log import log_method_call, log_exception_info
 from .devices import BTRFSDevice, BTRFSSubVolumeDevice, BTRFSVolumeDevice
@@ -61,17 +61,6 @@ import logging
 log = logging.getLogger("blivet")
 
 
-def empty_device(device):
-    empty = True
-    if device.partitioned:
-        partitions = device.children
-        empty = all([p.is_magic for p in partitions])
-    else:
-        empty = (device.format.type is None)
-
-    return empty
-
-
 class Blivet(object, metaclass=SynchronizedMeta):
 
     """ Top-level class for managing storage configuration. """
@@ -261,31 +250,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
         self.update_bootloader_disk_list()
 
     @property
-    def unused_devices(self):
-        used_devices = []
-        for root in self.roots:
-            for device in list(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.format.mountable and not new.format.mountpoint:
-                continue
-
-            used_devices.extend(new.ancestors)
-
-        for device in self.partitions:
-            if getattr(device, "is_logical", False):
-                extended = device.disk.format.extended_partition.path
-                used_devices.append(self.devicetree.get_device_by_path(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
@@ -467,100 +431,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
         swaps.sort(key=lambda d: d.name)
         return swaps
 
-    def should_clear(self, device, **kwargs):
-        """ Return True if a clearpart settings say a device should be cleared.
-
-            :param device: the device (required)
-            :type device: :class:`~.devices.StorageDevice`
-            :keyword clear_part_type: overrides :attr:`self.config.clear_part_type`
-            :type clear_part_type: int
-            :keyword clear_part_disks: overrides
-                                     :attr:`self.config.clear_part_disks`
-            :type clear_part_disks: list
-            :keyword clear_part_devices: overrides
-                                       :attr:`self.config.clear_part_devices`
-            :type clear_part_devices: list
-            :returns: whether or not clear_partitions should remove this device
-            :rtype: bool
-        """
-        clear_part_type = kwargs.get("clear_part_type", self.config.clear_part_type)
-        clear_part_disks = kwargs.get("clear_part_disks",
-                                      self.config.clear_part_disks)
-        clear_part_devices = kwargs.get("clear_part_devices",
-                                        self.config.clear_part_devices)
-
-        for disk in device.disks:
-            # this will not include disks with hidden formats like multipath
-            # and firmware raid member disks
-            if clear_part_disks and disk.name not in clear_part_disks:
-                return False
-
-        if not self.config.clear_non_existent:
-            if (device.is_disk and not device.format.exists) or \
-               (not device.is_disk and not device.exists):
-                return False
-
-        # the only devices we want to clear when clear_part_type is
-        # CLEARPART_TYPE_NONE are uninitialized disks, or disks with no
-        # partitions, in clear_part_disks, and then only when we have been asked
-        # to initialize disks as needed
-        if clear_part_type in [CLEARPART_TYPE_NONE, None]:
-            if not self.config.initialize_disks or not device.is_disk:
-                return False
-
-            if not empty_device(device):
-                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.is_magic:
-                return False
-
-            # We don't want to fool with extended partitions, freespace, &c
-            if not device.is_primary and not device.is_logical:
-                return False
-
-            if clear_part_type == CLEARPART_TYPE_LINUX and \
-               not device.format.linux_native and \
-               not device.get_flag(parted.PARTITION_LVM) and \
-               not device.get_flag(parted.PARTITION_RAID) and \
-               not device.get_flag(parted.PARTITION_SWAP):
-                return False
-        elif device.is_disk:
-            if device.partitioned and clear_part_type != CLEARPART_TYPE_ALL:
-                # if clear_part_type 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):
-                    return False
-
-            # Never clear disks with hidden formats
-            if device.format.hidden:
-                return False
-
-            # When clear_part_type 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 (clear_part_type == CLEARPART_TYPE_LINUX and
-                not ((self.config.initialize_disks and
-                      empty_device(device)) or
-                     (not device.partitioned and device.format.linux_native))):
-                return False
-
-        # Don't clear devices holding install media.
-        descendants = self.devicetree.get_dependent_devices(device)
-        if device.protected or any(d.protected for d in descendants):
-            return False
-
-        if clear_part_type == CLEARPART_TYPE_LIST and \
-           device.name not in clear_part_devices:
-            return False
-
-        return True
-
     def recursive_remove(self, device):
         """ Remove a device after removing its dependent devices.
 
@@ -572,42 +442,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
         """
         self.devicetree.recursive_remove(device)
 
-    def clear_partitions(self):
-        """ Clear partitions and dependent devices from disks.
-
-            This is also where zerombr is handled.
-        """
-        # 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.parted_partition.number,
-                            reverse=True)
-        for part in partitions:
-            log.debug("clearpart: looking at %s", part.name)
-            if not self.should_clear(part):
-                continue
-
-            self.recursive_remove(part)
-            log.debug("partitions: %s", [p.getDeviceNodeName() for p in part.parted_partition.disk.partitions])
-
-        # now remove any empty extended partitions
-        self.remove_empty_extended_partitions()
-
-        # ensure all disks have appropriate disklabels
-        for disk in self.disks:
-            zerombr = (self.config.zero_mbr and disk.format.type is None)
-            should_clear = self.should_clear(disk)
-            if should_clear:
-                self.recursive_remove(disk)
-
-            if zerombr or should_clear:
-                log.debug("clearpart: initializing %s", disk.name)
-                self.initialize_disk(disk)
-
-        self.update_bootloader_disk_list()
-
     def initialize_disk(self, disk):
         """ (Re)initialize a disk by creating a disklabel on it.
 
@@ -1136,44 +970,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
         for action in actions:
             self.devicetree.actions.add(action)
 
-    def format_by_default(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.linux_native:
-            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 must_format(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 safe_device_name(self, name):
         """ Convert a device name to something safe and return that.
 
@@ -1358,7 +1154,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
             os.mkdir("%s/etc" % get_sysroot())
 
         self.fsset.write()
-        self.make_mtab()
         iscsi.write(get_sysroot(), self)
         fcoe.write(get_sysroot())
         zfcp.write(get_sysroot())
@@ -1378,25 +1173,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
                 fields = [dasd.busid] + dasd.get_opts()
                 f.write("%s\n" % " ".join(fields),)
 
-    def turn_on_swap(self):
-        self.fsset.turn_on_swap(root_path=get_sysroot())
-
-    def mount_filesystems(self, read_only=None, skip_root=False):
-        self.fsset.mount_filesystems(root_path=get_sysroot(),
-                                     read_only=read_only, skip_root=skip_root)
-
-    def umount_filesystems(self, swapoff=True):
-        self.fsset.umount_filesystems(swapoff=swapoff)
-
-    def parse_fstab(self, chroot=None):
-        self.fsset.parse_fstab(chroot=chroot)
-
-    def mk_dev_root(self):
-        self.fsset.mk_dev_root()
-
-    def create_swap_file(self, device, size):
-        self.fsset.create_swap_file(device, size)
-
     @property
     def bootloader(self):
         if self._bootloader is None and flags.installer_mode:
@@ -1524,23 +1300,6 @@ class Blivet(object, metaclass=SynchronizedMeta):
     def root_device(self):
         return self.fsset.root_device
 
-    def make_mtab(self):
-        path = "/etc/mtab"
-        target = "/proc/self/mounts"
-        path = os.path.normpath("%s/%s" % (get_sysroot(), 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 compare_disks(self, first, second):
         if not isinstance(first, str):
             first = first.name
@@ -1830,37 +1589,3 @@ class Blivet(object, metaclass=SynchronizedMeta):
         self._free_space_snapshot = self.get_free_space()
 
         return self._free_space_snapshot
-
-    def add_fstab_swap(self, device):
-        """
-        Add swap device to the list of swaps that should appear in the fstab.
-
-        :param device: swap device that should be added to the list
-        :type device: blivet.devices.StorageDevice instance holding a swap format
-
-        """
-
-        self.fsset.add_fstab_swap(device)
-
-    def remove_fstab_swap(self, device):
-        """
-        Remove swap device from the list of swaps that should appear in the fstab.
-
-        :param device: swap device that should be removed from the list
-        :type device: blivet.devices.StorageDevice instance holding a swap format
-
-        """
-
-        self.fsset.remove_fstab_swap(device)
-
-    def set_fstab_swaps(self, devices):
-        """
-        Set swap devices that should appear in the fstab.
-
-        :param devices: iterable providing devices that should appear in the fstab
-        :type devices: iterable providing blivet.devices.StorageDevice instances holding
-                       a swap format
-
-        """
-
-        self.fsset.set_fstab_swaps(devices)
diff --git a/blivet/osinstall.py b/blivet/osinstall.py
index b6f798a..d6ab6f1 100644
--- a/blivet/osinstall.py
+++ b/blivet/osinstall.py
@@ -24,17 +24,21 @@ import shlex
 import os
 import stat
 import time
+import parted
 
 import gi
 gi.require_version("BlockDev", "1.0")
 
 from gi.repository import BlockDev as blockdev
 
+from pykickstart.constants import CLEARPART_TYPE_NONE, CLEARPART_TYPE_LINUX, CLEARPART_TYPE_ALL, CLEARPART_TYPE_LIST
+
 from . import util
 from . import get_sysroot, get_target_physical_root, error_handler, ERROR_RAISE
 
+from .blivet import Blivet
 from .storage_log import log_exception_info
-from .devices import FileDevice, NFSDevice, NoDevice, OpticalDevice, NetworkStorageDevice, DirectoryDevice, MDRaidArrayDevice
+from .devices import FileDevice, NFSDevice, NoDevice, OpticalDevice, NetworkStorageDevice, DirectoryDevice, MDRaidArrayDevice, PartitionDevice
 from .errors import FSTabTypeMismatchError, UnrecognizedFSTabEntryError, StorageError, FSResizeError, FormatResizeError, UnknownSourceDeviceError
 from .formats import get_device_format_class
 from .formats import get_format
@@ -283,6 +287,7 @@ class StorageDiscoveryConfig(object):
     """ Class to encapsulate various detection/initialization parameters. """
 
     def __init__(self):
+
         # storage configuration variables
         self.ignore_disk_interactive = False
         self.clear_part_type = None
@@ -1094,6 +1099,584 @@ class CryptTab(object):
         return self.mappings.get(key, default)
 
 
+class InstallerStorageConfig(Blivet):
+    """ Top-level class for managing installer-related storage configuration. """
+    def __init__(self, ksdata=None):
+        """
+            :keyword ksdata: kickstart data store
+            :type ksdata: :class:`pykickstart.Handler`
+        """
+        super(InstallerStorageConfig, self).__init__(ksdata=ksdata)
+
+        self.config = StorageDiscoveryConfig()
+        self.autopart_type = AUTOPART_TYPE_LVM
+
+        self.__luks_devs = {}
+
+        # these will both be empty until our reset method gets called
+        # instantiate our own devicetree here to override the default created
+        # in Blivet so that protected_dev_specs gets handled
+        self.devicetree = DeviceTree(passphrase=self.encryption_passphrase,
+                                     luks_dict=self.__luks_devs,
+                                     ignored_disks=self.ignored_disks,
+                                     exclusive_disks=self.exclusive_disks,
+                                     disk_images=self.disk_images,
+                                     protected_dev_specs=self.config.protected_dev_specs)
+        self.fsset = FSSet(self.devicetree)
+        self._free_space_snapshot = None
+
+    def reset(self, cleanup_only=False):
+        """ Reset storage configuration to reflect actual system state.
+            This will cancel any queued actions and rescan from scratch but not
+            clobber user-obtained information like passphrases, iscsi config, &c
+
+            :keyword cleanup_only: prepare the tree only to deactivate devices
+            :type cleanup_only: bool
+
+            See :meth:`devicetree.Devicetree.populate` for more information
+            about the cleanup_only keyword argument.
+        """
+        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.encryption_passphrase = None
+            for device in self.devices:
+                if device.format.type == "luks" and device.format.exists:
+                    self.save_passphrase(device)
+
+        if flags.installer_mode and not flags.image_install:
+            iscsi.startup()
+            fcoe.startup()
+            zfcp.startup()
+
+        self.devicetree.reset(passphrase=self.encryption_passphrase,
+                              luks_dict=self.__luks_devs,
+                              ignored_disks=self.ignored_disks,
+                              exclusive_disks=self.exclusive_disks,
+                              disk_images=self.disk_images,
+                              protected_dev_specs=self.config.protected_dev_specs)
+        self.devicetree.populate(cleanup_only=cleanup_only)
+        self.fsset = FSSet(self.devicetree)
+        self.edd_dict = get_edd_dict(self.partitioned)
+        self.devicetree.edd_dict = self.edd_dict
+        if self.bootloader:
+            # clear out bootloader attributes that refer to devices that are
+            # no longer in the tree
+            self.bootloader.reset()
+        if self.ksdata:
+             self.config.updates(self.ksdata)
+        self.roots = []
+        if flags.installer_mode:
+            self.roots = find_existing_installations(self.devicetree)
+            self.dump_state("initial")
+
+        if not flags.installer_mode:
+            self.devicetree.handle_nodev_filesystems()
+
+        self.update_bootloader_disk_list()
+
+    def get_free_space(self, disks=None, clear_part_type=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 clear_part_type allow specifying a set of disks other than
+            self.disks and a clear_part_type value other than
+            self.config.clear_part_type.
+
+            :keyword disks: overrides :attr:`disks`
+            :type disks: list
+            :keyword clear_part_type: overrides :attr:`self.config.clear_part_type`
+            :type clear_part_type: int
+            :returns: dict with disk name keys and tuple (disk, fs) free values
+            :rtype: dict
+
+            .. note::
+
+                The free space values are :class:`~.size.Size` instances.
+
+        """
+        if disks is None:
+            disks = self.disks
+
+        if clear_part_type is None:
+            clear_part_type = self.config.clear_part_type
+
+        free = {}
+        for disk in disks:
+            should_clear = self.should_clear(disk, clear_part_type=clear_part_type,
+                                             clear_part_disks=[disk.name])
+            if should_clear:
+                free[disk.name] = (disk.size, Size(0))
+                continue
+
+            disk_free = Size(0)
+            fs_free = Size(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.should_clear(partition,
+                                                     clear_part_type=clear_part_type,
+                                                     clear_part_disks=[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] = (disk_free, fs_free)
+
+        return free
+
+    def write(self):
+        Blivet.write(self)
+
+        self.fsset.write()
+        self.make_mtab()
+        iscsi.write(get_sysroot(), self)
+        fcoe.write(get_sysroot())
+        zfcp.write(get_sysroot())
+
+    @property
+    def boot_device(self):
+        dev = None
+        if self.fsset:
+            dev = self.mountpoints.get("/boot", self.root_device)
+        return dev
+
+    @property
+    def mountpoints(self):
+        return self.fsset.mountpoints
+
+    @property
+    def root_device(self):
+        return self.fsset.root_device
+
+    @property
+    def file_system_free_space(self):
+        """ Combined free space in / and /usr as :class:`~.size.Size`. """
+        mountpoints = ["/", "/usr"]
+        free = Size(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.format.free_space_estimate(device.size)
+
+        return free
+    def update_ksdata(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.ignored_disks:
+            self.ksdata.ignoredisk.drives = self.ignored_disks[:]
+        elif self.exclusive_disks:
+            self.ksdata.ignoredisk.onlyuse = self.exclusive_disks[:]
+
+        # autopart
+        self.ksdata.autopart.autopart = self.do_autopart
+        self.ksdata.autopart.type = self.autopart_type
+        self.ksdata.autopart.encrypted = self.encrypted_autopart
+
+        # clearpart
+        self.ksdata.clearpart.type = self.config.clear_part_type
+        self.ksdata.clearpart.drives = self.config.clear_part_disks[:]
+        self.ksdata.clearpart.devices = self.config.clear_part_devices[:]
+        self.ksdata.clearpart.initAll = self.config.initialize_disks
+        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.actions.find(action_type="destroy",
+                                                           object_type="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.do_autopart:
+            return
+
+        self._update_custom_storage_ksdata()
+
+    def _update_custom_storage_ksdata(self):
+        """ Update KSData for custom storage. """
+
+        # custom storage
+        ks_map = {PartitionDevice: ("PartData", "partition"),
+                  TmpFSDevice: ("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 list(self.mountpoints.values()) + self.swaps
+                           for a in d.ancestors))
+
+        # devices which share information with their distinct raw device
+        complementary_devices = [d for d in devices if d.raw_device is not d]
+
+        devices.sort(key=lambda d: len(d.ancestors))
+        for device in devices:
+            cls = next((c for c in ks_map if isinstance(device, c)), None)
+            if cls is None:
+                log.info("omitting ksdata: %s", device)
+                continue
+
+            class_attr, list_attr = ks_map[cls]
+
+            cls = getattr(self.ksdata, class_attr)
+            data = cls()    # all defaults
+
+            complements = [d for d in complementary_devices if d.raw_device is device]
+
+            if len(complements) > 1:
+                log.warning("omitting ksdata for %s, found too many (%d) complementary devices", device, len(complements))
+                continue
+
+            device = complements[0] if complements else device
+
+            device.populate_ksdata(data)
+
+            parent = getattr(self.ksdata, list_attr)
+            parent.dataList().append(data)
+
+    def empty_device(self, device):
+        empty = True
+        if device.partitioned:
+            partitions = device.children
+            empty = all([p.is_magic for p in partitions])
+        else:
+            empty = (device.format.type is None)
+
+        return empty
+
+    @property
+    def unused_devices(self):
+        used_devices = []
+        for root in self.roots:
+            for device in list(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.format.mountable and not new.format.mountpoint:
+                continue
+
+            used_devices.extend(new.ancestors)
+
+        for device in self.partitions:
+            if getattr(device, "is_logical", False):
+                extended = device.disk.format.extended_partition.path
+                used_devices.append(self.devicetree.get_device_by_path(extended))
+
+        used = set(used_devices)
+        _all = set(self.devices)
+        return list(_all.difference(used))
+
+    def should_clear(self, device, **kwargs):
+        """ Return True if a clearpart settings say a device should be cleared.
+
+            :param device: the device (required)
+            :type device: :class:`~.devices.StorageDevice`
+            :keyword clear_part_type: overrides :attr:`self.config.clear_part_type`
+            :type clear_part_type: int
+            :keyword clear_part_disks: overrides
+                                     :attr:`self.config.clear_part_disks`
+            :type clear_part_disks: list
+            :keyword clear_part_devices: overrides
+                                       :attr:`self.config.clear_part_devices`
+            :type clear_part_devices: list
+            :returns: whether or not clear_partitions should remove this device
+            :rtype: bool
+        """
+        clear_part_type = kwargs.get("clear_part_type", self.config.clear_part_type)
+        clear_part_disks = kwargs.get("clear_part_disks",
+                                      self.config.clear_part_disks)
+        clear_part_devices = kwargs.get("clear_part_devices",
+                                        self.config.clear_part_devices)
+
+        for disk in device.disks:
+            # this will not include disks with hidden formats like multipath
+            # and firmware raid member disks
+            if clear_part_disks and disk.name not in clear_part_disks:
+                return False
+
+        if not self.config.clear_non_existent:
+            if (device.is_disk and not device.format.exists) or \
+               (not device.is_disk and not device.exists):
+                return False
+
+        # the only devices we want to clear when clear_part_type is
+        # CLEARPART_TYPE_NONE are uninitialized disks, or disks with no
+        # partitions, in clear_part_disks, and then only when we have been asked
+        # to initialize disks as needed
+        if clear_part_type in [CLEARPART_TYPE_NONE, None]:
+            if not self.config.initialize_disks or not device.is_disk:
+                return False
+
+            if not self.empty_device(device):
+                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.is_magic:
+                return False
+
+            # We don't want to fool with extended partitions, freespace, &c
+            if not device.is_primary and not device.is_logical:
+                return False
+
+            if clear_part_type == CLEARPART_TYPE_LINUX and \
+               not device.format.linux_native and \
+               not device.get_flag(parted.PARTITION_LVM) and \
+               not device.get_flag(parted.PARTITION_RAID) and \
+               not device.get_flag(parted.PARTITION_SWAP):
+                return False
+        elif device.is_disk:
+            if device.partitioned and clear_part_type != CLEARPART_TYPE_ALL:
+                # if clear_part_type 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 self.empty_device(device):
+                    return False
+
+            # Never clear disks with hidden formats
+            if device.format.hidden:
+                return False
+
+            # When clear_part_type 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 (clear_part_type == CLEARPART_TYPE_LINUX and
+                not ((self.config.initialize_disks and
+                      self.empty_device(device)) or
+                     (not device.partitioned and device.format.linux_native))):
+                return False
+
+        # Don't clear devices holding install media.
+        descendants = self.devicetree.get_dependent_devices(device)
+        if device.protected or any(d.protected for d in descendants):
+            return False
+
+        if clear_part_type == CLEARPART_TYPE_LIST and \
+           device.name not in clear_part_devices:
+            return False
+
+        return True
+
+    def clear_partitions(self):
+        """ Clear partitions and dependent devices from disks.
+
+            This is also where zerombr is handled.
+        """
+        # 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.parted_partition.number,
+                            reverse=True)
+        for part in partitions:
+            log.debug("clearpart: looking at %s", part.name)
+            if not self.should_clear(part):
+                continue
+
+            self.recursive_remove(part)
+            log.debug("partitions: %s", [p.getDeviceNodeName() for p in part.parted_partition.disk.partitions])
+
+        # now remove any empty extended partitions
+        self.remove_empty_extended_partitions()
+
+        # ensure all disks have appropriate disklabels
+        for disk in self.disks:
+            zerombr = (self.config.zero_mbr and disk.format.type is None)
+            should_clear = self.should_clear(disk)
+            if should_clear:
+                self.recursive_remove(disk)
+
+            if zerombr or should_clear:
+                log.debug("clearpart: initializing %s", disk.name)
+                self.initialize_disk(disk)
+
+        self.update_bootloader_disk_list()
+
+    def format_by_default(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.linux_native:
+            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 must_format(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 turn_on_swap(self):
+        self.fsset.turn_on_swap(root_path=get_sysroot())
+
+    def mount_filesystems(self, read_only=None, skip_root=False):
+        self.fsset.mount_filesystems(root_path=get_sysroot(),
+                                     read_only=read_only, skip_root=skip_root)
+
+    def umount_filesystems(self, swapoff=True):
+        self.fsset.umount_filesystems(swapoff=swapoff)
+
+    def parse_fstab(self, chroot=None):
+        self.fsset.parse_fstab(chroot=chroot)
+
+    def mk_dev_root(self):
+        self.fsset.mk_dev_root()
+
+    def create_swap_file(self, device, size):
+        self.fsset.create_swap_file(device, size)
+
+    def make_mtab(self):
+        path = "/etc/mtab"
+        target = "/proc/self/mounts"
+        path = os.path.normpath("%s/%s" % (get_sysroot(), 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 add_fstab_swap(self, device):
+        """
+        Add swap device to the list of swaps that should appear in the fstab.
+
+        :param device: swap device that should be added to the list
+        :type device: blivet.devices.StorageDevice instance holding a swap format
+
+        """
+
+        self.fsset.add_fstab_swap(device)
+
+    def remove_fstab_swap(self, device):
+        """
+        Remove swap device from the list of swaps that should appear in the fstab.
+
+        :param device: swap device that should be removed from the list
+        :type device: blivet.devices.StorageDevice instance holding a swap format
+
+        """
+
+        self.fsset.remove_fstab_swap(device)
+
+    def set_fstab_swaps(self, devices):
+        """
+        Set swap devices that should appear in the fstab.
+
+        :param devices: iterable providing devices that should appear in the fstab
+        :type devices: iterable providing blivet.devices.StorageDevice instances holding
+                       a swap format
+
+        """
+
+        self.fsset.set_fstab_swaps(devices)
+
+
 def get_containing_device(path, devicetree):
     """ Return the device that a path resides on. """
     if not os.path.exists(path):
-- 
2.7.4

