diff --git a/pyanaconda/core/kickstart/commands.py b/pyanaconda/core/kickstart/commands.py index ed3e3401567..c1cacd03cff 100644 --- a/pyanaconda/core/kickstart/commands.py +++ b/pyanaconda/core/kickstart/commands.py @@ -23,7 +23,7 @@ # Supported kickstart commands. from pykickstart.commands.authselect import F28_Authselect as Authselect -from pykickstart.commands.autopart import F29_AutoPart as AutoPart +from pykickstart.commands.autopart import F38_AutoPart as AutoPart from pykickstart.commands.autostep import F34_AutoStep as AutoStep from pykickstart.commands.bootloader import F34_Bootloader as Bootloader from pykickstart.commands.btrfs import F23_BTRFS as BTRFS diff --git a/pyanaconda/modules/common/structures/partitioning.py b/pyanaconda/modules/common/structures/partitioning.py index 2e22e6a58da..20adc6f5c1f 100644 --- a/pyanaconda/modules/common/structures/partitioning.py +++ b/pyanaconda/modules/common/structures/partitioning.py @@ -32,6 +32,7 @@ def __init__(self): self._partitioning_scheme = conf.storage.default_scheme self._file_system_type = "" self._excluded_mount_points = [] + self._hibernation = False self._encrypted = False self._passphrase = "" @@ -91,6 +92,21 @@ def excluded_mount_points(self) -> List[Str]: """ return self._excluded_mount_points + @property + def hibernation(self) -> Bool: + """Should the partitioning include hibernation swap? + + If True a swap partition large enough for hibernation will be created + even if swap was not configured in the Anaconda configuration file. + + :return: True or False + """ + return self._hibernation + + @hibernation.setter + def hibernation(self, value: Bool): + self._hibernation = value + @excluded_mount_points.setter def excluded_mount_points(self, mount_points: List[Str]): self._excluded_mount_points = mount_points diff --git a/pyanaconda/modules/storage/partitioning/automatic/automatic_module.py b/pyanaconda/modules/storage/partitioning/automatic/automatic_module.py index e1d68869021..e7ac5bff47a 100644 --- a/pyanaconda/modules/storage/partitioning/automatic/automatic_module.py +++ b/pyanaconda/modules/storage/partitioning/automatic/automatic_module.py @@ -76,6 +76,8 @@ def process_kickstart(self, data): if data.autopart.noswap: request.excluded_mount_points.append("swap") + request.hibernation = data.autopart.hibernation + if data.autopart.encrypted: request.encrypted = True request.passphrase = data.autopart.passphrase @@ -104,6 +106,8 @@ def setup_kickstart(self, data): data.autopart.noboot = "/boot" in self.request.excluded_mount_points data.autopart.noswap = "swap" in self.request.excluded_mount_points + data.autopart.hibernation = self.request.hibernation + data.autopart.encrypted = self.request.encrypted # Don't generate sensitive information. diff --git a/pyanaconda/modules/storage/partitioning/automatic/automatic_partitioning.py b/pyanaconda/modules/storage/partitioning/automatic/automatic_partitioning.py index bdf27bc8dbf..3018f3ddb20 100644 --- a/pyanaconda/modules/storage/partitioning/automatic/automatic_partitioning.py +++ b/pyanaconda/modules/storage/partitioning/automatic/automatic_partitioning.py @@ -24,7 +24,8 @@ NonInteractivePartitioningTask from pyanaconda.modules.storage.partitioning.automatic.utils import get_candidate_disks, \ schedule_implicit_partitions, schedule_volumes, schedule_partitions, get_pbkdf_args, \ - get_default_partitioning, get_disks_for_implicit_partitions + get_default_partitioning, get_part_spec, get_disks_for_implicit_partitions +from pyanaconda.modules.storage.partitioning.specification import PartSpec from pyanaconda.core.storage import suggest_swap_size log = get_module_logger(__name__) @@ -85,7 +86,7 @@ def _configure_partitioning(self, storage): luks_data.pbkdf_args = pbkdf_args # Get the autopart requests. - requests = self._get_partitioning(storage, scheme, self._request.excluded_mount_points) + requests = self._get_partitioning(storage, scheme, self._request) # Do the autopart. self._do_autopart(storage, scheme, requests, encrypted, luks_format_args) @@ -122,37 +123,51 @@ def _get_luks_format_args(storage, request): } @staticmethod - def _get_partitioning(storage, scheme, excluded_mount_points=()): + def _get_partitioning(storage, scheme, request: PartitioningRequest): """Get the partitioning requests for autopart. :param storage: blivet.Blivet instance :param scheme: a type of the partitioning scheme - :param excluded_mount_points: a list of mount points to exclude + :param request: partitioning parameters :return: a list of full partitioning specs """ - requests = [] + specs = [] + swap = None - for request in get_default_partitioning(): + # Create partitioning specs based on the default configuration. + for spec in get_default_partitioning(): # Skip mount points excluded from the chosen scheme. - if request.schemes and scheme not in request.schemes: + if spec.schemes and scheme not in spec.schemes: continue # Skip excluded mount points. - if (request.mountpoint or request.fstype) in excluded_mount_points: + if (spec.mountpoint or spec.fstype) in request.excluded_mount_points: continue + # Detect swap. + if spec.fstype == "swap": + swap = spec + + specs.append(spec) + + # Add a swap if hibernation was requested in kickstart. + if request.hibernation and swap is None: + swap = get_part_spec({"name": "swap"}) + specs.append(swap) + + # Configure specs. + for spec in specs: # Set the default filesystem type. - if request.fstype is None: - request.fstype = storage.get_fstype(request.mountpoint) + if spec.fstype is None: + spec.fstype = storage.get_fstype(spec.mountpoint) # Update the size of swap. - if request.fstype == "swap": + if spec.fstype == "swap": disk_space = storage.get_disk_free_space() - request.size = suggest_swap_size(disk_space=disk_space) - - requests.append(request) + swap.size = suggest_swap_size(hibernation=request.hibernation, + disk_space=disk_space) - return requests + return specs @staticmethod def _do_autopart(storage, scheme, requests, encrypted=False, luks_fmt_args=None): diff --git a/pyanaconda/modules/storage/partitioning/automatic/utils.py b/pyanaconda/modules/storage/partitioning/automatic/utils.py index 1f62472e659..68225d6bc05 100644 --- a/pyanaconda/modules/storage/partitioning/automatic/utils.py +++ b/pyanaconda/modules/storage/partitioning/automatic/utils.py @@ -266,32 +266,40 @@ def get_default_partitioning(): # Get the product-specific partitioning. for attrs in conf.storage.default_partitioning: - 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, - ) - - partitioning.append(spec) + 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. diff --git a/tests/unit_tests/pyanaconda_tests/modules/storage/partitioning/test_module_part_automatic.py b/tests/unit_tests/pyanaconda_tests/modules/storage/partitioning/test_module_part_automatic.py index ce62765e7cb..384dd8e39ce 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/storage/partitioning/test_module_part_automatic.py +++ b/tests/unit_tests/pyanaconda_tests/modules/storage/partitioning/test_module_part_automatic.py @@ -20,7 +20,7 @@ import unittest import pytest -from unittest.mock import Mock, patch +from unittest.mock import Mock, patch, ANY from blivet.formats.luks import LUKS2PBKDFArgs from blivet.size import Size @@ -91,6 +91,7 @@ def test_request_property(self): 'partitioning-scheme': get_variant(Int, AUTOPART_TYPE_LVM_THINP), 'file-system-type': get_variant(Str, 'ext4'), 'excluded-mount-points': get_variant(List[Str], ['/home', '/boot', 'swap']), + 'hibernation': get_variant(Bool, False), 'encrypted': get_variant(Bool, True), 'passphrase': get_variant(Str, '123456'), 'cipher': get_variant(Str, 'aes-xts-plain64'), @@ -262,18 +263,22 @@ def test_get_partitioning(self, platform, suggest_swap_size): suggest_swap_size.return_value = Size("1024MiB") # Collect the requests. + partitioning_request = PartitioningRequest() + partitioning_request._excluded_mount_points = ["/home", "/boot", "swap"] requests = AutomaticPartitioningTask._get_partitioning( storage=storage, scheme=AUTOPART_TYPE_LVM, - excluded_mount_points=["/home", "/boot", "swap"] + request=partitioning_request ) assert ["/"] == [spec.mountpoint for spec in requests] + partitioning_request = PartitioningRequest() + partitioning_request._excluded_mount_points = [] requests = AutomaticPartitioningTask._get_partitioning( storage=storage, scheme=AUTOPART_TYPE_LVM, - excluded_mount_points=[] + request=partitioning_request ) assert ["/boot", "/", "/home"] == \ @@ -283,6 +288,88 @@ def test_get_partitioning(self, platform, suggest_swap_size): assert [Size("1GiB"), Size("1GiB"), Size("500MiB")] == \ [spec.size for spec in requests] + @patch('pyanaconda.modules.storage.partitioning.automatic.utils.platform') + @patch('pyanaconda.modules.storage.partitioning.automatic.utils.conf') + @patch('pyanaconda.modules.storage.partitioning.automatic.automatic_partitioning.suggest_swap_size') + def test_get_partitioning_hibernation(self, suggest_swap_size, mocked_config, platform): + """Test the creation of swap with PartitioningRequest.hibernation.""" + swap_size = Size("1GiB") + suggest_swap_size.return_value = swap_size + + storage = create_storage() + platform.partitions = [ + PartSpec(mountpoint="/boot", size=Size("1GiB")) + ] + + # Test Case: No swap, hibernation + mocked_config.storage.default_partitioning = [ + { + 'name': '/', + 'size': Size("50 GiB"), + }, + ] + partitioning_request = PartitioningRequest() + partitioning_request.hibernation = True + + requests = AutomaticPartitioningTask._get_partitioning( + storage=storage, + scheme=AUTOPART_TYPE_LVM, + request=partitioning_request + ) + + assert any(spec for spec in requests if spec.fstype == "swap") + assert list(spec.size for spec in requests if spec.fstype == "swap") == [swap_size] + suggest_swap_size.assert_called_with(hibernation=True, disk_space=ANY) + + # Test Case: No swap, no hibernation + partitioning_request = PartitioningRequest() + partitioning_request.hibernation = False + + requests = AutomaticPartitioningTask._get_partitioning( + storage=storage, + scheme=AUTOPART_TYPE_LVM, + request=partitioning_request + ) + + assert not any(spec for spec in requests if spec.fstype == "swap") + + # Test Case: Swap, hibernation + mocked_config.storage.default_partitioning = [ + { + 'name': '/', + 'size': Size("50 GiB"), + }, + { + 'name': 'swap', + }, + ] + partitioning_request = PartitioningRequest() + partitioning_request.hibernation = True + + requests = AutomaticPartitioningTask._get_partitioning( + storage=storage, + scheme=AUTOPART_TYPE_LVM, + request=partitioning_request + ) + + assert any(spec for spec in requests if spec.fstype == "swap") + assert list(spec.size for spec in requests if spec.fstype == "swap") == [swap_size] + suggest_swap_size.assert_called_with(hibernation=True, disk_space=ANY) + + # Test Case: Swap, no hibernation + partitioning_request = PartitioningRequest() + partitioning_request.hibernation = False + + requests = AutomaticPartitioningTask._get_partitioning( + storage=storage, + scheme=AUTOPART_TYPE_LVM, + request=partitioning_request + ) + + assert any(spec for spec in requests if spec.fstype == "swap") + assert list(spec.size for spec in requests if spec.fstype == "swap") == [swap_size] + suggest_swap_size.assert_called_with(hibernation=False, disk_space=ANY) + @patch('pyanaconda.modules.storage.partitioning.automatic.utils.conf') @patch('pyanaconda.modules.storage.partitioning.automatic.utils.platform') def test_get_partitioning_btrfs_only(self, platform, mocked_conf): @@ -301,17 +388,21 @@ def test_get_partitioning_btrfs_only(self, platform, mocked_conf): ] # Collect the requests for the Btrfs scheme. + partitioning_request = PartitioningRequest() requests = AutomaticPartitioningTask._get_partitioning( storage=storage, scheme=AUTOPART_TYPE_BTRFS, + request=partitioning_request, ) assert ["/", "/var"] == [spec.mountpoint for spec in requests] # Collect the requests for the LVM scheme. + partitioning_request = PartitioningRequest() requests = AutomaticPartitioningTask._get_partitioning( storage=storage, scheme=AUTOPART_TYPE_LVM, + request=partitioning_request, ) assert ["/"] == [spec.mountpoint for spec in requests] diff --git a/tests/unit_tests/pyanaconda_tests/modules/storage/test_module_storage.py b/tests/unit_tests/pyanaconda_tests/modules/storage/test_module_storage.py index 0c8c6fcb268..56bf1fc3d46 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/storage/test_module_storage.py +++ b/tests/unit_tests/pyanaconda_tests/modules/storage/test_module_storage.py @@ -881,6 +881,19 @@ def test_autopart_backuppassphrase_kickstart(self, publisher): self._test_kickstart(ks_in, ks_out) self._check_dbus_partitioning(publisher, PartitioningMethod.AUTOMATIC) + @patch_dbus_publish_object + def test_autopart_hibernation_kickstart(self, publisher): + """Test the autopart command with the hibernation option.""" + ks_in = """ + autopart --hibernation + """ + ks_out = """ + autopart --hibernation + """ + self._apply_partitioning_when_created() + self._test_kickstart(ks_in, ks_out) + self._check_dbus_partitioning(publisher, PartitioningMethod.AUTOMATIC) + @patch_dbus_publish_object def test_mount_kickstart(self, publisher): """Test the mount command."""