# Text storage configuration spoke classes # # Copyright (C) 2012 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 # # Some of the code here is copied from pyanaconda/ui/gui/spokes/storage.py # which has the same license and authored by David Lehman # from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.simpleline import TextWidget, CheckboxWidget from pykickstart.constants import AUTOPART_TYPE_LVM from blivet.size import Size from blivet.errors import StorageError from pyanaconda.flags import flags from pyanaconda.kickstart import doKickstartStorage from pyanaconda.threads import threadMgr, AnacondaThread from pykickstart.constants import * import logging log = logging.getLogger("anaconda") import gettext _ = lambda x: gettext.ldgettext("anaconda", x) P_ = lambda x, y, z: gettext.ldngettext("anaconda", x, y, z) __all__ = ["StorageSpoke", "AutoPartSpoke"] CLEARALL = _("Use All Space") CLEARLINUX = _("Replace Existing Linux system(s)") CLEARNONE = _("Use Free Space") parttypes = {CLEARALL: CLEARPART_TYPE_ALL, CLEARLINUX: CLEARPART_TYPE_LINUX, CLEARNONE: CLEARPART_TYPE_NONE} class FakeDiskLabel(object): def __init__(self, free=0): self.free = free class FakeDisk(object): def __init__(self, name, size=0, free=0, partitioned=True, vendor=None, model=None, serial=None, removable=False): self.name = name self.size = size self.format = FakeDiskLabel(free=free) self.partitioned = partitioned self.vendor = vendor self.model = model self.serial = serial self.removable = removable @property def description(self): return "%s %s" % (self.vendor, self.model) def getDisks(devicetree, fake=False): if not fake: disks = [d for d in devicetree.devices if d.isDisk and not d.format.hidden and not (d.protected and d.removable)] else: disks = [] disks.append(FakeDisk("sda", size=300000, free=10000, serial="00001", vendor="Seagate", model="Monster")) disks.append(FakeDisk("sdb", size=300000, free=300000, serial="00002", vendor="Seagate", model="Monster")) disks.append(FakeDisk("sdc", size=8000, free=2100, removable=True, vendor="SanDisk", model="Cruzer", serial="00003")) return disks class StorageSpoke(NormalTUISpoke): title = _("Install Destination") category = "destination" def __init__(self, app, data, storage, payload, instclass): NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) self.selected_disks = self.data.ignoredisk.onlyuse[:] self._ready = False # 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 = [] @property def completed(self): return bool(self.storage.rootDevice and not self.errors) @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("AnaStorageWatcher") @property def mandatory(self): return True @property def status(self): """ A short string describing the current status of storage setup. """ msg = _("No disks selected") if 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") # 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(bytes=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(spec="%s MB" % 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) 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("AnaStorageWatcher") # 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: c = CheckboxWidget(title="%i) %s" % (self.disks.index(disk) + 1, disk.name), completed=(disk.name in self.selected_disks)) self._window += [c, ""] self._window += [TextWidget(message), ""] return True def input(self, args, key): """Grab the disk choice and update things""" if key == "c": if self.selected_disks: 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 None try: number = int(key) self._update_disk_list(self.disks[number -1]) return None except (ValueError, KeyError, IndexError): return key def apply(self): if not flags.automatedInstall: # default to using autopart for interactive installs self.data.autopart.autopart = True self.autopart = self.data.autopart.autopart self.data.ignoredisk.onlyuse = self.selected_disks[:] self.data.clearpart.drives = self.selected_disks[:] 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" 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 as e: log.error("storage configuration failed: %s" % e) print _("storage configuration failed: %s") % e self.errors = [str(e)] self.data.clearpart.type = CLEARPART_TYPE_ALL self.data.clearpart.initAll = False self.storage.config.update(self.data) self.storage.autoPartType = self.data.clearpart.type self.storage.reset() self._ready = True else: print(_("Checking storage configuration...")) (self.errors, warnings) = self.storage.sanityCheck() self._ready = True for e in self.errors: log.error(e) print e for w in warnings: log.warn(w) print w def initialize(self): NormalTUISpoke.initialize(self) threadMgr.add(AnacondaThread(name="AnaStorageWatcher", 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("AnaStorageThread") self.disks = sorted(getDisks(self.storage.devicetree), key=lambda d: d.name) self._update_summary() self._ready = True class AutoPartSpoke(NormalTUISpoke): title = _("Autopartitioning Options") category = "destination" def __init__(self, app, data, storage, payload, instclass): NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) self.clearPartType = self.data.clearpart.type @property def indirect(self): return True @property def completed(self): return True # We're always complete 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 self.parttypelist = sorted(parttypes.keys()) for parttype in self.parttypelist: c = CheckboxWidget(title="%i) %s" % (self.parttypelist.index(parttype) + 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): self.data.clearpart.type = self.clearPartType self.data.clearpart.initAll = True def input(self, args, key): """Grab the choice and update things""" if key == "c": self.apply() self.close() return False try: number = int(key) self.clearPartType = parttypes[self.parttypelist[number -1]] self.apply() self.close() return False except (ValueError, KeyError, IndexError): return key