sanityCheck code is installer-specific so it should live in the anaconda codebase. And it should check minimal RAM requirements properly based on the installation mode.
Vratislav Podzimek (2): Move sanityCheck code to anaconda's codebase Make storage sanity check aware of base RAM requirements (#1123466)
pyanaconda/isys/__init__.py | 1 + pyanaconda/kickstart.py | 7 +- pyanaconda/storage_utils.py | 192 +++++++++++++++++++++++++++++++++++- pyanaconda/ui/gui/spokes/custom.py | 13 +-- pyanaconda/ui/gui/spokes/storage.py | 3 +- pyanaconda/ui/helpers.py | 9 +- pyanaconda/ui/tui/spokes/storage.py | 6 +- 7 files changed, 213 insertions(+), 18 deletions(-)
This is the right place it belongs to.
Signed-off-by: Vratislav Podzimek vpodzime@redhat.com --- pyanaconda/kickstart.py | 7 +- pyanaconda/storage_utils.py | 189 +++++++++++++++++++++++++++++++++++- pyanaconda/ui/gui/spokes/custom.py | 8 +- pyanaconda/ui/helpers.py | 5 +- pyanaconda/ui/tui/spokes/storage.py | 6 +- 5 files changed, 201 insertions(+), 14 deletions(-)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 09f2a88..fcbfa72 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -26,6 +26,7 @@ from blivet.devicelibs import swap from blivet.formats import getFormat from blivet.partitioning import doPartitioning from blivet.partitioning import growLVM +from blivet.errors import PartitioningError from blivet.size import Size from blivet import udev from blivet.platform import platform @@ -263,7 +264,7 @@ class AutoPart(commands.autopart.F21_AutoPart):
def execute(self, storage, ksdata, instClass): from blivet.partitioning import doAutoPartition - from blivet.partitioning import sanityCheck + from pyanaconda.storage_utils import sanity_check
if not self.autopart: return @@ -291,7 +292,9 @@ class AutoPart(commands.autopart.F21_AutoPart): storage.autoPartType = self.type
doAutoPartition(storage, ksdata) - sanityCheck(storage) + errors = sanity_check(storage) + if errors: + raise PartitioningError("autopart failed:\n" + "\n".join(error.message for error in errors))
class Bootloader(commands.bootloader.F21_Bootloader): def __init__(self, *args, **kwargs): diff --git a/pyanaconda/storage_utils.py b/pyanaconda/storage_utils.py index e6e3f70..f49cec0 100644 --- a/pyanaconda/storage_utils.py +++ b/pyanaconda/storage_utils.py @@ -25,7 +25,10 @@ import locale
from contextlib import contextmanager
+from blivet import arch +from blivet import util from blivet.size import Size +from blivet.platform import platform as _platform from blivet.devicefactory import DEVICE_TYPE_LVM from blivet.devicefactory import DEVICE_TYPE_LVM_THINP from blivet.devicefactory import DEVICE_TYPE_BTRFS @@ -33,7 +36,10 @@ from blivet.devicefactory import DEVICE_TYPE_MD from blivet.devicefactory import DEVICE_TYPE_PARTITION from blivet.devicefactory import DEVICE_TYPE_DISK
-from pyanaconda.i18n import N_ +from pyanaconda.i18n import _, N_ +from pyanaconda import isys +from pyanaconda.constants import productName + from pykickstart.constants import AUTOPART_TYPE_PLAIN, AUTOPART_TYPE_BTRFS from pykickstart.constants import AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP
@@ -128,3 +134,184 @@ def ui_storage_logger(): storage_log.addFilter(storage_filter) yield storage_log.removeFilter(storage_filter) + +class SanityException(Exception): + pass + +class SanityError(SanityException): + pass + +class SanityWarning(SanityException): + pass + +class LUKSDeviceWithoutKeyError(SanityError): + pass + +def sanity_check(storage): + """ + Run a series of tests to verify the storage configuration. + + This function is called at the end of partitioning so that + we can make sure you don't have anything silly (like no /, + a really small /, etc). + + :rtype: a list of SanityExceptions + :return: a list of accumulated errors and warnings + + """ + + exns = [] + + checkSizes = [('/usr', 250), ('/tmp', 50), ('/var', 384), + ('/home', 100), ('/boot', 75)] + mustbeonlinuxfs = ['/', '/var', '/tmp', '/usr', '/home', '/usr/share', '/usr/lib'] + mustbeonroot = ['/bin','/dev','/sbin','/etc','/lib','/root', '/mnt', 'lost+found', '/proc'] + + filesystems = storage.mountpoints + root = storage.fsset.rootDevice + swaps = storage.fsset.swapDevices + + if root: + if root.size < 250: + exns.append( + SanityWarning(_("Your root partition is less than 250 " + "megabytes which is usually too small to " + "install %s.") % (productName,))) + else: + exns.append( + SanityError(_("You have not defined a root partition (/), " + "which is required for installation of %s " + "to continue.") % (productName,))) + + # Prevent users from installing on s390x with (a) no /boot volume, (b) the + # root volume on LVM, and (c) the root volume not restricted to a single + # PV + # NOTE: There is not really a way for users to create a / volume + # restricted to a single PV. The backend support is there, but there are + # no UI hook-ups to drive that functionality, but I do not personally + # care. --dcantrell + if arch.isS390() and '/boot' not in storage.mountpoints and root: + if root.type == 'lvmlv' and not root.singlePV: + exns.append( + SanityError(_("This platform requires /boot on a dedicated " + "partition or logical volume. If you do not " + "want a /boot volume, you must place / on a " + "dedicated non-LVM partition."))) + + # FIXME: put a check here for enough space on the filesystems. maybe? + + for (mount, size) in checkSizes: + if mount in filesystems and filesystems[mount].size < size: + exns.append( + SanityWarning(_("Your %(mount)s partition is less than " + "%(size)s megabytes which is lower than " + "recommended for a normal %(productName)s " + "install.") + % {'mount': mount, 'size': size, + 'productName': productName})) + + for (mount, device) in filesystems.items(): + problem = filesystems[mount].checkSize() + if problem < 0: + exns.append( + SanityError(_("Your %(mount)s partition is too small for %(format)s formatting " + "(allowable size is %(minSize)s to %(maxSize)s)") + % {"mount": mount, "format": device.format.name, + "minSize": device.minSize, "maxSize": device.maxSize})) + elif problem > 0: + exns.append( + SanityError(_("Your %(mount)s partition is too large for %(format)s formatting " + "(allowable size is %(minSize)s to %(maxSize)s)") + % {"mount":mount, "format": device.format.name, + "minSize": device.minSize, "maxSize": device.maxSize})) + + if storage.bootloader and not storage.bootloader.skip_bootloader: + stage1 = storage.bootloader.stage1_device + if not stage1: + exns.append( + SanityError(_("No valid bootloader target device found. " + "See below for details."))) + pe = _platform.stage1MissingError + if pe: + exns.append(SanityError(_(pe))) + else: + storage.bootloader.is_valid_stage1_device(stage1) + exns.extend(SanityError(msg) for msg in storage.bootloader.errors) + exns.extend(SanityWarning(msg) for msg in storage.bootloader.warnings) + + stage2 = storage.bootloader.stage2_device + if stage1 and not stage2: + exns.append(SanityError(_("You have not created a bootable partition."))) + else: + storage.bootloader.is_valid_stage2_device(stage2) + exns.extend(SanityError(msg) for msg in storage.bootloader.errors) + exns.extend(SanityWarning(msg) for msg in storage.bootloader.warnings) + if not storage.bootloader.check(): + exns.extend(SanityError(msg) for msg in storage.bootloader.errors) + + # + # check that GPT boot disk on BIOS system has a BIOS boot partition + # + if _platform.weight(fstype="biosboot") and \ + stage1 and stage1.isDisk and \ + getattr(stage1.format, "labelType", None) == "gpt": + missing = True + for part in [p for p in storage.partitions if p.disk == stage1]: + if part.format.type == "biosboot": + missing = False + break + + if missing: + exns.append( + SanityError(_("Your BIOS-based system needs a special " + "partition to boot from a GPT disk label. " + "To continue, please create a 1MiB " + "'biosboot' type partition."))) + + if not swaps: + installed = util.total_memory() + required = Size("%s KiB" % isys.EARLY_SWAP_RAM) + + if installed < required: + exns.append( + SanityError(_("You have not specified a swap partition. " + "%(requiredMem)s of memory is required to continue installation " + "without a swap partition, but you only have %(installedMem)s.") + % {"requiredMem": required, + "installedMem": installed})) + else: + exns.append( + SanityWarning(_("You have not specified a swap partition. " + "Although not strictly required in all cases, " + "it will significantly improve performance " + "for most installations."))) + no_uuid = [s for s in swaps if s.format.exists and not s.format.uuid] + if no_uuid: + exns.append( + SanityWarning(_("At least one of your swap devices does not have " + "a UUID, which is common in swap space created " + "using older versions of mkswap. These devices " + "will be referred to by device path in " + "/etc/fstab, which is not ideal since device " + "paths can change under a variety of " + "circumstances. "))) + + for (mountpoint, dev) in filesystems.items(): + if mountpoint in mustbeonroot: + exns.append( + SanityError(_("This mount point is invalid. The %s directory must " + "be on the / file system.") % mountpoint)) + + if mountpoint in mustbeonlinuxfs and (not dev.format.mountable or not dev.format.linuxNative): + exns.append( + SanityError(_("The mount point %s must be on a linux file system.") % mountpoint)) + + if storage.rootDevice and storage.rootDevice.format.exists: + e = storage.mustFormat(storage.rootDevice) + if e: + exns.append(SanityError(e)) + + exns += storage._verifyLUKSDevicesHaveKey() + + return exns + diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py index 45ab6de..e0a78b8 100644 --- a/pyanaconda/ui/gui/spokes/custom.py +++ b/pyanaconda/ui/gui/spokes/custom.py @@ -56,8 +56,6 @@ from blivet.partitioning import doAutoPartition from blivet.errors import StorageError from blivet.errors import NoDisksError from blivet.errors import NotEnoughFreeSpaceError -from blivet.errors import SanityError -from blivet.errors import SanityWarning from blivet.errors import LUKSDeviceWithoutKeyError from blivet.devicelibs import raid from blivet.devices import LUKSDevice @@ -66,6 +64,8 @@ from pyanaconda.storage_utils import ui_storage_logger, device_type_from_autopar from pyanaconda.storage_utils import DEVICE_TEXT_PARTITION, DEVICE_TEXT_MAP, DEVICE_TEXT_MD from pyanaconda.storage_utils import PARTITION_ONLY_FORMAT_TYPES, MOUNTPOINT_DESCRIPTIONS from pyanaconda.storage_utils import NAMED_DEVICE_TYPES, CONTAINER_DEVICE_TYPES +from pyanaconda.storage_utils import SanityError, SanityWarning +from pyanaconda import storage_utils
from pyanaconda.ui.communication import hubQ from pyanaconda.ui.gui.spokes import NormalSpoke @@ -2167,7 +2167,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): def _do_autopart(self): """Helper function for on_create_clicked. Assumes a non-final context in which at least some errors - discovered by sanityCheck are not considered fatal because they + discovered by sanity_check are not considered fatal because they will be dealt with later.
Note: There are never any non-existent devices around when this runs. @@ -2209,7 +2209,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._storage_playground.doAutoPart = False log.debug("finished automatic partitioning")
- exns = self._storage_playground.sanityCheck() + exns = storage_utils.sanity_check(self._storage_playground) errors = [exn for exn in exns if isinstance(exn, SanityError) and not isinstance(exn, LUKSDeviceWithoutKeyError)] warnings = [exn for exn in exns if isinstance(exn, SanityWarning)] for error in errors: diff --git a/pyanaconda/ui/helpers.py b/pyanaconda/ui/helpers.py index 15597e8..e688a99 100644 --- a/pyanaconda/ui/helpers.py +++ b/pyanaconda/ui/helpers.py @@ -84,14 +84,13 @@ class StorageChecker(object): target=self.checkStorage))
def checkStorage(self): - from blivet.errors import SanityError - from blivet.errors import SanityWarning + from pyanaconda.storage_utils import sanity_check, SanityError, SanityWarning
threadMgr.wait(constants.THREAD_EXECUTE_STORAGE)
hubQ.send_not_ready(self._mainSpokeClass) hubQ.send_message(self._mainSpokeClass, _("Checking storage configuration...")) - exns = self.storage.sanityCheck() + exns = sanity_check(self.storage) errors = [exn.message for exn in exns if isinstance(exn, SanityError)] warnings = [exn.message for exn in exns if isinstance(exn, SanityWarning)] (StorageChecker.errors, StorageChecker.warnings) = (errors, warnings) diff --git a/pyanaconda/ui/tui/spokes/storage.py b/pyanaconda/ui/tui/spokes/storage.py index a0e05d9..e86f0bb 100644 --- a/pyanaconda/ui/tui/spokes/storage.py +++ b/pyanaconda/ui/tui/spokes/storage.py @@ -27,13 +27,11 @@ from pyanaconda.ui.categories.system import SystemCategory from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.simpleline import TextWidget, CheckboxWidget from pyanaconda.ui.tui.tuiobject import YesNoDialog -from pyanaconda.storage_utils import AUTOPART_CHOICES +from pyanaconda.storage_utils import AUTOPART_CHOICES, sanity_check, SanityError, SanityWarning
from blivet import storageInitialize, arch from blivet.size import Size from blivet.errors import StorageError, DasdFormatError -from blivet.errors import SanityError -from blivet.errors import SanityWarning from blivet.devices import DASDDevice, FcoeDiskDevice, iScsiDiskDevice, MultipathDevice, ZFCPDiskDevice from blivet.devicelibs.dasd import format_dasd, make_unformatted_dasd_list from pyanaconda.flags import flags @@ -393,7 +391,7 @@ class StorageSpoke(NormalTUISpoke): self._ready = True else: print(_("Checking storage configuration...")) - exns = self.storage.sanityCheck() + exns = sanity_check(self.storage) errors = [exn.message for exn in exns if isinstance(exn, SanityError)] warnings = [exn.message for exn in exns if isinstance(exn, SanityWarning)] (self.errors, self.warnings) = (errors, warnings)
----- Original Message -----
From: "Vratislav Podzimek" vpodzime@redhat.com To: anaconda-patches@lists.fedorahosted.org Sent: Tuesday, July 29, 2014 2:04:00 PM Subject: [PATCH 1/2] Move sanityCheck code to anaconda's codebase
This is the right place it belongs to.
Signed-off-by: Vratislav Podzimek vpodzime@redhat.com
pyanaconda/kickstart.py | 7 +- pyanaconda/storage_utils.py | 189 +++++++++++++++++++++++++++++++++++- pyanaconda/ui/gui/spokes/custom.py | 8 +- pyanaconda/ui/helpers.py | 5 +- pyanaconda/ui/tui/spokes/storage.py | 6 +- 5 files changed, 201 insertions(+), 14 deletions(-)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 09f2a88..fcbfa72 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -26,6 +26,7 @@ from blivet.devicelibs import swap from blivet.formats import getFormat from blivet.partitioning import doPartitioning from blivet.partitioning import growLVM +from blivet.errors import PartitioningError from blivet.size import Size from blivet import udev from blivet.platform import platform @@ -263,7 +264,7 @@ class AutoPart(commands.autopart.F21_AutoPart):
def execute(self, storage, ksdata, instClass): from blivet.partitioning import doAutoPartition
from blivet.partitioning import sanityCheck
from pyanaconda.storage_utils import sanity_check if not self.autopart: return@@ -291,7 +292,9 @@ class AutoPart(commands.autopart.F21_AutoPart): storage.autoPartType = self.type
doAutoPartition(storage, ksdata)
sanityCheck(storage)
errors = sanity_check(storage)if errors:raise PartitioningError("autopart failed:\n" +"\n".join(error.message for error in errors))
class Bootloader(commands.bootloader.F21_Bootloader): def __init__(self, *args, **kwargs): diff --git a/pyanaconda/storage_utils.py b/pyanaconda/storage_utils.py index e6e3f70..f49cec0 100644 --- a/pyanaconda/storage_utils.py +++ b/pyanaconda/storage_utils.py @@ -25,7 +25,10 @@ import locale
from contextlib import contextmanager
+from blivet import arch +from blivet import util from blivet.size import Size +from blivet.platform import platform as _platform from blivet.devicefactory import DEVICE_TYPE_LVM from blivet.devicefactory import DEVICE_TYPE_LVM_THINP from blivet.devicefactory import DEVICE_TYPE_BTRFS @@ -33,7 +36,10 @@ from blivet.devicefactory import DEVICE_TYPE_MD from blivet.devicefactory import DEVICE_TYPE_PARTITION from blivet.devicefactory import DEVICE_TYPE_DISK
-from pyanaconda.i18n import N_ +from pyanaconda.i18n import _, N_ +from pyanaconda import isys +from pyanaconda.constants import productName
from pykickstart.constants import AUTOPART_TYPE_PLAIN, AUTOPART_TYPE_BTRFS from pykickstart.constants import AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP
@@ -128,3 +134,184 @@ def ui_storage_logger(): storage_log.addFilter(storage_filter) yield storage_log.removeFilter(storage_filter)
+class SanityException(Exception):
- pass
+class SanityError(SanityException):
- pass
+class SanityWarning(SanityException):
- pass
+class LUKSDeviceWithoutKeyError(SanityError):
- pass
+def sanity_check(storage):
- """
- Run a series of tests to verify the storage configuration.
- This function is called at the end of partitioning so that
- we can make sure you don't have anything silly (like no /,
- a really small /, etc).
- :rtype: a list of SanityExceptions
- :return: a list of accumulated errors and warnings
- """
- exns = []
- checkSizes = [('/usr', 250), ('/tmp', 50), ('/var', 384),
('/home', 100), ('/boot', 75)]- mustbeonlinuxfs = ['/', '/var', '/tmp', '/usr', '/home', '/usr/share',
'/usr/lib']
- mustbeonroot = ['/bin','/dev','/sbin','/etc','/lib','/root', '/mnt',
'lost+found', '/proc']
- filesystems = storage.mountpoints
- root = storage.fsset.rootDevice
- swaps = storage.fsset.swapDevices
- if root:
if root.size < 250:exns.append(SanityWarning(_("Your root partition is less than 250 ""megabytes which is usually too small to ""install %s.") % (productName,)))- else:
exns.append(SanityError(_("You have not defined a root partition (/), ""which is required for installation of %s ""to continue.") % (productName,)))- # Prevent users from installing on s390x with (a) no /boot volume, (b)
the
- # root volume on LVM, and (c) the root volume not restricted to a single
- # PV
- # NOTE: There is not really a way for users to create a / volume
- # restricted to a single PV. The backend support is there, but there
are
- # no UI hook-ups to drive that functionality, but I do not personally
- # care. --dcantrell
- if arch.isS390() and '/boot' not in storage.mountpoints and root:
if root.type == 'lvmlv' and not root.singlePV:exns.append(SanityError(_("This platform requires /boot on a dedicated ""partition or logical volume. If you do not ""want a /boot volume, you must place / on a ""dedicated non-LVM partition.")))- # FIXME: put a check here for enough space on the filesystems. maybe?
- for (mount, size) in checkSizes:
if mount in filesystems and filesystems[mount].size < size:exns.append(SanityWarning(_("Your %(mount)s partition is less than ""%(size)s megabytes which is lower than ""recommended for a normal %(productName)s ""install.")% {'mount': mount, 'size': size,'productName': productName}))- for (mount, device) in filesystems.items():
problem = filesystems[mount].checkSize()if problem < 0:exns.append(SanityError(_("Your %(mount)s partition is too small for%(format)s formatting "
"(allowable size is %(minSize)s to%(maxSize)s)")
% {"mount": mount, "format": device.format.name,"minSize": device.minSize, "maxSize":device.maxSize}))
elif problem > 0:exns.append(SanityError(_("Your %(mount)s partition is too large for%(format)s formatting "
"(allowable size is %(minSize)s to%(maxSize)s)")
% {"mount":mount, "format": device.format.name,"minSize": device.minSize, "maxSize":device.maxSize}))
- if storage.bootloader and not storage.bootloader.skip_bootloader:
stage1 = storage.bootloader.stage1_deviceif not stage1:exns.append(SanityError(_("No valid bootloader target device found. ""See below for details.")))pe = _platform.stage1MissingErrorif pe:exns.append(SanityError(_(pe)))else:storage.bootloader.is_valid_stage1_device(stage1)exns.extend(SanityError(msg) for msg instorage.bootloader.errors)
exns.extend(SanityWarning(msg) for msg instorage.bootloader.warnings)
stage2 = storage.bootloader.stage2_deviceif stage1 and not stage2:exns.append(SanityError(_("You have not created a bootablepartition.")))
else:storage.bootloader.is_valid_stage2_device(stage2)exns.extend(SanityError(msg) for msg instorage.bootloader.errors)
exns.extend(SanityWarning(msg) for msg instorage.bootloader.warnings)
if not storage.bootloader.check():exns.extend(SanityError(msg) for msg instorage.bootloader.errors)
## check that GPT boot disk on BIOS system has a BIOS boot partition#if _platform.weight(fstype="biosboot") and \stage1 and stage1.isDisk and \getattr(stage1.format, "labelType", None) == "gpt":missing = Truefor part in [p for p in storage.partitions if p.disk == stage1]:if part.format.type == "biosboot":missing = Falsebreakif missing:exns.append(SanityError(_("Your BIOS-based system needs a special ""partition to boot from a GPT disk label. ""To continue, please create a 1MiB ""'biosboot' type partition.")))- if not swaps:
installed = util.total_memory()required = Size("%s KiB" % isys.EARLY_SWAP_RAM)if installed < required:exns.append(SanityError(_("You have not specified a swap partition. ""%(requiredMem)s of memory is required tocontinue installation "
"without a swap partition, but you only have%(installedMem)s.")
% {"requiredMem": required,"installedMem": installed}))else:exns.append(SanityWarning(_("You have not specified a swap partition. ""Although not strictly required in all cases,"
"it will significantly improve performance ""for most installations.")))- no_uuid = [s for s in swaps if s.format.exists and not s.format.uuid]
- if no_uuid:
exns.append(SanityWarning(_("At least one of your swap devices does not have"
"a UUID, which is common in swap space created ""using older versions of mkswap. These devices ""will be referred to by device path in ""/etc/fstab, which is not ideal since device ""paths can change under a variety of ""circumstances. ")))- for (mountpoint, dev) in filesystems.items():
if mountpoint in mustbeonroot:exns.append(SanityError(_("This mount point is invalid. The %s directorymust "
"be on the / file system.") % mountpoint))if mountpoint in mustbeonlinuxfs and (not dev.format.mountable ornot dev.format.linuxNative):
exns.append(SanityError(_("The mount point %s must be on a linux filesystem.") % mountpoint))
- if storage.rootDevice and storage.rootDevice.format.exists:
e = storage.mustFormat(storage.rootDevice)if e:exns.append(SanityError(e))- exns += storage._verifyLUKSDevicesHaveKey()
- return exns
diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py index 45ab6de..e0a78b8 100644 --- a/pyanaconda/ui/gui/spokes/custom.py +++ b/pyanaconda/ui/gui/spokes/custom.py @@ -56,8 +56,6 @@ from blivet.partitioning import doAutoPartition from blivet.errors import StorageError from blivet.errors import NoDisksError from blivet.errors import NotEnoughFreeSpaceError -from blivet.errors import SanityError -from blivet.errors import SanityWarning from blivet.errors import LUKSDeviceWithoutKeyError from blivet.devicelibs import raid from blivet.devices import LUKSDevice @@ -66,6 +64,8 @@ from pyanaconda.storage_utils import ui_storage_logger, device_type_from_autopar from pyanaconda.storage_utils import DEVICE_TEXT_PARTITION, DEVICE_TEXT_MAP, DEVICE_TEXT_MD from pyanaconda.storage_utils import PARTITION_ONLY_FORMAT_TYPES, MOUNTPOINT_DESCRIPTIONS from pyanaconda.storage_utils import NAMED_DEVICE_TYPES, CONTAINER_DEVICE_TYPES +from pyanaconda.storage_utils import SanityError, SanityWarning +from pyanaconda import storage_utils
from pyanaconda.ui.communication import hubQ from pyanaconda.ui.gui.spokes import NormalSpoke @@ -2167,7 +2167,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): def _do_autopart(self): """Helper function for on_create_clicked. Assumes a non-final context in which at least some errors
discovered by sanityCheck are not considered fatal because they
discovered by sanity_check are not considered fatal because they will be dealt with later. Note: There are never any non-existent devices around when this runs.@@ -2209,7 +2209,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._storage_playground.doAutoPart = False log.debug("finished automatic partitioning")
exns = self._storage_playground.sanityCheck()
exns = storage_utils.sanity_check(self._storage_playground) errors = [exn for exn in exns if isinstance(exn, SanityError) and not isinstance(exn, LUKSDeviceWithoutKeyError)] warnings = [exn for exn in exns if isinstance(exn, SanityWarning)] for error in errors:diff --git a/pyanaconda/ui/helpers.py b/pyanaconda/ui/helpers.py index 15597e8..e688a99 100644 --- a/pyanaconda/ui/helpers.py +++ b/pyanaconda/ui/helpers.py @@ -84,14 +84,13 @@ class StorageChecker(object): target=self.checkStorage))
def checkStorage(self):
from blivet.errors import SanityErrorfrom blivet.errors import SanityWarning
from pyanaconda.storage_utils import sanity_check, SanityError,SanityWarning
threadMgr.wait(constants.THREAD_EXECUTE_STORAGE) hubQ.send_not_ready(self._mainSpokeClass) hubQ.send_message(self._mainSpokeClass, _("Checking storage configuration..."))
exns = self.storage.sanityCheck()
exns = sanity_check(self.storage) errors = [exn.message for exn in exns if isinstance(exn, SanityError)] warnings = [exn.message for exn in exns if isinstance(exn, SanityWarning)] (StorageChecker.errors, StorageChecker.warnings) = (errors, warnings)diff --git a/pyanaconda/ui/tui/spokes/storage.py b/pyanaconda/ui/tui/spokes/storage.py index a0e05d9..e86f0bb 100644 --- a/pyanaconda/ui/tui/spokes/storage.py +++ b/pyanaconda/ui/tui/spokes/storage.py @@ -27,13 +27,11 @@ from pyanaconda.ui.categories.system import SystemCategory from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.simpleline import TextWidget, CheckboxWidget from pyanaconda.ui.tui.tuiobject import YesNoDialog -from pyanaconda.storage_utils import AUTOPART_CHOICES +from pyanaconda.storage_utils import AUTOPART_CHOICES, sanity_check, SanityError, SanityWarning
from blivet import storageInitialize, arch from blivet.size import Size from blivet.errors import StorageError, DasdFormatError -from blivet.errors import SanityError -from blivet.errors import SanityWarning from blivet.devices import DASDDevice, FcoeDiskDevice, iScsiDiskDevice, MultipathDevice, ZFCPDiskDevice from blivet.devicelibs.dasd import format_dasd, make_unformatted_dasd_list from pyanaconda.flags import flags @@ -393,7 +391,7 @@ class StorageSpoke(NormalTUISpoke): self._ready = True else: print(_("Checking storage configuration..."))
exns = self.storage.sanityCheck()
exns = sanity_check(self.storage) errors = [exn.message for exn in exns if isinstance(exn, SanityError)] warnings = [exn.message for exn in exns if isinstance(exn, SanityWarning)] (self.errors, self.warnings) = (errors, warnings)-- 1.9.3
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
It looks like you're being inconsistant w/ LUKSDeviceWithoutKeyError, since you import it from blivet but also define it in anaconda.
I think you're better off just moving it and blivet._verifyLUKSDevicesHaveKey() method into anaconda, preserving its place in the class hierarchy. That way, the bits of code that assign to various lists based on type of error will still do the right thing.
If you think that _verifyLUKSDevicesHaveKey() is actually finding out something that is blivet specific, you could change it to a public method that just returns a list of formatted LUKS devices that have no way of obtaining a key, and use that info to raise an exception in anaconda.
- mulhern
On Wed, 2014-07-30 at 04:38 -0400, Anne Mulhern wrote:
It looks like you're being inconsistant w/ LUKSDeviceWithoutKeyError, since you import it from blivet but also define it in anaconda.
I think you're better off just moving it and blivet._verifyLUKSDevicesHaveKey() method into anaconda, preserving its place in the class hierarchy. That way, the bits of code that assign to various lists based on type of error will still do the right thing.
Good point. I think moving verifyLUKSDevicesHaveKey to Anaconda would be the best solution.
I'll send a follow-up patch soon.
Thanks!
Minimum RAM requirements depend on the installation mode (GUI/TUI) so the storage sanity check needs a parameter telling it what the current minimum RAM requirement is.
Replace the use of the EARLY_SWAP_RAM constant (removed in the commit 0f200d705b6f167abc29cf93bdac13fa1bbaf6c4) with the NO_SWAP_EXTRA_RAM that has a better name and gives the right information independent on tthe installation mode.
Signed-off-by: Vratislav Podzimek vpodzime@redhat.com --- pyanaconda/isys/__init__.py | 1 + pyanaconda/storage_utils.py | 7 +++++-- pyanaconda/ui/gui/spokes/custom.py | 7 ++++--- pyanaconda/ui/gui/spokes/storage.py | 3 ++- pyanaconda/ui/helpers.py | 6 ++++-- 5 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/pyanaconda/isys/__init__.py b/pyanaconda/isys/__init__.py index 29e9b24..44f6144 100644 --- a/pyanaconda/isys/__init__.py +++ b/pyanaconda/isys/__init__.py @@ -54,6 +54,7 @@ else:
MIN_GUI_RAM = MIN_RAM + GUI_INSTALL_EXTRA_RAM SQUASHFS_EXTRA_RAM = 750 +NO_SWAP_EXTRA_RAM = 200
## Flush filesystem buffers. def sync (): diff --git a/pyanaconda/storage_utils.py b/pyanaconda/storage_utils.py index f49cec0..1d3bf5e 100644 --- a/pyanaconda/storage_utils.py +++ b/pyanaconda/storage_utils.py @@ -147,7 +147,7 @@ class SanityWarning(SanityException): class LUKSDeviceWithoutKeyError(SanityError): pass
-def sanity_check(storage): +def sanity_check(storage, min_ram=isys.MIN_RAM): """ Run a series of tests to verify the storage configuration.
@@ -155,6 +155,9 @@ def sanity_check(storage): we can make sure you don't have anything silly (like no /, a really small /, etc).
+ :param storage: an instance of the :class:`blivet.Blivet` class to check + :param min_ram: minimum RAM (in MiB) needed for the installation with swap + space available :rtype: a list of SanityExceptions :return: a list of accumulated errors and warnings
@@ -270,7 +273,7 @@ def sanity_check(storage):
if not swaps: installed = util.total_memory() - required = Size("%s KiB" % isys.EARLY_SWAP_RAM) + required = Size("%s MiB" % (min_ram + isys.NO_SWAP_EXTRA_RAM))
if installed < required: exns.append( diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py index e0a78b8..99e874e 100644 --- a/pyanaconda/ui/gui/spokes/custom.py +++ b/pyanaconda/ui/gui/spokes/custom.py @@ -37,6 +37,7 @@ from pyanaconda.threads import AnacondaThread, threadMgr from pyanaconda.constants import THREAD_EXECUTE_STORAGE, THREAD_STORAGE, THREAD_CUSTOM_STORAGE_INIT from pyanaconda.iutil import lowerASCII from pyanaconda.bootloader import BootLoaderError +from pyanaconda import isys
from blivet import devicefactory from blivet.formats import device_formats @@ -69,7 +70,7 @@ from pyanaconda import storage_utils
from pyanaconda.ui.communication import hubQ from pyanaconda.ui.gui.spokes import NormalSpoke -from pyanaconda.ui.gui.spokes.storage import StorageChecker +from pyanaconda.ui.helpers import StorageChecker from pyanaconda.ui.gui.spokes.lib.cart import SelectedDisksDialog from pyanaconda.ui.gui.spokes.lib.passphrase import PassphraseDialog from pyanaconda.ui.gui.spokes.lib.accordion import updateSelectorFromDevice, Accordion, Page, CreateNewPage, UnknownPage @@ -133,7 +134,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): title = N_("MANUAL PARTITIONING")
def __init__(self, data, storage, payload, instclass): - StorageChecker.__init__(self) + StorageChecker.__init__(self, min_ram=isys.MIN_GUI_RAM) NormalSpoke.__init__(self, data, storage, payload, instclass)
self._back_already_clicked = False @@ -2209,7 +2210,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._storage_playground.doAutoPart = False log.debug("finished automatic partitioning")
- exns = storage_utils.sanity_check(self._storage_playground) + exns = storage_utils.sanity_check(self._storage_playground, min_ram=isys.MIN_GUI_RAM) errors = [exn for exn in exns if isinstance(exn, SanityError) and not isinstance(exn, LUKSDeviceWithoutKeyError)] warnings = [exn for exn in exns if isinstance(exn, SanityWarning)] for error in errors: diff --git a/pyanaconda/ui/gui/spokes/storage.py b/pyanaconda/ui/gui/spokes/storage.py index aab3acf..479537b 100644 --- a/pyanaconda/ui/gui/spokes/storage.py +++ b/pyanaconda/ui/gui/spokes/storage.py @@ -67,6 +67,7 @@ from pyanaconda.product import productName from pyanaconda.flags import flags from pyanaconda.i18n import _, C_, CN_, P_ from pyanaconda import constants +from pyanaconda import isys from pyanaconda.bootloader import BootLoaderError
from pykickstart.constants import CLEARPART_TYPE_NONE, AUTOPART_TYPE_LVM @@ -227,7 +228,7 @@ class StorageSpoke(NormalSpoke, StorageChecker): title = CN_("GUI|Spoke", "INSTALLATION _DESTINATION")
def __init__(self, *args, **kwargs): - StorageChecker.__init__(self) + StorageChecker.__init__(self, min_ram=isys.MIN_GUI_RAM) NormalSpoke.__init__(self, *args, **kwargs) self.applyOnSkip = True
diff --git a/pyanaconda/ui/helpers.py b/pyanaconda/ui/helpers.py index e688a99..e6654b9 100644 --- a/pyanaconda/ui/helpers.py +++ b/pyanaconda/ui/helpers.py @@ -61,6 +61,7 @@ from pyanaconda import constants from pyanaconda.threads import threadMgr, AnacondaThread from pyanaconda.ui.communication import hubQ from pyanaconda.i18n import _ +from pyanaconda import isys
import logging import copy @@ -72,8 +73,9 @@ class StorageChecker(object): errors = [] warnings = []
- def __init__(self, mainSpokeClass="StorageSpoke"): + def __init__(self, min_ram=isys.MIN_RAM, mainSpokeClass="StorageSpoke"): self._mainSpokeClass = mainSpokeClass + self._min_ram = min_ram
@abstractproperty def storage(self): @@ -90,7 +92,7 @@ class StorageChecker(object):
hubQ.send_not_ready(self._mainSpokeClass) hubQ.send_message(self._mainSpokeClass, _("Checking storage configuration...")) - exns = sanity_check(self.storage) + exns = sanity_check(self.storage, min_ram=self._min_ram) errors = [exn.message for exn in exns if isinstance(exn, SanityError)] warnings = [exn.message for exn in exns if isinstance(exn, SanityWarning)] (StorageChecker.errors, StorageChecker.warnings) = (errors, warnings)
This code is installer-specific and should thus live in the installer's codebase.
Signed-off-by: Vratislav Podzimek vpodzime@redhat.com --- blivet/__init__.py | 167 +------------------------------------------------ blivet/errors.py | 16 +---- blivet/partitioning.py | 23 +------ 3 files changed, 5 insertions(+), 201 deletions(-)
diff --git a/blivet/__init__.py b/blivet/__init__.py index ff77b53..9ec31dd 100644 --- a/blivet/__init__.py +++ b/blivet/__init__.py @@ -70,7 +70,7 @@ import parted from pykickstart.constants import AUTOPART_TYPE_LVM, CLEARPART_TYPE_ALL, CLEARPART_TYPE_LINUX, CLEARPART_TYPE_LIST, CLEARPART_TYPE_NONE
from .storage_log import log_exception_info, log_method_call -from .errors import DeviceError, DirtyFSError, FSResizeError, FSTabTypeMismatchError, LUKSDeviceWithoutKeyError, UnknownSourceDeviceError, SanityError, SanityWarning, StorageError, UnrecognizedFSTabEntryError +from .errors import DeviceError, DirtyFSError, FSResizeError, FSTabTypeMismatchError, UnknownSourceDeviceError, StorageError, UnrecognizedFSTabEntryError, LUKSDeviceWithoutKeyError from .devices import BTRFSDevice, BTRFSSubVolumeDevice, BTRFSVolumeDevice, DirectoryDevice, FileDevice, LVMLogicalVolumeDevice, LVMThinLogicalVolumeDevice, LVMThinPoolDevice, LVMVolumeGroupDevice, MDRaidArrayDevice, NetworkStorageDevice, NFSDevice, NoDevice, OpticalDevice, PartitionDevice, TmpFSDevice, devicePathToName from .devicetree import DeviceTree from .deviceaction import ActionCreateDevice, ActionCreateFormat, ActionDestroyDevice, ActionDestroyFormat, ActionResizeDevice, ActionResizeFormat @@ -112,12 +112,6 @@ def enable_installer_mode(): global ERROR_RAISE
from pyanaconda import iutil # pylint: disable=redefined-outer-name - from pyanaconda.constants import shortProductName # pylint: disable=redefined-outer-name - from pyanaconda.constants import productName # pylint: disable=redefined-outer-name - from pyanaconda.bootloader import get_bootloader # pylint: disable=redefined-outer-name - from pyanaconda.errors import errorHandler # pylint: disable=redefined-outer-name - from pyanaconda.errors import ERROR_RAISE # pylint: disable=redefined-outer-name - if hasattr(iutil, 'getTargetPhysicalRoot'): # For anaconda versions > 21.43 _storageRoot = iutil.getTargetPhysicalRoot() # pylint: disable=no-name-in-module @@ -1562,165 +1556,6 @@ class Blivet(object): not d.format.hasKey): yield LUKSDeviceWithoutKeyError(_("LUKS device %s has no encryption key") % (dev.name,))
- def sanityCheck(self): - """ Run a series of tests to verify the storage configuration. - - This is installer-specific. - - This function is called at the end of partitioning so that - we can make sure you don't have anything silly (like no /, - a really small /, etc). - - :rtype: a list of SanityExceptions - :return: a list of accumulated errors and warnings - """ - exns = [] - - if not flags.installer_mode: - return exns - - checkSizes = [('/usr', 250), ('/tmp', 50), ('/var', 384), - ('/home', 100), ('/boot', 75)] - mustbeonlinuxfs = ['/', '/var', '/tmp', '/usr', '/home', '/usr/share', '/usr/lib'] - mustbeonroot = ['/bin','/dev','/sbin','/etc','/lib','/root', '/mnt', 'lost+found', '/proc'] - - filesystems = self.mountpoints - root = self.fsset.rootDevice - swaps = self.fsset.swapDevices - - if root: - if root.size < 250: - exns.append( - SanityWarning(_("Your root partition is less than 250 " - "megabytes which is usually too small to " - "install %s.") % (productName,))) - else: - exns.append( - SanityError(_("You have not defined a root partition (/), " - "which is required for installation of %s " - "to continue.") % (productName,))) - - # Prevent users from installing on s390x with (a) no /boot volume, (b) the - # root volume on LVM, and (c) the root volume not restricted to a single - # PV - # NOTE: There is not really a way for users to create a / volume - # restricted to a single PV. The backend support is there, but there are - # no UI hook-ups to drive that functionality, but I do not personally - # care. --dcantrell - if arch.isS390() and '/boot' not in self.mountpoints and root: - if root.type == 'lvmlv' and not root.singlePV: - exns.append( - SanityError(_("This platform requires /boot on a dedicated " - "partition or logical volume. If you do not " - "want a /boot volume, you must place / on a " - "dedicated non-LVM partition."))) - - # FIXME: put a check here for enough space on the filesystems. maybe? - - for (mount, size) in checkSizes: - if mount in filesystems and filesystems[mount].size < size: - exns.append( - SanityWarning(_("Your %(mount)s partition is less than " - "%(size)s megabytes which is lower than " - "recommended for a normal %(productName)s " - "install.") - % {'mount': mount, 'size': size, - 'productName': productName})) - - for (mount, device) in filesystems.items(): - problem = filesystems[mount].checkSize() - if problem < 0: - exns.append( - SanityError(_("Your %(mount)s partition is too small for %(format)s formatting " - "(allowable size is %(minSize)s to %(maxSize)s)") - % {"mount": mount, "format": device.format.name, - "minSize": device.minSize, "maxSize": device.maxSize})) - elif problem > 0: - exns.append( - SanityError(_("Your %(mount)s partition is too large for %(format)s formatting " - "(allowable size is %(minSize)s to %(maxSize)s)") - % {"mount":mount, "format": device.format.name, - "minSize": device.minSize, "maxSize": device.maxSize})) - - if self.bootloader and not self.bootloader.skip_bootloader: - stage1 = self.bootloader.stage1_device - if not stage1: - exns.append( - SanityError(_("No valid bootloader target device found. " - "See below for details."))) - pe = _platform.stage1MissingError - if pe: - exns.append(SanityError(_(pe))) - else: - self.bootloader.is_valid_stage1_device(stage1) - exns.extend(SanityError(msg) for msg in self.bootloader.errors) - exns.extend(SanityWarning(msg) for msg in self.bootloader.warnings) - - stage2 = self.bootloader.stage2_device - if stage1 and not stage2: - exns.append(SanityError(_("You have not created a bootable partition."))) - else: - self.bootloader.is_valid_stage2_device(stage2) - exns.extend(SanityError(msg) for msg in self.bootloader.errors) - exns.extend(SanityWarning(msg) for msg in self.bootloader.warnings) - if not self.bootloader.check(): - exns.extend(SanityError(msg) for msg in self.bootloader.errors) - - # - # check that GPT boot disk on BIOS system has a BIOS boot partition - # - if _platform.weight(fstype="biosboot") and \ - stage1 and stage1.isDisk and \ - getattr(stage1.format, "labelType", None) == "gpt": - missing = True - for part in [p for p in self.partitions if p.disk == stage1]: - if part.format.type == "biosboot": - missing = False - break - - if missing: - exns.append( - SanityError(_("Your BIOS-based system needs a special " - "partition to boot from a GPT disk label. " - "To continue, please create a 1MiB " - "'biosboot' type partition."))) - - if not swaps: - exns.append( - SanityWarning(_("You have not specified a swap partition. " - "Although not strictly required in all cases, " - "it will significantly improve performance " - "for most installations."))) - no_uuid = [s for s in swaps if s.format.exists and not s.format.uuid] - if no_uuid: - exns.append( - SanityWarning(_("At least one of your swap devices does not have " - "a UUID, which is common in swap space created " - "using older versions of mkswap. These devices " - "will be referred to by device path in " - "/etc/fstab, which is not ideal since device " - "paths can change under a variety of " - "circumstances. "))) - - for (mountpoint, dev) in filesystems.items(): - if mountpoint in mustbeonroot: - exns.append( - SanityError(_("This mount point is invalid. The %s directory must " - "be on the / file system.") % mountpoint)) - - if mountpoint in mustbeonlinuxfs and (not dev.format.mountable or not dev.format.linuxNative): - exns.append( - SanityError(_("The mount point %s must be on a linux file system.") % mountpoint)) - - if self.rootDevice and self.rootDevice.format.exists: - e = self.mustFormat(self.rootDevice) - if e: - exns.append(SanityError(e)) - - exns += self._verifyLUKSDevicesHaveKey() - - return exns - def dumpState(self, suffix): """ Dump the current device list to the storage shelf. """ key = "devices.%d.%s" % (time.time(), suffix) diff --git a/blivet/errors.py b/blivet/errors.py index e0e5871..d9f1f76 100644 --- a/blivet/errors.py +++ b/blivet/errors.py @@ -137,6 +137,9 @@ class LVMError(StorageError): class CryptoError(StorageError): pass
+class LUKSDeviceWithoutKeyError(StorageError): + pass + class MPathError(StorageError): pass
@@ -190,16 +193,3 @@ class UnknownSourceDeviceError(StorageError): # factories class DeviceFactoryError(StorageError): pass - -#sanity -class SanityException(StorageError): - pass - -class SanityError(SanityException): - pass - -class SanityWarning(SanityException): - pass - -class LUKSDeviceWithoutKeyError(SanityError): - pass diff --git a/blivet/partitioning.py b/blivet/partitioning.py index 48d72ff..bd7a0a8 100644 --- a/blivet/partitioning.py +++ b/blivet/partitioning.py @@ -26,7 +26,7 @@ from decimal import Decimal import parted from pykickstart.constants import AUTOPART_TYPE_BTRFS, AUTOPART_TYPE_LVM, AUTOPART_TYPE_LVM_THINP, AUTOPART_TYPE_PLAIN
-from .errors import DeviceError, NoDisksError, NotEnoughFreeSpaceError, PartitioningError, SanityError, SanityWarning +from .errors import DeviceError, NoDisksError, NotEnoughFreeSpaceError, PartitioningError from .flags import flags from .devices import PartitionDevice, LUKSDevice, devicePathToName from .formats import getFormat @@ -392,27 +392,6 @@ def doAutoPartition(storage, data): new_swaps = (dev for dev in storage.swaps if not dev.format.exists) storage.setFstabSwaps(new_swaps)
-def sanityCheck(storage): - """Do a sanity check in a partitioning context. - - :param storage: a :class:`~.Blivet` instance - :type storage: :class:`~.Blivet` - - Logs all SanityErrors and SanityWarnings, SanityErrors first. - - Raises a partitioning error on SanityErrors. - """ - exns = storage.sanityCheck() - errors = [exc for exc in exns if isinstance(exc, SanityError)] - warnings = [exc for exc in exns if isinstance(exc, SanityWarning)] - for error in errors: - log.error("%s", error.message) - for warning in warnings: - log.warning("%s", warning.message) - - if errors: - raise PartitioningError("\n".join(error.message for error in errors)) - def partitionCompare(part1, part2): """ More specifically defined partitions come first.
On Tue, 2014-07-29 at 14:03 +0200, Vratislav Podzimek wrote:
sanityCheck code is installer-specific so it should live in the anaconda codebase. And it should check minimal RAM requirements properly based on the installation mode.
Vratislav Podzimek (2): Move sanityCheck code to anaconda's codebase Make storage sanity check aware of base RAM requirements (#1123466)
pyanaconda/isys/__init__.py | 1 + pyanaconda/kickstart.py | 7 +- pyanaconda/storage_utils.py | 192 +++++++++++++++++++++++++++++++++++- pyanaconda/ui/gui/spokes/custom.py | 13 +-- pyanaconda/ui/gui/spokes/storage.py | 3 +- pyanaconda/ui/helpers.py | 9 +- pyanaconda/ui/tui/spokes/storage.py | 6 +- 7 files changed, 213 insertions(+), 18 deletions(-)
Looks good to me, thanks for working on this! :)
On 07/29/2014 07:03 AM, Vratislav Podzimek wrote:
sanityCheck code is installer-specific so it should live in the anaconda codebase. And it should check minimal RAM requirements properly based on the installation mode.
Vratislav Podzimek (2): Move sanityCheck code to anaconda's codebase Make storage sanity check aware of base RAM requirements (#1123466)
Thank you for getting this code out of blivet. I owe you a beer.
pyanaconda/isys/__init__.py | 1 + pyanaconda/kickstart.py | 7 +- pyanaconda/storage_utils.py | 192 +++++++++++++++++++++++++++++++++++- pyanaconda/ui/gui/spokes/custom.py | 13 +-- pyanaconda/ui/gui/spokes/storage.py | 3 +- pyanaconda/ui/helpers.py | 9 +- pyanaconda/ui/tui/spokes/storage.py | 6 +- 7 files changed, 213 insertions(+), 18 deletions(-)
anaconda-patches@lists.fedorahosted.org