Beispiel #1
0
    def __init__(self):
        super().__init__()
        self.do_autopart = False
        self.encrypted_autopart = False
        self.encryption_cipher = None
        self._escrow_certificates = {}

        self.autopart_escrow_cert = None
        self.autopart_add_backup_passphrase = False
        self.autopart_requests = []

        self._default_boot_fstype = None

        self._bootloader = None
        self.config = StorageDiscoveryConfig()
        self.autopart_type = AUTOPART_TYPE_LVM

        self.__luks_devs = {}
        self.fsset = FSSet(self.devicetree)
        self._free_space_snapshot = None

        self._short_product_name = shortProductName
        self._default_luks_version = DEFAULT_LUKS_VERSION

        self._autopart_luks_version = None
        self.autopart_pbkdf_args = None
Beispiel #2
0
    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.
        """
        # set up the disk images
        if conf.target.is_image:
            self.setup_disk_images()

        # 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)

        super().reset(cleanup_only=cleanup_only)

        self.fsset = FSSet(self.devicetree)

        # Clear out attributes that refer to devices that are no longer in the tree.
        self.bootloader.reset()

        self._mark_protected_devices()

        self.roots = []
        self.roots = find_existing_installations(self.devicetree)
        self.dump_state("initial")
Beispiel #3
0
 def __init__(self):
     super().__init__()
     self.protected_devices = []
     self._escrow_certificates = {}
     self._bootloader = None
     self.__luks_devs = {}
     self.fsset = FSSet(self.devicetree)
     self._short_product_name = shortProductName
     self._default_luks_version = DEFAULT_LUKS_VERSION
Beispiel #4
0
    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.
        """
        # set up the disk images
        if conf.target.is_image:
            self.setup_disk_images()

        # 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)

        super().reset(cleanup_only=cleanup_only)

        self.fsset = FSSet(self.devicetree)

        # Clear out attributes that refer to devices that are no longer in the tree.
        self.bootloader.reset()

        self._mark_protected_devices()

        self.roots = []
        self.roots = find_existing_installations(self.devicetree)
        self.dump_state("initial")
Beispiel #5
0
 def __init__(self):
     super().__init__()
     self.protected_devices = []
     self._escrow_certificates = {}
     self._default_boot_fstype = None
     self._bootloader = None
     self.__luks_devs = {}
     self.fsset = FSSet(self.devicetree)
     self._short_product_name = shortProductName
     self._default_luks_version = DEFAULT_LUKS_VERSION
Beispiel #6
0
class InstallerStorage(Blivet):
    """ Top-level class for managing installer-related storage configuration. """
    def __init__(self):
        super().__init__()
        self.protected_devices = []
        self._escrow_certificates = {}
        self._default_boot_fstype = None
        self._bootloader = None
        self.__luks_devs = {}
        self.fsset = FSSet(self.devicetree)
        self._short_product_name = shortProductName
        self._default_luks_version = DEFAULT_LUKS_VERSION

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

        return self._bootloader

    @property
    def boot_device(self):
        root_device = self.mountpoints.get("/")
        dev = self.mountpoints.get("/boot", root_device)
        return dev

    @property
    def default_boot_fstype(self):
        """The default filesystem type for the boot partition."""
        if self._default_boot_fstype:
            return self._default_boot_fstype

        return self.bootloader.stage2_format_types[0]

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

            Raise ValueError on invalid input.
        """
        log.debug("trying to set new default /boot fstype to '%s'", newtype)
        # This will raise ValueError if it isn't valid
        self._check_valid_fstype(newtype)
        self._default_boot_fstype = newtype

    @property
    def default_luks_version(self):
        """The default LUKS version."""
        return self._default_luks_version

    def set_default_luks_version(self, version):
        """Set the default LUKS version.

        :param version: a string with LUKS version
        :raises: ValueError on invalid input
        """
        log.debug("trying to set new default luks version to '%s'", version)
        self._check_valid_luks_version(version)
        self._default_luks_version = version

    def _check_valid_luks_version(self, version):
        get_format("luks", luks_version=version)

    def get_fstype(self, mountpoint=None):
        """ Return the default filesystem type based on mountpoint. """
        fstype = super().get_fstype(mountpoint=mountpoint)

        if mountpoint == "/boot":
            fstype = self.default_boot_fstype

        return fstype

    def get_escrow_certificate(self, url):
        """Get the escrow certificate.

        :param url: an URL of the certificate
        :return: a content of the certificate
        """
        if not url:
            return None

        certificate = self._escrow_certificates.get(url, None)

        if not certificate:
            certificate = download_escrow_certificate(url)
            self._escrow_certificates[url] = certificate

        return certificate

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

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

    def get_file_system_free_space(self, mount_points=("/", "/usr")):
        """Get total file system free space on the given mount points.

        Calculates total free space in / and /usr, by default.

        :param mount_points: a list of mount points
        :return: a total size
        """
        free = Size(0)
        btrfs_volumes = []

        for mount_point in mount_points:
            device = self.mountpoints.get(mount_point)
            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 get_disk_free_space(self, disks=None):
        """Get total free space on the given disks.

        Calculates free space available for use.

        :param disks: a list of disks or None
        :return: a total size
        """
        # Use all disks in the device tree by default.
        if disks is None:
            disks = self.disks

        # Get the dictionary of free spaces for each disk.
        snapshot = self.get_free_space(disks)

        # Calculate the total free space.
        return sum((disk_free for disk_free, fs_free in snapshot.values()),
                   Size(0))

    def get_disk_reclaimable_space(self, disks=None):
        """Get total reclaimable space on the given disks.

        Calculates free space unavailable but reclaimable
        from existing partitions.

        :param disks: a list of disks or None
        :return: a total size
        """
        # Use all disks in the device tree by default.
        if disks is None:
            disks = self.disks

        # Get the dictionary of free spaces for each disk.
        snapshot = self.get_free_space(disks)

        # Calculate the total reclaimable free space.
        return sum((fs_free for disk_free, fs_free in snapshot.values()),
                   Size(0))

    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.
        """
        # set up the disk images
        if conf.target.is_image:
            self.setup_disk_images()

        # 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)

        super().reset(cleanup_only=cleanup_only)

        self.fsset = FSSet(self.devicetree)

        # Clear out attributes that refer to devices that are no longer in the tree.
        self.bootloader.reset()

        self._mark_protected_devices()

        self.roots = []
        self.roots = find_existing_installations(self.devicetree)
        self.dump_state("initial")

    def _mark_protected_devices(self):
        """Mark protected devices.

        If a device is protected, mark it as such now. Once the tree
        has been populated, devices' protected attribute is how we will
        identify protected devices.
        """
        protected = []

        # Resolve the protected device specs to devices.
        for spec in self.protected_devices:
            dev = self.devicetree.resolve_device(spec)

            if dev is not None:
                log.debug("Protected device spec %s resolved to %s.", spec,
                          dev.name)
                protected.append(dev)

        # Find the live backing device and its parents.
        live_device_name = find_live_backing_device()

        if live_device_name:
            log.debug("Resolved live device to %s.", live_device_name)
            dev = self.devicetree.get_device_by_name(live_device_name,
                                                     hidden=True)
            protected.append(dev)
            protected.extend(dev.parents)

        # Mark the collected devices as protected.
        for dev in protected:
            log.debug("Marking device %s as protected.", dev.name)
            dev.protected = True

    def protect_devices(self, protected_names):
        """Protect given devices.

        :param protected_names: a list of device names
        """
        protected = set(protected_names)
        unprotected = set(self.protected_devices)

        # Mark unprotected devices.
        # Skip devices that should stay protected.
        for spec in unprotected - protected:
            device = self.devicetree.resolve_device(spec)

            if device:
                log.debug("Marking device %s as unprotected.", device.name)
                device.protected = False

        # Mark protected devices.
        # Skip devices that are already protected.
        for spec in protected - unprotected:
            device = self.devicetree.resolve_device(spec)

            if device:
                log.debug("Marking device %s as protected.", device.name)
                device.protected = True

        # Update the list.
        self.protected_devices = protected_names

    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 usable_disks(self):
        """Disks that can be used for the installation.

        :return: a list of disks
        """
        # Get all devices.
        devices = self.devicetree.devices

        # Add the hidden devices.
        if conf.target.is_image:
            devices += [
                d for d in self.devicetree._hidden
                if d.name in self.devicetree.disk_images
            ]
        else:
            devices += self.devicetree._hidden

        # Filter out the usable disks.
        disks = []
        for d in devices:
            if d.is_disk and not d.format.hidden and not d.protected:
                # Unformatted DASDs are detected with a size of 0, but they should
                # still show up as valid disks if this function is called, since we
                # can still use them; anaconda will know how to handle them, so they
                # don't need to be ignored anymore.
                if d.type == "dasd":
                    disks.append(d)
                elif d.size > 0 and d.media_present:
                    disks.append(d)

        # Remove duplicate names from the list.
        return sorted(set(disks), key=lambda d: d.name)

    def select_disks(self, selected_names):
        """Select disks that should be used for the installation.

        Hide usable disks that are not selected.

        :param selected_names: a list of disk names
        """
        for disk in self.usable_disks:
            if disk.name not in selected_names:
                if disk in self.devices:
                    self.devicetree.hide(disk)
            else:
                if disk not in self.devices:
                    self.devicetree.unhide(disk)

    @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 _get_hostname(self):
        """Return a hostname."""
        ignored_hostnames = {None, "", 'localhost', 'localhost.localdomain'}

        network_proxy = NETWORK.get_proxy()
        hostname = network_proxy.Hostname

        if hostname in ignored_hostnames:
            hostname = network_proxy.GetCurrentHostname()

        if hostname in ignored_hostnames:
            hostname = None

        return hostname

    def _get_container_name_template(self, prefix=None):
        """Return a template for suggest_container_name method."""
        prefix = prefix or ""  # make sure prefix is a string instead of None

        # try to create a device name incorporating the hostname
        hostname = self._get_hostname()

        if hostname:
            template = "%s_%s" % (prefix, hostname.split('.')[0].lower())
            template = self.safe_device_name(template)
        else:
            template = prefix

        if conf.target.is_image:
            template = "%s_image" % template

        return template

    def turn_on_swap(self):
        self.fsset.turn_on_swap(root_path=util.getSysroot())

    def mount_filesystems(self, read_only=None, skip_root=False):
        self.fsset.mount_filesystems(root_path=util.getSysroot(),
                                     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" % (util.getSysroot(), 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)
Beispiel #7
0
class InstallerStorage(Blivet):
    """ Top-level class for managing installer-related storage configuration. """
    def __init__(self):
        super().__init__()
        self.do_autopart = False
        self.encrypted_autopart = False
        self.encryption_cipher = None
        self._escrow_certificates = {}

        self.autopart_escrow_cert = None
        self.autopart_add_backup_passphrase = False
        self.autopart_requests = []

        self._default_boot_fstype = None

        self._bootloader = None
        self.config = StorageDiscoveryConfig()
        self.autopart_type = AUTOPART_TYPE_LVM

        self.__luks_devs = {}
        self.fsset = FSSet(self.devicetree)
        self._free_space_snapshot = None

        self._short_product_name = shortProductName
        self._default_luks_version = DEFAULT_LUKS_VERSION

        self._autopart_luks_version = None
        self.autopart_pbkdf_args = None

    def do_it(self, callbacks=None):
        """
        Commit queued changes to disk.

        :param callbacks: callbacks to be invoked when actions are executed
        :type callbacks: return value of the :func:`blivet.callbacks.create_new_callbacks_

        """
        super().do_it(callbacks=callbacks)

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

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

            for dev in boot_devs:
                if not hasattr(dev, "bootable"):
                    log.info("Skipping %s, not bootable", dev)
                    continue

                # 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.parted_disk.type == "msdos":
                    for p in dev.disk.format.parted_disk.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.label_type == "gpt" and \
                   dev.format.type not in ["efi", "macefi"]:
                    skip = True

                if skip:
                    log.info("Skipping %s", dev.name)
                    continue

                # hfs+ partitions on gpt can't be marked bootable via parted
                if dev.disk.format.parted_disk.type != "gpt" or \
                        dev.format.type not in ["hfs+", "macefi"]:
                    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.parted_partition.disk.supportsFeature(
                        parted.DISK_TYPE_PARTITION_NAME):
                    ped_partition = dev.parted_partition.getPedPartition()
                    ped_partition.set_name(dev.format.name)
                    log.info("Setting label on %s to '%s'", dev,
                             dev.format.name)

                dev.disk.setup()
                dev.disk.format.commit_to_disk()

        self.dump_state("final")

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

        return self._bootloader

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

        boot_disks = [d for d in self.disks if d.partitioned]
        boot_disks.sort(key=self.compare_disks_key)
        self.bootloader.set_disk_list(boot_disks)

    @property
    def boot_device(self):
        dev = None
        root_device = self.mountpoints.get("/")

        dev = self.mountpoints.get("/boot", root_device)
        return dev

    @property
    def default_boot_fstype(self):
        """The default filesystem type for the boot partition."""
        if self._default_boot_fstype:
            return self._default_boot_fstype

        fstype = None
        if self.bootloader:
            fstype = self.boot_fstypes[0]
        return fstype

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

            Raise ValueError on invalid input.
        """
        log.debug("trying to set new default /boot fstype to '%s'", newtype)
        # This will raise ValueError if it isn't valid
        self._check_valid_fstype(newtype)
        self._default_boot_fstype = newtype

    @property
    def default_luks_version(self):
        """The default LUKS version."""
        return self._default_luks_version

    def set_default_luks_version(self, version):
        """Set the default LUKS version.

        :param version: a string with LUKS version
        :raises: ValueError on invalid input
        """
        log.debug("trying to set new default luks version to '%s'", version)
        self._check_valid_luks_version(version)
        self._default_luks_version = version

    @property
    def autopart_luks_version(self):
        """The autopart LUKS version."""
        return self._autopart_luks_version or self._default_luks_version

    @autopart_luks_version.setter
    def autopart_luks_version(self, version):
        """Set the autopart LUKS version.

        :param version: a string with LUKS version
        :raises: ValueError on invalid input
        """
        self._check_valid_luks_version(version)
        self._autopart_luks_version = version

    def _check_valid_luks_version(self, version):
        get_format("luks", luks_version=version)

    def set_default_partitioning(self, requests):
        """Set the default partitioning.

        :param requests: a list of partitioning specs
        """
        self.autopart_requests = get_full_partitioning_requests(
            self, _platform, requests)

    def set_up_bootloader(self, early=False):
        """ Set up the boot loader.

            :keyword bool early: Set to True to skip stage1_device setup

            :raises BootloaderError: if stage1 setup fails

            If this needs to be run early, eg. to setup stage1_disk but
            not stage1_device 'early' should be set True to prevent
            it from raising BootloaderError
        """
        if not self.bootloader:
            log.warning("bootloader data missing")
            return

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

        # Need to make sure that boot drive has been setup from the latest information.
        # This will also set self.bootloader.stage1_disk.
        BootloaderExecutor().execute(self, dry_run=False)

        self.bootloader.stage2_device = self.boot_device
        if not early:
            self.bootloader.set_stage1_device(self.devices)

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

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

    def get_fstype(self, mountpoint=None):
        """ Return the default filesystem type based on mountpoint. """
        fstype = super().get_fstype(mountpoint=mountpoint)

        if mountpoint == "/boot":
            fstype = self.default_boot_fstype

        return fstype

    def get_escrow_certificate(self, url):
        """Get the escrow certificate.

        :param url: an URL of the certificate
        :return: a content of the certificate
        """
        if not url:
            return None

        certificate = self._escrow_certificates.get(url, None)

        if not certificate:
            certificate = download_escrow_certificate(url)
            self._escrow_certificates[url] = certificate

        return certificate

    @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:`blivet.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

    @property
    def free_space_snapshot(self):
        # if no snapshot is available, do it now and return it
        self._free_space_snapshot = self._free_space_snapshot or self.get_free_space(
        )

        return self._free_space_snapshot

    def create_free_space_snapshot(self):
        self._free_space_snapshot = self.get_free_space()

        return self._free_space_snapshot

    def get_free_space(self, disks=None, clear_part_type=None):  # pylint: disable=arguments-differ
        """ 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:`blivet.size.Size` instances.

        """

        # FIXME: we should definitely do something with this method -- it takes
        # different parameters than get_free_space from Blivet and does
        # different things too

        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 shutdown(self):
        """ Deactivate all devices. """
        try:
            self.devicetree.teardown_all()
        except Exception:  # pylint: disable=broad-except
            log_exception_info(log.error, "failure tearing down device tree")

    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.
        """
        # 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)

        super().reset(cleanup_only=cleanup_only)

        self.fsset = FSSet(self.devicetree)

        if self.bootloader:
            # clear out bootloader attributes that refer to devices that are
            # no longer in the tree
            self.bootloader.reset()

        self.update_bootloader_disk_list()
        self._mark_protected_devices()

        self.roots = []
        self.roots = find_existing_installations(self.devicetree)
        self.dump_state("initial")

    def _mark_protected_devices(self):
        """Mark protected devices.

        If a device is protected, mark it as such now. Once the tree
        has been populated, devices' protected attribute is how we will
        identify protected devices.
        """
        protected = []

        # Resolve the protected device specs to devices.
        for spec in self.config.protected_dev_specs:
            dev = self.devicetree.resolve_device(spec)

            if dev is not None:
                log.debug("Protected device spec %s resolved to %s.", spec,
                          dev.name)
                protected.append(dev)

        # Find the live backing device and its parents.
        live_device_name = find_live_backing_device()

        if live_device_name:
            log.debug("Resolved live device to %s.", live_device_name)
            dev = self.devicetree.get_device_by_name(live_device_name,
                                                     hidden=True)
            protected.append(dev)
            protected.extend(dev.parents)

        # Mark the collected devices as protected.
        for dev in protected:
            log.debug("Marking device %s as protected.", dev.name)
            dev.protected = True

    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:`blivet.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
        # CLEAR_PARTITIONS_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 [
                CLEAR_PARTITIONS_NONE, CLEAR_PARTITIONS_DEFAULT
        ]:
            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 == CLEAR_PARTITIONS_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 != CLEAR_PARTITIONS_ALL:
                # if clear_part_type is not CLEAR_PARTITIONS_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 CLEAR_PARTITIONS_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 == CLEAR_PARTITIONS_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 == CLEAR_PARTITIONS_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: getattr(p.parted_partition, "number", 1),
            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.name for p in part.disk.children])

        # 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:
                if disk.protected:
                    log.warning(
                        "cannot clear '%s': disk is protected or read only",
                        disk.name)
                else:
                    log.debug("clearpart: initializing %s", disk.name)
                    self.initialize_disk(disk)

        self.update_bootloader_disk_list()

    def _get_hostname(self):
        """Return a hostname."""
        ignored_hostnames = {None, "", 'localhost', 'localhost.localdomain'}

        network_proxy = NETWORK.get_proxy()
        hostname = network_proxy.Hostname

        if hostname in ignored_hostnames:
            hostname = network_proxy.GetCurrentHostname()

        if hostname in ignored_hostnames:
            hostname = None

        return hostname

    def _get_container_name_template(self, prefix=None):
        """Return a template for suggest_container_name method."""
        prefix = prefix or ""  # make sure prefix is a string instead of None

        # try to create a device name incorporating the hostname
        hostname = self._get_hostname()

        if hostname:
            template = "%s_%s" % (prefix, hostname.split('.')[0].lower())
            template = self.safe_device_name(template)
        else:
            template = prefix

        if conf.target.is_image:
            template = "%s_image" % template

        return template

    def turn_on_swap(self):
        self.fsset.turn_on_swap(root_path=util.getSysroot())

    def mount_filesystems(self, read_only=None, skip_root=False):
        self.fsset.mount_filesystems(root_path=util.getSysroot(),
                                     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" % (util.getSysroot(), 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)
Beispiel #8
0
class InstallerStorage(Blivet):
    """ Top-level class for managing installer-related storage configuration. """

    def __init__(self):
        super().__init__()
        self.protected_devices = []
        self._escrow_certificates = {}
        self._default_boot_fstype = None
        self._bootloader = None
        self.__luks_devs = {}
        self.fsset = FSSet(self.devicetree)
        self._short_product_name = shortProductName
        self._default_luks_version = DEFAULT_LUKS_VERSION

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

        return self._bootloader

    @property
    def boot_device(self):
        root_device = self.mountpoints.get("/")
        dev = self.mountpoints.get("/boot", root_device)
        return dev

    @property
    def default_boot_fstype(self):
        """The default filesystem type for the boot partition."""
        if self._default_boot_fstype:
            return self._default_boot_fstype

        return self.bootloader.stage2_format_types[0]

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

            Raise ValueError on invalid input.
        """
        log.debug("trying to set new default /boot fstype to '%s'", newtype)
        # This will raise ValueError if it isn't valid
        self._check_valid_fstype(newtype)
        self._default_boot_fstype = newtype

    @property
    def default_luks_version(self):
        """The default LUKS version."""
        return self._default_luks_version

    def set_default_luks_version(self, version):
        """Set the default LUKS version.

        :param version: a string with LUKS version
        :raises: ValueError on invalid input
        """
        log.debug("trying to set new default luks version to '%s'", version)
        self._check_valid_luks_version(version)
        self._default_luks_version = version

    def _check_valid_luks_version(self, version):
        get_format("luks", luks_version=version)

    def get_fstype(self, mountpoint=None):
        """ Return the default filesystem type based on mountpoint. """
        fstype = super().get_fstype(mountpoint=mountpoint)

        if mountpoint == "/boot":
            fstype = self.default_boot_fstype

        return fstype

    def get_escrow_certificate(self, url):
        """Get the escrow certificate.

        :param url: an URL of the certificate
        :return: a content of the certificate
        """
        if not url:
            return None

        certificate = self._escrow_certificates.get(url, None)

        if not certificate:
            certificate = download_escrow_certificate(url)
            self._escrow_certificates[url] = certificate

        return certificate

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

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

    def get_file_system_free_space(self, mount_points=("/", "/usr")):
        """Get total file system free space on the given mount points.

        Calculates total free space in / and /usr, by default.

        :param mount_points: a list of mount points
        :return: a total size
        """
        free = Size(0)
        btrfs_volumes = []

        for mount_point in mount_points:
            device = self.mountpoints.get(mount_point)
            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 get_disk_free_space(self, disks=None):
        """Get total free space on the given disks.

        Calculates free space available for use.

        :param disks: a list of disks or None
        :return: a total size
        """
        # Use all disks in the device tree by default.
        if disks is None:
            disks = self.disks

        # Get the dictionary of free spaces for each disk.
        snapshot = self.get_free_space(disks)

        # Calculate the total free space.
        return sum((disk_free for disk_free, fs_free in snapshot.values()), Size(0))

    def get_disk_reclaimable_space(self, disks=None):
        """Get total reclaimable space on the given disks.

        Calculates free space unavailable but reclaimable
        from existing partitions.

        :param disks: a list of disks or None
        :return: a total size
        """
        # Use all disks in the device tree by default.
        if disks is None:
            disks = self.disks

        # Get the dictionary of free spaces for each disk.
        snapshot = self.get_free_space(disks)

        # Calculate the total reclaimable free space.
        return sum((fs_free for disk_free, fs_free in snapshot.values()), Size(0))

    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.
        """
        # set up the disk images
        if conf.target.is_image:
            self.setup_disk_images()

        # 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)

        super().reset(cleanup_only=cleanup_only)

        self.fsset = FSSet(self.devicetree)

        # Clear out attributes that refer to devices that are no longer in the tree.
        self.bootloader.reset()

        self._mark_protected_devices()

        self.roots = []
        self.roots = find_existing_installations(self.devicetree)
        self.dump_state("initial")

    def _mark_protected_devices(self):
        """Mark protected devices.

        If a device is protected, mark it as such now. Once the tree
        has been populated, devices' protected attribute is how we will
        identify protected devices.
        """
        protected = []

        # Resolve the protected device specs to devices.
        for spec in self.protected_devices:
            dev = self.devicetree.resolve_device(spec)

            if dev is not None:
                log.debug("Protected device spec %s resolved to %s.", spec, dev.name)
                protected.append(dev)

        # Find the live backing device and its parents.
        live_device_name = find_live_backing_device()

        if live_device_name:
            log.debug("Resolved live device to %s.", live_device_name)
            dev = self.devicetree.get_device_by_name(live_device_name, hidden=True)
            protected.append(dev)
            protected.extend(dev.parents)

        # Mark the collected devices as protected.
        for dev in protected:
            log.debug("Marking device %s as protected.", dev.name)
            dev.protected = True

    def protect_devices(self, protected_names):
        """Protect given devices.

        :param protected_names: a list of device names
        """
        protected = set(protected_names)
        unprotected = set(self.protected_devices)

        # Mark unprotected devices.
        # Skip devices that should stay protected.
        for spec in unprotected - protected:
            device = self.devicetree.resolve_device(spec)

            if device:
                log.debug("Marking device %s as unprotected.", device.name)
                device.protected = False

        # Mark protected devices.
        # Skip devices that are already protected.
        for spec in protected - unprotected:
            device = self.devicetree.resolve_device(spec)

            if device:
                log.debug("Marking device %s as protected.", device.name)
                device.protected = True

        # Update the list.
        self.protected_devices = protected_names

    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 usable_disks(self):
        """Disks that can be used for the installation.

        :return: a list of disks
        """
        # Get all devices.
        devices = self.devicetree.devices

        # Add the hidden devices.
        if conf.target.is_image:
            devices += [
                d for d in self.devicetree._hidden
                if d.name in self.devicetree.disk_images
            ]
        else:
            devices += self.devicetree._hidden

        # Filter out the usable disks.
        disks = []
        for d in devices:
            if d.is_disk and not d.format.hidden and not d.protected:
                # Unformatted DASDs are detected with a size of 0, but they should
                # still show up as valid disks if this function is called, since we
                # can still use them; anaconda will know how to handle them, so they
                # don't need to be ignored anymore.
                if d.type == "dasd":
                    disks.append(d)
                elif d.size > 0 and d.media_present:
                    disks.append(d)

        # Remove duplicate names from the list.
        return sorted(set(disks), key=lambda d: d.name)

    def select_disks(self, selected_names):
        """Select disks that should be used for the installation.

        Hide usable disks that are not selected.

        :param selected_names: a list of disk names
        """
        for disk in self.usable_disks:
            if disk.name not in selected_names:
                if disk in self.devices:
                    self.devicetree.hide(disk)
            else:
                if disk not in self.devices:
                    self.devicetree.unhide(disk)

    @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 _get_hostname(self):
        """Return a hostname."""
        ignored_hostnames = {None, "", 'localhost', 'localhost.localdomain'}

        network_proxy = NETWORK.get_proxy()
        hostname = network_proxy.Hostname

        if hostname in ignored_hostnames:
            hostname = network_proxy.GetCurrentHostname()

        if hostname in ignored_hostnames:
            hostname = None

        return hostname

    def _get_container_name_template(self, prefix=None):
        """Return a template for suggest_container_name method."""
        prefix = prefix or ""  # make sure prefix is a string instead of None

        # try to create a device name incorporating the hostname
        hostname = self._get_hostname()

        if hostname:
            template = "%s_%s" % (prefix, hostname.split('.')[0].lower())
            template = self.safe_device_name(template)
        else:
            template = prefix

        if conf.target.is_image:
            template = "%s_image" % template

        return template

    def turn_on_swap(self):
        self.fsset.turn_on_swap(root_path=util.getSysroot())

    def mount_filesystems(self, read_only=None, skip_root=False):
        self.fsset.mount_filesystems(root_path=util.getSysroot(),
                                     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" % (util.getSysroot(), 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)