3e63d1dd37
Apply diff anaconda-20.25.16-1..anaconda-21.48.21-1
2570 lines
109 KiB
Python
2570 lines
109 KiB
Python
#
|
|
# Custom partitioning classes.
|
|
#
|
|
# Copyright (C) 2012-2014 Red Hat, Inc.
|
|
#
|
|
# This copyrighted material is made available to anyone wishing to use,
|
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
|
# the GNU General Public License v.2, or (at your option) any later version.
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details. You should have received a copy of the
|
|
# GNU General Public License along with this program; if not, write to the
|
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
|
# source code or documentation are not subject to the GNU General Public
|
|
# License and may only be used or replicated with the express permission of
|
|
# Red Hat, Inc.
|
|
#
|
|
# Red Hat Author(s): Chris Lumens <clumens@redhat.com>
|
|
# David Lehman <dlehman@redhat.com>
|
|
#
|
|
|
|
# TODO:
|
|
# - Deleting an LV is not reflected in available space in the bottom left.
|
|
# - 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.
|
|
# - Implement striping and mirroring for LVM.
|
|
# - Activating reformat should always enable resize for existing devices.
|
|
|
|
from pykickstart.constants import CLEARPART_TYPE_NONE
|
|
|
|
from pyanaconda.i18n import _, N_, CP_
|
|
from pyanaconda.product import productName, productVersion, translated_new_install_name
|
|
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.kickstart import refreshAutoSwapSize
|
|
from pyanaconda import isys
|
|
|
|
from blivet import devicefactory
|
|
from blivet.formats import device_formats
|
|
from blivet.formats import getFormat
|
|
from blivet.formats.fs import FS
|
|
from blivet.size import Size
|
|
from blivet import Root
|
|
from blivet.devicefactory import DEVICE_TYPE_LVM
|
|
from blivet.devicefactory import DEVICE_TYPE_BTRFS
|
|
from blivet.devicefactory import DEVICE_TYPE_PARTITION
|
|
from blivet.devicefactory import DEVICE_TYPE_MD
|
|
from blivet.devicefactory import DEVICE_TYPE_DISK
|
|
from blivet.devicefactory import DEVICE_TYPE_LVM_THINP
|
|
from blivet.devicefactory import SIZE_POLICY_AUTO
|
|
from blivet import findExistingInstallations
|
|
from blivet.partitioning import doAutoPartition
|
|
from blivet.errors import StorageError
|
|
from blivet.errors import NoDisksError
|
|
from blivet.errors import NotEnoughFreeSpaceError
|
|
from blivet.devicelibs import raid, crypto
|
|
from blivet.devices import LUKSDevice
|
|
|
|
from pyanaconda.storage_utils import ui_storage_logger, device_type_from_autopart
|
|
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, LUKSDeviceWithoutKeyError
|
|
from pyanaconda import storage_utils
|
|
|
|
from pyanaconda.ui.communication import hubQ
|
|
from pyanaconda.ui.gui.spokes import NormalSpoke
|
|
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
|
|
from pyanaconda.ui.gui.spokes.lib.refresh import RefreshDialog
|
|
from pyanaconda.ui.gui.spokes.lib.summary import ActionSummaryDialog
|
|
|
|
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import size_from_entry
|
|
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import validate_label, validate_mountpoint, get_raid_level
|
|
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import selectedRaidLevel, raidLevelSelection, defaultRaidLevel, requiresRaidSelection, containerRaidLevelsSupported, raidLevelsSupported, defaultContainerRaidLevel
|
|
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import get_container_type_name, RAID_NOT_ENOUGH_DISKS
|
|
from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import AddDialog, ConfirmDeleteDialog, DisksDialog, ContainerDialog
|
|
|
|
from pyanaconda.ui.gui.utils import setViewportBackground, fancy_set_sensitive, ignoreEscape
|
|
from pyanaconda.ui.gui.utils import really_hide, really_show, GtkActionList, timed_action
|
|
from pyanaconda.ui.categories.system import SystemCategory
|
|
|
|
from gi.repository import Gdk, Gtk
|
|
from gi.repository.AnacondaWidgets import MountpointSelector
|
|
|
|
from functools import wraps
|
|
|
|
import logging
|
|
log = logging.getLogger("anaconda")
|
|
|
|
__all__ = ["CustomPartitioningSpoke"]
|
|
|
|
NOTEBOOK_LABEL_PAGE = 0
|
|
NOTEBOOK_DETAILS_PAGE = 1
|
|
NOTEBOOK_LUKS_PAGE = 2
|
|
NOTEBOOK_UNEDITABLE_PAGE = 3
|
|
NOTEBOOK_INCOMPLETE_PAGE = 4
|
|
|
|
NEW_CONTAINER_TEXT = N_("Create a new %(container_type)s ...")
|
|
CONTAINER_TOOLTIP = N_("Create or select %(container_type)s")
|
|
|
|
DEVICE_CONFIGURATION_ERROR_MSG = N_("Device reconfiguration failed. Click for "
|
|
"details.")
|
|
UNRECOVERABLE_ERROR_MSG = N_("Storage configuration reset due to unrecoverable "
|
|
"error. Click for details.")
|
|
|
|
def dev_type_from_const(dev_type_const):
|
|
return getattr(devicefactory, dev_type_const, None)
|
|
|
|
def ui_storage_logged(func):
|
|
@wraps(func)
|
|
def decorated(*args, **kwargs):
|
|
with ui_storage_logger():
|
|
return func(*args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
|
|
builderObjects = ["customStorageWindow", "containerStore", "deviceTypeStore",
|
|
"partitionStore", "raidStoreFiltered", "raidLevelStore",
|
|
"addImage", "removeImage", "settingsImage",
|
|
"mountPointCompletion", "mountPointStore"]
|
|
mainWidgetName = "customStorageWindow"
|
|
uiFile = "spokes/custom.glade"
|
|
helpFile = "CustomSpoke.xml"
|
|
|
|
category = SystemCategory
|
|
title = N_("MANUAL PARTITIONING")
|
|
|
|
def __init__(self, data, storage, payload, instclass):
|
|
StorageChecker.__init__(self, min_ram=isys.MIN_GUI_RAM)
|
|
NormalSpoke.__init__(self, data, storage, payload, instclass)
|
|
|
|
self._back_already_clicked = False
|
|
self._storage_playground = None
|
|
|
|
self.passphrase = ""
|
|
|
|
self._current_selector = None
|
|
self._devices = []
|
|
self._error = None
|
|
self._hidden_disks = []
|
|
self._fs_types = [] # list of supported fstypes
|
|
self._free_space = Size(0)
|
|
|
|
self._device_size_text = None
|
|
self._device_disks = []
|
|
self._device_container_name = None
|
|
self._device_container_raid_level = None
|
|
self._device_container_encrypted = False
|
|
self._device_container_size = SIZE_POLICY_AUTO
|
|
self._device_name_dict = {DEVICE_TYPE_LVM: None,
|
|
DEVICE_TYPE_MD: None,
|
|
DEVICE_TYPE_LVM_THINP: None,
|
|
DEVICE_TYPE_PARTITION: "",
|
|
DEVICE_TYPE_BTRFS: "",
|
|
DEVICE_TYPE_DISK: ""}
|
|
|
|
self._initialized = False
|
|
|
|
def apply(self):
|
|
self.clear_errors()
|
|
|
|
self._unhide_unusable_disks()
|
|
|
|
new_swaps = (dev for dev in self.get_new_devices() if dev.format.type == "swap")
|
|
self.storage.setFstabSwaps(new_swaps)
|
|
|
|
# update the global passphrase
|
|
self.data.autopart.passphrase = self.passphrase
|
|
|
|
# make sure any device/passphrase pairs we've obtained are remembered
|
|
for device in self.storage.devices:
|
|
if device.format.type == "luks" and not device.format.exists:
|
|
if not device.format.hasKey:
|
|
device.format.passphrase = self.passphrase
|
|
|
|
self.storage.savePassphrase(device)
|
|
|
|
hubQ.send_ready("StorageSpoke", True)
|
|
|
|
@property
|
|
def indirect(self):
|
|
return True
|
|
|
|
# This spoke has no status since it's not in a hub
|
|
@property
|
|
def status(self):
|
|
return None
|
|
|
|
def _grabObjects(self):
|
|
self._configureBox = self.builder.get_object("configureBox")
|
|
|
|
self._partitionsViewport = self.builder.get_object("partitionsViewport")
|
|
self._partitionsNotebook = self.builder.get_object("partitionsNotebook")
|
|
|
|
self._whenCreateLabel = self.builder.get_object("whenCreateLabel")
|
|
|
|
self._availableSpaceLabel = self.builder.get_object("availableSpaceLabel")
|
|
self._totalSpaceLabel = self.builder.get_object("totalSpaceLabel")
|
|
self._summaryLabel = self.builder.get_object("summary_label")
|
|
|
|
# Buttons
|
|
self._addButton = self.builder.get_object("addButton")
|
|
self._applyButton = self.builder.get_object("applyButton")
|
|
self._configButton = self.builder.get_object("configureButton")
|
|
self._removeButton = self.builder.get_object("removeButton")
|
|
self._resetButton = self.builder.get_object("resetButton")
|
|
|
|
# Detailed configuration stuff
|
|
self._encryptCheckbox = self.builder.get_object("encryptCheckbox")
|
|
self._fsCombo = self.builder.get_object("fileSystemTypeCombo")
|
|
self._labelEntry = self.builder.get_object("labelEntry")
|
|
self._mountPointEntry = self.builder.get_object("mountPointEntry")
|
|
self._nameEntry = self.builder.get_object("nameEntry")
|
|
self._raidLevelCombo = self.builder.get_object("raidLevelCombo")
|
|
self._raidLevelLabel = self.builder.get_object("raidLevelLabel")
|
|
self._reformatCheckbox = self.builder.get_object("reformatCheckbox")
|
|
self._sizeEntry = self.builder.get_object("sizeEntry")
|
|
self._typeStore = self.builder.get_object("deviceTypeStore")
|
|
self._typeCombo = self.builder.get_object("deviceTypeCombo")
|
|
self._modifyContainerButton = self.builder.get_object("modifyContainerButton")
|
|
self._containerCombo = self.builder.get_object("containerCombo")
|
|
self._containerStore = self.builder.get_object("containerStore")
|
|
self._deviceDescLabel = self.builder.get_object("deviceDescLabel")
|
|
|
|
self._passphraseEntry = self.builder.get_object("passphraseEntry")
|
|
|
|
# Stores
|
|
self._raidStoreFilter = self.builder.get_object("raidStoreFiltered")
|
|
|
|
# Labels
|
|
self._selectedDeviceLabel = self.builder.get_object("selectedDeviceLabel")
|
|
self._selectedDeviceDescLabel = self.builder.get_object("selectedDeviceDescLabel")
|
|
self._encryptedDeviceLabel = self.builder.get_object("encryptedDeviceLabel")
|
|
self._encryptedDeviceDescLabel = self.builder.get_object("encryptedDeviceDescriptionLabel")
|
|
self._incompleteDeviceLabel = self.builder.get_object("incompleteDeviceLabel")
|
|
self._incompleteDeviceDescLabel = self.builder.get_object("incompleteDeviceDescriptionLabel")
|
|
self._incompleteDeviceOptionsLabel = self.builder.get_object("incompleteDeviceOptionsLabel")
|
|
self._uneditableDeviceLabel = self.builder.get_object("uneditableDeviceLabel")
|
|
self._uneditableDeviceDescLabel = self.builder.get_object("uneditableDeviceDescriptionLabel")
|
|
self._containerLabel = self.builder.get_object("containerLabel")
|
|
|
|
def initialize(self):
|
|
NormalSpoke.initialize(self)
|
|
self._grabObjects()
|
|
|
|
setViewportBackground(self.builder.get_object("availableSpaceViewport"), "#db3279")
|
|
setViewportBackground(self.builder.get_object("totalSpaceViewport"), "#60605b")
|
|
|
|
self._raidStoreFilter.set_visible_func(self._raid_level_visible)
|
|
|
|
self._accordion = Accordion()
|
|
self._partitionsViewport.add(self._accordion)
|
|
|
|
# Populate the list of valid filesystem types from the format classes.
|
|
# Unfortunately, we have to narrow them down a little bit more because
|
|
# this list will include things like PVs and RAID members.
|
|
self._fsCombo.remove_all()
|
|
|
|
threadMgr.add(AnacondaThread(name=THREAD_CUSTOM_STORAGE_INIT, target=self._initialize))
|
|
|
|
def _initialize(self):
|
|
self._fs_types = []
|
|
actions = GtkActionList()
|
|
for cls in device_formats.itervalues():
|
|
obj = cls()
|
|
|
|
# btrfs is always handled by on_device_type_changed
|
|
supported_fs = (obj.type != "btrfs" and
|
|
obj.type != "tmpfs" and
|
|
obj.supported and obj.formattable and
|
|
(isinstance(obj, FS) or
|
|
obj.type in ["biosboot", "prepboot", "swap"]))
|
|
if supported_fs:
|
|
actions.add_action(self._fsCombo.append_text, obj.name)
|
|
self._fs_types.append(obj.name)
|
|
|
|
actions.fire()
|
|
|
|
@property
|
|
def _clearpartDevices(self):
|
|
return [d for d in self._devices if d.name in self.data.clearpart.drives and d.partitioned]
|
|
|
|
@property
|
|
def unusedDevices(self):
|
|
unused_devices = [d for d in self._storage_playground.unusedDevices
|
|
if d.disks and d.mediaPresent and
|
|
not d.partitioned and d.direct]
|
|
# add incomplete VGs and MDs
|
|
incomplete = [d for d in self._storage_playground.devicetree._devices
|
|
if not getattr(d, "complete", True)]
|
|
unused_devices.extend(incomplete)
|
|
return unused_devices
|
|
|
|
@property
|
|
def bootLoaderDevices(self):
|
|
devices = []
|
|
format_types = ["biosboot", "prepboot"]
|
|
for device in self._devices:
|
|
if device.format.type not in format_types:
|
|
continue
|
|
|
|
disk_names = (d.name for d in device.disks)
|
|
# bootDrive may not be setup because it IS one of these.
|
|
if not self.data.bootloader.bootDrive or \
|
|
self.data.bootloader.bootDrive in disk_names:
|
|
devices.append(device)
|
|
|
|
return devices
|
|
|
|
@property
|
|
def _currentFreeInfo(self):
|
|
return self._storage_playground.getFreeSpace(clearPartType=CLEARPART_TYPE_NONE)
|
|
|
|
def _setCurrentFreeSpace(self):
|
|
"""Add up all the free space on selected disks and return it as a Size."""
|
|
self._free_space = sum(f[0] for f in self._currentFreeInfo.values())
|
|
|
|
def _currentTotalSpace(self):
|
|
"""Add up the sizes of all selected disks and return it as a Size."""
|
|
totalSpace = sum((disk.size for disk in self._clearpartDevices),
|
|
Size(0))
|
|
return totalSpace
|
|
|
|
def _updateSpaceDisplay(self):
|
|
# Set up the free space/available space displays in the bottom left.
|
|
self._setCurrentFreeSpace()
|
|
|
|
self._availableSpaceLabel.set_text(str(self._free_space))
|
|
self._totalSpaceLabel.set_text(str(self._currentTotalSpace()))
|
|
|
|
count = len(self.data.clearpart.drives)
|
|
summary = CP_("GUI|Custom Partitioning",
|
|
"%d _storage device selected",
|
|
"%d _storage devices selected",
|
|
count) % count
|
|
|
|
self._summaryLabel.set_text(summary)
|
|
self._summaryLabel.set_use_underline(True)
|
|
|
|
@ui_storage_logged
|
|
def _hide_unusable_disks(self):
|
|
self._hidden_disks = []
|
|
|
|
for disk in self._storage_playground.disks:
|
|
if disk.protected or not disk.mediaPresent:
|
|
# hide removable disks containing install media
|
|
self._hidden_disks.append(disk)
|
|
self._storage_playground.devicetree.hide(disk)
|
|
|
|
def _unhide_unusable_disks(self):
|
|
for disk in reversed(self._hidden_disks):
|
|
self._storage_playground.devicetree.unhide(disk)
|
|
|
|
def _reset_storage(self):
|
|
self._storage_playground = self.storage.copy()
|
|
self._hide_unusable_disks()
|
|
self._devices = self._storage_playground.devices
|
|
|
|
def refresh(self):
|
|
self.clear_errors()
|
|
NormalSpoke.refresh(self)
|
|
|
|
# Make sure the storage spoke execute method has finished before we
|
|
# copy the storage instance.
|
|
for thread_name in [THREAD_EXECUTE_STORAGE, THREAD_STORAGE]:
|
|
threadMgr.wait(thread_name)
|
|
|
|
self._back_already_clicked = False
|
|
|
|
self.passphrase = self.data.autopart.passphrase
|
|
self._reset_storage()
|
|
self._do_refresh()
|
|
|
|
self._updateSpaceDisplay()
|
|
self._applyButton.set_sensitive(False)
|
|
|
|
@property
|
|
def _current_page(self):
|
|
# The current page is really a function of the current selector.
|
|
# Whatever selector on the LHS is selected, the current page is the
|
|
# page containing that selector.
|
|
if not self._current_selector:
|
|
return None
|
|
|
|
for page in self._accordion.allPages:
|
|
if self._current_selector in page.members:
|
|
return page
|
|
|
|
return None
|
|
|
|
def _clear_current_selector(self):
|
|
""" If something is selected, deselect it
|
|
"""
|
|
if self._current_selector:
|
|
self._current_selector.set_chosen(False)
|
|
self._current_selector = None
|
|
|
|
def _get_autopart_type(self, autopartTypeCombo):
|
|
itr = autopartTypeCombo.get_active_iter()
|
|
if not itr:
|
|
return None
|
|
|
|
model = autopartTypeCombo.get_model()
|
|
return model[itr][1]
|
|
|
|
def _change_autopart_type(self, autopartTypeCombo):
|
|
"""
|
|
This is called when the autopart type combo on the left hand side of
|
|
custom partitioning is changed. We already know how to handle the case
|
|
where the user changes the type and then clicks the autopart link
|
|
button. This handles the case where the user changes the type and then
|
|
clicks the '+' button.
|
|
|
|
"""
|
|
|
|
self.data.autopart.type = self._get_autopart_type(autopartTypeCombo)
|
|
|
|
def get_new_devices(self):
|
|
# A device scheduled for formatting only belongs in the new root.
|
|
new_devices = [d for d in self._devices if d.direct and
|
|
not d.format.exists and
|
|
not d.partitioned]
|
|
|
|
# If mountpoints 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 self._storage_playground.mountpoints.values() if d.exists]
|
|
if new_mounts or new_devices:
|
|
new_devices.extend(self._storage_playground.mountpoints.values())
|
|
new_devices.extend(self.bootLoaderDevices)
|
|
|
|
new_devices = list(set(new_devices))
|
|
|
|
return new_devices
|
|
|
|
def _populate_accordion(self):
|
|
# Make sure we start with a clean state.
|
|
self._accordion.removeAllPages()
|
|
|
|
new_devices = self.get_new_devices()
|
|
|
|
# Now it's time to populate the accordion.
|
|
log.debug("ui: devices=%s", [d.name for d in self._devices])
|
|
log.debug("ui: unused=%s", [d.name for d in self.unusedDevices])
|
|
log.debug("ui: new_devices=%s", [d.name for d in new_devices])
|
|
|
|
ui_roots = self._storage_playground.roots[:]
|
|
|
|
# If we've not yet run autopart, add an instance of CreateNewPage. This
|
|
# ensures it's only added once.
|
|
if not new_devices:
|
|
page = CreateNewPage(translated_new_install_name(),
|
|
self.on_create_clicked,
|
|
self._change_autopart_type,
|
|
partitionsToReuse=bool(ui_roots) or bool(self.unusedDevices))
|
|
self._accordion.addPage(page, cb=self.on_page_clicked)
|
|
|
|
self._partitionsNotebook.set_current_page(NOTEBOOK_LABEL_PAGE)
|
|
self._whenCreateLabel.set_text(_("When you create mount points for "
|
|
"your %(name)s %(version)s installation, you'll be able to "
|
|
"view their details here.") % {"name" : productName,
|
|
"version" : productVersion})
|
|
else:
|
|
swaps = [d for d in new_devices if d.format.type == "swap"]
|
|
mounts = dict((d.format.mountpoint, d) for d in new_devices
|
|
if getattr(d.format, "mountpoint", None))
|
|
|
|
for device in new_devices:
|
|
if device in self.bootLoaderDevices:
|
|
mounts[device.format.type] = device
|
|
|
|
new_root = Root(mounts=mounts, swaps=swaps, name=translated_new_install_name())
|
|
ui_roots.insert(0, new_root)
|
|
|
|
# Add in all the existing (or autopart-created) operating systems.
|
|
for root in ui_roots:
|
|
# Don't make a page if none of the root's devices are left.
|
|
# Also, only include devices in an old page if the format is intact.
|
|
if not any(d for d in root.swaps + root.mounts.values()
|
|
if d in self._devices and d.disks and
|
|
(root.name == translated_new_install_name() or d.format.exists)):
|
|
continue
|
|
|
|
page = Page(root.name)
|
|
|
|
for (mountpoint, device) in root.mounts.iteritems():
|
|
if device not in self._devices or \
|
|
not device.disks or \
|
|
(root.name != translated_new_install_name() and not device.format.exists):
|
|
continue
|
|
|
|
selector = page.addSelector(device, self.on_selector_clicked,
|
|
mountpoint=mountpoint)
|
|
selector.root = root
|
|
|
|
for device in root.swaps:
|
|
if device not in self._devices or \
|
|
(root.name != translated_new_install_name() and not device.format.exists):
|
|
continue
|
|
|
|
selector = page.addSelector(device, self.on_selector_clicked)
|
|
selector.root = root
|
|
|
|
page.show_all()
|
|
self._accordion.addPage(page, cb=self.on_page_clicked)
|
|
|
|
# Anything that doesn't go with an OS we understand? Put it in the Other box.
|
|
if self.unusedDevices:
|
|
page = UnknownPage(_("Unknown"))
|
|
|
|
for u in sorted(self.unusedDevices, key=lambda d: d.name):
|
|
page.addSelector(u, self.on_selector_clicked)
|
|
|
|
page.show_all()
|
|
self._accordion.addPage(page, cb=self.on_page_clicked)
|
|
|
|
def _do_refresh(self, mountpointToShow=None):
|
|
# block mountpoint selector signal handler for now
|
|
self._initialized = False
|
|
self._clear_current_selector()
|
|
|
|
# Start with buttons disabled, since nothing is selected.
|
|
self._removeButton.set_sensitive(False)
|
|
self._configButton.set_sensitive(False)
|
|
|
|
# populate the accorion with roots and mount points
|
|
self._populate_accordion()
|
|
|
|
# And then open the first page by default. Most of the time, this will
|
|
# be fine since it'll be the new installation page.
|
|
self._initialized = True
|
|
firstPage = self._accordion.allPages[0]
|
|
self._accordion.expandPage(firstPage.pageTitle)
|
|
self._show_mountpoint(page=firstPage, mountpoint=mountpointToShow)
|
|
|
|
self._applyButton.set_sensitive(False)
|
|
self._resetButton.set_sensitive(len(self._storage_playground.devicetree.findActions()) > 0)
|
|
|
|
###
|
|
### RIGHT HAND SIDE METHODS
|
|
###
|
|
def add_new_selector(self, device):
|
|
""" Add an entry for device to the new install Page. """
|
|
page = self._accordion._find_by_title(translated_new_install_name()).get_child()
|
|
devices = [device]
|
|
if not page.members:
|
|
# remove the CreateNewPage and replace it with a regular Page
|
|
expander = self._accordion._find_by_title(translated_new_install_name())
|
|
expander.remove(expander.get_child())
|
|
|
|
page = Page(translated_new_install_name())
|
|
expander.add(page)
|
|
|
|
# also pull in biosboot and prepboot that are on our boot disk
|
|
devices.extend(self.bootLoaderDevices)
|
|
devices = list(set(devices))
|
|
|
|
for _device in devices:
|
|
page.addSelector(_device, self.on_selector_clicked)
|
|
|
|
page.show_all()
|
|
|
|
def _update_selectors(self):
|
|
""" Update all btrfs selectors' size properties. """
|
|
# we're only updating selectors in the new root. problem?
|
|
page = self._accordion._find_by_title(translated_new_install_name()).get_child()
|
|
for selector in page.members:
|
|
updateSelectorFromDevice(selector, selector.device)
|
|
|
|
def _replace_device(self, **kwargs):
|
|
""" Create a replacement device and update the device selector. """
|
|
selector = kwargs.pop("selector", None)
|
|
dev_type = kwargs.pop("device_type")
|
|
size = kwargs.pop("size")
|
|
new_device = self._storage_playground.factoryDevice(dev_type, size, **kwargs)
|
|
|
|
self._devices = self._storage_playground.devices
|
|
|
|
if selector:
|
|
# update the selector with the new device and its size
|
|
updateSelectorFromDevice(selector, new_device)
|
|
|
|
def _update_device_in_selectors(self, old_device, new_device):
|
|
for s in self._accordion.allSelectors:
|
|
if s._device == old_device:
|
|
updateSelectorFromDevice(s, new_device)
|
|
|
|
def _update_all_devices_in_selectors(self):
|
|
for s in self._accordion.allSelectors:
|
|
for new_device in self._storage_playground.devices:
|
|
if ((s._device.name == new_device.name) or
|
|
(getattr(s._device, "req_name", 1) == getattr(new_device, "req_name", 2)) and
|
|
s._device.type == new_device.type and
|
|
s._device.format.type == new_device.format.type):
|
|
updateSelectorFromDevice(s, new_device)
|
|
break
|
|
else:
|
|
log.warning("failed to replace device: %s", s._device)
|
|
|
|
def _add_device_type(self, dev_type_const):
|
|
self._typeStore.append([_(DEVICE_TEXT_MAP[dev_type_from_const(dev_type_const)]),
|
|
dev_type_const])
|
|
|
|
def _validate_mountpoint(self, mountpoint, device, device_type, new_fs_type,
|
|
reformat, encrypted, raid_level):
|
|
""" Validate various aspects of a mountpoint.
|
|
|
|
:param str mountpoint: the mountpoint
|
|
:param device: blivet.devices.Device instance
|
|
:param int device_type: one of an enumeration of device types
|
|
:param str new_fs_type: string representing the new filesystem type
|
|
:param bool reformat: whether the device is to be reformatted
|
|
:param bool encrypted: whether the device is to be encrypted
|
|
:param raid_level: instance of blivet.devicelibs.raid.RAIDLevel or None
|
|
"""
|
|
error = None
|
|
if device_type not in (DEVICE_TYPE_PARTITION, DEVICE_TYPE_MD) and \
|
|
mountpoint == "/boot/efi":
|
|
error = (_("/boot/efi must be on a device of type %(oneFsType)s or %(anotherFsType)s")
|
|
% {"oneFsType": _(DEVICE_TEXT_PARTITION), "anotherFsType": _(DEVICE_TEXT_MD)})
|
|
elif device_type != DEVICE_TYPE_PARTITION and \
|
|
new_fs_type in PARTITION_ONLY_FORMAT_TYPES:
|
|
error = (_("%(fs)s must be on a device of type %(type)s")
|
|
% {"fs" : new_fs_type, "type" : _(DEVICE_TEXT_PARTITION)})
|
|
elif mountpoint and encrypted and mountpoint.startswith("/boot"):
|
|
error = _("%s cannot be encrypted") % mountpoint
|
|
elif encrypted and new_fs_type in PARTITION_ONLY_FORMAT_TYPES:
|
|
error = _("%s cannot be encrypted") % new_fs_type
|
|
elif mountpoint == "/" and device.format.exists and not reformat:
|
|
error = _("You must create a new filesystem on the root device.")
|
|
|
|
if not error and \
|
|
(raid_level is not None or requiresRaidSelection(device_type)) and \
|
|
raid_level not in raidLevelsSupported(device_type):
|
|
error = _("Device does not support RAID level selection %s.") % raid_level
|
|
|
|
if not error and raid_level is not None:
|
|
min_disks = raid_level.min_members
|
|
if len(self._device_disks) < min_disks:
|
|
error = _(RAID_NOT_ENOUGH_DISKS) % {"level": raid_level,
|
|
"min" : min_disks,
|
|
"count": len(self._device_disks)}
|
|
|
|
return error
|
|
|
|
def _update_size_props(self):
|
|
self._update_selectors()
|
|
self._updateSpaceDisplay()
|
|
|
|
def _try_replace_device(self, selector, removed_device, new_device_info,
|
|
old_device_info):
|
|
if removed_device:
|
|
# we don't want to pass the device if we removed it
|
|
new_device_info["device"] = None
|
|
try:
|
|
self._replace_device(selector=selector, **new_device_info)
|
|
return True
|
|
except StorageError as e:
|
|
log.error("factoryDevice failed: %s", e)
|
|
# the factory's error handling has replaced all of the
|
|
# devices with copies, so update the selectors' devices
|
|
# accordingly
|
|
self._update_all_devices_in_selectors()
|
|
self._error = e
|
|
self.set_warning(_(DEVICE_CONFIGURATION_ERROR_MSG))
|
|
self.window.show_all()
|
|
|
|
if not removed_device:
|
|
# nothing more to do
|
|
return True
|
|
else:
|
|
# we have removed the old device so we now have to re-create it
|
|
# the disks need to be updated since we've replaced all
|
|
# of the devices with copies in the devicefactory error
|
|
# handler
|
|
old_disk_names = (d.name for d in old_device_info["disks"])
|
|
old_device_info["disks"] = [self._storage_playground.devicetree.getDeviceByName(n) for n in old_disk_names]
|
|
try:
|
|
self._replace_device(selector=selector, **old_device_info)
|
|
return True
|
|
except StorageError as e:
|
|
# failed to recover.
|
|
self.refresh() # this calls self.clear_errors
|
|
self._error = e
|
|
self.set_warning(_(UNRECOVERABLE_ERROR_MSG))
|
|
self.window.show_all()
|
|
return False
|
|
|
|
@ui_storage_logged
|
|
def _revert_reformat(self, device, use_dev):
|
|
# figure out the existing device and reset it
|
|
if not use_dev.format.exists:
|
|
original_device = use_dev
|
|
else:
|
|
original_device = device
|
|
|
|
log.debug("resetting device %s", original_device.name)
|
|
self._storage_playground.resetDevice(original_device)
|
|
|
|
def _bound_size(self, size, device):
|
|
# If no size was specified, we just want to grow to the maximum.
|
|
# But resizeDevice doesn't take None for a value.
|
|
if not size:
|
|
size = device.maxSize
|
|
elif size < device.minSize:
|
|
size = device.minSize
|
|
elif size > device.maxSize:
|
|
size = device.maxSize
|
|
|
|
return size
|
|
|
|
@ui_storage_logged
|
|
def _handle_size_change(self, size, old_size, device, use_dev):
|
|
# bound size to boundaries given by the device
|
|
size = self._bound_size(size, device)
|
|
size = device.alignTargetSize(size)
|
|
|
|
# And then we need to re-check that the max size is actually
|
|
# different from the current size.
|
|
_changed_size = False
|
|
if size != device.size and size == device.currentSize:
|
|
# size has been set back to its original value
|
|
actions = self._storage_playground.devicetree.findActions(action_type="resize",
|
|
devid=device.id)
|
|
for action in reversed(actions):
|
|
self._storage_playground.devicetree.cancelAction(action)
|
|
_changed_size = True
|
|
elif size != device.size:
|
|
log.debug("scheduling resize of device %s to %s", device.name, size)
|
|
|
|
try:
|
|
self._storage_playground.resizeDevice(device, size)
|
|
except StorageError as e:
|
|
log.error("failed to schedule device resize: %s", e)
|
|
device.size = old_size
|
|
self._error = e
|
|
self.set_warning(_("Device resize request failed. "
|
|
"Click for details."))
|
|
self.window.show_all()
|
|
else:
|
|
_changed_size = True
|
|
|
|
if _changed_size:
|
|
log.debug("new size: %s", device.size)
|
|
log.debug("target size: %s", device.targetSize)
|
|
|
|
# update the selector's size property
|
|
for s in self._accordion.allSelectors:
|
|
if s._device == device:
|
|
s.size = str(device.size)
|
|
|
|
# update size props of all btrfs devices' selectors
|
|
self._update_size_props()
|
|
|
|
@ui_storage_logged
|
|
def _handle_encryption_change(self, encrypted, device, old_device, selector):
|
|
if not encrypted:
|
|
log.info("removing encryption from %s", device.name)
|
|
self._storage_playground.destroyDevice(device)
|
|
self._devices.remove(device)
|
|
old_device = device
|
|
device = device.slave
|
|
selector.device = device
|
|
self._update_device_in_selectors(old_device, device)
|
|
else:
|
|
log.info("applying encryption to %s", device.name)
|
|
old_device = device
|
|
new_fmt = getFormat("luks", device=device.path)
|
|
self._storage_playground.formatDevice(device, new_fmt)
|
|
luks_dev = LUKSDevice("luks-" + device.name,
|
|
parents=[device])
|
|
self._storage_playground.createDevice(luks_dev)
|
|
self._devices.append(luks_dev)
|
|
device = luks_dev
|
|
selector.device = device
|
|
self._update_device_in_selectors(old_device, device)
|
|
|
|
self._devices = self._storage_playground.devices
|
|
|
|
# possibly changed device and old_device, need to return the new ones
|
|
return (device, old_device)
|
|
|
|
@ui_storage_logged
|
|
def _do_reformat(self, device, mountpoint, label, changed_encryption,
|
|
encrypted, selector, fs_type):
|
|
self.clear_errors()
|
|
#
|
|
# ENCRYPTION
|
|
#
|
|
old_device = None
|
|
if changed_encryption:
|
|
device, old_device = self._handle_encryption_change(encrypted,
|
|
device, old_device, selector)
|
|
#
|
|
# FORMATTING
|
|
#
|
|
log.info("scheduling reformat of %s as %s", device.name, fs_type)
|
|
old_format = device.format
|
|
new_format = getFormat(fs_type,
|
|
mountpoint=mountpoint, label=label,
|
|
device=device.path)
|
|
try:
|
|
self._storage_playground.formatDevice(device, new_format)
|
|
except StorageError as e:
|
|
log.error("failed to register device format action: %s", e)
|
|
device.format = old_format
|
|
self._error = e
|
|
self.set_warning(_("Device reformat request failed. "
|
|
"Click for details."))
|
|
self.window.show_all()
|
|
else:
|
|
# first, remove this selector from any old install page(s)
|
|
new_selector = None
|
|
for (page, _selector) in self._accordion.allMembers:
|
|
if _selector.device in (device, old_device):
|
|
if page.pageTitle == translated_new_install_name():
|
|
new_selector = _selector
|
|
continue
|
|
|
|
page.removeSelector(_selector)
|
|
if not page.members:
|
|
log.debug("removing empty page %s", page.pageTitle)
|
|
self._accordion.removePage(page.pageTitle)
|
|
|
|
# either update the existing selector or add a new one
|
|
if new_selector:
|
|
updateSelectorFromDevice(new_selector, device)
|
|
else:
|
|
self.add_new_selector(device)
|
|
|
|
# possibly changed device, need to return the new one
|
|
return device
|
|
|
|
def _save_right_side(self, selector):
|
|
""" Save settings from RHS and apply changes to the device.
|
|
|
|
This method must never trigger a call to self._do_refresh.
|
|
"""
|
|
|
|
# check if initialized and have something to operate on
|
|
if not self._initialized or not selector:
|
|
return
|
|
|
|
# only call _save_right_side if on the right page and some changes need
|
|
# to be saved (sensitivity of the Update Settings button reflects that)
|
|
if self._partitionsNotebook.get_current_page() != NOTEBOOK_DETAILS_PAGE or \
|
|
not self._applyButton.get_sensitive():
|
|
return
|
|
|
|
device = selector.device
|
|
if device not in self._devices:
|
|
# just-removed device
|
|
return
|
|
|
|
self._back_already_clicked = False
|
|
|
|
# dictionaries for many, many pieces of information about the device and
|
|
# requested changes, minimum required entropy for LUKS creation is
|
|
# always the same
|
|
new_device_info = {"min_luks_entropy": crypto.MIN_CREATE_ENTROPY}
|
|
old_device_info = {"min_luks_entropy": crypto.MIN_CREATE_ENTROPY}
|
|
|
|
new_device_info["device"] = device
|
|
use_dev = device.raw_device
|
|
|
|
log.info("ui: saving changes to device %s", device.name)
|
|
|
|
# TODO: member type (as a device type?)
|
|
|
|
# NAME
|
|
old_name = getattr(use_dev, "lvname", use_dev.name)
|
|
name = old_name
|
|
changed_name = False
|
|
if self._nameEntry.get_sensitive():
|
|
name = self._nameEntry.get_text()
|
|
changed_name = (name != old_name)
|
|
else:
|
|
# name entry insensitive means we don't control the name
|
|
name = None
|
|
|
|
old_device_info["name"] = old_name
|
|
new_device_info["name"] = name
|
|
|
|
# SIZE
|
|
old_size = device.size
|
|
|
|
# we are interested in size human readable representation change because
|
|
# that's what the user sees
|
|
same_size = self._device_size_text == self._sizeEntry.get_text()
|
|
if same_size:
|
|
size = old_size
|
|
else:
|
|
size = size_from_entry(self._sizeEntry)
|
|
changed_size = ((use_dev.resizable or not use_dev.exists) and
|
|
not same_size)
|
|
old_device_info["size"] = old_size
|
|
new_device_info["size"] = size
|
|
|
|
# DEVICE TYPE
|
|
device_type = self._get_current_device_type()
|
|
old_device_type = devicefactory.get_device_type(device)
|
|
changed_device_type = (old_device_type != device_type)
|
|
old_device_info["device_type"] = old_device_type
|
|
new_device_info["device_type"] = device_type
|
|
|
|
# REFORMAT
|
|
reformat = self._reformatCheckbox.get_active()
|
|
log.debug("reformat: %s", reformat)
|
|
|
|
# FS TYPE
|
|
old_fs_type = device.format.type
|
|
fs_type_index = self._fsCombo.get_active()
|
|
fs_type_str = self._fsCombo.get_model()[fs_type_index][0]
|
|
new_fs = getFormat(fs_type_str)
|
|
fs_type = new_fs.type
|
|
changed_fs_type = (old_fs_type != fs_type)
|
|
old_device_info["fstype"] = old_fs_type
|
|
new_device_info["fstype"] = fs_type
|
|
|
|
# ENCRYPTION
|
|
old_encrypted = isinstance(device, LUKSDevice)
|
|
encrypted = self._encryptCheckbox.get_active()
|
|
changed_encryption = (old_encrypted != encrypted)
|
|
old_device_info["encrypted"] = old_encrypted
|
|
new_device_info["encrypted"] = encrypted
|
|
|
|
# FS LABEL
|
|
label = self._labelEntry.get_text()
|
|
old_label = getattr(device.format, "label", "")
|
|
changed_label = (label != old_label)
|
|
old_device_info["label"] = old_label
|
|
new_device_info["label"] = label
|
|
if changed_label or changed_fs_type:
|
|
error = validate_label(label, new_fs)
|
|
if error:
|
|
self._error = error
|
|
self.set_warning(self._error)
|
|
self.window.show_all()
|
|
self._populate_right_side(selector)
|
|
return
|
|
|
|
# MOUNTPOINT
|
|
mountpoint = None # None means format type is not mountable
|
|
if self._mountPointEntry.get_sensitive():
|
|
mountpoint = self._mountPointEntry.get_text()
|
|
|
|
old_mountpoint = getattr(device.format, "mountpoint", "") or ""
|
|
old_device_info["mountpoint"] = old_mountpoint
|
|
new_device_info["mountpoint"] = mountpoint
|
|
if mountpoint is not None and (reformat or
|
|
mountpoint != old_mountpoint):
|
|
mountpoints = self._storage_playground.mountpoints.copy()
|
|
if old_mountpoint:
|
|
del mountpoints[old_mountpoint]
|
|
|
|
error = validate_mountpoint(mountpoint, mountpoints.keys())
|
|
if error:
|
|
self._error = error
|
|
self.set_warning(self._error)
|
|
self.window.show_all()
|
|
self._populate_right_side(selector)
|
|
return
|
|
|
|
if not old_mountpoint:
|
|
# prevent false positives below when "" != None
|
|
old_mountpoint = None
|
|
|
|
changed_mountpoint = (old_mountpoint != mountpoint)
|
|
|
|
# RAID LEVEL
|
|
raid_level = selectedRaidLevel(self._raidLevelCombo)
|
|
old_raid_level = get_raid_level(device)
|
|
changed_raid_level = (old_device_type == device_type and
|
|
device_type in (DEVICE_TYPE_MD,
|
|
DEVICE_TYPE_BTRFS) and
|
|
old_raid_level != raid_level)
|
|
old_device_info["raid_level"] = old_raid_level
|
|
new_device_info["raid_level"] = raid_level
|
|
|
|
##
|
|
## VALIDATION
|
|
##
|
|
error = self._validate_mountpoint(mountpoint, device, device_type,
|
|
fs_type_str, reformat, encrypted,
|
|
raid_level)
|
|
if error:
|
|
self.set_warning(error)
|
|
self.window.show_all()
|
|
self._populate_right_side(selector)
|
|
return
|
|
|
|
# If the device is a btrfs volume, the only things we can set/update
|
|
# are mountpoint and container-wide settings.
|
|
if device_type == DEVICE_TYPE_BTRFS and hasattr(use_dev, "subvolumes"):
|
|
size = 0
|
|
changed_size = False
|
|
encrypted = False
|
|
changed_encryption = False
|
|
raid_level = None
|
|
changed_raid_level = False
|
|
|
|
with ui_storage_logger():
|
|
# create a new factory using the appropriate size and type
|
|
factory = devicefactory.get_device_factory(self._storage_playground,
|
|
device_type, size,
|
|
disks=device.disks,
|
|
encrypted=encrypted,
|
|
raid_level=raid_level,
|
|
min_luks_entropy=crypto.MIN_CREATE_ENTROPY)
|
|
|
|
# CONTAINER
|
|
changed_container = False
|
|
old_container_name = None
|
|
container_name = self._device_container_name
|
|
container = factory.get_container()
|
|
old_container_encrypted = False
|
|
old_container_raid_level = None
|
|
old_container = None
|
|
old_container_size = SIZE_POLICY_AUTO
|
|
if not changed_device_type:
|
|
old_container = factory.get_container(device=use_dev)
|
|
if old_container:
|
|
old_container_name = old_container.name
|
|
old_container_encrypted = old_container.encrypted
|
|
old_container_raid_level = get_raid_level(old_container)
|
|
old_container_size = getattr(old_container, "size_policy",
|
|
old_container.size)
|
|
|
|
container = factory.get_container(name=container_name)
|
|
if old_container and container_name != old_container.name:
|
|
changed_container = True
|
|
|
|
old_device_info["container_name"] = old_container_name
|
|
new_device_info["container_name"] = container_name
|
|
|
|
container_encrypted = self._device_container_encrypted
|
|
old_device_info["container_encrypted"] = old_container_encrypted
|
|
new_device_info["container_encrypted"] = container_encrypted
|
|
changed_container_encrypted = (container_encrypted != old_container_encrypted)
|
|
|
|
container_raid_level = self._device_container_raid_level
|
|
if container_raid_level not in containerRaidLevelsSupported(device_type):
|
|
container_raid_level = defaultContainerRaidLevel(device_type)
|
|
|
|
old_device_info["container_raid_level"] = old_container_raid_level
|
|
new_device_info["container_raid_level"] = container_raid_level
|
|
changed_container_raid_level = (old_container_raid_level != container_raid_level)
|
|
|
|
container_size = self._device_container_size
|
|
old_device_info["container_size"] = old_container_size
|
|
new_device_info["container_size"] = container_size
|
|
changed_container_size = (old_container_size != container_size)
|
|
|
|
# DISK SET
|
|
old_disks = device.disks
|
|
if hasattr(device, "req_disks") and not device.exists:
|
|
old_disks = device.req_disks
|
|
|
|
disks = self._device_disks[:]
|
|
if container and changed_device_type:
|
|
log.debug("overriding disk set with container's")
|
|
disks = container.disks[:]
|
|
changed_disk_set = (set(old_disks) != set(disks))
|
|
old_device_info["disks"] = old_disks
|
|
new_device_info["disks"] = disks
|
|
log.debug("old disks: %s", [d.name for d in old_disks])
|
|
log.debug("new disks: %s", [d.name for d in disks])
|
|
|
|
log.debug("device: %s", device)
|
|
already_logged = {"disks", "device"}
|
|
# log the other changes (old_device_info doesn't have the 'device' key)
|
|
for key in (to_log for to_log in
|
|
old_device_info.keys() if to_log not in already_logged):
|
|
log.debug("old %s: %s", key, old_device_info[key])
|
|
log.debug("new %s: %s", key, new_device_info[key])
|
|
|
|
# XXX prevent multiple raid or encryption layers?
|
|
|
|
changed = (changed_name or changed_size or changed_device_type or
|
|
changed_label or changed_mountpoint or changed_disk_set or
|
|
changed_encryption or changed_raid_level or
|
|
changed_fs_type or
|
|
changed_container or changed_container_encrypted or
|
|
changed_container_raid_level or changed_container_size)
|
|
|
|
##
|
|
## Handle changes to an existing device
|
|
##
|
|
if not use_dev.exists:
|
|
if not changed:
|
|
log.debug("nothing changed for new device")
|
|
return
|
|
|
|
self.clear_errors()
|
|
|
|
if changed_device_type or changed_container:
|
|
# remove the current device
|
|
self._destroy_device(device)
|
|
if device in self._devices:
|
|
# the removal failed. don't continue.
|
|
log.error("device removal failed")
|
|
return
|
|
removed_device = True
|
|
else:
|
|
removed_device = False
|
|
|
|
with ui_storage_logger():
|
|
succ = self._try_replace_device(selector, removed_device,
|
|
new_device_info, old_device_info)
|
|
if not succ:
|
|
# failed, nothing more to be done
|
|
return
|
|
|
|
self._update_device_in_selectors(device, selector.device)
|
|
self._devices = self._storage_playground.devices
|
|
|
|
# update size properties and the right side
|
|
self._update_size_props()
|
|
self._populate_right_side(selector)
|
|
|
|
log.debug("leaving save_right_side")
|
|
return
|
|
|
|
##
|
|
## Handle changes to preexisting devices
|
|
##
|
|
|
|
# Handle deactivation of the reformat checkbutton after having committed
|
|
# a reformat.
|
|
if not reformat and (not use_dev.format.exists or
|
|
not device.format.exists):
|
|
self._revert_reformat(device, use_dev)
|
|
|
|
# Handle size change
|
|
if changed_size and device.resizable:
|
|
self._handle_size_change(size, old_size, device, use_dev)
|
|
|
|
# it's possible that reformat is active but fstype is unchanged, in
|
|
# which case we're not going to schedule another reformat unless
|
|
# encryption got toggled
|
|
do_reformat = (reformat and (changed_encryption or
|
|
changed_fs_type or
|
|
device.format.exists))
|
|
|
|
# Handle reformat
|
|
if do_reformat:
|
|
device = self._do_reformat(device, mountpoint, label, changed_encryption,
|
|
encrypted, selector, fs_type)
|
|
else:
|
|
# Set various attributes that do not require actions.
|
|
if old_label != label and hasattr(device.format, "label") and \
|
|
validate_label(label, device.format) == "":
|
|
self.clear_errors()
|
|
log.debug("updating label on %s to %s", device.name, label)
|
|
device.format.label = label
|
|
|
|
if mountpoint and old_mountpoint != mountpoint:
|
|
self.clear_errors()
|
|
log.debug("updating mountpoint of %s to %s", device.name, mountpoint)
|
|
device.format.mountpoint = mountpoint
|
|
if old_mountpoint:
|
|
updateSelectorFromDevice(selector, device)
|
|
else:
|
|
# add an entry to the new page but do not remove any entries
|
|
# from other pages since we haven't altered the filesystem
|
|
self.add_new_selector(device)
|
|
|
|
#
|
|
# NAME
|
|
#
|
|
if changed_name:
|
|
self.clear_errors()
|
|
use_dev._name = name
|
|
new_name = use_dev.name
|
|
log.debug("changing name of %s to %s", old_name, new_name)
|
|
if new_name in self._storage_playground.names:
|
|
use_dev._name = old_name
|
|
self.set_info(_("Specified name %s already in use.") % new_name)
|
|
else:
|
|
updateSelectorFromDevice(selector, device)
|
|
|
|
self._populate_right_side(selector)
|
|
|
|
def _raid_level_visible(self, model, itr, user_data):
|
|
device_type = self._get_current_device_type()
|
|
raid_level = raid.getRaidLevel(model[itr][1])
|
|
return raid_level in raidLevelsSupported(device_type)
|
|
|
|
def _populate_raid(self, raid_level):
|
|
""" Set up the raid-specific portion of the device details.
|
|
|
|
:param raid_level: RAID level
|
|
:type raid_level: instance of blivet.devicelibs.raid.RAIDLevel or None
|
|
"""
|
|
device_type = self._get_current_device_type()
|
|
log.debug("populate_raid: %s, %s", device_type, raid_level)
|
|
|
|
if not raidLevelsSupported(device_type):
|
|
map(really_hide, [self._raidLevelLabel, self._raidLevelCombo])
|
|
return
|
|
|
|
raid_level = raid_level or defaultRaidLevel(device_type)
|
|
raid_level_name = raidLevelSelection(raid_level)
|
|
|
|
# Set a default RAID level in the combo.
|
|
for (i, row) in enumerate(self._raidLevelCombo.get_model()):
|
|
if row[1] == raid_level_name:
|
|
self._raidLevelCombo.set_active(i)
|
|
break
|
|
|
|
map(really_show, [self._raidLevelLabel, self._raidLevelCombo])
|
|
|
|
def _get_current_device_type(self):
|
|
itr = self._typeCombo.get_active_iter()
|
|
if not itr:
|
|
return None
|
|
|
|
# we have the constant name in the second column of the store
|
|
return dev_type_from_const(self._typeStore[itr][1])
|
|
|
|
def _update_container_info(self, use_dev):
|
|
if hasattr(use_dev, "vg"):
|
|
self._device_container_name = use_dev.vg.name
|
|
self._device_container_raid_level = get_raid_level(use_dev.vg)
|
|
self._device_container_encrypted = use_dev.vg.encrypted
|
|
self._device_container_size = use_dev.vg.size_policy
|
|
elif hasattr(use_dev, "volume") or hasattr(use_dev, "subvolumes"):
|
|
volume = getattr(use_dev, "volume", use_dev)
|
|
self._device_container_name = volume.name
|
|
self._device_container_raid_level = get_raid_level(volume)
|
|
self._device_container_encrypted = volume.encrypted
|
|
self._device_container_size = volume.size_policy
|
|
else:
|
|
self._device_container_name = None
|
|
self._device_container_raid_level = None
|
|
self._device_container_encrypted = False
|
|
self._device_container_size = SIZE_POLICY_AUTO
|
|
|
|
self._device_container_raid_level = self._device_container_raid_level \
|
|
or defaultContainerRaidLevel(devicefactory.get_device_type(use_dev))
|
|
|
|
def _setup_fstype_combo(self, device):
|
|
# remove any fs types that aren't supported
|
|
remove_indices = []
|
|
for idx, row in enumerate(self._fsCombo.get_model()):
|
|
fs_type = row[0]
|
|
if fs_type not in self._fs_types:
|
|
remove_indices.append(idx)
|
|
continue
|
|
|
|
if fs_type == device.format.name:
|
|
self._fsCombo.set_active(idx)
|
|
|
|
# remove items from the combobox in reversed order so that item 3
|
|
# doesn't become item 2 by removing item 1 etc.
|
|
map(self._fsCombo.remove, reversed(remove_indices))
|
|
|
|
# if the current device has unsupported formatting, add an entry for it
|
|
if device.format.name not in self._fs_types:
|
|
self._fsCombo.append_text(device.format.name)
|
|
self._fsCombo.set_active(len(self._fsCombo.get_model()) - 1)
|
|
|
|
# Give them a way to reset to original formatting. Whenever we add a
|
|
# "reformat this" widget this will need revisiting.
|
|
if device.exists and \
|
|
device.format.type != device.originalFormat.type and \
|
|
device.originalFormat.type not in self._fs_types:
|
|
self._fsCombo.append_text(device.originalFormat.name)
|
|
|
|
def _setup_device_type_combo(self, device, use_dev, device_name):
|
|
# these device types should always be listed
|
|
should_appear = {"DEVICE_TYPE_PARTITION", "DEVICE_TYPE_LVM", "DEVICE_TYPE_LVM_THINP"}
|
|
|
|
# only include md if there are two or more disks
|
|
if (use_dev.type == "mdarray" or len(self._clearpartDevices) > 1):
|
|
should_appear.add("DEVICE_TYPE_MD")
|
|
|
|
# if the format is swap the device type can't be btrfs
|
|
if use_dev.format.type not in PARTITION_ONLY_FORMAT_TYPES + ("swap",):
|
|
should_appear.add("DEVICE_TYPE_BTRFS")
|
|
|
|
# only include disk if the current device is a disk
|
|
if use_dev.isDisk:
|
|
should_appear.add("DEVICE_TYPE_DISK")
|
|
|
|
# go through the store and remove things that shouldn't be included
|
|
# store.remove() updates or invalidates the passed iterator
|
|
itr = self._typeStore.get_iter_first()
|
|
valid = True
|
|
while itr and valid:
|
|
dev_type_const = self._typeStore[itr][1]
|
|
if dev_type_const not in should_appear:
|
|
valid = self._typeStore.remove(itr)
|
|
elif dev_type_const in should_appear:
|
|
# already seen, shouldn't be added to the list again
|
|
should_appear.remove(dev_type_const)
|
|
itr = self._typeStore.iter_next(itr)
|
|
|
|
# add missing device types
|
|
for dev_type_const in should_appear:
|
|
self._add_device_type(dev_type_const)
|
|
|
|
device_type = devicefactory.get_device_type(device)
|
|
|
|
for _type in self._device_name_dict.iterkeys():
|
|
if _type == device_type:
|
|
self._device_name_dict[_type] = device_name
|
|
continue
|
|
elif _type not in NAMED_DEVICE_TYPES:
|
|
continue
|
|
|
|
is_swap = device.format.type == "swap"
|
|
mountpoint = getattr(device.format, "mountpoint", None)
|
|
|
|
with ui_storage_logger():
|
|
name = self._storage_playground.suggestDeviceName(swap=is_swap,
|
|
mountpoint=mountpoint)
|
|
|
|
self._device_name_dict[_type] = name
|
|
|
|
itr = self._typeStore.get_iter_first()
|
|
while itr:
|
|
if dev_type_from_const(self._typeStore[itr][1]) == device_type:
|
|
self._typeCombo.set_active_iter(itr)
|
|
break
|
|
itr = self._typeStore.iter_next(itr)
|
|
else:
|
|
msg = "Didn't find device type %s in device type combobox" % device_type
|
|
raise KeyError(msg)
|
|
|
|
return device_type
|
|
|
|
def _set_devices_label(self):
|
|
device_disks = self._device_disks
|
|
if not device_disks:
|
|
devices_desc = _("No disks assigned")
|
|
else:
|
|
devices_desc = "%s (%s)" % (device_disks[0].description, device_disks[0].name)
|
|
num_disks = len(device_disks)
|
|
if num_disks > 1:
|
|
devices_desc += CP_("GUI|Custom Partitioning|Devices",
|
|
" and %d other", " and %d others",
|
|
num_disks - 1) % (num_disks -1 )
|
|
self._deviceDescLabel.set_text(devices_desc)
|
|
|
|
def _populate_right_side(self, selector):
|
|
log.debug("populate_right_side: %s", selector.device)
|
|
|
|
device = selector.device
|
|
use_dev = device.raw_device
|
|
|
|
if hasattr(use_dev, "req_disks") and not use_dev.exists:
|
|
self._device_disks = use_dev.req_disks[:]
|
|
else:
|
|
self._device_disks = device.disks[:]
|
|
|
|
self._update_container_info(use_dev)
|
|
|
|
log.debug("updated device_disks to %s", [d.name for d in self._device_disks])
|
|
log.debug("updated device_container_name to %s", self._device_container_name)
|
|
log.debug("updated device_container_raid_level to %s", self._device_container_raid_level)
|
|
log.debug("updated device_container_encrypted to %s", self._device_container_encrypted)
|
|
log.debug("updated device_container_size to %s", self._device_container_size)
|
|
|
|
self._selectedDeviceLabel.set_text(selector.props.name)
|
|
desc = _(MOUNTPOINT_DESCRIPTIONS.get(selector.props.name, ""))
|
|
self._selectedDeviceDescLabel.set_text(desc)
|
|
|
|
self._set_devices_label()
|
|
|
|
device_name = getattr(use_dev, "lvname", use_dev.name)
|
|
self._nameEntry.set_text(device_name)
|
|
|
|
self._mountPointEntry.set_text(getattr(device.format, "mountpoint", "") or "")
|
|
fancy_set_sensitive(self._mountPointEntry, device.format.mountable)
|
|
|
|
if hasattr(device.format, "label"):
|
|
if device.format.label is None:
|
|
device.format.label = ""
|
|
self._labelEntry.set_text(device.format.label)
|
|
else:
|
|
self._labelEntry.set_text("")
|
|
fancy_set_sensitive(self._labelEntry, True)
|
|
|
|
self._device_size_text = device.size.humanReadable(max_places=2)
|
|
self._sizeEntry.set_text(self._device_size_text)
|
|
|
|
self._reformatCheckbox.set_active(not device.format.exists)
|
|
fancy_set_sensitive(self._reformatCheckbox,
|
|
use_dev.exists and not use_dev.formatImmutable)
|
|
|
|
self._encryptCheckbox.set_active(isinstance(device, LUKSDevice))
|
|
self._encryptCheckbox.set_sensitive(self._reformatCheckbox.get_active())
|
|
ancestors = use_dev.ancestors
|
|
ancestors.remove(use_dev)
|
|
if any(a.format.type == "luks" for a in ancestors):
|
|
# The encryption checkbutton should not be sensitive if there is
|
|
# existing encryption below the leaf layer.
|
|
self._encryptCheckbox.set_sensitive(False)
|
|
|
|
# Set up the filesystem type combo.
|
|
self._setup_fstype_combo(device)
|
|
|
|
# Set up the device type combo.
|
|
device_type = self._setup_device_type_combo(device, use_dev, device_name)
|
|
|
|
fancy_set_sensitive(self._fsCombo, self._reformatCheckbox.get_active() and
|
|
device_type != DEVICE_TYPE_BTRFS)
|
|
|
|
# you can't change the type of an existing device
|
|
fancy_set_sensitive(self._typeCombo, not use_dev.exists)
|
|
fancy_set_sensitive(self._raidLevelCombo, not use_dev.exists)
|
|
|
|
# FIXME: device encryption should be mutually exclusive with container
|
|
# encryption
|
|
|
|
# FIXME: device raid should be mutually exclusive with container raid
|
|
|
|
# you can't encrypt a btrfs subvolume -- only the volume/container
|
|
# XXX CHECKME: encryption of thin logical volumes is not supported at this time
|
|
if device_type in [DEVICE_TYPE_BTRFS, DEVICE_TYPE_LVM_THINP]:
|
|
fancy_set_sensitive(self._encryptCheckbox, False)
|
|
|
|
# The size entry is only sensitive for resizable existing devices and
|
|
# new devices that are not btrfs subvolumes.
|
|
# Do this after the device type combo is set since
|
|
# on_device_type_changed doesn't account for device existence.
|
|
fancy_set_sensitive(self._sizeEntry, device.resizable or (not device.exists and device.format.type != "btrfs"))
|
|
|
|
if self._sizeEntry.get_sensitive():
|
|
self._sizeEntry.props.has_tooltip = False
|
|
elif device.format.type == "btrfs":
|
|
self._sizeEntry.set_tooltip_text(_("The space available to this mountpoint can be changed by modifying the volume below."))
|
|
else:
|
|
self._sizeEntry.set_tooltip_text(_("This file system may not be resized."))
|
|
|
|
self._populate_raid(get_raid_level(device))
|
|
self._populate_container(device=use_dev)
|
|
# do this last in case this was set sensitive in on_device_type_changed
|
|
if use_dev.exists or use_dev.type == "btrfs volume":
|
|
fancy_set_sensitive(self._nameEntry, False)
|
|
|
|
###
|
|
### SIGNAL HANDLERS
|
|
###
|
|
|
|
def on_key_pressed(self, widget, event, *args):
|
|
if not event or event and event.type != Gdk.EventType.KEY_RELEASE:
|
|
return
|
|
|
|
if event.keyval in [Gdk.KEY_Delete, Gdk.KEY_minus]:
|
|
# But we only want delete to work if you have focused a MountpointSelector,
|
|
# and not just any random widget. For those, it's likely the user wants
|
|
# to delete a character.
|
|
if isinstance(self.main_window.get_focus(), MountpointSelector):
|
|
self._removeButton.emit("clicked")
|
|
elif event.keyval == Gdk.KEY_plus:
|
|
# And we only want '+' to work if you don't have a text entry focused, since
|
|
# the user might be entering some free-form text that can include a plus.
|
|
if not isinstance(self.main_window.get_focus(), Gtk.Entry):
|
|
self._addButton.emit("clicked")
|
|
|
|
def _do_check(self):
|
|
self.clear_errors()
|
|
StorageChecker.errors = []
|
|
StorageChecker.warnings = []
|
|
|
|
# We can't overwrite the main Storage instance because all the other
|
|
# spokes have references to it that would get invalidated, but we can
|
|
# achieve the same effect by updating/replacing a few key attributes.
|
|
self.storage.devicetree._devices = self._storage_playground.devicetree._devices
|
|
self.storage.devicetree._actions = self._storage_playground.devicetree._actions
|
|
self.storage.devicetree._hidden = self._storage_playground.devicetree._hidden
|
|
self.storage.devicetree.dasd = self._storage_playground.devicetree.dasd
|
|
self.storage.devicetree.names = self._storage_playground.devicetree.names
|
|
self.storage.roots = self._storage_playground.roots
|
|
|
|
# set up bootloader and check the configuration
|
|
try:
|
|
self.storage.setUpBootLoader()
|
|
except BootLoaderError as e:
|
|
log.error("storage configuration failed: %s", e)
|
|
StorageChecker.errors = str(e).split("\n")
|
|
self.data.bootloader.bootDrive = ""
|
|
|
|
StorageChecker.checkStorage(self)
|
|
|
|
if self.errors:
|
|
self.set_warning(_("Error checking storage configuration. Click for details or press Done again to continue."))
|
|
elif self.warnings:
|
|
self.set_warning(_("Warning checking storage configuration. Click for details or press Done again to continue."))
|
|
|
|
# on_info_bar_clicked requires self._error to be set, so set it to the
|
|
# list of all errors and warnings that storage checking found.
|
|
self.window.show_all()
|
|
self._error = "\n".join(self.errors + self.warnings)
|
|
|
|
return self._error == ""
|
|
|
|
def on_back_clicked(self, button):
|
|
# Clear any existing errors
|
|
self.clear_errors()
|
|
|
|
# Save anything from the currently displayed mountpoint.
|
|
self._save_right_side(self._current_selector)
|
|
|
|
# And then display the summary screen. From there, the user will either
|
|
# head back to the hub, or stay on the custom screen.
|
|
self._storage_playground.devicetree.pruneActions()
|
|
self._storage_playground.devicetree.sortActions()
|
|
|
|
|
|
# If back has been clicked on once already and no other changes made on the screen,
|
|
# run the storage check now. This handles displaying any errors in the info bar.
|
|
if not self._back_already_clicked:
|
|
self._back_already_clicked = True
|
|
|
|
# If we hit any errors while saving things above, stop and let the
|
|
# user think about what they have done
|
|
if self._error is not None:
|
|
return
|
|
|
|
new_luks = [d for d in self._storage_playground.devices
|
|
if d.format.type == "luks" and not d.format.exists]
|
|
if new_luks:
|
|
dialog = PassphraseDialog(self.data)
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
|
|
if rc != 1:
|
|
# Cancel. Leave the old passphrase set if there was one.
|
|
return
|
|
|
|
self.passphrase = dialog.passphrase
|
|
|
|
for luks in new_luks:
|
|
if not luks.format.hasKey:
|
|
luks.format.passphrase = self.passphrase
|
|
|
|
if not self._do_check():
|
|
return
|
|
|
|
if len(self._storage_playground.devicetree.findActions()) > 0:
|
|
dialog = ActionSummaryDialog(self.data)
|
|
dialog.refresh(self._storage_playground.devicetree.findActions())
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
|
|
if rc != 1:
|
|
# Cancel. Stay on the custom screen.
|
|
return
|
|
|
|
NormalSpoke.on_back_clicked(self, button)
|
|
|
|
@ui_storage_logged
|
|
def _add_device(self, dev_info):
|
|
factory = devicefactory.get_device_factory(self._storage_playground,
|
|
dev_info["device_type"], dev_info["size"],
|
|
min_luks_entropy=crypto.MIN_CREATE_ENTROPY)
|
|
container = factory.get_container()
|
|
if container:
|
|
# don't override user-initiated changes to a defined container
|
|
dev_info["disks"] = container.disks
|
|
dev_info.update({"container_encrypted": container.encrypted,
|
|
"container_raid_level": get_raid_level(container),
|
|
"container_size": getattr(container, "size_policy",
|
|
container.size)})
|
|
|
|
# The container is already encrypted
|
|
if container.encrypted:
|
|
dev_info["encrypted"] = False
|
|
|
|
device_type = dev_info.pop("device_type")
|
|
try:
|
|
self._storage_playground.factoryDevice(device_type,
|
|
**dev_info)
|
|
except StorageError as e:
|
|
log.error("factoryDevice failed: %s", e)
|
|
log.debug("trying to find an existing container to use")
|
|
container = factory.get_container(allow_existing=True)
|
|
log.debug("found container %s", container)
|
|
if container:
|
|
# don't override user-initiated changes to a defined container
|
|
dev_info["disks"] = container.disks
|
|
dev_info.update({"container_encrypted": container.encrypted,
|
|
"container_raid_level": get_raid_level(container),
|
|
"container_size": getattr(container, "size_policy",
|
|
container.size),
|
|
"container_name": container.name})
|
|
try:
|
|
self._storage_playground.factoryDevice(device_type,
|
|
**dev_info)
|
|
except StorageError as e2:
|
|
log.error("factoryDevice failed w/ old container: %s", e2)
|
|
else:
|
|
type_str = _(DEVICE_TEXT_MAP[device_type])
|
|
self.set_info(_("Added new %(type)s to existing "
|
|
"container %(name)s.")
|
|
% {"type" : type_str, "name" : container.name})
|
|
self.window.show_all()
|
|
e = None
|
|
|
|
# the factory's error handling has replaced all of the devices
|
|
# with copies, so update the selectors' devices accordingly
|
|
self._update_all_devices_in_selectors()
|
|
|
|
if e:
|
|
self._error = e
|
|
self.set_error(_("Failed to add new device. Click for "
|
|
"details."))
|
|
self.window.show_all()
|
|
except OverflowError as e:
|
|
log.error("invalid size set for partition")
|
|
self._error = e
|
|
self.set_error(_("Invalid partition size set. Use a valid integer."))
|
|
self.window.show_all()
|
|
|
|
def on_add_clicked(self, button):
|
|
self._save_right_side(self._current_selector)
|
|
|
|
## initialize and run the AddDialog
|
|
dialog = AddDialog(self.data,
|
|
mountpoints=self._storage_playground.mountpoints.keys())
|
|
dialog.refresh()
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
|
|
if rc != 1:
|
|
# user cancel
|
|
dialog.window.destroy()
|
|
return
|
|
|
|
self._back_already_clicked = False
|
|
|
|
## gather data about the added mountpoint
|
|
# minimum entropy required for LUKS creation is always the same
|
|
# TODO: use instance of a class with properly named attributes
|
|
dev_info = {"min_luks_entropy": crypto.MIN_CREATE_ENTROPY}
|
|
|
|
# create a device of the default type, using any disks, with an
|
|
# appropriate fstype and mountpoint
|
|
dev_info["mountpoint"] = dialog.mountpoint
|
|
log.debug("requested size = %s ; available space = %s", dialog.size, self._free_space)
|
|
|
|
# if no size was entered, request as much of the free space as possible
|
|
if dialog.size is not None and dialog.size.convertTo(spec="mb") < 1:
|
|
dev_info["size"] = None
|
|
else:
|
|
dev_info["size"] = dialog.size
|
|
|
|
dev_info["fstype"] = self.storage.getFSType(dev_info["mountpoint"])
|
|
|
|
# The encryption setting as applied here means "encrypt leaf devices".
|
|
# If you want "encrypt my VG/PVs" you'll have to either use the autopart
|
|
# button or wait until we have a way to control container-level
|
|
# encryption.
|
|
dev_info["encrypted"] = self.data.autopart.encrypted
|
|
|
|
# we're doing nothing here to ensure that bootable requests end up on
|
|
# the boot disk, but the weight from platform should take care of this
|
|
|
|
if lowerASCII(dev_info["mountpoint"]) in ("swap", "biosboot", "prepboot"):
|
|
dev_info["mountpoint"] = None
|
|
|
|
dev_info["device_type"] = device_type_from_autopart(self.data.autopart.type)
|
|
if (dev_info["device_type"] != DEVICE_TYPE_PARTITION and
|
|
((dev_info["mountpoint"] and dev_info["mountpoint"].startswith("/boot")) or
|
|
dev_info["fstype"] in PARTITION_ONLY_FORMAT_TYPES)):
|
|
dev_info["device_type"] = DEVICE_TYPE_PARTITION
|
|
|
|
# we shouldn't create swap on a thinly provisioned volume
|
|
if dev_info["fstype"] == "swap" and dev_info["device_type"] == DEVICE_TYPE_LVM_THINP:
|
|
dev_info["device_type"] = DEVICE_TYPE_LVM
|
|
|
|
# encryption of thinly provisioned volumes isn't supported
|
|
if dev_info["encrypted"] and dev_info["device_type"] == DEVICE_TYPE_LVM_THINP:
|
|
dev_info["encrypted"] = False
|
|
|
|
# some devices should never be encrypted
|
|
if ((dev_info["mountpoint"] and dev_info["mountpoint"].startswith("/boot")) or
|
|
dev_info["fstype"] in PARTITION_ONLY_FORMAT_TYPES):
|
|
dev_info["encrypted"] = False
|
|
|
|
dev_info["disks"] = self._clearpartDevices
|
|
|
|
## clear errors and try to add the mountpoint/device
|
|
self.clear_errors()
|
|
self._add_device(dev_info)
|
|
|
|
## refresh internal state and UI elements
|
|
self._devices = self._storage_playground.devices
|
|
if not self._error:
|
|
self._do_refresh(mountpointToShow=dev_info["mountpoint"] or dev_info["fstype"])
|
|
else:
|
|
self._do_refresh()
|
|
self._updateSpaceDisplay()
|
|
|
|
@ui_storage_logged
|
|
def _remove_empty_parents(self, device):
|
|
# if this device has parents with no other children, remove them too
|
|
for parent in device.parents:
|
|
if parent.kids == 0 and not parent.isDisk:
|
|
self._destroy_device(parent)
|
|
|
|
@ui_storage_logged
|
|
def _destroy_device(self, device):
|
|
self.clear_errors()
|
|
is_logical_partition = getattr(device, "isLogical", False)
|
|
try:
|
|
if device.isDisk:
|
|
self._storage_playground.initializeDisk(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
|
|
self._storage_playground.recursiveRemove(device)
|
|
else:
|
|
self._storage_playground.destroyDevice(device)
|
|
except StorageError as e:
|
|
log.error("failed to schedule device removal: %s", e)
|
|
self._error = e
|
|
self.set_warning(_("Device removal request failed. Click "
|
|
"for details."))
|
|
self.window.show_all()
|
|
else:
|
|
if is_logical_partition:
|
|
self._storage_playground.removeEmptyExtendedPartitions()
|
|
|
|
# If we've just removed the last partition and the disklabel is pre-
|
|
# existing, reinitialize the disk.
|
|
if device.type == "partition" and device.exists and \
|
|
device.disk.format.exists:
|
|
if self._storage_playground.shouldClear(device.disk):
|
|
self._storage_playground.initializeDisk(device.disk)
|
|
|
|
self._devices = self._storage_playground.devices
|
|
|
|
# should this be in DeviceTree._removeDevice?
|
|
container = None
|
|
if hasattr(device, "vg"):
|
|
container = device.vg
|
|
device_type = devicefactory.get_device_type(device)
|
|
elif hasattr(device, "volume"):
|
|
container = device.volume
|
|
device_type = DEVICE_TYPE_BTRFS
|
|
|
|
if not container:
|
|
# no container, just remove empty parents of the device
|
|
self._remove_empty_parents(device)
|
|
return
|
|
|
|
# adjust container to size of remaining devices, if auto-sized
|
|
if container and not container.exists and \
|
|
self._storage_playground.devicetree.getChildren(container) and \
|
|
container.size_policy == SIZE_POLICY_AUTO:
|
|
cont_encrypted = container.encrypted
|
|
cont_raid = get_raid_level(container)
|
|
cont_size = container.size_policy
|
|
cont_name = container.name
|
|
factory = devicefactory.get_device_factory(self._storage_playground,
|
|
device_type, Size(0),
|
|
disks=container.disks,
|
|
container_name=cont_name,
|
|
container_encrypted=cont_encrypted,
|
|
container_raid_level=cont_raid,
|
|
container_size=cont_size,
|
|
min_luks_entropy=crypto.MIN_CREATE_ENTROPY)
|
|
factory.configure()
|
|
|
|
self._remove_empty_parents(device)
|
|
|
|
def _show_mountpoint(self, page=None, mountpoint=None):
|
|
if not self._initialized:
|
|
return
|
|
|
|
# Make sure there's something displayed on the RHS. If a page and
|
|
# mountpoint within that page is given, display that. Otherwise, just
|
|
# default to the first selector available.
|
|
if not page:
|
|
page = self._current_page
|
|
|
|
log.debug("show mountpoint: %s", page.pageTitle)
|
|
if not page.members:
|
|
self._clear_current_selector()
|
|
return
|
|
|
|
if not mountpoint:
|
|
self.on_selector_clicked(page.members[0])
|
|
return
|
|
|
|
for member in page.members:
|
|
if member.get_property("mountpoint").lower() == mountpoint.lower():
|
|
self.on_selector_clicked(member)
|
|
break
|
|
|
|
def on_remove_clicked(self, button):
|
|
# Nothing displayed on the RHS? Nothing to remove.
|
|
if not self._current_selector:
|
|
return
|
|
|
|
page = self._current_page
|
|
selector = self._current_selector
|
|
device = self._current_selector.device
|
|
root_name = None
|
|
if selector.root:
|
|
root_name = selector.root.name
|
|
elif page:
|
|
root_name = page.pageTitle
|
|
|
|
log.debug("removing device '%s' from page %s", device, root_name)
|
|
|
|
if root_name == translated_new_install_name():
|
|
if device.exists:
|
|
# This is an existing device that was added to the new page.
|
|
# All we want to do is revert any changes to the device and
|
|
# it will end up back in whatever old pages it came from.
|
|
with ui_storage_logger():
|
|
self._storage_playground.resetDevice(device)
|
|
|
|
log.debug("updated device: %s", device)
|
|
else:
|
|
# Destroying a non-existing device doesn't require any
|
|
# confirmation.
|
|
self._destroy_device(device)
|
|
else:
|
|
# This is a device that exists on disk and most likely has data
|
|
# on it. Thus, we first need to confirm with the user and then
|
|
# schedule actions to delete the thing.
|
|
dialog = ConfirmDeleteDialog(self.data)
|
|
snapshots = (device.direct and not device.isleaf)
|
|
dialog.refresh(getattr(device.format, "mountpoint", ""),
|
|
device.name, root_name, snapshots=snapshots)
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
|
|
if rc != 1:
|
|
dialog.window.destroy()
|
|
return
|
|
|
|
if dialog.deleteAll:
|
|
for dev in (s._device for s in page.members):
|
|
self._destroy_device(dev)
|
|
else:
|
|
self._destroy_device(device)
|
|
|
|
log.info("ui: removed device %s", device.name)
|
|
|
|
# Now that devices have been removed from the installation root,
|
|
# refreshing the display will have the effect of making them disappear.
|
|
# It's like they never existed.
|
|
self._updateSpaceDisplay()
|
|
self._do_refresh()
|
|
|
|
def on_summary_clicked(self, button):
|
|
dialog = SelectedDisksDialog(self.data)
|
|
dialog.refresh(self._clearpartDevices, self._currentFreeInfo,
|
|
showRemove=False, setBoot=False)
|
|
with self.main_window.enlightbox(dialog.window):
|
|
dialog.run()
|
|
|
|
def on_configure_clicked(self, button):
|
|
selector = self._current_selector
|
|
if not selector:
|
|
return
|
|
|
|
device = selector.device
|
|
if device.exists:
|
|
return
|
|
|
|
if self._get_current_device_type() in CONTAINER_DEVICE_TYPES:
|
|
# disk set management happens through container edit on RHS
|
|
return
|
|
|
|
self.clear_errors()
|
|
|
|
dialog = DisksDialog(self.data,
|
|
disks=self._clearpartDevices,
|
|
free=self._currentFreeInfo,
|
|
selected=self._device_disks)
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
|
|
if rc != 1:
|
|
return
|
|
|
|
disks = dialog.selected
|
|
log.debug("new disks for %s: %s", device.name, [d.name for d in disks])
|
|
if not disks:
|
|
self._error = "No disks selected. Keeping previous disk set."
|
|
self.set_info(self._error)
|
|
self.window.show_all()
|
|
return
|
|
|
|
if set(disks) != self._device_disks:
|
|
self._applyButton.set_sensitive(True)
|
|
|
|
self._device_disks = disks
|
|
self._set_devices_label()
|
|
self._populate_raid(selectedRaidLevel(self._raidLevelCombo))
|
|
|
|
def _container_encryption_change(self, old_encrypted, new_encrypted):
|
|
if not old_encrypted and new_encrypted:
|
|
# container set to be encrypted, we should make sure the leaf device
|
|
# is not encrypted and make the encryption checkbox insensitive
|
|
self._encryptCheckbox.set_active(False)
|
|
fancy_set_sensitive(self._encryptCheckbox, False)
|
|
elif old_encrypted and not new_encrypted:
|
|
fancy_set_sensitive(self._encryptCheckbox, True)
|
|
|
|
def run_container_editor(self, container=None, name=None):
|
|
""" Run container edit dialog and return True if changes were made. """
|
|
size = Size(0)
|
|
size_policy = self._device_container_size
|
|
if container:
|
|
container_name = container.name
|
|
size = container.size
|
|
size_policy = container.size_policy
|
|
elif name:
|
|
container_name = name
|
|
if name != self._device_container_name:
|
|
# creating a new container -- switch to the default
|
|
size_policy = SIZE_POLICY_AUTO
|
|
|
|
dialog = ContainerDialog(self.data,
|
|
device_type=self._get_current_device_type(),
|
|
name=container_name,
|
|
raid_level=self._device_container_raid_level,
|
|
encrypted=self._device_container_encrypted,
|
|
size_policy=size_policy,
|
|
size=size,
|
|
disks=self._clearpartDevices,
|
|
free=self._currentFreeInfo,
|
|
selected=self._device_disks,
|
|
storage=self._storage_playground,
|
|
exists=getattr(container, "exists", False))
|
|
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
dialog.window.destroy()
|
|
|
|
if rc != 1:
|
|
return
|
|
|
|
disks = dialog.selected
|
|
name = dialog.name
|
|
log.debug("new disks for %s: %s", name, [d.name for d in disks])
|
|
if not disks:
|
|
self._error = "No disks selected. Not saving changes."
|
|
self.set_info(self._error)
|
|
self.window.show_all()
|
|
return
|
|
|
|
log.debug("new container name: %s", name)
|
|
if name != container_name and name in self._storage_playground.names:
|
|
self._error = _("Volume Group name %s is already in use. Not "
|
|
"saving changes.") % name
|
|
self.set_info(self._error)
|
|
self.window.show_all()
|
|
return
|
|
|
|
if (set(disks) != set(self._device_disks) or
|
|
name != container_name or
|
|
dialog.raid_level != self._device_container_raid_level or
|
|
dialog.encrypted != self._device_container_encrypted or
|
|
dialog.size_policy != self._device_container_size):
|
|
self._applyButton.set_sensitive(True)
|
|
|
|
log.debug("new container raid level: %s", dialog.raid_level)
|
|
log.debug("new container encrypted: %s", dialog.encrypted)
|
|
log.debug("new container size: %s", dialog.size_policy)
|
|
|
|
if dialog.encrypted:
|
|
self._container_encryption_change(self._device_container_encrypted,
|
|
dialog.encrypted)
|
|
self._device_disks = disks
|
|
self._device_container_name = name
|
|
self._device_container_raid_level = dialog.raid_level
|
|
self._device_container_encrypted = dialog.encrypted
|
|
self._device_container_size = dialog.size_policy
|
|
self._set_devices_label()
|
|
|
|
return True
|
|
|
|
def _container_store_row(self, name, freeSpace=None):
|
|
if freeSpace is not None:
|
|
return [name, _("(%s free)") % freeSpace]
|
|
else:
|
|
return [name, ""]
|
|
|
|
def on_modify_container_clicked(self, button):
|
|
container_name = self._containerStore[self._containerCombo.get_active()][0]
|
|
container = self._storage_playground.devicetree.getDeviceByName(container_name)
|
|
|
|
# pass the name along with any found vg since we could be modifying a
|
|
# vg that hasn't been instantiated yet
|
|
if not self.run_container_editor(container=container, name=container_name):
|
|
return
|
|
|
|
log.debug("%s -> %s", container_name, self._device_container_name)
|
|
if container_name == self._device_container_name:
|
|
return
|
|
|
|
log.debug("renaming container %s to %s", container_name, self._device_container_name)
|
|
if container:
|
|
# btrfs volume name/label does not go in the name list
|
|
if container.name in self._storage_playground.devicetree.names:
|
|
self._storage_playground.devicetree.names.remove(container.name)
|
|
self._storage_playground.devicetree.names.append(self._device_container_name)
|
|
|
|
# until there's a setter for btrfs volume name
|
|
container._name = self._device_container_name
|
|
if container.format.type == "btrfs":
|
|
container.format.label = self._device_container_name
|
|
|
|
container_exists = getattr(container, "exists", False)
|
|
|
|
# TODO: implement and use function for finding item in combobox
|
|
for idx, data in enumerate(self._containerStore):
|
|
# we're looking for the original vg name
|
|
if data[0] == container_name:
|
|
break
|
|
else:
|
|
# no match found, just update selectors and return
|
|
self._update_selectors()
|
|
return
|
|
|
|
c = self._storage_playground.devicetree.getDeviceByName(self._device_container_name)
|
|
freeSpace = getattr(c, "freeSpace", None)
|
|
|
|
# else branch of for loop above ensures idx is defined
|
|
# pylint: disable=undefined-loop-variable
|
|
self._containerStore.insert(idx, self._container_store_row(self._device_container_name, freeSpace))
|
|
self._containerCombo.set_active(idx)
|
|
self._modifyContainerButton.set_sensitive(not container_exists)
|
|
self._containerStore.remove(self._containerStore.get_iter_from_string("%s" % (idx + 1)))
|
|
|
|
self._update_selectors()
|
|
|
|
def on_container_changed(self, combo):
|
|
ndx = combo.get_active()
|
|
if ndx == -1:
|
|
return
|
|
|
|
container_name = self._containerStore[ndx][0]
|
|
|
|
log.debug("new container selection: %s", container_name)
|
|
if container_name is None:
|
|
return
|
|
|
|
if self._device_container_name == container_name:
|
|
return
|
|
|
|
device_type = self._get_current_device_type()
|
|
container_type = get_container_type_name(device_type).lower()
|
|
new_text = _(NEW_CONTAINER_TEXT) % {"container_type": container_type}
|
|
create_new_container = container_name == new_text
|
|
if create_new_container:
|
|
# run the vg editor dialog with a default name and disk set
|
|
hostname = self.data.network.hostname
|
|
name = self._storage_playground.suggestContainerName(hostname=hostname)
|
|
new = self.run_container_editor(name=name)
|
|
for idx, data in enumerate(self._containerStore):
|
|
if new and data[0] == new_text:
|
|
c = self._storage_playground.devicetree.getDeviceByName(self._device_container_name)
|
|
freeSpace = getattr(c, "freeSpace", None)
|
|
row = self._container_store_row(self._device_container_name, freeSpace)
|
|
|
|
self._containerStore.insert(idx, row)
|
|
combo.set_active(idx) # triggers a call to this method
|
|
return
|
|
elif not new and data[0] == self._device_container_name:
|
|
combo.set_active(idx)
|
|
return
|
|
else:
|
|
self._device_container_name = container_name
|
|
|
|
container = self._storage_playground.devicetree.getDeviceByName(self._device_container_name)
|
|
container_exists = getattr(container, "exists", False) # might not be in the tree
|
|
|
|
if container:
|
|
self._device_container_raid_level = get_raid_level(container)
|
|
self._device_container_encrypted = container.encrypted
|
|
self._device_container_size = getattr(container, "size_policy",
|
|
container.size)
|
|
else:
|
|
self._device_container_raid_level = None
|
|
self._device_container_encrypted = self.data.autopart.encrypted
|
|
self._device_container_size = SIZE_POLICY_AUTO
|
|
|
|
self._modifyContainerButton.set_sensitive(not container_exists)
|
|
|
|
def _save_current_selector(self):
|
|
log.debug("current selector: %s", self._current_selector.device)
|
|
self._save_right_side(self._current_selector)
|
|
self._clear_current_selector()
|
|
|
|
def on_selector_clicked(self, selector):
|
|
if not self._initialized or (self._current_selector is selector):
|
|
return
|
|
|
|
# Take care of the previously chosen selector.
|
|
if self._current_selector:
|
|
# unselect the previously chosen selector
|
|
self._current_selector.set_chosen(False)
|
|
self._save_current_selector()
|
|
log.debug("new selector: %s", selector.device)
|
|
|
|
no_edit = False
|
|
if selector.device.format.type == "luks" and \
|
|
selector.device.format.exists:
|
|
self._partitionsNotebook.set_current_page(NOTEBOOK_LUKS_PAGE)
|
|
selectedDeviceLabel = self._encryptedDeviceLabel
|
|
selectedDeviceDescLabel = self._encryptedDeviceDescLabel
|
|
no_edit = True
|
|
elif not getattr(selector.device, "complete", True):
|
|
self._partitionsNotebook.set_current_page(NOTEBOOK_INCOMPLETE_PAGE)
|
|
selectedDeviceLabel = self._incompleteDeviceLabel
|
|
selectedDeviceDescLabel = self._incompleteDeviceDescLabel
|
|
|
|
if selector.device.type == "mdarray":
|
|
total = selector.device.memberDevices
|
|
missing = total - len(selector.device.parents)
|
|
txt = _("This Software RAID array is missing %(missingMembers)d of %(totalMembers)d member "
|
|
"partitions. You can remove it or select a different "
|
|
"device.") % {"missingMembers": missing, "totalMembers": total}
|
|
else:
|
|
total = selector.device.pvCount
|
|
missing = total - len(selector.device.parents)
|
|
txt = _("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}
|
|
self._incompleteDeviceOptionsLabel.set_text(txt)
|
|
no_edit = True
|
|
elif devicefactory.get_device_type(selector.device) is None:
|
|
self._partitionsNotebook.set_current_page(NOTEBOOK_UNEDITABLE_PAGE)
|
|
selectedDeviceLabel = self._uneditableDeviceLabel
|
|
selectedDeviceDescLabel = self._uneditableDeviceDescLabel
|
|
no_edit = True
|
|
|
|
if no_edit:
|
|
selectedDeviceLabel.set_text(selector.device.name)
|
|
desc = _(MOUNTPOINT_DESCRIPTIONS.get(selector.device.type, ""))
|
|
selectedDeviceDescLabel.set_text(desc)
|
|
selector.set_chosen(True)
|
|
self._current_selector = selector
|
|
self._configButton.set_sensitive(False)
|
|
self._removeButton.set_sensitive(True)
|
|
return
|
|
|
|
# Make sure we're showing details instead of the "here's how you create
|
|
# a new OS" label.
|
|
self._partitionsNotebook.set_current_page(NOTEBOOK_DETAILS_PAGE)
|
|
|
|
# Set up the newly chosen selector.
|
|
self._populate_right_side(selector)
|
|
selector.set_chosen(True)
|
|
self._current_selector = selector
|
|
|
|
self._applyButton.set_sensitive(False)
|
|
container_device = devicefactory.get_device_type(selector.device) in CONTAINER_DEVICE_TYPES
|
|
self._configButton.set_sensitive(not selector.device.exists and
|
|
not selector.device.protected and
|
|
not container_device)
|
|
self._removeButton.set_sensitive(not selector.device.protected)
|
|
return True
|
|
|
|
def on_page_clicked(self, page, mountpointToShow=None):
|
|
if not self._initialized:
|
|
return
|
|
|
|
log.debug("page clicked: %s", page.pageTitle)
|
|
if self._current_selector:
|
|
self._save_current_selector()
|
|
|
|
self._show_mountpoint(page=page, mountpoint=mountpointToShow)
|
|
|
|
# This is called when a Page header is clicked upon so we can support
|
|
# deleting an entire installation at once and displaying something
|
|
# on the RHS.
|
|
if isinstance(page, CreateNewPage):
|
|
# Make sure we're showing "here's how you create a new OS" label
|
|
# instead of device/mountpoint details.
|
|
self._partitionsNotebook.set_current_page(NOTEBOOK_LABEL_PAGE)
|
|
self._removeButton.set_sensitive(False)
|
|
else:
|
|
self._removeButton.set_sensitive(True)
|
|
|
|
@ui_storage_logged
|
|
def _do_autopart(self):
|
|
"""Helper function for on_create_clicked.
|
|
Assumes a non-final context in which at least some errors
|
|
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.
|
|
"""
|
|
log.debug("running automatic partitioning")
|
|
self._storage_playground.doAutoPart = True
|
|
self.clear_errors()
|
|
try:
|
|
self._storage_playground.createFreeSpaceSnapshot()
|
|
# doAutoPartitions needs stage1_disk setup so it will reuse existing partitions
|
|
self._storage_playground.setUpBootLoader(early=True)
|
|
|
|
refreshAutoSwapSize(self._storage_playground)
|
|
doAutoPartition(self._storage_playground, self.data,
|
|
min_luks_entropy=crypto.MIN_CREATE_ENTROPY)
|
|
except NoDisksError as e:
|
|
# No handling should be required for this.
|
|
log.error("doAutoPartition failed: %s", e)
|
|
self._error = e
|
|
self.set_error(_("No disks selected."))
|
|
self.window.show_all()
|
|
except NotEnoughFreeSpaceError as e:
|
|
# No handling should be required for this.
|
|
log.error("doAutoPartition failed: %s", e)
|
|
self._error = e
|
|
self.set_error(_("Not enough free space on selected disks."))
|
|
self.window.show_all()
|
|
except (StorageError, BootLoaderError) as e:
|
|
log.error("doAutoPartition failed: %s", e)
|
|
self._reset_storage()
|
|
self._error = e
|
|
self.set_error(_("Automatic partitioning failed. Click "
|
|
"for details."))
|
|
self.window.show_all()
|
|
else:
|
|
self._devices = self._storage_playground.devices
|
|
# mark all new containers for automatic size management
|
|
for device in self._devices:
|
|
if not device.exists and hasattr(device, "size_policy"):
|
|
device.size_policy = SIZE_POLICY_AUTO
|
|
finally:
|
|
self._storage_playground.doAutoPart = False
|
|
log.debug("finished automatic partitioning")
|
|
|
|
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:
|
|
log.error(error.message)
|
|
for warning in warnings:
|
|
log.warning(warning.message)
|
|
|
|
if errors:
|
|
messages = "\n".join(error.message for error in errors)
|
|
log.error("doAutoPartition failed: %s", messages)
|
|
self._reset_storage()
|
|
self._error = messages
|
|
self.set_error(_("Automatic partitioning failed. Click "
|
|
"for details."))
|
|
self.window.show_all()
|
|
|
|
def on_create_clicked(self, button, autopartTypeCombo):
|
|
# Then do autopartitioning. We do not do any clearpart first. This is
|
|
# custom partitioning, so you have to make your own room.
|
|
self._storage_playground.autoPartType = self._get_autopart_type(autopartTypeCombo)
|
|
self._do_autopart()
|
|
|
|
# Refresh the spoke to make the new partitions appear.
|
|
log.debug("refreshing ui")
|
|
self._do_refresh()
|
|
log.debug("finished refreshing ui")
|
|
log.debug("updating space display")
|
|
self._updateSpaceDisplay()
|
|
log.debug("finished updating space display")
|
|
|
|
def on_reformat_toggled(self, widget):
|
|
active = widget.get_active()
|
|
|
|
self._encryptCheckbox.set_sensitive(active)
|
|
if self._current_selector:
|
|
device = self._current_selector.device.raw_device
|
|
|
|
ancestors = device.ancestors
|
|
ancestors.remove(device)
|
|
if any(a.format.type == "luks" and a.format.exists for a in ancestors):
|
|
# The encryption checkbutton should not be sensitive if there is
|
|
# existing encryption below the leaf layer.
|
|
self._encryptCheckbox.set_sensitive(False)
|
|
|
|
# you can't encrypt a btrfs subvolume -- only the volume/container
|
|
device_type = self._get_current_device_type()
|
|
if device_type == DEVICE_TYPE_BTRFS:
|
|
self._encryptCheckbox.set_active(False)
|
|
|
|
self._encryptCheckbox.set_sensitive(device_type != DEVICE_TYPE_BTRFS)
|
|
fancy_set_sensitive(self._fsCombo, active)
|
|
|
|
def on_fs_type_changed(self, combo):
|
|
if not self._initialized:
|
|
return
|
|
|
|
new_type = combo.get_active_text()
|
|
if new_type is None:
|
|
return
|
|
log.debug("fs type changed: %s", new_type)
|
|
fmt = getFormat(new_type)
|
|
fancy_set_sensitive(self._mountPointEntry, fmt.mountable)
|
|
|
|
def _populate_container(self, device=None):
|
|
""" Set up the vg widgets for lvm or hide them for other types. """
|
|
device_type = self._get_current_device_type()
|
|
if device is None:
|
|
if self._current_selector is None:
|
|
return
|
|
|
|
device = self._current_selector.device
|
|
if device:
|
|
device = device.raw_device
|
|
|
|
container_size_policy = SIZE_POLICY_AUTO
|
|
if device_type not in CONTAINER_DEVICE_TYPES:
|
|
# just hide the buttons with no meaning for non-container devices
|
|
map(really_hide, [self._containerLabel, self._containerCombo, self._modifyContainerButton])
|
|
return
|
|
|
|
# else really populate the container
|
|
# set up the vg widgets and then bail out
|
|
if devicefactory.get_device_type(device) == device_type:
|
|
_device = device
|
|
else:
|
|
_device = None
|
|
|
|
with ui_storage_logger():
|
|
factory = devicefactory.get_device_factory(self._storage_playground,
|
|
device_type,
|
|
0, min_luks_entropy=crypto.MIN_CREATE_ENTROPY)
|
|
container = factory.get_container(device=_device)
|
|
default_container_name = getattr(container, "name", None)
|
|
if container:
|
|
container_size_policy = container.size_policy
|
|
|
|
container_type_text = get_container_type_name(device_type)
|
|
self._containerLabel.set_text(container_type_text.title())
|
|
self._containerStore.clear()
|
|
if device_type == DEVICE_TYPE_BTRFS:
|
|
containers = self._storage_playground.btrfsVolumes
|
|
else:
|
|
containers = self._storage_playground.vgs
|
|
|
|
default_seen = False
|
|
for c in containers:
|
|
self._containerStore.append(self._container_store_row(c.name, getattr(c, "freeSpace", None)))
|
|
if default_container_name and c.name == default_container_name:
|
|
default_seen = True
|
|
self._containerCombo.set_active(containers.index(c))
|
|
|
|
if default_container_name is None:
|
|
hostname = self.data.network.hostname
|
|
default_container_name = self._storage_playground.suggestContainerName(hostname=hostname)
|
|
|
|
log.debug("default container is %s", default_container_name)
|
|
self._device_container_name = default_container_name
|
|
self._device_container_size = container_size_policy
|
|
|
|
if not default_seen:
|
|
self._containerStore.append(self._container_store_row(default_container_name))
|
|
self._containerCombo.set_active(len(self._containerStore) - 1)
|
|
|
|
self._containerStore.append(self._container_store_row(_(NEW_CONTAINER_TEXT) % {"container_type": container_type_text.lower()}))
|
|
self._containerCombo.set_tooltip_text(_(CONTAINER_TOOLTIP) % {"container_type": container_type_text.lower()})
|
|
if default_container_name is None:
|
|
self._containerCombo.set_active(len(self._containerStore) - 1)
|
|
|
|
map(really_show, [self._containerLabel, self._containerCombo, self._modifyContainerButton])
|
|
|
|
# make the combo and button insensitive for existing LVs
|
|
can_change_container = (device is not None and not device.exists and
|
|
device != container)
|
|
fancy_set_sensitive(self._containerCombo, can_change_container)
|
|
container_exists = getattr(container, "exists", False)
|
|
self._modifyContainerButton.set_sensitive(not container_exists)
|
|
|
|
def _resolve_btrfs_restrictions(self, should_be_btrfs):
|
|
model = self._fsCombo.get_model()
|
|
btrfs_pos = None
|
|
for idx, data in enumerate(model):
|
|
if data[0] == "btrfs":
|
|
btrfs_pos = idx
|
|
|
|
active_index = self._fsCombo.get_active()
|
|
current_fstype = self._fsCombo.get_active_text()
|
|
if btrfs_pos and not should_be_btrfs:
|
|
self._fsCombo.remove(btrfs_pos)
|
|
if current_fstype == "btrfs":
|
|
for i, row in enumerate(model):
|
|
if row[0] == self.storage.defaultFSType:
|
|
active_index = i
|
|
break
|
|
elif should_be_btrfs and not btrfs_pos:
|
|
self._fsCombo.append_text("btrfs")
|
|
active_index = len(self._fsCombo.get_model()) - 1
|
|
|
|
self._fsCombo.set_active(active_index)
|
|
|
|
def on_device_type_changed(self, combo):
|
|
if not self._initialized:
|
|
return
|
|
|
|
new_type = self._get_current_device_type()
|
|
itr = combo.get_active_iter()
|
|
if not itr:
|
|
return
|
|
|
|
active_dev_type = self._typeStore[itr]
|
|
log.debug("device_type_changed: %s %s", new_type, active_dev_type)
|
|
if new_type is None:
|
|
return
|
|
|
|
# if device type is not btrfs we want to make sure btrfs is not in the
|
|
# fstype combo
|
|
should_be_btrfs = False
|
|
fs_type_sensitive = True
|
|
|
|
raid_level = None
|
|
if new_type == DEVICE_TYPE_BTRFS:
|
|
# add btrfs to the fstype combo and lock it in
|
|
test_fmt = getFormat("btrfs")
|
|
should_be_btrfs = test_fmt.supported and test_fmt.formattable
|
|
fs_type_sensitive = False
|
|
elif new_type == DEVICE_TYPE_MD:
|
|
raid_level = defaultRaidLevel(new_type)
|
|
|
|
# lvm uses the RHS to set disk set. no foolish minds here.
|
|
exists = self._current_selector and self._current_selector.device.exists
|
|
self._configButton.set_sensitive(not exists and new_type not in CONTAINER_DEVICE_TYPES)
|
|
|
|
# this has to be done before calling populate_raid since it will need
|
|
# the raid level combo to contain the relevant raid levels for the new
|
|
# device type
|
|
self._raidStoreFilter.refilter()
|
|
|
|
self._populate_raid(raid_level)
|
|
self._populate_container()
|
|
|
|
fancy_set_sensitive(self._nameEntry, new_type in NAMED_DEVICE_TYPES)
|
|
self._nameEntry.set_text(self._device_name_dict[new_type])
|
|
fancy_set_sensitive(self._sizeEntry, new_type != DEVICE_TYPE_BTRFS)
|
|
|
|
fancy_set_sensitive(self._fsCombo, self._reformatCheckbox.get_active() and
|
|
fs_type_sensitive)
|
|
|
|
self._resolve_btrfs_restrictions(should_be_btrfs)
|
|
|
|
def clear_errors(self):
|
|
self._error = None
|
|
self.clear_info()
|
|
|
|
# This callback is for the button that just resets the UI to anaconda's
|
|
# current understanding of the disk layout.
|
|
def on_reset_clicked(self, *args):
|
|
msg = _("Continuing with this action will reset all your partitioning selections "
|
|
"to their current on-disk state.")
|
|
|
|
dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL,
|
|
message_type=Gtk.MessageType.WARNING,
|
|
buttons=Gtk.ButtonsType.NONE,
|
|
message_format=msg)
|
|
dlg.set_decorated(False)
|
|
dlg.add_buttons(_("_Reset selections"), 0, _("_Preserve current selections"), 1)
|
|
dlg.set_default_response(1)
|
|
|
|
with self.main_window.enlightbox(dlg):
|
|
rc = dlg.run()
|
|
dlg.destroy()
|
|
|
|
if rc == 0:
|
|
self.refresh()
|
|
|
|
# This callback is for the button that has anaconda go back and rescan the
|
|
# disks to pick up whatever changes the user made outside our control.
|
|
def on_refresh_clicked(self, *args):
|
|
dialog = RefreshDialog(self.data, self.storage)
|
|
ignoreEscape(dialog.window)
|
|
with self.main_window.enlightbox(dialog.window):
|
|
rc = dialog.run()
|
|
dialog.window.destroy()
|
|
|
|
if rc == 1:
|
|
# User hit OK on the dialog, indicating they stayed on the dialog
|
|
# until rescanning completed and now needs to go back to the
|
|
# main storage spoke.
|
|
self.skipTo = "StorageSpoke"
|
|
elif rc != 2:
|
|
# User either hit cancel on the dialog or closed it via escape, so
|
|
# there was no rescanning done.
|
|
# NOTE: rc == 2 means the user clicked on the link that takes them
|
|
# back to the hub.
|
|
return
|
|
|
|
# Can't use this spoke's on_back_clicked method as that will try to
|
|
# save the right hand side, which is no longer valid. The user must
|
|
# go back and select their disks all over again since whatever they
|
|
# did on the shell could have changed what disks are available.
|
|
NormalSpoke.on_back_clicked(self, None)
|
|
|
|
def on_info_bar_clicked(self, *args):
|
|
log.debug("info bar clicked: %s (%s)", self._error, args)
|
|
if not self._error:
|
|
return
|
|
|
|
dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL,
|
|
message_type=Gtk.MessageType.ERROR,
|
|
buttons=Gtk.ButtonsType.CLOSE,
|
|
message_format=str(self._error))
|
|
dlg.set_decorated(False)
|
|
|
|
with self.main_window.enlightbox(dlg):
|
|
dlg.run()
|
|
dlg.destroy()
|
|
|
|
@timed_action(delay=50, threshold=100)
|
|
def on_update_settings_clicked(self, button):
|
|
""" call _save_right_side, then, perhaps, populate_right_side. """
|
|
self._save_right_side(self._current_selector)
|
|
self._applyButton.set_sensitive(False)
|
|
|
|
@timed_action(delay=50, threshold=100)
|
|
def on_unlock_clicked(self, *args):
|
|
""" try to open the luks device, populate, then call _do_refresh. """
|
|
self.clear_errors()
|
|
device = self._current_selector.device
|
|
log.info("trying to unlock %s...", device.name)
|
|
passphrase = self._passphraseEntry.get_text()
|
|
device.format.passphrase = passphrase
|
|
try:
|
|
device.setup()
|
|
device.format.setup()
|
|
except StorageError as e:
|
|
log.error("failed to unlock %s: %s", device.name, e)
|
|
device.teardown(recursive=True)
|
|
self._error = e
|
|
device.format.passphrase = None
|
|
self._passphraseEntry.set_text("")
|
|
self.set_warning(_("Failed to unlock encrypted block device. "
|
|
"Click for details"))
|
|
self.window.show_all()
|
|
return
|
|
|
|
log.info("unlocked %s, now going to populate devicetree...", device.name)
|
|
with ui_storage_logger():
|
|
luks_dev = LUKSDevice(device.format.mapName,
|
|
parents=[device],
|
|
exists=True)
|
|
self._storage_playground.devicetree._addDevice(luks_dev)
|
|
# save the passphrase for possible reset and to try for other devs
|
|
self._storage_playground.savePassphrase(device)
|
|
# XXX What if the user has changed things using the shell?
|
|
self._storage_playground.devicetree.populate()
|
|
# look for new roots
|
|
self._storage_playground.roots = findExistingInstallations(self._storage_playground.devicetree)
|
|
|
|
self._devices = self._storage_playground.devices
|
|
self._clear_current_selector()
|
|
self._do_refresh()
|
|
|
|
def on_value_changed(self, *args):
|
|
self._applyButton.set_sensitive(True)
|