btrfs, lvm, and md will all use raid level strings within the UI and those will be converted as needed by the DeviceFactory classes. --- pyanaconda/storage/__init__.py | 30 +++-- pyanaconda/storage/devicelibs/mdraid.py | 20 ++- pyanaconda/ui/gui/spokes/custom.py | 222 ++++++++++++++++++++++++++++--- 3 files changed, 232 insertions(+), 40 deletions(-)
diff --git a/pyanaconda/storage/__init__.py b/pyanaconda/storage/__init__.py index b07bce1..af15757 100644 --- a/pyanaconda/storage/__init__.py +++ b/pyanaconda/storage/__init__.py @@ -1905,11 +1905,9 @@ class Storage(object): def getDeviceFactory(self, device_type, size, **kwargs): """ Return a suitable DeviceFactory instance for device_type. """ disks = kwargs.get("disks", []) + raid_level = kwargs.get("raid_level") encrypted = kwargs.get("encrypted", False)
- # md, btrfs, lvm - raid_level = kwargs.get("level") - class_table = {AUTOPART_TYPE_LVM: LVMFactory, AUTOPART_TYPE_BTRFS: BTRFSFactory, AUTOPART_TYPE_PLAIN: PartitionFactory} @@ -1949,10 +1947,7 @@ class Storage(object): disks the set of disks we can allocate from encrypted boolean
- level (btrfs/md only) RAID level - - striped (lvm only) boolean - mirrored (lvm only) boolean + raid_level (btrfs/md/lvm only) RAID level (string)
device an already-defined but non-existent device to adjust instead of creating a new device @@ -1967,11 +1962,7 @@ class Storage(object): device = kwargs.get("device")
# md, btrfs - raid_level = kwargs.get("level") - - # lvm - striped = kwargs.get("striped") - mirrored = kwargs.get("mirrored") + raid_level = kwargs.get("raid_level")
if not fstype: fstype = self.getFSType(mountpoint=mountpoint) @@ -3017,7 +3008,7 @@ class DeviceFactory(object): self.size_func_kwargs = {} # keyword args to pass to size function
# choose a size set class for member partition allocation - if raid_level in ("raid1", "raid10"): + if raid_level is not None and raid_level.startswith("raid"): self.set_class = SameSizeSet else: self.set_class = TotalSizeSet @@ -3116,6 +3107,11 @@ class BTRFSFactory(DeviceFactory): elif self.raid_level in ("raid1", "raid10"): return self.size * len(self.disks)
+ def new_device(self, *args, **kwargs): + kwargs["dataLevel"] = self.raid_level + kwargs["metaDataLevel"] = self.raid_level + return super(BTRFSFactory, self).new_device(*args, **kwargs) + class LVMFactory(DeviceFactory): type_desc = "lvm" member_format = "lvmpv" @@ -3162,7 +3158,7 @@ class MDFactory(DeviceFactory): type_desc = "md" member_format = "mdmember" new_container_attr = None - new_device_attr = "newMD" + new_device_attr = "newMDArray"
def __init__(self, storage, size, disks, raid_level, encrypted): super(MDFactory, self).__init__(storage, size, disks, raid_level, @@ -3177,3 +3173,9 @@ class MDFactory(DeviceFactory): def device_size(self): return get_member_space(self.size, len(self.disks), **self.size_func_kwargs) + + def new_device(self, *args, **kwargs): + kwargs["level"] = self.raid_level + kwargs["totalDevices"] = len(kwargs.get("parents")) + kwargs["memberDevices"] = len(kwargs.get("parents")) + return super(MDFactory, self).new_device(*args, **kwargs) diff --git a/pyanaconda/storage/devicelibs/mdraid.py b/pyanaconda/storage/devicelibs/mdraid.py index 221ee04..c2aad01 100644 --- a/pyanaconda/storage/devicelibs/mdraid.py +++ b/pyanaconda/storage/devicelibs/mdraid.py @@ -77,6 +77,13 @@ def getRaidLevels():
raid_levels = getRaidLevels()
+raid_descriptors = {RAID10: ("raid10", "RAID10", "10", 10), + RAID6: ("raid6", "RAID6", "6", 6), + RAID5: ("raid5", "RAID5", "5", 5), + RAID4: ("raid4", "RAID4", "4", 4), + RAID1: ("raid1", "mirror", "RAID1", "1", 1), + RAID0: ("raid0", "stripe", "RAID0", "0", 0)} + def raidLevel(descriptor): for level in raid_levels: if isRaid(level, descriptor): @@ -84,15 +91,14 @@ def raidLevel(descriptor): else: raise ValueError, "invalid raid level descriptor %s" % descriptor
+def raidLevelString(level): + if level in raid_descriptors.keys(): + return raid_descriptors[level][0] + else: + raise ValueError("invalid raid level constant %s") % level + def isRaid(raid, raidlevel): """Return whether raidlevel is a valid descriptor of raid""" - raid_descriptors = {RAID10: ("RAID10", "raid10", "10", 10), - RAID6: ("RAID6", "raid6", "6", 6), - RAID5: ("RAID5", "raid5", "5", 5), - RAID4: ("RAID4", "raid4", "4", 4), - RAID1: ("mirror", "RAID1", "raid1", "1", 1), - RAID0: ("stripe", "RAID0", "raid0", "0", 0)} - if raid in raid_descriptors: return raidlevel in raid_descriptors[raid] else: diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py index 7e5b59d..5ce6624 100644 --- a/pyanaconda/ui/gui/spokes/custom.py +++ b/pyanaconda/ui/gui/spokes/custom.py @@ -24,6 +24,11 @@ # - this is only true for preexisting LVs # - Device descriptions, suggested sizes, etc. should be moved out into a support file. # - Tabbing behavior in the accordion is weird. +# - Update feature space costs when size spinner changes. +# - Either disable stripe/mirror for LVM or implement it for device creation. +# - If you click to add a mountpoint while editing a device the lightbox +# screenshot is taken prior to the ui update so the background shows the old +# size and free space while you're deciding on a size for the new device.
import gettext _ = lambda x: gettext.ldgettext("anaconda", x) @@ -43,6 +48,7 @@ from pyanaconda.storage import Root from pyanaconda.storage.partitioning import doPartitioning from pyanaconda.storage.partitioning import doAutoPartition from pyanaconda.storage.errors import StorageError +from pyanaconda.storage.devicelibs import mdraid
from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.spokes import NormalSpoke @@ -63,11 +69,34 @@ new_install_name = _("New %s %s Installation") % (productName, productVersion)
# btrfs has to be the last type in the list since it gets removed and added # depending on the device's formatting +# FIXME: use these everywhere instead of the AUTOPART_TYPE constants DEVICE_TYPE_LVM = 0 DEVICE_TYPE_MD = 1 DEVICE_TYPE_PARTITION = 2 DEVICE_TYPE_BTRFS = 3
+# raid feature names. These are the basis for some UI widget names. +raid_features = ["Performance", "Redundancy", "Error", "DistError", + "RedundantError"] + +# feature names by raid level +raid_level_features = {"raid0": ["Performance"], + "raid1": ["Redundancy"], + "raid10": ["Performance", "Redundancy"], + "raid4": ["Performance", "Error"], + "raid5": ["Performance", "DistError"], + "raid6": ["Performance", "RedundantError"]} + +# disabled features by raid_level +raid_disabled_features = {"raid1": ["Error", "DistError", "RedundantError"]} + +# reference raid level by feature name +feature_raid_levels = {"Performance": "raid0", + "Redundancy": "raid1", + "Error": "raid4", + "DistError": "raid5", + "RedundantError": "raid6"} + class UIStorageFilter(logging.Filter): def filter(self, record): record.name = "storage.ui" @@ -531,8 +560,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if not selector: return
- labelEntry = self.builder.get_object("labelEntry") - device = selector._device if device not in self._devices: # just-removed device @@ -568,11 +595,14 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # FIXME: shouldn't we be getting this from the ui somehow instead? mountpoint = getattr(device.format, "mountpoint", None)
+ raid_level = self._get_raid_level() + with ui_storage_logger(): # create a new factory using the appropriate size and type - # XXX ignoring raid, encryption for now + # XXX ignoring encryption for now factory = self.__storage.getDeviceFactory(device_type, size, - disks=device.disks) + disks=device.disks, + raid_level=raid_level)
# for raid settings, we'll need to adjust the member set and container, # and possibly also its devices @@ -584,7 +614,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # current device, then create a device of the requested type. device_types = {"partition": AUTOPART_TYPE_PLAIN, "lvmlv": AUTOPART_TYPE_LVM, - "btrfs subvolume": AUTOPART_TYPE_BTRFS} + "btrfs subvolume": AUTOPART_TYPE_BTRFS, + "mdarray": None} current_device_type = device_types.get(device.type) if current_device_type != device_type: # remove the current device @@ -593,7 +624,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._destroy_device(device)
with ui_storage_logger(): - # XXX skipping encryption, raid for now + # XXX skipping encryption for now # Use any disks with space in addition to any disks used by # a defined container. disks = [d for d in self._clearpartDevices @@ -604,7 +635,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): log.debug("disks: %s" % [d.name for d in disks]) self.__storage.newDevice(device_type, size, fstype=fs_type, disks=disks, - mountpoint=mountpoint, label=label) + mountpoint=mountpoint, label=label, + raid_level=raid_level) self._devices = self.__storage.devices
# newest device should be the one with the highest id @@ -631,7 +663,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): with ui_storage_logger(): self.__storage.newDevice(device_type, size, device=device, - disks=device.disks) + disks=device.disks, + raid_level=raid_level)
log.debug("updating selector size to '%s'" % str(Size(spec="%f MB" % device.size)).upper()) @@ -662,6 +695,141 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): getattr(device.format, "mountpoint", mountpoint) != mountpoint: device.format.mountpoint = mountpoint
+ def _get_raid_widget_dict(self, device_type): + """ Return dict of widget tuples with feature keys for device_type. """ + if device_type == DEVICE_TYPE_MD: + prefix = "raid" + elif device_type == DEVICE_TYPE_BTRFS: + prefix = "btrfs" + elif device_type == DEVICE_TYPE_LVM: + prefix = "lvm" + else: + return {} + + widget_dict = {} + for feature in raid_features: + button = self.builder.get_object("%s%sCheckbox" % (prefix, feature)) + label = self.builder.get_object("%s%sLabel" % (prefix, feature)) + if button and label: + widget_dict[feature] = (button, label) + + return widget_dict + + def _get_raid_level(self): + """ Return the raid level string based on the current ui selections. """ + device_type = self.builder.get_object("deviceTypeCombo").get_active() + widget_dict = self._get_raid_widget_dict(device_type) + if not widget_dict: + return None + + active = [] + for feature in raid_features: + if feature not in widget_dict: + continue + + (button, label) = widget_dict[feature] + if button.get_active(): + active.append(feature) + + raid_level = None + for (level, feature_set) in raid_level_features.items(): + if set(active) == set(feature_set): + raid_level = level + break + + if raid_level is None: + # this is okay for lvm or btrfs but not for md until we add linear + log.error("UI: failed to get raid level (%s)" % active) + + return raid_level + + def on_raid_feature_toggled(self, widget): + new_state = widget.get_active() + log.debug("widget %s new state: %s" % (widget, new_state)) + + raid_level = self._get_raid_level() + + # now that we've established a raid level, update disabled features + self._update_disabled_raid_features(raid_level) + + def _get_raid_disabled_features(self, raid_level): + """ Return a list of disabled features based on raid level. """ + disabled = raid_disabled_features.get(raid_level, []) + disk_count = len(self._clearpartDevices) + # go through each feature's reference raid level, filtering any that + # require more disks than we have available + for feature in raid_features: + # XXX we're using the mdraid rules for min members + level = mdraid.raidLevel(feature_raid_levels[feature]) + min_disks = mdraid.get_raid_min_members(level) + if min_disks > disk_count and feature not in disabled: + disabled.append(feature) + + return disabled + + def _update_disabled_raid_features(self, raid_level): + """ Update disabled feature widgets based on raid level. """ + device_type = self.builder.get_object("deviceTypeCombo").get_active() + widget_dict = self._get_raid_widget_dict(device_type) + disabled = self._get_raid_disabled_features(raid_level) + for feature in raid_features: + if feature not in widget_dict: + continue + + (button, label) = widget_dict[feature] + button.set_sensitive(feature not in disabled) + + def _populate_raid(self, device_type, raid_level, size): + """ Set up the raid-specific portion of the device details. """ + log.debug("populate_raid: %s, %s" % (device_type, raid_level)) + + if device_type == DEVICE_TYPE_MD: + base_level = "raid0" # FIXME: should be linear + + level_label = self.builder.get_object("raidLevelLabel") + level_label.set_text(raid_level.upper()) + factory_type = None + elif device_type == DEVICE_TYPE_BTRFS: + base_level = "single" + factory_type = AUTOPART_TYPE_BTRFS + else: + return + + # Create a DeviceFactory to use to calculate the disk space needs for + # this device with various raid features enabled. + with ui_storage_logger(): + factory = self.__storage.getDeviceFactory(factory_type, size, + disks=self._clearpartDevices, + raid_level=base_level) + + widget_dict = self._get_raid_widget_dict(device_type) + base_size = factory.device_size + active = raid_level_features[raid_level] + disabled = self._get_raid_disabled_features(raid_level) + for feature in raid_features: + if feature not in widget_dict: + # this feature isn't supported for this device type + continue + + (button, label) = widget_dict[feature] + + # is this feature enabled for the current raid level? + button.set_active(feature in active) + + # what is the incremental disk space requirement for this feature? + # TODO: update this when the size spinner changes + level = mdraid.raidLevel(feature_raid_levels[feature]) + min_disks = mdraid.get_raid_min_members(level) + if min_disks <= len(factory.disks): + factory.raid_level = feature_raid_levels[feature] + delta = factory.device_size - base_size + label.set_text("+%s" % str(Size(spec="%fmb" % delta)).upper()) + else: + label.set_text("(not enough disks)") + + # some features are not available to some raid levels + button.set_sensitive(feature not in disabled) + def _populate_right_side(self, selector): log.debug("populate_right_side: %s" % selector._device) encryptCheckbox = self.builder.get_object("encryptCheckbox") @@ -726,14 +894,21 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): fsCombo.remove(len(fsCombo.get_model()) - 1)
# FIXME: What do we do if we can't figure it out? + raid_level = None if device.type == "lvmlv": + # TODO: striping/mirroring typeCombo.set_active(DEVICE_TYPE_LVM) elif device.type == "mdarray": typeCombo.set_active(DEVICE_TYPE_MD) + raid_level = mdraid.raidLevelString(device.level) elif device.type == "partition": typeCombo.set_active(DEVICE_TYPE_PARTITION) elif device.type.startswith("btrfs"): typeCombo.set_active(DEVICE_TYPE_BTRFS) + if hasattr(device, "volume"): + raid_level = device.volume.dataLevel + else: + raid_level = device.dataLevel
# you can't change the type of an existing device typeCombo.set_sensitive(not device.exists) @@ -745,6 +920,9 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): fsCombo.set_active(i) break
+ self._populate_raid(typeCombo.get_active(), raid_level, device.size) + self.builder.get_object("optionsNotebook").set_sensitive(not device.exists) + ### ### SIGNAL HANDLERS ### @@ -959,6 +1137,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
self._configButton.set_sensitive(True) self._removeButton.set_sensitive(True) + return True
def on_page_clicked(self, page): log.debug("page clicked: %s" % getattr(page, "pageTitle", None)) @@ -1010,29 +1189,34 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._show_first_mountpoint()
def on_device_type_changed(self, combo): - text = combo.get_active_text() + new_type = combo.get_active() + log.debug("device_type_changed: %s %s" % (new_type, + combo.get_active_text()))
# if device type is not btrfs we want to make sure btrfs is not in the # fstype combo include_btrfs = False fs_type_sensitive = True
- if text == _("BTRFS"): + # eventually LVM will be handled in the else clause + if new_type in (DEVICE_TYPE_PARTITION, DEVICE_TYPE_LVM): + self._optionsNotebook.hide() + else: self._optionsNotebook.show() - self._optionsNotebook.set_current_page(DEVICE_TYPE_BTRFS) + self._optionsNotebook.set_current_page(new_type)
+ raid_level = None + if new_type == DEVICE_TYPE_BTRFS: # add btrfs to the fstype combo and lock it in test_fmt = getFormat("btrfs") include_btrfs = test_fmt.supported and test_fmt.formattable fs_type_sensitive = False - elif text == _("LVM"): - self._optionsNotebook.show() - self._optionsNotebook.set_current_page(DEVICE_TYPE_LVM) - elif text == _("RAID"): - self._optionsNotebook.show() - self._optionsNotebook.set_current_page(DEVICE_TYPE_MD) - elif text == _("Standard Partition"): - self._optionsNotebook.hide() + raid_level = "single" + elif new_type == DEVICE_TYPE_MD: + raid_level = "raid0" + + size = self.builder.get_object("sizeSpinner").get_value() + self._populate_raid(new_type, raid_level, size)
# begin btrfs magic fsCombo = self.builder.get_object("fileSystemTypeCombo")