anaconda/anaconda-40.22.3.13/pyanaconda/modules/storage/partitioning/automatic/utils.py

544 lines
20 KiB
Python
Raw Normal View History

2024-11-14 21:39:56 -08:00
#
# Copyright (C) 2009-2015 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): Dave Lehman <dlehman@redhat.com>
#
import parted
from blivet.size import Size
from blivet.devices.partition import PartitionDevice, FALLBACK_DEFAULT_PART_SIZE
from blivet.devices.luks import LUKSDevice
from blivet.devices.lvm import DEFAULT_THPOOL_RESERVE
from blivet.errors import NotEnoughFreeSpaceError, NoDisksError
from blivet.formats import get_format
from blivet.formats.luks import LUKS2PBKDFArgs
from blivet.partitioning import get_free_regions, get_next_partition_type
from pykickstart.constants import AUTOPART_TYPE_BTRFS, AUTOPART_TYPE_LVM, \
AUTOPART_TYPE_LVM_THINP, AUTOPART_TYPE_PLAIN
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.i18n import _
from pyanaconda.modules.common.errors.storage import ProtectedDeviceError
from pyanaconda.modules.storage.partitioning.specification import PartSpec
from pyanaconda.modules.storage.platform import platform
log = get_module_logger(__name__)
def get_pbkdf_args(luks_version, pbkdf_type=None, max_memory_kb=0, iterations=0, time_ms=0):
"""Get the pbkdf arguments.
:param luks_version: a version of LUKS
:param pbkdf_type: a type of PBKDF
:param max_memory_kb: a memory cost for PBKDF
:param iterations: a number of iterations
:param time_ms: an iteration time in ms
:return:
"""
# PBKDF arguments are not supported for LUKS 1.
if luks_version != "luks2":
return None
# Use defaults.
if not pbkdf_type and not max_memory_kb and not iterations and not time_ms:
log.debug("Using default PBKDF args.")
return None
# Use specified arguments.
return LUKS2PBKDFArgs(pbkdf_type or None, max_memory_kb or 0, iterations or 0, time_ms or 0)
def lookup_alias(devicetree, alias):
"""Look up a device of the given alias in the device tree.
:param devicetree: a device tree to look up devices
:param alias: an alias name
:return: a device object
"""
for dev in devicetree.devices:
if getattr(dev, "req_name", None) == alias:
return dev
return None
def shrink_device(storage, device, size):
"""Shrink the size of the device.
:param storage: a storage model
:param device: a device to shrink
:param size: a new size of the device
"""
if device.protected:
raise ProtectedDeviceError(device.name)
# The device size is small enough.
if device.size <= size:
log.debug("The size of %s is already %s.", device.name, device.size)
return
# Resize the device.
log.debug("Shrinking a size of %s to %s.", device.name, size)
aligned_size = device.align_target_size(size)
storage.resize_device(device, aligned_size)
def remove_device(storage, device):
"""Remove a device after removing its dependent devices.
If the device is protected, do nothing. If the device has
protected children, just remove the unprotected ones.
:param storage: a storage model
:param device: a device to remove
"""
if device.protected:
raise ProtectedDeviceError(device.name)
# Only remove unprotected children if any protected.
if any(d.protected for d in device.children):
log.debug("Removing unprotected children of %s.", device.name)
for child in (d for d in device.children if not d.protected):
storage.recursive_remove(child)
return
# No protected children, remove the device
log.debug("Removing device %s.", device.name)
storage.recursive_remove(device)
def get_candidate_disks(storage):
"""Return a list of disks to be used for autopart/reqpart.
Disks must be partitioned and have a single free region large enough
for a default-sized (500MiB) partition.
:param storage: the storage object
:type storage: an instance of InstallerStorage
:return: a list of partitioned disks with at least 500MiB of free space
:rtype: list of :class:`blivet.devices.StorageDevice`
"""
usable_disks = []
for disk in storage.partitioned:
if not disk.format.supported or disk.protected:
continue
usable_disks.append(disk)
free_disks = []
for disk in usable_disks:
if get_next_partition_type(disk.format.parted_disk) is None:
# new partition can't be added to the disk -- there is no free slot
# for a primary partition and no extended partition
continue
part = disk.format.first_partition
while part:
if not part.type & parted.PARTITION_FREESPACE:
part = part.nextPartition()
continue
if Size(part.getLength(unit="B")) > PartitionDevice.default_size:
free_disks.append(disk)
break
part = part.nextPartition()
if not usable_disks:
raise NoDisksError(_("No usable disks selected."))
if not free_disks:
raise NotEnoughFreeSpaceError(_("Not enough free space on selected disks."))
return free_disks
def get_disks_for_implicit_partitions(disks, scheme, requests):
"""Return a list of disks that can be used for implicit partitions.
:param disks: a list of candidate disks
:param scheme: a type of the partitioning scheme
:param requests: a list of partitioning requests
:return: a list of disks that can be used for implicit partitions
"""
# There will be no implicit partitions.
if scheme == AUTOPART_TYPE_PLAIN:
return []
# Calculate slots for requested partitions.
requested_slots = 0
for request in requests:
if request.is_partition(scheme):
requested_slots += 1
# Collect extra disks for implicit partitions.
extra_disks = []
for disk in disks:
parted_disk = disk.format.parted_disk
supports_extended = parted_disk.supportsFeature(parted.DISK_TYPE_EXTENDED)
available_slots = parted_disk.maxPrimaryPartitionCount - parted_disk.primaryPartitionCount
# Skip disks that will be used for requested partitions.
if requested_slots and not supports_extended and available_slots <= requested_slots:
requested_slots -= available_slots
log.debug("Don't use %s for implicit partitions.", disk.name)
else:
requested_slots = 0
extra_disks.append(disk)
log.debug("Found disks for implicit partitions: %s", [d.name for d in extra_disks])
return extra_disks
def schedule_implicit_partitions(storage, disks, scheme, encrypted=False, luks_fmt_args=None):
"""Schedule creation of a lvm/btrfs member partitions for autopart.
We create one such partition on each disk. They are not allocated until
later (in :func:`doPartitioning`).
:param storage: the storage object
:type storage: an instance of InstallerStorage
:param disks: list of partitioned disks with free space
:type disks: list of :class:`blivet.devices.StorageDevice`
:param scheme: a type of the partitioning scheme
:type scheme: int
:param encrypted: encrypt the scheduled partitions
:type encrypted: bool
:param luks_fmt_args: arguments for the LUKS format constructor
:type luks_fmt_args: dict
:return: list of newly created (unallocated) partitions
:rtype: list of :class:`blivet.devices.PartitionDevice`
"""
# create a separate pv or btrfs partition for each disk with free space
devs = []
# only schedule the partitions if either lvm or btrfs autopart was chosen
if scheme == AUTOPART_TYPE_PLAIN:
return devs
for disk in disks:
if encrypted:
fmt_type = "luks"
fmt_args = luks_fmt_args or {}
else:
if scheme in (AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP):
fmt_type = "lvmpv"
else:
fmt_type = "btrfs"
fmt_args = {}
part = storage.new_partition(fmt_type=fmt_type,
fmt_args=fmt_args,
grow=True,
parents=[disk])
storage.create_device(part)
devs.append(part)
log.debug("Created the implicit partition %s for %s.", part.name, disk.name)
return devs
def get_default_partitioning():
"""Get the default partitioning requests.
:return: a list of partitioning specs
"""
# Get the platform-specific partitioning.
partitioning = list(platform.partitions)
# Get the product-specific partitioning.
for attrs in conf.storage.default_partitioning:
partitioning.append(get_part_spec(attrs))
return partitioning
def get_part_spec(attrs):
"""Creates an instance of PartSpec.
:param attrs: A dictionary containing the configuration
:return: a partitioning spec
:rtype: PartSpec
"""
name = attrs.get("name")
swap = name == "swap"
schemes = set()
if attrs.get("btrfs"):
schemes.add(AUTOPART_TYPE_BTRFS)
spec = PartSpec(
mountpoint=name if not swap else None,
fstype=None if not swap else "swap",
lv=True,
thin=not swap,
btr=not swap,
size=attrs.get("min") or attrs.get("size"),
max_size=attrs.get("max"),
grow="min" in attrs,
required_space=attrs.get("free") or 0,
encrypted=True,
schemes=schemes,
)
return spec
def schedule_partitions(storage, disks, implicit_devices, scheme, requests, encrypted=False,
luks_fmt_args=None):
"""Schedule creation of autopart/reqpart partitions.
This only schedules the requests for actual partitions.
:param storage: the storage object
:type storage: an instance of InstallerStorage
:param disks: list of partitioned disks with free space
:type disks: list of :class:`blivet.devices.StorageDevice`
:param implicit_devices: list of implicit devices
:type implicit_devices: list of :class:`blivet.devices.StorageDevice`
:param scheme: a type of the partitioning scheme
:type scheme: int
:param requests: list of partitioning requests
:type requests: list of :class:`~.storage.partspec.PartSpec` instances
:param encrypted: encrypt the scheduled partitions
:type encrypted: bool
:param luks_fmt_args: arguments for the LUKS format constructor
:type luks_fmt_args: dict
"""
# basis for requests with required_space is the sum of the sizes of the
# two largest free regions
all_free = (Size(reg.getLength(unit="B")) for reg in get_free_regions(disks))
all_free = sorted(all_free, reverse=True)
if not all_free:
# this should never happen since we've already filtered the disks
# to those with at least 500MiB free
log.error("no free space on disks %s", [d.name for d in disks])
return
free = all_free[0]
if len(all_free) > 1:
free += all_free[1]
# The boot disk must be set at this point. See if any platform-specific
# stage1 device we might allocate already exists on the boot disk.
stage1_device = None
for device in storage.devices:
if storage.bootloader.stage1_disk not in device.disks:
continue
if storage.bootloader.is_valid_stage1_device(device, early=True):
stage1_device = device
break
#
# First pass is for partitions only. We'll do LVs later.
#
for request in requests:
if request.lv and scheme in (AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP):
continue
if request.btr and scheme == AUTOPART_TYPE_BTRFS:
continue
if request.required_space and request.required_space > free:
continue
elif request.fstype in ("prepboot", "efi", "macefi", "hfs+") and \
(storage.bootloader.skip_bootloader or stage1_device):
# there should never be a need for more than one of these
# partitions, so skip them.
log.info("skipping unneeded stage1 %s request", request.fstype)
log.debug("%s", request)
if request.fstype in ["efi", "macefi"] and stage1_device:
# Set the mountpoint for the existing EFI boot partition
stage1_device.format.mountpoint = "/boot/efi"
log.debug("%s", stage1_device)
continue
elif request.fstype == "biosboot":
is_gpt = (stage1_device and
getattr(stage1_device.format, "label_type", None) == "gpt")
has_bios_boot = (stage1_device and
any([p.format.type == "biosboot"
for p in storage.partitions
if p.disk == stage1_device]))
if (storage.bootloader.skip_bootloader or
not (stage1_device and stage1_device.is_disk and
is_gpt and not has_bios_boot)):
# there should never be a need for more than one of these
# partitions, so skip them.
log.info("skipping unneeded stage1 %s request", request.fstype)
log.debug("%s", request)
log.debug("%s", stage1_device)
continue
if request.size > all_free[0]:
# no big enough free space for the requested partition
raise NotEnoughFreeSpaceError(_("No big enough free space on disks for "
"automatic partitioning"))
if request.encrypted and encrypted:
fmt_type = "luks"
fmt_args = luks_fmt_args or {}
else:
fmt_type = request.fstype
fmt_args = {}
dev = storage.new_partition(fmt_type=fmt_type,
fmt_args=fmt_args,
size=request.size,
grow=request.grow,
maxsize=request.max_size,
mountpoint=request.mountpoint,
parents=disks)
# schedule the device for creation
storage.create_device(dev)
if request.encrypted and encrypted:
luks_fmt = get_format(request.fstype,
device=dev.path,
mountpoint=request.mountpoint)
luks_dev = LUKSDevice("luks-%s" % dev.name,
fmt=luks_fmt,
size=dev.size,
parents=dev)
storage.create_device(luks_dev)
if scheme in (AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP, AUTOPART_TYPE_BTRFS):
# doing LVM/BTRFS -- make sure the newly created partition fits in some
# free space together with one of the implicitly requested partitions
smallest_implicit = sorted(implicit_devices, key=lambda d: d.size)[0]
if (request.size + smallest_implicit.size) > all_free[0]:
# not enough space to allocate the smallest implicit partition
# and the request, make the implicit partitions smaller in
# attempt to make space for the request
for implicit_req in implicit_devices:
implicit_req.size = FALLBACK_DEFAULT_PART_SIZE
return implicit_devices
def schedule_volumes(storage, devices, scheme, requests, encrypted=False):
"""Schedule creation of autopart lvm/btrfs volumes.
Schedules encryption of member devices if requested, schedules creation
of the container (:class:`blivet.devices.LVMVolumeGroupDevice` or
:class:`blivet.devices.BTRFSVolumeDevice`) then schedules creation of the
autopart volume requests.
If an appropriate bootloader stage1 device exists on the boot drive, any
autopart request to create another one will be skipped/discarded.
:param storage: the storage object
:type storage: an instance of InstallerStorage
:param devices: list of member partitions
:type devices: list of :class:`blivet.devices.PartitionDevice`
:param scheme: a type of the partitioning scheme
:type scheme: int
:param requests: list of partitioning requests
:type requests: list of :class:`~.storage.partspec.PartSpec` instances
:param encrypted: encrypt the scheduled partitions
:type encrypted: bool
"""
if not devices:
return
if scheme in (AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP):
new_container = storage.new_vg
new_volume = storage.new_lv
format_name = "lvmpv"
else:
new_container = storage.new_btrfs
new_volume = storage.new_btrfs
format_name = "btrfs"
if encrypted:
pvs = []
for dev in devices:
pv = LUKSDevice("luks-%s" % dev.name,
fmt=get_format(format_name, device=dev.path),
size=dev.size,
parents=dev)
pvs.append(pv)
storage.create_device(pv)
else:
pvs = devices
# create a vg containing all of the autopart pvs
container = new_container(parents=pvs)
storage.create_device(container)
#
# Convert requests into Device instances and schedule them for creation.
#
# Second pass, for LVs only.
pool = None
for request in requests:
btr = bool(scheme == AUTOPART_TYPE_BTRFS and request.btr)
lv = bool(scheme in (AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP) and request.lv)
thinlv = bool(scheme == AUTOPART_TYPE_LVM_THINP and request.lv and request.thin)
if thinlv and pool is None:
# create a single thin pool in the vg
pool = storage.new_lv(parents=[container], thin_pool=True, grow=True)
storage.create_device(pool)
# make sure VG reserves space for the pool to grow if needed
container.thpool_reserve = DEFAULT_THPOOL_RESERVE
if not btr and not lv and not thinlv:
continue
# required space isn't relevant on btrfs
if (lv or thinlv) and \
request.required_space and request.required_space > container.size:
continue
if request.fstype is None:
if btr:
# btrfs volumes can only contain btrfs filesystems
request.fstype = "btrfs"
else:
request.fstype = storage.default_fstype
kwargs = {"mountpoint": request.mountpoint,
"fmt_type": request.fstype}
if lv or thinlv:
if thinlv:
parents = [pool]
else:
parents = [container]
kwargs.update({"parents": parents,
"grow": request.grow,
"maxsize": request.max_size,
"size": request.size,
"thin_volume": thinlv})
else:
kwargs.update({"parents": [container],
"size": request.size,
"subvol": True})
dev = new_volume(**kwargs)
# schedule the device for creation
storage.create_device(dev)