qubes-installer-qubes-os/anaconda/pyanaconda/ui/tui/spokes/storage.py
Marek Marczykowski-Górecki 701ced5ddb anaconda: update to 22.20.13-1
Apply diff anaconda-21.48.21-1..anaconda-22.20.13-1
2016-03-22 02:27:17 +13:00

547 lines
22 KiB
Python

# Text storage configuration spoke 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): Jesse Keating <jkeating@redhat.com>
#
# Some of the code here is copied from pyanaconda/ui/gui/spokes/storage.py
# which has the same license and authored by David Lehman <dlehman@redhat.com>
#
from pyanaconda.ui.lib.disks import getDisks, applyDiskSelection
from pyanaconda.ui.categories.system import SystemCategory
from pyanaconda.ui.tui.spokes import NormalTUISpoke
from pyanaconda.ui.tui.simpleline import TextWidget, CheckboxWidget
from pyanaconda.ui.tui.tuiobject import YesNoDialog
from pyanaconda.storage_utils import AUTOPART_CHOICES, sanity_check, SanityError, SanityWarning
from blivet import arch
from blivet.size import Size
from blivet.errors import StorageError, DasdFormatError
from blivet.devices import DASDDevice, FcoeDiskDevice, iScsiDiskDevice, MultipathDevice, ZFCPDiskDevice
from blivet.devicelibs.dasd import format_dasd, make_unformatted_dasd_list
from pyanaconda.flags import flags
from pyanaconda.kickstart import doKickstartStorage, resetCustomStorageData
from pyanaconda.threads import threadMgr, AnacondaThread
from pyanaconda.constants import THREAD_STORAGE, THREAD_STORAGE_WATCHER, THREAD_DASDFMT, DEFAULT_AUTOPART_TYPE
from pyanaconda.constants_text import INPUT_PROCESSED
from pyanaconda.i18n import _, P_, N_
from pyanaconda.bootloader import BootLoaderError
from pykickstart.constants import CLEARPART_TYPE_ALL, CLEARPART_TYPE_LINUX, CLEARPART_TYPE_NONE, AUTOPART_TYPE_LVM
from pykickstart.errors import KickstartValueError
from collections import OrderedDict
import logging
log = logging.getLogger("anaconda")
__all__ = ["StorageSpoke", "AutoPartSpoke"]
CLEARALL = N_("Use All Space")
CLEARLINUX = N_("Replace Existing Linux system(s)")
CLEARNONE = N_("Use Free Space")
PARTTYPES = {CLEARALL: CLEARPART_TYPE_ALL, CLEARLINUX: CLEARPART_TYPE_LINUX,
CLEARNONE: CLEARPART_TYPE_NONE}
class StorageSpoke(NormalTUISpoke):
"""
Storage spoke where users proceed to customize storage features such
as disk selection, partitioning, and fs type.
"""
title = N_("Installation Destination")
category = SystemCategory
def __init__(self, app, data, storage, payload, instclass):
NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
self._ready = False
self.selected_disks = self.data.ignoredisk.onlyuse[:]
self.selection = None
self.autopart = None
self.clearPartType = None
# This list gets set up once in initialize and should not be modified
# except perhaps to add advanced devices. It will remain the full list
# of disks that can be included in the install.
self.disks = []
self.errors = []
self.warnings = []
if self.data.zerombr.zerombr and arch.isS390():
# if zerombr is specified in a ks file and there are unformatted
# dasds, automatically format them. pass in storage.devicetree here
# instead of storage.disks since mediaPresent is checked on disks;
# a dasd needing dasdfmt will fail this media check though
to_format = make_unformatted_dasd_list(d.name for d in getDisks(self.storage.devicetree))
if to_format:
self.run_dasdfmt(to_format)
if not flags.automatedInstall:
# default to using autopart for interactive installs
self.data.autopart.autopart = True
@property
def completed(self):
retval = bool(self.storage.rootDevice and not self.errors)
return retval
@property
def ready(self):
# By default, the storage spoke is not ready. We have to wait until
# storageInitialize is done.
return self._ready and not (threadMgr.get(THREAD_STORAGE_WATCHER) or threadMgr.get(THREAD_DASDFMT))
@property
def mandatory(self):
return True
@property
def showable(self):
return not flags.dirInstall
@property
def status(self):
""" A short string describing the current status of storage setup. """
msg = _("No disks selected")
if flags.automatedInstall and not self.storage.rootDevice:
msg = _("Kickstart insufficient")
elif self.data.ignoredisk.onlyuse:
msg = P_(("%d disk selected"),
("%d disks selected"),
len(self.data.ignoredisk.onlyuse)) % len(self.data.ignoredisk.onlyuse)
if self.errors:
msg = _("Error checking storage configuration")
elif self.warnings:
msg = _("Warning checking storage configuration")
# Maybe show what type of clearpart and which disks selected?
elif self.data.autopart.autopart:
msg = _("Automatic partitioning selected")
else:
msg = _("Custom partitioning selected")
return msg
def _update_disk_list(self, disk):
""" Update self.selected_disks based on the selection."""
name = disk.name
# if the disk isn't already selected, select it.
if name not in self.selected_disks:
self.selected_disks.append(name)
# If the disk is already selected, deselect it.
elif name in self.selected_disks:
self.selected_disks.remove(name)
def _update_summary(self):
""" Update the summary based on the UI. """
count = 0
capacity = 0
free = Size(0)
# pass in our disk list so hidden disks' free space is available
free_space = self.storage.getFreeSpace(disks=self.disks)
selected = [d for d in self.disks if d.name in self.selected_disks]
for disk in selected:
capacity += disk.size
free += free_space[disk.name][0]
count += 1
summary = (P_(("%d disk selected; %s capacity; %s free ..."),
("%d disks selected; %s capacity; %s free ..."),
count) % (count, str(Size(capacity)), free))
if len(self.disks) == 0:
summary = _("No disks detected. Please shut down the computer, connect at least one disk, and restart to complete installation.")
elif count == 0:
summary = (_("No disks selected; please select at least one disk to install to."))
# Append storage errors to the summary
if self.errors:
summary = summary + "\n" + "\n".join(self.errors)
elif self.warnings:
summary = summary + "\n" + "\n".join(self.warnings)
return summary
def refresh(self, args = None):
NormalTUISpoke.refresh(self, args)
# Join the initialization thread to block on it
# This print is foul. Need a better message display
print(_("Probing storage..."))
threadMgr.wait(THREAD_STORAGE_WATCHER)
# synchronize our local data store with the global ksdata
# Commment out because there is no way to select a disk right
# now without putting it in ksdata. Seems wrong?
#self.selected_disks = self.data.ignoredisk.onlyuse[:]
self.autopart = self.data.autopart.autopart
message = self._update_summary()
# loop through the disks and present them.
for disk in self.disks:
disk_info = self._format_disk_info(disk)
c = CheckboxWidget(title="%i) %s" % (self.disks.index(disk) + 1, disk_info),
completed=(disk.name in self.selected_disks))
self._window += [c, ""]
# if we have more than one disk, present an option to just
# select all disks
if len(self.disks) > 1:
c = CheckboxWidget(title="%i) %s" % (len(self.disks) + 1, _("Select all")),
completed=(self.selection == len(self.disks)))
self._window += [c, ""]
self._window += [TextWidget(message), ""]
return True
def _select_all_disks(self):
""" Mark all disks as selected for use in partitioning. """
for disk in self.disks:
if disk.name not in self.selected_disks:
self._update_disk_list(disk)
def _format_disk_info(self, disk):
""" Some specialized disks are difficult to identify in the storage
spoke, so add and return extra identifying information about them.
Since this is going to be ugly to do within the confines of the
CheckboxWidget, pre-format the display string right here.
"""
# show this info for all disks
format_str = "%s: %s (%s)" % (disk.model, disk.size, disk.name)
disk_attrs = []
# now check for/add info about special disks
if (isinstance(disk, MultipathDevice) or isinstance(disk, iScsiDiskDevice) or isinstance(disk, FcoeDiskDevice)):
if hasattr(disk, "wwid"):
disk_attrs.append(disk.wwid)
elif isinstance(disk, DASDDevice):
if hasattr(disk, "busid"):
disk_attrs.append(disk.busid)
elif isinstance (disk, ZFCPDiskDevice):
if hasattr(disk, "fcp_lun"):
disk_attrs.append(disk.fcp_lun)
if hasattr(disk, "wwpn"):
disk_attrs.append(disk.wwpn)
if hasattr(disk, "hba_id"):
disk_attrs.append(disk.hba_id)
# now append all additional attributes to our string
for attr in disk_attrs:
format_str += ", %s" % attr
return format_str
def input(self, args, key):
"""Grab the disk choice and update things"""
try:
keyid = int(key) - 1
self.selection = keyid
if len(self.disks) > 1 and keyid == len(self.disks):
self._select_all_disks()
else:
self._update_disk_list(self.disks[keyid])
return INPUT_PROCESSED
except (ValueError, IndexError):
if key.lower() == "c":
if self.selected_disks:
# check selected disks to see if we have any unformatted DASDs
# if we're on s390x, since they need to be formatted before we
# can use them.
if arch.isS390():
to_format = make_unformatted_dasd_list(self.selected_disks)
if to_format:
self.run_dasdfmt(to_format)
return None
newspoke = AutoPartSpoke(self.app, self.data, self.storage,
self.payload, self.instclass)
self.app.switch_screen_modal(newspoke)
self.apply()
self.execute()
self.close()
return INPUT_PROCESSED
else:
return key
def run_dasdfmt(self, to_format):
"""
This generates the list of DASDs requiring dasdfmt and runs dasdfmt
against them.
"""
# if the storage thread is running, wait on it to complete before taking
# any further actions on devices; most likely to occur if user has
# zerombr in their ks file
threadMgr.wait(THREAD_STORAGE)
# ask user to verify they want to format if zerombr not in ks file
if not self.data.zerombr.zerombr:
# prepare our msg strings; copied directly from dasdfmt.glade
summary = _("The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs cannot be used during installation.\n\n")
warntext = _("Warning: All storage changes made using the installer will be lost when you choose to format.\n\nProceed to run dasdfmt?\n")
displaytext = summary + "\n".join("/dev/" + d for d in to_format) + "\n" + warntext
# now show actual prompt; note -- in cmdline mode, auto-answer for
# this is 'no', so unformatted DASDs will remain so unless zerombr
# is added to the ks file
question_window = YesNoDialog(self._app, displaytext)
self._app.switch_screen_modal(question_window)
if not question_window.answer:
# no? well fine then, back to the storage spoke with you;
return None
for disk in to_format:
try:
print(_("Formatting /dev/%s. This may take a moment.") % disk)
format_dasd(disk)
except DasdFormatError as err:
# Log errors if formatting fails, but don't halt the installer
log.error(str(err))
continue
def apply(self):
self.autopart = self.data.autopart.autopart
self.data.ignoredisk.onlyuse = self.selected_disks[:]
self.data.clearpart.drives = self.selected_disks[:]
if self.data.autopart.type is None:
self.data.autopart.type = AUTOPART_TYPE_LVM
if self.autopart:
self.clearPartType = CLEARPART_TYPE_ALL
else:
self.clearPartType = CLEARPART_TYPE_NONE
for disk in self.disks:
if disk.name not in self.selected_disks and \
disk in self.storage.devices:
self.storage.devicetree.hide(disk)
elif disk.name in self.selected_disks and \
disk not in self.storage.devices:
self.storage.devicetree.unhide(disk)
self.data.bootloader.location = "mbr"
if self.data.bootloader.bootDrive and \
self.data.bootloader.bootDrive not in self.selected_disks:
self.data.bootloader.bootDrive = ""
self.storage.bootloader.reset()
self.storage.config.update(self.data)
# If autopart is selected we want to remove whatever has been
# created/scheduled to make room for autopart.
# If custom is selected, we want to leave alone any storage layout the
# user may have set up before now.
self.storage.config.clearNonExistent = self.data.autopart.autopart
def execute(self):
print(_("Generating updated storage configuration"))
try:
doKickstartStorage(self.storage, self.data, self.instclass)
except (StorageError, KickstartValueError) as e:
log.error("storage configuration failed: %s", e)
print(_("storage configuration failed: %s") % e)
self.errors = [str(e)]
self.data.bootloader.bootDrive = ""
self.data.clearpart.type = CLEARPART_TYPE_ALL
self.data.clearpart.initAll = False
self.storage.config.update(self.data)
self.storage.autoPartType = self.data.autopart.type
self.storage.reset()
# now set ksdata back to the user's specified config
applyDiskSelection(self.storage, self.data, self.selected_disks)
except BootLoaderError as e:
log.error("BootLoader setup failed: %s", e)
print(_("storage configuration failed: %s") % e)
self.errors = [str(e)]
self.data.bootloader.bootDrive = ""
else:
print(_("Checking storage configuration..."))
exns = sanity_check(self.storage)
errors = [exn.message for exn in exns if isinstance(exn, SanityError)]
warnings = [exn.message for exn in exns if isinstance(exn, SanityWarning)]
(self.errors, self.warnings) = (errors, warnings)
for e in self.errors:
log.error(e)
print(e)
for w in self.warnings:
log.warning(w)
print(w)
finally:
resetCustomStorageData(self.data)
self._ready = True
def initialize(self):
NormalTUISpoke.initialize(self)
threadMgr.add(AnacondaThread(name=THREAD_STORAGE_WATCHER,
target=self._initialize))
self.selected_disks = self.data.ignoredisk.onlyuse[:]
# Probably need something here to track which disks are selected?
def _initialize(self):
"""
Secondary initialize so wait for the storage thread to complete before
populating our disk list
"""
threadMgr.wait(THREAD_STORAGE)
self.disks = sorted(getDisks(self.storage.devicetree),
key=lambda d: d.name)
# if only one disk is available, go ahead and mark it as selected
if len(self.disks) == 1:
self._update_disk_list(self.disks[0])
self._update_summary()
self._ready = True
class AutoPartSpoke(NormalTUISpoke):
""" Autopartitioning options are presented here. """
title = N_("Autopartitioning Options")
category = SystemCategory
def __init__(self, app, data, storage, payload, instclass):
NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
self.clearPartType = self.data.clearpart.type
self.parttypelist = sorted(PARTTYPES.keys())
@property
def indirect(self):
return True
def refresh(self, args = None):
NormalTUISpoke.refresh(self, args)
# synchronize our local data store with the global ksdata
self.clearPartType = self.data.clearpart.type
# I dislike "is None", but bool(0) returns false :(
if self.clearPartType is None:
# Default to clearing everything.
self.clearPartType = CLEARPART_TYPE_ALL
for i, parttype in enumerate(self.parttypelist):
c = CheckboxWidget(title="%i) %s" % (i + 1, _(parttype)),
completed=(PARTTYPES[parttype] == self.clearPartType))
self._window += [c, ""]
message = _("Installation requires partitioning of your hard drive. Select what space to use for the install target.")
self._window += [TextWidget(message), ""]
return True
def apply(self):
# kind of a hack, but if we're actually getting to this spoke, there
# is no doubt that we are doing autopartitioning, so set autopart to
# True. In the case of ks installs which may not have defined any
# partition options, autopart was never set to True, causing some
# issues. (rhbz#1001061)
self.data.autopart.autopart = True
self.data.clearpart.type = self.clearPartType
self.data.clearpart.initAll = True
def input(self, args, key):
"""Grab the choice and update things"""
try:
keyid = int(key) - 1
except ValueError:
if key.lower() == "c":
newspoke = PartitionSchemeSpoke(self.app, self.data, self.storage,
self.payload, self.instclass)
self.app.switch_screen_modal(newspoke)
self.apply()
self.close()
return INPUT_PROCESSED
else:
return key
if 0 <= keyid < len(self.parttypelist):
self.clearPartType = PARTTYPES[self.parttypelist[keyid]]
self.apply()
return INPUT_PROCESSED
class PartitionSchemeSpoke(NormalTUISpoke):
""" Spoke to select what partitioning scheme to use on disk(s). """
title = N_("Partition Scheme Options")
category = SystemCategory
def __init__(self, app, data, storage, payload, instclass):
NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
self.partschemes = OrderedDict()
pre_select = self.data.autopart.type or DEFAULT_AUTOPART_TYPE
for i, item in enumerate(AUTOPART_CHOICES):
self.partschemes[item[0]] = item[1]
if item[1] == pre_select:
self._selection = i
@property
def indirect(self):
return True
def refresh(self, args=None):
NormalTUISpoke.refresh(self, args)
schemelist = self.partschemes.keys()
for i, sch in enumerate(schemelist):
box = CheckboxWidget(title="%i) %s" %(i + 1, _(sch)), completed=(i == self._selection))
self._window += [box, ""]
message = _("Select a partition scheme configuration.")
self._window += [TextWidget(message), ""]
return True
def input(self, args, key):
""" Grab the choice and update things. """
try:
keyid = int(key) - 1
except ValueError:
if key.lower() == "c":
self.apply()
self.close()
return INPUT_PROCESSED
else:
return key
if 0 <= keyid < len(self.partschemes):
self._selection = keyid
return INPUT_PROCESSED
def apply(self):
""" Apply our selections. """
schemelist = self.partschemes.values()
self.data.autopart.type = schemelist[self._selection]