1242 lines
40 KiB
Python
1242 lines
40 KiB
Python
#
|
|
# Utilities for the interactive partitioning module
|
|
#
|
|
# Copyright (C) 2019 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.
|
|
#
|
|
import itertools
|
|
import re
|
|
|
|
from blivet import devicefactory
|
|
from blivet.devicelibs import crypto, raid
|
|
from blivet.devices import LUKSDevice, MDRaidArrayDevice, LVMVolumeGroupDevice
|
|
from blivet.errors import StorageError
|
|
from blivet.formats import get_format
|
|
from blivet.size import Size
|
|
|
|
from pyanaconda.anaconda_loggers import get_module_logger
|
|
from pyanaconda.core.i18n import _
|
|
from pyanaconda.modules.common.errors.configuration import StorageConfigurationError
|
|
from pyanaconda.modules.common.errors.storage import UnsupportedDeviceError, UnknownDeviceError
|
|
from pyanaconda.modules.common.structures.device_factory import DeviceFactoryRequest, \
|
|
DeviceFactoryPermissions
|
|
from pyanaconda.modules.storage.disk_initialization import DiskInitializationConfig
|
|
from pyanaconda.modules.storage.platform import platform, PLATFORM_MOUNT_POINTS
|
|
from pyanaconda.core.product import get_product_name, get_product_version
|
|
from pyanaconda.modules.storage.devicetree.root import Root
|
|
from pyanaconda.modules.storage.devicetree.utils import get_supported_filesystems, \
|
|
is_supported_filesystem
|
|
from pyanaconda.core.storage import DEVICE_TEXT_MAP, PARTITION_ONLY_FORMAT_TYPES, \
|
|
NAMED_DEVICE_TYPES, CONTAINER_DEVICE_TYPES, SUPPORTED_DEVICE_TYPES
|
|
|
|
log = get_module_logger(__name__)
|
|
|
|
|
|
def filter_unsupported_disklabel_devices(devices):
|
|
"""Return input list minus any devices that exist on an unsupported disklabel."""
|
|
return [d for d in devices if not any(
|
|
not getattr(p, "disklabel_supported", True) for p in d.ancestors
|
|
)]
|
|
|
|
|
|
def collect_used_devices(storage):
|
|
"""Collect devices used in existing or new installations.
|
|
|
|
:param storage: an instance of Blivet
|
|
:return: a list of devices
|
|
"""
|
|
used_devices = []
|
|
|
|
for root in storage.roots:
|
|
for device in root.devices:
|
|
if device not in storage.devices:
|
|
continue
|
|
used_devices.extend(device.ancestors)
|
|
|
|
for new in [d for d in storage.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 storage.partitions:
|
|
if getattr(device, "is_logical", False):
|
|
extended = device.disk.format.extended_partition.path
|
|
used_devices.append(storage.devicetree.get_device_by_path(extended))
|
|
|
|
return used_devices
|
|
|
|
|
|
def collect_unused_devices(storage):
|
|
"""Collect devices that are not used in existing or new installations.
|
|
|
|
:param storage: an instance of Blivet
|
|
:return: a list of devices
|
|
"""
|
|
used_devices = set(collect_used_devices(storage))
|
|
|
|
unused = [
|
|
d for d in storage.devices
|
|
if d.disks
|
|
and d.media_present
|
|
and not d.partitioned
|
|
and (d.direct or d.isleaf)
|
|
and d not in used_devices
|
|
]
|
|
|
|
# Add incomplete VGs and MDs
|
|
incomplete = [
|
|
d for d in storage.devicetree._devices
|
|
if not getattr(d, "complete", True)
|
|
]
|
|
|
|
# Add partitioned devices with unsupported format.
|
|
unsupported = [
|
|
d for d in storage.partitioned
|
|
if not d.format.supported
|
|
]
|
|
|
|
return filter_unsupported_disklabel_devices(unused + incomplete + unsupported)
|
|
|
|
|
|
def collect_bootloader_devices(storage, boot_drive):
|
|
"""Collect the bootloader devices.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param boot_drive: a name of the bootloader drive
|
|
:return: a list of devices
|
|
"""
|
|
devices = []
|
|
|
|
for device in storage.devices:
|
|
if device.format.type not in ["biosboot", "prepboot"]:
|
|
continue
|
|
|
|
# Boot drive may not be setup because it IS one of these.
|
|
if not boot_drive or boot_drive in (d.name for d in device.disks):
|
|
devices.append(device)
|
|
|
|
return filter_unsupported_disklabel_devices(devices)
|
|
|
|
|
|
def collect_new_devices(storage, boot_drive):
|
|
"""Collect new devices.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param boot_drive: a name of the bootloader drive
|
|
:return: a list of devices
|
|
"""
|
|
# A device scheduled for formatting only belongs in the new root.
|
|
new_devices = [
|
|
d for d in storage.devices
|
|
if d.direct
|
|
and not d.format.exists
|
|
and not d.partitioned
|
|
]
|
|
|
|
# If mount points have been assigned to any existing devices, go ahead
|
|
# and pull those in along with any existing swap devices. It doesn't
|
|
# matter if the formats being mounted exist or not.
|
|
new_mounts = [
|
|
d for d in storage.mountpoints.values() if d.exists
|
|
]
|
|
|
|
if new_mounts or new_devices:
|
|
new_devices.extend(storage.mountpoints.values())
|
|
new_devices.extend(collect_bootloader_devices(storage, boot_drive))
|
|
|
|
# Remove duplicates, but keep the order.
|
|
return filter_unsupported_disklabel_devices(list(dict.fromkeys(new_devices)))
|
|
|
|
|
|
def collect_roots(storage):
|
|
"""Collect roots of existing installations.
|
|
|
|
:param storage: an instance of Blivet
|
|
:return: a list of roots
|
|
"""
|
|
roots = []
|
|
supported_devices = set(filter_unsupported_disklabel_devices(storage.devices))
|
|
|
|
# Get the name of the new installation.
|
|
new_root_name = get_new_root_name()
|
|
|
|
for root in storage.roots:
|
|
# Get the name.
|
|
name = root.name
|
|
|
|
# Get the supported devices.
|
|
devices = [
|
|
d for d in root.devices
|
|
if d in supported_devices
|
|
and (d.format.exists or root.name == new_root_name)
|
|
]
|
|
|
|
# Get the supported mount points.
|
|
mounts = {
|
|
m: d for m, d in root.mounts.items()
|
|
if d in supported_devices
|
|
and (d.format.exists or root.name == new_root_name)
|
|
and d.disks
|
|
}
|
|
|
|
if not devices and not mounts:
|
|
continue
|
|
|
|
# Add a root with supported devices.
|
|
roots.append(Root(
|
|
name=name,
|
|
devices=devices,
|
|
mounts=mounts,
|
|
))
|
|
|
|
return roots
|
|
|
|
|
|
def get_new_root_name():
|
|
"""Get the name of the new installation.
|
|
|
|
:return: a translated string
|
|
"""
|
|
return _("New {name} {version} Installation").format(
|
|
name=get_product_name(), version=get_product_version()
|
|
)
|
|
|
|
|
|
def create_new_root(storage, boot_drive):
|
|
"""Create a new root from the given devices.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param boot_drive: a name of the bootloader drive
|
|
:return: a new root
|
|
"""
|
|
devices = collect_new_devices(
|
|
storage=storage,
|
|
boot_drive=boot_drive
|
|
)
|
|
|
|
mounts = {
|
|
d.format.mountpoint: d for d in devices
|
|
if getattr(d.format, "mountpoint", None)
|
|
}
|
|
|
|
return Root(
|
|
name=get_new_root_name(),
|
|
devices=devices,
|
|
mounts=mounts,
|
|
)
|
|
|
|
|
|
def collect_mount_points():
|
|
"""Collect supported mount points.
|
|
|
|
:return: a list of paths
|
|
"""
|
|
paths = ["/", "/boot", "/home", "/var"]
|
|
|
|
# Add the mount point requirements for bootloader stage1 devices.
|
|
paths.extend(platform.stage1_constraints[PLATFORM_MOUNT_POINTS])
|
|
|
|
# Sort the list now so all the real mount points go to the front,
|
|
# then add all the pseudo mount points we have.
|
|
paths.sort()
|
|
paths += ["swap"]
|
|
|
|
for fmt in ["appleboot", "biosboot", "prepboot"]:
|
|
if get_format(fmt).supported:
|
|
paths += [fmt]
|
|
|
|
return paths
|
|
|
|
|
|
def validate_label(label, fmt):
|
|
"""Validate the label.
|
|
|
|
:param str label: a label
|
|
:param DeviceFormat fmt: a device format to label
|
|
:return: an error message
|
|
"""
|
|
if not label:
|
|
return None
|
|
|
|
if not fmt.labeling():
|
|
return _("Cannot set label on file system.")
|
|
|
|
if not fmt.label_format_ok(label):
|
|
return _("Unacceptable label format for file system.")
|
|
|
|
return None
|
|
|
|
|
|
def validate_mount_point(path, mount_points):
|
|
"""Validate the given path of a mount point.
|
|
|
|
:param path: a path to validate
|
|
:param mount_points: a list of existing mount points
|
|
:return: an error message
|
|
"""
|
|
system_mount_points = ["/dev", "/proc", "/run", "/sys"]
|
|
|
|
if path in mount_points:
|
|
return _("That mount point is already in use. Try something else?")
|
|
|
|
if not path:
|
|
return _("Please enter a valid mount point.")
|
|
|
|
if path in system_mount_points:
|
|
return _("That mount point is invalid. Try something else?")
|
|
|
|
if ((len(path) > 1 and path.endswith("/")) or
|
|
not path.startswith("/") or
|
|
" " in path or
|
|
re.search(r'/\.*/', path) or
|
|
re.search(r'/\.+$', path)):
|
|
# - does not end with '/' unless mountpoint _is_ '/'
|
|
# - starts with '/' except for "swap", &c
|
|
# - does not contain spaces
|
|
# - does not contain pairs of '/' enclosing zero or more '.'
|
|
# - does not end with '/' followed by one or more '.'
|
|
return _("That mount point is invalid. Try something else?")
|
|
|
|
return None
|
|
|
|
|
|
def validate_container_name(storage, name):
|
|
"""Validate the given container name.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param name: a container name
|
|
:return: an error message or None
|
|
"""
|
|
safe_name = storage.safe_device_name(name)
|
|
|
|
if name != safe_name:
|
|
return _("Invalid container name.")
|
|
|
|
if name in storage.names:
|
|
return _("Name is already in use.")
|
|
|
|
return None
|
|
|
|
|
|
def get_raid_level_by_name(name):
|
|
"""Get the RAID level object for the given name.
|
|
|
|
:param name: a name of the RAID level
|
|
:return: an instance of RAIDLevel
|
|
"""
|
|
if not name:
|
|
return None
|
|
|
|
return raid.get_raid_level(name)
|
|
|
|
|
|
def validate_raid_level(raid_level, num_members):
|
|
"""Validate the given raid level.
|
|
|
|
:param raid_level: a RAID level
|
|
:param num_members: a number of members
|
|
:return: an error message
|
|
"""
|
|
if num_members < raid_level.min_members:
|
|
return _("The RAID level you have selected ({level}) requires more disks "
|
|
"({min}) than you currently have selected ({count}).").format(
|
|
level=raid_level,
|
|
min=raid_level.min_members,
|
|
count=num_members
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
def validate_device_factory_request(storage, request: DeviceFactoryRequest):
|
|
"""Validate the given device info.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param request: a device factory request to validate
|
|
:return: an error message
|
|
"""
|
|
device = storage.devicetree.resolve_device(request.device_spec)
|
|
device_type = request.device_type
|
|
reformat = request.reformat
|
|
fs_type = request.format_type
|
|
encrypted = request.device_encrypted
|
|
raid_level = get_raid_level_by_name(request.device_raid_level)
|
|
mount_point = request.mount_point
|
|
label = request.label
|
|
num_disk = len(request.disks)
|
|
|
|
changed_label = label != getattr(device.format, "label", "")
|
|
changed_fstype = fs_type != device.format.type
|
|
|
|
if changed_label or changed_fstype:
|
|
error = validate_label(
|
|
label,
|
|
get_format(fs_type)
|
|
)
|
|
if error:
|
|
return error
|
|
|
|
is_format_mountable = get_format(fs_type).mountable
|
|
changed_mount_point = mount_point != getattr(device.format, "mountpoint", "")
|
|
|
|
if reformat and is_format_mountable and not mount_point:
|
|
return _("Please enter a mount point.")
|
|
|
|
if changed_mount_point and mount_point:
|
|
error = validate_mount_point(
|
|
mount_point,
|
|
storage.mountpoints.keys()
|
|
)
|
|
if error:
|
|
return error
|
|
|
|
supported_types = (devicefactory.DEVICE_TYPE_PARTITION, devicefactory.DEVICE_TYPE_MD)
|
|
|
|
if mount_point == "/boot/efi" and device_type not in supported_types:
|
|
return _("/boot/efi must be on a device of type {type} or {another}").format(
|
|
type=_(DEVICE_TEXT_MAP[devicefactory.DEVICE_TYPE_PARTITION]),
|
|
another=_(DEVICE_TEXT_MAP[devicefactory.DEVICE_TYPE_MD])
|
|
)
|
|
|
|
if device_type != devicefactory.DEVICE_TYPE_PARTITION and \
|
|
fs_type in PARTITION_ONLY_FORMAT_TYPES:
|
|
return _("{fs} must be on a device of type {type}").format(
|
|
fs=fs_type,
|
|
type=_(DEVICE_TEXT_MAP[devicefactory.DEVICE_TYPE_PARTITION])
|
|
)
|
|
|
|
if mount_point and encrypted and mount_point.startswith("/boot"):
|
|
return _("{} cannot be encrypted").format(mount_point)
|
|
|
|
if encrypted and fs_type in PARTITION_ONLY_FORMAT_TYPES:
|
|
return _("{} cannot be encrypted").format(fs_type)
|
|
|
|
if mount_point == "/" and device.format.exists and not reformat:
|
|
return _("You must create a new file system on the root device.")
|
|
|
|
if (raid_level is not None or device_type == devicefactory.DEVICE_TYPE_MD) and \
|
|
raid_level not in get_supported_raid_levels(device_type):
|
|
return _("Device does not support RAID level selection {}.").format(raid_level)
|
|
|
|
if raid_level is not None:
|
|
error = validate_raid_level(
|
|
raid_level,
|
|
num_disk
|
|
)
|
|
if error:
|
|
return error
|
|
|
|
return None
|
|
|
|
|
|
def suggest_device_name(storage, device):
|
|
"""Get a suggestion for a device name.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: a device to name
|
|
:return:
|
|
"""
|
|
return storage.suggest_device_name(
|
|
swap=bool(device.format.type == "swap"),
|
|
mountpoint=getattr(device.format, "mountpoint", None)
|
|
)
|
|
|
|
|
|
def revert_reformat(storage, device):
|
|
"""Revert reformat of the given device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: a device to reset
|
|
"""
|
|
# Skip if formats exists.
|
|
if device.format.exists and device.raw_device.format.exists:
|
|
log.debug("Nothing to revert for %s.", device.name)
|
|
return
|
|
|
|
# Figure out the existing device.
|
|
if not device.raw_device.format.exists:
|
|
original_device = device.raw_device
|
|
else:
|
|
original_device = device
|
|
|
|
# Reset it.
|
|
log.debug("Resetting device %s.", original_device.name)
|
|
storage.reset_device(original_device)
|
|
|
|
|
|
def resize_device(storage, device, new_size, old_size):
|
|
"""Resize the given device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: a device to resize
|
|
:param new_size: a new size
|
|
:param old_size: an old size
|
|
:return: True if the device changed its size, otherwise False
|
|
:raise: StorageError if we fail to schedule the device resize
|
|
"""
|
|
log.debug("Resizing device %s to %s.", device, new_size)
|
|
|
|
# If a LUKS device is being displayed, adjust the size
|
|
# to the appropriate size for the raw device.
|
|
use_size = new_size
|
|
use_old_size = old_size
|
|
|
|
if device.raw_device is not device:
|
|
use_size = new_size + crypto.LUKS_METADATA_SIZE
|
|
use_old_size = device.raw_device.size
|
|
|
|
# Bound size to boundaries given by the device.
|
|
use_size = device.raw_device.align_target_size(use_size)
|
|
use_size = bound_size(use_size, device.raw_device, use_old_size)
|
|
use_size = device.raw_device.align_target_size(use_size)
|
|
|
|
# And then we need to re-check that the max size is actually
|
|
# different from the current size.
|
|
|
|
if use_size == device.size or use_size == device.raw_device.size:
|
|
# The size hasn't changed.
|
|
log.debug("Canceled resize of device %s to %s.", device.raw_device.name, use_size)
|
|
return False
|
|
|
|
if new_size == device.current_size or use_size == device.current_size:
|
|
# The size has been set back to its original value.
|
|
log.debug("Removing resize of device %s.", device.raw_device.name)
|
|
|
|
actions = storage.devicetree.actions.find(
|
|
action_type="resize",
|
|
devid=device.raw_device.id
|
|
)
|
|
|
|
for action in reversed(actions):
|
|
storage.devicetree.actions.remove(action)
|
|
|
|
return bool(actions)
|
|
else:
|
|
# the size has changed
|
|
log.debug("Scheduling resize of device %s to %s.", device.raw_device.name, use_size)
|
|
|
|
try:
|
|
storage.resize_device(device.raw_device, use_size)
|
|
except (StorageError, ValueError) as e:
|
|
log.exception("Failed to schedule device resize: %s", e)
|
|
device.raw_device.size = use_old_size
|
|
raise StorageError(str(e)) from None
|
|
|
|
log.debug(
|
|
"Device %s has size: %s (target %s)",
|
|
device.raw_device.name,
|
|
device.raw_device.size, device.raw_device.target_size
|
|
)
|
|
return True
|
|
|
|
|
|
def bound_size(size, device, old_size):
|
|
"""Returns a size bounded by the maximum and minimum size for the device.
|
|
|
|
:param size: the candidate size
|
|
:type size: :class:`blivet.size.Size`
|
|
:param device: the device being displayed
|
|
:type device: :class:`blivet.devices.StorageDevice`
|
|
:param old_size: the fallback size
|
|
:type old_size: :class:`blivet.size.Size`
|
|
:returns: a size to which to set the device
|
|
:rtype: :class:`blivet.size.Size`
|
|
|
|
If size is 0, interpreted as set size to maximum possible.
|
|
If no maximum size is available, reset size to old_size, but
|
|
log a warning.
|
|
"""
|
|
max_size = device.max_size
|
|
min_size = device.min_size
|
|
if not size:
|
|
if max_size:
|
|
log.info("No size specified, using maximum size for "
|
|
"this device (%d).", max_size)
|
|
size = max_size
|
|
else:
|
|
log.warning("No size specified and no maximum size available, "
|
|
"setting size back to original size (%d).", old_size)
|
|
size = old_size
|
|
else:
|
|
if max_size:
|
|
if size > max_size:
|
|
log.warning("Size specified (%d) is greater than the maximum "
|
|
"size for this device (%d), using maximum size.",
|
|
size, max_size)
|
|
size = max_size
|
|
else:
|
|
log.warning("Unknown upper bound on size. Using requested size (%d).",
|
|
size)
|
|
|
|
if size < min_size:
|
|
log.warning("Size specified (%d) is less than the minimum size for "
|
|
"this device (%d), using minimum size.", size, min_size)
|
|
size = min_size
|
|
|
|
return size
|
|
|
|
|
|
def change_encryption(storage, device, encrypted, luks_version):
|
|
"""Change encryption of the given device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: a device to change
|
|
:param encrypted: should we encrypt the device?
|
|
:param luks_version: a version of LUKS
|
|
:return: a LUKS device or a LUKS device parent device
|
|
"""
|
|
if not encrypted:
|
|
log.info("Removing encryption from %s.", device.name)
|
|
storage.destroy_device(device)
|
|
return device.raw_device
|
|
else:
|
|
log.info("Applying encryption to %s.", device.name)
|
|
luks_version = luks_version or storage.default_luks_version
|
|
new_fmt = get_format("luks", device=device.path, luks_version=luks_version)
|
|
storage.format_device(device, new_fmt)
|
|
luks_dev = LUKSDevice("luks-" + device.name, parents=[device])
|
|
storage.create_device(luks_dev)
|
|
return luks_dev
|
|
|
|
|
|
def reformat_device(storage, device, fstype, mountpoint, label):
|
|
"""Reformat the given device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: a device to reformat
|
|
:param fstype: a file system type
|
|
:param mountpoint: a mount point
|
|
:param label: a label
|
|
:raise: StorageError if we fail to format the device
|
|
"""
|
|
log.info("Scheduling reformat of %s as %s.", device.name, fstype)
|
|
|
|
old_format = device.format
|
|
new_format = get_format(
|
|
fstype,
|
|
mountpoint=mountpoint,
|
|
label=label,
|
|
device=device.path
|
|
)
|
|
|
|
try:
|
|
storage.format_device(device, new_format)
|
|
except (StorageError, ValueError) as e:
|
|
log.exception("Failed to register device format action: %s", e)
|
|
device.format = old_format
|
|
raise StorageError(str(e)) from None
|
|
|
|
|
|
def get_device_luks_version(device):
|
|
"""Get the LUKS version of the given device.
|
|
|
|
:param device: a device
|
|
:return: a LUKS version or an empty string
|
|
"""
|
|
device = device.raw_device
|
|
|
|
if device.format.type == "luks":
|
|
return device.format.luks_version
|
|
|
|
return ""
|
|
|
|
|
|
def get_container_luks_version(container):
|
|
"""Get the LUKS version of the given container.
|
|
|
|
:param container: a container
|
|
:return: a LUKS version or an empty string
|
|
"""
|
|
for device in itertools.chain([container], container.parents):
|
|
luks_version = get_device_luks_version(device)
|
|
|
|
if luks_version:
|
|
return luks_version
|
|
|
|
return ""
|
|
|
|
|
|
def get_device_raid_level(device):
|
|
"""Get the RAID level of the given device.
|
|
|
|
:param device: a device
|
|
:return: a RAID level
|
|
"""
|
|
device = device.raw_device
|
|
|
|
if hasattr(device, "level"):
|
|
return device.level
|
|
|
|
if hasattr(device, "data_level"):
|
|
return device.data_level
|
|
|
|
return None
|
|
|
|
|
|
def get_device_raid_level_name(device):
|
|
"""Get the RAID level name of the given device."""
|
|
raid_level = get_device_raid_level(device)
|
|
return raid_level.name if raid_level else ""
|
|
|
|
|
|
def get_container_raid_level(container):
|
|
"""Get the RAID level of the given container.
|
|
|
|
:param container: a container
|
|
:return: a RAID level
|
|
"""
|
|
# Try to get a RAID level of this device.
|
|
raid_level = get_device_raid_level(container)
|
|
|
|
if raid_level:
|
|
return raid_level
|
|
|
|
device = container.raw_device
|
|
|
|
# Or get a RAID level of the LVM container.
|
|
if hasattr(device, "lvs") and len(device.parents) == 1:
|
|
return get_container_raid_level(device.parents[0])
|
|
|
|
return None
|
|
|
|
|
|
def get_container_raid_level_name(device):
|
|
"""Get the RAID level name of the given container."""
|
|
raid_level = get_container_raid_level(device)
|
|
return raid_level.name if raid_level else ""
|
|
|
|
|
|
def collect_file_system_types(device):
|
|
"""Collect supported file system types for the given device.
|
|
|
|
:param device: a device
|
|
:return: a list of file system types
|
|
"""
|
|
# Collect the supported filesystem types.
|
|
supported_types = set(get_supported_filesystems())
|
|
|
|
# Add possibly unsupported but still required file system types:
|
|
# Add the device format type.
|
|
if device.format.type:
|
|
supported_types.add(device.format.type)
|
|
|
|
# Add the original device format type.
|
|
if device.exists and device.original_format.type:
|
|
supported_types.add(device.original_format.type)
|
|
|
|
return sorted(supported_types)
|
|
|
|
|
|
def collect_device_types(device):
|
|
"""Collect supported device types for the given device.
|
|
|
|
:param device: a device
|
|
:return: a list of device types
|
|
"""
|
|
# Collect the supported device types.
|
|
supported_types = set(SUPPORTED_DEVICE_TYPES)
|
|
supported_types.add(devicefactory.DEVICE_TYPE_MD)
|
|
|
|
# Include the type of the given device.
|
|
supported_types.add(devicefactory.get_device_type(device))
|
|
|
|
# Include btrfs if it is both allowed and supported.
|
|
fmt = get_format("btrfs")
|
|
|
|
if fmt.supported \
|
|
and fmt.formattable \
|
|
and device.raw_device.format.type not in PARTITION_ONLY_FORMAT_TYPES + ("swap",):
|
|
supported_types.add(devicefactory.DEVICE_TYPE_BTRFS)
|
|
|
|
return sorted(filter(devicefactory.is_supported_device_type, supported_types))
|
|
|
|
|
|
def get_device_factory_arguments(storage, request: DeviceFactoryRequest, subset=None):
|
|
"""Get the device factory arguments for the given request.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param request: a device factory request
|
|
:param subset: a subset of argument names to return or None
|
|
:return: a dictionary of device factory arguments
|
|
"""
|
|
args = {
|
|
"device_type": request.device_type,
|
|
"device": storage.devicetree.get_device_by_name(request.device_spec),
|
|
"disks": [storage.devicetree.get_device_by_name(d) for d in request.disks],
|
|
"mountpoint": request.mount_point or None,
|
|
"fstype": request.format_type or None,
|
|
"label": request.label or None,
|
|
"luks_version": request.luks_version or storage.default_luks_version,
|
|
"device_name": request.device_name or None,
|
|
"size": Size(request.device_size) or None,
|
|
"raid_level": get_raid_level_by_name(request.device_raid_level),
|
|
"encrypted": request.device_encrypted,
|
|
"container_name": request.container_name or None,
|
|
"container_size": get_container_size_policy_by_number(request.container_size_policy),
|
|
"container_raid_level": get_raid_level_by_name(request.container_raid_level),
|
|
"container_encrypted": request.container_encrypted,
|
|
}
|
|
|
|
if subset:
|
|
args = {name: value for name, value in args.items() if name in subset}
|
|
|
|
log.debug(
|
|
"Generated factory arguments: {\n%s\n}",
|
|
",\n".join("{} = {}".format(name, repr(value)) for name, value in args.items())
|
|
)
|
|
|
|
return args
|
|
|
|
|
|
def generate_device_factory_request(storage, device) -> DeviceFactoryRequest:
|
|
"""Generate a device info for the given device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: a device
|
|
:return: a device factory request
|
|
"""
|
|
device_type = devicefactory.get_device_type(device)
|
|
|
|
if device_type is None:
|
|
raise UnsupportedDeviceError("Unsupported type of {}.".format(device.name))
|
|
|
|
# Generate the device data.
|
|
request = DeviceFactoryRequest()
|
|
request.device_spec = device.name
|
|
request.device_name = getattr(device.raw_device, "lvname", device.raw_device.name)
|
|
request.device_size = device.size.get_bytes()
|
|
request.device_type = device_type
|
|
request.reformat = not device.format.exists
|
|
request.format_type = device.format.type or ""
|
|
request.device_encrypted = isinstance(device, LUKSDevice)
|
|
request.luks_version = get_device_luks_version(device)
|
|
request.label = getattr(device.format, "label", "") or ""
|
|
request.mount_point = getattr(device.format, "mountpoint", "") or ""
|
|
request.device_raid_level = get_device_raid_level_name(device)
|
|
|
|
if hasattr(device, "req_disks") and not device.exists:
|
|
disks = device.req_disks
|
|
else:
|
|
disks = device.disks
|
|
|
|
request.disks = [d.name for d in disks]
|
|
|
|
if request.device_type not in CONTAINER_DEVICE_TYPES:
|
|
return request
|
|
|
|
# Generate the container data.
|
|
factory = devicefactory.get_device_factory(
|
|
storage,
|
|
device_type=device_type,
|
|
device=device.raw_device
|
|
)
|
|
container = factory.get_container()
|
|
|
|
if container:
|
|
set_container_data(request, container)
|
|
|
|
return request
|
|
|
|
|
|
def set_container_data(request: DeviceFactoryRequest, container):
|
|
"""Set the container data in the device factory request.
|
|
|
|
:param request: a device factory request
|
|
:param container: a container
|
|
"""
|
|
request.container_spec = container.name
|
|
request.container_name = container.name
|
|
request.container_encrypted = container.encrypted
|
|
request.container_raid_level = get_container_raid_level_name(container)
|
|
request.container_size_policy = get_container_size_policy(container)
|
|
|
|
if request.container_encrypted:
|
|
request.luks_version = get_container_luks_version(container)
|
|
|
|
|
|
def generate_container_data(storage, request: DeviceFactoryRequest):
|
|
"""Generate the container data for the device factory request.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param request: a device factory request
|
|
"""
|
|
# Reset all container data.
|
|
request.reset_container_data()
|
|
|
|
# Check the device type.
|
|
if request.device_type not in CONTAINER_DEVICE_TYPES:
|
|
return
|
|
|
|
# Find a container of the requested type.
|
|
device = storage.devicetree.resolve_device(request.device_spec)
|
|
container = get_container(storage, request.device_type, device.raw_device)
|
|
|
|
if container:
|
|
# Set the request from the found container.
|
|
set_container_data(request, container)
|
|
else:
|
|
# Set the request from a new container.
|
|
request.container_name = storage.suggest_container_name()
|
|
request.container_raid_level = get_default_container_raid_level_name(
|
|
request.device_type
|
|
)
|
|
|
|
|
|
def update_container_data(storage, request: DeviceFactoryRequest, container_name):
|
|
"""Update the container data in the device factory request.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param request: a device factory request
|
|
:param container_name: a container name to apply
|
|
"""
|
|
# Reset all container data.
|
|
request.reset_container_data()
|
|
|
|
# Check the device type.
|
|
if request.device_type not in CONTAINER_DEVICE_TYPES:
|
|
raise StorageError("Invalid device type.")
|
|
|
|
# Find the container in the device tree if any.
|
|
container = storage.devicetree.get_device_by_name(container_name)
|
|
|
|
if container:
|
|
# Set the request from the found container.
|
|
set_container_data(request, container)
|
|
|
|
# Use the container's disks.
|
|
request.disks = [d.name for d in container.disks]
|
|
else:
|
|
# Set the request from the new container.
|
|
request.container_name = container_name
|
|
request.container_raid_level = get_default_container_raid_level_name(
|
|
request.device_type
|
|
)
|
|
|
|
|
|
def generate_device_factory_permissions(storage, request: DeviceFactoryRequest):
|
|
"""Generate permissions for the requested device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param request: a device factory request
|
|
:return: device factory permissions
|
|
"""
|
|
permissions = DeviceFactoryPermissions()
|
|
device = storage.devicetree.resolve_device(request.device_spec)
|
|
container = storage.devicetree.resolve_device(request.container_name)
|
|
fmt = get_format(request.format_type)
|
|
|
|
if not device:
|
|
raise UnknownDeviceError(request.device_spec)
|
|
|
|
if device.protected:
|
|
return permissions
|
|
|
|
permissions.device_type = not device.raw_device.exists
|
|
permissions.device_raid_level = not device.raw_device.exists
|
|
permissions.mount_point = fmt.mountable
|
|
|
|
permissions.label = \
|
|
request.reformat \
|
|
and fmt.labeling()
|
|
|
|
permissions.reformat = \
|
|
device.raw_device.exists \
|
|
and not device.raw_device.format_immutable \
|
|
and is_supported_filesystem(request.format_type)
|
|
|
|
permissions.device_size = \
|
|
device.resizable or (
|
|
not device.exists
|
|
and request.device_type not in {
|
|
devicefactory.DEVICE_TYPE_BTRFS
|
|
}
|
|
)
|
|
|
|
permissions.device_name = \
|
|
not device.raw_device.exists \
|
|
and device.raw_device.type != "btrfs volume" \
|
|
and request.device_type in NAMED_DEVICE_TYPES
|
|
|
|
permissions.format_type = \
|
|
request.reformat \
|
|
and request.device_type not in {
|
|
devicefactory.DEVICE_TYPE_BTRFS
|
|
}
|
|
|
|
permissions.device_encrypted = \
|
|
request.reformat \
|
|
and not request.container_encrypted \
|
|
and request.device_type not in {
|
|
devicefactory.DEVICE_TYPE_BTRFS
|
|
} \
|
|
and not any(
|
|
a.format.type == "luks" and a.format.exists
|
|
for a in device.raw_device.ancestors if a != device
|
|
)
|
|
|
|
permissions.disks = \
|
|
not device.exists \
|
|
and not device.raw_device.exists \
|
|
and request.device_type not in CONTAINER_DEVICE_TYPES
|
|
|
|
can_change_container = \
|
|
request.device_type in CONTAINER_DEVICE_TYPES \
|
|
and not getattr(container, "exists", False)
|
|
|
|
can_replace_container = \
|
|
request.device_type in CONTAINER_DEVICE_TYPES \
|
|
and not device.raw_device.exists \
|
|
and device.raw_device != container
|
|
|
|
permissions.container_spec = can_replace_container
|
|
permissions.container_name = can_change_container
|
|
permissions.container_encrypted = can_change_container
|
|
permissions.container_raid_level = can_change_container
|
|
permissions.container_size_policy = can_change_container
|
|
|
|
return permissions
|
|
|
|
|
|
def reset_device(storage, device):
|
|
"""Reset the given device in the storage model.
|
|
|
|
FIXME: Merge with destroy_device.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: an instance of a device
|
|
:raise: StorageConfigurationError in case of failure
|
|
"""
|
|
log.debug("Reset device: %s", device.name)
|
|
|
|
try:
|
|
if device.exists:
|
|
# Revert changes done to an existing device.
|
|
storage.reset_device(device)
|
|
else:
|
|
# Destroy a non-existing device.
|
|
_destroy_device(storage, device)
|
|
except (StorageError, ValueError) as e:
|
|
log.exception("Failed to reset a device: %s", e)
|
|
raise StorageConfigurationError(str(e)) from None
|
|
|
|
|
|
def destroy_device(storage, device):
|
|
"""Destroy the given device in the storage model.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: an instance of a device
|
|
:raise: StorageConfigurationError in case of failure
|
|
"""
|
|
log.debug("Destroy device: %s", device.name)
|
|
|
|
try:
|
|
_destroy_device(storage, device)
|
|
except (StorageError, ValueError) as e:
|
|
log.exception("Failed to destroy a device: %s", e)
|
|
raise StorageConfigurationError(str(e)) from None
|
|
|
|
|
|
def _destroy_device(storage, device):
|
|
"""Destroy the given device in the storage model.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device: an instance of a device
|
|
"""
|
|
# Remove the device.
|
|
if device.is_disk:
|
|
if device.partitioned and not device.format.supported:
|
|
storage.recursive_remove(device)
|
|
storage.initialize_disk(device)
|
|
elif device.direct and not device.isleaf:
|
|
# We shouldn't call this method for with non-leaf devices
|
|
# except for those which are also directly accessible like
|
|
# lvm snapshot origins and btrfs subvolumes that contain
|
|
# other subvolumes.
|
|
storage.recursive_remove(device)
|
|
else:
|
|
storage.destroy_device(device)
|
|
|
|
# Remove empty extended partitions.
|
|
if getattr(device, "is_logical", False):
|
|
storage.remove_empty_extended_partitions()
|
|
|
|
# If we've just removed the last partition and the disk label
|
|
# is preexisting, reinitialize the disk.
|
|
if device.type == "partition" and device.exists and device.disk.format.exists:
|
|
config = DiskInitializationConfig()
|
|
config.initialize_labels = True
|
|
|
|
if config.can_initialize(storage, device.disk):
|
|
storage.initialize_disk(device.disk)
|
|
|
|
# Get the device container.
|
|
if hasattr(device, "vg"):
|
|
container = device.vg
|
|
device_type = devicefactory.get_device_type(device)
|
|
elif hasattr(device, "volume"):
|
|
container = device.volume
|
|
device_type = devicefactory.DEVICE_TYPE_BTRFS
|
|
else:
|
|
container = None
|
|
device_type = None
|
|
|
|
# Adjust container to size of remaining devices, if auto-sized.
|
|
if (container and not container.exists and container.children and
|
|
container.size_policy == devicefactory.SIZE_POLICY_AUTO):
|
|
# Create the device factory.
|
|
factory = devicefactory.get_device_factory(
|
|
storage,
|
|
device_type=device_type,
|
|
size=Size(0),
|
|
disks=container.disks,
|
|
container_name=container.name,
|
|
container_encrypted=container.encrypted,
|
|
container_raid_level=get_container_raid_level(container),
|
|
container_size=container.size_policy,
|
|
)
|
|
|
|
# Configure the factory's devices.
|
|
factory.configure()
|
|
|
|
# Finally, remove empty parents of the device, except for btrfs subvolumes.
|
|
for parent in device.parents:
|
|
if not parent.children and not parent.is_disk and not parent.type == "btrfs subvolume":
|
|
destroy_device(storage, parent)
|
|
|
|
|
|
def rename_container(storage, container, name):
|
|
"""Rename the given container.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param container: an instance of a container
|
|
:param name: a new name of the container
|
|
"""
|
|
log.debug("Rename container %s to %s.", container.name, name)
|
|
|
|
try:
|
|
container.name = name
|
|
except ValueError as e:
|
|
log.exception("Failed to rename container: %s", str(e))
|
|
raise StorageError(str(e)) from None
|
|
|
|
# Fix the btrfs label.
|
|
if container.format.type == "btrfs":
|
|
container.format.label = name
|
|
|
|
|
|
def get_container(storage, device_type, device=None):
|
|
"""Get a container of the given type.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device_type: a device type
|
|
:param device: a defined factory device or None
|
|
:return: a container device
|
|
"""
|
|
if device_type not in CONTAINER_DEVICE_TYPES:
|
|
raise StorageError("Invalid device type {}".format(device_type))
|
|
|
|
if device and devicefactory.get_device_type(device) != device_type:
|
|
device = None
|
|
|
|
factory = devicefactory.get_device_factory(
|
|
storage,
|
|
device_type=device_type,
|
|
size=Size(0),
|
|
)
|
|
|
|
return factory.get_container(device=device)
|
|
|
|
|
|
def get_container_size_policy(container):
|
|
"""Get a container size policy."""
|
|
size = getattr(container, "size_policy", container.size)
|
|
|
|
if size is None:
|
|
return devicefactory.SIZE_POLICY_AUTO
|
|
|
|
if size > 0:
|
|
return Size(size).get_bytes()
|
|
|
|
return size
|
|
|
|
|
|
def get_container_size_policy_by_number(number):
|
|
"""Get a container size policy by the given number."""
|
|
if number <= 0:
|
|
return number
|
|
|
|
return Size(number)
|
|
|
|
|
|
def get_default_container_raid_level_name(device_type):
|
|
"""Get the default RAID level for this device type's container type.
|
|
|
|
:param int device_type: a device_type
|
|
:return str: a name of the default RAID level or an empty string
|
|
"""
|
|
if device_type == devicefactory.DEVICE_TYPE_BTRFS:
|
|
return "single"
|
|
|
|
return ""
|
|
|
|
|
|
def collect_containers(storage, device_type):
|
|
"""Collect containers of the given type.
|
|
|
|
:param storage: an instance of Blivet
|
|
:param device_type: a device type
|
|
:return: a list of container devices
|
|
"""
|
|
if device_type == devicefactory.DEVICE_TYPE_BTRFS:
|
|
return storage.btrfs_volumes
|
|
else:
|
|
return storage.vgs
|
|
|
|
|
|
def get_supported_raid_levels(device_type):
|
|
"""Get RAID levels for the specified device type.
|
|
|
|
:param device_type: a type of the device
|
|
:return: a list of RAID levels
|
|
"""
|
|
return devicefactory.get_supported_raid_levels(device_type)
|
|
|
|
|
|
def check_device_completeness(device):
|
|
"""Check that the specified device is complete.
|
|
|
|
:param device: a device to check
|
|
:return: an error message or None
|
|
"""
|
|
if getattr(device, "complete", True):
|
|
return None
|
|
|
|
if isinstance(device, MDRaidArrayDevice):
|
|
total = device.member_devices
|
|
missing = total - len(device.parents)
|
|
return _("This Software RAID array is missing %(missing)d of %(total)d "
|
|
"member partitions. You can remove it or select a different "
|
|
"device.") % {"missing": missing, "total": total}
|
|
|
|
if isinstance(device, LVMVolumeGroupDevice):
|
|
total = device.pv_count
|
|
missing = total - len(device.parents)
|
|
return _("This LVM Volume Group is missing %(missingPVs)d of %(totalPVs)d "
|
|
"physical volumes. You can remove it or select a different "
|
|
"device.") % {"missingPVs": missing, "totalPVs": total}
|
|
|
|
return _("This %(type)s device is missing member devices. You can remove "
|
|
"it or select a different device.") % {"type": device.type}
|